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

@ -0,0 +1,82 @@
# Root custody consumer delivery
USER-WP-0037-T02 implements accepted NK-ADR-0017 without creating custody entries.
The runtime remains disabled unless USER_ENGINE_CUSTODY_ENABLED=true. Optional
AccountAuthority.root_grant is a source fact, never a flex-auth decision.
## Configuration and owner boundary
The deployment owner is rapp-user-engine: namespace user-engine, Deployment
user-engine, ServiceAccount user-engine. A dedicated projected JWT with audience
openbao must be supplied; the existing flex-auth audience token is unsuitable.
railiance-platform supplies auth/kubernetes role user-engine-root-custody-reader,
exact-path-only reader policy, no default policy, and maximum 60-second token TTL.
No ESO AppRole or operator token is used. Login occurs on every read; revoke-self
is best-effort and may be denied by the exact-path policy. TTL limits that residue;
the consumer does not claim confirmed revocation.
| Variable | Binding |
| --- | --- |
| USER_ENGINE_CUSTODY_ENABLED | true to opt in; unset defaults disabled |
| USER_ENGINE_CUSTODY_BAO_URL | Owner-confirmed origin only; no path or credentials |
| USER_ENGINE_CUSTODY_AUTH_MOUNT | kubernetes |
| USER_ENGINE_CUSTODY_ROLE | user-engine-root-custody-reader |
| USER_ENGINE_CUSTODY_JWT_FILE | Dedicated audience=openbao projected JWT file |
| USER_ENGINE_CUSTODY_TIMEOUT | Total request deadline, default 3 seconds |
| USER_ENGINE_CUSTODY_ALLOW_HTTP | Default false; true requires reviewed in-cluster transport trust and network controls |
The path is fixed to /v1/platform/data/custody/net-kingdom/platform-root and the
whole JSON document is data.data.document. Responses are bounded to 256 KiB;
redirects, proxy environment settings, reported cache hits, compressed bodies,
invalid records and source changes are rejected. Errors never print values.
HTTPS uses system CA verification by default. The currently proposed in-cluster
HTTP route has no TLS origin authentication; owner-approved network confinement
and exact origin binding are required before enabling it. A configured origin
alone does not prove live trust.
The embedded custody_canon package contains validator/schema and provenance
hashes, pinned to NetKingdom db319335b27b61b855382ee5af5d690f06eeb010. Only
schema lookup is relocated. Policy updates require reviewing canon and refreshing
pin/hashed provenance. No sibling checkout is used in production. root_grant
retains exact subject, fixed grant, record id/revision, owner read observed_at,
and expires_at from the record's tighter cache bound. Check time is sampled
after I/O; local reads never renew owner read time.
## Durable baseline and controlled initialization
The existing USER_ENGINE_DATABASE_URL holds the singleton
user_engine_custody_baseline table using a separate connection. Compare-and-swap
prevents competing workers overwriting a newer baseline. Empty/revoked records
also persist. Startup never creates an initial baseline from a record automatically.
After the publisher has returned a reviewed source/read admission receipt, an
attended owner may initialize an absent baseline. An explicitly approved empty
entries[] document establishes no grant and needs no invented ceremony. Every
non-empty grant still requires human custody-ceremony provenance. Initialize using:
```sh
python -m user_engine.custody_bootstrap --expected-sha256 <publisher-document-digest>
```
Run with the same secret-backed runtime environment and dedicated reader JWT.
The non-secret digest must come from a reviewed publisher receipt, not fixtures;
it hashes UTF-8 JSON with sorted keys, compact separators and ensure_ascii=True.
The command performs a fresh authenticated read, validates record/id/time and
digest, then atomically initializes only an absent baseline. It emits one boolean
outcome and never resets an existing baseline. It creates no custody entry.
Source/record changes and database restores require owner reconciliation; there
is intentionally no automatic reset command. Preserve baseline high-water records
outside the restored database and compare with publisher history/audit receipts.
If both source and consumer databases restore backwards, their mutually agreeing
old state cannot detect rollback by itself. Keep this feature disabled through
restore until owner audit/history establishes the latest revision and a reviewed
reconciliation updates the baseline. Never delete the row to make a read pass.
## Remaining live acceptance
The code and offline fixtures do not establish a real grant. Delivery requires
publisher ceremony provenance, role/policy receipts, projected-token packaging,
network trust, owner-reviewed baseline initialization, image digest promotion and
positive/denial/outage/revocation/supersession/restore receipts through the actual
Hub workload route. USER-WP-0037-T03 retains its separate Hub admission blocker.

View file

@ -3,6 +3,7 @@ name = "user-engine"
version = "0.1.0"
description = "Headless user-domain and profile engine."
requires-python = ">=3.12"
dependencies = ["jsonschema>=4.10,<5"]
[project.optional-dependencies]
postgres = ["psycopg[binary]>=3.2,<4"]
@ -21,6 +22,7 @@ where = ["src"]
[tool.setuptools.package-data]
user_engine = ["migrations/postgres/*.sql"]
"user_engine.custody_canon" = ["*.json"]
[tool.user-engine]
package = "user_engine"

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(

View file

@ -62,8 +62,25 @@ class AccountAuthorityTests(unittest.TestCase):
self.store.find_identity = checked
with self.assertRaises(AuthorizationDenied): self.lookup()
def test_custody_mapping_only_after_independent_authorization(self):
calls = []
class Custody:
def observe(inner, **values):
calls.append(values)
return {"fixture": "observed-fact"} if values["tenant"] == "platform" else None
self.service.custody = Custody()
result = self.lookup(tenant="platform")
self.assertEqual(result.root_grant, {"fixture": "observed-fact"})
self.assertEqual(calls[-1], {"issuer": self.session.actor.issuer, "subject": self.session.actor.subject, "tenant": "platform"})
self.policy.effect = AuthorizationEffect.DENY
with self.assertRaises(AuthorizationDenied):
self.lookup(tenant="platform")
self.assertEqual(len(calls), 1)
def test_does_not_emit_root_grants_or_profile_details(self):
result = self.lookup()
self.assertIsNone(result.root_grant)
self.assertFalse(hasattr(result,'root_entitled'))
self.assertFalse(hasattr(result,'roles'))
self.assertFalse(hasattr(result,'primary_email'))

View file

@ -8,12 +8,7 @@ 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
from user_engine.custody_canon import policy
class Baselines:
@ -31,7 +26,7 @@ class Baselines:
return True
@unittest.skipIf(policy is None, "optional sibling NetKingdom validator/jsonschema absent")
@unittest.skipUnless(CANON.exists(), "optional sibling NetKingdom fixtures absent")
class CustodyAdapterTests(unittest.TestCase):
def setUp(self):
self.document = json.loads((CANON / "fixtures/valid/grant.json").read_text())
@ -106,6 +101,14 @@ class CustodyAdapterTests(unittest.TestCase):
self.assertIsNone(self.observe())
self.assertIsNone(self.baselines.value)
def test_check_clock_sampled_after_owner_read(self):
self.adapter.clock = lambda: self.now + timedelta(seconds=2)
self.read_at = self.now + timedelta(seconds=1)
result = self.adapter.observe(**self.subject, tenant="platform")
self.assertIsNotNone(result)
self.assertEqual(result["observed_at"], self.read_at.isoformat().replace("+00:00", "Z"))
def test_bootstrap_and_persistence_conflict_deny(self):
self.adapter.allow_initial_baseline = False
self.assertIsNone(self.observe())

View file

@ -0,0 +1,33 @@
import hashlib
import json
import unittest
import test_custody_adapter as adapter_fixtures
CANON = adapter_fixtures.CANON
from user_engine.custody_bootstrap import initialize_baseline
@unittest.skipUnless(CANON.exists(), "optional sibling fixtures absent")
class CustodyBootstrapTests(unittest.TestCase):
def setUp(self):
self.fixture = adapter_fixtures.CustodyAdapterTests()
self.fixture.setUp()
self.fixture.adapter.clock = lambda: self.fixture.now
self.digest = hashlib.sha256(json.dumps(self.fixture.document, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()).hexdigest()
def test_authenticated_receipt_match_initializes_once(self):
self.assertTrue(initialize_baseline(self.fixture.adapter, expected_sha256=self.digest))
self.assertFalse(initialize_baseline(self.fixture.adapter, expected_sha256=self.digest))
self.assertEqual(self.fixture.reads, 1)
def test_wrong_receipt_and_failure_do_not_seed_baseline(self):
self.assertFalse(initialize_baseline(self.fixture.adapter, expected_sha256="0" * 64))
self.fixture.failure = True
self.assertFalse(initialize_baseline(self.fixture.adapter, expected_sha256=self.digest))
self.assertIsNone(self.fixture.baselines.value)
def test_empty_approved_document_initializes_no_grant(self):
self.fixture.document["entries"] = []
digest = hashlib.sha256(json.dumps(self.fixture.document, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()).hexdigest()
self.assertTrue(initialize_baseline(self.fixture.adapter, expected_sha256=digest))
self.assertIsNone(self.fixture.observe())

View file

@ -0,0 +1,14 @@
import hashlib
from importlib.resources import files
import json
import unittest
from user_engine.custody_canon import policy
class CustodyPolicyPackageTests(unittest.TestCase):
def test_pinned_policy_and_schema_integrity(self):
package = files("user_engine.custody_canon")
receipt = json.loads(package.joinpath("provenance.json").read_text())
self.assertEqual(hashlib.sha256(package.joinpath("policy.py").read_bytes()).hexdigest(), receipt["packaged_policy_sha256"])
self.assertEqual(hashlib.sha256(package.joinpath("custody-record_v0.1.schema.json").read_bytes()).hexdigest(), receipt["schema_sha256"])
self.assertEqual(policy.load_schema()["$id"], "https://policy.coulomb.social/schemas/custody-record/v0.1")

View file

@ -0,0 +1,130 @@
import io
import json
from pathlib import Path
import tempfile
import unittest
from datetime import datetime, timezone
from urllib.error import HTTPError
from user_engine.custody_runtime import OpenBaoCustodyReader, CustodyReadUnavailable
class Response(io.BytesIO):
def __init__(self, value, *, headers=None):
super().__init__(json.dumps(value).encode())
self.headers = headers or {}
self.status = 200
class Opener:
def __init__(self, responses):
self.responses, self.requests = list(responses), []
def open(self, request, timeout):
self.requests.append((request, timeout))
response = self.responses.pop(0)
if isinstance(response, Exception):
raise response
return response
class CustodyTransportTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.jwt = Path(self.temp.name) / "token"
self.jwt.write_text("fixture-jwt\n")
self.now = datetime.now(timezone.utc)
self.document = {"revision": 4}
def reader(self, opener, **options):
return OpenBaoCustodyReader(base_url="https://bao.example", auth_mount="kubernetes",
role="user-engine-root-custody-reader", jwt_file=str(self.jwt), source_id="trusted",
opener=opener, clock=lambda: self.now, **options)
def login(self):
return Response({"auth": {"client_token": "fixture-reader-token"}})
def test_dedicated_login_fresh_exact_read_and_revoke(self):
opener = Opener([self.login(), Response({"data": {"data": {"document": self.document}}}), Response({})])
result = self.reader(opener).read()
self.assertEqual(result.document, self.document)
self.assertEqual(result.read_at, self.now)
login, read, revoke = [entry[0] for entry in opener.requests]
self.assertEqual(json.loads(login.data), {"role": "user-engine-root-custody-reader", "jwt": "fixture-jwt"})
self.assertEqual(read.full_url, "https://bao.example/v1/platform/data/custody/net-kingdom/platform-root")
self.assertEqual(read.get_header("X-vault-token"), "fixture-reader-token")
self.assertTrue(revoke.full_url.endswith("auth/token/revoke-self"))
self.assertTrue(all(0 < timeout <= 3 for _, timeout in opener.requests))
def test_owner_denial_errors_are_suppressed(self):
opener = Opener([HTTPError("https://secret-value.invalid", 403, "sensitive", {}, None)])
with self.assertRaises(CustodyReadUnavailable) as error:
self.reader(opener).read()
self.assertEqual(str(error.exception), "custody read unavailable")
self.assertIsNone(error.exception.__cause__)
def test_cache_hit_malformed_and_oversized_responses_deny(self):
for response in (Response({"data": {}}, headers={"Age": "1"}), Response({"wrong": "envelope"}),
Response({"data": {"data": {"document": {"padding": "x" * 2000}}}})):
opener = Opener([self.login(), response, Response({})])
with self.assertRaises(CustodyReadUnavailable):
self.reader(opener, max_bytes=1024).read()
def test_token_rotation_is_read_from_file_each_login(self):
opener = Opener([self.login(), Response({"data": {"data": {"document": self.document}}}), Response({}),
self.login(), Response({"data": {"data": {"document": self.document}}}), Response({})])
reader = self.reader(opener)
reader.read()
self.jwt.write_text("rotated-fixture-jwt")
reader.read()
self.assertEqual(json.loads(opener.requests[3][0].data)["jwt"], "rotated-fixture-jwt")
def test_plaintext_untrusted_origin_and_path_are_configuration_errors(self):
for origin in ("http://bao.example", "https://user:pass@bao.example", "https://bao.example/path"):
with self.assertRaises(ValueError):
OpenBaoCustodyReader(base_url=origin, auth_mount="kubernetes", role="reader", jwt_file="fixture", source_id="source")
with self.assertRaises(ValueError):
self.reader(Opener([]), data_path="platform/data/other")
class CustodyNestedIntegrationTests(unittest.TestCase):
def test_nested_owner_response_to_serialized_account_authority(self):
from datetime import timedelta
from dataclasses import replace
from user_engine.adapters.local import InMemoryUserEngineStore
from user_engine.custody import CustodyObservationAdapter
from user_engine.custody_canon import policy
from user_engine.service import UserEngineService
from user_engine.testing.fixtures import FixtureIdentityClaimsAdapter, StaticAuthorizationCheckPort, human_actor_claims
from user_engine.web import _jsonable
from test_custody_adapter import Baselines, CANON
if not CANON.exists():
self.skipTest("optional sibling fixtures absent")
document = json.loads((CANON / "fixtures/valid/grant.json").read_text())
now = datetime(2026, 10, 3, 12, tzinfo=timezone.utc)
claims = human_actor_claims()
claims.update(iss=document["entries"][0]["subject"]["issuer"], sub=document["entries"][0]["subject"]["subject"], tenant="platform")
with tempfile.TemporaryDirectory() as directory:
jwt_file = Path(directory) / "token"
jwt_file.write_text("fixture-openbao-audience-jwt")
opener = Opener([Response({"auth": {"client_token": "fixture-token"}}),
Response({"data": {"data": {"document": document}}}), Response({})])
source = "https://bao.example/v1/platform/data/custody/net-kingdom/platform-root#auth=kubernetes/user-engine-root-custody-reader"
reader = OpenBaoCustodyReader(base_url="https://bao.example", auth_mount="kubernetes",
role="user-engine-root-custody-reader", jwt_file=str(jwt_file), source_id=source,
opener=opener, clock=lambda: now)
adapter = CustodyObservationAdapter(reader=reader, baselines=Baselines(), policy=policy,
source_id=source, record_id=document["record_id"], allow_initial_baseline=True,
clock=lambda: now + timedelta(milliseconds=1))
service = UserEngineService(store=InMemoryUserEngineStore(), identity_adapter=FixtureIdentityClaimsAdapter(),
authorization=StaticAuthorizationCheckPort(), custody=adapter)
session = service.me(claims)
result = service.lookup_account_authority(replace(session.actor, subject="fixture-hub-reader"),
issuer=claims["iss"], subject=claims["sub"], tenant="platform")
envelope = _jsonable(result)
observation = envelope["root_grant"]
self.assertEqual(observation["source"], {"record_id": document["record_id"], "revision": 1})
self.assertEqual(observation["subject"], document["entries"][0]["subject"])
self.assertEqual(observation["observed_at"], "2026-10-03T12:00:00Z")
self.assertEqual(observation["expires_at"], "2026-10-03T12:15:00Z")
self.assertEqual(opener.requests[1][0].full_url, "https://bao.example/v1/platform/data/custody/net-kingdom/platform-root")

150
uv.lock generated
View file

@ -1,6 +1,15 @@
version = 1
requires-python = ">=3.12"
[[package]]
name = "attrs"
version = "26.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 },
]
[[package]]
name = "cffi"
version = "2.1.1"
@ -136,6 +145,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429 },
]
[[package]]
name = "jsonschema"
version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "jsonschema-specifications" },
{ name = "referencing" },
{ name = "rpds-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 },
]
[[package]]
name = "jsonschema-specifications"
version = "2025.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
]
[[package]]
name = "psycopg"
version = "3.3.5"
@ -217,6 +253,116 @@ crypto = [
{ name = "cryptography" },
]
[[package]]
name = "referencing"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 },
]
[[package]]
name = "rpds-py"
version = "2026.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691 },
{ url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542 },
{ url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180 },
{ url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067 },
{ url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509 },
{ url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754 },
{ url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189 },
{ url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750 },
{ url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576 },
{ url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807 },
{ url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187 },
{ url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030 },
{ url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185 },
{ url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394 },
{ url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753 },
{ url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012 },
{ url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203 },
{ url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984 },
{ url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815 },
{ url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545 },
{ url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828 },
{ url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678 },
{ url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811 },
{ url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382 },
{ url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832 },
{ url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011 },
{ url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431 },
{ url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710 },
{ url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454 },
{ url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063 },
{ url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510 },
{ url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495 },
{ url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454 },
{ url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583 },
{ url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919 },
{ url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725 },
{ url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255 },
{ url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060 },
{ url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960 },
{ url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356 },
{ url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319 },
{ url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508 },
{ url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504 },
{ url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380 },
{ url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976 },
{ url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840 },
{ url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282 },
{ url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403 },
{ url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055 },
{ url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419 },
{ url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848 },
{ url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369 },
{ url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673 },
{ url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500 },
{ url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978 },
{ url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350 },
{ url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486 },
{ url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068 },
{ url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600 },
{ url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726 },
{ url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587 },
{ url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585 },
{ url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479 },
{ url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418 },
{ url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123 },
{ url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351 },
{ url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827 },
{ url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966 },
{ url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680 },
{ url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853 },
{ url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715 },
{ url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864 },
{ url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430 },
{ url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877 },
{ url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933 },
{ url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274 },
{ url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763 },
{ url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467 },
{ url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689 },
{ url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340 },
{ url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179 },
{ url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993 },
{ url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909 },
{ url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584 },
{ url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357 },
{ url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533 },
{ url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204 },
{ url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719 },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
@ -239,6 +385,9 @@ wheels = [
name = "user-engine"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "jsonschema" },
]
[package.optional-dependencies]
oidc = [
@ -254,6 +403,7 @@ runtime = [
[package.metadata]
requires-dist = [
{ name = "jsonschema", specifier = ">=4.10,<5" },
{ name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = ">=3.2,<4" },
{ name = "psycopg", extras = ["binary"], marker = "extra == 'runtime'", specifier = ">=3.2,<4" },
{ name = "pyjwt", extras = ["crypto"], marker = "extra == 'oidc'", specifier = ">=2.10,<3" },

View file

@ -51,7 +51,7 @@ id: USER-WP-0037-T02
status: wait
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."
blocking_reason: "Consumer transport, durable baseline, pinned canon and optional mapping implemented; awaits NK-WP-0045-T05 real provenance/admission plus owner image promotion, receipt-bound baseline initialization and live integration evidence."
priority: high
state_hub_task_id: "23a5c211-277b-561e-a4b3-0f8e99537cac"
```
@ -122,6 +122,44 @@ 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.
2026-10-03 continued go-forward implementation: the source is now complete for
the confirmed OpenBao/Kubernetes interface. Added dedicated projected-JWT login,
exact-path fresh KV reader (256 KiB; bounded timeout; denied redirects/cache hits;
safe errors), persistent PostgreSQL baseline compare-and-swap in the existing
database, optional AccountAuthority.root_grant and disabled-by-default runtime
configuration. Owner check time is sampled after I/O; returned observation retains
the canon-derived expires_at. Embedded validator/schema/provenance are pinned to
NetKingdom `db319335b27b61b855382ee5af5d690f06eeb010`, with schema lookup only
relocated. No sibling checkout dependency in the runtime.
Confirmed deployment source in `rapp-user-engine/manifests/runtime.yaml`: namespace
and ServiceAccount user-engine; existing flex-auth audience is not reused. Platform
agreed dedicated auth/kubernetes role user-engine-root-custody-reader, audience
openbao, no default policy and 60-second maximum token lifetime. The packaging
owner prepares the separate projected token and disabled overlay. No live grant,
credential provisioning or deployment acceptance is inferred from source changes.
`python -m user_engine.custody_bootstrap --expected-sha256 <publisher-digest>` is
the concrete controlled initialization tool: fresh authenticated read, exact
source/record binding and validated canonical JSON digest from a reviewed owner
receipt; initialize absent baseline once, never reset. Restore/source changes
require external producer audit/high-water reconciliation before reactivation.
Complete procedures and configuration: `docs/root-custody-runtime.md`.
Validation: full make test passed296 tests (eight optional skips), with layer
conformance and diff checks passing; isolated disposable
PostgreSQL16 verified bootstrap CAS, competing-writer conflict and reconnect
persistence. Wheel build succeeded with embedded policy/schema/provenance. Offline
integration covers nested data.data.document to serialized AccountAuthority,
exact source/path/role mapping, post-read timing and emitted expires_at. No live
OpenBao or production database was accessed by these checks.
Founder reports no existing ceremony record; the approved initial document may
contain entries[] and therefore no grant. T02 remains wait for authenticated
publisher receipt (empty no-grant initialization is acceptable), platform reader admission,
owner image promotion, reviewed baseline initialization and live acceptance.
USER-WP-0037 stays blocked; T03 retains its independent Hub admission dependency.
## Authenticated workload transport and private acceptance
```task