Implement authenticated custody observations and durable root-grant baseline
Some checks failed
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 27s
Account journey acceptance / journeys (push) Failing after 6s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1029d-2c93-7500-a0e0-e3e089668b29
This commit is contained in:
tegwick 2026-10-03 19:50:15 +02:00
parent 1999a144c9
commit 0d0751d39d
18 changed files with 1129 additions and 14 deletions

View file

@ -1,4 +1,4 @@
"""Proposed NK-ADR-0017 observation seam; deliberately absent from runtime.
"""Accepted NK-ADR-0017 custody observations; runtime is unconfigured by default.
Reader and baseline implementations are deployment-owner responsibilities.
No claims, account roles, fixture identities or local cache can create a grant.
@ -7,8 +7,8 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Mapping, Protocol
from datetime import datetime, timezone
from typing import Any, Callable, Mapping, Protocol
@dataclass(frozen=True)
@ -62,22 +62,24 @@ class CustodyObservationAdapter:
self, *, reader: CustodyReader, baselines: CustodyBaselineStore,
policy: CustodyPolicy, source_id: str, record_id: str,
allow_initial_baseline: bool = False,
clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
):
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
self.clock = clock
def observe(
self, *, issuer: str, subject: str, tenant: str, now: datetime,
self, *, issuer: str, subject: str, tenant: str, now: datetime | None = None,
) -> 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:
if tenant != "platform":
return None
try:
previous = self.baselines.load()
@ -90,6 +92,10 @@ class CustodyObservationAdapter:
):
return None
read = self.reader.read()
# Sample after owner I/O; request-start time is not a check time.
now = self.clock() if now is None else now
if now.utcoffset() is None:
return None
document = deepcopy(dict(read.document))
if (read.source_id != self.source_id
or document.get("record_id") != self.record_id

View file

@ -0,0 +1,60 @@
"""Controlled baseline initialization; never publishes a grant or resets history."""
from copy import deepcopy
from datetime import datetime, timezone
import hashlib
import json
import re
from user_engine.custody import CustodyBaseline
def initialize_baseline(adapter, *, expected_sha256):
"""Bind initial authenticated read to a human-reviewed publisher receipt.
Receipt digest is SHA256 of UTF-8 canonical JSON (sorted keys, compact
separators, ensure_ascii=True). Existing baselines are never overwritten.
Exceptions/input values are suppressed, including credential errors.
"""
try:
if not re.fullmatch("[0-9a-f]{64}", expected_sha256):
return False
if adapter.baselines.load() is not None:
return False
read = adapter.reader.read()
now = adapter.clock()
document = deepcopy(dict(read.document))
if (read.source_id != adapter.source_id
or document.get("record_id") != adapter.record_id
or document.get("owner_system") != "net-kingdom-custody"
or adapter.policy.validate(document)
or read.read_at.utcoffset() is None or now.utcoffset() is None):
return False
age = (now - read.read_at).total_seconds()
updated = datetime.fromisoformat(document["updated_at"].replace("Z", "+00:00"))
if updated > read.read_at or not 0 <= age <= document["freshness"]["max_cache_seconds"]:
return False
digest = hashlib.sha256(json.dumps(document, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()).hexdigest()
if digest != expected_sha256:
return False
return adapter.baselines.compare_and_store(None, CustodyBaseline(adapter.source_id, document))
except Exception:
return False
def main():
import argparse
from user_engine.runtime import _custody_adapter, _required
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--expected-sha256", required=True)
args = parser.parse_args()
try:
adapter = _custody_adapter(_required("USER_ENGINE_DATABASE_URL"))
accepted = adapter is not None and initialize_baseline(adapter, expected_sha256=args.expected_sha256)
except Exception:
accepted = False
print("custody baseline initialized" if accepted else "custody baseline not initialized")
return 0 if accepted else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1 @@
"""Pinned NetKingdom custody policy; provenance in provenance.json."""

View file

@ -0,0 +1,135 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://policy.coulomb.social/schemas/custody-record/v0.1",
"title": "NetKingdom platform-root custody record v0.1",
"description": "Machine-readable, non-secret form of the platform-root custody record (NK-ADR-0017, status accepted; NK-WP-0045-T02). The record is the single authority for the root grant. Entries are keyed by exact (issuer, subject), never by username. The root grant is relation platform-root-custodian in tenant:platform only; directory membership and IAM claims are projections and cannot ground an entry. Revocation and supersession are recorded custody actions that add a record revision; entries are never deleted. No secret material: token, key, share, recovery-code, password and reset-link fields are rejected everywhere.",
"type": "object",
"additionalProperties": false,
"required": [
"schema_version",
"kind",
"record_id",
"revision",
"updated_at",
"owner_system",
"freshness",
"entries"
],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"schema_version": { "const": "0.1" },
"kind": { "const": "netkingdom-custody-record" },
"record_id": {
"description": "Stable identifier of the custody record. Revisions of one record share it.",
"type": "string",
"pattern": "^nk-custody:[a-z0-9][a-z0-9-]{0,62}$"
},
"revision": {
"description": "Monotonic per record: every custody action (grant, revocation, supersession) increments it, so a revocation is distinguishable from a stale read.",
"type": "integer",
"minimum": 1
},
"updated_at": { "type": "string", "format": "date-time" },
"owner_system": { "const": "net-kingdom-custody" },
"freshness": {
"description": "Consumer cache bound. Observations older than this are stale; root-grant-dependent actions re-check and fail closed when the record is unreadable or stale.",
"type": "object",
"additionalProperties": false,
"required": ["max_cache_seconds", "on_unreadable"],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"max_cache_seconds": { "type": "integer", "minimum": 0, "maximum": 900 },
"on_unreadable": { "const": "no-grant" }
}
},
"entries": {
"type": "array",
"items": { "$ref": "#/$defs/entry" }
}
},
"$defs": {
"nonSecretName": {
"description": "Field names that would carry secret material are rejected at every level.",
"type": "string",
"not": {
"pattern": "(?i)(token|key|secret|share|recovery|password|passphrase|reset|seed|private|otp|credential)"
}
},
"timestamp": { "type": "string", "format": "date-time" },
"subject": {
"description": "Exact OIDC subject reference. No username, email, group or role is accepted as the key.",
"type": "object",
"additionalProperties": false,
"required": ["issuer", "subject"],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"issuer": { "type": "string", "pattern": "^https://[^\\s/]+(/[^\\s]*)?$" },
"subject": { "type": "string", "minLength": 1, "maxLength": 255, "pattern": "^\\S+$" }
}
},
"grant": {
"description": "Membership-envelope mapping from NK-ADR-0017 section 2. Fixed: no other tenant, relation or owner is representable.",
"type": "object",
"additionalProperties": false,
"required": ["scope_type", "scope_id", "relation", "owner_system"],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"scope_type": { "const": "tenant" },
"scope_id": { "const": "platform" },
"relation": { "const": "platform-root-custodian" },
"owner_system": { "const": "net-kingdom-custody" }
}
},
"action": {
"description": "A recorded custody action with actor, reason and the record revision it introduced.",
"type": "object",
"additionalProperties": false,
"required": ["at", "actor", "reason", "revision"],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"at": { "$ref": "#/$defs/timestamp" },
"actor": { "$ref": "#/$defs/subject" },
"reason": { "type": "string", "minLength": 1, "maxLength": 500 },
"revision": { "type": "integer", "minimum": 1 }
}
},
"evidence": {
"description": "Non-secret pointer to the custody ceremony that grounds the entry. A projection (directory group, IAM claim) is never a basis.",
"type": "object",
"additionalProperties": false,
"required": ["basis", "ref"],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"basis": { "const": "custody-ceremony" },
"ref": { "type": "string", "minLength": 1, "maxLength": 300 }
}
},
"supersession": {
"type": "object",
"additionalProperties": false,
"required": ["entry_id", "revision"],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"entry_id": { "$ref": "#/$defs/entryId" },
"revision": { "type": "integer", "minimum": 1 }
}
},
"entryId": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,62}$" },
"entry": {
"type": "object",
"additionalProperties": false,
"required": ["entry_id", "subject", "grant", "evidence", "granted"],
"propertyNames": { "$ref": "#/$defs/nonSecretName" },
"properties": {
"entry_id": { "$ref": "#/$defs/entryId" },
"subject": { "$ref": "#/$defs/subject" },
"grant": { "$ref": "#/$defs/grant" },
"evidence": { "$ref": "#/$defs/evidence" },
"granted": { "$ref": "#/$defs/action" },
"superseded_by": { "$ref": "#/$defs/supersession" },
"revoked": { "$ref": "#/$defs/action" }
},
"not": { "required": ["superseded_by", "revoked"] }
}
}
}

View file

@ -0,0 +1,231 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["jsonschema>=4.10,<5"]
# ///
"""Validate platform-root custody records (NK-ADR-0017, NK-WP-0045-T03).
Checks the JSON Schema (`canon/schemas/custody-record_v0.1.schema.json`) and
the semantics the schema cannot express: unique entries, at most one active
grant per exact (issuer, subject), revision ordering, supersession links,
append-only successors, and secret-looking values. It also provides the
fail-closed read rule a consumer applies to an observation (15-minute bound).
Offline and read-only: it never contacts an issuer, OpenBao or a directory.
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import pathlib
import re
import sys
from typing import Any
import jsonschema
SCHEMA = pathlib.Path(__file__).with_name("custody-record_v0.1.schema.json")
MAX_CACHE_SECONDS = 900
# Value shapes that look like secret material (OpenBao/Vault tokens, PEM keys,
# JWTs). Field names are rejected by the schema; this catches smuggled values.
SECRET_VALUE = re.compile(
r"(\bhv[sbr]\.[A-Za-z0-9_-]{8,}|\bs\.[A-Za-z0-9]{20,}"
r"|-----BEGIN [A-Z ]*PRIVATE KEY-----"
r"|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.)"
)
def load_schema() -> dict[str, Any]:
return json.loads(SCHEMA.read_text(encoding="utf-8"))
def parse_time(value: str) -> dt.datetime:
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
def key(entry: dict[str, Any]) -> tuple[str, str]:
return (entry["subject"]["issuer"], entry["subject"]["subject"])
def is_active(entry: dict[str, Any]) -> bool:
return "revoked" not in entry and "superseded_by" not in entry
def schema_errors(document: Any, schema: dict[str, Any]) -> list[str]:
validator = jsonschema.Draft202012Validator(
schema, format_checker=jsonschema.FormatChecker()
)
errors = []
for error in sorted(validator.iter_errors(document), key=lambda e: list(e.path)):
where = "/".join(str(part) for part in error.path) or "<root>"
errors.append(f"schema: {where}: {error.message}")
return errors
def _walk_strings(node: Any, path: str = ""):
if isinstance(node, dict):
for name, value in node.items():
yield from _walk_strings(value, f"{path}/{name}")
elif isinstance(node, list):
for index, value in enumerate(node):
yield from _walk_strings(value, f"{path}/{index}")
elif isinstance(node, str):
yield path or "<root>", node
def semantic_errors(record: dict[str, Any]) -> list[str]:
errors: list[str] = []
for where, value in _walk_strings(record):
if SECRET_VALUE.search(value):
errors.append(f"{where}: value looks like secret material")
revision = record["revision"]
entries = record["entries"]
by_id: dict[str, dict[str, Any]] = {}
for entry in entries:
if entry["entry_id"] in by_id:
errors.append(f"{entry['entry_id']}: duplicate entry_id")
by_id[entry["entry_id"]] = entry
active: dict[tuple[str, str], str] = {}
for entry in entries:
eid = entry["entry_id"]
granted = entry["granted"]
if granted["revision"] > revision:
errors.append(f"{eid}: granted.revision is after record revision")
if is_active(entry):
if key(entry) in active:
errors.append(
f"{eid}: second active grant for {key(entry)} "
f"(already {active[key(entry)]})"
)
active[key(entry)] = eid
revoked = entry.get("revoked")
if revoked:
if not granted["revision"] < revoked["revision"] <= revision:
errors.append(f"{eid}: revoked.revision must follow the grant and not exceed the record revision")
if parse_time(revoked["at"]) < parse_time(granted["at"]):
errors.append(f"{eid}: revoked before granted")
link = entry.get("superseded_by")
if link:
successor = by_id.get(link["entry_id"])
if successor is None or link["entry_id"] == eid:
errors.append(f"{eid}: superseded_by names no other entry")
else:
if successor["granted"]["revision"] != link["revision"]:
errors.append(f"{eid}: superseded_by.revision must equal the successor's granted.revision")
if parse_time(successor["granted"]["at"]) < parse_time(granted["at"]):
errors.append(f"{eid}: successor granted before the superseded entry")
if not granted["revision"] < link["revision"] <= revision:
errors.append(f"{eid}: superseded_by.revision must follow the grant and not exceed the record revision")
return errors
def validate(record: Any, schema: dict[str, Any] | None = None) -> list[str]:
errors = schema_errors(record, schema or load_schema())
if errors:
return errors
return semantic_errors(record)
def successor_errors(previous: dict[str, Any], current: dict[str, Any]) -> list[str]:
"""A later read must be a strictly newer, append-only revision."""
if validate(previous) or validate(current):
return ["invalid record in revision comparison"]
errors: list[str] = []
if previous["record_id"] != current["record_id"]:
return ["record_id changed between revisions"]
if current["revision"] < previous["revision"]:
return ["revision went backwards: stale read, keep the newer observation"]
if current["revision"] == previous["revision"]:
if current != previous:
errors.append("same revision with different content")
return errors
now = {entry["entry_id"]: entry for entry in current["entries"]}
before = {entry["entry_id"]: entry for entry in previous["entries"]}
if parse_time(current["updated_at"]) < parse_time(previous["updated_at"]):
errors.append("updated_at went backwards")
for entry in current["entries"]:
earlier = before.get(entry["entry_id"], {})
for field in ("granted", "revoked", "superseded_by"):
if field in entry and field not in earlier:
if entry[field]["revision"] <= previous["revision"]:
errors.append(f"{entry['entry_id']}: new {field} must follow the previous record revision")
for entry in previous["entries"]:
later = now.get(entry["entry_id"])
if later is None:
errors.append(f"{entry['entry_id']}: entry removed; custody actions append, never delete")
continue
for field in ("subject", "grant", "evidence", "granted"):
if later[field] != entry[field]:
errors.append(f"{entry['entry_id']}: {field} rewritten")
for field in ("revoked", "superseded_by"):
if field in entry and later.get(field) != entry[field]:
errors.append(f"{entry['entry_id']}: {field} rewritten")
return errors
def observe_root_grant(
record: dict[str, Any] | None,
issuer: str,
subject: str,
tenant: str,
read_at: dt.datetime,
now: dt.datetime,
previous_record: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
"""Return the ADR-0017 section 3 observation, or None (fail closed).
None when: the tenant is not `platform`; the record is missing, invalid or
older than the cache bound; or no active entry matches the exact
(issuer, subject).
"""
if tenant != "platform" or record is None or validate(record):
return None
if previous_record is not None and successor_errors(previous_record, record):
return None
if read_at.utcoffset() is None or now.utcoffset() is None:
return None
if parse_time(record["updated_at"]) > read_at:
return None
bound = min(record["freshness"]["max_cache_seconds"], MAX_CACHE_SECONDS)
if not dt.timedelta(0) <= now - read_at <= dt.timedelta(seconds=bound):
return None
for entry in record["entries"]:
if (key(entry) == (issuer, subject) and is_active(entry)
and parse_time(entry["granted"]["at"]) <= read_at):
return {
"subject": dict(entry["subject"]),
"grant": dict(entry["grant"]),
"source": {"record_id": record["record_id"], "revision": record["revision"]},
"observed_at": read_at.isoformat().replace("+00:00", "Z"),
"expires_at": (read_at + dt.timedelta(seconds=bound)).isoformat().replace("+00:00", "Z"),
}
return None
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("paths", nargs="+", type=pathlib.Path)
args = parser.parse_args(argv)
schema = load_schema()
failed = False
for path in args.paths:
try:
errors = validate(json.loads(path.read_text(encoding="utf-8")), schema)
except (OSError, ValueError):
errors = ["unreadable input"]
# jsonschema messages may contain the rejected secret value. Never
# expose those messages (or input paths) through the command line.
if errors:
print(f"custody record: rejected ({len(errors)} errors)", file=sys.stderr)
failed = failed or bool(errors)
if not errors:
print("custody record: ok")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,8 @@
{
"repository": "net-kingdom",
"commit": "db319335b27b61b855382ee5af5d690f06eeb010",
"source_validator_sha256": "9156e399c8d92a45b817bfc3738c89843487ce57e1718ed747f804f95cc719bb",
"schema_sha256": "bb6cd6da5dc48eddcf118ae78c2cacfafd5f860098fd8a8368fe6bc845776d07",
"packaged_policy_sha256": "a1ba5a6f586a7d64e406aebcde783ab033db010be31c132593f07fb0a93a7bc4",
"adaptation": "Schema resource lookup only; no algorithm changes."
}

View file

@ -0,0 +1,168 @@
"""Bounded owner transport and durable baseline for accepted custody observations."""
from __future__ import annotations
from datetime import datetime, timezone
import json
import math
from pathlib import Path
import re
import time
from typing import Callable
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
from user_engine.custody import CustodyBaseline, CustodyRead
class CustodyReadUnavailable(RuntimeError):
def __init__(self):
super().__init__("custody read unavailable")
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
class OpenBaoCustodyReader:
"""Dedicated Kubernetes login, fresh exact KV read, best-effort revoke.
Never reuse the flex-auth JWT, ESO AppRole or a cached document. Tokens and
owner response/error bodies are never included in raised errors or reprs.
"""
def __init__(self, *, base_url: str, auth_mount: str, role: str,
jwt_file: str, source_id: str,
data_path: str = "platform/data/custody/net-kingdom/platform-root",
timeout_seconds: float = 3, max_bytes: int = 262144,
allow_http: bool = False, opener=None,
clock: Callable = lambda: datetime.now(timezone.utc)):
url = urlsplit(base_url)
if (url.scheme not in ({"https", "http"} if allow_http else {"https"})
or not url.hostname or url.username or url.password
or url.path not in ("", "/") or url.query or url.fragment):
raise ValueError("trusted OpenBao origin required")
if (not re.fullmatch(r"[a-zA-Z0-9_-]+", auth_mount)
or not re.fullmatch(r"[a-zA-Z0-9_-]+", role)
or data_path != "platform/data/custody/net-kingdom/platform-root"
or not source_id or not jwt_file
or not math.isfinite(timeout_seconds) or not 0 < timeout_seconds <= 30
or not 1024 <= max_bytes <= 262144):
raise ValueError("invalid custody reader configuration")
self.base_url = base_url.rstrip("/")
self.auth_mount, self.role, self.jwt_file = auth_mount, role, jwt_file
self.source_id, self.data_path = source_id, data_path
self.timeout_seconds, self.max_bytes = timeout_seconds, max_bytes
self.opener = opener or build_opener(ProxyHandler({}), NoRedirect())
self.clock = clock
def _request(self, path, *, deadline, token=None, body=None):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise CustodyReadUnavailable()
headers = {"Accept": "application/json", "Cache-Control": "no-cache, no-store"}
if token:
headers["X-Vault-Token"] = token
data = None if body is None else json.dumps(body).encode()
if data is not None:
headers["Content-Type"] = "application/json"
request = Request(self.base_url + "/v1/" + path, data=data, headers=headers,
method="GET" if data is None else "POST")
with self.opener.open(request, timeout=remaining) as response:
if response.status != 200:
raise CustodyReadUnavailable()
if (response.headers.get("Age") not in (None, "0")
or response.headers.get("X-Cache", "").lower() not in ("", "miss")
or response.headers.get("Content-Encoding", "identity") != "identity"):
raise CustodyReadUnavailable()
# Read incrementally under one overall deadline, including body I/O.
chunks, size = [], 0
while True:
if time.monotonic() >= deadline:
raise CustodyReadUnavailable()
read_chunk = getattr(response, "read1", response.read)
chunk = read_chunk(min(65536, self.max_bytes + 1 - size))
size += len(chunk)
if size > self.max_bytes:
raise CustodyReadUnavailable()
if not chunk:
break
chunks.append(chunk)
return json.loads(b"".join(chunks))
def read(self):
token = None
deadline = time.monotonic() + self.timeout_seconds
try:
with Path(self.jwt_file).open("rb") as handle:
raw = handle.read(16385)
if len(raw) > 16384:
raise CustodyReadUnavailable()
jwt = raw.decode().strip()
if not jwt or "\n" in jwt or "\r" in jwt:
raise CustodyReadUnavailable()
login = self._request("auth/" + self.auth_mount + "/login", deadline=deadline,
body={"role": self.role, "jwt": jwt})
token = login["auth"]["client_token"]
if not isinstance(token, str) or not token or "\n" in token or "\r" in token:
raise CustodyReadUnavailable()
envelope = self._request(self.data_path, deadline=deadline, token=token)
document = envelope["data"]["data"]["document"]
if not isinstance(document, dict):
raise CustodyReadUnavailable()
return CustodyRead(self.source_id, document, self.clock())
except Exception:
raise CustodyReadUnavailable() from None
finally:
# Revoke via same trusted origin. This never renews observation time.
if token:
try:
request = Request(self.base_url + "/v1/auth/token/revoke-self",
data=b"{}", headers={"X-Vault-Token": token}, method="POST")
remaining = deadline - time.monotonic()
if remaining > 0:
with self.opener.open(request, timeout=remaining):
pass
except Exception:
pass
class PostgresCustodyBaselineStore:
"""One persistent singleton row, with compare-and-swap across workers.
Use a separate DB connection; no domain-store transaction is committed here.
Backups/restores must preserve this security high-water mark. A rolled-back
database requires controlled reconciliation before runtime reactivation.
"""
def __init__(self, connection):
self.connection = connection
with connection.transaction():
with connection.cursor() as cursor:
cursor.execute("CREATE TABLE IF NOT EXISTS user_engine_custody_baseline "
"(singleton boolean PRIMARY KEY CHECK (singleton), payload jsonb NOT NULL)")
@staticmethod
def _payload(baseline):
return json.dumps({"source_id": baseline.source_id, "document": baseline.document}, sort_keys=True)
def load(self):
with self.connection.transaction():
with self.connection.cursor() as cursor:
cursor.execute("SELECT payload FROM user_engine_custody_baseline WHERE singleton = true")
row = cursor.fetchone()
if row is None:
return None
value = json.loads(row[0]) if isinstance(row[0], str) else row[0]
return CustodyBaseline(value["source_id"], value["document"])
def compare_and_store(self, previous, current):
with self.connection.transaction():
with self.connection.cursor() as cursor:
if previous is None:
cursor.execute("INSERT INTO user_engine_custody_baseline (singleton, payload) "
"VALUES (true, %s::jsonb) ON CONFLICT DO NOTHING RETURNING singleton",
(self._payload(current),))
else:
cursor.execute("UPDATE user_engine_custody_baseline SET payload = %s::jsonb "
"WHERE singleton = true AND payload = %s::jsonb RETURNING singleton",
(self._payload(current), self._payload(previous)))
return cursor.fetchone() is not None

View file

@ -42,6 +42,7 @@ def create_application() -> PortalApplication:
expected_issuer=_required("USER_ENGINE_OIDC_ISSUER"),
expected_audience=_required("USER_ENGINE_OIDC_AUDIENCE"),
),
custody=_custody_adapter(database_url),
authorization=FlexAuthHTTPAdapter(
base_url=_required("USER_ENGINE_FLEX_AUTH_URL"),
timeout_seconds=float(os.environ.get("USER_ENGINE_FLEX_AUTH_TIMEOUT", "3")),
@ -171,5 +172,34 @@ def _required(name: str) -> str:
return value
def _custody_adapter(database_url):
"""Opt-in only after owner delivery/admission and baseline bootstrap."""
if os.environ.get("USER_ENGINE_CUSTODY_ENABLED", "false").lower() != "true":
return None
import psycopg
from user_engine.custody import CustodyObservationAdapter
from user_engine.custody_runtime import OpenBaoCustodyReader, PostgresCustodyBaselineStore
from user_engine.custody_canon import policy
origin = _required("USER_ENGINE_CUSTODY_BAO_URL")
mount = _required("USER_ENGINE_CUSTODY_AUTH_MOUNT")
role = _required("USER_ENGINE_CUSTODY_ROLE")
source = origin.rstrip("/") + "/v1/platform/data/custody/net-kingdom/platform-root#auth=" + mount + "/" + role
reader = OpenBaoCustodyReader(
base_url=origin, auth_mount=mount, role=role,
jwt_file=_required("USER_ENGINE_CUSTODY_JWT_FILE"), source_id=source,
timeout_seconds=float(os.environ.get("USER_ENGINE_CUSTODY_TIMEOUT", "3")),
allow_http=os.environ.get("USER_ENGINE_CUSTODY_ALLOW_HTTP", "false").lower() == "true",
)
baselines = PostgresCustodyBaselineStore(psycopg.connect(database_url))
return CustodyObservationAdapter(
reader=reader, baselines=baselines, policy=policy, source_id=source,
record_id="nk-custody:platform-root", allow_initial_baseline=False,
)
if __name__ == "__main__":
main()

View file

@ -9,6 +9,8 @@ from dataclasses import dataclass, field, replace
from datetime import datetime, timedelta
from typing import Any, Callable, Iterable, Mapping
from user_engine.custody import CustodyObservationAdapter
from user_engine.domain import (
Account,
AccessControlFact,
@ -110,7 +112,7 @@ class ReadinessReport:
@dataclass(frozen=True)
class AccountAuthority:
"""Current account state only; not a root entitlement or tenant lifecycle grant."""
"""Account state and optional observed custody fact; never authorization."""
issuer: str
subject: str
tenant: str
@ -121,6 +123,7 @@ class AccountAuthority:
tenant_account_status: AccountStatus | None
observed_at: datetime
decision_id: str | None
root_grant: Mapping[str, Any] | None = None
@dataclass(frozen=True)
@ -310,11 +313,13 @@ class UserEngineService:
identity_adapter: IdentityClaimsAdapter,
authorization: AuthorizationCheckPort,
factor_verifier: FactorVerificationAdapter | None = None,
custody: CustodyObservationAdapter | None = None,
) -> None:
self.store = store
self.identity_adapter = identity_adapter
self.authorization = authorization
self.factor_verifier = factor_verifier
self.custody = custody
if not self.store.ready:
self.store.migrate()
@ -1820,6 +1825,8 @@ class UserEngineService:
account_status=account.status,
tenant_account_status=scoped.status if scoped is not None else None,
observed_at=observed_at, decision_id=decision.decision_id,
root_grant=(self.custody.observe(issuer=issuer, subject=subject, tenant=tenant)
if self.custody is not None else None),
)
def create_user(