Skip to content
Merged
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.PHONY: validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-package validate-cli validate-formula doctor probe
.PHONY: validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-supply-chain validate-package validate-cli validate-formula doctor probe

PYTHON ?= python3
RUBY ?= ruby
CLI := bin/agent-machine
BOOTSTRAP_CLI := sh $(CLI)
FORMULA := packaging/homebrew/Formula/agent-machine.rb
LOCAL_AGENTPOD := examples/local-podman-llama-cpp.agent-pod.json
PINNED_AGENTPOD := examples/local-podman-llama-cpp.pinned.agent-pod.json
K8S_AGENTPOD := examples/k8s-topolvm.agent-pod.json
LOCAL_QUADLET := deploy/quadlet/agent-machine-llama-cpp.container
K8S_MANIFEST := deploy/k8s/llama-cpp-topolvm-pod.yaml
Expand All @@ -17,8 +18,9 @@ RECEIPT_DIR := examples
DEPLOYMENT_RECEIPT_ID := urn:srcos:agent-machine:deployment-receipt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
DECIDED_AT := 2026-05-04T12:51:00Z
PYCLI := PYTHONPATH=src $(PYTHON) -m agent_machine.cli
PYMOD := PYTHONPATH=src $(PYTHON) -m

validate: validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-package validate-cli validate-formula
validate: validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-supply-chain validate-package validate-cli validate-formula

validate-json:
$(PYTHON) scripts/validate-json.py
Expand Down Expand Up @@ -56,6 +58,10 @@ validate-activation:
$(PYCLI) activate evaluate $(LOCAL_AGENTPOD) $(FAIL_POLICY) $(FAIL_GRANT) --deployment-receipt-id $(DEPLOYMENT_RECEIPT_ID) --storage-receipt-dir $(RECEIPT_DIR) --decided-at $(DECIDED_AT) --decision-id urn:srcos:agent-machine:activation-decision:local-llama-cpp-fail-closed --pretty >/tmp/agent-machine-pycli-evaluate-activation-fail-closed.json
$(BOOTSTRAP_CLI) activate evaluate $(LOCAL_AGENTPOD) $(READY_POLICY) $(READY_GRANT) --deployment-receipt-id $(DEPLOYMENT_RECEIPT_ID) --storage-receipt-dir $(RECEIPT_DIR) --decided-at $(DECIDED_AT) --decision-id urn:srcos:agent-machine:activation-decision:local-llama-cpp-allowed --pretty >/tmp/agent-machine-bootstrap-evaluate-activation-allowed.json

validate-supply-chain:
$(PYTHON) scripts/validate-supply-chain.py
$(PYMOD) agent_machine.supply_chain $(PINNED_AGENTPOD) --strict

validate-package:
$(PYTHON) scripts/validate-package.py

Expand Down
22 changes: 20 additions & 2 deletions contracts/agent-pod.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
},
"imageOrCommand": {
"type": "string",
"description": "Container image, systemd unit command, executable, or Kubernetes workload reference. Do not include secrets."
"description": "Container image, systemd unit command, executable, or Kubernetes workload reference. Do not include secrets. Release-candidate container images must be digest-pinned."
},
"entrypoint": { "type": "string" },
"args": {
Expand All @@ -108,7 +108,25 @@
"enum": ["never", "on-failure", "always", "policy-managed"]
},
"serviceAccountName": { "type": "string" },
"namespace": { "type": "string" }
"namespace": { "type": "string" },
"imageDigest": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$",
"description": "Resolved image digest for release-candidate/production validation."
},
"imageReferencePolicy": {
"type": "string",
"enum": ["tag-allowed-bootstrap", "digest-required", "digest-pinned"],
"description": "Bootstrap examples may allow tags; release-candidate and production artifacts must be digest-pinned."
},
"sbomRef": {
"type": "string",
"description": "Non-secret SBOM/provenance reference for the runtime image or artifact."
},
"provenanceRef": {
"type": "string",
"description": "Non-secret provenance reference for the runtime image or artifact."
}
}
},
"resources": {
Expand Down
132 changes: 132 additions & 0 deletions docs/architecture/image-digest-pinning-and-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Image Digest Pinning and Provenance Gate

Agent Machine treats mutable image tags as acceptable only for bootstrap examples and dry-run operator evaluation. Release-candidate and production deployment artifacts must be digest-pinned and must carry non-secret provenance/SBOM references where available.

## Decision

Bootstrap mode may allow mutable image tags when the AgentPod explicitly declares:

```text
runtime.imageReferencePolicy = tag-allowed-bootstrap
```

Strict mode requires:

```text
runtime.imageOrCommand contains @sha256:<digest>
OR runtime.imageDigest is sha256:<digest>

runtime.imageReferencePolicy is digest-required or digest-pinned
runtime.sbomRef is present
runtime.provenanceRef is present
```

Strict mode is a release-candidate gate, not a bootstrap convenience.

## Why this matters

Agent Machine is allowed to render local Quadlet files and Kubernetes manifests. If those artifacts use mutable tags in production-like flows, a later pull could resolve to different code. That would break deterministic evidence, invalidate receipt assumptions, and weaken Policy Fabric and Agent Registry decisions.

Digest pinning gives the receipt chain a stable artifact identity.

## Current implementation

Implemented now:

- optional AgentPod runtime fields:
- `imageDigest`;
- `imageReferencePolicy`;
- `sbomRef`;
- `provenanceRef`;
- package helper module:
- `src/agent_machine/supply_chain.py`;
- validation wrapper:
- `scripts/validate-supply-chain.py`;
- digest-pinned example:
- `examples/local-podman-llama-cpp.pinned.agent-pod.json`;
- Makefile stage:
- `validate-supply-chain`.

## Bootstrap mode behavior

Bootstrap mode validates basic consistency but permits mutable tags when explicitly marked.

Expected bootstrap example:

```json
{
"runtime": {
"imageOrCommand": "ghcr.io/ggerganov/llama.cpp:server",
"imageReferencePolicy": "tag-allowed-bootstrap"
}
}
```

If a mutable image has no explicit policy, the validator warns.

## Strict mode behavior

Strict mode fails unless digest/provenance requirements are satisfied.

Expected strict example:

```json
{
"runtime": {
"imageOrCommand": "ghcr.io/ggerganov/llama.cpp@sha256:1111111111111111111111111111111111111111111111111111111111111111",
"imageDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"imageReferencePolicy": "digest-pinned",
"sbomRef": "urn:srcos:sbom:llama-cpp-server-placeholder",
"provenanceRef": "urn:srcos:provenance:llama-cpp-server-placeholder"
}
}
```

Strict mode rejects:

- mutable image tags;
- missing image digest;
- malformed digest;
- disagreement between `imageOrCommand@sha256` and `runtime.imageDigest`;
- missing `imageReferencePolicy`;
- `tag-allowed-bootstrap` policy;
- missing SBOM reference;
- missing provenance reference.

## Validation commands

Bootstrap and strict validation together:

```bash
make validate-supply-chain
```

Direct strict validation:

```bash
PYTHONPATH=src python3 -m agent_machine.supply_chain \
examples/local-podman-llama-cpp.pinned.agent-pod.json \
--strict
```

## Release-gate rule

A release candidate may not use mutable image tags in activation-capable AgentPods.

Production-ready Agent Machine must additionally connect image digests to:

- DeploymentReceipt artifacts;
- ActivationDecision inputs;
- AgentPlaneRuntimeEvidence artifacts;
- SBOM/provenance references;
- future signed release evidence bundles.

## Non-goals for this bootstrap gate

- Verifying signatures online.
- Pulling container images.
- Resolving mutable tags to real digests.
- Generating SBOMs.
- Submitting provenance to a transparency log.

Those are production-hardening tasks. This bootstrap gate ensures the contract and validator can already distinguish mutable bootstrap examples from digest-pinned release candidates.
6 changes: 4 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Agent Machine is a bootstrap runtime-control substrate for SourceOS agent worklo
| [AgentPod manifest generation](architecture/agentpod-manifest-generation.md) | Contract-to-plan-to-manifest generation rules. |
| [Deployment safety](architecture/deployment-safety.md) | Skeleton-vs-production manifest rules and safety gates. |
| [Receipt chain](architecture/receipt-chain.md) | AgentPod source to plan, manifest, receipt, policy, registry, and AgentPlane evidence. |
| [Image digest pinning and provenance](architecture/image-digest-pinning-and-provenance.md) | Supply-chain strict-mode gate for digest-pinned release-candidate artifacts. |
| [Runtime package layout](architecture/runtime-package-layout.md) | Migration from loose scripts to `src/agent_machine/` package modules. |
| [Homebrew Python dependencies](architecture/homebrew-python-dependencies.md) | Current dependency strategy for render/evaluation commands. |
| [Local LVM and TopoLVM profile](architecture/local-lvm-and-topolvm-profile.md) | Local and Kubernetes storage/cache/evidence profile. |
Expand Down Expand Up @@ -89,6 +90,7 @@ validate-render
validate-evidence
validate-governance
validate-activation
validate-supply-chain
validate-package
validate-cli
validate-formula
Expand All @@ -100,8 +102,8 @@ Agent Machine remains production-blocked by design until the release gate passes

Current blockers:

- visible green CI run;
- image digest pinning and provenance gate;
- main-branch CI visibility and branch protection;
- real image digest pinning/provenance from trusted build artifacts;
- real Policy Fabric client or endpoint;
- real Agent Registry grant resolver;
- real AgentPlane evidence submission/staging client;
Expand Down
1 change: 1 addition & 0 deletions examples/k8s-topolvm.agent-pod.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"runtime": {
"mode": "kubernetes",
"imageOrCommand": "ghcr.io/ggerganov/llama.cpp:server",
"imageReferencePolicy": "tag-allowed-bootstrap",
"entrypoint": "llama-server",
"args": [
"--host",
Expand Down
1 change: 1 addition & 0 deletions examples/local-podman-llama-cpp.agent-pod.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"runtime": {
"mode": "podman-quadlet",
"imageOrCommand": "ghcr.io/ggerganov/llama.cpp:server",
"imageReferencePolicy": "tag-allowed-bootstrap",
"entrypoint": "llama-server",
"args": [
"--host",
Expand Down
114 changes: 114 additions & 0 deletions examples/local-podman-llama-cpp.pinned.agent-pod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"specVersion": "0.1.0",
"id": "urn:srcos:agent-machine:agent-pod:local-podman-llama-cpp-pinned",
"kind": "AgentPod",
"podType": "local-podman-quadlet",
"profile": "m2-asahi-linux",
"workload": {
"name": "local-llama-cpp-provider-pinned",
"purpose": "inference-provider",
"description": "Digest-pinned local llama.cpp provider example for release-candidate supply-chain validation.",
"agentIdentityRequired": true,
"agentIdentityRef": "urn:srcos:agent:local-inference-provider",
"workroomRef": "urn:srcos:workroom:local-default",
"topicRef": "urn:srcos:topic:agent-machine"
},
"runtime": {
"mode": "podman-quadlet",
"imageOrCommand": "ghcr.io/ggerganov/llama.cpp@sha256:1111111111111111111111111111111111111111111111111111111111111111",
"imageDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"imageReferencePolicy": "digest-pinned",
"sbomRef": "urn:srcos:sbom:llama-cpp-server-placeholder",
"provenanceRef": "urn:srcos:provenance:llama-cpp-server-placeholder",
"entrypoint": "llama-server",
"args": [
"--host",
"127.0.0.1",
"--port",
"8080"
],
"networkMode": "loopback",
"restartPolicy": "policy-managed"
},
"resources": {
"cpu": {
"request": "2",
"limit": "6"
},
"memoryBytes": {
"request": 8589934592,
"limit": 25769803776
},
"accelerators": [
{
"type": "cpu",
"required": true,
"count": 1
},
{
"type": "vulkan",
"required": false,
"count": 1
}
]
},
"storage": [
{
"cacheTierId": "urn:srcos:agent-machine:cache-tier:linux-workstation-warm-cache",
"volumeClass": "agent-models",
"mountPath": "/models",
"hostPath": "/var/lib/agent-machine/models/gguf",
"accessMode": "read-only",
"sensitive": true
},
{
"cacheTierId": "urn:srcos:agent-machine:cache-tier:linux-workstation-warm-cache",
"volumeClass": "agent-cache-warm",
"mountPath": "/agent/cache/warm",
"hostPath": "/var/lib/agent-machine/cache/warm",
"accessMode": "runtime-managed",
"sensitive": true
},
{
"cacheTierId": "urn:srcos:agent-machine:cache-tier:linux-workstation-warm-cache",
"volumeClass": "agent-evidence",
"mountPath": "/agent/evidence",
"hostPath": "/var/lib/agent-machine/evidence",
"accessMode": "read-write",
"sensitive": false
}
],
"ports": [
{
"name": "openai-compatible-http",
"containerPort": 8080,
"hostPort": 8080,
"exposure": "loopback"
}
],
"policy": {
"policyFabricRequired": true,
"agentRegistryRequired": true,
"networkPolicyRequired": true,
"egressPolicyRequired": true,
"allowPrivileged": false,
"allowHostNetwork": false,
"allowSecretInSpec": false,
"toolUseRequiresGrant": true,
"cacheReuseRequiresPolicy": true
},
"receipts": {
"required": true,
"emitPlacement": true,
"emitStorage": true,
"emitRuntime": true,
"includeRawContent": false
},
"labels": {
"sourceos.profile": "m2-asahi-linux",
"sourceos.runtime": "podman-quadlet",
"sourceos.provider": "llama.cpp",
"sourceos.exposure": "loopback-only",
"sourceos.supply-chain": "digest-pinned-placeholder"
}
}
3 changes: 3 additions & 0 deletions scripts/validate-package.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def main() -> int:
import agent_machine.cli
import agent_machine.evidence
import agent_machine.governance
import agent_machine.supply_chain
import agent_machine.renderers.k8s
import agent_machine.renderers.plan
import agent_machine.renderers.quadlet
Expand Down Expand Up @@ -46,6 +47,8 @@ def main() -> int:
raise AssertionError("stable_digest must be key-order independent")
if not stable_text_digest("agent-machine").startswith("sha256:"):
raise AssertionError("stable_text_digest must return sha256: prefixed digest")
if not agent_machine.supply_chain.is_sha256_digest("sha256:" + "a" * 64):
raise AssertionError("supply_chain.is_sha256_digest rejected valid digest")
if str(default_model_cache_path()) != "/var/lib/agent-machine/models":
raise AssertionError("unexpected default model cache path")
if str(default_evidence_path()) != "/var/lib/agent-machine/evidence":
Expand Down
Loading
Loading