Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/model-plane-receipts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: model-plane-receipts

# Teeth for the Model Plane (Tranche 7) receipt emitter shim: proves source-os emits
# spec-conformant, hash-chained InferenceReceipts and that tampered / local-only receipts
# are rejected (SEAM-011). CI needs no model — the emitter path is what is under test.

on:
pull_request:
paths:
- 'modules/model-plane/**'
- '.github/workflows/model-plane-receipts.yml'
push:
branches:
- main
paths:
- 'modules/model-plane/**'
- '.github/workflows/model-plane-receipts.yml'

jobs:
inferenced-receipt-teeth:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install jsonschema
run: python -m pip install --quiet 'jsonschema>=4.22,<5'

- name: Assert emitter is vendored byte-verbatim (consume-not-fork)
run: |
set -euo pipefail
expected=6881246b8e41a515fb1b29df645faeb9be17d8195d22a7e0d5ce15f8477d8e6a
actual=$(sha256sum modules/model-plane/tools/inference_receipt_emitter.py | awk '{print $1}')
if [ "$actual" != "$expected" ]; then
echo "ERR: vendored emitter diverged from canonical (expected $expected, got $actual)."
echo " Refresh from prophet-platform apps/receipt-gateway; do not fork (SEAM-011)."
exit 1
fi
echo "OK: emitter matches canonical sha256:$expected"

- name: inferenced emit-shim teeth
run: python modules/model-plane/inferenced_shim.py --selftest

- name: Canonical emitter self-test
run: python modules/model-plane/tools/inference_receipt_emitter.py --selftest
47 changes: 47 additions & 0 deletions docs/model-plane/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Model Plane on source-os (Tranche 7)

The Model Plane is source-os's tiered, on-device inference substrate: placement tiers
**T0–T4** where a tier boundary *is* a data-residency boundary, with provenance receipts,
consent-gated escalation, and governed distillation. The canonical contracts are owned by
`SourceOS-Linux/sourceos-spec` (Tranche 7); source-os is a downstream **emitter**.

## Status

This directory currently ships the **emitter shim** for the serving path — the smallest
real slice that proves source-os can emit a spec-conformant, hash-chained
`InferenceReceipt` natively. The full daemon set is not yet built.

| Slice | Item | State |
|-------|------|-------|
| T7-9 | `docs/model-plane/architecture.md` | this stub |
| T7-10 | `profiles/model-plane/{constrained,standard,workstation,cluster-node}.nix` | not built |
| T7-11 | `modules/modelplaned/` (catalog + residency) | not built |
| T7-12 | `modules/inferenced/` (serving) | **emitter shim only** (`modules/model-plane/inferenced_shim.py`) |
| T7-13 | `modules/embeddingd/` | not built |
| T7-14 | `modules/visiond/` (no network namespace) | not built |
| T7-15 | `modules/distilld/` (governed distillation) | not built |
| T7-20 | `docs/seam-registry.md` — SEAM-014..017 | not built |

Everything not built here is tracked in the T7 follow-up issue.

## Receipt spine (SEAM-011)

Every completion a serving daemon finishes must leave an `InferenceReceipt` in the estate's
single **hash-chained** ledger — a local-only ledger is not permitted (SEAM-011). source-os
**consumes** the canonical emitter (`prophet-platform apps/receipt-gateway`,
vendored byte-verbatim under `modules/model-plane/tools/`); it does not re-implement the
chain. `modules/model-plane/inferenced_shim.py` is the serving-path caller: given the
content-addressed base-model digest and the input/output of a finished completion, it
appends an on-device receipt via the vendored `emit_receipt()`.

Teeth (`.github/workflows/model-plane-receipts.yml`): a produced receipt validates against
`InferenceReceipt.schema.json` and chains (prevHash continuity); a tampered entry and a
local-only (unchained) entry are both rejected; and the vendored emitter is asserted to
match the canonical sha256 (consume-not-fork guard).

## Off-device escalation

An on-device (`on_device_only`) completion carries no lease and no escalation. A completion
served off-device (`sovereign_cluster` / `external_permitted`) crosses a data boundary and
must carry an authorizing capability lease and a non-empty escalation chain (SEAM-015);
the schema enforces this. Off-device serving is out of scope for the emitter shim.
Binary file not shown.
144 changes: 144 additions & 0 deletions modules/model-plane/inferenced_shim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""`inferenced` receipt-emission shim (Model Plane, Tranche 7 — emitter half of T7-12).

The full `inferenced` serving daemon (T7-12) is not built here (it is blocker-laden:
HellGraph ledger service, base-model licensing, credentials). This shim is the SMALLEST
real slice that proves source-os can emit a spec-conformant, hash-chained
`InferenceReceipt` NATIVELY when a completion finishes: it takes the facts a real serving
daemon has at completion time (the content-addressed base-model digest, the task, and the
input/output) and appends an on-device receipt to the estate's hash-chained ledger
(SEAM-011: no local-only ledger).

Consume-not-fork: all chaining/canonicalisation/verification is delegated to the vendored
canonical emitter (tools/inference_receipt_emitter.py, byte-verbatim from
prophet-platform apps/receipt-gateway). This file adds no chain logic of its own.

CLI:
inferenced_shim.py --ledger <path> --base-model-digest sha256:<64hex> \
--task <label> --input-file <f> --output-file <f>
inferenced_shim.py --selftest # emits + verifies a synthetic chain (labelled synthetic)

exit 0 ok; 1 conformance/chain failure; 2 usage/dependency error.
"""
from __future__ import annotations

import argparse
import json
import sys
import tempfile
from pathlib import Path

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE / "tools"))

try:
import jsonschema
except ImportError:
print("ERR: jsonschema not installed", file=sys.stderr)
raise SystemExit(2)

from inference_receipt_emitter import ( # noqa: E402
SCHEMA, canonical, emit_receipt, sha256, verify_ledger,
)


def _validator() -> "jsonschema.Draft202012Validator":
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
jsonschema.Draft202012Validator.check_schema(schema)
return jsonschema.Draft202012Validator(schema)


def emit_completion_receipt(ledger: Path, *, base_model_digest: str, task: str,
input_text: str, output_text: str,
compute_device: str = "cpu") -> dict:
"""Emit one on-device InferenceReceipt for a finished completion (delegates to canonical)."""
return emit_receipt(
ledger, base_model_digest=base_model_digest, task=task,
input_text=input_text, output_text=output_text,
provider_daemon="inferenced", tier="T1", compute_device=compute_device,
)


def _selftest() -> int:
"""Teeth: emit a synthetic chain, prove conformance + chaining, and prove the chain
rejects a tampered entry and a local-only (unchained) entry. The completions are
LABELLED SYNTHETIC — no model runs here; this proves the emitter path, not an LLM run."""
validator = _validator()
digest = "sha256:" + "a" * 64
with tempfile.TemporaryDirectory() as d:
ledger = Path(d) / "inference-ledger.jsonl"
for i in range(3):
r = emit_completion_receipt(
ledger, base_model_digest=digest, task="selftest",
input_text=f"synthetic prompt {i}", output_text=f"synthetic completion {i}")
if list(validator.iter_errors(r)):
print(f"FAIL: emitted receipt {i} schema-invalid", file=sys.stderr)
return 1
ok, msg = verify_ledger(ledger, validator)
if not ok:
print(f"FAIL: chain should be valid: {msg}", file=sys.stderr)
return 1
print(f"OK conformance+chain: {msg}")

# Tamper -> chain breaks.
lines = ledger.read_text(encoding="utf-8").splitlines()
e1 = json.loads(lines[1]); e1["outputHash"] = sha256("tampered")
lines[1] = canonical(e1)
ledger.write_text("\n".join(lines) + "\n", encoding="utf-8")
if verify_ledger(ledger, validator)[0]:
print("FAIL: tamper not detected — chain has no teeth", file=sys.stderr)
return 1
print("OK tamper-evidence: mutation detected")

# Local-only (unchained, seq>=1 without ledgerPrevHash) rejected by schema + verifier.
with tempfile.TemporaryDirectory() as d:
ledger = Path(d) / "local-only.jsonl"
base = emit_completion_receipt(ledger, base_model_digest=digest, task="t",
input_text="a", output_text="b")
local_only = dict(base); local_only["ledgerSeq"] = 1
local_only.pop("ledgerPrevHash", None)
if not list(validator.iter_errors(local_only)):
print("FAIL: schema accepted a local-only (unchained) entry", file=sys.stderr)
return 1
with ledger.open("a", encoding="utf-8") as f:
f.write(canonical(local_only) + "\n")
if verify_ledger(ledger, validator)[0]:
print("FAIL: verifier accepted a local-only entry (SEAM-011 hole)", file=sys.stderr)
return 1
print("OK local-only rejected: schema + verifier both refuse an unchained entry")

print("PASS: inferenced emit-shim teeth all bite")
return 0


def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(description="inferenced InferenceReceipt emit shim")
ap.add_argument("--selftest", action="store_true")
ap.add_argument("--ledger", type=Path)
ap.add_argument("--base-model-digest")
ap.add_argument("--task")
ap.add_argument("--input-file", type=Path)
ap.add_argument("--output-file", type=Path)
ap.add_argument("--compute-device", default="cpu")
args = ap.parse_args(argv[1:])

if args.selftest:
return _selftest()

missing = [n for n, v in (("--ledger", args.ledger), ("--base-model-digest", args.base_model_digest),
("--task", args.task), ("--input-file", args.input_file),
("--output-file", args.output_file)) if v is None]
if missing:
print(f"ERR: missing required args: {', '.join(missing)}", file=sys.stderr)
return 2
r = emit_completion_receipt(
args.ledger, base_model_digest=args.base_model_digest, task=args.task,
input_text=args.input_file.read_text(encoding="utf-8"),
output_text=args.output_file.read_text(encoding="utf-8"),
compute_device=args.compute_device)
print(json.dumps(r, indent=2))
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Loading
Loading