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 |
tenant_id | token.allowed_tenants[0] | Tenant isolation, cache keys, Brain queries |
ExecuteDispatcherPayload and ExecuteAgentPayload contain only execution context — messages, agent_id, config, platform. No user_id, tenant_id, or permissions.
# Router resolves tenant from token, never from payloaddef _resolve_tenant_id(token) -> str: if token and token.allowed_tenants: return token.allowed_tenants[0] return "default"Three-Layer Registration Security
When a project calls RegisterTools, three checks run sequentially:
| Layer | Check | Fails if… |
|---|---|---|
| A: Tenant binding | project_id ∈ token.allowed_tenants | Token doesn’t include this project |
| B: Permission | has_registration_access(perms, project_id) | Missing tools:register:{id} permission |
| C: Ownership | verify_project_owner(project_id, tenant) | Project registered by different owner |
DeregisterTools also enforces Layers A+B and cleans up stored stream secrets.
SecureTool Enforcement
Every tool is wrapped in SecureTool at registration time:
required_permission: auto-generated astool:{name}(e.g.,tool:execute_acme_sql)bound_tenant: tool only executes for its owning project_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 Integrity Verification — verifies prompt text against HMAC signature. Mismatch raises
TamperDetectedError→grpc.StatusCode.ABORTED - Provenance Injection — records
node:{name},shield:secrets:read:{path}, andprompt:{llm_name}:{version}into execution provenance
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)
- Per-registration one-time
stream_secret— consumed on first connect; reconnect requires re-registration - Fail-closed rejection (no valid credentials)
Agent-Aware Token Minting
Router mints ContextToken per request using:
- Default permissions from
SecurityPoliciesConfig - Agent max permissions from contextunity.view (
AgentConfigCache, TTL 5 min) - Effective = intersection of both
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.