Security & Token Architecture
ContextRouter enforces multi-layer security for all inter-service communication. Identity is never carried in payloads — ContextToken is the exclusive source of truth.
Token as Single Point of Truth (SPOT)
Identity fields are never in request payloads. ContextToken is the sole source:
| Field | Source | Used By |
|---|---|---|
user_id | token.user_id | Trace metadata, memory personalization |
project_id | SDK identity / execution metadata | Registration, BiDi routing, graph registry |
allowed_tenants | Full tuple on token + manifest | Tool scope, Shield paths, registration auth |
Runtime execution may still pick a primary tenant for cache keys (legacy paths); that is not project_id and must not collapse a multi-tenant project to [0] at registration. See Project identity & registration.
ExecuteDispatcherPayload and ExecuteAgentPayload contain only execution context — messages, agent_id, config, platform. No user_id, tenant_id, or permissions.
Three-Layer Registration Security
When a project calls RegisterManifest, checks run sequentially:
| Layer | Check | Fails if… |
|---|---|---|
| A: Tenant scope | Token covers all bundle allowed_tenants | Any manifest tenant missing from token |
| B: Permission | authorize(..., registration_project_id=project_id) | Registration denied for project |
| C: Registration identity | Bundle project_id matches token project and registration permission target | Token tries to register another project |
Registration bundle SSOT: router:registrations:{project_id} (+ :hash).
Re-registering a bundle for the same project replaces its in-memory graph/tool state and persisted registration state.
SecureTool Enforcement
Every tool is wrapped in SecureTool at registration time:
required_permission: auto-generated astool:{name}(e.g.,tool:execute_acme_sql)bound_allowed_tenants: tool only executes when token tenants intersect this set (full project scope at registration)_enforce_permission()runs on every_run()/_arun()calladmin:allbypasses tenant isolation (dashboard only)- Fail-closed: no token →
PermissionError
SecureNode Execution Wrapper
Every LLM and tool node in a registered graph is wrapped via make_secure_node():
- Capability Stripping — attenuates
ContextTokento minimum required scope - Prompt Source & Integrity — OSS prompts are HMAC-verified locally; Shield-backed prompts are fetched from Shield with the attenuated node token and checked against
prompt_version - Provenance Injection — records
node:{name},shield:secrets:read:{path}, andprompt:{llm_name}:{version}into execution provenance
Shield-Backed Prompts
When services.shield.enabled: true, the SDK publishes canonical node prompts to Shield at {project_id}/prompts/{node_name} for every allowed_tenants entry, then strips resolved prompt text from the Router registration bundle. Router execution fetches the prompt from Shield using the active request tenant when it is inside the node’s effective tenant scope, otherwise the first resolved effective tenant, and an attenuated token with agent_id=node:{project_id}:{node_name}.
Shield permits prompt reads only when the caller has coarse shield:secrets:read, the token tenant scope covers the requested tenant, the verified token project owns the prompt path, and the agent id matches the owning node. Prompt reads do not require prompt-specific shield:secrets:read:{project}/prompts/{node} scopes. API-key reads remain path-specific.
Shield Pre-LLM Guard
Before executing any LangGraph, user input is sent to Shield via gRPC Scan call:
- Scans for prompt injection, jailbreaks, and PII
- Uses SPOT authentication: end-user’s token is propagated to Shield (not a generic service token)
- Permission inheritance:
router:executeimplicitly inheritsshield:checkviacontextunity.core.permissions.inheritance - Open Source: fails-open when
CU_SHIELD_GRPC_URLis unset
Stream Authentication
Bidirectional streams (tool executor pattern) authenticate via:
- Shield-verified auth tokens (production)
- Ready payload binding — Router checks
ready.project_idagainst token project, tenant scope,stream:executorpermission, andready.tools ⊆ registered project tools - Fail-closed rejection (no valid token/scope/permission/tool binding)
Agent-Aware Token Minting
Router executes with a verified caller/project ContextToken and attenuates it for graph execution provenance. It does not augment permissions from a dashboard-side agent cache.
Effective access comes from:
- ContextToken scopes verified by gRPC auth / Shield / local HMAC auth.
- Manifest-registered graph and tool bindings.
- Secure node/tool checks that fail closed when the token lacks the required scope.
Permission Profiles
| Profile | Permissions |
|---|---|
rag_readonly | graph:rag, brain:read, memory:read |
rag_full | + memory:write, trace:write |
admin | admin:all (superadmin) |
Graph permissions are manifest-driven: each project gets graph:{template} matching its manifest — never graph:*.
Graph Access Control
RAG graphs check has_graph_access(token.permissions, "rag") before invocation. Graph-level denial raises PermissionError before execution.