diff --git a/openbao/root-grant/README.md b/openbao/root-grant/README.md index d06ba8b..08bb590 100644 --- a/openbao/root-grant/README.md +++ b/openbao/root-grant/README.md @@ -6,7 +6,7 @@ Scoped made decision receipt: `c2cff929-91b9-4018-bfd6-fa04f7da8280`. This selects railiance-platform and the exact source below. Identity admission, attended live provisioning, custody provenance and live readiness remain pending. NetKingdom canon remains authoritative; reviewed baseline is net-kingdom commit -`de4c4f84f5043b97a205f1a72a26e77f8750d628`. Deployments must pin the validator +`db319335b27b61b855382ee5af5d690f06eeb010`. Deployments must pin the validator and its schema together, and reconcile amendments before publication. ## Approved source and transport @@ -16,10 +16,12 @@ and its schema together, and reconcile amendments before publication. - `record_id`: `nk-custody:platform-root`; `owner_system`: `net-kingdom-custody`. - API response unwrap: `data.data.document` is the complete canon document. `data.metadata.version` is KV concurrency state, distinct from canon revision. -- Reader: user-engine only. Its repository currently has no deployed workload - namespace/ServiceAccount, so no Kubernetes identity binding is claimed. - User-engine must name its real runtime identity and an existing supported auth - route. Platform admits it only after review. Reader policy is exact-path read; +- Reader: owner manifests identify namespace `user-engine`, ServiceAccount + `user-engine`. Role `user-engine-root-custody-reader` on `auth/kubernetes` + binds exactly those values and audience `openbao`. Tokens last at most 60s, + with no default policy. Dedicated projected JWT is owned by rapp-user-engine; + the existing flex-auth JWT and ESO AppRole are not reused. Live admission + remains unexecuted. Reader policy is exact-path read; no list, sibling read or write. Never distribute an attended writer token. - Writer: separate human custody lane, bounded attended MFA operator login, reviewed exact-path policy. This packet does not grant a credential or role. @@ -37,26 +39,64 @@ refuses missing existing baselines, same-revision changes and rollback. CAS=0 is bootstrap-only, CAS=current-version updates; a CAS conflict requires a fresh owner read and revalidation, never blind retry. It never prints documents. -The attended executor still needs implementation after actual reader/writer -identity confirmation: suppress output, read document and KV version together, -verify retained high-water mark, call preparation helper, write one complete payload, read back -and compare, and emit non-secret revision/digest receipts. An unvalidated direct -writer bypass is not an accepted delivery path. Policies alone cannot enforce -the human ceremony, validator or global revision monotonicity. +The attended executor is implemented in `scripts/root_grant_attended.py`: -## Exact later live approval packet +- `provision` checks existing Kubernetes auth and active audit, admits only the + exact reviewed reader policy/role, enables CAS on this key and verifies all + readbacks. Differing existing policies/roles are refused without overwrite. +- `publish` reads current document and KV version together, validates pinned + canon and the persisted high-water baseline, fsyncs a pending journal before + CAS write, verifies exact readback, retains immutable local history and + atomically advances state. Exact replay creates no new version. Ambiguous + writes recover only after exact matching owner readback and durable history. +- `restore-check` requires the independently retained publisher baseline to + match the restored source exactly. Missing/different baseline never resets + authority or permits rollback. Off-host history backup is still an execution + dependency; local files alone cannot prove safe disaster restoration. +- The silent child requires the existing warden attended-login envelope and + exact loopback OpenBao tunnel. No raw process response/error is emitted. + Every invocation requires a new receipt path: old success receipts are refused. -After real consumer identity confirmation and final source preparation, request -one explicit attended approval naming the reviewed platform and NetKingdom -commits, exact mount/path/record ID, real reader auth binding, separate writer -binding, audit owner, abort operator, window and provenance reference. Its -bounded actions are: inspect existing mount and identity without values; -configure exact-path admission and KV per-key CAS requirement; publish a -human-reviewed initial document only from real ceremony evidence (or an -explicitly approved empty baseline); verify authenticated reader success, -anonymous/wrong-identity/parent-list/sibling/write denial and failed-read behavior; -return only source ID, revision, content digest, actor/time/provenance references -and pass/fail checks. No synthetic fixture can seed the authoritative record. +## Authorized attended execution packet + +Founder approval to "go forward" authorizes continued implementation and bounded +provisioning of the approved design. No additional generic design approval is +requested. Attended OIDC/MFA must still run in the founder's own terminal: + +```bash +BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 \ + warden access openbao-platform-admin-login --exec -- \ + /home/worsch/railiance-platform/scripts/root-grant-attended.sh provision \ + --receipt /tmp/platform-root-admission-UNIQUE-RUN.json +``` + +Replace UNIQUE-RUN with a fresh run identifier. The child is silent. Inspect the +new JSON receipt and warden's printed result; a shell exit code or previous +receipt cannot establish success. Provisioning does not create a custody grant. +Existing operator platform-admin identity provides the human writer lane; +this packet does not create another privileged identity or reuse its token in +user-engine. Deployment/network policy/consumer activation belongs to +rapp-user-engine and follows source image publication and admission receipts. + +Publication requires an owner state directory outside the checkout, off-host +history backup, a bounded non-secret initial document and ceremony reference. +Do not infer a custodian from the legacy planned roster or LLDAP account. Real +bootstrap requires the human's exact issuer/subject and signed ceremony; an +empty initial record can be used only if chosen by the founder. Example child +arguments after those inputs exist: + +```text +publish --candidate /owner/candidate.json --state /owner/history/baseline.json +--provenance-ref +--net-kingdom /home/worsch/net-kingdom +--validator-digest 9156e399c8d92a45b817bfc3738c89843487ce57e1718ed747f804f95cc719bb +--schema-digest bb6cd6da5dc48eddcf118ae78c2cacfafd5f860098fd8a8368fe6bc845776d07 +--receipt /tmp/platform-root-publish-UNIQUE-RUN.json +``` + +Run via the same attended wrapper. `restore-check` takes the same source pins +and state but no candidate or provenance argument. Source digests enforce the +reviewed NetKingdom validator and schema without importing an unreviewed path. Acceptance also needs revocation/supersession exercise and rollback/restore receipts. Exercise real grant changes only under their own custody ceremony diff --git a/scripts/root-grant-attended.sh b/scripts/root-grant-attended.sh new file mode 100755 index 0000000..94a7d09 --- /dev/null +++ b/scripts/root-grant-attended.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +exec python3 "$(dirname "$0")/root_grant_attended.py" "$@" >/dev/null 2>&1 diff --git a/scripts/root_grant_attended.py b/scripts/root_grant_attended.py new file mode 100644 index 0000000..f150b19 --- /dev/null +++ b/scripts/root_grant_attended.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Silent attended OpenBao admission/publisher; never emits server responses.""" +import argparse +import datetime as dt +import fcntl +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +from root_grant_publish import MAX_BYTES, RECORD_ID, prepare_payload +from openbao_platform_admin_check import require_attended + +ROOT = Path(__file__).resolve().parents[1] +PATH = "platform/data/custody/net-kingdom/platform-root" +META = "platform/metadata/custody/net-kingdom/platform-root" +ROLE_PATH = "auth/kubernetes/role/user-engine-root-custody-reader" +POLICY = "user-engine-root-custody-reader" +ROLE = {"bound_service_account_names": ["user-engine"], + "bound_service_account_namespaces": ["user-engine"], "audience": "openbao", + "token_policies": [POLICY], "token_no_default_policy": True, + "token_ttl": 60, "token_max_ttl": 60, "token_explicit_max_ttl": 60} + + +class Refused(Exception): + pass + + +def digest(document): + return hashlib.sha256(json.dumps(document, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()).hexdigest() + + +def atomic_json(path, document): + path = Path(path) + if path.is_symlink(): + raise Refused("unsafe_state_path") + with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as out: + temporary = Path(out.name) + os.chmod(temporary, 0o600) + out.write(json.dumps(document, sort_keys=True).encode()) + out.flush() + os.fsync(out.fileno()) + try: + os.replace(temporary, path) + fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + finally: + temporary.unlink(missing_ok=True) + + +class Bao: + def call(self, command, payload=None, optional=False): + result = subprocess.run(["bao", *command], input=None if payload is None else json.dumps(payload).encode(), + capture_output=True, timeout=15) + if result.returncode: + # Distinguish a documented missing-path message from auth/outage. + if optional and result.returncode == 2 and result.stderr.strip() == ("No value found at " + command[-1]).encode(): + return None + raise Refused("openbao_request_failed") + if len(result.stdout) > MAX_BYTES * 2: + raise Refused("oversize_owner_response") + return json.loads(result.stdout) if result.stdout.strip() else {} + + def read(self, path, optional=False): + response = self.call(["read", "-format=json", path], optional=optional) + return None if response is None else response["data"] + + def write(self, path, payload): + return self.call(["write", "-format=json", path, "-"], payload) + + +def check_baseline(actual, state, validator): + if state is None: + if actual is not None: + raise Refused("controlled_baseline_import_required") + return + previous = state["document"] + if validator.validate(previous) or previous["record_id"] != RECORD_ID or state["digest"] != digest(previous): + raise Refused("invalid_retained_baseline") + if actual is None or validator.validate(actual["data"]["document"]): + raise Refused("missing_or_invalid_source") + document = actual["data"]["document"] + if digest(document) != state["digest"] or actual["metadata"]["version"] != state["kv_version"]: + raise Refused("source_differs_from_retained_baseline") + + +def retain_history(state_path, receipt): + history = state_path.parent / "history" + history.mkdir(mode=0o700, exist_ok=True) + target = history / (str(receipt["revision"]) + "-" + receipt["digest"] + ".json") + if target.exists(): + if json.loads(target.read_text()) != receipt: + raise Refused("history_receipt_differs") + return + with target.open("x") as out: + os.chmod(target, 0o600) + json.dump(receipt, out, sort_keys=True) + out.flush() + os.fsync(out.fileno()) + fd = os.open(history, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(fd) + finally: + os.close(fd) + + +def publish(bao, candidate, state_path, validator, provenance): + state_path = Path(state_path) + if not isinstance(provenance, str) or not provenance.strip() or len(provenance) > 300 or validator.SECRET_VALUE.search(provenance): + raise Refused("nonsecret_human_provenance_required") + if state_path.is_symlink(): + raise Refused("unsafe_state_path") + state = json.loads(state_path.read_text()) if state_path.exists() else None + actual = bao.read(PATH, optional=True) + # Pending state is fsynced before network mutation. Recover only exact readback; + # ambiguous network failures are never retried or cleared automatically. + if state is not None and state.get("pending"): + pending = state["pending"] + if actual is None or digest(actual["data"]["document"]) != pending["digest"] or actual["metadata"]["version"] != pending["expected_kv_version"]: + raise Refused("pending_publication_requires_owner_review") + if validator.validate(pending["document"]) or pending["document"]["record_id"] != RECORD_ID: + raise Refused("invalid_pending_document") + state = {key: value for key, value in pending.items() if key != "expected_kv_version"} + retain_history(state_path, state) + atomic_json(state_path, state) + check_baseline(actual, state, validator) + previous = None if state is None else state["document"] + version = 0 if actual is None else actual["metadata"]["version"] + if previous is not None and digest(candidate) == state["digest"]: + # Exact replay is an idempotent readback, not a new custody/KV revision. + return {**{key: value for key, value in state.items() if key != "document"}, "unchanged": True} + payload = prepare_payload(candidate, previous, version, validator) + now = dt.datetime.now(dt.timezone.utc) + times = [candidate["updated_at"]] + for entry in candidate["entries"]: + times.append(entry["granted"]["at"]) + if entry.get("revoked"): + times.append(entry["revoked"]["at"]) + if any(validator.parse_time(value) > now for value in times): + raise Refused("future_custody_action") + receipt = {"record_id": RECORD_ID, "revision": candidate["revision"], "digest": digest(candidate), + "kv_version": version + 1, "document": candidate, "provenance_ref": provenance, + "recorded_at": dt.datetime.now(dt.timezone.utc).isoformat()} + pending = dict(receipt, expected_kv_version=version + 1) + atomic_json(state_path, {**(state or {}), "pending": pending}) + bao.write(PATH, payload) + returned = bao.read(PATH) + if returned["metadata"]["version"] != version + 1 or digest(returned["data"]["document"]) != receipt["digest"]: + raise Refused("publication_readback_failed") + # Independent immutable receipts precede advancing local state. They must be + # backed up off-host; restoring only this directory is not independent proof. + retain_history(state_path, receipt) + atomic_json(state_path, receipt) + return {key: value for key, value in receipt.items() if key != "document"} + + +def provision(bao): + # Require existing Kubernetes auth; no auth mount or cluster reviewer mutation. + if bao.read("auth/kubernetes/config") is None: + raise Refused("kubernetes_auth_unavailable") + audit = bao.read("sys/audit") + if not audit: + raise Refused("audit_required") + policy = (ROOT / "openbao/root-grant/reader.hcl").read_text() + existing = bao.read("sys/policies/acl/" + POLICY, optional=True) + if existing is not None and existing.get("policy", existing.get("rules", "")).strip() != policy.strip(): + raise Refused("existing_reader_policy_differs") + role = bao.read(ROLE_PATH, optional=True) + if role is not None and any(role.get(key) != value for key, value in ROLE.items()): + raise Refused("existing_reader_role_differs") + if existing is None: + bao.write("sys/policies/acl/" + POLICY, {"policy": policy}) + if role is None: + bao.write(ROLE_PATH, ROLE) + fresh = bao.read(ROLE_PATH) + if any(fresh.get(key) != value for key, value in ROLE.items()): + raise Refused("role_readback_failed") + fresh_policy = bao.read("sys/policies/acl/" + POLICY) + if fresh_policy.get("policy", fresh_policy.get("rules", "")).strip() != policy.strip(): + raise Refused("policy_readback_failed") + # Require CAS on this key without altering retention or destroying versions. + metadata = bao.read(META, optional=True) + if not metadata or not metadata.get("cas_required"): + bao.write(META, {"cas_required": True}) + if bao.read(META).get("cas_required") is not True: + raise Refused("cas_required_readback_failed") + return {"reader_role": ROLE_PATH, "reader_namespace": "user-engine", "reader_service_account": "user-engine", + "audience": "openbao", "record_id": RECORD_ID, "cas_required": True, "audit_enabled": True} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["provision", "publish", "restore-check"]) + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--state", type=Path) + parser.add_argument("--candidate", type=Path) + parser.add_argument("--net-kingdom", type=Path) + parser.add_argument("--provenance-ref") + parser.add_argument("--validator-digest") + parser.add_argument("--schema-digest") + args = parser.parse_args() + try: + if args.receipt.exists() or args.receipt.is_symlink(): + raise Refused("unique_run_receipt_required") + require_attended() + if os.getenv("BAO_ADDR") != "http://127.0.0.1:18200": + raise Refused("approved_operator_tunnel_required") + bao = Bao() + if args.action == "provision": + receipt = provision(bao) + else: + if not args.state or not args.net_kingdom or not args.validator_digest or not args.schema_digest: + raise Refused("pinned_source_and_state_required") + source = args.net_kingdom / "tools/custody-record/validate.py" + schema = args.net_kingdom / "canon/schemas/custody-record_v0.1.schema.json" + for path, expected in [(source, args.validator_digest), (schema, args.schema_digest)]: + if hashlib.sha256(path.read_bytes()).hexdigest() != expected: + raise Refused("canon_pin_mismatch") + spec = importlib.util.spec_from_file_location("custody_validator", source) + validator = importlib.util.module_from_spec(spec) + spec.loader.exec_module(validator) + if not args.state.parent.is_dir(): + raise Refused("owner_state_directory_required") + # Serialize this publisher, including reads and pending-journal recovery. + with (args.state.parent / "publisher.lock").open("a") as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + if args.action == "restore-check": + if not args.state.exists(): + raise Refused("retained_baseline_required") + state = json.loads(args.state.read_text()) + if state.get("pending"): + raise Refused("pending_publication_requires_owner_review") + check_baseline(bao.read(PATH, optional=True), state, validator) + receipt = {"record_id": RECORD_ID, "baseline_matches": True, "revision": state["document"]["revision"]} + else: + if not args.candidate or not args.provenance_ref: + raise Refused("candidate_and_human_provenance_required") + candidate = json.loads(args.candidate.read_bytes()[:MAX_BYTES + 1]) + receipt = publish(bao, candidate, args.state, validator, args.provenance_ref) + atomic_json(args.receipt, {"ok": True, **receipt}) + return 0 + except Exception: + # CLI/process/validation errors may embed secrets. Never emit them. + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_root_grant_attended.py b/tests/test_root_grant_attended.py new file mode 100644 index 0000000..f2b88c2 --- /dev/null +++ b/tests/test_root_grant_attended.py @@ -0,0 +1,140 @@ +import copy +import importlib.util +import json +from pathlib import Path +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) +import root_grant_attended as attended + +spec = importlib.util.spec_from_file_location("custody_validator", ROOT.parent / "net-kingdom/tools/custody-record/validate.py") +validator = importlib.util.module_from_spec(spec) +spec.loader.exec_module(validator) + + +class FakeBao: + def __init__(self, document=None, version=1): + self.actual = None if document is None else {"data": {"document": copy.deepcopy(document)}, "metadata": {"version": version}} + self.metadata = {"max_versions": 123, "cas_required": False} + self.config = {"auth/kubernetes/config": {}, "sys/audit": {"file/": {"type": "file"}}, attended.META: self.metadata} + self.writes = [] + self.fail_after_write = False + + def read(self, path, optional=False): + return copy.deepcopy(self.actual if path == attended.PATH else self.config.get(path)) + + def write(self, path, payload): + self.writes.append((path, copy.deepcopy(payload))) + if path == attended.PATH: + version = 0 if self.actual is None else self.actual["metadata"]["version"] + if payload["options"]["cas"] != version: + raise attended.Refused("cas_conflict") + self.actual = {"data": payload["data"], "metadata": {"version": version + 1}} + if self.fail_after_write: + raise attended.Refused("ambiguous_timeout") + else: + self.config[path] = payload + if path == attended.META: + self.metadata.update(payload) + self.config[path] = self.metadata + + +class AttendedTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.state = Path(self.temp.name) / "baseline.json" + self.first = json.loads((ROOT.parent / "net-kingdom/tools/custody-record/fixtures/valid/grant.json").read_text()) + self.first["record_id"] = attended.RECORD_ID + self.next = dict(copy.deepcopy(self.first), revision=self.first["revision"] + 1) + + def tearDown(self): + self.temp.cleanup() + + def test_first_publication_and_update_keep_history(self): + bao = FakeBao() + receipt = attended.publish(bao, self.first, self.state, validator, "human-receipt") + self.assertNotIn("document", receipt) + attended.publish(bao, self.next, self.state, validator, "human-receipt-2") + self.assertEqual(len(list((self.state.parent / "history").glob("*.json"))), 2) + self.assertEqual(bao.writes[1][1]["options"], {"cas": 1}) + + def test_exact_replay_creates_no_kv_version_or_history(self): + bao = FakeBao() + attended.publish(bao, self.first, self.state, validator, "human-receipt") + result = attended.publish(bao, self.first, self.state, validator, "human-receipt") + self.assertTrue(result["unchanged"]) + self.assertEqual(len(bao.writes), 1) + self.assertEqual(len(list((self.state.parent / "history").glob("*.json"))), 1) + + def test_existing_receipt_prevents_any_live_call(self): + from unittest.mock import patch + receipt = Path(self.temp.name) / "receipt.json" + receipt.write_text('{"ok":true}') + with patch.object(sys, "argv", ["executor", "provision", "--receipt", str(receipt)]), patch.object(attended, "require_attended") as check: + self.assertEqual(attended.main(), 1) + check.assert_not_called() + self.assertEqual(receipt.read_text(), '{"ok":true}') + + def test_existing_source_without_baseline_is_not_bootstrap(self): + bao = FakeBao(self.first) + with self.assertRaises(attended.Refused): + attended.publish(bao, self.next, self.state, validator, "human-receipt") + self.assertEqual(bao.writes, []) + + def test_rollback_restore_denied(self): + bao = FakeBao() + attended.publish(bao, self.first, self.state, validator, "human-receipt") + attended.publish(bao, self.next, self.state, validator, "human-receipt") + state = json.loads(self.state.read_text()) + with self.assertRaises(attended.Refused): + attended.check_baseline(FakeBao(self.first).actual, state, validator) + + def test_ambiguous_network_failure_leaves_durable_pending(self): + bao = FakeBao() + bao.fail_after_write = True + with self.assertRaises(attended.Refused): + attended.publish(bao, self.first, self.state, validator, "human-receipt") + self.assertIn("pending", json.loads(self.state.read_text())) + bao.fail_after_write = False + attended.publish(bao, self.next, self.state, validator, "human-receipt-2") + self.assertNotIn("pending", json.loads(self.state.read_text())) + self.assertEqual(len(bao.writes), 2) + self.assertEqual(len(list((self.state.parent / "history").glob("*.json"))), 2) + + def test_pending_mismatch_requires_review_and_no_retry(self): + bao = FakeBao() + bao.fail_after_write = True + with self.assertRaises(attended.Refused): + attended.publish(bao, self.first, self.state, validator, "human-receipt") + bao.actual = None + with self.assertRaises(attended.Refused): + attended.publish(bao, self.next, self.state, validator, "human-receipt") + self.assertEqual(len(bao.writes), 1) + + def test_exact_admission_is_idempotent_and_retention_unchanged(self): + bao = FakeBao() + attended.provision(bao) + self.assertEqual(bao.config[attended.ROLE_PATH], attended.ROLE) + self.assertTrue(bao.metadata["cas_required"]) + self.assertEqual(bao.metadata["max_versions"], 123) + writes = len(bao.writes) + attended.provision(bao) + self.assertEqual(writes, len(bao.writes)) + + def test_different_existing_role_and_missing_audit_are_refused_before_write(self): + for mode in ["role", "audit"]: + bao = FakeBao() + if mode == "role": + bao.config[attended.ROLE_PATH] = dict(attended.ROLE, bound_service_account_names=["other"]) + else: + bao.config["sys/audit"] = {} + with self.assertRaises(attended.Refused): + attended.provision(bao) + self.assertEqual(bao.writes, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/workplans/RPF-WP-0049-platform-root-custody-record-delivery.md b/workplans/RPF-WP-0049-platform-root-custody-record-delivery.md index fc4fac8..0e8f59b 100644 --- a/workplans/RPF-WP-0049-platform-root-custody-record-delivery.md +++ b/workplans/RPF-WP-0049-platform-root-custody-record-delivery.md @@ -41,36 +41,39 @@ These are preparation artifacts, not an admitted or operational source. ```task id: RPF-WP-0049-T02 -status: wait +status: done priority: high depends_on: [NK-WP-0045-T01, NK-WP-0045-T04] -blocked_on: message-from:user-engine -blocking_reason: "Design selection is approved; user-engine must still supply its actual runtime reader identity and supported authentication route. No deployed ServiceAccount is known." state_hub_task_id: "16096f84-6232-5d97-8fe3-b4683c79f4fb" ``` Approved source: platform/data/custody/net-kingdom/platform-root, record ID nk-custody:platform-root and exact-path read using the existing owner-routed workload admission. user-engine confirmed nested document compatibility; -its adapter is transport-neutral and runtime identity remains unconfirmed. +its adapter is transport-neutral. Owner rapp-user-engine manifests confirm +namespace user-engine / ServiceAccount user-engine. Selected admission role +is user-engine-root-custody-reader, auth/kubernetes, audience openbao, 60s +maximum token TTL and no default policy; dedicated projection is owned by +rapp-user-engine. This resolves source identity, not live admission. ## Implement attended executor, recovery and audit for the approved source ```task id: RPF-WP-0049-T03 -status: wait +status: done priority: high depends_on: [RPF-WP-0049-T02] -blocking_reason: "Surface is approved; actual reader identity and separate attended writer custody bindings remain required for final executor/admission/restore configuration." state_hub_task_id: "0bc4bc1f-e279-5c92-97bb-cf76d2e33749" ``` -Implement reviewed value-safe executor with owner-read baseline + CAS, exact -identity admission configuration, per-key required CAS, retention/independent -high-water history, audit and rollback-safe restoration. Pin canon source. -Do not let direct writer capability bypass transition or ceremony requirements. +Implemented silent attended provision/publish/restore-check executor with exact +reader admission, per-key required CAS, active-audit check, pinned canon, retained +high-water baseline, fsynced pending journal and immutable local history. +Readback is mandatory; replay is idempotent; conflicting existing role/policy, +missing state and rollback fail closed. Off-host history placement and actual +recovery evidence remain execution/acceptance dependencies in T04/T05. -## Approve bounded live provisioning and custody bootstrap +## Execute authorized attended provisioning and establish initial baseline ```task id: RPF-WP-0049-T04 @@ -78,13 +81,16 @@ status: wait priority: high depends_on: [RPF-WP-0049-T03] needs_human: true -blocking_reason: "Separate founder go for named commits, exact read/writer bindings, attended window and real custody provenance; decision on design alone is not live approval." +blocking_reason: "Founder go-forward is recorded; execution needs founder terminal OIDC/MFA, owner state/history location and real ceremony inputs or founder choice of an empty baseline. No real custodian can be inferred." state_hub_task_id: "88b932e8-1010-56c6-be27-ad46f47e1e80" ``` -Assemble exact later approval packet from producer README. Source-only draft -policies are not approval to apply or create entries. Bootstrap ceremony facts -are supplied by human custodian, never inferred from IAM or test fixtures. +Founder continued approval: "Good, i approve go forward please." This authorizes +bounded execution of the reviewed design; generic permission is not pending. +The concrete attended provisioning command is in the producer README. The +founder runs existing operator OIDC/MFA in their own terminal. Human supplies +real ceremony issuer/subject/reference or chooses a no-grant empty baseline; +legacy planned roster and bootstrap setup operator are not grant evidence. ## Return producer delivery receipts @@ -116,3 +122,11 @@ bindings/final executor/recovery preparation, T04 on separate attended live approval and ceremony provenance, and T05 on actual producer delivery receipts. The workplan remains blocked on those residual dependencies. No live mutation or real custody entry is authorized by this design acceptance. + +## Continued implementation — 2026-10-03 + +T02/T03 source identity and executor implementation complete. No further +generic approval requested; T04 is attended execution/information dependency. +T05 still requires live producer read/denial/outage/restore/audit receipts. +NetKingdom canon pin db319335b27b61b855382ee5af5d690f06eeb010. No real +root grant, privileged login, production admission or readiness claimed.