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:

KeyRequiredMeaning
typeyesmcp, inference or a2a.
nameyesUnique on this node. DNS-style labels separated by dots (db-reader, build.runner). Policy grants refer to type://name.
descriptionnoShown in discovery results and in the console.
target_urlmcp: this or command; inference and a2a: yesBackend URL that the node proxies to. Embedded credentials (http://user:pass@) are refused.
commandmcp only: this or target_urlArgument vector of a subprocess that speaks MCP over stdio. The node starts and supervises it.
envnocommand only. Environment for the subprocess.
target_auth_pathnotarget_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: command or target_url, not both. A target_url must be a Streamable HTTP MCP endpoint.
  • inference: target_url only. It must be the root URL of the backend without /v1. The node adds the prefix. Callers reach the service through the node’s /v1 endpoint or at /sam/<peer>/inference/<name>/v1/....
  • a2a: target_url only. The agent card is fetched from /.well-known/agent-card.json on 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.

KeyMeaning
rulesDerive new facts from existing ones: head(...) <- body(...).
checksConditions that must all hold: check if .... A failing check denies the request regardless of any policy.
policiesallow 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:

FactSource
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

KeyMeaning
require_labelsA 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>.