From 7790c327618e935dda8d16a11e6a61e2dc471665 Mon Sep 17 00:00:00 2001 From: mdheller Date: Mon, 3 Aug 2026 01:44:53 -0400 Subject: [PATCH] feat(model-plane): emit hash-chained InferenceReceipts natively (T7-16) Agent Machine grants a model-plane capability to an agent pod and authenticates the local inference socket; every completion over that socket must leave a provenance receipt (its InferenceProvider contract declares receiptRequired). This makes Agent Machine emit spec-conformant, hash-chained InferenceReceipts natively into the estate's single ledger (SEAM-011: no local-only ledger). Consume-not-fork: the schema and the hash-chain/canonical-JSON/ledger machinery are vendored VERBATIM from the canonical emitter in prophet-platform apps/receipt-gateway (source commit + sha256 recorded in provenance). The only addition is emit_socket_inference_receipt(), which binds requestingAgentRef + capabilityLeaseRef (what socket auth resolves) into the receipt before chaining. Teeth (scripts/validate-inference-receipt.py, wired into make validate): a produced receipt validates against InferenceReceipt.schema.json and chains (prevHash continuity); a tampered entry and a local-only unchained entry are both rejected; an off-device receipt without lease+escalation is schema-rejected (SEAM-015). --- Makefile | 7 +- .../model-plane/InferenceReceipt.schema.json | 361 ++++++++++++++++++ docs/model-plane-inference-receipts.md | 56 +++ scripts/validate-inference-receipt.py | 114 ++++++ src/agent_machine/inference_receipt.py | 192 ++++++++++ 5 files changed, 728 insertions(+), 2 deletions(-) create mode 100644 contracts/model-plane/InferenceReceipt.schema.json create mode 100644 docs/model-plane-inference-receipts.md create mode 100755 scripts/validate-inference-receipt.py create mode 100644 src/agent_machine/inference_receipt.py diff --git a/Makefile b/Makefile index f764cbf..945defe 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: validate-no-merge-duplication validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-attestation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts doctor probe validate-artifact-digest-honesty validate-consent-before-staging +.PHONY: validate-no-merge-duplication validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-attestation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts doctor probe validate-artifact-digest-honesty validate-consent-before-staging validate-inference-receipt PYTHON ?= python3 RUBY ?= ruby @@ -22,7 +22,7 @@ DECIDED_AT := 2026-05-04T12:51:00Z PYCLI := PYTHONPATH=src $(PYTHON) -m agent_machine.cli PYMOD := PYTHONPATH=src $(PYTHON) -m -validate: validate-no-merge-duplication validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-attestation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts validate-artifact-digest-honesty validate-consent-before-staging +validate: validate-no-merge-duplication validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-policy-fabric validate-agent-registry validate-superconscious-runtime-plan validate-activation validate-attestation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts validate-artifact-digest-honesty validate-consent-before-staging validate-inference-receipt validate-no-merge-duplication: $(PYTHON) scripts/validate-no-merge-duplication.py @@ -34,6 +34,9 @@ validate-artifact-digest-honesty: validate-consent-before-staging: $(PYTHON) scripts/validate-consent-before-staging.py +validate-inference-receipt: + $(PYTHON) scripts/validate-inference-receipt.py + validate-json: $(PYTHON) scripts/validate-json.py diff --git a/contracts/model-plane/InferenceReceipt.schema.json b/contracts/model-plane/InferenceReceipt.schema.json new file mode 100644 index 0000000..18a760a --- /dev/null +++ b/contracts/model-plane/InferenceReceipt.schema.json @@ -0,0 +1,361 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/InferenceReceipt.json", + "$comment": "VENDORED VERBATIM (do not edit) from SocioProphet/prophet-platform apps/receipt-gateway/schemas/model-plane/InferenceReceipt.schema.json @ commit abd98805, sha256:3d4c2a55070d65dbc902ba0880ea61c9ae6cbbca2b675c559a01a18a8b734499. Canonical owner: SourceOS-Linux/sourceos-spec (Tranche 7). Refresh via: cp from prophet-platform; do not diverge (SEAM-011).", + "title": "InferenceReceipt", + "description": "VENDORED from SourceOS-Linux/sourceos-spec (Tranche 7, canonical owner). Provenance receipt emitted for every model-plane completion (Tranche 7 / Model Plane). Identifies the tier, the content-addressed base model / adapter / tokenizer digests, the serving daemon and provider, the data-residency class the request was served under, and the escalation chain if any. This is the provenance primitive that makes on-device inference auditable: it references the existing Model Carry family (InferenceProvider, ModelResidency, SourceOSModelCarryRef) rather than restating it. Receipts are ledger-bound; a local-only ledger is not permitted (SEAM-011).", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "specVersion", + "issuedAt", + "providerDaemon", + "tier", + "baseModelDigest", + "task", + "inputHash", + "outputHash", + "dataResidencyClass", + "ledgerSeq" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^urn:srcos:inference-receipt:", + "description": "Stable URN identifier. Pattern: urn:srcos:inference-receipt:" + }, + "type": { + "const": "InferenceReceipt", + "description": "Discriminator constant \u2014 always \"InferenceReceipt\"." + }, + "specVersion": { + "type": "string", + "description": "Spec version of this document, e.g. \"2.1.0\"." + }, + "issuedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the completion finished and the receipt was issued." + }, + "requestingAgentRef": { + "type": [ + "string", + "null" + ], + "pattern": "^urn:srcos:agent-passport:", + "description": "AgentPassport URN of the agent that requested the inference. Null only for internal system-initiated inference." + }, + "requestingAgentClass": { + "type": [ + "string", + "null" + ], + "enum": [ + "system_core", + "intelligence_automation", + "app_helper", + "legacy_bridge", + "third_party", + null + ], + "description": "Mirror of the requesting agent's AgentPassport.agent_class, for at-a-glance audit. Authoritative source is the referenced passport." + }, + "capabilityLeaseRef": { + "type": [ + "string", + "null" + ], + "pattern": "^urn:srcos:lease:", + "description": "AgentCapabilityLease URN authorizing this provider connection. Null is only valid for on_device_only T0 system inference; any non-on_device_only receipt MUST carry a lease (enforced below)." + }, + "providerDaemon": { + "type": "string", + "enum": [ + "inferenced", + "embeddingd", + "visiond", + "distilld" + ], + "description": "The model-plane serving daemon that produced this completion. Note: modelplaned is a catalog authority and does not perform inference, so it never emits an InferenceReceipt." + }, + "providerRef": { + "type": [ + "string", + "null" + ], + "pattern": "^urn:srcos:inference-provider:", + "description": "InferenceProvider URN backing the serving daemon." + }, + "tier": { + "type": "string", + "enum": [ + "T0", + "T1", + "T2", + "T3", + "T4" + ], + "description": "Placement tier that served the request. Tier boundaries are data-residency boundaries (Model Plane \u00a7II)." + }, + "baseModelDigest": { + "type": "string", + "pattern": "^sha256:[a-fA-F0-9]{64}$", + "description": "Content-addressed digest of the base model weights that served the request." + }, + "adapterDigest": { + "type": [ + "string", + "null" + ], + "pattern": "^sha256:[a-fA-F0-9]{64}$", + "description": "Content-addressed digest of the active LoRA adapter, or null when the base model served the request with no adapter." + }, + "tokenizerDigest": { + "type": [ + "string", + "null" + ], + "pattern": "^sha256:[a-fA-F0-9]{64}$", + "description": "Content-addressed digest of the tokenizer used." + }, + "modelCarryRef": { + "type": [ + "string", + "null" + ], + "pattern": "^urn:srcos:model-carry-ref:", + "description": "SourceOSModelCarryRef URN under which the model was carried/authorized." + }, + "modelResidencyRef": { + "type": [ + "string", + "null" + ], + "pattern": "^urn:srcos:model-residency:", + "description": "ModelResidency URN observed at serving time." + }, + "task": { + "type": "string", + "description": "Task label the request was routed as, e.g. summarization, embedding, agent_classification, ocr-recognize." + }, + "inputHash": { + "type": "string", + "pattern": "^sha256:[a-fA-F0-9]{64}$", + "description": "Hash of the canonical input. The receipt records that an inference occurred and over what, not the content." + }, + "inputTokenCount": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "description": "Input token count, or null for non-text modalities." + }, + "outputHash": { + "type": "string", + "pattern": "^sha256:[a-fA-F0-9]{64}$", + "description": "Hash of the canonical output." + }, + "outputTokenCount": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "description": "Output token count, or null for non-text modalities." + }, + "confidence": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "maximum": 1, + "description": "Model-reported confidence used for the escalation trigger, or null when not estimated." + }, + "confidenceMethod": { + "type": [ + "string", + "null" + ], + "enum": [ + "logit-entropy", + "verbalized", + "calibration-head", + "downstream-validation", + "none", + null + ], + "description": "How `confidence` was derived. REVIEW HARDENING (Model Plane OQ1 / finding: confidence is the load-bearing escalation gate and is self-reported): recording the method makes the number auditable and lets downstream calibration checks run. A null method with a non-null confidence should be treated as uncalibrated." + }, + "latencyMs": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "description": "End-to-end serving latency in milliseconds." + }, + "escalatedFrom": { + "type": [ + "string", + "null" + ], + "enum": [ + "T0", + "T1", + "T2", + "T3", + null + ], + "description": "The tier this request was escalated FROM, or null if served at first placement. Must be null when dataResidencyClass is on_device_only (enforced below)." + }, + "escalationChain": { + "type": "array", + "description": "Ordered EscalationDecision URNs recording each tier/residency boundary this request crossed. Empty when served at first placement.", + "items": { + "type": "string", + "pattern": "^urn:srcos:escalation-decision:" + } + }, + "dataResidencyClass": { + "type": "string", + "enum": [ + "on_device_only", + "sovereign_cluster", + "external_permitted" + ], + "description": "The residency class this completion was served under. Any class other than on_device_only means the input crossed a data boundary and MUST be accompanied by a lease and a non-empty escalation chain (enforced below; SEAM-015)." + }, + "computeDevice": { + "type": [ + "string", + "null" + ], + "enum": [ + "ane", + "gpu", + "cpu", + "npu", + "remote", + null + ], + "description": "Hardware path that executed the compute." + }, + "ledgerSeq": { + "type": "integer", + "minimum": 0, + "description": "Monotonic sequence number of this receipt in the append-only ledger." + }, + "ledgerPrevHash": { + "type": [ + "string", + "null" + ], + "pattern": "^sha256:[a-fA-F0-9]{64}$", + "description": "REVIEW HARDENING (finding: the ledger is the trust anchor for every Model Plane provenance claim but `ledgerSeq` alone does not bind entry N to N-1). Hash of the prior ledger entry, making the ledger tamper-evident/hash-chained so an enumerated contribution list cannot be retroactively rewritten." + }, + "evidenceHash": { + "type": [ + "string", + "null" + ], + "pattern": "^sha256:[a-fA-F0-9]{64}$", + "description": "Hash of the canonical receipt evidence payload." + }, + "evidenceRefs": { + "type": "array", + "description": "Additional evidence URNs or content hashes.", + "items": { + "type": "string" + } + } + }, + "allOf": [ + { + "$comment": "SEAM-015: a completion served off-device (not on_device_only) must carry both an authorizing capability lease and a non-empty escalation chain. Possession of the output is not authorization for the crossing.", + "if": { + "properties": { + "dataResidencyClass": { + "enum": [ + "sovereign_cluster", + "external_permitted" + ] + } + }, + "required": [ + "dataResidencyClass" + ] + }, + "then": { + "required": [ + "capabilityLeaseRef", + "escalatedFrom", + "escalationChain" + ], + "properties": { + "capabilityLeaseRef": { + "type": "string" + }, + "escalatedFrom": { + "type": "string", + "enum": [ + "T0", + "T1", + "T2", + "T3" + ] + }, + "escalationChain": { + "minItems": 1 + } + } + } + }, + { + "$comment": "An on_device_only completion cannot have been escalated from a lower tier.", + "if": { + "properties": { + "dataResidencyClass": { + "const": "on_device_only" + } + }, + "required": [ + "dataResidencyClass" + ] + }, + "then": { + "properties": { + "escalatedFrom": { + "const": null + } + } + } + }, + { + "$comment": "REVIEW HARDENING (ledger tamper-evidence): any non-genesis entry (ledgerSeq >= 1) must carry the prior entry's hash, so the append-only ledger is hash-chained and an enumerated contribution list cannot be retroactively rewritten.", + "if": { + "properties": { + "ledgerSeq": { + "minimum": 1 + } + }, + "required": [ + "ledgerSeq" + ] + }, + "then": { + "required": [ + "ledgerPrevHash" + ], + "properties": { + "ledgerPrevHash": { + "type": "string" + } + } + } + } + ] +} diff --git a/docs/model-plane-inference-receipts.md b/docs/model-plane-inference-receipts.md new file mode 100644 index 0000000..d41bed1 --- /dev/null +++ b/docs/model-plane-inference-receipts.md @@ -0,0 +1,56 @@ +# Model Plane — InferenceReceipt emission (T7-16) + +Tranche 7 slice **T7-16: "Model plane capability grants + socket auth"** for +`agent-machine`. Agent Machine grants a model-plane capability to an agent pod and +authenticates the local inference socket. Every completion served over that +authenticated socket must leave a provenance receipt — the `InferenceProvider` +contract already declares `policy.receiptRequired: true`. This slice makes Agent +Machine emit that receipt **natively**, into the estate's single hash-chained +ledger (SEAM-011: a local-only ledger is not permitted). + +## What this ships + +- `contracts/model-plane/InferenceReceipt.schema.json` — the canonical + `InferenceReceipt` schema, **vendored verbatim** from + `SocioProphet/prophet-platform apps/receipt-gateway/schemas/model-plane/` + (canonical owner: `SourceOS-Linux/sourceos-spec`, Tranche 7). Provenance + (source commit + sha256) is recorded in the schema's `$comment`. +- `src/agent_machine/inference_receipt.py` — the hash-chain / canonical-JSON / + ledger machinery is **vendored verbatim** from the canonical emitter + (`prophet-platform apps/receipt-gateway/tools/inference_receipt_emitter.py`). + We consume it, we do not re-implement the chain. The only Agent-Machine + addition is `emit_socket_inference_receipt()`, which binds the two facts socket + auth resolves — `requestingAgentRef` (the AgentPassport URN) and + `capabilityLeaseRef` (the AgentCapabilityLease URN that granted the model-plane + capability) — into the receipt before it is chained, so the grant is covered by + the tamper-evident hash. Both refs are required arguments: a socket-authed + inference with no capability grant is not authorized and is never emitted + (fail-closed at the call site). +- `scripts/validate-inference-receipt.py` — teeth, wired into `make validate` via + `make validate-inference-receipt`. + +## Teeth + +The validator fails CI unless all of these hold: + +1. A receipt for a socket-authed, capability-granted inference validates against + the vendored schema. +2. Successive receipts chain (`ledgerPrevHash` continuity; genesis has none). +3. A tampered entry breaks the chain and is rejected. +4. A local-only receipt (a non-genesis entry with no `ledgerPrevHash`, i.e. not + bound into the ledger) is rejected by **both** the schema and the verifier + (SEAM-011). +5. An off-device receipt without a capability lease + escalation chain is + schema-rejected (SEAM-015) — the grant is load-bearing, not decorative. + +## Boundary / not in this slice + +- `contracts/inference-provider.schema.json` (Agent Machine's own + `InferenceProvider` declaration) is unchanged. It declares *that* a provider + requires receipts; the vendored `InferenceReceipt` schema is *what* the emitted + receipt conforms to. Reconciling the two `InferenceProvider` models + (Agent Machine's local one vs. the sourceos-spec Model Carry family) is tracked + upstream and is out of scope here. +- The live socket-auth daemon that calls `emit_socket_inference_receipt()` on + every real completion, and a persistent ledger service (vs. per-call file), are + the remaining productionization steps. See the T7-16 follow-up issue. diff --git a/scripts/validate-inference-receipt.py b/scripts/validate-inference-receipt.py new file mode 100755 index 0000000..77fbda6 --- /dev/null +++ b/scripts/validate-inference-receipt.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Teeth for T7-16: Agent Machine emits spec-conformant, hash-chained InferenceReceipts. + +Proves (exit 0 only if ALL hold): + 1. A receipt emitted for a socket-authed, capability-granted inference validates + against the vendored InferenceReceipt.schema.json. + 2. Successive receipts chain (ledgerPrevHash continuity; genesis has none). + 3. A TAMPERED entry breaks the chain and is rejected. + 4. A LOCAL-ONLY receipt (non-genesis entry with no ledgerPrevHash — i.e. not bound + into the estate ledger) is rejected by BOTH the schema and the verifier (SEAM-011). + 5. An OFF-DEVICE receipt without a capability lease + escalation is schema-rejected + (SEAM-015) — the socket-auth grant is load-bearing, not decorative. + +exit 0 = all teeth bite; 1 = a tooth failed to bite (regression). +""" +from __future__ import annotations + +import sys +import tempfile +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO / "src")) + +from agent_machine.inference_receipt import ( # noqa: E402 + canonical, emit_socket_inference_receipt, load_validator, sha256, verify_ledger, +) + +DIGEST = "sha256:" + "a" * 64 +AGENT = "urn:srcos:agent-passport:app-helper-42" +LEASE = "urn:srcos:lease:model-plane-inference-0001" +PROVIDER = "urn:srcos:inference-provider:local-llama-cpp" + + +def main() -> int: + validator = load_validator() + failures: list[str] = [] + + with tempfile.TemporaryDirectory() as d: + ledger = Path(d) / "inference-ledger.jsonl" + + # Tooth 1 + 2: emit chained, socket-authed receipts; whole ledger conforms + chains. + for i in range(3): + r = emit_socket_inference_receipt( + ledger, base_model_digest=DIGEST, task="agent_classification", + input_text=f"authed prompt {i}", output_text=f"completion {i}", + requesting_agent_ref=AGENT, capability_lease_ref=LEASE, provider_ref=PROVIDER, + ) + if list(validator.iter_errors(r)): + failures.append(f"emitted receipt {i} is schema-invalid") + if r.get("capabilityLeaseRef") != LEASE or r.get("requestingAgentRef") != AGENT: + failures.append(f"receipt {i} did not bind socket-auth grant/agent") + ok, msg = verify_ledger(ledger, validator) + if not ok: + failures.append(f"chain should be valid but: {msg}") + else: + print(f"OK conformance+chain: {msg}") + + # Tooth 3: tamper entry 1 -> chain must break. + lines = ledger.read_text(encoding="utf-8").splitlines() + import json + e1 = json.loads(lines[1]); e1["outputHash"] = sha256("tampered") + lines[1] = canonical(e1) + ledger.write_text("\n".join(lines) + "\n", encoding="utf-8") + ok, msg = verify_ledger(ledger, validator) + if ok: + failures.append("tampered receipt NOT detected — chain has no teeth") + else: + print(f"OK tamper-evidence: {msg}") + + # Tooth 4: local-only receipt (seq>=1, no ledgerPrevHash) rejected by schema AND verifier. + with tempfile.TemporaryDirectory() as d: + import json + ledger = Path(d) / "local-only.jsonl" + base = emit_socket_inference_receipt( + ledger, base_model_digest=DIGEST, task="t", input_text="a", output_text="b", + requesting_agent_ref=AGENT, capability_lease_ref=LEASE) + local_only = dict(base) + local_only["ledgerSeq"] = 1 + local_only.pop("ledgerPrevHash", None) # unchained -> local-only + schema_rejects = bool(list(validator.iter_errors(local_only))) + with ledger.open("a", encoding="utf-8") as f: + f.write(canonical(local_only) + "\n") + verifier_rejects = not verify_ledger(ledger, validator)[0] + if schema_rejects and verifier_rejects: + print("OK local-only rejected: schema + verifier both refuse an unchained entry") + else: + failures.append( + f"local-only entry accepted (schema_rejects={schema_rejects}, verifier_rejects={verifier_rejects})") + + # Tooth 5: off-device receipt without lease/escalation is schema-rejected (SEAM-015). + off_device = { + "id": "urn:srcos:inference-receipt:x-0", "type": "InferenceReceipt", + "specVersion": "2.1.0", "issuedAt": "2026-08-03T00:00:00Z", + "providerDaemon": "inferenced", "tier": "T3", "baseModelDigest": DIGEST, + "task": "t", "inputHash": sha256("a"), "outputHash": sha256("b"), + "dataResidencyClass": "external_permitted", # off-device, but no lease/escalation + "ledgerSeq": 0, + } + if list(validator.iter_errors(off_device)): + print("OK SEAM-015: off-device receipt without lease+escalation is rejected") + else: + failures.append("off-device receipt without lease/escalation was accepted (SEAM-015 hole)") + + if failures: + for f in failures: + print(f"FAIL: {f}", file=sys.stderr) + return 1 + print("PASS: T7-16 InferenceReceipt teeth all bite") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/agent_machine/inference_receipt.py b/src/agent_machine/inference_receipt.py new file mode 100644 index 0000000..721261f --- /dev/null +++ b/src/agent_machine/inference_receipt.py @@ -0,0 +1,192 @@ +"""InferenceReceipt emission for Agent Machine (Tranche 7 / T7-16). + +T7-16 = "Model plane capability grants + socket auth". Agent Machine grants a +model-plane capability to an agent pod and authenticates the local inference +socket; every completion served over that authenticated socket MUST leave a +provenance receipt (its InferenceProvider contract already declares +`policy.receiptRequired`). This module makes Agent Machine emit that receipt +NATIVELY, into the estate's one hash-chained ledger (SEAM-011: no local-only +ledger). + +CONSUME-NOT-FORK: the hash-chain / canonical-JSON / ledger machinery below is +VENDORED VERBATIM from the canonical emitter + SocioProphet/prophet-platform apps/receipt-gateway/tools/inference_receipt_emitter.py + @ commit abd98805, + sha256:6881246b8e41a515fb1b29df645faeb9be17d8195d22a7e0d5ce15f8477d8e6a +(canonical owner: SourceOS-Linux/sourceos-spec, Tranche 7). Do NOT re-implement +the chain here; refresh by re-copying from prophet-platform so the ledger stays +interoperable with the receipt-gateway. `emit_socket_inference_receipt` is the +only Agent-Machine-specific addition: it binds the capability grant +(capabilityLeaseRef) and requesting agent (requestingAgentRef) that socket auth +resolved, then delegates to the vendored `emit_receipt`. +""" + +from __future__ import annotations + +import hashlib +import json +from collections import deque +from datetime import datetime, timezone +from pathlib import Path + +try: + import fcntl # POSIX advisory locking (Linux/macOS) +except ImportError: # pragma: no cover + fcntl = None + +import jsonschema + +# Vendored schema shipped alongside the repo contracts (see contracts/model-plane/). +SCHEMA = Path(__file__).resolve().parents[2] / "contracts" / "model-plane" / "InferenceReceipt.schema.json" + +_UNSET = object() # distinguishes "estimate it" from an explicit None (record null) + + +# --- BEGIN vendored-verbatim block (prophet-platform inference_receipt_emitter.py) --- +def sha256(s: str) -> str: + return "sha256:" + hashlib.sha256(s.encode("utf-8")).hexdigest() + + +def canonical(obj: dict) -> str: + return json.dumps(obj, sort_keys=True, separators=(",", ":")) + + +def _last_entry(f) -> dict | None: + """Last non-blank ledger entry, constant memory (deque(maxlen=1) over the file).""" + f.seek(0) + tail = deque((line for line in f if line.strip()), maxlen=1) + return json.loads(tail[0]) if tail else None + + +def emit_receipt(ledger: Path, *, base_model_digest: str, task: str, input_text: str, + output_text: str, provider_daemon: str = "inferenced", tier: str = "T1", + tokenizer_digest: str | None = None, compute_device: str = "cpu", + input_token_count=_UNSET, output_token_count=_UNSET, + extra: dict | None = None) -> dict: + """Append one InferenceReceipt to the hash-chained ledger, return it. + + Read-tail + append happen under an exclusive advisory lock so concurrent emitters + cannot race into duplicate ledgerSeq or a broken chain (single-writer per entry). + + `extra` (Agent-Machine addition, not a chain change) merges caller-supplied + top-level fields such as requestingAgentRef / capabilityLeaseRef / providerRef + BEFORE the entry is chained, so they are covered by the tamper-evident hash. + """ + ledger.parent.mkdir(parents=True, exist_ok=True) + with ledger.open("a+", encoding="utf-8") as f: + if fcntl is not None: + fcntl.flock(f, fcntl.LOCK_EX) + try: + prev = _last_entry(f) + seq = (prev["ledgerSeq"] + 1) if prev else 0 + receipt = { + "id": f"urn:srcos:inference-receipt:{task}-{seq}", + "type": "InferenceReceipt", + "specVersion": "2.1.0", + "issuedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "providerDaemon": provider_daemon, + "tier": tier, + "baseModelDigest": base_model_digest, + "tokenizerDigest": tokenizer_digest, + "task": task, + "inputHash": sha256(input_text), + "inputTokenCount": len(input_text.split()) if input_token_count is _UNSET else input_token_count, + "outputHash": sha256(output_text), + "outputTokenCount": len(output_text.split()) if output_token_count is _UNSET else output_token_count, + "dataResidencyClass": "on_device_only", + "escalatedFrom": None, + "escalationChain": [], + "computeDevice": compute_device, + "ledgerSeq": seq, + } + if extra: + receipt.update(extra) + if seq >= 1: + # hash-chain: bind this entry to the canonical prior entry (SEAM-011) + receipt["ledgerPrevHash"] = sha256(canonical(prev)) + f.write(canonical(receipt) + "\n") + f.flush() + finally: + if fcntl is not None: + fcntl.flock(f, fcntl.LOCK_UN) + return receipt + + +def verify_ledger(ledger: Path, validator: "jsonschema.Draft202012Validator") -> tuple[bool, str]: + if not ledger.exists(): + return False, f"ledger not found: {ledger}" + try: + lines = [l for l in ledger.read_text(encoding="utf-8").splitlines() if l.strip()] + except OSError as exc: + return False, f"cannot read ledger: {exc}" + prev = None + for i, line in enumerate(lines): + try: + r = json.loads(line) + except json.JSONDecodeError as exc: + return False, f"entry {i} is not valid JSON: {exc}" + errs = sorted(validator.iter_errors(r), key=lambda e: list(e.path)) + if errs: + return False, f"entry {i} schema-invalid: {errs[0].message}" + if r["ledgerSeq"] != i: + return False, f"entry {i} has ledgerSeq {r['ledgerSeq']} (expected {i})" + if i >= 1: + expect = sha256(canonical(prev)) + if r.get("ledgerPrevHash") != expect: + return False, f"entry {i} chain broken: ledgerPrevHash != hash(entry {i-1})" + prev = r + return True, f"{len(lines)} receipts: schema-conformant + unbroken hash-chain" +# --- END vendored-verbatim block --- + + +def load_validator() -> "jsonschema.Draft202012Validator": + schema = json.loads(SCHEMA.read_text(encoding="utf-8")) + jsonschema.Draft202012Validator.check_schema(schema) + return jsonschema.Draft202012Validator(schema) + + +def emit_socket_inference_receipt( + ledger: Path, + *, + base_model_digest: str, + task: str, + input_text: str, + output_text: str, + requesting_agent_ref: str, + capability_lease_ref: str, + provider_ref: str | None = None, + provider_daemon: str = "inferenced", + tier: str = "T1", + tokenizer_digest: str | None = None, + compute_device: str = "cpu", +) -> dict: + """T7-16: emit a receipt for a completion served over the authenticated model socket. + + Socket auth resolved WHO asked (`requesting_agent_ref`, an AgentPassport URN) and + under WHICH grant (`capability_lease_ref`, an AgentCapabilityLease URN). Both are + recorded in the receipt and covered by the hash-chain. A socket-authed inference + with no capability grant is not authorized and must not be emitted, so both refs + are required arguments (fail-closed at the call site). + """ + if not requesting_agent_ref.startswith("urn:srcos:agent-passport:"): + raise ValueError("requesting_agent_ref must be an AgentPassport URN (urn:srcos:agent-passport:...)") + if not capability_lease_ref.startswith("urn:srcos:lease:"): + raise ValueError("capability_lease_ref must be an AgentCapabilityLease URN (urn:srcos:lease:...)") + extra = { + "requestingAgentRef": requesting_agent_ref, + "capabilityLeaseRef": capability_lease_ref, + } + if provider_ref is not None: + extra["providerRef"] = provider_ref + return emit_receipt( + ledger, + base_model_digest=base_model_digest, + task=task, + input_text=input_text, + output_text=output_text, + provider_daemon=provider_daemon, + tier=tier, + tokenizer_digest=tokenizer_digest, + compute_device=compute_device, + extra=extra, + )