From 9a53021b79d42738b1f330efdf5990b8469a0c9e Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:29:12 -0400 Subject: [PATCH 1/2] feat(schemas): Model Plane store manifests ModelManifest + ModelAdapterManifest (T7-2) Resolves #242. Content-addressed store manifests complementing (not duplicating) SourceOSModelCarryRef. ModelAdapterManifest named to avoid AdapterDescriptor collision. Teeth verified both ways: ModelManifest invalid w/o signature (SEAM-014); ModelAdapterManifest invalid w/o baseModelDigest (SEAM-017) or signature; both require SPDX license (MIT/Apache-only checkable, closes OQ3). See ADR-0016. --- CHANGELOG.md | 1 + docs/adr/0016-model-plane-store-manifests.md | 51 ++++++++++++++++++++ examples/model-adapter-manifest.json | 23 +++++++++ examples/model-manifest.json | 23 +++++++++ schemas/ModelAdapterManifest.json | 50 +++++++++++++++++++ schemas/ModelManifest.json | 48 ++++++++++++++++++ schemas/README.md | 7 ++- 7 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 docs/adr/0016-model-plane-store-manifests.md create mode 100644 examples/model-adapter-manifest.json create mode 100644 examples/model-manifest.json create mode 100644 schemas/ModelAdapterManifest.json create mode 100644 schemas/ModelManifest.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5507d66..27dcaea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Thi ## [Unreleased] ### 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`. - 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/0016-model-plane-store-manifests.md b/docs/adr/0016-model-plane-store-manifests.md new file mode 100644 index 0000000..0f98e11 --- /dev/null +++ b/docs/adr/0016-model-plane-store-manifests.md @@ -0,0 +1,51 @@ +# ADR-0016: Model Plane store manifests (Tranche 7, T7-2) + +**Date:** 2026-08-02 +**Status:** `Accepted` + +--- + +## Context + +Tranche 7 (T7-2) calls for `ModelManifest` and `AdapterManifest` — the content-addressed store manifests the Model Plane spec §IV places beside the weights (`/var/lib/sourceos/models/{base,adapters}/sha256-/manifest.json` + `signature.sig`). ADR-0015 deferred this pending two reconciliations (tracked as issue #242): + +1. The store manifest appears to overlap `SourceOSModelCarryRef`. +2. "AdapterManifest" collides in name/concept with the existing `AdapterDescriptor`, which types **connector/actuation** adapters (api/cli/sdk/event/file). + +## Decision + +Add two new schemas, `ModelManifest` and `ModelAdapterManifest`. + +- **`ModelManifest`** — the content-addressed manifest for base-model weights at rest: `modelDigest` (the store key), architecture, quantization, format, tokenizer digest, context length, default tier, modalities, `license`, and a required `signature` descriptor. +- **`ModelAdapterManifest`** — the LoRA adapter manifest: `adapterDigest`, the `baseModelDigest` it binds to, task, rank, alpha, target modules, format, `license`, a required `signature`, and an optional `evalReportDigest`. + +**Naming:** the adapter manifest is `ModelAdapterManifest`, **not** `AdapterManifest`, so it does not collide with `AdapterDescriptor`. This resolves reconciliation (2). + +**Relationship to `SourceOSModelCarryRef`:** the manifest and the carry-ref are complementary, not duplicative. The manifest is the store-level content-addressed truth about what the weights *are*; `SourceOSModelCarryRef` is the governance/policy wrapper about how the OS may *carry/prepare* a model reference (carryPolicy, cachePolicy, `mutableModelState: false`, router/governance refs). A carry-ref's `modelRef` points at a manifest digest; the manifest's optional `carryRefs` back-link to carry-refs. This resolves reconciliation (1). + +**Teeth (schema `if/then`/required, verified both ways):** +- SEAM-014 — a `ModelManifest` is invalid without a `signature` (no model loads without signature verification). +- SEAM-017 — a `ModelAdapterManifest` is invalid without `baseModelDigest` (rejected on mismatch) and without a `signature`. +- MIT/Apache-only estate rule — both require an SPDX `license`, so the rule is checkable before wiring; non-SPDX licenses use `LicenseRef-` (e.g. `LicenseRef-Gemma`, `LicenseRef-Llama-Community`), which are visibly NOT Apache/MIT. This closes Model Plane spec OQ3 at the contract layer. + +## Alternatives considered + +| Alternative | Reason not chosen | +|-------------|------------------| +| Name it `AdapterManifest` per the spec text | Collides with `AdapterDescriptor` (connector adapters) — a downstream-mapping hazard. | +| Extend `SourceOSModelCarryRef` instead of new manifests | The carry-ref is a governance reference with policy, not a content-addressed store record; conflating them would overload one type with two concerns. | +| Reuse `ArtifactManifest`/`PackageManifest` | Generic artifact/package manifests do not carry model-specific fields (architecture, quantization, LoRA rank/alpha, base-model binding). | +| Put signature/license as optional | Then SEAM-014/017 and the MIT/Apache rule would not be enforceable at the contract layer. | + +## Consequences + +- Positive: `AdapterPromotionDecision` (ADR-0015) `candidateAdapterDigest`/`baseModelDigest` now reference real manifest digests; SEAM-014/017 and the license rule are contract-enforced. +- Positive: the store layout in source-os T7-11 (`modelplaned`) now has typed manifests to read/verify. +- Follow-up (SHACL, ontogenesis T7-8): the cross-document invariant `ModelAdapterManifest.baseModelDigest == ModelManifest.modelDigest` (when `baseModelManifestRef` is set) and `carryRef.modelRef == manifest.modelDigest` are cross-document and cannot be expressed in JSON Schema `if/then`. + +## References + +- SourceOS Model Plane — Architecture Specification v0.1, §IV, §IX (SEAM-014/017), §XIV (T7-2) +- ADR-0015 — Model Plane inference-provenance schemas (deferred T7-2 here) +- Issue #242 (this decision); epic #241 +- Reused/related: `schemas/SourceOSModelCarryRef.json`, `schemas/AdapterDescriptor.json`, `schemas/AdapterPromotionDecision.json` diff --git a/examples/model-adapter-manifest.json b/examples/model-adapter-manifest.json new file mode 100644 index 0000000..417a057 --- /dev/null +++ b/examples/model-adapter-manifest.json @@ -0,0 +1,23 @@ +{ + "id": "urn:srcos:model-adapter-manifest:summarization-v3", + "type": "ModelAdapterManifest", + "specVersion": "2.1.0", + "adapterDigest": "sha256:8888888888888888888888888888888888888888888888888888888888888888", + "displayName": "summarization-v3", + "baseModelDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "baseModelManifestRef": "urn:srcos:model-manifest:generalist-3b-q4k", + "task": "summarization", + "rank": 16, + "alpha": 32, + "targetModules": ["q_proj", "v_proj", "o_proj"], + "format": "safetensors", + "tokenizerDigest": null, + "license": "Apache-2.0", + "signature": { + "algorithm": "ed25519", + "keyId": "srcos-model-signing-2026", + "signatureDigest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "evalReportDigest": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "evidenceRefs": ["urn:srcos:adapter-promotion-decision:2026-06-10-summ-v3"] +} diff --git a/examples/model-manifest.json b/examples/model-manifest.json new file mode 100644 index 0000000..ea9ccc8 --- /dev/null +++ b/examples/model-manifest.json @@ -0,0 +1,23 @@ +{ + "id": "urn:srcos:model-manifest:generalist-3b-q4k", + "type": "ModelManifest", + "specVersion": "2.1.0", + "modelDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "displayName": "generalist-3b", + "architecture": "llama", + "parameterCount": 3200000000, + "format": "gguf", + "quantization": "q4_k_m", + "tokenizerDigest": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "contextLength": 8192, + "defaultTier": "T1", + "modalities": ["text"], + "license": "Apache-2.0", + "signature": { + "algorithm": "ed25519", + "keyId": "srcos-model-signing-2026", + "signatureDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "carryRefs": ["urn:srcos:model-carry-ref:generalist-3b-q4k"], + "evidenceRefs": [] +} diff --git a/schemas/ModelAdapterManifest.json b/schemas/ModelAdapterManifest.json new file mode 100644 index 0000000..247ac04 --- /dev/null +++ b/schemas/ModelAdapterManifest.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/ModelAdapterManifest.json", + "title": "ModelAdapterManifest", + "description": "Content-addressed store manifest for a LoRA (model) adapter in the Model Plane store (Tranche 7, T7-2). Sits at /var/lib/sourceos/models/adapters/sha256-/manifest.json. Named ModelAdapterManifest — NOT AdapterManifest — to avoid collision with AdapterDescriptor, which types connector/actuation adapters (api/cli/sdk), an unrelated sense of \"adapter\". A valid adapter manifest MUST declare the base-model digest it binds to (SEAM-017: rejected on mismatch) and MUST carry signature info (SEAM-017 signature verification). Referenced by AdapterPromotionDecision via candidateAdapterDigest / baseModelDigest.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "specVersion", + "adapterDigest", + "baseModelDigest", + "task", + "rank", + "alpha", + "format", + "license", + "signature" + ], + "properties": { + "id": { "type": "string", "pattern": "^urn:srcos:model-adapter-manifest:", "description": "Stable URN identifier. Pattern: urn:srcos:model-adapter-manifest:" }, + "type": { "const": "ModelAdapterManifest", "description": "Discriminator constant — always \"ModelAdapterManifest\"." }, + "specVersion": { "type": "string", "description": "Spec version of this document, e.g. \"2.1.0\"." }, + "adapterDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Content address of the adapter weights — the directory key in the content-addressed store." }, + "displayName": { "type": ["string", "null"], "description": "Human-readable adapter name, e.g. \"summarization-v3\"." }, + "baseModelDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Digest of the base model this adapter binds to. Loading against a base whose digest differs is rejected (SEAM-017)." }, + "baseModelManifestRef": { "type": ["string", "null"], "pattern": "^urn:srcos:model-manifest:", "description": "Optional URN of the ModelManifest for the base model; its modelDigest must equal baseModelDigest." }, + "task": { "type": "string", "description": "Task the adapter specializes, e.g. summarization, translation-de-en, agent_classification." }, + "rank": { "type": "integer", "minimum": 1, "description": "LoRA rank (r)." }, + "alpha": { "type": "number", "exclusiveMinimum": 0, "description": "LoRA scaling factor (alpha)." }, + "targetModules": { "type": "array", "description": "Attention/MLP submodules the adapter targets, e.g. q_proj, v_proj, o_proj, gate_proj.", "items": { "type": "string" } }, + "format": { "type": "string", "enum": ["safetensors", "gguf", "mlx"], "description": "On-disk adapter format." }, + "tokenizerDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Tokenizer digest if the adapter changes tokenization; null to inherit the base model's tokenizer." }, + "license": { "type": "string", "description": "SPDX license identifier for the adapter weights (e.g. Apache-2.0, MIT). Required so the MIT/Apache-only rule is checkable; a distilled adapter also inherits any base-model license constraint." }, + "signature": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "keyId", "signatureDigest"], + "description": "Detached-signature descriptor. Required — no adapter loads without signature verification (SEAM-017).", + "properties": { + "algorithm": { "type": "string", "description": "Signature algorithm, e.g. ed25519, ecdsa-p256, rsa-pss-sha256." }, + "keyId": { "type": "string", "description": "Trust-store key identifier that must verify this signature." }, + "signatureDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Digest of the detached signature blob stored beside the manifest." } + } + }, + "evalReportDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Content-addressed digest of the eval report produced at training time; referenced by the AdapterPromotionDecision that promotes this adapter." }, + "evidenceRefs": { "type": "array", "description": "Evidence URNs or content hashes for training provenance, contributing override events, and eval.", "items": { "type": "string" } } + } +} diff --git a/schemas/ModelManifest.json b/schemas/ModelManifest.json new file mode 100644 index 0000000..9c9c6af --- /dev/null +++ b/schemas/ModelManifest.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/ModelManifest.json", + "title": "ModelManifest", + "description": "Content-addressed store manifest for a base model in the Model Plane store (Tranche 7, T7-2). Sits at /var/lib/sourceos/models/base/sha256-/manifest.json and describes the weights at rest: architecture, quantization, tokenizer digest, format, and the detached signature. Distinct from SourceOSModelCarryRef (a governance/carry *reference* with policy, which points AT a manifest digest) and from ArtifactManifest/PackageManifest (generic artifacts). No model loads without signature verification (SEAM-014), so a valid manifest MUST carry signature info; and it MUST declare a license so the MIT/Apache-only estate rule is checkable before wiring.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "specVersion", + "modelDigest", + "architecture", + "format", + "quantization", + "license", + "signature" + ], + "properties": { + "id": { "type": "string", "pattern": "^urn:srcos:model-manifest:", "description": "Stable URN identifier. Pattern: urn:srcos:model-manifest:" }, + "type": { "const": "ModelManifest", "description": "Discriminator constant — always \"ModelManifest\"." }, + "specVersion": { "type": "string", "description": "Spec version of this document, e.g. \"2.1.0\"." }, + "modelDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Content address of the model weights — the directory key in the content-addressed store." }, + "displayName": { "type": ["string", "null"], "description": "Human-readable model name, e.g. \"generalist-3b\"." }, + "architecture": { "type": "string", "description": "Model architecture family, e.g. llama, qwen2, gemma2, phi3." }, + "parameterCount": { "type": ["integer", "null"], "minimum": 0, "description": "Approximate parameter count, or null when unknown." }, + "format": { "type": "string", "enum": ["gguf", "safetensors", "mlx", "onnx"], "description": "On-disk weight format." }, + "quantization": { "type": "string", "description": "Quantization/precision label, e.g. q4_k_m, q2_k, int8, mixed-2-4bit, bf16." }, + "tokenizerDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Content-addressed digest of the tokenizer, or null when the tokenizer is embedded in the weights." }, + "contextLength": { "type": ["integer", "null"], "minimum": 0, "description": "Maximum context length in tokens, or null when unknown." }, + "defaultTier": { "type": ["string", "null"], "enum": ["T0", "T1", "T2", "T3", "T4", null], "description": "The Model Plane tier this model is normally resident at." }, + "modalities": { "type": "array", "description": "Modalities the model serves.", "items": { "type": "string", "enum": ["text", "code", "embedding", "reranking", "vision", "audio", "video", "multimodal"] } }, + "license": { "type": "string", "description": "SPDX license identifier for the model weights, e.g. \"Apache-2.0\", \"MIT\". Required so the estate's MIT/Apache-only rule can be enforced before a model is wired; use \"LicenseRef-\" for non-SPDX licenses (e.g. LicenseRef-Gemma, LicenseRef-Llama-Community), which are NOT MIT/Apache and must be flagged." }, + "signature": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "keyId", "signatureDigest"], + "description": "Detached-signature descriptor. Required — no model loads without signature verification against the trust store (SEAM-014).", + "properties": { + "algorithm": { "type": "string", "description": "Signature algorithm, e.g. ed25519, ecdsa-p256, rsa-pss-sha256." }, + "keyId": { "type": "string", "description": "Trust-store key identifier that must verify this signature." }, + "signatureDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Digest of the detached signature blob (signature.sig) stored beside the manifest." } + } + }, + "carryRefs": { "type": "array", "description": "Optional back-links to SourceOSModelCarryRef URNs that carry this model.", "items": { "type": "string", "pattern": "^urn:srcos:model-carry-ref:" } }, + "evidenceRefs": { "type": "array", "description": "Evidence URNs or content hashes for provenance, checksums, and quantization procedure.", "items": { "type": "string" } } + } +} diff --git a/schemas/README.md b/schemas/README.md index cb1d355..0db55b9 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -14,15 +14,18 @@ The governed-inference provenance layer atop the existing Agent Machine / Model | `InferenceReceipt.json` | InferenceReceipt | `urn:srcos:inference-receipt:` | | `EscalationDecision.json` | EscalationDecision | `urn:srcos:escalation-decision:` | | `AdapterPromotionDecision.json` | AdapterPromotionDecision | `urn:srcos:adapter-promotion-decision:` | +| `ModelManifest.json` | ModelManifest | `urn:srcos:model-manifest:` | +| `ModelAdapterManifest.json` | ModelAdapterManifest | `urn:srcos:model-adapter-manifest:` | 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). - **`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 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 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). -Validation: `ajv validate -s schemas/.json -d examples/.json`. Canonical examples: `examples/inference-receipt.json`, `examples/escalation-decision.json`, `examples/adapter-promotion-decision.json`. ADR: `docs/adr/0015-model-plane-inference-provenance.md`. +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`. --- From b5da77f3890b4bdcc459f1b2840be26cc5a98d30 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:35:57 -0400 Subject: [PATCH 2/2] fix(schemas): address Copilot review on manifests - ModelAdapterManifest.targetModules now required + minItems:1 + uniqueItems (a LoRA adapter that doesn't declare target modules is not applicable) - ModelManifest.modalities minItems:1 + uniqueItems when present Negative tests added for empty/missing/duplicate cases. --- schemas/ModelAdapterManifest.json | 3 ++- schemas/ModelManifest.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/schemas/ModelAdapterManifest.json b/schemas/ModelAdapterManifest.json index 247ac04..465dc60 100644 --- a/schemas/ModelAdapterManifest.json +++ b/schemas/ModelAdapterManifest.json @@ -14,6 +14,7 @@ "task", "rank", "alpha", + "targetModules", "format", "license", "signature" @@ -29,7 +30,7 @@ "task": { "type": "string", "description": "Task the adapter specializes, e.g. summarization, translation-de-en, agent_classification." }, "rank": { "type": "integer", "minimum": 1, "description": "LoRA rank (r)." }, "alpha": { "type": "number", "exclusiveMinimum": 0, "description": "LoRA scaling factor (alpha)." }, - "targetModules": { "type": "array", "description": "Attention/MLP submodules the adapter targets, e.g. q_proj, v_proj, o_proj, gate_proj.", "items": { "type": "string" } }, + "targetModules": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Attention/MLP submodules the adapter targets, e.g. q_proj, v_proj, o_proj, gate_proj. Required and non-empty — an adapter that does not declare its target modules is not applicable by a consumer.", "items": { "type": "string" } }, "format": { "type": "string", "enum": ["safetensors", "gguf", "mlx"], "description": "On-disk adapter format." }, "tokenizerDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Tokenizer digest if the adapter changes tokenization; null to inherit the base model's tokenizer." }, "license": { "type": "string", "description": "SPDX license identifier for the adapter weights (e.g. Apache-2.0, MIT). Required so the MIT/Apache-only rule is checkable; a distilled adapter also inherits any base-model license constraint." }, diff --git a/schemas/ModelManifest.json b/schemas/ModelManifest.json index 9c9c6af..4f0ca17 100644 --- a/schemas/ModelManifest.json +++ b/schemas/ModelManifest.json @@ -29,7 +29,7 @@ "tokenizerDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-fA-F0-9]{64}$", "description": "Content-addressed digest of the tokenizer, or null when the tokenizer is embedded in the weights." }, "contextLength": { "type": ["integer", "null"], "minimum": 0, "description": "Maximum context length in tokens, or null when unknown." }, "defaultTier": { "type": ["string", "null"], "enum": ["T0", "T1", "T2", "T3", "T4", null], "description": "The Model Plane tier this model is normally resident at." }, - "modalities": { "type": "array", "description": "Modalities the model serves.", "items": { "type": "string", "enum": ["text", "code", "embedding", "reranking", "vision", "audio", "video", "multimodal"] } }, + "modalities": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Modalities the model serves. When present, at least one, no duplicates.", "items": { "type": "string", "enum": ["text", "code", "embedding", "reranking", "vision", "audio", "video", "multimodal"] } }, "license": { "type": "string", "description": "SPDX license identifier for the model weights, e.g. \"Apache-2.0\", \"MIT\". Required so the estate's MIT/Apache-only rule can be enforced before a model is wired; use \"LicenseRef-\" for non-SPDX licenses (e.g. LicenseRef-Gemma, LicenseRef-Llama-Community), which are NOT MIT/Apache and must be flagged." }, "signature": { "type": "object",