diff --git a/CHANGELOG.md b/CHANGELOG.md index 27dcaea..d0b4f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Thi ### Added - Model Plane store manifests v0.1 (Tranche 7 / T7-2, resolves #242): `ModelManifest` (content-addressed base-model store manifest — `modelDigest` store key, architecture, quantization, format, tokenizer digest, context length, default tier, modalities) and `ModelAdapterManifest` (LoRA adapter manifest — `adapterDigest`, the `baseModelDigest` it binds to, task, rank, alpha, target modules, eval-report digest). Named `ModelAdapterManifest` (not `AdapterManifest`) to avoid collision with `AdapterDescriptor` (connector/actuation adapters). Complement, not duplicate, `SourceOSModelCarryRef` (a governance/carry *reference* pointing AT a manifest digest via `modelRef`; the manifest back-links via `carryRefs`). **Teeth (verified both ways):** a `ModelManifest` is schema-invalid without a `signature` descriptor (SEAM-014); a `ModelAdapterManifest` is invalid without `baseModelDigest` (SEAM-017, rejected on base mismatch) and without a `signature`; **both require an SPDX `license`** so the MIT/Apache-only rule is checkable before wiring (non-SPDX uses `LicenseRef-` e.g. `LicenseRef-Gemma`/`LicenseRef-Llama-Community`, visibly not Apache/MIT — closes Model Plane spec OQ3 at the contract layer). Includes canonical examples (`examples/model-manifest.json`, `examples/model-adapter-manifest.json`) and `docs/adr/0016-model-plane-store-manifests.md`. +- Model Plane provider classification + review-hardening (Tranche 7, resolves #243; ADR-0017). **#243 resolved without a core-ontology break:** rather than a sixth `AgentPassport.agent_class`, a model-serving daemon is a `system_core` host process (its `AgentPassport`) *plus* an `InferenceProvider` record; the two compose via a new optional `InferenceProvider.passportRef` (`urn:srcos:agent-passport:` URN). **Design-review teeth added to the T7 provenance schemas (verified both ways):** (1) *ledger hash-chain* — `InferenceReceipt`/`EscalationDecision`/`AdapterPromotionDecision` now require a non-null `ledgerPrevHash` for any non-genesis entry (`ledgerSeq >= 1`; `seq 0` exempt), so an enumerated contribution list cannot be retroactively rewritten; (2) *biometric hard boundary* — an `EscalationDecision` whose T0 `sensitivityCheck.sensitiveCategories` contains `biometric` is schema-forced to `verdict: refused` / `refusalReason: biometric-boundary`, so face/speaker embeddings cannot cross a tier boundary under any grant (new example `examples/escalation-decision.refused-biometric.json`). - Ingestion-Pipeline contract family v0.1 (the parse → chunk → embed seam): `IngestedDocument` (a source parsed to one canonical plaintext offset space, `extractionDigest` RECOMPUTED from the text), `Chunk` (a span of that text with an optional embedding), `ExtractedEntity` (an entity grounded at exact document spans), and `EmbeddingRequest` (the one OpenAI-compatible embed-call shape for the whole estate). Generalizes the Noetica `agent-machine/doc-store` pipeline (`extractText`/`chunkTextWithSpans`/`linkDocGrounds`/`hgPutChunk` + an ollama embedder) into a portable contract, ending the platform-services-not-Noetica-only duplication against `prophet-platform/apps/embeddings`. **Normative invariant — one vector space or none**: `EmbeddingRequest` pins the sovereign model (`nomic-ai/nomic-embed-text-v1.5`) and dimension (`768`) by `const`, and every embedded `Chunk` must name that same model/dimension and carry a vector of exactly that length — a request for another model or a Matryoshka-truncated dimension fails by construction, so two producers cannot write into two silently-incomparable spaces. **Span soundness**: the validator recomputes `documentText[start:end] == chunk.text` and `sha256(text) == contentHash`, so a chunk can never silently misquote the document it cites; grounding soundness holds the same bar for `ExtractedEntity` mentions. Includes measured/pre-embedding chunk examples, seven negative conformance vectors (`fixtures/ingestion-pipeline/conformance.json`), a `validate-ingestion-pipeline-examples` target, and a normative spec note (`specs/ingestion-pipeline-contract.md`). Intended reference implementations: Noetica `doc-store` (rewired to the shared service) as producer, `prophet-platform/apps/embeddings` as service. - Model Plane inference-provenance contracts v0.1 (Tranche 7 / T7-1, T7-3, T7-4): `InferenceReceipt` (the per-completion provenance primitive — tier, content-addressed base/adapter/tokenizer digests, serving daemon, data-residency class, escalation chain, ledger-bound), `EscalationDecision` (the governed tier / data-residency boundary crossing) and `AdapterPromotionDecision` (human-governed LoRA-adapter promotion). These sit **atop** the existing Agent Machine / Model Carry family and reference `InferenceProvider`, `ModelResidency`, `SourceOSModelCarryRef`, and `ExternalModelProviderProfile` rather than restate them; grant references use the estate's `urn:srcos:lease:` (AgentCapabilityLease), not the spec's illustrative `urn:srcos:grant:`. **The governance gates are schema `if/then` teeth, verified to fire both ways:** an off-device `InferenceReceipt` cannot validate without an authorizing lease and a non-empty escalation chain (SEAM-015); an `EscalationDecision` cannot be `permitted` without an authorizing `capabilityLeaseRef` *and* a passing T0 sensitivity check, with `prompt-unanswered` as the fail-closed resolution for non-interactive workloads (SEAM-015); an `AdapterPromotionDecision` cannot be `promoted` without a verified signature, per-event training consent, all eval gates passing (including an adversarial-poisoning probe), a named human promoter, and a mandatory rollback target (SEAM-016/017). All three carry optional `ledgerPrevHash` (append-only-ledger hash-chaining, so an enumerated contribution list cannot be retroactively rewritten) and the receipt carries optional `confidenceMethod` (the escalation trigger is self-reported — recording the method makes it auditable). Note: `AdapterPromotionDecision` governs *model* (LoRA) adapters and is distinct from `AdapterDescriptor` (connector/actuation adapters). Includes canonical examples (`examples/inference-receipt.json`, `examples/escalation-decision.json`, `examples/adapter-promotion-decision.json`) and `docs/adr/0015-model-plane-inference-provenance.md`, which records the reconciliation and the two deferred T7 decisions — `ModelManifest`/`AdapterManifest` (T7-2, overlaps `SourceOSModelCarryRef` + name collision) and adding `inference_provider` to `AgentPassport.agent_class` (T7-5, a core-ontology change vs the deliberate separate-object `InferenceProvider` modeling). - DeviceService contract v0.1 (FOG & CITIZEN PLANE W8.7 — the estate's first southbound device abstraction): `DeviceProfile` (what a device IS — protocol, typed protocol binding, and the exact metric set it produces with units, value types, operating ranges and protocol-native source addresses; digest-pinned by a `definitionDigest` the validator RECOMPUTES) and `DeviceReading` (one observation — deviceRef, metric, value, unit, quality, observedAt, carried on the MPCC `ConversationEvent` envelope with byte-identical sub-schemas, parity machine-enforced). EdgeX Foundry's lesson landed as a contract: ONE southbound interface, N protocol drivers — a driver speaks a protocol, it does not invent a vocabulary. **Normative invariant — a reading is ATTRIBUTABLE OR IT IS NOTHING**: `deviceRef`, `deviceProfileRef`, `profileDigest`, `metric`, `sourceAddress` and `unit` are all required, and the validator resolves every one of them across the example set (profile exists, digest matches the recomputed declaration, metric is declared, unit and source address agree, value is of the declared type and inside the declared range, `provenanceLinks` independently name both device and profile). Digest-pinning closes the retroactive-legalisation path — widening a range after the fact produces a new digest and orphans the readings it was meant to legalise, the `UpdateHealthProbe` construct applied to metrology. Quality is closed at five values with `unavailable` schema-bound to a null value plus a `NullAbsenceRecord` reference (the existing 12-kind MPCC absence taxonomy is reused, not re-invented); `stale` and `substituted` are normatively not `ok`. Simulated devices are a first-class labelled member of the protocol taxonomy (`protocol: "virtual"` ⇒ `synthetic:simulated-device`, enforced in both directions) — the `model-generated` admissibility rule applied to sensors. `observedAt`/`receivedAt`/`wallTime` stay distinct because `observedAt` → `receivedAt` is the southbound latency a twin's sync budget is spent on. Read-only at v0.1 (`access` closed to `"read"`): actuation must travel the `EffectRequest` → `EffectDecision` lifecycle. Distinct from `DeviceIdentity` (operator-workstation trust, bound via `identityRef`) and `TelemetryEvent` (agent-session diagnostics). Includes a BLE-GATT and a virtual profile plus measured and typed-absence reading examples, twenty-one negative conformance vectors (`fixtures/device-service/conformance.json`), a `validate-device-service-examples` target, and a normative spec note (`specs/device-service-contract.md`). Reference implementation: `device-service` in `SocioProphet/prophet-platform`. diff --git a/docs/adr/0017-model-plane-provider-classification-and-hardening.md b/docs/adr/0017-model-plane-provider-classification-and-hardening.md new file mode 100644 index 0000000..23aad6e --- /dev/null +++ b/docs/adr/0017-model-plane-provider-classification-and-hardening.md @@ -0,0 +1,43 @@ +# ADR-0017: Model Plane provider classification + review-hardening (Tranche 7) + +**Date:** 2026-08-02 +**Status:** `Accepted` + +--- + +## Context + +Two open items from ADR-0015 / the Model Plane design review: + +1. **T7-5 / #243** — the spec (§VIII) proposed adding a sixth `AgentPassport.agent_class`, `inference_provider`. But the estate already models inference providers as the separate `InferenceProvider` object, and `AgentPassport`'s five-class model (ADR-0014) is enforced as *exactly five* by `tools/validate_agent_passport_examples.py`. +2. **Design-review findings** with a schema-expressible fix: the ledger is the trust anchor for every provenance claim but `ledgerSeq` alone did not bind entry N to N-1 (finding #4); and the biometric boundary was described but not enforced at the escalation contract (finding #7). + +## Decision + +**#243 — do NOT add a sixth `agent_class`.** A model-serving daemon (`inferenced`, `visiond`, `embeddingd`, `distilld`) is a host process classified by its `AgentPassport` (typically `system_core` for a platform daemon) **and** described operationally by an `InferenceProvider` record (trust posture, network requirement, modalities). The two compose. To make the composition concrete, add an optional `InferenceProvider.passportRef` (`urn:srcos:agent-passport:` URN) linking the operational provider to its host-process classification. This keeps the five-class model intact and honors the estate's deliberate separate-object modeling. + +**Review-hardening (schema teeth, verified both ways):** +- **Ledger hash-chain (finding #4):** on `InferenceReceipt`, `EscalationDecision`, and `AdapterPromotionDecision`, any non-genesis entry (`ledgerSeq >= 1`) must carry a non-null `ledgerPrevHash`. `ledgerSeq: 0` (genesis) is exempt. The append-only ledger is now hash-chained, so an enumerated contribution list cannot be retroactively rewritten. +- **Biometric hard boundary (finding #7):** on `EscalationDecision`, if the T0 `sensitivityCheck.sensitiveCategories` contains `biometric`, the crossing is schema-forced to `verdict: refused` with `refusalReason: biometric-boundary`. A `permitted` crossing carrying a biometric category is now schema-invalid — face/speaker embeddings cannot cross a tier boundary under any grant. + +## Alternatives considered + +| Alternative | Reason not chosen | +|-------------|------------------| +| Add `inference_provider` as a 6th `agent_class` | Breaks the `EXPECTED_CLASSES` "exactly five" invariant + ADR-0014; duplicates governance already carried by `InferenceProvider` + Model Plane receipts/leases. | +| Leave provider↔passport link implicit | Then "compose, don't add a class" is unenforceable/untraceable; the optional `passportRef` makes it explicit without a breaking change. | +| Make `ledgerPrevHash` always required | Breaks a legitimate genesis (`seq: 0`) entry; `>= 1` is the correct condition. | +| Enforce biometric boundary only in SHACL/impl | The escalation contract is exactly where the crossing decision lives; a schema gate is the earliest, cheapest refusal. | + +## Consequences + +- Positive: #243 resolved without a core-ontology break; two review findings closed as contract-enforced teeth. +- Positive: `passportRef` gives ontogenesis/agent-machine a typed link from an operational provider to its host passport. +- Follow-up (non-schema findings, filed separately): confidence self-report/router-vs-confidence contradiction (spec §II/§VII text), served==receipt emission reconciliation (workstation-contracts T7-19), trust concentration in T0 and the boundary-crossing incentive gradient (spec/ADR discussion), and the cross-document `sensitiveCategories`-completeness guarantee (ontogenesis SHACL T7-8). Schema `if/then` cannot express these. + +## References + +- SourceOS Model Plane — Architecture Specification v0.1, §II, §VII, §VIII +- ADR-0014 (five-class AgentPassport), ADR-0015 (inference-provenance schemas) +- Issue #243 (this decision); epic #241 +- Changed: `schemas/InferenceProvider.json` (+`passportRef`), `schemas/InferenceReceipt.json`, `schemas/EscalationDecision.json`, `schemas/AdapterPromotionDecision.json` diff --git a/examples/escalation-decision.refused-biometric.json b/examples/escalation-decision.refused-biometric.json new file mode 100644 index 0000000..41bcc28 --- /dev/null +++ b/examples/escalation-decision.refused-biometric.json @@ -0,0 +1,30 @@ +{ + "id": "urn:srcos:escalation-decision:2026-06-09-4472-biometric", + "type": "EscalationDecision", + "specVersion": "2.1.0", + "decidedAt": "2026-06-09T22:03:10Z", + "machineRef": "urn:srcos:agent-machine:workstation-01", + "task": "face_recognition_assist", + "fromTier": "T0", + "toTier": "T3", + "fromResidencyClass": "on_device_only", + "toResidencyClass": "sovereign_cluster", + "trigger": "low-confidence", + "sensitivityCheck": { + "performed": true, + "result": "fail", + "classifierDigest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "sensitiveCategories": ["biometric"] + }, + "capabilityLeaseRef": null, + "targetProviderRef": null, + "targetExternalProviderRef": null, + "inputHash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "verdict": "refused", + "refusalReason": "biometric-boundary", + "policyDecisionRef": "urn:srcos:decision:escalate-4472", + "ledgerSeq": 4472, + "ledgerPrevHash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "evidenceHash": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "evidenceRefs": [] +} diff --git a/schemas/AdapterPromotionDecision.json b/schemas/AdapterPromotionDecision.json index 04adbaf..780c3f4 100644 --- a/schemas/AdapterPromotionDecision.json +++ b/schemas/AdapterPromotionDecision.json @@ -16,7 +16,8 @@ "evalReportDigest", "evalGates", "verdict", - "rollbackTargetDigest" + "rollbackTargetDigest", + "ledgerSeq" ], "properties": { "id": { "type": "string", "pattern": "^urn:srcos:adapter-promotion-decision:", "description": "Stable URN identifier. Pattern: urn:srcos:adapter-promotion-decision:" }, @@ -89,6 +90,17 @@ "required": ["rejectionReason"], "properties": { "rejectionReason": { "type": "string" } } } + }, + { + "$comment": "REVIEW HARDENING (ledger tamper-evidence): any non-genesis entry (ledgerSeq >= 1) must carry the prior entry's hash, so a promotion's enumerated contribution list cannot be rewritten after the fact.", + "if": { + "properties": { "ledgerSeq": { "minimum": 1 } }, + "required": ["ledgerSeq"] + }, + "then": { + "required": ["ledgerPrevHash"], + "properties": { "ledgerPrevHash": { "type": "string" } } + } } ] } diff --git a/schemas/EscalationDecision.json b/schemas/EscalationDecision.json index 369b72d..00d0a2b 100644 --- a/schemas/EscalationDecision.json +++ b/schemas/EscalationDecision.json @@ -19,7 +19,8 @@ "trigger", "sensitivityCheck", "inputHash", - "verdict" + "verdict", + "ledgerSeq" ], "properties": { "id": { "type": "string", "pattern": "^urn:srcos:escalation-decision:", "description": "Stable URN identifier. Pattern: urn:srcos:escalation-decision:" }, @@ -98,6 +99,36 @@ "required": ["targetExternalProviderRef"], "properties": { "targetExternalProviderRef": { "type": "string" } } } + }, + { + "$comment": "SEAM-015 BIOMETRIC HARD BOUNDARY: if the T0 sensitivity check flags a biometric category (face/speaker embeddings), no crossing is permitted under any grant — the verdict must be refused with refusalReason biometric-boundary. A permitted crossing carrying a biometric category is schema-invalid.", + "if": { + "properties": { + "sensitivityCheck": { + "properties": { "sensitiveCategories": { "contains": { "const": "biometric" } } }, + "required": ["sensitiveCategories"] + } + }, + "required": ["sensitivityCheck"] + }, + "then": { + "required": ["refusalReason"], + "properties": { + "verdict": { "const": "refused" }, + "refusalReason": { "const": "biometric-boundary" } + } + } + }, + { + "$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.", + "if": { + "properties": { "ledgerSeq": { "minimum": 1 } }, + "required": ["ledgerSeq"] + }, + "then": { + "required": ["ledgerPrevHash"], + "properties": { "ledgerPrevHash": { "type": "string" } } + } } ] } diff --git a/schemas/InferenceProvider.json b/schemas/InferenceProvider.json index 1289a3a..93cd1b9 100644 --- a/schemas/InferenceProvider.json +++ b/schemas/InferenceProvider.json @@ -17,6 +17,7 @@ "supportedModalities": { "type": "array", "minItems": 1, "description": "Modalities the provider can serve.", "items": { "type": "string", "enum": ["text", "code", "embedding", "reranking", "vision", "audio", "video", "multimodal", "tool-calling"] } }, "openAICompatible": { "type": "boolean", "description": "Whether the provider exposes an OpenAI-compatible API surface." }, "requiresNetwork": { "type": "boolean", "description": "Whether the provider requires network egress beyond loopback/local IPC." }, + "passportRef": { "type": ["string", "null"], "pattern": "^urn:srcos:agent-passport:", "description": "Optional AgentPassport URN of the host process that runs this provider daemon. A model-serving daemon is classified by its AgentPassport (typically system_core for a platform daemon) AND described operationally by this InferenceProvider record — the two compose, which is why the Model Plane does not add a sixth agent_class (ADR-0017)." }, "evidenceRefs": { "type": "array", "description": "Evidence URNs or hashes for provider validation, binary provenance, and compatibility testing.", "items": { "type": "string" } } } } diff --git a/schemas/InferenceReceipt.json b/schemas/InferenceReceipt.json index 054bb2e..66ecf7e 100644 --- a/schemas/InferenceReceipt.json +++ b/schemas/InferenceReceipt.json @@ -77,6 +77,17 @@ "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/schemas/README.md b/schemas/README.md index 0db55b9..c3b1c92 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -19,13 +19,13 @@ The governed-inference provenance layer atop the existing Agent Machine / Model These types make on-device inference auditable in the ways Apple Intelligence's silent loop does not: - **`InferenceReceipt`** — the provenance primitive emitted for every completion: tier, content-addressed base/adapter/tokenizer digests, serving daemon, the data-residency class served under, and the escalation chain. Ledger-bound (a local-only ledger is not permitted, SEAM-011). Off-device receipts (`sovereign_cluster`/`external_permitted`) are schema-required to carry an authorizing lease and a non-empty escalation chain — possession of the output is not authorization for the crossing (SEAM-015). -- **`EscalationDecision`** — the governed record of a tier / data-residency boundary crossing. Fail-closed by construction: a `permitted` verdict is schema-impossible without an authorizing capability lease **and** a passing T0 sensitivity check; ∅-grant or an unanswered background consent prompt (`refusalReason: prompt-unanswered`) resolves to refusal, never a silent downgrade (SEAM-015). +- **`EscalationDecision`** — the governed record of a tier / data-residency boundary crossing. Fail-closed by construction: a `permitted` verdict is schema-impossible without an authorizing capability lease **and** a passing T0 sensitivity check; ∅-grant or an unanswered background consent prompt (`refusalReason: prompt-unanswered`) resolves to refusal, never a silent downgrade (SEAM-015). **Biometric hard boundary:** a crossing whose T0 sensitivity check flags a `biometric` category is schema-forced to `refused`/`biometric-boundary` — face/speaker embeddings never cross a tier boundary under any grant. - **`AdapterPromotionDecision`** — adapter promotion as a human-governed decision, never an automatic OS update. Enumerates every contributing `OverrideEvent` (the property Apple's loop lacks), and a `promoted` verdict is schema-impossible without a verified signature, per-event training consent, all eval gates passing (including an adversarial-poisoning probe), a named human promoter, and a mandatory rollback target (SEAM-016, SEAM-017). Governs *model* (LoRA) adapters — distinct from `AdapterDescriptor` (connector/actuation adapters). - **`ModelManifest` / `ModelAdapterManifest`** (T7-2) — the content-addressed *store* manifests sitting beside the weights. Distinct from `SourceOSModelCarryRef` (a governance/carry *reference* that points AT a manifest digest). A valid manifest is schema-impossible without signature info (SEAM-014) and — for the adapter — the `baseModelDigest` it binds to (SEAM-017, rejected on mismatch); both require an SPDX `license` so the estate's MIT/Apache-only rule is checkable before wiring. The adapter manifest is named `ModelAdapterManifest`, not `AdapterManifest`, to avoid colliding with `AdapterDescriptor` (connector adapters). -All three provenance types carry an optional `ledgerPrevHash` (hash-chain the append-only ledger so an enumerated contribution list cannot be retroactively rewritten) and, on the receipt, an optional `confidenceMethod` (the escalation trigger is self-reported — recording the method makes it auditable). +All three provenance types hash-chain the append-only ledger: `ledgerPrevHash` is required for any non-genesis entry (`ledgerSeq >= 1`), so an enumerated contribution list cannot be retroactively rewritten; the receipt also carries an optional `confidenceMethod` (the escalation trigger is self-reported — recording the method makes it auditable). A model-serving daemon is not a new agent class — it is a `system_core` host process (its `AgentPassport`) plus an `InferenceProvider` record, linked via `InferenceProvider.passportRef` (ADR-0017). -Validation: `ajv validate -s schemas/.json -d examples/.json`. Canonical examples: `examples/inference-receipt.json`, `examples/escalation-decision.json`, `examples/adapter-promotion-decision.json`, `examples/model-manifest.json`, `examples/model-adapter-manifest.json`. ADRs: `docs/adr/0015-model-plane-inference-provenance.md`, `docs/adr/0016-model-plane-store-manifests.md`. +Validation: `ajv validate -s schemas/.json -d examples/.json`. Canonical examples: `examples/inference-receipt.json`, `examples/escalation-decision.json`, `examples/escalation-decision.refused-biometric.json`, `examples/adapter-promotion-decision.json`, `examples/model-manifest.json`, `examples/model-adapter-manifest.json`. ADRs: `docs/adr/0015-model-plane-inference-provenance.md`, `docs/adr/0016-model-plane-store-manifests.md`, `docs/adr/0017-model-plane-provider-classification-and-hardening.md`. ---