From 8373686e26791c5d5c01bce03122700a027c249d Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 27 May 2026 06:42:00 -0400 Subject: [PATCH 1/4] Replay external trust provider files --- ...external-trust-signal-provider.schema.json | 346 ++++++++++++++++++ .../external-trust-signal-providers.md | 66 ++++ ...external-trust-signal-provider.active.json | 147 ++++++++ .../external-trust-signal-provider.stale.json | 78 ++++ src/agent_machine/external_trust.py | 227 ++++++++++++ 5 files changed, 864 insertions(+) create mode 100644 contracts/external-trust-signal-provider.schema.json create mode 100644 docs/architecture/external-trust-signal-providers.md create mode 100644 examples/external-trust-signal-provider.active.json create mode 100644 examples/external-trust-signal-provider.stale.json create mode 100644 src/agent_machine/external_trust.py diff --git a/contracts/external-trust-signal-provider.schema.json b/contracts/external-trust-signal-provider.schema.json new file mode 100644 index 0000000..0c896fc --- /dev/null +++ b/contracts/external-trust-signal-provider.schema.json @@ -0,0 +1,346 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:srcos:agent-machine:schema:external-trust-signal-provider:v0.1.0", + "title": "ExternalTrustSignalProvider", + "description": "Secret-free adapter request/response contract for optional non-authoritative external identity, reputation, certificate-tier, counterparty, and registry lookup signals used as Agent Registry verifier inputs.", + "type": "object", + "additionalProperties": false, + "required": [ + "specVersion", + "id", + "kind", + "request", + "response", + "receiptSafety", + "observedAt" + ], + "$defs": { + "signalType": { + "type": "string", + "enum": [ + "agent-identity", + "cert-tier", + "reputation-score", + "counterparty-check", + "registry-lookup", + "other" + ] + }, + "sha256Digest": { + "type": [ + "string", + "null" + ], + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "freshness": { + "type": "object", + "additionalProperties": false, + "required": [ + "maxAgeSeconds", + "observedAgeSeconds", + "fresh" + ], + "properties": { + "maxAgeSeconds": { + "type": "integer", + "minimum": 0 + }, + "observedAgeSeconds": { + "type": "integer", + "minimum": 0 + }, + "fresh": { + "type": "boolean" + } + } + }, + "signature": { + "type": "object", + "additionalProperties": false, + "required": [ + "required", + "observed", + "signatureRef", + "signerRef" + ], + "properties": { + "required": { + "type": "boolean" + }, + "observed": { + "type": "boolean" + }, + "signatureRef": { + "type": [ + "string", + "null" + ] + }, + "signerRef": { + "type": [ + "string", + "null" + ] + } + } + }, + "externalTrustSignal": { + "type": "object", + "additionalProperties": false, + "required": [ + "providerRef", + "signalType", + "signalRef", + "signalDigest", + "verifiedAt", + "freshness", + "signature", + "authority", + "failureReason" + ], + "properties": { + "providerRef": { + "type": "string" + }, + "signalType": { + "$ref": "#/$defs/signalType" + }, + "signalRef": { + "type": "string" + }, + "signalDigest": { + "$ref": "#/$defs/sha256Digest" + }, + "verifiedAt": { + "type": "string" + }, + "freshness": { + "$ref": "#/$defs/freshness" + }, + "signature": { + "$ref": "#/$defs/signature" + }, + "authority": { + "type": "string", + "const": "non-authoritative-verifier-input" + }, + "failureReason": { + "type": [ + "string", + "null" + ] + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "properties": { + "specVersion": { + "type": "string", + "const": "0.1.0" + }, + "id": { + "type": "string", + "pattern": "^urn:srcos:agent-machine:external-trust-signal-provider:[a-z0-9][a-z0-9-]*$" + }, + "kind": { + "type": "string", + "const": "ExternalTrustSignalProvider" + }, + "request": { + "type": "object", + "additionalProperties": false, + "required": [ + "requestId", + "providerRef", + "agentPodId", + "requestedAgentIdentityRef", + "sessionRef", + "workroomRef", + "topicRef", + "requestedSignalTypes", + "verificationFreshnessSeconds", + "requestedExpiresAt", + "signatureRequired" + ], + "properties": { + "requestId": { + "type": "string" + }, + "providerRef": { + "type": "string" + }, + "agentPodId": { + "type": "string", + "pattern": "^urn:srcos:agent-machine:agent-pod:[a-z0-9][a-z0-9-]*$" + }, + "requestedAgentIdentityRef": { + "type": "string" + }, + "sessionRef": { + "type": "string" + }, + "workroomRef": { + "type": [ + "string", + "null" + ] + }, + "topicRef": { + "type": [ + "string", + "null" + ] + }, + "requestedSignalTypes": { + "type": "array", + "items": { + "$ref": "#/$defs/signalType" + }, + "minItems": 1, + "uniqueItems": true + }, + "verificationFreshnessSeconds": { + "type": "integer", + "minimum": 0 + }, + "requestedExpiresAt": { + "type": [ + "string", + "null" + ] + }, + "signatureRequired": { + "type": "boolean" + } + } + }, + "response": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "usableForGrantResolution", + "providerRef", + "authority", + "verifiedAt", + "freshness", + "signals", + "failureReason" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "available", + "unavailable", + "stale", + "malformed", + "unsigned", + "denied", + "error" + ] + }, + "usableForGrantResolution": { + "type": "boolean" + }, + "providerRef": { + "type": "string" + }, + "authority": { + "type": "string", + "const": "non-authoritative-verifier-input" + }, + "verifiedAt": { + "type": [ + "string", + "null" + ] + }, + "freshness": { + "$ref": "#/$defs/freshness" + }, + "signals": { + "type": "array", + "items": { + "$ref": "#/$defs/externalTrustSignal" + } + }, + "failureReason": { + "type": [ + "string", + "null" + ] + } + } + }, + "receiptSafety": { + "type": "object", + "additionalProperties": false, + "required": [ + "includeRawContent", + "rawPromptContentIncluded", + "rawKvCacheContentIncluded", + "secretValuesIncluded", + "privateMemoryIncluded", + "apiKeysIncluded", + "walletPrivateKeysIncluded", + "rawCredentialsIncluded", + "rawUserDataIncluded" + ], + "properties": { + "includeRawContent": { + "type": "boolean", + "const": false + }, + "rawPromptContentIncluded": { + "type": "boolean", + "const": false + }, + "rawKvCacheContentIncluded": { + "type": "boolean", + "const": false + }, + "secretValuesIncluded": { + "type": "boolean", + "const": false + }, + "privateMemoryIncluded": { + "type": "boolean", + "const": false + }, + "apiKeysIncluded": { + "type": "boolean", + "const": false + }, + "walletPrivateKeysIncluded": { + "type": "boolean", + "const": false + }, + "rawCredentialsIncluded": { + "type": "boolean", + "const": false + }, + "rawUserDataIncluded": { + "type": "boolean", + "const": false + } + } + }, + "observedAt": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/docs/architecture/external-trust-signal-providers.md b/docs/architecture/external-trust-signal-providers.md new file mode 100644 index 0000000..976a62b --- /dev/null +++ b/docs/architecture/external-trust-signal-providers.md @@ -0,0 +1,66 @@ +# External Trust Signal Providers + +`ExternalTrustSignalProvider` is the adapter contract for optional external identity, reputation, certificate-tier, counterparty, and registry lookup signals used by the local Agent Registry grant resolver. + +External trust signals are verifier inputs only. They are never authorization, never runtime placement permission, and never a replacement for local SourceOS grant resolution. + +## Boundary + +```text +AgentPod + -> local grant request + -> optional ExternalTrustSignalProvider adapter + -> local Agent Registry resolver + -> AgentRegistryGrant + -> ActivationDecision +``` + +The adapter can represent PCH/ERC-8004-style prior art: identity assertion, certificate tier, reputation score, counterparty check, and registry lookup. The adapter must not bind Agent Machine to PCH, ERC-8004, x402, Base, USDC, any hosted dashboard, any payment rail, or any external root of trust. + +## Request shape + +The request side records: + +- `providerRef`: external verifier provider or local mirror reference. +- `agentPodId`: AgentPod under evaluation. +- `requestedAgentIdentityRef`: non-human runtime participant identity. +- `sessionRef`: session boundary. +- `workroomRef` and `topicRef`: workspace context. +- `requestedSignalTypes`: identity, cert-tier, reputation, counterparty, registry lookup, or other. +- `verificationFreshnessSeconds`: maximum accepted age for the signal. +- `requestedExpiresAt`: requested validity window. +- `signatureRequired`: whether signatures are required for the signal to be usable. + +## Response shape + +The response side records: + +- `status`: `available`, `unavailable`, `stale`, `malformed`, `unsigned`, `denied`, or `error`. +- `usableForGrantResolution`: true only when the response may be considered by the local grant resolver. +- `authority`: fixed to `non-authoritative-verifier-input`. +- `freshness`: max age, observed age, and freshness result. +- `signals`: one or more typed signals. +- `failureReason`: required when the response is not usable. + +Each signal records provider reference, signal type, signal reference, optional digest, verification time, freshness, signature posture, authority, failure reason, and notes. + +## Semantic rules + +A usable adapter response must satisfy all of these conditions: + +- status is `available`; +- `usableForGrantResolution=true`; +- response authority is `non-authoritative-verifier-input`; +- response freshness is true; +- at least one signal is present; +- every signal uses the requested provider reference; +- every signal type was requested; +- every signal authority is `non-authoritative-verifier-input`; +- if signatures are required, every signal has an observed signature, signature reference, and signer reference; +- no raw prompt, KV-cache, secrets, private memory, API keys, private wallet keys, raw credentials, or raw user data appear in the payload. + +An unusable adapter response must be ignored by the local grant resolver or cause fail-closed behavior when local policy requires that signal. Stale, unavailable, malformed, unsigned, denied, or error responses cannot authorize activation. + +## Non-authority rule + +External trust signal providers can reduce or annotate local risk. They cannot widen requested scope, grant tools, grant storage, grant cache reuse, grant memory access, expose models, or activate an AgentPod. Only local `PolicyAdmission` plus local `AgentRegistryGrant` can lead to an `ActivationDecision` that allows activation. diff --git a/examples/external-trust-signal-provider.active.json b/examples/external-trust-signal-provider.active.json new file mode 100644 index 0000000..c1532b2 --- /dev/null +++ b/examples/external-trust-signal-provider.active.json @@ -0,0 +1,147 @@ +{ + "specVersion": "0.1.0", + "id": "urn:srcos:agent-machine:external-trust-signal-provider:local-pch-style-active", + "kind": "ExternalTrustSignalProvider", + "request": { + "requestId": "urn:srcos:agent-machine:external-trust-request:local-pch-style-active", + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "agentPodId": "urn:srcos:agent-machine:agent-pod:local-podman-llama-cpp", + "requestedAgentIdentityRef": "urn:srcos:agent:local-inference-provider", + "sessionRef": "urn:srcos:session:local-bootstrap", + "workroomRef": "urn:srcos:workroom:local-default", + "topicRef": "urn:srcos:topic:agent-machine", + "requestedSignalTypes": [ + "agent-identity", + "cert-tier", + "reputation-score", + "registry-lookup" + ], + "verificationFreshnessSeconds": 3600, + "requestedExpiresAt": "2026-05-04T13:50:00Z", + "signatureRequired": true + }, + "response": { + "status": "available", + "usableForGrantResolution": true, + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "authority": "non-authoritative-verifier-input", + "verifiedAt": "2026-05-04T12:50:10Z", + "freshness": { + "maxAgeSeconds": 3600, + "observedAgeSeconds": 10, + "fresh": true + }, + "signals": [ + { + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "signalType": "agent-identity", + "signalRef": "urn:srcos:external-trust-signal:local-agent-identity", + "signalDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "verifiedAt": "2026-05-04T12:50:10Z", + "freshness": { + "maxAgeSeconds": 3600, + "observedAgeSeconds": 10, + "fresh": true + }, + "signature": { + "required": true, + "observed": true, + "signatureRef": "urn:srcos:signature:local-agent-identity", + "signerRef": "urn:srcos:signer:local-trust-mirror" + }, + "authority": "non-authoritative-verifier-input", + "failureReason": null, + "notes": [ + "Identity signal is adapter input only; local Agent Registry remains authoritative." + ] + }, + { + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "signalType": "cert-tier", + "signalRef": "urn:srcos:external-trust-signal:local-agent-cert-tier-bronze", + "signalDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "verifiedAt": "2026-05-04T12:50:10Z", + "freshness": { + "maxAgeSeconds": 3600, + "observedAgeSeconds": 10, + "fresh": true + }, + "signature": { + "required": true, + "observed": true, + "signatureRef": "urn:srcos:signature:local-agent-cert-tier", + "signerRef": "urn:srcos:signer:local-trust-mirror" + }, + "authority": "non-authoritative-verifier-input", + "failureReason": null, + "notes": [ + "Cert-tier signal cannot authorize activation by itself." + ] + }, + { + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "signalType": "reputation-score", + "signalRef": "urn:srcos:external-trust-signal:local-agent-reputation-score", + "signalDigest": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "verifiedAt": "2026-05-04T12:50:10Z", + "freshness": { + "maxAgeSeconds": 3600, + "observedAgeSeconds": 10, + "fresh": true + }, + "signature": { + "required": true, + "observed": true, + "signatureRef": "urn:srcos:signature:local-agent-reputation-score", + "signerRef": "urn:srcos:signer:local-trust-mirror" + }, + "authority": "non-authoritative-verifier-input", + "failureReason": null, + "notes": [ + "Reputation score can inform risk but cannot widen requested scope." + ] + }, + { + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "signalType": "registry-lookup", + "signalRef": "urn:srcos:external-trust-signal:local-agent-registry-lookup", + "signalDigest": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "verifiedAt": "2026-05-04T12:50:10Z", + "freshness": { + "maxAgeSeconds": 3600, + "observedAgeSeconds": 10, + "fresh": true + }, + "signature": { + "required": true, + "observed": true, + "signatureRef": "urn:srcos:signature:local-agent-registry-lookup", + "signerRef": "urn:srcos:signer:local-trust-mirror" + }, + "authority": "non-authoritative-verifier-input", + "failureReason": null, + "notes": [ + "Registry lookup is evidence for local grant resolution, not authorization." + ] + } + ], + "failureReason": null + }, + "receiptSafety": { + "includeRawContent": false, + "rawPromptContentIncluded": false, + "rawKvCacheContentIncluded": false, + "secretValuesIncluded": false, + "privateMemoryIncluded": false, + "apiKeysIncluded": false, + "walletPrivateKeysIncluded": false, + "rawCredentialsIncluded": false, + "rawUserDataIncluded": false + }, + "observedAt": "2026-05-04T12:50:10Z", + "labels": { + "sourceos.external-trust.prototype": "true", + "sourceos.external-trust.usable": "true", + "sourceos.external-trust.authority": "non-authoritative" + } +} diff --git a/examples/external-trust-signal-provider.stale.json b/examples/external-trust-signal-provider.stale.json new file mode 100644 index 0000000..f674f15 --- /dev/null +++ b/examples/external-trust-signal-provider.stale.json @@ -0,0 +1,78 @@ +{ + "specVersion": "0.1.0", + "id": "urn:srcos:agent-machine:external-trust-signal-provider:local-pch-style-stale", + "kind": "ExternalTrustSignalProvider", + "request": { + "requestId": "urn:srcos:agent-machine:external-trust-request:local-pch-style-stale", + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "agentPodId": "urn:srcos:agent-machine:agent-pod:local-podman-llama-cpp", + "requestedAgentIdentityRef": "urn:srcos:agent:local-inference-provider", + "sessionRef": "urn:srcos:session:local-bootstrap", + "workroomRef": "urn:srcos:workroom:local-default", + "topicRef": "urn:srcos:topic:agent-machine", + "requestedSignalTypes": [ + "agent-identity", + "cert-tier", + "reputation-score", + "registry-lookup" + ], + "verificationFreshnessSeconds": 300, + "requestedExpiresAt": "2026-05-04T13:50:00Z", + "signatureRequired": true + }, + "response": { + "status": "stale", + "usableForGrantResolution": false, + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "authority": "non-authoritative-verifier-input", + "verifiedAt": "2026-05-04T11:40:00Z", + "freshness": { + "maxAgeSeconds": 300, + "observedAgeSeconds": 4210, + "fresh": false + }, + "signals": [ + { + "providerRef": "urn:srcos:external-trust-provider:pch-style-local-mirror", + "signalType": "agent-identity", + "signalRef": "urn:srcos:external-trust-signal:local-agent-identity", + "signalDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "verifiedAt": "2026-05-04T11:40:00Z", + "freshness": { + "maxAgeSeconds": 300, + "observedAgeSeconds": 4210, + "fresh": false + }, + "signature": { + "required": true, + "observed": true, + "signatureRef": "urn:srcos:signature:local-agent-identity", + "signerRef": "urn:srcos:signer:local-trust-mirror" + }, + "authority": "non-authoritative-verifier-input", + "failureReason": "stale", + "notes": [ + "Stale external signal is not usable for grant resolution." + ] + } + ], + "failureReason": "External trust signal cache is stale; local grant resolver must ignore it or fail closed." + }, + "receiptSafety": { + "includeRawContent": false, + "rawPromptContentIncluded": false, + "rawKvCacheContentIncluded": false, + "secretValuesIncluded": false, + "privateMemoryIncluded": false, + "apiKeysIncluded": false, + "walletPrivateKeysIncluded": false, + "rawCredentialsIncluded": false, + "rawUserDataIncluded": false + }, + "observedAt": "2026-05-04T12:50:10Z", + "labels": { + "sourceos.external-trust.prototype": "true", + "sourceos.external-trust.usable": "false", + "sourceos.external-trust.failure": "stale" + } +} diff --git a/src/agent_machine/external_trust.py b/src/agent_machine/external_trust.py new file mode 100644 index 0000000..4ac3fd5 --- /dev/null +++ b/src/agent_machine/external_trust.py @@ -0,0 +1,227 @@ +"""External trust signal adapter validation. + +ExternalTrustSignalProvider artifacts are optional verifier inputs for Agent +Registry grant resolution. They are never authorization and must never become +the SourceOS root of trust. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path +from typing import Any + +from agent_machine.contracts import load_json, schema_by_kind, validate_instance + +AUTHORITY = "non-authoritative-verifier-input" +USABLE_STATUS = "available" +UNUSABLE_STATUSES = {"unavailable", "stale", "malformed", "unsigned", "denied", "error"} +SIGNAL_TYPES = { + "agent-identity", + "cert-tier", + "reputation-score", + "counterparty-check", + "registry-lookup", + "other", +} +EXTRA_SAFETY_FLAGS = [ + "apiKeysIncluded", + "walletPrivateKeysIncluded", + "rawCredentialsIncluded", + "rawUserDataIncluded", +] +BASE_SAFETY_FLAGS = [ + "includeRawContent", + "rawPromptContentIncluded", + "rawKvCacheContentIncluded", + "secretValuesIncluded", + "privateMemoryIncluded", +] + + +def validate_external_trust_signal_provider_schema(path: Path, root: Path | None = None) -> dict[str, Any]: + validate_instance(path, schema_by_kind(root)["ExternalTrustSignalProvider"]) + value = load_json(path) + if not isinstance(value, dict): + raise AssertionError(f"{path}: ExternalTrustSignalProvider root must be an object") + return value + + +def validate_external_trust_signal_provider_semantics(provider: dict[str, Any], source: str = "") -> None: + request = _require_object(provider.get("request"), f"{source}: request") + response = _require_object(provider.get("response"), f"{source}: response") + safety = _require_object(provider.get("receiptSafety"), f"{source}: receiptSafety") + + provider_ref = request.get("providerRef") + if response.get("providerRef") != provider_ref: + raise AssertionError(f"{source}: response.providerRef must match request.providerRef") + + requested_signal_types = request.get("requestedSignalTypes") + if not isinstance(requested_signal_types, list) or not requested_signal_types: + raise AssertionError(f"{source}: request.requestedSignalTypes must be a non-empty list") + if len(requested_signal_types) != len(set(requested_signal_types)): + raise AssertionError(f"{source}: request.requestedSignalTypes must not contain duplicates") + if not set(requested_signal_types).issubset(SIGNAL_TYPES): + raise AssertionError(f"{source}: request.requestedSignalTypes contains unsupported values") + + freshness_window = request.get("verificationFreshnessSeconds") + if not isinstance(freshness_window, int) or freshness_window < 0: + raise AssertionError(f"{source}: request.verificationFreshnessSeconds must be a non-negative integer") + + status = response.get("status") + usable = response.get("usableForGrantResolution") + if status == USABLE_STATUS: + if usable is not True: + raise AssertionError(f"{source}: available response requires usableForGrantResolution=true") + if response.get("failureReason") is not None: + raise AssertionError(f"{source}: available response must not carry failureReason") + elif status in UNUSABLE_STATUSES: + if usable is not False: + raise AssertionError(f"{source}: response.status={status} requires usableForGrantResolution=false") + if not response.get("failureReason"): + raise AssertionError(f"{source}: response.status={status} requires failureReason") + else: + raise AssertionError(f"{source}: unsupported external trust status {status!r}") + + if response.get("authority") != AUTHORITY: + raise AssertionError(f"{source}: response.authority must be {AUTHORITY}") + + response_freshness = _require_object(response.get("freshness"), f"{source}: response.freshness") + _assert_freshness(response_freshness, f"{source}: response.freshness") + if usable is True and response_freshness.get("fresh") is not True: + raise AssertionError(f"{source}: usable external trust response must be fresh") + if status == "stale" and response_freshness.get("fresh") is not False: + raise AssertionError(f"{source}: stale response requires freshness.fresh=false") + + signals = response.get("signals") + if not isinstance(signals, list): + raise AssertionError(f"{source}: response.signals must be a list") + if usable is True and not signals: + raise AssertionError(f"{source}: usable external trust response requires at least one signal") + + signal_types_seen: list[str] = [] + for index, signal in enumerate(signals): + signal_source = f"{source}: response.signals[{index}]" + _assert_signal_payload( + signal, + signal_source, + expected_provider_ref=str(provider_ref), + requested_signal_types=set(requested_signal_types), + signature_required=bool(request.get("signatureRequired")), + usable_response=bool(usable), + ) + signal_types_seen.append(str(signal.get("signalType"))) + + if len(signal_types_seen) != len(set(signal_types_seen)): + raise AssertionError(f"{source}: response.signals must not contain duplicate signalType entries") + + _assert_safety_flags(safety, source) + + +def external_trust_signal_usable(provider: dict[str, Any]) -> bool: + """Return true only when an adapter result can be used as local verifier input. + + This result is still not authorization. It can only be considered by a local + Agent Registry grant resolver. + """ + response = provider.get("response", {}) + return ( + response.get("status") == USABLE_STATUS + and response.get("usableForGrantResolution") is True + and response.get("authority") == AUTHORITY + and response.get("freshness", {}).get("fresh") is True + ) + + +def _require_object(value: Any, source: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise AssertionError(f"{source} must be an object") + return value + + +def _assert_signal_payload( + signal: Any, + source: str, + *, + expected_provider_ref: str, + requested_signal_types: set[str], + signature_required: bool, + usable_response: bool, +) -> None: + signal_doc = _require_object(signal, source) + if signal_doc.get("providerRef") != expected_provider_ref: + raise AssertionError(f"{source}.providerRef must match request.providerRef") + signal_type = signal_doc.get("signalType") + if signal_type not in requested_signal_types: + raise AssertionError(f"{source}.signalType must be one of the requested signal types") + if signal_doc.get("authority") != AUTHORITY: + raise AssertionError(f"{source}.authority must be {AUTHORITY}") + if usable_response and signal_doc.get("failureReason") is not None: + raise AssertionError(f"{source}.failureReason must be null for usable responses") + + freshness = _require_object(signal_doc.get("freshness"), f"{source}.freshness") + _assert_freshness(freshness, f"{source}.freshness") + if usable_response and freshness.get("fresh") is not True: + raise AssertionError(f"{source}: usable response cannot include stale signal") + + signature = _require_object(signal_doc.get("signature"), f"{source}.signature") + if signature_required: + if signature.get("required") is not True: + raise AssertionError(f"{source}.signature.required must be true when request.signatureRequired=true") + if signature.get("observed") is not True: + raise AssertionError(f"{source}.signature.observed must be true when signatures are required") + if not signature.get("signatureRef"): + raise AssertionError(f"{source}.signature.signatureRef is required when signatures are required") + if not signature.get("signerRef"): + raise AssertionError(f"{source}.signature.signerRef is required when signatures are required") + + +def _assert_freshness(freshness: dict[str, Any], source: str) -> None: + max_age = freshness.get("maxAgeSeconds") + observed_age = freshness.get("observedAgeSeconds") + fresh = freshness.get("fresh") + if not isinstance(max_age, int) or max_age < 0: + raise AssertionError(f"{source}.maxAgeSeconds must be a non-negative integer") + if not isinstance(observed_age, int) or observed_age < 0: + raise AssertionError(f"{source}.observedAgeSeconds must be a non-negative integer") + if not isinstance(fresh, bool): + raise AssertionError(f"{source}.fresh must be a boolean") + if observed_age > max_age and fresh is True: + raise AssertionError(f"{source}.fresh cannot be true when observedAgeSeconds exceeds maxAgeSeconds") + if observed_age <= max_age and fresh is False: + raise AssertionError(f"{source}.fresh cannot be false when observedAgeSeconds is within maxAgeSeconds") + + +def _assert_safety_flags(safety: dict[str, Any], source: str) -> None: + for key in BASE_SAFETY_FLAGS + EXTRA_SAFETY_FLAGS: + if safety.get(key) is not False: + raise AssertionError(f"{source}: receiptSafety.{key} must be false") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Validate an ExternalTrustSignalProvider artifact") + parser.add_argument("external_trust_json", type=Path) + parser.add_argument("--expect", choices=["usable", "unusable"], required=True) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + provider = validate_external_trust_signal_provider_schema(args.external_trust_json) + validate_external_trust_signal_provider_semantics(provider, str(args.external_trust_json)) + usable = external_trust_signal_usable(provider) + if args.expect == "usable" and not usable: + raise AssertionError(f"{args.external_trust_json}: expected usable external trust signal") + if args.expect == "unusable" and usable: + raise AssertionError(f"{args.external_trust_json}: expected unusable external trust signal") + print(f"VALID external trust {args.expect} {args.external_trust_json}") + 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 9fc6c39cca8b5d259a2af0c34696aeb956db1d35 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 27 May 2026 07:20:58 -0400 Subject: [PATCH 2/4] Wire external trust schema mapping --- src/agent_machine/contracts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/agent_machine/contracts.py b/src/agent_machine/contracts.py index 1c9d983..de28265 100644 --- a/src/agent_machine/contracts.py +++ b/src/agent_machine/contracts.py @@ -53,6 +53,7 @@ def schema_by_kind(root: Path | None = None) -> dict[str, Path]: "AgentRegistryGrant": base / "agent-registry-grant.schema.json", "CacheTier": base / "cache-tier.schema.json", "DeploymentReceipt": base / "deployment-receipt.schema.json", + "ExternalTrustSignalProvider": base / "external-trust-signal-provider.schema.json", "InferenceProvider": base / "inference-provider.schema.json", "PolicyAdmission": base / "policy-admission.schema.json", "ReleaseEvidenceBundle": base / "release-evidence-bundle.schema.json", From 3e239f237988d83195a5ebb4cfadcf4c26e4e149 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 27 May 2026 08:07:55 -0400 Subject: [PATCH 3/4] Wire external trust governance validation --- scripts/validate-governance.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/validate-governance.py b/scripts/validate-governance.py index ca2c9e4..1475522 100644 --- a/scripts/validate-governance.py +++ b/scripts/validate-governance.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate PolicyAdmission and AgentRegistryGrant semantic consistency.""" +"""Validate governance semantic consistency.""" from __future__ import annotations @@ -12,6 +12,10 @@ sys.path.insert(0, str(SRC_ROOT)) from agent_machine.contracts import load_json # noqa: E402 +from agent_machine.external_trust import ( # noqa: E402 + external_trust_signal_usable, + validate_external_trust_signal_provider_semantics, +) from agent_machine.governance import ( # noqa: E402 assert_activation_fails_closed, assert_activation_ready, @@ -33,6 +37,11 @@ "active_activation": REPO_ROOT / "examples" / "agent-registry-grant.active-activation.json", } +EXTERNAL_TRUST_EXAMPLES = { + "usable": REPO_ROOT / "examples" / "external-trust-signal-provider.active.json", + "stale": REPO_ROOT / "examples" / "external-trust-signal-provider.stale.json", +} + def validate_policy_examples() -> dict[str, dict]: values = {} @@ -54,6 +63,22 @@ def validate_grant_examples() -> dict[str, dict]: return values +def validate_external_trust_examples() -> dict[str, dict]: + values = {} + for name, path in EXTERNAL_TRUST_EXAMPLES.items(): + value = load_json(path) + validate_external_trust_signal_provider_semantics(value, str(path.relative_to(REPO_ROOT))) + values[name] = value + print(f"VALID external trust semantics {path.relative_to(REPO_ROOT)}") + + if not external_trust_signal_usable(values["usable"]): + raise AssertionError("usable external trust example must be usable for local grant resolution") + if external_trust_signal_usable(values["stale"]): + raise AssertionError("stale external trust example must not be usable for local grant resolution") + print("VALID external trust usable/stale matrix") + return values + + def validate_activation_matrix(policies: dict[str, dict], grants: dict[str, dict]) -> None: fail_closed_cases = [ ("missing", "missing"), @@ -85,6 +110,7 @@ def validate_activation_matrix(policies: dict[str, dict], grants: dict[str, dict def main() -> int: policies = validate_policy_examples() grants = validate_grant_examples() + validate_external_trust_examples() validate_activation_matrix(policies, grants) return 0 From 69be095619ac045de9f0228ef1e0edccebea9a41 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 27 May 2026 08:30:57 -0400 Subject: [PATCH 4/4] Wire external trust package validation --- scripts/validate-package.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/validate-package.py b/scripts/validate-package.py index 55425cb..5cab6c2 100644 --- a/scripts/validate-package.py +++ b/scripts/validate-package.py @@ -18,6 +18,7 @@ def main() -> int: import agent_machine.activation import agent_machine.cli import agent_machine.evidence + import agent_machine.external_trust import agent_machine.governance import agent_machine.policy_fabric import agent_machine.release_bundle @@ -42,6 +43,7 @@ def main() -> int: "AgentPod", "AgentPlaneRuntimeEvidence", "AgentRegistryGrant", + "ExternalTrustSignalProvider", "PolicyAdmission", "ReleaseEvidenceBundle", "SignedReleaseBundleEnvelope",