Node configuration file
sam-node.yaml declares what a node is (labels), what it publishes
(services), what it requires from callers (attenuation) and what it requires
from providers (egress). Both sam-node join and sam-node run read it from
--config, by default ./sam-node.yaml. A missing file means an empty
configuration. Unknown keys are an error, because a typo under attenuation
would otherwise weaken the node without notice.
version: "v1alpha1"
labels:
region: eu-west-1
team: platform
services:
- type: mcp
name: code-reviewer
description: "Reviews diffs"
target_url: "http://127.0.0.1:7777/mcp"
- type: mcp
name: filesystem
command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/srv/docs"]
env:
NODE_OPTIONS: "--max-old-space-size=256"
- type: inference
name: vllm
target_url: "http://127.0.0.1:8000"
target_auth_path: /etc/sam/vllm-key
- type: a2a
name: triage
target_url: "http://127.0.0.1:9999"
attenuation:
rules:
- 'maintenance() <- time($t), $t > 2026-12-31T00:00:00Z;'
checks:
- 'check if label("team", "platform");'
policies:
- 'deny if service("mcp", "filesystem"), group("contractors");'
- 'deny if maintenance();'
egress:
require_labels:
jurisdiction: eu
version
"v1alpha1", or omitted (read as v1alpha1). A node refuses a version it
does not know.
labels
A map of key: value. Keys are 1 to 63 characters from [a-zA-Z0-9_.-].
Values are non-empty, at most 255 characters, and contain no ,, = or
control characters, because the wire form is a comma-separated key=value
list. A malformed label stops the node at start.
Labels are sent at enrollment. The control plane writes them into the node’s
credential as label(key, value) facts, but only if a role the node holds
allows them through allowed_labels. A label that the role does not permit
fails the enrollment. Changing labels requires a new enrollment. Matching is
exact and case-sensitive.
services
A list. Each entry has these keys:
| Key | Required | Meaning |
|---|---|---|
type | yes | mcp, inference or a2a. |
name | yes | Unique on this node. DNS-style labels separated by dots (db-reader, build.runner). Policy grants refer to type://name. |
description | no | Shown in discovery results and in the console. |
target_url | mcp: this or command; inference and a2a: yes | Backend URL that the node proxies to. Embedded credentials (http://user:pass@) are refused. |
command | mcp only: this or target_url | Argument vector of a subprocess that speaks MCP over stdio. The node starts and supervises it. |
env | no | command only. Environment for the subprocess. |
target_auth_path | no | target_url only. File whose contents the node presents to the backend: a bare token as Authorization: Bearer <token>, or user:pass as HTTP Basic. Read once at start, never logged or advertised. It overrides any Authorization header that a caller sent. |
Rules by type:
mcp:commandortarget_url, not both. Atarget_urlmust be a Streamable HTTP MCP endpoint.inference:target_urlonly. It must be the root URL of the backend without/v1. The node adds the prefix. Callers reach the service through the node’s/v1endpoint or at/sam/<peer>/inference/<name>/v1/....a2a:target_urlonly. The agent card is fetched from/.well-known/agent-card.jsonon the backend and served again with mesh URLs. The card must use the A2A 1.0 format.
Before a service is advertised, the node probes the backend
(--backend-probe-timeout, 2 seconds). A backend that does not answer is
not advertised, and the log records this.
Services exist only through this file. There is no runtime API that adds a service. An agent with access to the node’s API therefore cannot point the mesh at a new backend.
attenuation
Biscuit Datalog that the node adds to every authorization decision for
requests it receives. Each entry is one statement. A trailing ; is
optional. A statement that does not parse stops the node at start.
| Key | Meaning |
|---|---|
rules | Derive new facts from existing ones: head(...) <- body(...). |
checks | Conditions that must all hold: check if .... A failing check denies the request regardless of any policy. |
policies | allow if ... and deny if ..., evaluated before the baseline policies. The first matching policy wins. A deny here overrides a mesh grant. An allow here admits a caller that the mesh did not grant, but never a caller that fails a check. |
Facts available:
| Fact | Source |
|---|---|
service($type, $name) | The request: which service is being called. |
time($now) | Injected at evaluation. |
connection_peer_id($id) | The authenticated peer. |
user($sub), email($e), group($g), idp_role($r) | The caller’s OIDC claims, from its credential. |
role($r) | The caller’s mesh roles. |
label($k, $v) | The caller’s attested labels. |
node($id), client_peer_id($id), expiration($t) | The caller’s credential binding and expiry. |
granted_service_*, granted_target_*, granted_agent_* | The caller’s grants. |
target_fact($name, $value) | This node’s own identity facts, for target matching. |
agent($id) | The agent the caller says it acts for, when present. |
The mobile app has the same three lists in its settings, with the same syntax and the same errors.
egress
| Key | Meaning |
|---|---|
require_labels | A map of key: value. Every remote provider that this node sends a request to must have all of these labels attested in its credential, whether or not the caller asked for labels. Same syntax rules as labels. |
This is the operator’s floor. A caller’s X-Sam-Required-Labels header is
checked separately. It can add requirements but cannot remove or relax the
floor. A floor that names a label which no provider carries makes the node
unable to reach any provider, which can serve as an egress kill switch. Omit
the block for no floor.
Kubernetes
The sam-node Helm chart renders its config: value as this file and
restarts the pod when it changes. Because the backend runs in the same pod,
target_url there is always http://127.0.0.1:<port>.