diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/artifact-manifest.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/artifact-manifest.json new file mode 100644 index 00000000..82432287 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/artifact-manifest.json @@ -0,0 +1,33 @@ +{ + "scope": "two-path canonical source follow-through; root publication pending", + "files": [ + { + "path": "integration-manifest.json", + "sha256": "sha256:97c9d28bb094e6089b2b24ce4e1ae4ba90d319f1042cd83871429953e9eda97d" + }, + { + "path": "integration-result.json", + "sha256": "sha256:cb081438de0b8ffc2c8e47464699b4d0ba9f88af61c25c852b8078dc867d86ce" + }, + { + "path": "owner-plan.post.md", + "sha256": "sha256:b984425e043e08e5bd1e53dbef90839af5258b8fdeee057d2293a93b78bb492d" + }, + { + "path": "source.patch", + "sha256": "sha256:682dd10dcd39e148e84dacd91e29c5b6016d30029e0d7c2c82f3cd3f82739261" + }, + { + "path": "verification.json", + "sha256": "sha256:8cc33b5f431b4b4d206a17da4e96ecba8e5a12cb3e88092bc82710a12bd8374d" + }, + { + "path": "workplan.post.md", + "sha256": "sha256:c35019a8609e52fc4a7f5cc14e7b12cdc17dffbc8cd0b564a48a6b9fb6b5035d" + }, + { + "path": "workplan.pre.md", + "sha256": "sha256:b3cab4e8d14b3b03179fc72a6e9ce1c0ee840cfa3247c022c1324d60378bde5b" + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/integration-manifest.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/integration-manifest.json new file mode 100644 index 00000000..b944dcb7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/integration-manifest.json @@ -0,0 +1,22 @@ +{ + "scope": "exact two-path canonical source integration only; no Git/Hub/native actions", + "paths": [ + { + "path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md", + "pre_sha256": "sha256:b3cab4e8d14b3b03179fc72a6e9ce1c0ee840cfa3247c022c1324d60378bde5b", + "post_sha256": "sha256:c35019a8609e52fc4a7f5cc14e7b12cdc17dffbc8cd0b564a48a6b9fb6b5035d", + "postimage": "workplan.post.md" + }, + { + "path": "docs/keycape-isolated-consumer-continuity-plan.md", + "pre_sha256": null, + "post_sha256": "sha256:b984425e043e08e5bd1e53dbef90839af5258b8fdeee057d2293a93b78bb492d", + "postimage": "owner-plan.post.md" + } + ], + "unrelated_worktree_hashes": { + ".custodian-brief.md": "sha256:4b3536c5d30aa558f9ba455b5f8984e54fe451a1792388be7006f2dc0a361193", + "WORK-RECORDS.md": "sha256:ec39fefa21b5a2dde0bab42af82c9354c475f3ae64d13d45f9405796a04cc790" + }, + "frozen_packet_manifest_sha256": "sha256:e557347945780f2ae81782a64af0cb976e43aca9c25b0f7444167473adccb0a2" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/integration-result.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/integration-result.json new file mode 100644 index 00000000..6b9a4e75 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/integration-result.json @@ -0,0 +1,17 @@ +{ + "status": "exact_two_source_paths_applied", + "git_hub_and_native_actions": false, + "paths": [ + { + "path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md", + "pre_sha256": "sha256:b3cab4e8d14b3b03179fc72a6e9ce1c0ee840cfa3247c022c1324d60378bde5b", + "post_sha256": "sha256:c35019a8609e52fc4a7f5cc14e7b12cdc17dffbc8cd0b564a48a6b9fb6b5035d" + }, + { + "path": "docs/keycape-isolated-consumer-continuity-plan.md", + "pre_sha256": null, + "post_sha256": "sha256:b984425e043e08e5bd1e53dbef90839af5258b8fdeee057d2293a93b78bb492d" + } + ], + "unrelated_worktree_hashes_preserved": true +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/owner-plan.post.md b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/owner-plan.post.md new file mode 100644 index 00000000..6c898623 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/owner-plan.post.md @@ -0,0 +1,99 @@ +# Original KeyCape isolated consumer continuity plan + +Owner: secrets-engine. Source outcome: SECRETS-WP-0015-T03, 2026-10-17. +This is a reviewed source plan, not native assent, a custody grant or an +executed recovery. T04 remains a human wait on the complete NK49T33 attempt. + +The [frozen owner packet](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/OWNER-PLAN.md) +contains the exact custody/command/negative/equality/timeout/abort plan and source +references. Its owner-plan SHA-256 is +`sha256:d358aaeedc18c42c591d3301ec34f493e63bf579bf5a17b6ccc7e32ec88ea8ed`. +The packet remains immutable; this source note does not revise its authority. + +## Exact original selector and source + +Use the existing [public selector](keycape-recovery-selector.json): issuer +`https://kc.coulomb.social`, client/audience `secrets-engine-openbao`, env name +`KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET`, subject +`service:secrets-engine`, tenant `tenant:platform`, exactly role +`secrets-engine`, scope `openbao:login`, denied scope `openbao:admin`. +The current consumer `service_auth.py` source SHA-256 is +`sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0`. +Its existing AST-only selector preflight passes at that exact hash. Historical +issuer/consumer successes predate the archive and retain their historical scope. + +The packet's [one-shot observer candidate](../../the-custodian/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer_contract_observer.py) +is `sha256:23467730be61625d53b34c09e62f737a35e3def10d22a3a64664835961d3f4bd`. +Its frozen four-file consumer-source manifest is +`sha256:5b0409d7141514295e7d0f636ff6fa5dbe9d1f0d4d23b2f9ebce84a174d69499`. +The observer remains a source candidate: it was tested with synthetic unsigned +tokens only and is not installed, invoked on real material, or wired into either +NK49 recovery or ordinary consumer authentication. Five synthetic tests pass; +seven parser-negative variants and source/input refusal checks are within those +tests, not extra native successes. + +## Claims and custody boundaries + +1. **Original issuer:** the existing frozen NK49 native checker binds the + original archived registration/env, original TLS/trust/JWKS, wrong-secret and + excess-scope refusal, and exact original config/registry/policy/receipt + equality. Its receipt keeps downstream consumer reconnect false. +2. **Consumer claim compatibility:** after a newly reviewed NK49 source/admission + closure, the existing issuer `service-token` command may mint a short-lived + original-identity token in the issuer's private tmpfs. A reviewed same-session + wrapper would hand only that bounded token object over private stdin to the + exact Secrets claim observer, then prove transient-token/resource cleanup. + The original client secret stays in the restored issuer environment. Native + issuer/JWKS verification precedes consumer parsing; the observer verifies no + signature or native provenance. This proposed callback is not implemented. +3. **Full original OpenBao consumer continuity:** requires Platform/rapp-openbao's + independently recovered original backend/custody, original KV registration, + narrow OIDC source role and actual operator authentication/MFA dependencies, + plus exact isolated routing/trust. NK49's identity archive does not supply + OpenBao Raft recovery. A fresh backend, production login or injected secret + cannot substitute for this return. Then use the unchanged ordinary attended + OpenBao in-memory secret source, KeyCape exchange, actual OpenBao JWT login + and definite operator/engine session revocation. + +The current no-file/no-fallback direction remains. Do not inject the recovered +issuer secret into ordinary `login_service_jwt`, create a persistent consumer +copy, use ambient/bootstrap credentials, or redirect production. An exceptional +source would need a separate explicit owner/founder custody decision; none is +proposed or implemented here. Service identity/login-only policy is not protected +backend execution authority. SECRETS-WP-0008-T06/0013 retain those scopes; +finished SECRETS-WP-0012 remains finished. + +## Actual acceptance and next owner action + +The next action is to assemble NK49T33's exact owner-held original TLS/archive/ +private-admission packet and surviving offline founder authorization before any +key prompt. Matching hashes, possession of an archive key, this workplan and +historical CCRs are not that authorization. Bind exact source/image/executor, +original issuer/case/target, deadline and permitted isolated effects in the +existing supervised recovery process. No new auth or approval service is needed. + +Keep original ciphertext/transfer/source/binary/trust and manifest-binding-mode +interfaces. Private manifests and equality hashes stay inside custody. Freeze any +new observer callback/dependency closure, native command count and phase-specific +negative parsing; its source/synthetic lifecycle review must precede real input. +Use the existing bounded NK49 execution, exact pidfd/session quiescence and +registered tmpfs/container cleanup. Abort on unknown provenance, changed original +bytes, ambiguous daemon action or incomplete cleanup; preserve failed receipts +and uncertainty rather than retrying or reinitializing state. + +Require an actual positive original-issuer token and exact consumer claims, +including no extra roles. Native wrong-expected-audience/required-role refusals +are verifier checks, not OpenBao authorization. The observer's seven deliberately +modified payloads prove parser refusal only; unsigned mutations prove no signed +claim denial. Full backend acceptance separately requires real resource-server +wrong-audience/scope/role refusal, restored policy/role/KV equality and session +revocation through the existing owner path. Missing original signed negative +cases stay missing; do not create grants or claim signature failure isolates a +particular claim. A fresh attempt token is not a preserved pre-outage token. + +T04 remains wait/needs_human, depends on T03 and NK-WP-0049-T33, and retains +actual native acceptance. T18 owns actual selected-consumer recovery; T19 owns +retained-factor MFA. Return separate closed issuer/consumer-contract/backend +receipts tied to one attempt. Export source/public signing/TLS hashes, allowed +identifiers, booleans and cleanup facts only; never secret/token/private manifest +or raw provider output. A human wait is not an automatic worker wakeup. diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/publish_owner.py b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/publish_owner.py new file mode 100644 index 00000000..95d311cd --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/publish_owner.py @@ -0,0 +1,50 @@ +"""Publish only independently reviewed offline owner handoffs, preserving other work.""" +from pathlib import Path +import argparse,hashlib,json,subprocess,sys +base=Path(__file__).resolve().parent +sys.path.insert(0,'/home/worsch/repo-manager/src') +from repo_manager.identifiers import ensure_missing_work_record_identifiers +p=argparse.ArgumentParser();p.add_argument('lane',choices=['cluster','secrets']);p.add_argument('action',choices=['commit','sync']);a=p.parse_args() +manifest=json.loads((base/(a.lane+'-integration.json')).read_text());owner=manifest['owner'];root=Path('/home/worsch')/owner +paths=list(manifest['files']);backup=base/(owner+'-publication-backup') +def git(*argv):return subprocess.check_output(['git','-C',str(root),*argv],stderr=subprocess.PIPE) +def sha(path):return 'sha256:'+hashlib.sha256(path.read_bytes()).hexdigest() +if a.action=='commit': + assert git('rev-parse','HEAD').decode().strip()==manifest['base_revision'] + for rel,pin in manifest['files'].items():assert sha(root/rel)==pin + assert not backup.exists();backup.mkdir() + originals={} + for name in ['.custodian-brief.md','WORK-RECORDS.md']: + if (root/name).exists(): + (backup/(name+'.worktree')).write_bytes((root/name).read_bytes());originals[name]=git('ls-files','--stage','--',name).decode() + (backup/'originals.json').write_text(json.dumps(originals)) + planned=ensure_missing_work_record_identifiers(root,execute=False) + assert set(planned['files_changed'])<=set(paths),'unrelated identifier writeback' + assigned=ensure_missing_work_record_identifiers(root) + inputs={rel:sha(root/rel) for rel in paths} + git('add','--',*paths) + r=subprocess.run(['git','-C',str(root),'commit','--only','-m','docs(recovery): deliver isolated original consumer custody plan','--',*paths],capture_output=True,text=True,timeout=180) + receipt={'owner':owner,'exit_code':r.returncode,'stdout':r.stdout,'stderr':r.stderr,'head':git('rev-parse','HEAD').decode().strip(),'files':inputs,'managed_identifier_assignments':assigned} + (base/(owner+'-commit.json')).write_text(json.dumps(receipt,indent=2)+'\n') + for name,index in originals.items(): + assert (root/name).read_bytes()==(backup/(name+'.worktree')).read_bytes() + assert git('ls-files','--stage','--',name).decode()==index + print(json.dumps({'owner':owner,'commit_exit':r.returncode,'head':receipt['head']}));raise SystemExit(r.returncode) +else: + try: + r=subprocess.run(['statehub','fix-consistency','--repo',owner,'--json'],cwd=root,capture_output=True,text=True,timeout=300) + (base/(owner+'-consistency.json')).write_text(r.stdout) + (base/(owner+'-consistency.stderr.txt')).write_text(r.stderr) + assert r.returncode==0,'consistency failed; retained receipt' + # Legacy sync omits record_id for some tasks; use the existing canonical + # forge projection rather than declaring matching statuses sufficient. + r=subprocess.run(['/home/worsch/repo-manager/.venv/bin/rmgr','sync','--path',str(root),'--push'],cwd=root,capture_output=True,text=True,timeout=180) + (base/(owner+'-projection.json')).write_text(r.stdout) + (base/(owner+'-projection.stderr.txt')).write_text(r.stderr) + finally: + for name,index in json.loads((backup/'originals.json').read_text()).items(): + (root/name).write_bytes((backup/(name+'.worktree')).read_bytes()) + if index: + mode,oid,stage,path=index.split();assert stage=='0';git('update-index','--cacheinfo',mode,oid,path) + assert git('ls-files','--stage','--',name).decode()==index + print(json.dumps({'owner':owner,'projection_exit':r.returncode,'preexisting_other_work_preserved':True}));raise SystemExit(r.returncode) diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-commit.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-commit.json new file mode 100644 index 00000000..a4ed9d0e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-commit.json @@ -0,0 +1,22 @@ +{ + "owner": "secrets-engine", + "exit_code": 0, + "stdout": "[main 691dc74] docs(recovery): deliver isolated original consumer custody plan\n 2 files changed, 130 insertions(+), 15 deletions(-)\n create mode 100644 docs/keycape-isolated-consumer-continuity-plan.md\n", + "stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 2, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n", + "head": "691dc742362eeec45d5bd32d2b468f31a959748e", + "files": { + "docs/keycape-isolated-consumer-continuity-plan.md": "sha256:b984425e043e08e5bd1e53dbef90839af5258b8fdeee057d2293a93b78bb492d", + "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md": "sha256:c35019a8609e52fc4a7f5cc14e7b12cdc17dffbc8cd0b564a48a6b9fb6b5035d" + }, + "managed_identifier_assignments": { + "schema": "repo-manager.identifier-assignment.v1", + "ok": true, + "executed": true, + "repo": "secrets-engine", + "namespace": "helixforge", + "assignments": [], + "historical_invalid_identifiers": [], + "historical_identity_collisions": [], + "files_changed": [] + } +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-consistency.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-consistency.json new file mode 100644 index 00000000..5300fa14 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-consistency.json @@ -0,0 +1,137 @@ +{ + "repo_slug": "secrets-engine", + "repo_path": "/home/worsch/secrets-engine", + "source_revision": "691dc742362eeec45d5bd32d2b468f31a959748e", + "source_revision_reason": null, + "issues": [ + { + "severity": "WARN", + "check_id": "C-33", + "message": "WORK-RECORDS.md is missing or stale \u2014 regenerate via fix-consistency (CUST-WP-0061-T04 generated work-record index)", + "file_path": "WORK-RECORDS.md", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-05", + "message": "Title drift in 'secrets-wp-0001': file='Bootstrap secrets-engine MVP' db='Bootstrap State Hub integration' (file wins)", + "file_path": "workplans/SECRETS-WP-0002-bootstrap.md", + "db_id": "6c9a8c0d-18b5-41ac-8cd5-a8e84fb286b4", + "file_value": "Bootstrap secrets-engine MVP", + "db_value": "Bootstrap State Hub integration", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-23", + "message": "Lifecycle drift in 'secrets-wp-0013': workplan status 'proposed' has an active task \u2014 report only (ADR-007: do not auto-promote proposed)", + "file_path": "workplans/SECRETS-WP-0013-task-scoped-openbao-execution.md", + "db_id": "5e38aecc-85ca-5f55-94c6-9fbb08604819", + "file_value": "proposed", + "db_value": "proposed", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-04", + "message": "Status drift in 'secrets-wp-0015': file='active' db='ready' (file wins)", + "file_path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md", + "db_id": "68a0642e-4908-5260-89c9-3adce60e6a4d", + "file_value": "active", + "db_value": "ready", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-10", + "message": "Task status drift 'SECRETS-WP-0015-T03': file='done' db='todo' (file wins)", + "file_path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md#SECRETS-WP-0015-T03", + "db_id": "6262a915-7655-50be-b9f7-481f816d7335", + "file_value": "done", + "db_value": "todo", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-22", + "message": "Task description drift 'SECRETS-WP-0015-T03': file content differs from DB (file wins)", + "file_path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md#SECRETS-WP-0015-T03", + "db_id": "6262a915-7655-50be-b9f7-481f816d7335", + "file_value": "Completed 2026-10-17 as source-plan preparation only. The reviewed owner plan\nis [keycape-isolated-consumer-continuity-p", + "db_value": "Next: map the exact original archive-delivered registration and producer-native\nchecker result to the intended consumer,", + "fixable": true + }, + { + "severity": "WARN", + "check_id": "C-39", + "message": "Human wait task 'SECRETS-WP-0015-T04' in workplan 'SECRETS-WP-0015' has needs_human: true without decision_id \u2014 unrouted human gate", + "file_path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md#SECRETS-WP-0015-T04", + "db_id": "", + "file_value": "needs_human: true", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-41", + "message": "Task wait-qualifier drift 'SECRETS-WP-0015-T04': file={'needs_human': True, 'blocking_reason': 'Source plan delivered in T03. Await NK-WP-0049-T33 original TLS/archive/private admission, exact native owner authority, and original OpenBao/custody recovery for actual consumer continuity.', 'decision_id': None, 'blocked_on': None, 'wait_until': None} db={'needs_human': True, 'blocking_reason': 'Await exact consumer proof/custody plan plus original TLS/private admission readiness; actual attended owner path/authority remains required.', 'decision_id': None, 'blocked_on': None, 'wait_until': None} (file wins)", + "file_path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md#SECRETS-WP-0015-T04", + "db_id": "aeec6eb0-39e2-5f7c-8f5d-778133edfc73", + "file_value": "{\"blocked_on\": null, \"blocking_reason\": \"Source plan delivered in T03. Await NK-WP-0049-T33 original TLS/archive/private admission, exact native owner authority, and original OpenBao/custody recovery for actual consumer continuity.\", \"decision_id\": null, \"needs_human\": true, \"wait_until\": null}", + "db_value": "{\"blocked_on\": null, \"blocking_reason\": \"Await exact consumer proof/custody plan plus original TLS/private admission readiness; actual attended owner path/authority remains required.\", \"decision_id\": null, \"needs_human\": true, \"wait_until\": null}", + "fixable": true + }, + { + "severity": "INFO", + "check_id": "C-08", + "message": "Closed DB workstream 'secrets-wp-0002' (id=7929a2d1\u2026, status=finished) has no backing workplan file", + "file_path": "", + "db_id": "7929a2d1-3006-5d4b-85e4-c62da055ee9b", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-28", + "message": "3 unread inbox message(s) older than 3 day(s): railiance-platform:0490edcb, the-custodian:23172cb2, the-custodian:51e9eace", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + }, + { + "severity": "WARN", + "check_id": "C-28", + "message": "2 unread message(s) lack thread_id for supersession tracking: the-custodian:23172cb2, the-custodian:51e9eace", + "file_path": "", + "db_id": "", + "file_value": "", + "db_value": "", + "fixable": false + } + ], + "fixes_applied": [ + "bindings: synced 17/17", + "C-17 cleared: pushed 1 backlogged commit(s)", + "C-33 fixed: regenerated WORK-RECORDS.md", + "C-05 fixed: workstream 6c9a8c0d\u2026 title \u2192 'Bootstrap secrets-engine MVP'", + "C-04 fixed: workstream 68a0642e\u2026 status \u2192 'active'", + "C-10 fixed: task 6262a915\u2026 status \u2192 'done'", + "C-22 fixed: task 6262a915\u2026 description updated", + "C-41 fixed: task aeec6eb0\u2026 wait qualifiers synced", + "brief: .custodian-brief.md updated", + "push: pushed" + ], + "summary": { + "fail": 0, + "automation_error": 0, + "warn": 10, + "info": 1 + }, + "result": "warn" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-consistency.stderr.txt b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-consistency.stderr.txt new file mode 100644 index 00000000..3345563f --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-owner-followthrough-r1/secrets-engine-consistency.stderr.txt @@ -0,0 +1 @@ +WARN: git commit failed for writeback: Generated-only commits rejected: staged diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/OWNER-PLAN.md b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/OWNER-PLAN.md new file mode 100644 index 00000000..b690148a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/OWNER-PLAN.md @@ -0,0 +1,285 @@ +# Original KeyCape consumer outage proof: SECRETS-WP-0015-T03 review candidate + +Prepared 2026-10-17. Scope is the exact owner proof/custody plan and a bounded +source observer. No value, private archive, token or TLS material was retrieved; +no namespace/container, issuer/OpenBao endpoint or native approval was used. +NK-WP-0049 remains read-only here. Existing T04/native/human gates remain open. + +## Actual gap and selected deliverable + +The current source owner selector is established, and its AST-only preflight +passes the already-reviewed service_auth.py digest. Historical 7 October issuer +issuance and 10 October OpenBao consumer login predate the 16 October archive. +They prove their historical operations, not that the current archive contains +an unmodified registration, that the original TLS trust is recoverable, or that +a consumer works with ordinary OpenBao/KeyCape unavailable. + +The current NK49 adapter proves the restored issuer's native registration checks +and keeps `downstream_consumer_reconnect_proven=false`. It never executes Secrets +Engine's consumer validator. Its native Go verifier requires the requested roles +to be present; Secrets Engine requires roles to equal exactly `[secrets-engine]`, +including no extra role. Native Go verification and Python claim preflight are +therefore distinct useful acceptance oracles. The latter explicitly does not +verify signatures. No security defect or privilege grant is inferred from this +split; OpenBao remains the actual resource-server signature verifier. + +The ordinary Secrets consumer reads its client secret only from OpenBao through +an attended OIDC memory source, self-revokes that operator session, exchanges at +KeyCape, then obtains a login-only OpenBao JWT session and self-revokes it. Files, +ambient secrets and bootstrap fallback are refused. If ordinary OpenBao is down, +that first custody read cannot happen. If only isolated KeyCape is restored, +the human OIDC/backend graph is also not established. Calling probe-openbao on a +reachable production backend would establish current liveness, not the outage +claim. Do not add a recovery client-secret fallback to make that call succeed. + +**Smallest deliverable now:** run the exact consumer claim validator against a +fresh token from the restored original issuer, with native issuer/JWKS verification +before that validator and no client-secret transfer out of the issuer. Report +consumer-contract compatibility only. Full backend continuity remains a separate +owner-admitted original-OpenBao recovery proof, not a second invented broker. + +## Exact public selector and owner coordinates + +| Field | Required value | +|---|---| +| Issuer / token URL | https://kc.coulomb.social / https://kc.coulomb.social/token | +| Client / audience | secrets-engine-openbao / secrets-engine-openbao | +| Original secret env | KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET | +| Subject / principal type | service:secrets-engine / service | +| Tenant / roles | tenant:platform / exactly [secrets-engine] | +| Scope / denied scope | openbao:login / openbao:admin | +| Claim profile | RS256, groups [], aal1/client_secret, mfa false, source key-cape; <=900s lifetime | +| Ordinary consumer custody | platform/data/workloads/keycape/secrets-engine-openbao, field CLIENT_SECRET | +| Ordinary operator source | auth/netkingdom role keycape-secrets-engine-openbao-operator-workload-kv-read; memory only; 5m/4 uses | +| Actual resource-server contract | JWT mount keycape-services, role secrets-engine-openbao, bound issuer above | +| Login-only policy | secrets-engine-login-self; self endpoints only, explicit client-secret data/metadata deny | + +The native role and ordinary custody coordinates are Platform/Mason-owned, not +new grants by Secrets Engine. CCR-2026-0038 login identity does not authorize +protected backend effects. SECRETS-WP-0008-T06/0013 retain those obligations. +The current `require_backend_execution_ready` continues to refuse service-jwt +protected execution before approval consumption or any backend call. + +## Frozen interfaces and admission + +Retain the exact original ciphertext identity and independently verified transfer +receipt already identified by NK49, the existing `explicit-digest-v1` or explicit +`ciphertext-derived-v1` private-manifest binding mode, source closure and target. +Private manifest/config/key/client-secret hashes stay inside custody; only allowed +public signing/certificate/source hashes and boolean equality results leave it. +Do not reconstruct a private digest from guessed public metadata. + +The fixed restored issuer image is +`forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010`. +Current launcher remains exactly six args: + +```text +run-isolated-keycape.sh VERIFIED_TRANSFER_RECEIPT ADMISSION ADMISSION_SHA256 TLS_CERT TLS_KEY TRUST +``` + +Current session: fixed local Docker Unix socket, two nonroot read-only containers, +network-none/shared loopback namespace, no host ports or restart/log driver, +512Mi/1CPU/64PIDs per container, owner-only tmpfs, exact source/binary/trust pins +and pre-import cleanup/reaper obligations. The helper and original issuer share +only their reviewed private tmpfs roots. Public source files and helper code for +this consumer observer need a newly reviewed closure before any real attempt. +No new image, Docker socket mount, external network, daemon or auth service is +needed by the observer design. + +`source-references.json` records exact observed public source. Root integrated the reviewed NK49 adapter source as +`c52ca7f405416d437951844c0640b86e677c3523`; the adapter +`sha256:86f37add7221e4c73e3f9d878c09a3cc81061627cbf024ed6887d973d929b718` +matches that committed checkpoint. Remote publication/projection is separate +root follow-through. Final source/admission pins must use that actual integrated +closure, with any subsequent changes separately reviewed. The existing frozen actual R6 recovery proof retains its own bytes, +failures and scope; adding this observer cannot inherit its proof digest. + +The reviewed source observer imports exactly the existing four-file Secrets +claim-validation closure, from a protected read-only staged copy. It verifies +those source hashes against `consumer-source-manifest.json`, then accepts at most +64KiB of strict duplicate-free token JSON over private stdin. It never calls a +secret source, exchange, subprocess, OpenBao or network. Output contains booleans +only, including explicit false signature/native-provenance/backend claims. +The manifest hash must be pinned in the new admission; it is not authorization. + +## Narrow original-token consumer check after native owner approval + +1. Complete NK49T35 original TLS/trust disposition, NK49T36 original registration + and private admission, and T33 exact command/revision/authority review. Original + registration must be unique, confidential, client_credentials-enabled and + bound to exactly the original env selector after authenticated decryption. + The exact-one guard runs **after** decryption, not before its key prompt. +2. Inside the existing protected restore/session path, execute the unchanged + native verify-client against the restored original issuer with original + environment and TLS/JWKS. Require every positive/native wrong-secret and + openbao:admin refusal already required by NK49. Establish before/after exact + original config, complete registry/policy/receipt tree and environment equality. +3. Use the pinned issuer's **existing** `service-token` entry point, in that same + isolated issuer, with `-issuer https://kc.coulomb.social -client-id + secrets-engine-openbao -audience secrets-engine-openbao -scope openbao:login + -secret-env KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET -out + /tmp/secrets-engine-consumer-contract.json`. The command takes an env name, + never a secret value. It independently verifies the issued token against the + restored original JWKS. Output is an exclusive-create mode-0600 bearer-token + file on the issuer's existing 16Mi tmpfs, never a workstation client-secret + copy. Existing file/symlink or unknown containment stops before issuance. +4. The reviewed wrapper reads that bounded new token object only into its private + process memory, removes the tmpfs bearer file, then delivers the bytes over + sealed stdin to the one-shot observer in the existing network-none helper. + The original client secret never leaves the restored issuer env. No token + value, auth header, private claim body or raw error reaches tool stdout, + logs, Git, State Hub or a safe receipt. A failed transfer/cleanup is failure, + not authority to mint another token or use a host file fallback. +5. Run `python3 -I -B consumer_contract_observer.py --source-root + --source-manifest + --source-manifest-sha256 + sha256:5b0409d7141514295e7d0f636ff6fa5dbe9d1f0d4d23b2f9ebce84a174d69499`. + The final admitted wrapper supplies stdin privately; do not paste a token + into a shell command or run this manually with real values. Require exact + actual positive claim preflight and every labelled local claim-parser negative. +6. Separately, reuse native verify-client with a deliberately wrong expected + audience and a deliberately absent required role against the same existing + original registration. Each expected mismatch must fail for its intended + verification phase with original TLS/signature positively established first; + a generic transport/TLS failure is not a negative pass. This changes no + registration or grant. Its wrong-audience verifier result is not an OpenBao + role denial. Keep the already-required actual excessive-scope/wrong-secret + issuer denials. Source/review tests must bind phase-specific negative parsing + before this can be an executable native callback. +7. Repeat full original config/env/registry/policy/receipt/JWKS equality, then + record a separate consumer-contract observer receipt bound to the same run, + original ciphertext/admission, native verifier/source and observer closure. + Preserve the existing NK49 receipt's downstream-consumer flag false. Do not + broaden its closed allowlist or change its native status to a backend result. +8. Reuse NK49's exact pidfd/deadline quiescence and owned resource cleanup. + Explicitly prove the token tmpfs path absent before final teardown and all + owning tmpfs/container/plaintext resources absent afterward. All original + signing material and generated JWTs stay confined to this boundary. A JWT + is not individually revoked by deleting its file; it remains time-bound up + to 900 seconds. No original provider grant is changed to simulate revocation. + +This observer is **source preparation**, not yet wired into NK49. The old six-arg +runner cannot call it. A minimal separately reviewed owner integration adds the +frozen observer/source manifest to existing preflight/dependency closure, invokes +it while the original isolated session still exists and records the separate +closed receipt before existing cleanup. Do not import the consumer after its +cleanup or add a background process outside the existing reaper session. +Private token handling and extra native executions require a fresh exact NK49 +admission and synthetic lifecycle/phase-failure tests first. No real execution is +proposed until root has that complete revision and the actual owner/human assent. + +## Negative/equality oracles and what they establish + +| Oracle | Required result / scope | +|---|---| +| Public source selector | all eight fields and fixed consumer source hash match; no archive/native claim | +| Original archived binding | exactly one original registration and exact delivered env binding after verified archive restore | +| TLS/signing continuity | original accepted cert/trust/issuer and original signing SPKI in live isolated JWKS; no self-signed substitute | +| Native positive | original valid secret yields correctly signed scoped token at restored issuer | +| Wrong unrelated secret | explicit native HTTP/authentication refusal; not predecessor-rotation continuity | +| Excess requested scope | original issuer explicitly refuses openbao:admin | +| Actual consumer positive | exact frozen Python validator accepts that privately delivered original-issuer token, including exact roles/no extras | +| Wrong audience/required role | native verifier expected-binding mismatch on correctly reached original issuer; not an OpenBao authorization case | +| Parser negatives | seven in-memory deliberately modified payloads refused: aud, scope, wrong role, extra role, tenant, subject, expiry; **noncryptographic** checks only | +| Source/manifest/input failure | source drift, wrong source-manifest pin, duplicate/oversize/malformed JSON and forbidden extra token refuse without private diagnostics | +| Restored equality | exact original config/environment and complete registry/policy/receipt bytes equal before/after; original public signing identity equal | +| Token freshness | newly issued token is within <=900s; this is attempt freshness, not recovery of a pre-outage token | +| Cleanup | no transient token file, owning tmpfs/container/private plaintext, process/session or daemon uncertainty hidden | + +The observer's synthetic tokens deliberately have no real signature and may pass +its positive shape check. That is expected and is explicitly reported as +`signature_verified_here=false`; it must never be treated as token authentication. +Original-token provenance comes from the exact owner wrapper/native checker, +not a caller-supplied `verified: true` field or self-attesting JSON. + +## Full original OpenBao consumer continuity: separate required owner return + +To run the ordinary `login_service_jwt`/probe-openbao during a genuine ordinary- +service outage, Platform/rapp-openbao must first supply its existing independently +recoverable **original** OpenBao snapshot, unseal/recovery custody, exact version, +audit/lease policy and isolated restore receipt. The NK49 identity archive does +not contain that OpenBao Raft state. A fresh empty backend with a hand-authored +matching role, a dev root token, or a fabricated KV copy is not recovery of the +original consumer. No snapshot/key/unseal retrieval is requested here. + +The isolated backend must have the restored original CLIENT_SECRET KV version, +original narrow operator OIDC source role, original keycape-services JWT mount, +role, policy and trust/discovery configuration. Bind private byte equality to +owner-held original snapshots inside custody and export booleans/public metadata +only. Restored ordinary OIDC source operation also needs the selected operator's +actual original authentication/MFA dependency graph, callback/local routing and +original TLS; this may depend on NK49T19 and cannot be inferred from service-token +issuance. An isolated DNS route must not redirect production consumers or contact +ordinary production issuer/OpenBao. Supply exact owner-controlled isolated +endpoint/callback mapping, trust and egress before changing source config. + +Then use the **unchanged** current attended OpenBao in-memory source to read and +self-revoke, current client exchange, actual restored OpenBao JWT login, exact +self-only policy/capability checks and definite engine session self-revocation. +Prove wrong-audience, wrong-scope and wrong-role **resource-server** denials with +an owner-admitted method that changes neither original registration nor roles. +If no appropriately signed negative can be produced under existing original +registrations, retain the missing negative; unsigned or payload-tampered tokens +prove only signature refusal, not which claim binding caused denial. Historical +CCR negative test clients are not invented archive contents or new grants. +Protected catalog execution remains separately refused/not exercised. + +If the owner instead chooses an exceptional isolated source for CLIENT_SECRET, +that is a new explicit scoped custody/governance decision against the current +only-OpenBao direction, not an implementation detail or a callback injection +shortcut. This packet recommends no exception and implements none. T04 remains +waiting until the actually chosen complete path is admitted and succeeds. + +## Offline authority, timeout, abort and receipts + +The recovery launcher validates metadata/source/containment, not a live approval +token. Possession of a backup age key, a matching hash, a historical CCR, current +owner files or source task status is not authority. Under ordinary issuer/approval +outage, use the existing founder-controlled supervised recovery process to retain +an exact offline action decision **before** custody prompts: named owner/executor, +proposal/revision/digest, original archive/transfer/admission/source/image/target, +permitted isolated token issuance/consumer check, no production/backend contact, +expiry and abort/recovery. Native approval-object consumption is only claimed +where the existing surviving approval path actually supplies it. No synthetic +approval, bootstrapped signing trust or new approval/auth service is proposed. +Existing root-approved work elsewhere does not authorize this action. + +Use the existing 1800-second execution deadline and exact parent/session pidfd +quiescence contract; native verifier timeout is currently 150 seconds. Bound the +added service-token invocation (CLI's own max five minutes), observer (30 seconds) +and negative checks within the same remaining deadline, with no retry. Exact +numbers and maximum additional issuances need source-owner final review before +native admission. Declared maximum additional service-client exchanges for this +observer design is three (one successful service-token, one wrong-expected-aud, +one wrong-required-role); each native verifier also performs its declared +scope/secret cases if configured, so final argv/oracle count must be frozen. +No payment/provider spend is invoked, but workstation resource use, recovery +burden and EUR loss bound remain unknown and supervised, never priced as free. + +Any source/private-admission/equality/provenance/negative-phase mismatch stops +without another key request or token issuance. Preserve the failing phase and +original recovery receipts; quiesce only exact owned processes and remove only +registered owned token/tmpfs/container objects. Parent death or in-flight Docker +uncertainty remains uncertain even after absence is observed. Preserve both the +failed return and cleanup return. Do not clear uncertainty by reinitializing +archives, rotating/replacing keys, editing the registry, redirecting production, +creating clients, reissuing TLS silently or restarting ordinary services. + +Safe combined evidence contains the same attempt ID, source/observer/manifest +hashes, original ciphertext/admission references, public certificate/SPKI hashes, +positive/negative phase booleans, original equality, issuer-only versus consumer- +contract versus backend claim scope, timeouts and exact cleanup obligations. +No token, client secret, seed, private manifest, config body, private key, user +record or raw upstream response belongs in it. + +## Review disposition and live records + +The packet makes SECRETS-WP-0015-T03's plan/custody dependencies concrete and adds +a reviewable one-shot source observer with five synthetic tests. It changes no +canonical source/workplan, owner decision or authority. Root may integrate the +owner-reviewed plan/observer into the existing owner source task after review; +source delivery is distinct from T04 native acceptance. T04 keeps its dependency +on NK49T33; NK49T36/T18/T19 and Platform original OpenBao/TLS/private custody/ +resource-server negative gates remain. No driver, intake, message or worker was +created. Existing finished0012 stays finished; steady-state adoption0008 and +protected effects0013 retain their scopes. diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/artifact-manifest.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/artifact-manifest.json new file mode 100644 index 00000000..77a90678 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/artifact-manifest.json @@ -0,0 +1,40 @@ +{ + "proposal_id": "SECRETS-WP-0015-T03-outage-consumer-source-r1", + "revision": 1, + "status": "source-review-candidate", + "native_authorized": false, + "files": [ + { + "path": "OWNER-PLAN.md", + "sha256": "sha256:d358aaeedc18c42c591d3301ec34f493e63bf579bf5a17b6ccc7e32ec88ea8ed" + }, + { + "path": "consumer-proof-interface.json", + "sha256": "sha256:50ea7080133f27a690e2fb559ea6e4686b1990ce725809419a0768081832406f" + }, + { + "path": "consumer-source-manifest.json", + "sha256": "sha256:5b0409d7141514295e7d0f636ff6fa5dbe9d1f0d4d23b2f9ebce84a174d69499" + }, + { + "path": "consumer_contract_observer.py", + "sha256": "sha256:23467730be61625d53b34c09e62f737a35e3def10d22a3a64664835961d3f4bd" + }, + { + "path": "existing-selector-preflight.json", + "sha256": "sha256:831e631bbb45bfe7e8cf196e0044601668a5e6800b8e4b56ae417045e9869d18" + }, + { + "path": "source-checks.json", + "sha256": "sha256:83604266a79a9fbc41790fb0ffdb5ab90b7d991971aaa5df16acac22a87e8c48" + }, + { + "path": "source-references.json", + "sha256": "sha256:f488eeab35504656c6f3b3c2721ca1c46b19eb529b9b0f05e9424cddf6913335" + }, + { + "path": "test_consumer_contract_observer.py", + "sha256": "sha256:34984176c281a3aa123b0ce6138e18e7dda5fae631e277e5b1726b223458f5a2" + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer-proof-interface.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer-proof-interface.json new file mode 100644 index 00000000..4ccff2fa --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer-proof-interface.json @@ -0,0 +1,63 @@ +{ + "schema": "secrets-engine.outage-consumer-proof-plan.v1", + "status": "source-review-candidate-not-native-admission", + "workplan_task": "SECRETS-WP-0015-T03", + "original_selector": { + "client_id": "secrets-engine-openbao", + "secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET", + "audience": "secrets-engine-openbao", + "scope": "openbao:login", + "subject": "service:secrets-engine", + "tenant": "tenant:platform", + "roles": "secrets-engine", + "deny_scope": "openbao:admin" + }, + "observer_source_manifest_sha256": "sha256:5b0409d7141514295e7d0f636ff6fa5dbe9d1f0d4d23b2f9ebce84a174d69499", + "future_private_input": { + "transport": "sealed stdin from reviewed same-session native service-token handoff", + "max_bytes": 65536, + "strict_keys": [ + "access_token", + "token_type", + "expires_in" + ], + "client_secret_permitted": false, + "input_logged_or_retained": false, + "signature_verification": "prior native original issuer/JWKS checker, not this observer" + }, + "native_issuer_receipt_preserved": { + "schema": "netkingdom.isolated-keycape-recovery.v1", + "status": "isolated_keycape_native_registration_checked", + "downstream_consumer_reconnect_proven": false + }, + "observer_receipt": { + "schema": "secrets-engine.isolated-consumer-claim-observer.v1", + "status": "consumer_claim_preflight_matches_not_backend_acceptance", + "required_positive": "consumer_claim_preflight_matches", + "required_false": [ + "signature_verified_here", + "native_input_provenance_verified_here", + "client_secret_source_exercised", + "openbao_login_performed", + "protected_backend_operation_performed", + "downstream_consumer_reconnect_proven", + "network_contacted" + ], + "negative_scope": "local claim-parser checks only; altered payloads are not signed native token proofs" + }, + "wrapper_required_binding": [ + "same_attempt_id", + "ciphertext_sha256", + "admission_sha256", + "native_verifier_sha256", + "original_public_signing_spki_sha256", + "original_tls_trust_disposition", + "observer_sha256", + "consumer_source_manifest_sha256", + "native_source_closure", + "before_after_original_equality", + "private_token_absence_and_resource_cleanup" + ], + "implemented_native_callback": false, + "full_original_openbao_consumer_acceptance": "pending independent original OpenBao/custody/SSO graph restore, unchanged attended source, real resource-server denial/revocation and owner admission" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer-source-manifest.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer-source-manifest.json new file mode 100644 index 00000000..f13cb86d --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer-source-manifest.json @@ -0,0 +1,6 @@ +{ + "src/secrets_engine/__init__.py": "sha256:6536fc21988755467f71a91e79c90bf984f1ee16e696566a03cc4c1240920573", + "src/secrets_engine/errors.py": "sha256:4cd0bfdc009556b35d4359a37d797a6bacf644ca4969d46e33f60435b4bafac8", + "src/secrets_engine/safe_paths.py": "sha256:90e31769f66f34910b562d64bebcaa3e8cea23e79678eff4d625efcefc0d4f73", + "src/secrets_engine/service_auth.py": "sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer_contract_observer.py b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer_contract_observer.py new file mode 100644 index 00000000..0926c056 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/consumer_contract_observer.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Review candidate: frozen consumer claim preflight, never authentication. + +Only the reviewed recovery wrapper may deliver a short-lived token via private +stdin after its independent original-issuer/JWKS verification. This program +performs no network/secret acquisition/OpenBao login and cannot prove that origin. +It emits no bearer token, claims, secret, parser diagnostic or provider output. +""" +from __future__ import annotations +import argparse +import base64 +from datetime import datetime, timezone +import hashlib +import importlib +import json +from pathlib import Path +import stat +import sys + +SOURCE_FILES = ( + 'src/secrets_engine/__init__.py', 'src/secrets_engine/errors.py', + 'src/secrets_engine/safe_paths.py', 'src/secrets_engine/service_auth.py', +) +LIMIT = 65536 + + +def unique(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError() + result[key] = value + return result + + +def source_module(root, manifest_path, expected): + if not expected.startswith('sha256:') or len(expected) != 71: + raise ValueError() + raw = manifest_path.read_bytes() + if len(raw) > LIMIT or 'sha256:' + hashlib.sha256(raw).hexdigest() != expected: + raise ValueError() + entries = json.loads(raw, object_pairs_hook=unique) + if set(entries) != set(SOURCE_FILES): + raise ValueError() + root = root.absolute() + for name in SOURCE_FILES: + path = root / name + if any(p.is_symlink() for p in (path, *path.parents)): + raise ValueError() + st = path.stat() + if not stat.S_ISREG(st.st_mode) or st.st_size > 1024 * 1024: + raise ValueError() + if 'sha256:' + hashlib.sha256(path.read_bytes()).hexdigest() != entries[name]: + raise ValueError() + if any(n == 'secrets_engine' or n.startswith('secrets_engine.') for n in sys.modules): + raise ValueError() + sys.path.insert(0, str(root / 'src')) + return importlib.import_module('secrets_engine.service_auth') + + +def observe(module, raw): + if not 0 < len(raw) <= LIMIT: + raise ValueError() + value = json.loads(raw, object_pairs_hook=unique) + if not isinstance(value, dict) or set(value) != {'access_token', 'token_type', 'expires_in'}: + raise ValueError() + token = value['access_token'] + if (not isinstance(token, str) or not token or value['token_type'].lower() != 'bearer' + or type(value['expires_in']) is not int or not 0 < value['expires_in'] <= 900): + raise ValueError() + cfg = module.KeyCapeServiceAuthConfig( + token_url='https://kc.coulomb.social/token', issuer='https://kc.coulomb.social') + now = datetime.now(timezone.utc) + checked = module.preflight_service_jwt(token, cfg, now=now) + # These are intentionally noncryptographic parser negatives. A body edit + # invalidates the old signature; this is not a signed foreign-claim oracle. + replacements = { + 'wrong_audience': {'aud': 'isolated-denied-audience'}, + 'excess_scope': {'scope': 'openbao:login openbao:admin'}, + 'wrong_role': {'roles': ['isolated-denied-role']}, + 'extra_role': {'roles': ['secrets-engine', 'isolated-denied-role']}, + 'wrong_tenant': {'tenant': 'tenant:isolated-denied'}, + 'wrong_subject': {'sub': 'service:isolated-denied'}, + 'expired': {'exp': int(now.timestamp()) - 1}, + } + parts = token.split('.') + negatives = {} + for name, replacement in replacements.items(): + body = {**checked.claims, **replacement} + encoded = base64.urlsafe_b64encode(json.dumps(body, separators=(',', ':')).encode()).decode().rstrip('=') + candidate = '.'.join((parts[0], encoded, parts[2])) + try: + module.preflight_service_jwt(candidate, cfg, now=now) + except module.BackendError: + negatives[name] = True + else: + negatives[name] = False + if not all(negatives.values()): + raise ValueError() + return { + 'schema': 'secrets-engine.isolated-consumer-claim-observer.v1', + 'status': 'consumer_claim_preflight_matches_not_backend_acceptance', + 'consumer_claim_preflight_matches': True, + 'claim_parser_refusals': negatives, + 'signature_verified_here': False, + 'native_input_provenance_verified_here': False, + 'client_secret_source_exercised': False, + 'openbao_login_performed': False, + 'protected_backend_operation_performed': False, + 'downstream_consumer_reconnect_proven': False, + 'network_contacted': False, + } + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source-root', type=Path, required=True) + parser.add_argument('--source-manifest', type=Path, required=True) + parser.add_argument('--source-manifest-sha256', required=True) + args = parser.parse_args() + try: + module = source_module(args.source_root, args.source_manifest, args.source_manifest_sha256) + result = observe(module, sys.stdin.buffer.read(LIMIT + 1)) + except Exception: + result = {'schema': 'secrets-engine.isolated-consumer-claim-observer.v1', + 'status': 'consumer_claim_preflight_refused', + 'private_diagnostics_suppressed': True} + print(json.dumps(result, sort_keys=True)) + return 0 if result['status'] == 'consumer_claim_preflight_matches_not_backend_acceptance' else 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/existing-selector-preflight.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/existing-selector-preflight.json new file mode 100644 index 00000000..5a68f5ae --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/existing-selector-preflight.json @@ -0,0 +1,17 @@ +{ + "schema": "secrets-engine.recovery-selector-preflight.v1", + "status": "source_selector_matches_not_native_assent", + "source_sha256": "sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0", + "consumer": { + "client_id": "secrets-engine-openbao", + "secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET", + "audience": "secrets-engine-openbao", + "scope": "openbao:login", + "subject": "service:secrets-engine", + "tenant": "tenant:platform", + "roles": "secrets-engine", + "deny_scope": "openbao:admin" + }, + "credential_source_exercised": false, + "network_contacted": false +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/source-checks.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/source-checks.json new file mode 100644 index 00000000..e4901633 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/source-checks.json @@ -0,0 +1,12 @@ +{ + "scope": "source-only observer; all tokens synthetic and intentionally not cryptographically valid", + "command": "python3 -B -m unittest discover -s docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1 -p test_consumer_contract_observer.py", + "tests_passed": 5, + "claim_parser_negative_variants": 7, + "existing_public_selector_preflight": "passed at exact previously reviewed c0ade429 source digest", + "native_calls": 0, + "credentials_read": false, + "ordinary_or_recovered_issuer_contacted": false, + "openbao_contacted": false, + "all_source_reference_files_match_head": true +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/source-references.json b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/source-references.json new file mode 100644 index 00000000..ebaf8de4 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/source-references.json @@ -0,0 +1,166 @@ +{ + "scope": "public source and historical evidence; no native source assent", + "references": [ + { + "repo": "secrets-engine", + "path": "src/secrets_engine/__init__.py", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:6536fc21988755467f71a91e79c90bf984f1ee16e696566a03cc4c1240920573", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "src/secrets_engine/errors.py", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:4cd0bfdc009556b35d4359a37d797a6bacf644ca4969d46e33f60435b4bafac8", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "src/secrets_engine/safe_paths.py", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:90e31769f66f34910b562d64bebcaa3e8cea23e79678eff4d625efcefc0d4f73", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "src/secrets_engine/service_auth.py", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "src/secrets_engine/engine_auth.py", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:e0479cf6d8c13233cf99dc39491143478c021597941ae8b6ba0e37957a42f330", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "docs/keycape-recovery-selector.json", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:02077af3c18b24f5e4a85992a3d4d0df51a8e458de1e48b48f7eb5f482257012", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "docs/keycape-isolated-recovery-selector.md", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:ed23b0df3d4b49e4b76df795455153c805971ccd37886836c960f550709e0739", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "docs/keycape-openbao-consumer-custody.md", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:73efdd549f3feaccc53eefd6f1b95c8b9eb7091648f94ad80978ba5381be9124", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "docs/evidence/2026-10-10-keycape-openbao-probe.json", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:bf0b5df9f030499d96770f1d9fb2091a8d37da71422a6d456be7200d3a1a1782", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:b3cab4e8d14b3b03179fc72a6e9ce1c0ee840cfa3247c022c1324d60378bde5b", + "matches_head": true + }, + { + "repo": "secrets-engine", + "path": "tools/check-keycape-recovery-selector.py", + "revision": "c59bbf568c0be8a72ac593508f48c9b1f61ec4c1", + "sha256": "sha256:e3e5e3ee743a212247701d6cb40fcf5b7f19349126fa9b32cbef9a3cc9459150", + "matches_head": true + }, + { + "repo": "net-kingdom", + "path": "tools/bootstrap-recovery/isolated-keycape.py", + "revision": "c52ca7f405416d437951844c0640b86e677c3523", + "sha256": "sha256:86f37add7221e4c73e3f9d878c09a3cc81061627cbf024ed6887d973d929b718", + "matches_head": true + }, + { + "repo": "net-kingdom", + "path": "tools/bootstrap-recovery/isolated-keycape-session.py", + "revision": "c52ca7f405416d437951844c0640b86e677c3523", + "sha256": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22", + "matches_head": true + }, + { + "repo": "net-kingdom", + "path": "tools/bootstrap-recovery/run-isolated-keycape.sh", + "revision": "c52ca7f405416d437951844c0640b86e677c3523", + "sha256": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668", + "matches_head": true + }, + { + "repo": "net-kingdom", + "path": "tools/bootstrap-recovery/prepare-isolated-keycape.py", + "revision": "c52ca7f405416d437951844c0640b86e677c3523", + "sha256": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288", + "matches_head": true + }, + { + "repo": "net-kingdom", + "path": "tools/bootstrap-recovery/identity-backup-profile.json", + "revision": "c52ca7f405416d437951844c0640b86e677c3523", + "sha256": "sha256:40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1", + "matches_head": true + }, + { + "repo": "key-cape", + "path": "src/internal/authclient/verify.go", + "revision": "258d899a78fd4673b92004d1b10717b4de03d26f", + "sha256": "sha256:804735be927ba7d0228ac378056730040d3ccf2ed020b4da0714085bae972f25", + "matches_head": true + }, + { + "repo": "key-cape", + "path": "src/internal/authclient/cli.go", + "revision": "258d899a78fd4673b92004d1b10717b4de03d26f", + "sha256": "sha256:376a8e0d1d60825a8b3ef21d371e3fc353c0a34e461712a6899e264278edbe7f", + "matches_head": true + }, + { + "repo": "key-cape", + "path": "src/internal/authclient/client.go", + "revision": "258d899a78fd4673b92004d1b10717b4de03d26f", + "sha256": "sha256:61fb150349c5d3714450a90834c59af9a579a8a649c4149297a402cfcf6e39c5", + "matches_head": true + }, + { + "repo": "railiance-platform", + "path": "docs/contracts/secrets-engine-openbao-jwt-login.yaml", + "revision": "08c1f036f2750097750f435efd76e8f6ee6df393", + "sha256": "sha256:a98663c316611c4b2773f197c734befb8446e8efe20f563da018c358c3b11d77", + "matches_head": true + }, + { + "repo": "railiance-platform", + "path": "credential-change-requests/CCR-2026-0038-secrets-engine-service-jwt-login.yaml", + "revision": "08c1f036f2750097750f435efd76e8f6ee6df393", + "sha256": "sha256:65b2cfb772623da59c351033ab4eb2ec7adfbd109778e787c7fe1b0e185a3bc0", + "matches_head": true + }, + { + "repo": "railiance-platform", + "path": "openbao/auth/secrets-engine-jwt-role.json", + "revision": "08c1f036f2750097750f435efd76e8f6ee6df393", + "sha256": "sha256:529dc23193be42cbe0d4440f902a64ca7c9158da158c9da13396aa0030480f40", + "matches_head": true + }, + { + "repo": "railiance-platform", + "path": "openbao/policies/secrets-engine-login-self.hcl", + "revision": "08c1f036f2750097750f435efd76e8f6ee6df393", + "sha256": "sha256:d1a2969dd2949c822972e9b32243650567ac123a669f3fc609e9bfea977bb4eb", + "matches_head": true + } + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/test_consumer_contract_observer.py b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/test_consumer_contract_observer.py new file mode 100644 index 00000000..c7166e46 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/consumer-outage-r1/test_consumer_contract_observer.py @@ -0,0 +1,95 @@ +"""Synthetic observer checks: no cryptographic/native/authentication claim.""" +import base64 +from datetime import datetime, timezone +import hashlib +import json +from pathlib import Path +import subprocess +import tempfile +import unittest + +HERE = Path(__file__).resolve().parent +OWNER = Path('/home/worsch/secrets-engine') +MANIFEST = HERE / 'consumer-source-manifest.json' + + +def segment(value): + return base64.urlsafe_b64encode(json.dumps(value).encode()).decode().rstrip('=') + + +def envelope(**overrides): + now = int(datetime.now(timezone.utc).timestamp()) + claims = {'iss': 'https://kc.coulomb.social', 'sub': 'service:secrets-engine', + 'aud': 'secrets-engine-openbao', 'principal_type': 'service', + 'tenant': 'tenant:platform', 'groups': [], 'roles': ['secrets-engine'], + 'scope': 'openbao:login', 'iat': now, 'exp': now + 900, + 'assurance': {'level': 'aal1', 'methods': ['client_secret'], + 'mfa': False, 'source': 'key-cape'}} + claims.update(overrides) + token = segment({'alg': 'RS256', 'typ': 'JWT'}) + '.' + segment(claims) + '.synthetic_no_signature' + return {'access_token': token, 'token_type': 'Bearer', 'expires_in': 900} + + +def run(raw, *, owner=OWNER, digest=None): + expected = digest or 'sha256:' + hashlib.sha256(MANIFEST.read_bytes()).hexdigest() + result = subprocess.run(['python3', '-I', '-B', str(HERE/'consumer_contract_observer.py'), + '--source-root', str(owner), '--source-manifest', str(MANIFEST), + '--source-manifest-sha256', expected], input=raw, + capture_output=True, timeout=10) + return result, json.loads(result.stdout) + + +class ObserverTests(unittest.TestCase): + def test_synthetic_contract_is_explicitly_noncryptographic(self): + value = envelope() + result, report = run(json.dumps(value).encode()) + self.assertEqual(result.returncode, 0) + self.assertTrue(all(report['claim_parser_refusals'].values())) + for field in ('signature_verified_here', 'native_input_provenance_verified_here', + 'client_secret_source_exercised', 'openbao_login_performed', + 'protected_backend_operation_performed', + 'downstream_consumer_reconnect_proven', 'network_contacted'): + self.assertIs(report[field], False) + self.assertNotIn(value['access_token'].encode(), result.stdout + result.stderr) + self.assertEqual(result.stderr, b'') + + def test_wrong_actual_input_claims_refuse_without_private_diagnostics(self): + for change in ({'aud': 'wrong'}, {'roles': ['admin']}, {'scope': 'openbao:admin'}): + with self.subTest(change=change): + value = envelope(**change) + result, report = run(json.dumps(value).encode()) + self.assertEqual(result.returncode, 1) + self.assertEqual(report['status'], 'consumer_claim_preflight_refused') + self.assertNotIn(value['access_token'].encode(), result.stdout + result.stderr) + self.assertEqual(result.stderr, b'') + + def test_forbidden_extra_token_and_duplicate_json_refuse(self): + value = envelope(); value['refresh_token'] = 'synthetic-forbidden' + for raw in (json.dumps(value).encode(), b'{"access_token":"a","access_token":"b"}'): + result, report = run(raw) + self.assertEqual(result.returncode, 1) + self.assertTrue(report['private_diagnostics_suppressed']) + self.assertEqual(result.stderr, b'') + + def test_oversize_and_malformed_refuse(self): + for raw in (b'x' * 65537, b'not json'): + result, report = run(raw) + self.assertEqual(result.returncode, 1) + self.assertTrue(report['private_diagnostics_suppressed']) + + def test_source_manifest_and_source_body_drift_refuse(self): + result, _ = run(json.dumps(envelope()).encode(), digest='sha256:' + '0'*64) + self.assertEqual(result.returncode, 1) + with tempfile.TemporaryDirectory(prefix='se-consumer-source-drift-') as directory: + target = Path(directory) + for name in json.loads(MANIFEST.read_text()): + p = target/name; p.parent.mkdir(parents=True, exist_ok=True) + p.write_bytes((OWNER/name).read_bytes()) + p = target/'src/secrets_engine/service_auth.py'; p.write_bytes(p.read_bytes()+b'\n# drift\n') + result, report = run(json.dumps(envelope()).encode(), owner=target) + self.assertEqual(result.returncode, 1) + self.assertEqual(report['status'], 'consumer_claim_preflight_refused') + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-17/scanner-response-r1/source-references.json b/docs/changes/netkingdom-target-2026-10-17/scanner-response-r1/source-references.json new file mode 100644 index 00000000..d5920115 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/scanner-response-r1/source-references.json @@ -0,0 +1,80 @@ +{ + "status": "public_source_and_sanitized_prior_evidence_only", + "observed_at": "2026-10-17T02:13:49.246721+00:00", + "references": [ + { + "repo": "repo-manager", + "path": "AGENTS.md", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:b461f40a102fa25593807f9cd4213a411f3201e458df43dbcfde41b8251e1477" + }, + { + "repo": "repo-manager", + "path": "INTENT.md", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:62afa83ecb1fd733abce07489d48d0d7c7e14ccf9def9f561c118cf8ec5409b2" + }, + { + "repo": "repo-manager", + "path": "SCOPE.md", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:bca5806e11f04e4550ce5c147d4d98e6c0014fed1f17fa3f134e5b193adec43c" + }, + { + "repo": "repo-manager", + "path": "history/2026-08-09-genesis.md", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:dbd4111a25d42fc3e1b1bcf921416fb9506c7f3d8f54e8f0dc82f5bbaeb4217d" + }, + { + "repo": "repo-manager", + "path": "docs/secret-detection.md", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:1158dd4180b8c08717112dcbe6fa8f09ac0e2a44dd4d7d9bac2edf629d1766dc" + }, + { + "repo": "repo-manager", + "path": "src/repo_manager/secret_scan.py", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:e709c658f779534fcab093207b0a78e49d54fc584c515fdf7ac535ce75f6c400" + }, + { + "repo": "repo-manager", + "path": "src/repo_manager/cli.py", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:1120e8cc50341a2b02fd7d940a79f75a26ec6230c00dd1da76acaf0c6e494014" + }, + { + "repo": "repo-manager", + "path": "tests/test_secret_scan.py", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:827ab6382bb63fed8d2e82ebb45ac7a28789abeb397a831f340e5f19d7cc7876" + }, + { + "repo": "repo-manager", + "path": "workplans/RMGR-WP-0021-secret-detection-controls.md", + "head": "3bcb1e1cf3e58b263f690534f7f28024d75cdbf6", + "sha256": "sha256:966d15ccdabc8edf98bc58f9422ea190566d05fe4208f4749f59a9731f4e108d" + }, + { + "repo": "the-custodian", + "path": "docs/changes/netkingdom-target-2026-10-17/scanner-unavailable-r1/namespace-hook-results.json", + "sha256": "sha256:5aaef79c0be4ae3cf721482784b255dde795559363bfb671e64ba6252fec59bc" + }, + { + "repo": "the-custodian", + "path": "docs/changes/netkingdom-target-2026-10-17/scanner-unavailable-r1/manifest.json", + "sha256": "sha256:fb678cd82c41dd76ad0f056e1c3513e5339bc2ae82daf3cd17c9c85a43679665" + }, + { + "repo": "the-custodian", + "path": "docs/changes/netkingdom-target-2026-10-17/native-gates-r1/native-gate-audit.json", + "sha256": "sha256:ab803e795af61fd027f5930d127592316133de38b68a8fe61b89a1f9803a9ff2" + }, + { + "repo": "the-custodian", + "path": "workplans/CUST-WP-0073-agent-credential-separation.md", + "sha256": "sha256:8cbf27819b4d8c689c6afadf0dd3eabe8a8cd4b399d20f05a5e7fe8749b9b43a" + } + ] +}