Agent runtime — anchor an agent's policy, decisions, and outputs
An agent run isn't one artifact — it's a trail. The policy the agent was running under at session start. The decisions it committed to as it ran. The evidence bundle that ties the run together. The handoff packet an auditor uses to verify it later without trusting the operator. This guide covers the four-part agent-session pattern, the Python
Session()context manager (agent_anchor.py), and the MCP tool surface for any MCP-compatible client.
Companion docs: API reference · OpenAPI spec · What to hash · Agents implementer spec · Production checklist · Compatibility map · Worked example: ResearchAgent
1. The 60-second framing
A Satsignal agent session binds a whole run into a proof trail:
- Policy snapshot. What the agent was allowed to do — model, tools, system policy, budget, permissions. Anchored at session start.
category: "policy_snapshot". - Decision commitments. One commitment per meaningful decision or output. Each is a fresh-nonce envelope so timing pre-discloses (the chain says "this hash was known by T") without content pre-disclosing (the payload stays sealed until you reveal it).
category: "commitment". - Evidence-bundle manifest. A final Merkle manifest binding the whole decision set into one root, anchored once at session end.
category: "evidence_bundle",mode: "manifest". - Handoff JSON. An off-chain file the auditor receives — the labels, the hashes, the txids, the manifest root needed to verify the session offline.
agent_anchor.pywrites this automatically.
"Session" describes the trail, not the cadence. By default each decision anchors as it happens — N + 2 chain anchors for an N-decision run (one policy, N decisions, one manifest). That's the stronger choice: it proves each decision was committed before it was later disclosed. Anchoring once per session (commit_each=False) is the lighter alternative; the manifest still proves the decision set but timing is proven for the set rather than each individual decision.
2. Use this when
- You're running an autonomous or semi-autonomous agent and you need a tamper-evident record of what the agent did and what it was allowed to do.
- You need to demonstrate to a counterparty (auditor, compliance, customer) that the agent committed to a decision before it was disclosed — a chain-anchored "I committed to outputting this answer at time T" predicate.
- You're integrating against an MCP host (Claude Desktop, Claude Code, an MCP-speaking framework) and want anchoring as a drop-in tool surface.
- Your runtime is Python and you want the agent-shaped anchoring discipline (policy → decisions → manifest → handoff) without writing the wiring yourself.
- You're doing eval scoring or model-grading work and need a proof that the eval methodology and the scored outputs were fixed at run time, not back-edited.
Don't use this when:
- You only have one file to anchor — use Files.
- The "agent" is just a script with no policy snapshot — the files path covers that simpler case; the session pattern is overkill.
- The decisions are < 5 and time-batched is fine — direct API with N anchors works too; the session helper saves wiring, not capability.
3. What you send
Three ways in
| route | when | shape |
|---|---|---|
Python Session() | runtime is Python | agent_anchor.py, stdlib only, six-line context manager |
| MCP tools | runtime speaks Model Context Protocol | satsignal-mcp exposes six tools over stdio |
| Direct API | non-Python / non-MCP runtime | N + 2 POST /api/v1/anchors calls with the right categories |
The four wire shapes (any route)
All three routes ultimately produce the same wire calls. Useful to know the underlying shape even if you're using the helper.
Policy snapshot (one per session, at start):
{
"folder_slug": "agent-runs-prod",
"sha256_hex": "<sha256 of policy snapshot JSON>",
"file_size": <bytes>,
"category": "policy_snapshot",
"session_id": "run-2026-05-26-001",
"label": "agent-alpha policy v3"
}
Decision commitment (N per session, one per decision):
{
"folder_slug": "agent-runs-prod",
"sha256_hex": "<sha256 of canonical decision envelope>",
"file_size": <bytes>,
"category": "commitment",
"session_id": "run-2026-05-26-001",
"label": "decision-3 (tool_call: search)"
}
The decision envelope is a JSON object {nonce, payload} where nonce is fresh 16-byte hex and payload is the decision content. JCS-canonicalize then sha256 — the canonical bytes are what gets anchored. The nonce is what makes the hash unguessable from a small payload space; see What to hash for the recipe.
Evidence-bundle manifest (one per session, at end):
{
"folder_slug": "agent-runs-prod",
"items": [
{"label": "decision-1", "sha256_hex": "<decision 1 sha>"},
{"label": "decision-2", "sha256_hex": "<decision 2 sha>"}
],
"category": "evidence_bundle",
"session_id": "run-2026-05-26-001",
"label": "agent-alpha run 2026-05-26-001 manifest"
}
A manifest-backed proof is detected by the presence of items (not sha256_hex); the API reports mode: "manifest" for it. The policy snapshot is anchored separately (category policy_snapshot) and is not a manifest leaf — the manifest binds only the per-decision commitments. That is the wire shape for a multi-item batch — an evidence shape, not a privacy mode (it works under a Standard or Sealed proof alike). See Manifest for the wire shape in depth — up to 10,000 items per anchor.
Broker-computed commitment (witness the broker's record, not the runtime's story)
The decision commitment above anchors a payload the agent runtime supplies via s.decide(...); a compromised runtime can anchor an internally-consistent commitment decoupled from any real grant or captured API response. When you are an authorization broker that runs as a separate trust domain from the runtime, have the broker (not the runtime) compute the commitment over {session_id, oauth_grant_id, tool, scope, user_ref, request_digest, response_digest, ts} — same category: "commitment" wire shape, same fresh-nonce envelope, same canonicalization. See Grant-bound commitment. This binds the witness to the broker's recorded request/response, not the agent's self-report — it still does not prove the grant decision was correct policy.
session_id is a query label, not the binding
Tag every anchor in a run with the same session_id so you can retrieve them via GET /api/v1/anchors?session_id=run-2026-05-26-001. The session_id is off-chain by design — the cryptographic "these anchors belong together" binding is the evidence-bundle manifest's Merkle root, anchored at end of session. Skip the manifest and you have a label but no proof.
4. What you store
The four-part trail, all sides
In your own system, per session:
session_id— your stable handle. Use it to query the anchor set.- Policy snapshot's
proof_id+txid+ the snapshot JSON itself. The JSON is the canonical artifact; the chain side is the timestamp. - Per-decision:
proof_id,txid, the canonical decision envelope JSON. Persist the envelope (with its nonce), not just the payload — the verifier needs the exact bytes that were hashed. - Evidence-bundle manifest's
proof_id,txid, and theitems[]array. The items are the labels + hashes; with those plus the per-decision hashes in your store, an auditor can recompute the Merkle root and compare to what was anchored. - The handoff JSON (
agent_anchor.pywrites it for you, but you persist it). This is the single file that ships to the auditor.
Handoff JSON shape
agent_anchor.py writes a file shaped like:
{
"schema": "satsignal-agent-handoff-v1",
"session_id": "run-2026-05-26-001",
"folder_slug": "agent-runs-prod",
"policy": {
"proof_id": "...",
"txid": "...",
"sha256_hex": "...",
"canonical_bytes_b64": "<base64 of the canonical policy JSON>"
},
"decisions": [
{
"label": "decision-1",
"proof_id": "...",
"txid": "...",
"sha256_hex": "...",
"canonical_bytes_b64": "<base64 of the canonical decision envelope>"
}
],
"manifest": {
"proof_id": "...",
"txid": "...",
"root": "...",
"leaf_count": 3
}
}
The auditor receives this file. They (a) re-hash each canonical_bytes_b64, (b) confirm the hash matches sha256_hex, (c) confirm each txid chain-confirms, (d) recompute the manifest root from the decision hashes in order, (e) compare to the on-chain manifest root.
5. What verification needs later
The auditor checks, in order:
- Each disclosed decision recomputes to its recorded hash. The canonical envelope is in the handoff JSON; sha256 it, compare to the policy / decision / manifest item hashes.
- Each anchor chain-confirms. Resolve each
txidagainst any public BSV node, parse the OP_RETURN, confirm the embeddeddoc_hashmatches the canonical doc in the bundle. - The manifest root recomputes from the ordered decision hashes. Walk the
items[]in order, build the Merkle tree, compare the root to what the manifest anchor committed. This is what proves no decision was added or removed after the fact.
If any decision's payload is meant to stay sealed (the commitment was the on-chain part; the reveal is bilateral with the counterparty), the auditor verifies only the timing predicate: "the hash was known by block time T". They can later compare to the revealed payload when it's disclosed.
Cross-link: What to hash — Agent decisions covers the nonce envelope and why hashing a stringified Python dict is the most-common verification-time failure mode.
6. Copy-paste example
Python — Session() context manager
The recommended path. Stdlib only, no SDK to install — drop agent_anchor.py next to your code.
import os
from agent_anchor import Session
SYSTEM_PROMPT = "You are a careful agent..."
with Session(api_key=os.environ["SATSIGNAL_API_KEY"],
folder_slug="agent-runs-prod") as s:
# 1. Anchor the policy snapshot at session start.
s.policy(
system_policy_text=SYSTEM_PROMPT,
model_config={"model": "claude-opus-4-7"},
)
# 2. Each decide() anchors one decision as it happens
# (commit_each=True is the default).
for step in plan:
result = run_step(step)
s.decide(step.label, result)
# 3. __exit__ anchors the manifest + writes handoff.json
# automatically. No further code needed.
agent_anchor.py is at satsignal.cloud/agent_anchor.py. Worked example at satsignal.cloud/example_agent_snapshot.py.
MCP — drop into any MCP host
pip install satsignal-mcp
In your MCP host's server-config block (e.g. claude_desktop_config.json):
{
"mcpServers": {
"satsignal": {
"command": "satsignal-mcp",
"env": {
"SATSIGNAL_API_KEY": "sk_..."
}
}
}
}
Important — MCP-host env-var rebinding. Some hosts (notably Claude Desktop) strip or rebind environment variables at server- launch time. A SATSIGNAL_API_KEY exported in your shell does not reach the MCP child process. Bind the key explicitly in the host's server-config env: {...} block — relying on process-env inheritance produces a 401 with no breadcrumb.
The six tools satsignal-mcp exposes:
| tool | what it does |
|---|---|
anchor_file | anchor a file by path |
anchor_text | anchor a text blob (hashed in-process) |
anchor_json | anchor a JSON object (JCS-canonicalized) |
lookup_hash | resolve a file hash → txid (standard-mode anchors only) |
verify_file_against_bundle | full verify (re-hashes original file, detects tampering) |
chain_confirm_bundle | fast chain-only check; does NOT detect file tampering |
verify_file_against_bundle is the full verifier; chain_confirm_bundle is the fast path that confirms a bundle's txid is on chain without re-hashing the artifact. Use the former when the verification guarantee matters; the latter when you just want a "yes the chain side is fine" signal.
Repo: github.com/Steleet/satsignal-mcp. PyPI: pypi.org/project/satsignal-mcp/. stdio transport in v0.1; SSE coming later.
Direct API — non-Python, non-MCP
The wire shapes from §3 wired together in any HTTP client. Pseudo- code (replace with your runtime's HTTP library):
POST /api/v1/anchors {policy snapshot body} → get policy_proof_id
for each step:
decision = {"nonce": rand16hex(), "payload": step.payload}
canonical = jcs(decision)
POST /api/v1/anchors {sha256(canonical), category: "commitment", session_id, ...}
persist (label, canonical, proof_id, txid)
POST /api/v1/anchors {items: [...], category: "evidence_bundle", mode: manifest}
→ get manifest_proof_id, manifest_root
write handoff.json
If a broker in a separate trust domain (rather than the runtime) is the trusted boundary, the broker fills payload from its own grant + request/response record instead of step.payload — see Grant-bound commitment.
7. Production notes
Strong timing vs batch
Default is strong timing (commit_each=True): each decision anchors as it happens. N + 2 chain anchors. The strongest property: each decision proven-committed before it was later disclosed. Use this for any flow where the "I committed before I revealed" predicate matters — sealed bids, eval results disclosure, agent self-grading.
Batch (commit_each=False): decisions held locally and only the policy snapshot + final manifest anchor. 2 anchors regardless of N. Lighter on quota and on chain footprint; the manifest still proves the decision set but the timing predicate is proven "by the time of the manifest anchor" for the whole set rather than per-decision.
Pick strong timing unless you have a specific quota / cost reason to batch.
Quota and rate limits
Each session burns N + 2 anchors against your monthly quota (strong timing) or 2 anchors (batch). Wire GET /api/v1/usage into your agent's pre-flight check so a quota exhaustion doesn't strand a session mid-run.
Per-key rate limit: see Files §7 for header semantics. Same rate-limiter applies; agent sessions aren't special-cased.
Idempotency
agent_anchor.py derives a per-decision Idempotency-Key from <session_id>:<decision_label>. Re-running the same labelled decision with the same body returns the cached anchor — useful when a step retries on transient errors. The MCP tools do the same.
Direct-API integrators: derive the key the same way for free idempotency.
Failed-mid-session recovery
If the session helper crashes after some decisions anchored but before the manifest:
# Resume — pass the same session_id; the helper queries existing
# anchors and continues from where it left off.
with Session(api_key=..., folder_slug="...",
session_id="run-2026-05-26-001",
resume=True) as s:
# ...
The manifest's Merkle root depends on the ordered decision hashes. If you re-run decisions out of order on resume, the manifest binds a different root than the one you'd have produced on a single-shot run. Keep ordering stable across resumes.
Key rotation
Mid-session key rotation will fail the session — the cached Idempotency-Key records bind to the original key. If you must rotate, end the session first (write the manifest with the old key), then start a fresh session under the new key.
For the full pre-flight checklist (key rotation, broadcast failure recovery, support flow), see Production checklist.
8. Errors you might see
| code | name | meaning | what to do |
|---|---|---|---|
400 | invalid_category | category not in the enum | use policy_snapshot, commitment, or evidence_bundle |
400 | manifest_too_many_items | manifest carries > 10,000 items | split into multiple sub-manifests + a top-level manifest of those |
400 | manifest_empty | items[] is empty | a manifest must carry ≥1 item; send at least one decision leaf (the policy snapshot is anchored separately, not as a manifest leaf) |
401 | unauthorized | API key wrong or unset (MCP: env not reaching child) | bind the key in the host's env: {...} block |
404 | folder_not_found | folder doesn't exist | create the folder first |
409 | idempotency_key_reuse_body_mismatch | session resumed with a body change | use a fresh session_id or align the body |
429 | quota_exceeded | monthly anchor budget gone | email hello@satsignal.cloud for a cap lift; sessions need N + 2 slots |
9. Legacy field aliases (if your code sends them)
Same as all other paths: responses emit the canonical proof / folder names only; requests written against the legacy spellings keep working forever.
Full canonical/legacy mapping across endpoints, fields, scopes, CLI flags, and error codes: Compatibility map.
10. Where this fits
Worked example: ResearchAgent makes its decisions auditable — full agent-session integration walkthrough with a 6-month-later regulator-audit scenario.
- The deeper agent cookbook — full
satsignal-agent-handoff-v1schema, CI transport testing, the full lifecycle categories — is at proof.satsignal.cloud/agents. - For the exact byte-selection recipes used by
agent_anchor.py(policy snapshot canonicalization, decision envelope nonce shape, manifest leaf construction), see What to hash. - For the manifest mode wire details (the
items[]array, the Merkle leaf rule, the 10,000-leaf cap), see Manifest. - If you are an authorization broker running in a separate trust domain from the runtime and need the commitment to witness your recorded request/response (the bytes you captured at the grant boundary), not the runtime's self-report, see Grant-bound commitment — the broker computes and anchors
{oauth_grant_id, tool, scope, user_ref, request_digest, response_digest}(abbreviated; the guide has the full payload). - If the agent run is just files (no policy snapshot, no decision envelopes), the simpler Files path covers it.
- For LangChain agents specifically: the
langchain-satsignaladapter wraps these primitives as LangChain tools — same on-chain shape, drop-in for agent components. See github.com/Steleet/langchain-satsignal.
Questions about this specification? Email hello@satsignal.cloud.