Tutorial 01 — Single Node: Receive, Verify, and Activate Offline¶
Objective: Build a signed, policy-verified software supply chain from source to a running edge node — without any cloud dependency at activation time.
What you will demonstrate:
Build a policy bundle from Rego rules
Push an agent artifact to a local OCI registry
Attach the activation lock (with BLAKE3 behavioral fingerprint) and policy bundle as OCI sidecars
Sign all artifacts with cosign
Run the 4-step supply-chain verification pipeline
Load the policy into the autonomy runtime
Observe that
tool.echois allowed andtool.shellis denied by policy
Time: ~15 minutes (with Docker images already pulled)
Architecture¶
┌─────────────────────────────────────────────────────────────────┐
│ Developer Machine │
│ │
│ Rego files │
│ (echo_allow.rego, ─┐ │
│ shell_deny.rego) │ │
│ ▼ │
│ autonomy policy build │
│ │ │
│ ▼ │
│ demo-bundle.tar.gz │
│ │ │
│ ├──────────────────────────────────┐ │
│ ▼ │ │
│ autonomy oci push-test-artifact │ │
│ │ │ │
│ ▼ ▼ │
│ localhost:5000/autonomy-demo/ demo-bundle │
│ agent:v1 (digest) │ │
│ │ │ │
│ ▼ │ │
│ autonomy oci attach-lock (lock.json) │ │
│ autonomy oci attach-policy (bundle) ◄────────┘ │
│ │ │
│ ▼ [SKIP_SIGNING=1 to omit] │
│ cosign sign (key-based) │
│ │ │
│ ▼ │
│ autonomy verify (4-step pipeline): │
│ Step 1: cosign signature present │
│ Step 2: agent_artifact.digest matches OCI manifest │
│ Step 3: behavioral_fingerprint matches BLAKE3(canonical) │
│ Step 4: policy_bundle.semver compatible with runtime │
│ │ │
│ ▼ │
│ autonomy policy load → runtime /data/policy/current/ │
│ │ │
│ ▼ │
│ POST /v1/tool ──► ToolServer │
│ │ │
│ evaluate() │
│ │ │
│ ┌────────┴────────┐ │
│ allow (echo) deny (shell) │
│ │ │ │
│ WAL Append WAL Append │
│ OTLP export OTLP export │
└─────────────────────────────────────────────────────────────────┘
Step 0: Start the Stack¶
cd $REPO
make demo-up
Wait for the stack to be healthy:
make demo-smoke
Expected output:
[demo] waiting for registry...
[demo] registry healthy ✓
[demo] waiting for runtime...
[demo] runtime healthy ✓
/health responded: {"status":"ok","mode":"normal"}
Smoke test passed
Note: The runtime starts in
mode: normalwith no policy bundle loaded, meaning all tool calls default todenyviadenyAllEvaluator{}. Evidence:cmd/autonomy/commands/runtime.go—denyAllEvaluator{}is used whenmgr.ActiveBundle()returns no bundle.
Step 1: Build the Policy Bundle¶
Rego files are in demo/policies/. Inspect them:
cat demo/policies/echo_allow.rego
cat demo/policies/shell_deny.rego
shell_deny.rego and echo_allow.rego are standard Rego modules in package autonomy.
Policy evaluator note: The runtime uses embedded OPA/Rego evaluation. Rego modules are compiled once at load (
PrepareForEval) and evaluated per tool call againstdata.autonomy.allow. Evidence:policy/evaluator.go(NewEvaluator,Eval),policy/evaluator_test.go.
Build the bundle:
autonomy policy build \
--in demo/policies/ \
--out /tmp/demo-bundle.tar.gz \
--version 1.0.0 \
--name autonomy-demo
Expected output:
bundle written: /tmp/demo-bundle.tar.gz (version=1.0.0, hash=sha256:...)
Inspect the bundle manifest:
autonomy policy inspect --bundle /tmp/demo-bundle.tar.gz
Expected output (abridged):
name: autonomy-demo
version: 1.0.0
bundle_hash: sha256:...
required_runtime_ver: (none)
created_at: 2026-...
files:
echo_allow.rego
shell_deny.rego
Evidence:
policy/builder.go:Build(),policy/builder.go:BundleManifest
Step 2: Push the Agent Artifact¶
Push a minimal OCI artifact (for demo purposes; in production this would be your agent image):
autonomy oci push-test-artifact --image localhost:5000/autonomy-demo/agent:v1
Expected output:
pushed: localhost:5000/autonomy-demo/agent:v1@sha256:<digest>
Save the digest:
AGENT_DIGEST=$(autonomy oci push-test-artifact --image localhost:5000/autonomy-demo/agent:v1 2>&1 | grep -oP 'sha256:[a-f0-9]+')
echo "Agent digest: $AGENT_DIGEST"
Evidence:
oci/oras.go:PushTestArtifact()
Step 3: Build the Activation Lock¶
Create the lock file with live digests:
POLICY_DIGEST=$(sha256sum /tmp/demo-bundle.tar.gz | awk '{print "sha256:" $1}')
cat > /tmp/demo.lock.json <<EOF
{
"lock_format_version": "0.1.0",
"als_spec_version": "0.1.0",
"tooling_version": "autonomyops/adk/0.1.0",
"agent_artifact": {
"digest": "${AGENT_DIGEST}",
"image_ref": "localhost:5000/autonomy-demo/agent:v1"
},
"policy_bundle": {
"digest": "${POLICY_DIGEST}",
"ref": "oci://localhost:5000/autonomy-demo/agent:v1-policy"
},
"inputs": {
"env_constraints": {
"allowed_tools": ["tool.echo"],
"max_tokens": 4096
},
"prompt_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
},
"behavioral_fingerprint": ""
}
EOF
Compute the BLAKE3 behavioral fingerprint (canonicalizes key order first, then hashes):
autonomy lock fingerprint --in /tmp/demo.lock.json
Expected output:
blake3:<64-char-hex>
The fingerprint is now stored in the file. Verify:
autonomy lock fingerprint --in /tmp/demo.lock.json --verify
Expected output:
fingerprint OK: blake3:<64-char-hex>
Determinism gate:
make check-goldenrunsTestJSONFingerprintGoldenin CI to verify that the canonical JSON encoding and BLAKE3 hash are reproducible across builds. Evidence:lock/lock_test.go:TestJSONFingerprintGolden,lock/lock.go:ComputeFingerprint()
Step 4: Attach Sidecars to the OCI Image¶
Attach the policy bundle:
autonomy oci attach-policy \
--image localhost:5000/autonomy-demo/agent:v1 \
--bundle /tmp/demo-bundle.tar.gz
Expected output:
attached policy bundle via referrers-api (or sidecar-tag)
image: localhost:5000/autonomy-demo/agent:v1
mode: referrers-api
digest: sha256:...
Registry capability: The attach command probes the registry for OCI Referrers API support (cached 1 hour at
~/.autonomy/registry_caps.json). Falls back to sidecar tag (<image-tag>-policy) if the registry does not support it. Evidence:oci/probe.go:SupportsReferrers(),oci/attach.go:AttachPolicyBundle()
Attach the activation lock:
autonomy oci attach-lock \
--image localhost:5000/autonomy-demo/agent:v1 \
--lock /tmp/demo.lock.json
Expected output:
attached lock via referrers-api
image: localhost:5000/autonomy-demo/agent:v1
mode: referrers-api
digest: sha256:...
Step 5: Sign the Supply Chain (Optional but Recommended)¶
If cosign is installed (cosign version), sign all artifacts:
cosign sign --key demo/keys/cosign.key \
localhost:5000/autonomy-demo/agent:v1 \
--tlog-upload=false
Warning:
demo/keys/cosign.keyis a demo key for local testing only. Rotate keys before production use.
To skip signing entirely (unsigned path — reduced security guarantees):
export SKIP_SIGNING=1
The verification step in Tutorial 01 Step 6 will note that signing was skipped and treat it as a soft-fail with a warning.
Step 6: Run the 4-Step Supply-Chain Verification¶
autonomy verify \
--image localhost:5000/autonomy-demo/agent:v1 \
--pub demo/keys/cosign.pub \
--require-lock \
--require-policy
Expected output (signed path):
[step 1] cosign: signature present ✓
[step 2] agent_artifact.digest: sha256:... matches OCI manifest ✓
[step 3] behavioral_fingerprint: blake3:... matches canonical hash ✓
[step 4] policy_bundle: version 1.0.0 compatible with runtime 0.1.0 ✓
verification passed
The 4 steps are:
Step |
Check |
Failure = |
|---|---|---|
1 |
cosign signature present on image |
|
2 |
|
|
3 |
|
|
4 |
|
|
Tamper evidence: If any byte of the lock is changed after signing, step 2 or 3 will fail. This is demonstrated in Drill 4 of Tutorial 03. Evidence:
oci/sign/verify.go,TestVerify_TamperedAgentDigest,TestVerify_TamperedFingerprint
Step 7: Load the Policy into the Runtime¶
autonomy policy load \
--bundle /tmp/demo-bundle.tar.gz \
--manager-dir demo/data/policy
Expected output:
loaded bundle: autonomy-demo@1.0.0 (sha256:...)
current slot: /demo/data/policy/current/
lkg slot: /demo/data/policy/lkg/ (promoted from previous current)
Check the active policy:
autonomy policy status --manager-dir demo/data/policy
Expected output:
current: autonomy-demo 1.0.0 (loaded 2026-...)
lkg: (none)
Restart the runtime container to pick up the new policy:
docker compose -f demo/docker-compose.yml restart runtime
sleep 3 # wait for runtime to restart
curl -s http://localhost:7777/health | jq .
Expected output:
{"status":"ok","mode":"normal"}
LKG (Last-Known-Good) invariant: If the new bundle fails the compatibility check, the current slot is unchanged and the LKG slot is preserved. Evidence:
policy/manager.go:LoadBundle(),TestManager_LKGPreservedAfterRejection
Step 8: Observe Policy Enforcement¶
Call tool.echo (expected: ALLOW):
curl -s -X POST http://localhost:7777/v1/tool \
-H 'Content-Type: application/json' \
-d '{"kind":"tool.echo","params":{"message":"hello from tutorial 01"}}' | jq .
Expected output:
{
"decision": "allow",
"reason": "policy: no deny rule matched",
"output": "hello from tutorial 01",
"policy_ref": "sha256:...",
"audit_id": "01JXXXXXX..."
}
Call tool.shell (expected: DENY):
curl -s -X POST http://localhost:7777/v1/tool \
-H 'Content-Type: application/json' \
-d '{"kind":"tool.shell","params":{"command":"id"}}' | jq .
Expected output:
{
"decision": "deny",
"reason": "policy: ...",
"output": null,
"policy_ref": "sha256:...",
"audit_id": "01JXXXXXX..."
}
Only the decision: "deny" contract is strict in this step; reason text may
vary by policy implementation details.
Fail-closed guarantee: The
audit_idis included in both allow and deny responses. It references the WAL sequence where the decision was recorded before the response was returned. Evidence:runtime/server.go:handleTool(),runtime/server.go:emitDecision()
Retrieve a decision from the WAL using the audit ID:
AUDIT_ID="01JXXXXXX..." # substitute from above
curl -s http://localhost:7777/v1/audit/$AUDIT_ID | jq .
Step 9: Run the Python Agent Demo¶
The included Python agent calls tool.echo (expects allow) and tool.shell (expects deny):
cd demo/agent_py
uv run --frozen python agent.py --runtime http://127.0.0.1:7777
Expected output:
[agent] calling tool.echo...
[agent] tool.echo → allow (audit_id=01JXXXXXX) ✓
[agent] calling tool.shell...
[agent] tool.shell → deny (policy: tool.shell is denied) ✓
[agent] all invariants confirmed
exit 0
Exit codes:
0— PASS: echo allowed, shell denied1— FAIL: security violation (deny bypassed or allow missed)2— Runtime unreachable
Trust boundary: The Python agent is untrusted. The Go runtime is the policy authority. The adapter calls
POST /v1/tooland cannot modify the decision. Evidence:runtime/interceptor.go:TestInterceptorDenyCannotBeOverridden
Automated Version¶
The full Tutorial 01 flow is automated:
# With signing (requires cosign):
make demo-run
# Without signing:
make demo-run-unsigned
Both targets call scripts 01_build.sh, 02_push_attach_sign.sh, 03_verify_and_run.sh
in sequence, with structured [PASS] / [FAIL] output markers.
Troubleshooting¶
Symptom |
Cause |
Fix |
|---|---|---|
|
Docker stack not running |
|
|
cosign not run, |
Run |
|
Lock file modified after fingerprint was computed |
Re-run |
|
Runtime not restarted after |
|
|
|
Unset env, restart runtime |
|
|
Rebuild bundle without |
What Just Happened¶
Built a
.tar.gzpolicy bundle from Rego rules with a locked BLAKE3 hashPushed an OCI agent artifact and attached lock + policy as OCI sidecar referrers
Signed all artifacts with cosign (or skipped signing for unsigned demo)
Verified the supply chain via a 4-step pipeline: cosign signature, digest, fingerprint, semver
Loaded the policy into the runtime’s managed cache (current + LKG slots)
Observed that
tool.echois allowed andtool.shellis denied at the tool boundaryConfirmed that deny decisions cannot be overridden by the caller
Evidence Links¶
Claim |
File |
Symbol |
|---|---|---|
Policy bundle build |
|
|
OCI sidecar attach |
|
|
Registry probe |
|
|
4-step verification |
|
|
Behavioral fingerprint |
|
|
Policy manager slots |
|
|
Tool enforcement |
|
|
Fail-closed invariant |
|
|