Cross-Platform Agent Identity
This guide covers the practical steps for deploying one logical agent across multiple platforms and ensuring it resolves to a single identity in the Kanoniv graph.
Architecture overview
A cross-platform agent deployment has three layers:
┌──────────────────────────┐
│ Kanoniv Identity Graph │
│ │
│ Canonical Agent Entity │
│ agt_001: support-agent │
└──────────┬───────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌────────▼────────┐ ┌──────▼──────────┐ ┌─────▼───────────┐
│ Web Instance │ │ Slack Instance │ │ Telegram Instance│
│ │ │ │ │ │
│ DID: a1b2c3... │ │ DID: a1b2c3... │ │ DID: a1b2c3... │
│ instance: web-1 │ │ instance: slk-1│ │ instance: tg-1 │
│ API key: kn_web │ │ API key: kn_slk│ │ API key: kn_tg │
└─────────────────┘ └────────────────┘ └──────────────────┘Each instance has its own API key and instance ID, but they share a DID (cryptographic path) or resolve to the same entity via matching signals (probabilistic path). The canonical agent entity in the graph is the single source of truth.
Option 1: Shared keypair (recommended)
The simplest approach. Generate one keypair and distribute it to all instances.
Step 1: Generate the keypair
# Generate once, store securely
npx @kanoniv/agent-auth generate-keypair > support-agent-keypair.jsonOr programmatically:
from kanoniv_agent_auth import AgentKeyPair
import json
keypair = AgentKeyPair.generate()
secret = {
"secret_key_hex": keypair.secret_key_hex(),
"did": keypair.identity().did
}
# Store in your secrets manager
print(json.dumps(secret))Step 2: Store in your secrets manager
# AWS Secrets Manager
aws secretsmanager create-secret \
--name kanoniv/support-agent/keypair \
--secret-string "$(cat support-agent-keypair.json)"
# Or environment variable in your orchestrator
kubectl create secret generic support-agent-key \
--from-file=keypair.json=support-agent-keypair.jsonStep 3: Deploy across platforms
Each deployment reads the same secret:
# Web deployment
KANONIV_AGENT_NAME=support-agent \
KANONIV_INSTANCE_ID=web-1 \
KANONIV_AGENT_KEY_PATH=/secrets/support-agent-keypair.json \
KANONIV_API_KEY=kn_live_web_... \
npx @kanoniv/mcp
# Slack deployment
KANONIV_AGENT_NAME=support-agent \
KANONIV_INSTANCE_ID=slack-1 \
KANONIV_AGENT_KEY_PATH=/secrets/support-agent-keypair.json \
KANONIV_API_KEY=kn_live_slack_... \
npx @kanoniv/mcp
# Telegram deployment
KANONIV_AGENT_NAME=support-agent \
KANONIV_INSTANCE_ID=telegram-1 \
KANONIV_AGENT_KEY_PATH=/secrets/support-agent-keypair.json \
KANONIV_API_KEY=kn_live_tg_... \
npx @kanoniv/mcpAll three instances produce the same DID. The agent registry tracks them as separate instances of the same agent. The identity graph links them to one canonical entity. Memory is shared from the first request.
Step 4: Verify resolution
# List all instances of the agent
curl "https://api.kanoniv.com/v1/agent-registry/?name=support-agent" \
-H "X-API-Key: kn_live_..."Response:
[
{
"name": "support-agent",
"instance_id": "web-1",
"status": "online",
"last_seen_at": "2026-03-15T14:30:00Z"
},
{
"name": "support-agent",
"instance_id": "slack-1",
"status": "online",
"last_seen_at": "2026-03-15T14:28:00Z"
},
{
"name": "support-agent",
"instance_id": "telegram-1",
"status": "idle",
"last_seen_at": "2026-03-15T14:10:00Z"
}
]Three instances, one agent. All share the same canonical entity and memory.
Option 2: Delegation chains from the same root
When you cannot share keypairs - different security domains, different teams, compliance requirements - use delegation chains from a common root authority. The shared root DID becomes the strongest matching signal for probabilistic resolution.
Step 1: Create the root authority
The root is typically a human operator or a platform-level identity:
from kanoniv_agent_auth import AgentKeyPair
root = AgentKeyPair.generate()
print(f"Root DID: {root.identity().did}")
# did:agent:root_9f8e7d6c5b4a3f2e1d0c9b8aStep 2: Generate per-instance keypairs
web_agent = AgentKeyPair.generate()
slack_agent = AgentKeyPair.generate()
telegram_agent = AgentKeyPair.generate()Step 3: Delegate from root to each instance
from kanoniv_agent_auth import Delegation, Caveat
import json
# Same scopes for all instances
scopes = ["resolve", "search", "memorize", "create_task"]
web_delegation = Delegation.create_root(
root, web_agent.identity().did,
json.dumps([
{"type": "action_scope", "value": scopes},
{"type": "expires_at", "value": "2026-06-01T00:00:00.000Z"}
])
)
slack_delegation = Delegation.create_root(
root, slack_agent.identity().did,
json.dumps([
{"type": "action_scope", "value": scopes},
{"type": "expires_at", "value": "2026-06-01T00:00:00.000Z"}
])
)
telegram_delegation = Delegation.create_root(
root, telegram_agent.identity().did,
json.dumps([
{"type": "action_scope", "value": scopes},
{"type": "expires_at", "value": "2026-06-01T00:00:00.000Z"}
])
)Step 4: Register each instance with root authority metadata
# Web instance registration
POST /v1/agent-registry/
{
"name": "support-agent-web",
"description": "Customer support - web channel",
"capabilities": ["resolve", "memorize", "search"],
"metadata": {
"root_authority_did": "did:agent:root_9f8e7d6c5b4a3f2e1d0c9b8a",
"platform": "web",
"delegation_id": "del_web_..."
}
}
# Slack instance registration
POST /v1/agent-registry/
{
"name": "support-bot-slack",
"description": "Customer support - Slack channel",
"capabilities": ["resolve", "memorize", "search", "create_task"],
"metadata": {
"root_authority_did": "did:agent:root_9f8e7d6c5b4a3f2e1d0c9b8a",
"platform": "slack",
"delegation_id": "del_slack_..."
}
}The resolution engine picks up root_authority_did as a matching signal. Combined with similar names and overlapping capabilities, the agents resolve to the same entity.
Step 5: Configure the resolution spec
Use the agent spec from Probabilistic Resolution with root_authority_did as the strongest signal:
rules:
- name: root_authority_exact
type: exact
field: root_authority_did
weight: 0.5
- name: name_fuzzy
type: similarity
field: agent_name
algorithm: jaro_winkler
threshold: 0.75
weight: 0.3
- name: capabilities_overlap
type: similarity
field: capabilities
algorithm: cosine
threshold: 0.65
weight: 0.2
decision:
thresholds:
match: 0.80
review: 0.60Integration patterns
Pattern 1: Slack bot + web widget + API client
The most common multi-channel deployment. One agent handles customer interactions across three surfaces.
Customer asks on website --> Web widget (support-agent, web-1)
|
v resolves customer entity
Stores: "Customer asked about pricing"
|
Customer follows up on Slack --> Slack bot (support-agent, slack-1)
|
v resolves same customer entity
Retrieves: "Customer asked about pricing"
Stores: "Sent pricing PDF via Slack"
|
API call from internal tool --> API client (support-agent, api-1)
|
v resolves same customer entity
Retrieves: both prior memories
Stores: "Created Salesforce opportunity"Each interaction adds to the shared memory. The agent knows the full context regardless of which channel the customer uses next.
Pattern 2: Regional instances with unified identity
Deploy the same agent in multiple regions for latency, but maintain a single identity:
# US East
KANONIV_AGENT_NAME=data-agent \
KANONIV_INSTANCE_ID=us-east-1 \
KANONIV_API_KEY=kn_live_use1_... \
npx @kanoniv/mcp
# EU West
KANONIV_AGENT_NAME=data-agent \
KANONIV_INSTANCE_ID=eu-west-1 \
KANONIV_API_KEY=kn_live_euw1_... \
npx @kanoniv/mcpBoth instances share the keypair and resolve to the same entity. Memory is unified. A customer resolved in US East is recognized by the EU West instance on the next request.
Pattern 3: Specialized sub-agents with shared root
A manager agent delegates to specialized sub-agents, each handling a different channel but sharing a root authority:
Manager Agent (root)
did:agent:manager_...
|
|-- delegates to --> SDR Agent (web)
| did:agent:sdr_web_...
| scopes: [resolve, memorize, search]
|
|-- delegates to --> SDR Agent (email)
| did:agent:sdr_email_...
| scopes: [resolve, memorize]
|
|-- delegates to --> SDR Agent (LinkedIn)
did:agent:sdr_li_...
scopes: [resolve, memorize, search]All three SDR agents share the same root authority DID. The resolution engine matches them based on this signal plus name similarity ("SDR Agent" prefix) and capability overlap.
The registry as the authoritative source
The agent registry is the ground truth for agent instances within a tenant. It tracks:
- Which agents exist - auto-registered on first API contact
- Which instances are running - online/idle/offline status per instance
- What each agent can do - capabilities list
- Who delegated to whom - root authority metadata
After resolution, the registry still shows individual instances (you need operational visibility into which pods are running), but the identity graph consolidates them into a single canonical entity. Query the registry for operational status. Query the identity graph for unified identity, memory, and reputation.
# Operational view: which instances are running?
GET /v1/agent-registry/?name=support-agent
# Identity view: who is this agent?
GET /v1/canonical/agt_001/linked
# Memory view: what does this agent know?
GET /v1/memory/by-entity/agt_001Troubleshooting
Agents not resolving to the same entity
Check the matching signals:
- Shared keypair? Verify both instances produce the same DID. If the DIDs match, cryptographic resolution should be instant.
- Root authority? If using delegation chains, verify both instances include
root_authority_didin their metadata. - Spec configured? Ensure the agent identity spec is active for your tenant. Check that thresholds are not too high for the available signal strength.
- Blocking keys? Agents must share at least one blocking key value to be compared. If names are very different and there is no shared root authority, they may never enter the same candidate block.
Merged agent has wrong canonical name
Survivorship controls which instance's values win. Adjust source_order in the spec to prioritize the preferred source:
survivorship:
strategy: source_priority
source_order: [web-registry, slack-registry, telegram-registry]Memory not appearing after merge
Memory retrieval requires the include_memory: true flag on resolve calls. Without it, the response includes entity data but not memory entries. Also verify the memory entries are in active status - archived or superseded entries are filtered by default.
