SAM Policy & Authorization Reference
SAM uses a decentralized authorization model powered by Biscuit.
The sam-hub authenticates users via OIDC and injects Facts into their token based on policies.yaml. The sam-node operates offline, evaluating the token against baseline rules and optional local attenuation policies.
1. OIDC to Biscuit Translation
The Hub automatically translates OIDC claims into undeniable cryptographic facts:
| OIDC Claim / Data | Biscuit Fact | Description |
|---|---|---|
sub | user("<sub-id>") | The unique subject ID from the identity provider. |
email | email("<email>") | The user’s email address (if present). |
groups | group("<group-name>") | One fact is injected for each group the user possesses. |
roles / Resolved Roles | role("<role-name>") | One fact is injected for each role mapped or direct role. |
| Peer ID | node("<peer-id>"), client_peer_id("<peer-id>") | Binds the token to the specific agent’s libp2p cryptographic identity. |
| Expiration | expiration(<date>) | The token expiration date based on the OIDC session. |
1.1 Translating Identity to Capability: OIDC to Biscuit
The core innovation of the SAM Network’s security model is translating standard web identity (OIDC JSON Web Tokens defined in OpenID Connect Core 1.0) into decentralized capability tokens (Biscuits). This translation happens securely at the sam-hub during the authentication phase.
The Datalog authority facts generated by the Hub are constructed using the Biscuit Symbol Table specification, ensuring compact serialization and cryptographically undeniable credentials.
Here is exactly how an OIDC JWT is transformed into a policy-ready Biscuit:
1. Claim Extraction
When an agent or user submits a valid OIDC JWT, the sam-hub verifies the token’s signature against the Identity Provider. Once validated, the Hub extracts the payload claims—typically attributes like sub (subject), email, and custom arrays like groups or roles.
2. Datalog Fact Generation
Biscuit policies are written in Datalog, a declarative logic language. The sam-hub acts as a translator, mapping the JSON claims from the OIDC token into immutable Datalog facts.
For example, an incoming OIDC payload like this:
{
"sub": "user-12345",
"email": "agent@google.com",
"groups": ["beta-testers", "engineering"]
}
Is translated by the Hub into the following Datalog facts:
user("user-12345");
email("agent@google.com");
group("beta-testers");
group("engineering");
3. Minting the Authority Block
The sam-hub takes these generated Datalog facts and embeds them into the Authority Block of a brand new Biscuit token. The Hub signs this block with its private cryptographic key.
Because the facts are sealed in the Authority Block by the Hub, any sam-node in the mesh can implicitly trust that the user holding the Biscuit possesses those specific emails and groups.
4. Policy Evaluation at the Node
When the agent presents the Biscuit to a sam-node to execute a tool, the node evaluates its local policies against the facts embedded in the token.
Because the OIDC claims were translated into Datalog, a node administrator can write elegant, logic-based rules in policy.go or their YAML configs:
// The node will only execute the tool if the Hub certified the agent is in the engineering group
allow if group("engineering");
2. Hub Policy Schema (policies.yaml)
Admins define central permissions by mapping OIDC roles to specific capabilities.
allowed_targets: Defines which logical groups or specific peers a user can route messages to, analogous to Active Directory security groups. Target definitions must be formatted as resolved facts (e.g.,group:<name>,user:<sub-id>,email:<email>,role:<role-name>, ornode:<peer-id>). Note: These are evaluated dynamically at the destination node using its own identity (see Section 3.1).allowed_services: Defines the application-level tools or endpoints a user can access. Services use a stricttype://nameconvention (e.g.,mcp://db-agentorinference://openrouter).- Strict Namespaces: There are no implicit fallbacks.
system://...is used for internal services,mcp://...for node services,inference://...for AI models, etc. Service names must be valid domain labels (e.g.value1.value2.value3). - Wildcards: SAM natively supports domain-level wildcards to build complex policies. The hub generates specific facts like
granted_service_exact($type, $name),granted_service_prefix($type, $prefix),granted_service_suffix($type, $suffix),granted_service_all_in_type($type), orgranted_service_all_types(). You can grant access to an entire type viamcp://*, allow prefix-based wildcard matching likemcp://*.service.local(which matches services ending in.service.local), suffix-based wildcard matching likemcp://service.*(which matches services starting withservice.), or global access to everything via*. Note that arbitrary partial matches (e.g.,dev-*or*-prod) are not supported because they do not align with domain boundaries and will fail DNS name validation. - MCP Namespace Convention: The
mcp://prefix (api.MCPServicePrefix) is the explicit convention for all Model Context Protocol targets. When remote nodes query local nodes for tool catalogs, if the local service is an MCP server, the proxy layer strictly enforces the authorization policy against themcp://prefix.
- Strict Namespaces: There are no implicit fallbacks.
[!NOTE] The
*global wildcard is a special case. It grantsgranted_service_all_types()fact, allowing the caller to invoke any tool regardless of its namespace or name.
version: "v1alpha1"
roles:
data-scientist:
allowed_targets:
- "node:12D3KooW..." # Specific peer ID
- "group:backend-nodes" # Logical group of peers
- "role:admin" # Nodes possessing the admin role
- "user:auth0|123456" # Node bound to a specific user sub
- "email:db@example.com" # Node bound to a specific email
allowed_services:
- "mcp://db-agent" # Access to specific MCP server
- "inference://openrouter" # Access to inference endpoints
- "mcp://*" # Wildcard access to all MCP servers
custom_datalog:
- 'department("analytics");' # Raw injected facts
3. Node Local Attenuation Schema (sam-node-config.yaml)
Local developers can further restrict access to their specific node. Local policies can only attenuate (restrict) access, never expand it beyond what the Hub allowed. They are evaluated entirely at the destination node.
version: "v1alpha1"
attenuation:
rules:
- 'deny if user("untrusted_sub_id");'
checks:
- 'check if time($time), $time < 2026-12-31T00:00:00Z;'
3.1 Evaluating allowed_targets at the Destination
The SAM network operates on a Zero Trust architecture. The origin node does not police its own traffic. When the Hub injects allowed_targets permissions (like - "group:backend-nodes") into the token, it creates granted_target_group("backend-nodes") capability facts and seals the token with a target_restricted() fact. If no targets are specified, the Hub mints a target_unrestricted() fact.
It is up to the destination node to mathematically prove that it is the intended target. To do this, the destination node automatically injects its own identity into the local authorization context as target facts (e.g., target_fact("group", "backend-nodes")).
The destination node enforces this via a baseline check:
check if allow_network_target($fact, $val) or target_unrestricted();
Because the target logic is baked directly into the node’s middleware, you don’t need to write manual Datalog rules for it. The node dynamically deduces allow_network_target($fact, $val) if any of its injected target_facts match the granted_target_* facts presented in the incoming token.
If the token is target_restricted() and the destination node does not possess an identity matching the granted targets, the connection is instantly rejected.
[!NOTE] Policy Evaluation Precedence Local policies defined in
sam-node-config.yamlare evaluated before baseline rules. This means local administrators can write rules that explicitlydenyaccess based on custom logic, effectively overriding any broad access granted by the Hub’s baseline policies. Local policies can only attenuate (restrict) access, never expand it beyond what the Hub allowed.
4. Node Baseline Security Rules
Every sam-node enforces a set of baseline security rules (defined in Go code) to secure the transport layer. These rules run automatically before evaluating custom OIDC or local policies:
4.1 Replay & Impersonation Prevention
Every request must prove that the libp2p cryptographic peer ID of the connection matches the client peer ID embedded in the authorization token:
check if client_peer_id($id), connection_peer_id($id);
4.2 The Catalog Service (sam.catalog)
To allow remote peers to discover tools and query connectivity, each node hosts a built-in catalog service at the special target sam.catalog. This service exposes local metadata tools (e.g. list_local_services, get_mesh_info).
To ensure tool discovery works out-of-the-box, the node automatically injects a baseline rule allowing all verified peers to access it:
allow if service("system", "sam.catalog");
[!IMPORTANT] Without this baseline rule (or if a custom local attenuation policy explicitly blocks it), remote nodes will not be able to retrieve this node’s tool catalog. As a result, agents across the mesh will fail to discover or call any of this node’s tools.
5. Ingress Authorization Pipeline (Execution Flow)
When a node receives an incoming connection request (via P2P HTTP Ingress or wrapped protocol streams like MCP/Inference), it performs a multi-stage verification pipeline.
5.1 Pipeline Stages
graph TD
A[Incoming Connection] --> B(Stage 1: Connection Gating)
B -->|Banned/Revoked| C[Drop Connection]
B -->|Allowed| D(Stage 2: Biscuit Token Verification)
D --> E[Run 1: Authorize Node's Own Identity Token]
E --> F[Query & Inject Destination Target Facts]
F --> G[Run 2: Authorize Caller's Request Token]
G -->|Success| H[Proxy to Downstream Service]
G -->|Denied| I[Return 403 Forbidden / Reject Auth Frame]Stage 1: Connection Gating (Layer 2)
- Performed immediately at the connection manager level (
gate.go). - Checks the remote peer ID against local blacklist (
Store.IsBanned) and revocation caches (revokedPeers). - Does not parse Biscuit tokens. Connection is dropped early if matched.
- Performed immediately at the connection manager level (
Stage 2: Biscuit Token Verification (Layer 3/4)
- Handled inside node middleware (
middleware.go). - Performs exactly two Biscuit authorizer execution runs to process authorization:
- Handled inside node middleware (
Run 1: Destination Identity Fact Evaluation
- Target: The node’s own identity token (issued by the Hub during enrollment).
- Purpose: We must mathematically evaluate the node’s own OIDC group/user claims and node ID to produce
target_fact(...)datalog assertions (e.g.target_fact("group", "backend-nodes")). These facts represent who we are. - Mechanism:
- We create an authorizer for our own token signed by the Hub’s public key.
- We add a baseline
allow if truepolicy. This policy is required by Biscuit so that the authorizer can execute (since the token itself holds only identity facts and has no operation-level authorization policies). - We execute
Authorize()to verify our token signature and validate internal checks (such as expiration). - We query
api.TargetFactRulesagainst this authorizer to extract target facts. - We inject these extracted
target_factassertions into the main request authorizer.
Run 2: Caller Request Authorization
- Target: The caller’s request token (the Biscuit token presented in the request’s
X-Sam-Biscuitheader orAuthFrame). - Purpose: Verifies that the caller has been granted access by the Hub to the requested target service, checks for replay protection, and evaluates local attenuation constraints.
- Mechanism:
- We create an authorizer for the caller’s token using the Hub’s public key.
- We inject the target matching facts derived from Run 1.
- We add baseline rules (e.g. peer ID matching connection ID check, system catalog access policies) and local attenuation configurations (
sam-node-config.yaml). - We execute
Authorize()to check all signatures, checks, and policies. If successful, the request is forwarded to the underlying service.