Prepare custody observation adapter pending authenticated owner delivery
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 55s
Account journey acceptance / journeys (push) Successful in 11s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1029d-2c93-7500-a0e0-e3e089668b29
This commit is contained in:
tegwick 2026-10-03 18:58:57 +02:00
parent 22eef7f902
commit c11c290290
3 changed files with 290 additions and 4 deletions

119
src/user_engine/custody.py Normal file
View file

@ -0,0 +1,119 @@
"""Proposed NK-ADR-0017 observation seam; deliberately absent from runtime.
Reader and baseline implementations are deployment-owner responsibilities.
No claims, account roles, fixture identities or local cache can create a grant.
"""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Mapping, Protocol
@dataclass(frozen=True)
class CustodyRead:
source_id: str
document: Mapping[str, Any]
read_at: datetime
@dataclass(frozen=True)
class CustodyBaseline:
source_id: str
document: Mapping[str, Any]
class CustodyReader(Protocol):
def read(self) -> CustodyRead:
"""Authenticated fresh owner read, bounded by timeout and response size.
source_id is trusted transport configuration, never response metadata.
Return actual successful owner read time; raise on failure or cache hits.
"""
...
class CustodyBaselineStore(Protocol):
def load(self) -> CustodyBaseline | None: ...
def compare_and_store(
self, previous: CustodyBaseline | None, current: CustodyBaseline,
) -> bool:
"""Atomically persist only if previous still matches; survive restart.
Return false on conflict or write failure. Never reset on source changes.
"""
...
class CustodyPolicy(Protocol):
"""NetKingdom's version-pinned validator and observe_root_grant functions."""
def validate(self, record: Any) -> list[str]: ...
def successor_errors(self, previous: dict, current: dict) -> list[str]: ...
def observe_root_grant(
self, record: dict, issuer: str, subject: str, tenant: str,
read_at: datetime, now: datetime, previous_record: dict | None = None,
) -> dict | None: ...
class CustodyObservationAdapter:
def __init__(
self, *, reader: CustodyReader, baselines: CustodyBaselineStore,
policy: CustodyPolicy, source_id: str, record_id: str,
allow_initial_baseline: bool = False,
):
if not source_id or not record_id:
raise ValueError("trusted source and record bindings required")
self.reader, self.baselines, self.policy = reader, baselines, policy
self.source_id, self.record_id = source_id, record_id
self.allow_initial_baseline = allow_initial_baseline
def observe(
self, *, issuer: str, subject: str, tenant: str, now: datetime,
) -> dict | None:
"""Always re-read; failures never reuse or refresh an old observation.
Diagnostics are suppressed because transport/validator errors may contain
credentials or rejected data. This supplies a fact, never authorization.
"""
if tenant != "platform" or now.utcoffset() is None:
return None
try:
previous = self.baselines.load()
if previous is None and not self.allow_initial_baseline:
return None
if previous is not None and (
previous.source_id != self.source_id
or previous.document.get("record_id") != self.record_id
or self.policy.validate(previous.document)
):
return None
read = self.reader.read()
document = deepcopy(dict(read.document))
if (read.source_id != self.source_id
or document.get("record_id") != self.record_id
or document.get("owner_system") != "net-kingdom-custody"
or read.read_at.utcoffset() is None
or read.read_at > now
or self.policy.validate(document)):
return None
old = deepcopy(dict(previous.document)) if previous else None
if old is not None and self.policy.successor_errors(old, document):
return None
# Reject stale/future documents even when they contain no grant.
updated = datetime.fromisoformat(document["updated_at"].replace("Z", "+00:00"))
age = (now - read.read_at).total_seconds()
if updated > read.read_at or not 0 <= age <= document["freshness"]["max_cache_seconds"]:
return None
result = self.policy.observe_root_grant(
document, issuer, subject, tenant, read.read_at, now,
previous_record=old,
)
# Keep revocation/empty revisions too; absence does not reset history.
current = CustodyBaseline(self.source_id, deepcopy(document))
if not self.baselines.compare_and_store(previous, current):
return None
return deepcopy(result)
except Exception:
return None

View file

@ -0,0 +1,130 @@
import copy
import importlib.util
import json
from pathlib import Path
import unittest
from datetime import datetime, timedelta, timezone
from user_engine.custody import CustodyBaseline, CustodyObservationAdapter, CustodyRead
CANON = Path(__file__).resolve().parents[2] / "net-kingdom/tools/custody-record"
try:
spec = importlib.util.spec_from_file_location("custody_canon", CANON / "validate.py")
policy = importlib.util.module_from_spec(spec)
spec.loader.exec_module(policy)
except (FileNotFoundError, ModuleNotFoundError):
policy = None
class Baselines:
def __init__(self):
self.value = None
self.conflict = False
def load(self):
return copy.deepcopy(self.value)
def compare_and_store(self, previous, current):
if self.conflict or previous != self.value:
return False
self.value = copy.deepcopy(current)
return True
@unittest.skipIf(policy is None, "optional sibling NetKingdom validator/jsonschema absent")
class CustodyAdapterTests(unittest.TestCase):
def setUp(self):
self.document = json.loads((CANON / "fixtures/valid/grant.json").read_text())
self.now = datetime(2026, 10, 1, 10, tzinfo=timezone.utc)
self.read_at = self.now
self.source = "fixture-authenticated-owner"
self.baselines = Baselines()
self.reads = 0
self.failure = False
self.adapter = CustodyObservationAdapter(
reader=self, baselines=self.baselines, policy=policy,
source_id=self.source, record_id=self.document["record_id"],
allow_initial_baseline=True,
)
self.subject = dict(self.document["entries"][0]["subject"])
def read(self):
self.reads += 1
if self.failure:
raise RuntimeError("sensitive error must never escape")
return CustodyRead(self.source, self.document, self.read_at)
def observe(self, **overrides):
return self.adapter.observe(**{**self.subject, "tenant": "platform", "now": self.now, **overrides})
def test_exact_mapping_and_fresh_read_each_time(self):
result = self.observe()
self.assertEqual(result["subject"], self.subject)
self.assertEqual(result["source"], {"record_id": self.document["record_id"], "revision": 1})
result["subject"]["subject"] = "changed"
self.assertEqual(self.observe()["subject"], self.subject)
self.assertEqual(self.reads, 2)
self.assertIsNone(self.observe(issuer=self.subject["issuer"] + "/"))
self.assertIsNone(self.observe(subject="unknown"))
self.assertIsNone(self.observe(tenant="other"))
def test_failure_and_stale_read_never_refresh_old_grant(self):
self.assertIsNotNone(self.observe())
baseline = self.baselines.load()
self.failure = True
self.assertIsNone(self.observe())
self.failure = False
self.now += timedelta(seconds=901)
self.assertIsNone(self.observe())
self.assertEqual(self.baselines.load(), baseline)
def test_revocation_retained_and_rollback_denied_across_restart(self):
grant = copy.deepcopy(self.document)
self.assertIsNotNone(self.observe())
self.document = json.loads((CANON / "fixtures/valid/revocation.json").read_text())
self.now = self.read_at = datetime(2026, 10, 3, 12, tzinfo=timezone.utc)
self.assertIsNone(self.observe())
self.assertEqual(self.baselines.value.document["revision"], 2)
self.adapter = CustodyObservationAdapter(reader=self, baselines=self.baselines, policy=policy,
source_id=self.source, record_id=grant["record_id"])
self.document = grant
self.assertIsNone(self.observe())
self.assertEqual(self.baselines.value.document["revision"], 2)
def test_same_revision_mutation_and_source_change_denied(self):
self.assertIsNotNone(self.observe())
self.document["entries"][0]["granted"]["reason"] = "rewritten"
self.assertIsNone(self.observe())
self.source = "other-owner"
self.assertIsNone(self.observe())
def test_invalid_projection_and_future_clock_denied(self):
self.now -= timedelta(seconds=1)
self.assertIsNone(self.observe())
self.now = self.read_at
self.document = json.loads((CANON / "fixtures/invalid/projection-only-grant.json").read_text())
self.assertIsNone(self.observe())
self.assertIsNone(self.baselines.value)
def test_bootstrap_and_persistence_conflict_deny(self):
self.adapter.allow_initial_baseline = False
self.assertIsNone(self.observe())
self.assertEqual(self.reads, 0)
self.adapter.allow_initial_baseline = True
self.baselines.conflict = True
self.assertIsNone(self.observe())
def test_empty_and_supersession_revisions_are_retained(self):
self.document["entries"] = []
self.assertIsNone(self.observe())
self.assertEqual(self.baselines.value.document["revision"], 1)
# Separate approved bootstrap to the grant fixture, then handover.
self.baselines.value = None
self.document = json.loads((CANON / "fixtures/valid/grant.json").read_text())
self.assertIsNotNone(self.observe())
self.document = json.loads((CANON / "fixtures/valid/supersession.json").read_text())
self.now = self.read_at = datetime(2026, 10, 3, 12, tzinfo=timezone.utc)
self.assertIsNone(self.observe())
successor = self.document["entries"][1]["subject"]
self.assertIsNotNone(self.observe(**successor))
self.assertEqual(self.baselines.value.document["revision"], 2)

View file

@ -49,10 +49,9 @@ production deployment was added. T02/T03 retain those outstanding owner gates.
```task
id: USER-WP-0037-T02
status: wait
depends_on: [NK-WP-0045-T01, NK-WP-0045-T04]
needs_human: true
decision_id: cef97ccd-4ee7-40fe-859e-1df9c9360ecb
blocking_reason: "Mapping specified in proposed NK-ADR-0017; awaits founder acceptance (NK-WP-0045-T01) and the custody-record read surface (NK-WP-0045-T04)."
depends_on: [NK-WP-0045-T05]
blocked_on: repo:railiance-platform
blocking_reason: "ADR and OpenBao design approved; awaits NK-WP-0045-T05 authenticated delivery plus consumer identity admission, durable baseline, canon policy packaging and live mapping/integration receipts."
priority: high
state_hub_task_id: "23a5c211-277b-561e-a4b3-0f8e99537cac"
```
@ -85,6 +84,44 @@ accepts or amends NK-ADR-0017) and NK-WP-0045-T04 (read-surface decision), both
routed to hub decision cef97ccd (packet D10). No code changed: the field is
added once the read surface exists; until then no root grant is returned.
2026-10-03 source-side progress (handoff `d6efcf93`): added transport-neutral
`src/user_engine/custody.py`. Injected source binding, owner read timestamp,
NetKingdom validation/transition/observation policy and atomic persistent baseline
interfaces; every observation performs a fresh owner read, including writes.
Read failures, stale/future data, rollback, same-revision mutations and persistence
conflicts yield no grant. Empty/revoked revisions retain the monotonic baseline.
Initial baseline establishment defaults denied and needs an approved bootstrap
procedure. No runtime binding or AccountAuthority root field is activated.
Approved OpenBao design envelope supplied by railiance-platform is
`data.data.document` containing the complete canon document. This is compatible
with the reader seam; the design approval does not establish an authenticated live source. This
repo contains no Kubernetes deployment/SA identity to use as an admission receipt.
Remaining owner requirements: version-pinned canon policy packaging, bounded
authenticated reader, atomic durable baseline store with controlled rebootstrap,
real read identity/provenance and denial/outage/recovery acceptance; then add the
optional AccountAuthority mapping and downstream integration receipts. T02 remains
`wait`, workplan remains `blocked`; local fixtures do not establish live delivery.
Founder approval received during this session: ADR-0017 and the proposed
OpenBao delivery design are approved; NK-WP-0045-T01/T04 no longer block T02.
This does not authorize live provisioning/deployment or create a custody entry.
The remaining dependency is NK-WP-0045-T05 owner delivery plus user-engine's
runtime admission, durable baseline, policy packaging and integration receipts.
Validation: `PYTHONDONTWRITEBYTECODE=1 make test` passed all 284 tests
(eight optional skips), including seven new cross-repo canon fixture checks;
layer conformance and `git diff --check` passed. The fixture tests optionally
load the sibling validator and skip if it or jsonschema is absent; production
has no dependency on a sibling checkout and no policy loader is configured.
Security orientation: emergent knowledge 0.2.3/start digest
`60f8811c6b94b30d357a0d9e5022f2a8ea52e69b47138d3a536755f3295310b5`,
withheld due to changed informed-decision source. Reviewed current local source
README plus current NetKingdom companion and custodian build guidance; restricted
work to offline source/tests, with no authority or live-readiness inference.
## Authenticated workload transport and private acceptance
```task