From c5a3135b8e55868c40fdcf84f41ebd6a29fb6d57 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 10:47:00 -0400 Subject: [PATCH 1/9] Add local Agent Registry grant resolver --- src/agent_machine/agent_registry.py | 344 ++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 src/agent_machine/agent_registry.py diff --git a/src/agent_machine/agent_registry.py b/src/agent_machine/agent_registry.py new file mode 100644 index 0000000..258965d --- /dev/null +++ b/src/agent_machine/agent_registry.py @@ -0,0 +1,344 @@ +"""Local Agent Registry grant resolver for Agent Machine. + +This module is a bootstrap stand-in for a real Agent Registry client. It resolves +secret-free AgentRegistryGrant artifacts from explicit files or local stores and +can produce a fail-closed missing-grant stub when no grant is present. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +from agent_machine.contracts import load_json, schema_by_kind +from agent_machine.governance import GRANT_SCOPE_KEYS, validate_agent_registry_grant_semantics + +DEFAULT_ISSUED_AT = "1970-01-01T00:00:00Z" +EMPTY_SCOPE = { + "providerIds": [], + "modelRefs": [], + "toolRefs": [], + "cacheScopeRefs": [], + "memoryScopeRefs": [], + "storageScopeRefs": [], + "evidenceScopeRefs": [], +} + + +def validate_payload_against_kind(value: dict[str, Any], kind: str, root: Path | None = None) -> None: + schema_path = schema_by_kind(root)[kind] + schema_payload = load_json(schema_path) + try: + from jsonschema.validators import validator_for + except ImportError as exc: # pragma: no cover + raise RuntimeError( + "Missing dependency: jsonschema. Install with `python -m pip install -r requirements-dev.txt`." + ) from exc + validator_cls = validator_for(schema_payload) + validator_cls.check_schema(schema_payload) + validator = validator_cls(schema_payload) + errors = sorted(validator.iter_errors(value), key=lambda err: list(err.path)) + if errors: + rendered = [] + for err in errors: + location = "/".join(str(part) for part in err.path) or "" + rendered.append(f" - {location}: {err.message}") + raise AssertionError(f"{kind} failed schema validation:\n" + "\n".join(rendered)) + + +def validate_agent_registry_grant_payload(grant: dict[str, Any], root: Path | None = None, source: str = "") -> None: + validate_payload_against_kind(grant, "AgentRegistryGrant", root) + validate_agent_registry_grant_semantics(grant, source=source) + + +def iter_json_files(directory: Path) -> list[Path]: + if not directory.exists(): + raise AssertionError(f"grant store directory does not exist: {directory}") + if not directory.is_dir(): + raise AssertionError(f"grant store path is not a directory: {directory}") + return sorted(path for path in directory.rglob("*.json") if path.is_file()) + + +def load_agent_registry_grants( + *, + files: list[Path] | None = None, + directories: list[Path] | None = None, + root: Path | None = None, +) -> list[dict[str, Any]]: + """Load AgentRegistryGrant objects from files and/or local store directories.""" + grants: list[dict[str, Any]] = [] + seen_paths: set[Path] = set() + + for path in files or []: + resolved = path.resolve() + seen_paths.add(resolved) + value = load_json(path) + if not isinstance(value, dict): + raise AssertionError(f"{path}: agent registry grant file root must be an object") + if value.get("kind") != "AgentRegistryGrant": + raise AssertionError(f"{path}: expected kind=AgentRegistryGrant") + validate_agent_registry_grant_payload(value, root, source=str(path)) + grants.append(value) + + for directory in directories or []: + for path in iter_json_files(directory): + resolved = path.resolve() + if resolved in seen_paths: + continue + value = load_json(path) + if isinstance(value, dict) and value.get("kind") == "AgentRegistryGrant": + validate_agent_registry_grant_payload(value, root, source=str(path)) + grants.append(value) + seen_paths.add(resolved) + + grant_ids: dict[str, dict[str, Any]] = {} + for grant in grants: + grant_id = grant.get("id") + if not isinstance(grant_id, str): + raise AssertionError("AgentRegistryGrant loaded without string id") + if grant_id in grant_ids: + raise AssertionError(f"duplicate AgentRegistryGrant id loaded: {grant_id}") + grant_ids[grant_id] = grant + return grants + + +def request_matches( + grant: dict[str, Any], + *, + agentpod_id: str, + requested_agent_identity_ref: str, + session_ref: str, + agent_machine_id: str | None = None, + workroom_ref: str | None = None, + topic_ref: str | None = None, +) -> bool: + request = grant.get("request", {}) + if request.get("agentPodId") != agentpod_id: + return False + if request.get("requestedAgentIdentityRef") != requested_agent_identity_ref: + return False + if request.get("sessionRef") != session_ref: + return False + if agent_machine_id and request.get("agentMachineId") != agent_machine_id: + return False + if workroom_ref and request.get("workroomRef") != workroom_ref: + return False + if topic_ref and request.get("topicRef") != topic_ref: + return False + return True + + +def requested_scope_from_inputs( + *, + provider_id: str | None = None, + model_ref: str | None = None, + tool_refs: list[str] | None = None, + storage_scope_ref: str | None = None, + evidence_scope_ref: str | None = None, +) -> dict[str, list[str]]: + scope = {key: [] for key in GRANT_SCOPE_KEYS} + if provider_id: + scope["providerIds"].append(provider_id) + if model_ref: + scope["modelRefs"].append(model_ref) + for tool_ref in tool_refs or []: + if tool_ref not in scope["toolRefs"]: + scope["toolRefs"].append(tool_ref) + if storage_scope_ref: + scope["storageScopeRefs"].append(storage_scope_ref) + if evidence_scope_ref: + scope["evidenceScopeRefs"].append(evidence_scope_ref) + return scope + + +def missing_agent_registry_grant_stub( + *, + agentpod_id: str, + requested_agent_identity_ref: str, + session_ref: str, + issued_at: str, + agent_machine_id: str | None = None, + workroom_ref: str | None = None, + topic_ref: str | None = None, + requested_scope: dict[str, list[str]] | None = None, + requested_expires_at: str | None = None, +) -> dict[str, Any]: + suffix = agentpod_id.split(":")[-1] + scope = {key: list((requested_scope or EMPTY_SCOPE).get(key) or []) for key in GRANT_SCOPE_KEYS} + return { + "specVersion": "0.1.0", + "id": f"urn:srcos:agent-machine:agent-registry-grant:missing-{suffix}", + "kind": "AgentRegistryGrant", + "request": { + "requestId": f"urn:srcos:agent-machine:grant-request:missing-{suffix}", + "agentMachineId": agent_machine_id, + "agentPodId": agentpod_id, + "requestedAgentIdentityRef": requested_agent_identity_ref, + "sessionRef": session_ref, + "workroomRef": workroom_ref, + "topicRef": topic_ref, + "requestedScope": scope, + "requestedExpiresAt": requested_expires_at, + }, + "grant": { + "status": "missing", + "authorizationGranted": False, + "grantRef": None, + "grantDigest": None, + "reason": "No matching AgentRegistryGrant was resolved; activation must fail closed.", + "expiresAt": None, + "revocationStatus": "unavailable", + "revocationRef": None, + "revocationHookRef": None, + "externalTrustSignals": [], + }, + "scope": { + "allowed": {key: [] for key in GRANT_SCOPE_KEYS}, + "denied": scope, + }, + "receiptSafety": { + "includeRawContent": False, + "rawPromptContentIncluded": False, + "rawKvCacheContentIncluded": False, + "secretValuesIncluded": False, + "privateMemoryIncluded": False, + }, + "issuedAt": issued_at, + "labels": { + "sourceos.registry.resolver": "local-store", + "sourceos.activation.fail-closed": "true", + }, + } + + +def resolve_agent_registry_grant( + *, + grants: list[dict[str, Any]], + agentpod_id: str, + requested_agent_identity_ref: str, + session_ref: str, + agent_machine_id: str | None = None, + workroom_ref: str | None = None, + topic_ref: str | None = None, + grant_id: str | None = None, + expected_status: str | None = None, + allow_missing_stub: bool = True, + issued_at: str = DEFAULT_ISSUED_AT, + requested_scope: dict[str, list[str]] | None = None, + requested_expires_at: str | None = None, + root: Path | None = None, +) -> dict[str, Any]: + """Resolve one AgentRegistryGrant or return a fail-closed missing stub. + + Ambiguity is a hard failure. A caller may disambiguate by grant_id or expected_status. + """ + if grant_id: + matches = [grant for grant in grants if grant.get("id") == grant_id] + else: + matches = [ + grant + for grant in grants + if request_matches( + grant, + agentpod_id=agentpod_id, + requested_agent_identity_ref=requested_agent_identity_ref, + session_ref=session_ref, + agent_machine_id=agent_machine_id, + workroom_ref=workroom_ref, + topic_ref=topic_ref, + ) + ] + if expected_status: + matches = [grant for grant in matches if grant.get("grant", {}).get("status") == expected_status] + + if len(matches) == 1: + validate_agent_registry_grant_payload(matches[0], root, source=str(matches[0].get("id"))) + return matches[0] + if not matches: + if not allow_missing_stub: + raise AssertionError("no matching AgentRegistryGrant found") + stub = missing_agent_registry_grant_stub( + agentpod_id=agentpod_id, + requested_agent_identity_ref=requested_agent_identity_ref, + session_ref=session_ref, + agent_machine_id=agent_machine_id, + workroom_ref=workroom_ref, + topic_ref=topic_ref, + requested_scope=requested_scope, + requested_expires_at=requested_expires_at, + issued_at=issued_at, + ) + validate_agent_registry_grant_payload(stub, root, source="missing-grant-stub") + return stub + + ids = ", ".join(sorted(str(grant.get("id")) for grant in matches)) + raise AssertionError(f"ambiguous AgentRegistryGrant match; disambiguate with grant_id or expected_status: {ids}") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Resolve AgentRegistryGrant from local Agent Registry files/stores") + parser.add_argument("agentpod_json", type=Path) + parser.add_argument("--grant-file", action="append", type=Path, default=[]) + parser.add_argument("--grant-dir", action="append", type=Path, default=[]) + parser.add_argument("--requested-agent-identity-ref", required=True) + parser.add_argument("--session-ref", required=True) + parser.add_argument("--agent-machine-id") + parser.add_argument("--workroom-ref") + parser.add_argument("--topic-ref") + parser.add_argument("--grant-id") + parser.add_argument("--expected-status", choices=["missing", "active", "expired", "revoked", "denied", "unknown"]) + parser.add_argument("--no-missing-stub", action="store_true") + parser.add_argument("--provider-id") + parser.add_argument("--model-ref") + parser.add_argument("--tool-ref", action="append", default=[]) + parser.add_argument("--storage-scope-ref") + parser.add_argument("--evidence-scope-ref") + parser.add_argument("--requested-expires-at") + parser.add_argument("--issued-at", default=DEFAULT_ISSUED_AT) + parser.add_argument("--pretty", action="store_true") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + agentpod = load_json(args.agentpod_json) + if not isinstance(agentpod, dict) or agentpod.get("kind") != "AgentPod": + raise AssertionError(f"{args.agentpod_json}: expected kind=AgentPod") + grants = load_agent_registry_grants(files=args.grant_file, directories=args.grant_dir) + grant = resolve_agent_registry_grant( + grants=grants, + agentpod_id=str(agentpod.get("id")), + requested_agent_identity_ref=args.requested_agent_identity_ref, + session_ref=args.session_ref, + agent_machine_id=args.agent_machine_id, + workroom_ref=args.workroom_ref, + topic_ref=args.topic_ref, + grant_id=args.grant_id, + expected_status=args.expected_status, + allow_missing_stub=not args.no_missing_stub, + requested_scope=requested_scope_from_inputs( + provider_id=args.provider_id, + model_ref=args.model_ref, + tool_refs=args.tool_ref, + storage_scope_ref=args.storage_scope_ref, + evidence_scope_ref=args.evidence_scope_ref, + ), + requested_expires_at=args.requested_expires_at, + issued_at=args.issued_at, + ) + if args.pretty: + print(json.dumps(grant, indent=2, sort_keys=True)) + else: + print(json.dumps(grant, sort_keys=True, separators=(",", ":"))) + 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 From 8e4c54208c84f8065f5717724c760ee40c975bca Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 10:54:36 -0400 Subject: [PATCH 2/9] Add AgentRegistryGrant resolver wrapper --- scripts/resolve-agent-registry-grant.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 scripts/resolve-agent-registry-grant.py diff --git a/scripts/resolve-agent-registry-grant.py b/scripts/resolve-agent-registry-grant.py new file mode 100644 index 0000000..c5349d7 --- /dev/null +++ b/scripts/resolve-agent-registry-grant.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +"""Resolve AgentRegistryGrant from local Agent Registry 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.agent_registry import main # noqa: E402 + +if __name__ == "__main__": + raise SystemExit(main()) From 849718329c41628244a08755a3b73ffe9e9a7705 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 10:58:27 -0400 Subject: [PATCH 3/9] Add Agent Registry resolver validation wrapper --- scripts/validate-agent-registry.py | 128 +++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 scripts/validate-agent-registry.py diff --git a/scripts/validate-agent-registry.py b/scripts/validate-agent-registry.py new file mode 100644 index 0000000..3f76e6b --- /dev/null +++ b/scripts/validate-agent-registry.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Validate local Agent Registry grant 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.agent_registry import ( # noqa: E402 + load_agent_registry_grants, + requested_scope_from_inputs, + resolve_agent_registry_grant, + validate_agent_registry_grant_payload, +) + +AGENTPOD_ID = "urn:srcos:agent-machine:agent-pod:local-podman-llama-cpp" +AGENT_MACHINE_ID = "urn:srcos:agent-machine:m2-asahi-local" +IDENTITY_REF = "urn:srcos:agent:local-inference-provider" +SESSION_REF = "urn:srcos:session:local-bootstrap" +WORKROOM_REF = "urn:srcos:workroom:local-default" +TOPIC_REF = "urn:srcos:topic:agent-machine" +PROVIDER_ID = "urn:srcos:agent-machine:inference-provider:asahi-llama-cpp" +ISSUED_AT = "2026-05-04T12:51:00Z" + + +def expect_status(grant: dict, expected: str, label: str) -> None: + observed = grant.get("grant", {}).get("status") + if observed != expected: + raise AssertionError(f"{label}: expected status={expected}, observed {observed}") + validate_agent_registry_grant_payload(grant, REPO_ROOT, source=label) + print(f"VALID registry resolve {label} status={expected}") + + +def expect_ambiguous(grants: list[dict]) -> None: + try: + resolve_agent_registry_grant( + grants=grants, + agentpod_id=AGENTPOD_ID, + requested_agent_identity_ref=IDENTITY_REF, + session_ref=SESSION_REF, + agent_machine_id=AGENT_MACHINE_ID, + workroom_ref=WORKROOM_REF, + topic_ref=TOPIC_REF, + allow_missing_stub=False, + root=REPO_ROOT, + ) + except AssertionError as exc: + if "ambiguous AgentRegistryGrant" not in str(exc): + raise + print("VALID registry resolve ambiguous grant requires disambiguation") + return + raise AssertionError("expected ambiguous AgentRegistryGrant resolution to fail") + + +def main() -> int: + grants = load_agent_registry_grants(directories=[REPO_ROOT / "examples"], root=REPO_ROOT) + if len(grants) < 4: + raise AssertionError("expected at least four AgentRegistryGrant examples") + + expect_ambiguous(grants) + + active_activation = resolve_agent_registry_grant( + grants=grants, + agentpod_id=AGENTPOD_ID, + requested_agent_identity_ref=IDENTITY_REF, + session_ref=SESSION_REF, + agent_machine_id=AGENT_MACHINE_ID, + workroom_ref=WORKROOM_REF, + topic_ref=TOPIC_REF, + grant_id="urn:srcos:agent-machine:agent-registry-grant:active-loopback-activation", + root=REPO_ROOT, + ) + expect_status(active_activation, "active", "active-activation") + + revoked = resolve_agent_registry_grant( + grants=grants, + agentpod_id=AGENTPOD_ID, + requested_agent_identity_ref=IDENTITY_REF, + session_ref=SESSION_REF, + agent_machine_id=AGENT_MACHINE_ID, + workroom_ref=WORKROOM_REF, + topic_ref=TOPIC_REF, + expected_status="revoked", + root=REPO_ROOT, + ) + expect_status(revoked, "revoked", "revoked") + + missing = resolve_agent_registry_grant( + grants=grants, + agentpod_id=AGENTPOD_ID, + requested_agent_identity_ref="urn:srcos:agent:unresolved-provider", + session_ref=SESSION_REF, + agent_machine_id=AGENT_MACHINE_ID, + workroom_ref=WORKROOM_REF, + topic_ref=TOPIC_REF, + allow_missing_stub=True, + requested_scope=requested_scope_from_inputs( + provider_id=PROVIDER_ID, + tool_refs=["urn:srcos:tool:start-provider", "urn:srcos:tool:mount-cache"], + ), + issued_at=ISSUED_AT, + root=REPO_ROOT, + ) + expect_status(missing, "missing", "generated-missing-stub") + + by_id = resolve_agent_registry_grant( + grants=grants, + agentpod_id=AGENTPOD_ID, + requested_agent_identity_ref=IDENTITY_REF, + session_ref=SESSION_REF, + grant_id="urn:srcos:agent-machine:agent-registry-grant:active-render-only", + root=REPO_ROOT, + ) + expect_status(by_id, "active", "grant-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 From aa0149acf5a64457633c49380034641b490257aa Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 13:45:59 -0400 Subject: [PATCH 4/9] Add Python CLI Agent Registry resolver command --- src/agent_machine/cli.py | 159 ++++++++++++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 29 deletions(-) diff --git a/src/agent_machine/cli.py b/src/agent_machine/cli.py index 7cdd4fb..2666531 100644 --- a/src/agent_machine/cli.py +++ b/src/agent_machine/cli.py @@ -285,35 +285,44 @@ def cmd_policy_resolve(args: argparse.Namespace) -> int: return 0 -def resolve_activation_policy_and_grant(args: argparse.Namespace, agentpod: dict[str, Any], policy_fabric: Any) -> tuple[dict[str, Any], dict[str, Any]]: - """Resolve activation policy/grant from explicit files or local policy store.""" - policy_json = args.policy_json - grant_json = args.grant_json - - # Backward-compatible shorthand: - # agent-machine activate evaluate --policy-dir examples ... - # argparse first assigns the single optional positional to policy_json, so we - # reinterpret it as grant_json when a policy store/resolver option is present. - resolver_requested = bool(args.policy_file or args.policy_dir or args.policy_id or args.expected_status) - if grant_json is None and policy_json is not None and resolver_requested: - grant_json = policy_json - policy_json = None - - if grant_json is None: - raise AssertionError( - "grant JSON is required. Use either ` ` " - "or ` --policy-dir `" - ) - - if policy_json is not None: - return load_json(policy_json), load_json(grant_json) - - policies = policy_fabric.load_policy_admissions( - files=args.policy_file, - directories=args.policy_dir, +def cmd_registry_resolve(args: argparse.Namespace) -> int: + agent_registry = import_renderer(lambda: __import__("agent_machine.agent_registry", fromlist=["_unused"])) + agentpod = load_json(args.agentpod_json) + grants = agent_registry.load_agent_registry_grants(files=args.grant_file, directories=args.grant_dir, root=REPO_ROOT) + grant = agent_registry.resolve_agent_registry_grant( + grants=grants, + agentpod_id=str(agentpod.get("id")), + requested_agent_identity_ref=args.requested_agent_identity_ref, + session_ref=args.session_ref, + agent_machine_id=args.agent_machine_id, + workroom_ref=args.workroom_ref, + topic_ref=args.topic_ref, + grant_id=args.grant_id, + expected_status=args.expected_status, + allow_missing_stub=not args.no_missing_stub, + issued_at=args.issued_at, + requested_expires_at=args.requested_expires_at, + requested_scope=agent_registry.requested_scope_from_inputs( + provider_id=args.provider_id, + model_ref=args.model_ref, + tool_refs=args.tool_ref, + storage_scope_ref=args.storage_scope_ref, + evidence_scope_ref=args.evidence_scope_ref, + ), root=REPO_ROOT, ) - policy = policy_fabric.resolve_policy_admission( + if args.pretty: + print(json.dumps(grant, indent=2, sort_keys=True)) + else: + print(json.dumps(grant, sort_keys=True, separators=(",", ":"))) + return 0 + + +def resolve_activation_policy(args: argparse.Namespace, agentpod: dict[str, Any], policy_fabric: Any) -> dict[str, Any]: + if args.policy_json is not None: + return load_json(args.policy_json) + policies = policy_fabric.load_policy_admissions(files=args.policy_file, directories=args.policy_dir, root=REPO_ROOT) + return policy_fabric.resolve_policy_admission( policies=policies, agentpod_id=str(agentpod.get("id")), request_type="activation", @@ -326,14 +335,63 @@ def resolve_activation_policy_and_grant(args: argparse.Namespace, agentpod: dict decided_at=args.decided_at, root=REPO_ROOT, ) - return policy, load_json(grant_json) + + +def resolve_activation_grant(args: argparse.Namespace, agentpod: dict[str, Any], agent_registry: Any) -> dict[str, Any]: + if args.grant_json is not None: + return load_json(args.grant_json) + grants = agent_registry.load_agent_registry_grants(files=args.grant_file, directories=args.grant_dir, root=REPO_ROOT) + return agent_registry.resolve_agent_registry_grant( + grants=grants, + agentpod_id=str(agentpod.get("id")), + requested_agent_identity_ref=args.requested_agent_identity_ref, + session_ref=args.session_ref, + agent_machine_id=args.agent_machine_id, + workroom_ref=args.workroom_ref, + topic_ref=args.topic_ref, + grant_id=args.grant_id, + expected_status=args.grant_expected_status, + allow_missing_stub=not args.no_missing_grant_stub, + issued_at=args.issued_at, + requested_expires_at=args.requested_expires_at, + requested_scope=agent_registry.requested_scope_from_inputs( + provider_id=args.provider_id, + model_ref=args.model_ref, + tool_refs=args.tool_ref, + storage_scope_ref=args.storage_scope_ref, + evidence_scope_ref=args.evidence_scope_ref, + ), + root=REPO_ROOT, + ) + + +def normalize_activation_positionals(args: argparse.Namespace) -> None: + """Preserve old shorthand while supporting policy/grant store resolution.""" + policy_resolver_requested = bool(args.policy_file or args.policy_dir or args.policy_id or args.expected_status) + grant_resolver_requested = bool(args.grant_file or args.grant_dir or args.grant_id or args.grant_expected_status) + if args.grant_json is None and args.policy_json is not None and policy_resolver_requested and not grant_resolver_requested: + args.grant_json = args.policy_json + args.policy_json = None + if args.grant_json is None and not grant_resolver_requested: + raise AssertionError( + "grant JSON is required. Use ` `, " + "` --policy-dir `, or provide --grant-dir/--grant-file." + ) + if args.policy_json is None and not policy_resolver_requested: + raise AssertionError( + "policy JSON is required. Use ` ` " + "or provide --policy-dir/--policy-file." + ) def cmd_activate_evaluate(args: argparse.Namespace) -> int: activation = import_renderer(lambda: __import__("agent_machine.activation", fromlist=["_unused"])) policy_fabric = import_renderer(lambda: __import__("agent_machine.policy_fabric", fromlist=["_unused"])) + agent_registry = import_renderer(lambda: __import__("agent_machine.agent_registry", fromlist=["_unused"])) agentpod = load_json(args.agentpod_json) - policy, grant = resolve_activation_policy_and_grant(args, agentpod, policy_fabric) + normalize_activation_positionals(args) + policy = resolve_activation_policy(args, agentpod, policy_fabric) + grant = resolve_activation_grant(args, agentpod, agent_registry) storage_receipts = activation.load_storage_receipts( files=args.storage_receipt_file, directories=args.storage_receipt_dir, @@ -360,6 +418,27 @@ def cmd_activate_evaluate(args: argparse.Namespace) -> int: return 0 +def add_registry_resolver_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--grant-file", action="append", type=Path, default=[]) + parser.add_argument("--grant-dir", action="append", type=Path, default=[]) + parser.add_argument("--requested-agent-identity-ref", required=True) + parser.add_argument("--session-ref", required=True) + parser.add_argument("--agent-machine-id") + parser.add_argument("--workroom-ref") + parser.add_argument("--topic-ref") + parser.add_argument("--grant-id") + parser.add_argument("--expected-status", choices=["missing", "active", "expired", "revoked", "denied", "unknown"]) + parser.add_argument("--no-missing-stub", action="store_true") + parser.add_argument("--provider-id") + parser.add_argument("--model-ref") + parser.add_argument("--tool-ref", action="append", default=[]) + parser.add_argument("--storage-scope-ref") + parser.add_argument("--evidence-scope-ref") + parser.add_argument("--requested-expires-at") + parser.add_argument("--issued-at", default="1970-01-01T00:00:00Z") + parser.add_argument("--pretty", action="store_true") + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Agent Machine Python CLI") subcommands = parser.add_subparsers(dest="command", required=True) @@ -421,6 +500,13 @@ def build_parser() -> argparse.ArgumentParser: policy_resolve.add_argument("--pretty", action="store_true") policy_resolve.set_defaults(func=cmd_policy_resolve) + registry = subcommands.add_parser("registry", help="Resolve Agent Registry grant artifacts") + registry_subcommands = registry.add_subparsers(dest="registry_command", required=True) + registry_resolve = registry_subcommands.add_parser("resolve", help="Resolve an AgentRegistryGrant from local files/stores") + registry_resolve.add_argument("agentpod_json", type=Path) + add_registry_resolver_args(registry_resolve) + registry_resolve.set_defaults(func=cmd_registry_resolve) + activate = subcommands.add_parser("activate", help="Evaluate activation readiness") activate_subcommands = activate.add_subparsers(dest="activate_command", required=True) activate_evaluate = activate_subcommands.add_parser("evaluate", help="Evaluate AgentPod activation decision") @@ -433,8 +519,23 @@ def build_parser() -> argparse.ArgumentParser: activate_evaluate.add_argument("--policy-id") activate_evaluate.add_argument("--expected-status", choices=["missing", "allowed", "denied", "not-required", "unknown"]) activate_evaluate.add_argument("--no-missing-stub", action="store_true") + activate_evaluate.add_argument("--grant-file", action="append", type=Path, default=[]) + activate_evaluate.add_argument("--grant-dir", action="append", type=Path, default=[]) + activate_evaluate.add_argument("--grant-id") + activate_evaluate.add_argument("--grant-expected-status", choices=["missing", "active", "expired", "revoked", "denied", "unknown"]) + activate_evaluate.add_argument("--no-missing-grant-stub", action="store_true") + activate_evaluate.add_argument("--requested-agent-identity-ref", default="urn:srcos:agent:local-inference-provider") + activate_evaluate.add_argument("--session-ref", default="urn:srcos:session:local-bootstrap") + activate_evaluate.add_argument("--workroom-ref") + activate_evaluate.add_argument("--topic-ref") activate_evaluate.add_argument("--agent-machine-id") activate_evaluate.add_argument("--provider-id") + activate_evaluate.add_argument("--model-ref") + activate_evaluate.add_argument("--tool-ref", action="append", default=[]) + activate_evaluate.add_argument("--storage-scope-ref") + activate_evaluate.add_argument("--evidence-scope-ref") + activate_evaluate.add_argument("--requested-expires-at") + activate_evaluate.add_argument("--issued-at", default="1970-01-01T00:00:00Z") activate_evaluate.add_argument("--storage-receipt-ref", action="append", default=[]) activate_evaluate.add_argument("--storage-receipt-file", action="append", type=Path, default=[]) activate_evaluate.add_argument("--storage-receipt-dir", action="append", type=Path, default=[]) From b8aaf0441ab01602551feb7f8144617836e5f505 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 13:48:29 -0400 Subject: [PATCH 5/9] Delegate registry resolution from bootstrap CLI --- bin/agent-machine | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/agent-machine b/bin/agent-machine index 9bfb7f3..ffc701f 100644 --- a/bin/agent-machine +++ b/bin/agent-machine @@ -59,7 +59,8 @@ Usage: agent-machine render quadlet [--compare ] agent-machine render k8s [--compare ] agent-machine policy resolve --policy-dir --deployment-receipt-id [--expected-status allowed] - agent-machine activate evaluate [policy.json] --deployment-receipt-id [--policy-dir ] [--storage-receipt-dir ] [--pretty] + agent-machine registry resolve --grant-dir --requested-agent-identity-ref --session-ref [--expected-status active] + agent-machine activate evaluate [policy.json] [grant.json] --deployment-receipt-id [--policy-dir ] [--grant-dir ] [--storage-receipt-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 @@ -280,6 +281,10 @@ case "$COMMAND" in shift || true delegate_python_cli policy "$@" ;; + registry) + shift || true + delegate_python_cli registry "$@" + ;; activate) shift || true delegate_python_cli activate "$@" From 3d271cd4630edde72801a4ea50e9db1b1c7113d8 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 13:51:50 -0400 Subject: [PATCH 6/9] Validate Agent Registry package import --- scripts/validate-package.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/validate-package.py b/scripts/validate-package.py index a5b12e4..f0ea700 100644 --- a/scripts/validate-package.py +++ b/scripts/validate-package.py @@ -15,6 +15,7 @@ def main() -> int: import agent_machine import agent_machine.activation + import agent_machine.agent_registry import agent_machine.cli import agent_machine.evidence import agent_machine.governance @@ -57,6 +58,8 @@ def main() -> int: 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 agent_machine.agent_registry.DEFAULT_ISSUED_AT != "1970-01-01T00:00:00Z": + raise AssertionError("unexpected agent_registry default issued_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": From 63d849b5b8ebc149dd265229a7e1073dc2336d71 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 13:56:03 -0400 Subject: [PATCH 7/9] Validate Agent Registry resolver --- Makefile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 535d96b..38a3404 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.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 +.PHONY: validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula doctor probe PYTHON ?= python3 RUBY ?= ruby @@ -16,12 +16,13 @@ FAIL_POLICY := examples/policy-admission.missing.json FAIL_GRANT := examples/agent-registry-grant.missing.json RECEIPT_DIR := examples POLICY_DIR := examples +GRANT_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-policy-fabric 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-agent-registry validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-json: $(PYTHON) scripts/validate-json.py @@ -58,11 +59,17 @@ validate-policy-fabric: $(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-agent-registry: + $(PYTHON) scripts/validate-agent-registry.py + $(PYTHON) scripts/resolve-agent-registry-grant.py $(LOCAL_AGENTPOD) --grant-dir $(GRANT_DIR) --grant-id urn:srcos:agent-machine:agent-registry-grant:active-loopback-activation --requested-agent-identity-ref urn:srcos:agent:local-inference-provider --session-ref urn:srcos:session:local-bootstrap --agent-machine-id urn:srcos:agent-machine:m2-asahi-local --pretty >/tmp/agent-machine-registry-resolve-active.json + $(PYCLI) registry resolve $(LOCAL_AGENTPOD) --grant-dir $(GRANT_DIR) --grant-id urn:srcos:agent-machine:agent-registry-grant:active-render-only --requested-agent-identity-ref urn:srcos:agent:local-inference-provider --session-ref urn:srcos:session:local-bootstrap --agent-machine-id urn:srcos:agent-machine:m2-asahi-local --pretty >/tmp/agent-machine-pycli-registry-resolve-render-only.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 + $(PYCLI) activate evaluate $(LOCAL_AGENTPOD) $(READY_POLICY) --grant-dir $(GRANT_DIR) --grant-id urn:srcos:agent-machine:agent-registry-grant:active-loopback-activation --deployment-receipt-id $(DEPLOYMENT_RECEIPT_ID) --requested-agent-identity-ref urn:srcos:agent:local-inference-provider --session-ref urn:srcos:session:local-bootstrap --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-grant-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: From 1df9b1e1d0ec05a7dba42a006495bcb18f6313c2 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 15:56:43 -0400 Subject: [PATCH 8/9] Document local Agent Registry grant resolver --- .../agent-registry-grant-resolution.md | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/architecture/agent-registry-grant-resolution.md diff --git a/docs/architecture/agent-registry-grant-resolution.md b/docs/architecture/agent-registry-grant-resolution.md new file mode 100644 index 0000000..565a9bd --- /dev/null +++ b/docs/architecture/agent-registry-grant-resolution.md @@ -0,0 +1,144 @@ +# AgentRegistryGrant Resolution + +Agent Machine now has a local Agent Registry grant resolver for bootstrap and dry-run activation flows. This is a deterministic local-store resolver, not a production Agent Registry client. + +## Purpose + +Activation evaluation should not require callers to manually select an `AgentRegistryGrant` forever. The resolver lets Agent Machine scan explicit files or directories and select the matching grant by request shape. + +The resolver supports: + +- explicit grant files; +- local grant store directories; +- request matching by AgentPod ID, requested agent identity, session, optional AgentMachine, workroom, and topic; +- disambiguation by grant ID or expected grant status; +- fail-closed missing-grant stub generation; +- semantic validation through governance rules; +- activation evaluation using a locally resolved grant. + +## Current commands + +Resolve a grant from a local store by explicit grant ID: + +```bash +agent-machine registry resolve \ + examples/local-podman-llama-cpp.agent-pod.json \ + --grant-dir examples \ + --grant-id urn:srcos:agent-machine:agent-registry-grant:active-loopback-activation \ + --requested-agent-identity-ref urn:srcos:agent:local-inference-provider \ + --session-ref urn:srcos:session:local-bootstrap \ + --agent-machine-id urn:srcos:agent-machine:m2-asahi-local \ + --pretty +``` + +Resolve a grant by status and request shape: + +```bash +agent-machine registry resolve \ + examples/local-podman-llama-cpp.agent-pod.json \ + --grant-dir examples \ + --expected-status revoked \ + --requested-agent-identity-ref urn:srcos:agent:local-inference-provider \ + --session-ref urn:srcos:session:local-bootstrap \ + --agent-machine-id urn:srcos:agent-machine:m2-asahi-local \ + --workroom-ref urn:srcos:workroom:local-default \ + --topic-ref urn:srcos:topic:agent-machine \ + --pretty +``` + +Evaluate activation using a resolved registry grant: + +```bash +agent-machine activate evaluate \ + examples/local-podman-llama-cpp.agent-pod.json \ + examples/policy-admission.allowed-activation.json \ + --grant-dir examples \ + --grant-id urn:srcos:agent-machine:agent-registry-grant:active-loopback-activation \ + --deployment-receipt-id urn:srcos:agent-machine:deployment-receipt:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ + --requested-agent-identity-ref urn:srcos:agent:local-inference-provider \ + --session-ref urn:srcos:session:local-bootstrap \ + --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 +``` + +## Fail-closed behavior + +If no matching grant is found and missing stubs are allowed, the resolver emits a synthetic `AgentRegistryGrant` with: + +```text +grant.status = missing +grant.authorizationGranted = false +grant.revocationStatus = unavailable +``` + +That stub denies requested scopes and causes `ActivationDecision` to fail closed. + +If `--no-missing-stub` is provided and no grant matches, resolution fails. + +## Ambiguity behavior + +If multiple grants match the request, resolution fails unless the caller disambiguates with: + +```text +--grant-id +``` + +or: + +```text +--expected-status active|revoked|expired|denied|missing|unknown +``` + +This is deliberate. Silent selection among conflicting grants would be unsafe. + +## Scope behavior + +The resolver can construct a requested scope for generated missing stubs from CLI arguments: + +```text +--provider-id +--model-ref +--tool-ref +--storage-scope-ref +--evidence-scope-ref +``` + +For resolved real grants, schema and governance validation ensure `scope.allowed` does not exceed `request.requestedScope`. + +## Bootstrap boundary + +This resolver is not a production Agent Registry client. It does not: + +- call a remote Agent Registry endpoint; +- verify grant signatures; +- resolve revocations online; +- prove session freshness; +- bind identity to live proof-of-life; +- prove grant freshness beyond the contents of local artifacts. + +It is the bootstrap adapter shape that a real Agent Registry client can replace. + +## Validation + +Agent Registry resolver validation is part of: + +```bash +make validate-agent-registry +make validate +``` + +The validation path checks: + +- directory scanning; +- schema and semantic validation; +- ambiguity rejection; +- active activation grant resolution; +- revoked grant resolution; +- generated missing stubs; +- grant-id disambiguation; +- CLI registry resolution; +- activation evaluation using a resolved local grant. From ccb83cdae2dd7a4042accf8ffa2ddd22b99d0252 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 7 May 2026 16:10:11 -0400 Subject: [PATCH 9/9] Link AgentRegistryGrant resolution docs --- docs/index.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 6f9f25d..e26312d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,7 @@ Agent Machine is a bootstrap runtime-control substrate for SourceOS agent worklo | [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. | +| [AgentRegistryGrant resolution](architecture/agent-registry-grant-resolution.md) | Local Agent Registry grant resolver and fail-closed missing-grant 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. | @@ -95,6 +96,7 @@ validate-render validate-evidence validate-governance validate-policy-fabric +validate-agent-registry validate-activation validate-supply-chain validate-release-bundle @@ -114,7 +116,7 @@ Current blockers: - real image digest pinning/provenance from trusted build artifacts; - real release bundle signature verification; - real Policy Fabric client or endpoint; -- real Agent Registry grant resolver; +- real Agent Registry client or endpoint; - real AgentPlane evidence submission/staging client; - local LVM provisioning/probe implementation; - TopoLVM runtime integration beyond skeleton manifests;