Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 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-sourceos-projections 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-sourceos-projections 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 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.
3 changes: 3 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,9 +94,11 @@ 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
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
Loading