Skip to content
Closed
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-supply-chain validate-release-bundle validate-package validate-cli validate-formula doctor probe
.PHONY: validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula doctor probe

PYTHON ?= python3
RUBY ?= ruby
Expand All @@ -15,12 +15,13 @@ READY_GRANT := examples/agent-registry-grant.active-activation.json
FAIL_POLICY := examples/policy-admission.missing.json
FAIL_GRANT := examples/agent-registry-grant.missing.json
RECEIPT_DIR := examples
POLICY_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-supply-chain validate-release-bundle validate-package validate-cli validate-formula
validate: validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula

validate-json:
$(PYTHON) scripts/validate-json.py
Expand Down Expand Up @@ -52,10 +53,16 @@ validate-evidence:
validate-governance:
$(PYTHON) scripts/validate-governance.py

validate-policy-fabric:
$(PYTHON) scripts/validate-policy-fabric.py
$(PYTHON) scripts/resolve-policy-admission.py $(LOCAL_AGENTPOD) --policy-dir $(POLICY_DIR) --expected-status allowed --deployment-receipt-id $(DEPLOYMENT_RECEIPT_ID) --agent-machine-id urn:srcos:agent-machine:m2-asahi-local --provider-id urn:srcos:agent-machine:inference-provider:asahi-llama-cpp --pretty >/tmp/agent-machine-policy-resolve-allowed.json
$(PYCLI) policy resolve $(LOCAL_AGENTPOD) --policy-dir $(POLICY_DIR) --expected-status denied --deployment-receipt-id $(DEPLOYMENT_RECEIPT_ID) --agent-machine-id urn:srcos:agent-machine:m2-asahi-local --provider-id urn:srcos:agent-machine:inference-provider:asahi-llama-cpp --pretty >/tmp/agent-machine-pycli-policy-resolve-denied.json

validate-activation:
$(PYTHON) scripts/validate-activation.py
$(PYTHON) scripts/evaluate-activation.py $(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-evaluate-activation-allowed.json
$(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
$(PYCLI) activate evaluate $(LOCAL_AGENTPOD) $(READY_GRANT) --policy-dir $(POLICY_DIR) --expected-status allowed --deployment-receipt-id $(DEPLOYMENT_RECEIPT_ID) --agent-machine-id urn:srcos:agent-machine:m2-asahi-local --provider-id urn:srcos:agent-machine:inference-provider:asahi-llama-cpp --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-pycli-resolved-policy-activation-allowed.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:
Expand All @@ -66,6 +73,9 @@ validate-release-bundle:
$(PYTHON) scripts/validate-release-bundle.py
$(PYTHON) scripts/generate-release-evidence.py --pretty >/tmp/agent-machine-release-evidence-bundle.json

validate-sourceos-projections:
$(PYTHON) scripts/validate-sourceos-projection-fixtures.py

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

Expand Down
7 changes: 6 additions & 1 deletion bin/agent-machine
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Usage:
agent-machine render receipt <agentpod.json> [--pretty] [--artifact-path <path>]
agent-machine render quadlet <agentpod.json> [--compare <file>]
agent-machine render k8s <agentpod.json> [--compare <file>]
agent-machine activate evaluate <agentpod.json> <policy.json> <grant.json> --deployment-receipt-id <id> [--storage-receipt-dir <dir>] [--storage-receipt-file <file>] [--pretty]
agent-machine policy resolve <agentpod.json> --policy-dir <dir> --deployment-receipt-id <id> [--expected-status allowed]
agent-machine activate evaluate <agentpod.json> [policy.json] <grant.json> --deployment-receipt-id <id> [--policy-dir <dir>] [--storage-receipt-dir <dir>] [--pretty]

This is the bootstrap CLI. It is intentionally conservative: it discovers host/runtime hints and never emits secrets, raw prompts, raw KV-cache contents, or credentials.
EOF
Expand Down Expand Up @@ -275,6 +276,10 @@ case "$COMMAND" in
shift || true
delegate_python_cli render "$@"
;;
policy)
shift || true
delegate_python_cli policy "$@"
;;
activate)
shift || true
delegate_python_cli activate "$@"
Expand Down
120 changes: 120 additions & 0 deletions docs/architecture/policy-admission-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# PolicyAdmission Resolution

Agent Machine now has a local Policy Fabric admission resolver for bootstrap and dry-run activation flows. This is a deterministic local-store resolver, not a production Policy Fabric client.

## Purpose

Activation evaluation should not require callers to manually pick a `PolicyAdmission` file forever. The resolver lets Agent Machine scan explicit files or directories and select the matching `PolicyAdmission` by request shape.

The resolver supports:

- explicit policy files;
- local policy store directories;
- request matching by AgentPod ID, request type, deployment receipt ID, AgentMachine ID, and provider ID;
- disambiguation by policy ID or expected status;
- fail-closed missing-admission stub generation;
- semantic validation through governance rules.

## Current commands

Resolve a policy decision from a local store:

```bash
agent-machine policy resolve \
examples/local-podman-llama-cpp.agent-pod.json \
--policy-dir examples \
--expected-status allowed \
--deployment-receipt-id urn:srcos:agent-machine:deployment-receipt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
--agent-machine-id urn:srcos:agent-machine:m2-asahi-local \
--provider-id urn:srcos:agent-machine:inference-provider:asahi-llama-cpp \
--pretty
```

Evaluate activation using a resolved policy from a local store:

```bash
agent-machine activate evaluate \
examples/local-podman-llama-cpp.agent-pod.json \
examples/agent-registry-grant.active-activation.json \
--policy-dir examples \
--expected-status allowed \
--deployment-receipt-id urn:srcos:agent-machine:deployment-receipt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
--agent-machine-id urn:srcos:agent-machine:m2-asahi-local \
--provider-id urn:srcos:agent-machine:inference-provider:asahi-llama-cpp \
--storage-receipt-dir examples \
--decided-at 2026-05-04T12:51:00Z \
--decision-id urn:srcos:agent-machine:activation-decision:local-llama-cpp-allowed \
--pretty
```

Evaluate activation with an explicit policy file:

```bash
agent-machine activate evaluate \
examples/local-podman-llama-cpp.agent-pod.json \
examples/policy-admission.allowed-activation.json \
examples/agent-registry-grant.active-activation.json \
--deployment-receipt-id urn:srcos:agent-machine:deployment-receipt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
--storage-receipt-dir examples \
--pretty
```

## Fail-closed behavior

If no matching policy is found and missing stubs are allowed, the resolver emits a synthetic `PolicyAdmission` with:

```text
decision.status = missing
decision.authorizationGranted = false
```

That stub denies activation-sensitive scopes and causes `ActivationDecision` to fail closed.

If `--no-missing-stub` is provided and no policy matches, resolution fails.

## Ambiguity behavior

If multiple policy decisions match the request, resolution fails unless the caller disambiguates with:

```text
--policy-id <id>
```

or:

```text
--expected-status allowed|denied|missing|not-required|unknown
```

This is deliberate. Silent selection among conflicting policy decisions would be unsafe.

## Bootstrap boundary

This resolver is not a production Policy Fabric client. It does not:

- call a remote Policy Fabric endpoint;
- verify policy bundle signatures;
- resolve revocations online;
- evaluate policy source code;
- prove freshness beyond the contents of local artifacts.

It is the bootstrap adapter shape that a real Policy Fabric client can replace.

## Validation

Policy resolver validation is part of:

```bash
make validate-policy-fabric
make validate
```

The validation path checks:

- directory scanning;
- schema and semantic validation;
- ambiguity rejection;
- allowed/denied disambiguation;
- generated missing stubs;
- CLI policy resolution;
- activation evaluation using a resolved local policy.
2 changes: 2 additions & 0 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. |
| [PolicyAdmission resolution](architecture/policy-admission-resolution.md) | Local Policy Fabric admission resolver and fail-closed missing-decision behavior. |
| [Image digest pinning and provenance](architecture/image-digest-pinning-and-provenance.md) | Supply-chain strict-mode gate for digest-pinned release-candidate artifacts. |
| [Release evidence bundle](architecture/release-evidence-bundle.md) | Deterministic validation/source/inventory/render/supply-chain/readiness bundle. |
| [Signed release bundle envelope](architecture/signed-release-bundle-envelope.md) | Signing envelope contract for release evidence bundles. |
Expand Down Expand Up @@ -93,6 +94,7 @@ validate-quadlet
validate-render
validate-evidence
validate-governance
validate-policy-fabric
validate-activation
validate-supply-chain
validate-release-bundle
Expand Down
17 changes: 17 additions & 0 deletions scripts/resolve-policy-admission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
"""Resolve PolicyAdmission from local Policy Fabric files/stores."""

from __future__ import annotations

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))

from agent_machine.policy_fabric import main # noqa: E402

if __name__ == "__main__":
raise SystemExit(main())
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.policy_fabric
import agent_machine.release_bundle
import agent_machine.supply_chain
import agent_machine.renderers.k8s
Expand Down Expand Up @@ -54,6 +55,8 @@ def main() -> int:
raise AssertionError("supply_chain.is_sha256_digest rejected valid digest")
if agent_machine.release_bundle.DEFAULT_REPOSITORY != "SourceOS-Linux/agent-machine":
raise AssertionError("unexpected release_bundle default repository")
if agent_machine.policy_fabric.DEFAULT_DECIDED_AT != "1970-01-01T00:00:00Z":
raise AssertionError("unexpected policy_fabric default decided_at")
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
116 changes: 116 additions & 0 deletions scripts/validate-policy-fabric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Validate local Policy Fabric admission resolution behavior."""

from __future__ import annotations

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_ROOT = REPO_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))

from agent_machine.policy_fabric import ( # noqa: E402
load_policy_admissions,
resolve_policy_admission,
validate_policy_admission_payload,
)

AGENTPOD_ID = "urn:srcos:agent-machine:agent-pod:local-podman-llama-cpp"
AGENT_MACHINE_ID = "urn:srcos:agent-machine:m2-asahi-local"
PROVIDER_ID = "urn:srcos:agent-machine:inference-provider:asahi-llama-cpp"
DEPLOYMENT_RECEIPT_ID = "urn:srcos:agent-machine:deployment-receipt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
DECIDED_AT = "2026-05-04T12:51:00Z"


def expect_status(policy: dict, expected: str, label: str) -> None:
observed = policy.get("decision", {}).get("status")
if observed != expected:
raise AssertionError(f"{label}: expected status={expected}, observed {observed}")
validate_policy_admission_payload(policy, REPO_ROOT, source=label)
print(f"VALID policy resolve {label} status={expected}")


def expect_ambiguous(policies: list[dict]) -> None:
try:
resolve_policy_admission(
policies=policies,
agentpod_id=AGENTPOD_ID,
request_type="activation",
deployment_receipt_id=DEPLOYMENT_RECEIPT_ID,
agent_machine_id=AGENT_MACHINE_ID,
provider_id=PROVIDER_ID,
allow_missing_stub=False,
root=REPO_ROOT,
)
except AssertionError as exc:
if "ambiguous PolicyAdmission" not in str(exc):
raise
print("VALID policy resolve ambiguous activation requires disambiguation")
return
raise AssertionError("expected ambiguous PolicyAdmission resolution to fail")


def main() -> int:
policies = load_policy_admissions(directories=[REPO_ROOT / "examples"], root=REPO_ROOT)
if len(policies) < 4:
raise AssertionError("expected at least four PolicyAdmission examples")

expect_ambiguous(policies)

allowed = resolve_policy_admission(
policies=policies,
agentpod_id=AGENTPOD_ID,
request_type="activation",
deployment_receipt_id=DEPLOYMENT_RECEIPT_ID,
agent_machine_id=AGENT_MACHINE_ID,
provider_id=PROVIDER_ID,
expected_status="allowed",
root=REPO_ROOT,
)
expect_status(allowed, "allowed", "allowed-activation")

denied = resolve_policy_admission(
policies=policies,
agentpod_id=AGENTPOD_ID,
request_type="activation",
deployment_receipt_id=DEPLOYMENT_RECEIPT_ID,
agent_machine_id=AGENT_MACHINE_ID,
provider_id=PROVIDER_ID,
expected_status="denied",
root=REPO_ROOT,
)
expect_status(denied, "denied", "denied-activation")

missing = resolve_policy_admission(
policies=policies,
agentpod_id=AGENTPOD_ID,
request_type="activation",
deployment_receipt_id=DEPLOYMENT_RECEIPT_ID,
agent_machine_id=AGENT_MACHINE_ID,
provider_id="urn:srcos:agent-machine:inference-provider:unresolved-provider",
allow_missing_stub=True,
decided_at=DECIDED_AT,
root=REPO_ROOT,
)
expect_status(missing, "missing", "generated-missing-stub")

by_id = resolve_policy_admission(
policies=policies,
agentpod_id=AGENTPOD_ID,
request_type="activation",
deployment_receipt_id=DEPLOYMENT_RECEIPT_ID,
policy_id="urn:srcos:agent-machine:policy-admission:allowed-loopback-activation",
root=REPO_ROOT,
)
expect_status(by_id, "allowed", "policy-id")
return 0


if __name__ == "__main__":
try:
raise SystemExit(main())
except (AssertionError, RuntimeError) as exc:
print(str(exc), file=sys.stderr)
raise SystemExit(1) from exc
Loading