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.echo is allowed and tool.shell is 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: normal with no policy bundle loaded, meaning all tool calls default to deny via denyAllEvaluator{}. Evidence: cmd/autonomy/commands/runtime.godenyAllEvaluator{} is used when mgr.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 against data.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-golden runs TestJSONFingerprintGolden in 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 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

ErrNotSigned

2

agent_artifact.digest in lock matches OCI manifest digest

ErrDigestMismatch

3

behavioral_fingerprint in lock matches BLAKE3(canonical(lock))

ErrFingerprintMismatch

4

policy_bundle.policy_bundle_version satisfies required_runtime_version range

ErrSemverIncompat

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_id is 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 denied

  • 1 — 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/tool and 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

registry: connection refused

Docker stack not running

make demo-up

verification failed: ErrNotSigned

cosign not run, --require-lock flag used

Run make demo-run-unsigned or sign first

verification failed: ErrFingerprintMismatch

Lock file modified after fingerprint was computed

Re-run autonomy lock fingerprint --in <lock>

tool.echo deny after policy load

Runtime not restarted after policy load

docker compose restart runtime

mode: strict in /health

AUTONOMY_STRICT_MODE=1 env set

Unset env, restart runtime

bundle: incompatible runtime version

required_runtime_version range excludes runtime

Rebuild bundle without --runtime-version


What Just Happened

  • Built a .tar.gz policy bundle from Rego rules with a locked BLAKE3 hash

  • Pushed 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.echo is allowed and tool.shell is denied at the tool boundary

  • Confirmed that deny decisions cannot be overridden by the caller

Next Tutorial

Tutorial 02 — Multi-Node: Seed Once, Update Everywhere