Security Model¶
Trust boundary¶
┌─────────────────────────────────────────────────────┐
│ Untrusted zone │
│ │
│ Python adapter ─── POST /v1/tool ──► Go runtime │ ◄─ Trusted zone
│ LangChain tool │
│ Any HTTP client │ │
└─────────────────────────────────────────│──────────┘
│
policy evaluation (OPA)
│
Allow │ Deny
│
tool execution (allow only)
The runtime is the sole policy authority. Adapters submit tool call intents
via POST /v1/tool. The runtime evaluates policy and either executes the tool
(allow) or returns HTTP 403 (deny). No adapter code path can flip deny to allow.
Fail-closed: if the policy evaluator returns an error for any reason, the runtime defaults to Deny. An unavailable or corrupt policy bundle produces deny-all behavior, not allow-all.
Runtime API contract¶
POST /v1/tool
Content-Type: application/json
{"kind":"<tool-kind>","params":{...}}
HTTP status |
Meaning |
|---|---|
200 |
Decision = allow; tool executed; response contains |
403 |
Decision = deny; tool never ran; response contains |
400 |
Allow decision but tool execution failed (invalid params, endpoint not allowlisted) |
405 |
Non-POST request |
The decision field is always present in the response body.
The policy_ref field carries the version of the active bundle that made the decision.
Supply-chain verification order¶
autonomy verify applies four checks in sequence. All must pass; the command
is fail-closed.
Step 1 — Signatures
cosign verify image
cosign verify <tag>-lock (if --require-lock)
cosign verify <tag>-policy (if --require-policy)
Step 2 — OCI digest integrity
Resolve live manifest digest
Compare against agent_artifact.digest in attached lock file
Step 3 — Behavioral fingerprint
Recompute BLAKE3 fingerprint of lock file
Compare against behavioral_fingerprint field
Step 4 — Semver consistency
Parse version tag from policy_bundle.ref
Compare major.minor against bundle manifest.json version field
Each step assumes the previous step passed.
Skipping --require-lock or --require-policy removes those cosign checks but
does not skip the OCI digest and fingerprint checks for whichever artifacts
are present.
Signing implementation¶
AutonomyOps uses cosign as an external CLI binary (Option A — subprocess) rather than the cosign Go SDK.
Why CLI and not the Go SDK?
The cosign Go SDK pulls in hundreds of transitive dependencies: sigstore, TUF, OIDC, Rekor, and their chains. Keeping the cosign binary as an external dependency gives the same functionality while keeping the Go binary lean and the dependency graph auditable.
Where the CLI is invoked:
Component |
Invocation |
Purpose |
|---|---|---|
|
subprocess |
Shared cosign wrapper for |
|
via |
|
|
via |
|
|
subprocess |
Single-artifact signature check |
|
subprocess |
Policy bundle signature pre-flight |
No Go SDK import: github.com/sigstore/cosign/v2 is not imported anywhere.
Prerequisites: cosign must be in PATH for autonomy sign, autonomy verify,
and autonomy policy fetch to function. Runtime policy evaluation does not
require cosign.
Install cosign: https://github.com/sigstore/cosign/releases
Distinct error types for each failure mode:
Sentinel |
Step |
Cause |
|---|---|---|
|
1 |
No cosign signature in registry |
|
2 |
SHA-256 OCI digest ≠ lock record |
|
3 |
BLAKE3 behavioral fingerprint ≠ stored value |
|
4 |
policy_bundle_version major.minor mismatch |
|
1 |
|
|
1 |
|
|
any |
cosign binary not in PATH |
Key management¶
Demo keys live in demo/keys/. They are not suitable for production.
Regenerate:
bash demo/keys/generate.sh
The cosign.key file is PKCS8 PEM, encrypted with COSIGN_PASSWORD.
Set the environment variable before signing or verifying:
COSIGN_PASSWORD=<secret> autonomy sign \
--image localhost:5000/agent:v1 \
--key demo/keys/cosign.key \
--lock --policy
Timestamp annotation¶
By default (AUTONOMY_TRUST_TIME=true), every signature carries an
autonomy.signed-at annotation (RFC3339 UTC). autonomy verify rejects
signatures older than --max-age (default: 8760h / 1 year).
Disable timestamp enforcement for air-gapped environments where clocks are unreliable:
AUTONOMY_TRUST_TIME=false autonomy verify \
--image <ref> --pub <key>
Weakened: with AUTONOMY_TRUST_TIME=false, a compromised private key can
produce signatures with no temporal bound. Use only with an out-of-band key
rotation process.
Air-gapped operation¶
All runtime operations work without external network access.
Build the binary on an internet-connected machine, copy to the air-gapped host.
Push images and policy bundles to a private registry before the gap.
autonomy policy fetchpulls a cached bundle from the private registry.autonomy verifyrequires the cosign public key on disk and a local registry; no Sigstore transparency log or certificate authority is contacted when using key-based signing.
The capability probe (autonomy oci probe) and attachment operations contact
only the registry specified in --image. There are no callbacks to external
services.
Policy evaluation¶
Runtime policy evaluation uses OPA/Rego from the loaded policy bundle.
The query is data.autonomy.allow, with input containing action kind and
params.
Decision behavior is fail-closed:
If OPA returns
allow == true, runtime allows the action.If evaluation errors, returns no result, or returns false, runtime denies.
This preserves the same trust boundary: adapters submit requests, runtime remains the sole policy authority.
Adapter trust level¶
Adapters — Python RuntimeClient, LangChain RuntimeTool, any HTTP client —
are untrusted. They:
Can submit tool call requests.
Cannot override policy decisions.
Cannot execute tools directly; they receive only the runtime’s output.
Cannot suppress
PolicyDeniedError; the exception propagates unconditionally.
The Python adapter enforces this at the type level:
call_tool() returns ToolResult on allow and raises PolicyDeniedError on deny.
There is no return path that converts a deny into a non-exceptional result.
Telemetry and audit¶
Every policy decision emits an autonomy.decision event to the WAL with:
tool— the action kindoutcome—allowordenyreason— human-readable explanation from the evaluatorpolicy_ref— the bundle version that made the decision
The WAL is append-only and fsynced on each write.
Events cannot be deleted from the WAL; telemetry drain deletes only from the
SQLite priority buffer, not from the source WAL.
telemetry drain reads from the consumer cursor (telemetry.pos) and advances
that cursor only after successful delivery, providing at-least-once semantics.
Receivers should deduplicate by stable event identity (event_id).
To forward events to an OTLP collector:
autonomy telemetry drain --endpoint http://collector:4318
Error events drain first (PriorityHigh), decisions and actions second
(PriorityNormal), lifecycle events last (PriorityLow).