Skip to content

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:

FieldSourceUsed By
user_idtoken.user_idTrace metadata, memory personalization
project_idSDK identity / execution metadataRegistration, BiDi routing, graph registry
allowed_tenantsFull tuple on token + manifestTool 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:

LayerCheckFails if…
A: Tenant scopeToken covers all bundle allowed_tenantsAny manifest tenant missing from token
B: Permissionauthorize(..., registration_project_id=project_id)Registration denied for project
C: Registration identityBundle project_id matches token project and registration permission targetToken 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 as tool:{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() call
  • admin:all bypasses 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():

  1. Capability Stripping — attenuates ContextToken to minimum required scope
  2. 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
  3. Provenance Injection — records node:{name}, shield:secrets:read:{path}, and prompt:{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:execute implicitly inherits shield:check via contextunity.core.permissions.inheritance
  • Open Source: fails-open when CU_SHIELD_GRPC_URL is unset

Stream Authentication

Bidirectional streams (tool executor pattern) authenticate via:

  1. Shield-verified auth tokens (production)
  2. Ready payload binding — Router checks ready.project_id against token project, tenant scope, stream:executor permission, and ready.tools ⊆ registered project tools
  3. 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:

  1. ContextToken scopes verified by gRPC auth / Shield / local HMAC auth.
  2. Manifest-registered graph and tool bindings.
  3. Secure node/tool checks that fail closed when the token lacks the required scope.

Permission Profiles

ProfilePermissions
rag_readonlygraph:rag, brain:read, memory:read
rag_full+ memory:write, trace:write
adminadmin: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.