diff --git a/docs/evidence/2026-10-11-keycape-client-custody-native-preparation.json b/docs/evidence/2026-10-11-keycape-client-custody-native-preparation.json new file mode 100644 index 0000000..5b2e29e --- /dev/null +++ b/docs/evidence/2026-10-11-keycape-client-custody-native-preparation.json @@ -0,0 +1,65 @@ +{ + "at": "2026-10-11", + "facts": [ + "Neither exact custody lane policy grants sys/capabilities-self; no default policy. Existing survey sys/capabilities queries lane tokens in memory.", + "Existing survey source receipt display_name is token-claude-MASON-WP-0009-T04; required T05 grant is token-claude-MASON-WP-0009-T05.", + "ESO lifecycle uses separately issued ServiceAccount TokenRequest JWT, not controller or Secret token.", + "Nonmember CLI failure never accepted; independently trusted identity and same-flow owner-correlated groups refusal still required.", + "Previously accepted service JWT positive/login/revoke proofs retained; new preparation is distinct custody-reader evidence." + ], + "founder_native_disposition": "not-yet-recorded", + "native_result": "not-run", + "owner_source_review": "not-yet-performed", + "preparation_result": "corrected-source-preparation", + "prepared_by": "custodian-codex delegated preparation for ops-mason; no owner authority assumed", + "previous_manifest_sha256": "e5d4781c6280c3bc8a3930c63cfb563f59752465720d6c2c3e95eb0551f8e949", + "prior_outcome": "independent source changes_requested; not native success", + "proposal_id": "MASON-WP-0009-T05-native-capability-proof-20261011", + "refinements": [ + "endpoint-specific HTTP status/content/body/object/error handling; revoke only204empty", + "nonempty typed bounded native accessors; recovered-token cleanup", + "positive-integer TTL", + "incrementally bounded subprocess capture; no unbounded PIPE claim", + "approved digest guard before token/receipt/native IO" + ], + "remaining_gates": [ + "ops-mason owner formal exact-source review", + "supervisor exact source disposition and founder scoped native attendance", + "operator native capability matrix with cleanup", + "ESO native capability matrix with cleanup; platform TokenRequest approval", + "key-cape validated same-attempt human/nonmember/aal2 evidence and platform correlated bound-group refusal", + "ops-mason native owner acceptance and ops-warden pointer activation" + ], + "routing": { + "initial_default_memory_append_failed_outside_sandbox": true, + "memory_disabled_read_succeeded": true, + "mode": "pointer", + "route": "openbao-structure-survey", + "warden_executes": false + }, + "safety": { + "commits": 0, + "credential_reads": 0, + "credential_requests": 0, + "foreign_owner_reviews": 0, + "native_actions": 0, + "owner_checkout_writes": 0, + "policy_widening": 0, + "pushes": 0, + "secret_gets": 0 + }, + "schema": "mason-native-preparation/v1", + "source_scope": "existing MASON-WP-0009-T05 only; source preparation, no live acceptance; unrelated WORK-RECORDS.md and ADHOC-2026-10-11.md preserved", + "source_sha256": { + "docs/keycape-client-custody-attended-verification.md": "554d20f722c13742eb86460991df420035cec11a469fef09e09567a64e07b247", + "scripts/verify-keycape-client-custody.py": "f7685646dca48db5874174940c1826db186e1ed73e167814d8c0f37af3ad619c", + "tests/test_keycape_client_custody_verifier.py": "ad981635eb97f48ca2e092cdbcb6e47007d89eb272a85578485dbb299bb7c479", + "workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md": "9bea9cb4f2619c42a6cf3a7cf1fb14edc8539d702c437939b397ad9ebe1ce7d6" + }, + "task": "MASON-WP-0009-T05", + "verification": { + "command": "python3 -m unittest discover -s /tmp/mason-jwt-capability-next-stage-v2/ops-mason/tests -q", + "result": "passed-offline", + "tests": 29 + } +} diff --git a/docs/keycape-client-custody-attended-verification.md b/docs/keycape-client-custody-attended-verification.md new file mode 100644 index 0000000..55d5fe8 --- /dev/null +++ b/docs/keycape-client-custody-attended-verification.md @@ -0,0 +1,168 @@ +# MASON-WP-0009-T05: founder-run native capability proof + +Preparation only. No credential was read, native session started, TokenRequest +issued, policy widened, KV GET performed, or route activated during preparation. +The existing T05 task stays in progress. Formal ops-mason owner source review, +founder attendance approval for the exact staged digest, and live native receipts +remain necessary. An `--approval-reference` argument records the founder's prior +disposition; it cannot approve itself or establish runtime isolation. + +## Exact reuse and authority + +The existing verifier `scripts/verify-fluid-telegram-lane.sh` demonstrates attended +OIDC and value-free capabilities, but accepts every failed login as a nonmember +refusal. It is not reused as proof. secrets-engine's +`OpenBaoOidcClientSecretSource` demonstrates `bao login -no-store -format=json`, +but its next operation reads CLIENT_SECRET, so that provider is not invoked here. +Existing `scripts/bao-session.sh grant MASON-WP-0009-T05` provides query authority: +45m `ops-mason-survey`, founder minting, explicit data denies, no policy/role/token +write. The grant may also carry `default`; all other token/identity policies are +refused. No new policy or credential owner is proposed. + +Source inspection found neither CCR-0033's ESO reader nor CCR-0034's operator +reader grants `sys/capabilities-self` (both omit default). Calling capabilities +under their own tokens would prove endpoint denial, not the required path matrix. +The verifier uses the survey grant's existing `sys/capabilities` to query the +native lane token in memory. The lane token never enters argv, environment, +receipts or a token helper. All HTTP calls use a fixed loopback endpoint, reject +redirects/proxies and allow only login, self lookup, capability query and self +revocation endpoints. No secret or metadata GET exists in the HTTP interface. + +Credential routing was read first: `WARDEN_MEMORY=0 warden route show +openbao-structure-survey --json`. That entry routes to ops-mason and is not a +credential vending surface. The initial default route lookup printed routing +metadata but failed to append its memory event outside this sandbox; no escalation +was requested. There is no active operator route in current ops-warden source; +the previously reported draft proposal must be accepted there separately. + +## Concrete attended proposal + +Proposal ID: `MASON-WP-0009-T05-native-capability-proof-20261011`. +Target: existing OpenBao on the railiance01 loopback tunnel and, for ESO only, +railiance01's `external-secrets/external-secrets` ServiceAccount TokenRequest. +Expected result: two exact native read-only matrices, confirmed lane/query +self-revokes, and a separately accepted nonmember refusal. Estimated software +cost EUR 0; bounded founder browser/session time around 10–20 minutes. Risk: +short-lived read-capable session tokens exist in founder process memory, and ESO +TokenRequest briefly mints a 10m JWT for the existing ServiceAccount. No secret +value is requested. No policy or deployment changes occur. Actual runtime +isolation remains unverified; exact source review and founder attendance are +procedural gates, not an autopilot grant. + +After ops-mason accepts the source and the supervisor records the exact digest +and scoped founder go, the founder runs from their terminal. For each mode, +obtain a fresh existing survey grant through the established session helper. +Do not hand an agent an operator token or inspect either session file in chat. +Use a new output path each time and a private browser session for the chosen human. +The verifier requires TTY attendance, rejects ambient BAO_TOKEN/VAULT_TOKEN and +namespaces, and refuses a source digest different from the reviewed digest. + +```bash +python3 scripts/verify-keycape-client-custody.py operator \ + --attended --approval-reference '' \ + --approved-source-sha256 '' \ + --survey-token-fd 3 --receipt '' \ + 3< "$HOME/.claude-bao-token" +``` + +Repeat with `eso` and then `nonmember`, fresh grant and receipt for each. Opening +the existing grant FD is performed only in the founder terminal. The verifier +closes it and self-revokes the query token. Existing `bao-session.sh revoke` is +then used by the founder to remove the stale grant file; its printed success is +not accepted as revocation evidence because that helper suppresses revoke errors. +The verifier's native revoke receipt is the relevant evidence. + +For ESO the runner first captures node metadata via fixed `ssh -oBatchMode=yes +railiance01 'kubectl get nodes -o json'` and refuses any estate other than one node +with address `92.205.62.239`. It then captures only the exact audience `openbao`, +10m ServiceAccount TokenRequest JWT in memory and logs in at +`auth/kubernetes/login` as `external-secrets-keycape-secrets-engine-openbao`. +No Secret object, pod or controller token is read. The independently minted lane +token is revoked; the running ESO controller's token is never touched. If the +node metadata does not expose the documented IP, stop for platform owner source +review of the estate pin; do not loosen the check at runtime. + +## Receipt acceptance and remaining proof gates + +Schema `mason-client-custody-native/v1` includes exact script digest, proposal +reference, mode, timestamp, accessor SHA256, matrix and cleanup booleans. Files +are reserved with O_EXCL/O_NOFOLLOW and mode 0600. Raw CLI/HTTP errors and auth +responses are never copied into receipts. Capability queries must return every +named path; missing rows, unexpected default/identity policies, wrong role, +wrong ESO namespace/name, TTL over 5m/15m, any data write or parent/sibling/metadata +capability fail closed. `capability_matrix_observed` means observations passed; +`owner_acceptance: pending` is retained even when exit status is zero. + +Operator and ESO acceptance requires ops-mason's actual owner review against the +existing CCRs/source receipts, native role/policy/TTL observations, exact read-only +matrix and **both** lane/query revocation confirmations. Operator role/claims +remain grounded in the existing approved CCR-0034 binding, not a CLI role label +chosen by the human. ESO metadata must show the fixed service account and role. + +Nonmember never auto-passes. A semantic groups mismatch from the OIDC callback is +only `group_refusal_candidate`; transport failure, cancellation, HTTP 403 alone, +wrong issuer, malformed JSON, or MFA failure is `unproved_login_failure`. An +unexpected successful nonmember login is revoked and fails the run. + +Before ops-mason accepts check 3, key-cape supplies a value-free owner-authenticated +identity record proving the **same attempted subject** is human, outside +net-kingdom-admins and aal2, with validated issuer/signature/expiry; platform supplies +a correlated native OpenBao callback refusal specifically for groups. The records +must bind the same attempt using an owner-verified request/flow correlation digest, +not merely nearby timestamps, a user-selected `nonmember` mode or decoded JWT +claims. Include owner record ids/digests, observed time, subject fingerprint, +issuer validation boolean, `group_member=false`, `aal2=true`, callback endpoint, +refusal class and no token issued. The identity/flow digest must be produced and +verified inside the owners' attended/audit surface; the runner does not possess +the upstream OIDC JWT and cannot manufacture this evidence. If owners cannot +produce this correlation, check 3 stays unproved. Never publish raw JWTs, callback +URLs/codes/states, cookies, session ids or authentication errors. + +This is separate from KEY-WP-0039 human/coding-agent JWT boundary evidence and +secrets-engine's service identity denial matrix. Those do not prove this reader's +nonmember binding. The existing successful service-jwt exchange and self-revokes +remain accepted prior evidence; do not discard or regenerate them merely to +complete these different gates. ops-warden may activate the pointer only after +all T05 required owner-native receipts are actually accepted. + +## Cleanup and failure recovery + +On every obtained lane token, including policy mismatch/unexpected nonmember +success, attempt lane revoke-self, then query revoke-self. A failed revoke changes +result to unproved. A query token that fails its identity check is not trusted for +further calls; record no cleanup confirmation and use the established founder +revocation path. Callback timeout/malformed native auth may leave a token whose +handle was not recovered: no cleanup success is claimed; the role's 5m/15m TTL +bounds it and the platform owner decides recovery. No policy is widened to retry. + +The Kubernetes TokenRequest JWT cannot be individually revoked by this procedure; +it expires after 10m and is never persisted. Do not delete the shared ServiceAccount +or revoke the controller's token as cleanup. No KV rollback exists because no KV +request occurred. Session cleanup is the rollback; any unconfirmed cleanup is +explicit open evidence and blocks acceptance until owner resolution. + + +## Corrected source revision — 2026-10-11 + +The prior immutable stage is retained at `/tmp/mason-jwt-capability-next-stage` +(manifest e5d4781c6280c3bc8a3930c63cfb563f59752465720d6c2c3e95eb0551f8e949). +Independent source review requested changes: its HTTP202/text response could +falsely confirm revocation and missing native accessors could hash the empty +string. This revised source rejects every unsupported status/content/body/error +shape. Lookup/capability/Kubernetes-login require HTTP200 application/json and +endpoint-specific object data/auth, no `errors` member, with bounded headers/body. +Revoke succeeds only on HTTP204 with an empty body. Error envelopes never imply +cleanup; a recovered token from a mixed login envelope is retained solely for +finally cleanup. Nonempty bounded typed actual accessors are required before +fingerprinting; empty/malformed query accessors still revoke a scope-validated +query token, and empty/malformed lane accessors still revoke recovered lane tokens. + +TTL is a positive integer within the declared bound, with booleans refused. +CLI stdout/stderr are captured while running with a 64KiB limit per stream; +excess or timeout kills the local process group and leaves native outcome +unproved. No unlimited capture is described as bounded. The exact approved +source digest is checked before receipt/token/native IO. 29 offline tests cover +these corrections, actual relative redirect refusal with hostile proxy settings, +synthetic oversized subprocess output, valid native204 cleanup versus bad202, +missing-accessor cleanup, and mixed-error session recovery. Source corrections +are not a new native success, owner acceptance, founder go or adoption proof. diff --git a/docs/reviews/2026-10-11-keycape-client-custody-independent-source-review-refined.json b/docs/reviews/2026-10-11-keycape-client-custody-independent-source-review-refined.json new file mode 100644 index 0000000..b943abd --- /dev/null +++ b/docs/reviews/2026-10-11-keycape-client-custody-independent-source-review-refined.json @@ -0,0 +1,81 @@ +{ + "reviewer": "ops-mason-owner (agent ops-mason, codex)", + "decision": "approved", + "comment": "INTENT Mission four-phase source-grounded construction consistency and Responsibility boundary permit this capability-only verification preparation with no secret-value retrieval or access widening. Exact reviewed revision2 manifest SHA256 4dbf808e2d79c2e6600a163e147db7d15b075e59a8edf8cd236f31fdce9b5155, script f7685646dca48db5874174940c1826db186e1ed73e167814d8c0f37af3ad619c. Own completed work item ADHOC-2026-10-11-T03 refined preparation recorded before this formal entry; first changes-requested review remains historical. Both blocking source defects corrected and independently verified. Approval is source review only: no founder authorization, runtime isolation attestation, native acceptance, route activation or whole-plan closure.", + "own_task": "ADHOC-2026-10-11-T03", + "own_completed_record_sha256": "7fadf4475ff765999b71659495b786737a9641c8ad875afc97bd264a8c9f0aec", + "reviewed_manifest_sha256": "4dbf808e2d79c2e6600a163e147db7d15b075e59a8edf8cd236f31fdce9b5155", + "reviewed_files": [ + { + "original_sha256": null, + "path": "scripts/verify-keycape-client-custody.py", + "repo": "ops-mason", + "staged_path": "/tmp/mason-jwt-capability-next-stage-v2/ops-mason/scripts/verify-keycape-client-custody.py", + "staged_sha256": "f7685646dca48db5874174940c1826db186e1ed73e167814d8c0f37af3ad619c" + }, + { + "original_sha256": null, + "path": "tests/test_keycape_client_custody_verifier.py", + "repo": "ops-mason", + "staged_path": "/tmp/mason-jwt-capability-next-stage-v2/ops-mason/tests/test_keycape_client_custody_verifier.py", + "staged_sha256": "ad981635eb97f48ca2e092cdbcb6e47007d89eb272a85578485dbb299bb7c479" + }, + { + "original_sha256": null, + "path": "docs/keycape-client-custody-attended-verification.md", + "repo": "ops-mason", + "staged_path": "/tmp/mason-jwt-capability-next-stage-v2/ops-mason/docs/keycape-client-custody-attended-verification.md", + "staged_sha256": "554d20f722c13742eb86460991df420035cec11a469fef09e09567a64e07b247" + }, + { + "original_sha256": "857458214d94f7852f841bad25887625ee4d90575a79b5153465c661a2e0089f", + "path": "workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md", + "repo": "ops-mason", + "staged_path": "/tmp/mason-jwt-capability-next-stage-v2/ops-mason/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md", + "staged_sha256": "9bea9cb4f2619c42a6cf3a7cf1fb14edc8539d702c437939b397ad9ebe1ce7d6" + }, + { + "original_sha256": null, + "path": "docs/evidence/2026-10-11-keycape-client-custody-native-preparation.json", + "repo": "ops-mason", + "staged_path": "/tmp/mason-jwt-capability-next-stage-v2/ops-mason/docs/evidence/2026-10-11-keycape-client-custody-native-preparation.json", + "staged_sha256": "8c6b15ce589f322d7f01e8ce6415f3726f34cb301b29b2436618ce4806a12494" + } + ], + "prior_review": "docs/reviews/2026-10-11-keycape-client-custody-independent-source-review.json", + "prior_outcome_preserved": "changes_requested on revision1", + "corrections_verified": [ + "lookup/login/capability exact200JSON/endpoint-shape/errors absence", + "revoke204emptybody only; failures leave confirmation false and result unproved", + "typed nonempty bounded native accessors; safe recovered-token cleanup on missing correlation handle", + "mixed auth+errors preserves recovered token solely for cleanup, never acceptance", + "typed exact positiveTTLlimits; expired/unbounded refused", + "early source digest mismatch before credentialFD or nativeAPI access", + "HTTP preparse response bound; native subprocess incremental per-stream64KiB capture with process cleanup", + "fixed origin, proxy disabled, all redirectfollowing refused" + ], + "tests": { + "maker_suite": "29 passed independently", + "additional_independent_suite": "4 passed", + "additional_tests_sha256": "ce4dd7ebd2f88f031de9cf8aba89f6c761f80f67d56c56adc3717c5d3a902176", + "additional_counterexamples": [ + "actual cross-origin302target receives no request/token", + "absent revoke response and cleanup failure remain unproved", + "expiredTTL and missing accessor refuse and attempt recovered-token cleanup", + "oversized native stderr refused before full capture" + ] + }, + "remaining_live_gates": [ + "founder scoped exact-source/native attendance go", + "operator and ESO actual native matrices+metadata+both revokes accepted by owner", + "issuer-authenticated nonmember human/aal2 plus same-attempt platform groups-refusal correlation", + "unknown/native timeout or failed cleanup owner resolution", + "ops-warden pointer acceptance and activation" + ], + "native_actions": false, + "credential_requests": false, + "secret_values_accessed": false, + "owner_checkout_writes": false, + "approval_or_admission_fields_changed": false, + "whole_plan_closed": false +} diff --git a/scripts/verify-keycape-client-custody.py b/scripts/verify-keycape-client-custody.py new file mode 100644 index 0000000..472a74e --- /dev/null +++ b/scripts/verify-keycape-client-custody.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +"""MASON-WP-0009-T05 attended, value-free lane verifier; no KV GETs. + +The existing operator/ESO policies do NOT grant capabilities-self. An existing +read-only ops-mason-survey grant queries their tokens via sys/capabilities. +This file prepares a founder-run procedure, not a new authorization surface. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess +import selectors +import signal +import sys +import time +import urllib.request +import urllib.error + +DATA = 'platform/data/workloads/keycape/secrets-engine-openbao' +OP_ROLE = 'keycape-secrets-engine-openbao-operator-workload-kv-read' +ESO_ROLE = 'external-secrets-keycape-secrets-engine-openbao' +POLICY = 'workload-kv-read-keycape-secrets-engine-openbao-' +PATHS = [DATA, DATA.replace('/data/', '/metadata/'), + 'platform/data/workloads/keycape', 'platform/data/workloads/keycape/', + DATA + '-other', 'platform/data/workloads/keycape/other', + 'platform/data/workloads/keycape/*', 'sys/policies/acl/ops-mason-survey', + 'auth/token/create', 'auth/token/revoke-self'] +BASE = 'http://127.0.0.1:18200' + +class Refused(Exception): + pass + +class RecoveredLoginRefused(Refused): + """Retain only an already recovered session for finally cleanup, never proof.""" + def __init__(self, code, auth): + super().__init__(code) + token = auth.get('client_token') if isinstance(auth, dict) else None + self.token = token if isinstance(token, str) and 0 < len(token) <= 4096 and not any(c.isspace() for c in token) else '' + +class Transport(Exception): + pass + +class Api: + """Hard-coded endpoint allowlist prevents secret GETs, role/policy writes.""" + def __init__(self): + self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), + NoRedirect()) + def call(self, endpoint, token='', body=None): + allowed = {'auth/token/lookup-self', 'auth/token/revoke-self', + 'sys/capabilities', 'sys/capabilities-self', + 'auth/kubernetes/login'} + if endpoint not in allowed: + raise Refused('endpoint_not_allowlisted') + req = urllib.request.Request(BASE + '/v1/' + endpoint, + data=None if body is None else json.dumps(body).encode(), + headers={'Content-Type': 'application/json', 'X-Vault-Token': token}, + method='GET' if endpoint == 'auth/token/lookup-self' else 'POST') + try: + with self.opener.open(req, timeout=20) as response: + if endpoint == 'auth/token/revoke-self': + # OpenBao's native self-revoke success is 204 with no body. + if response.status != 204 or response.read(1): + raise Refused('unconfirmed_revoke_response') + return {} + if response.status != 200 or response.headers.get_content_type() != 'application/json': + raise Refused('invalid_status_or_content_type') + length = response.headers.get('Content-Length') + if length is not None and (not length.isdecimal() or int(length) > 65536): + raise Refused('oversized_or_invalid_content_length') + raw = response.read(65537) + if len(raw) > 65536: + raise Refused('oversized_response') + result = json.loads(raw) + if not isinstance(result, dict): + raise Refused('invalid_response_envelope') + if 'errors' in result: + if endpoint == 'auth/kubernetes/login': + raise RecoveredLoginRefused('native_error_envelope', result.get('auth')) + raise Refused('native_error_envelope') + key = 'auth' if endpoint == 'auth/kubernetes/login' else 'data' + if not isinstance(result.get(key), dict): + raise Refused('invalid_endpoint_response_shape') + return result + except urllib.error.HTTPError as error: + error.close() # never read/print server errors + raise Refused('http_' + str(error.code)) from None + except (OSError, ValueError, urllib.error.URLError): + raise Transport('transport_or_invalid_response') from None + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, *args, **kwargs): + raise Refused('redirect_refused') + +def digest(value): + return hashlib.sha256(value.encode()).hexdigest() + +def accessor_digest(value): + if not isinstance(value, str) or not 1 <= len(value) <= 256 or any(c.isspace() for c in value): + raise Refused('missing_or_invalid_native_accessor') + return digest(value) + +def checked_caps(response, expected): + data = response.get('data', response) + # Only batch-shaped named results accepted, never substitute a default. + if not isinstance(data, dict) or not all(path in data for path in expected): + raise Refused('incomplete_capability_matrix') + result = {} + for path, caps in expected.items(): + actual = data[path] + if not isinstance(actual, list) or sorted(actual) != sorted(caps): + raise Refused('capability_mismatch') + result[path] = actual + return result + +def survey(api, token): + info = api.call('auth/token/lookup-self', token)['data'] + if info.get('identity_policies'): + raise Refused('query_identity_policy_additions') + policies = set(info.get('policies', [])) + if policies not in ({'ops-mason-survey'}, {'ops-mason-survey', 'default'}): + raise Refused('query_policy_mismatch') + if info.get('display_name') != 'token-claude-MASON-WP-0009-T05' or (type(info.get('ttl')) is not int or not 0 < info['ttl'] <= 2700): + raise Refused('query_identity_or_ttl_mismatch') + expected = {DATA: ['deny'], 'sys/policies/acl/ops-mason-survey': ['deny'], + 'auth/token/create': ['deny'], 'sys/capabilities': ['update']} + checked_caps(api.call('sys/capabilities-self', token, {'paths': list(expected)}), expected) + return info + +def safe_env(home): + # Never inherit credential variables, token helpers, proxies, CLI plugins. + return {'PATH': '/home/worsch/.local/bin:/usr/local/bin:/usr/bin:/bin', 'HOME': home, + 'BAO_ADDR': BASE, 'VAULT_ADDR': BASE, + 'DISPLAY': os.environ.get('DISPLAY', ''), + 'WSL_INTEROP': os.environ.get('WSL_INTEROP', ''), + 'WSL_DISTRO_NAME': os.environ.get('WSL_DISTRO_NAME', '')} + +def run(argv, env): + # Capture each stream with a real memory bound while the child runs; never + # retain an unbounded PIPE response and only check its length afterwards. + process = None + try: + process = subprocess.Popen(argv, env=env, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True) + buffers = {process.stdout: bytearray(), process.stderr: bytearray()} + deadline = time.monotonic() + 300 + with selectors.DefaultSelector() as selector: + for stream in buffers: + selector.register(stream, selectors.EVENT_READ) + while selector.get_map(): + remaining = deadline - time.monotonic() + if remaining <= 0: + raise Transport('native_transport_or_timeout') + for key, _ in selector.select(min(remaining, 1)): + chunk = os.read(key.fileobj.fileno(), 8192) + if not chunk: + selector.unregister(key.fileobj) + continue + target = buffers[key.fileobj] + if len(target) + len(chunk) > 65536: + raise Refused('oversized_native_response') + target.extend(chunk) + remaining = deadline - time.monotonic() + if remaining <= 0: + raise Transport('native_transport_or_timeout') + code = process.wait(timeout=remaining) + return code, bytes(buffers[process.stdout]), bytes(buffers[process.stderr]) + except (OSError, subprocess.TimeoutExpired): + raise Transport('native_transport_or_timeout') from None + finally: + if process is not None: + if process.poll() is None: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait() + process.stdout.close() + process.stderr.close() + +def oidc(env, runner=run): + # -no-store is mandatory; output/errors stay inside the founder's process. + code, raw, err = runner(['/home/worsch/.local/bin/bao', 'login', '-no-store', '-format=json', + '-method=oidc', '-path=netkingdom', 'role=' + OP_ROLE], env) + if code: + # A candidate refusal still needs independent issuer + correlated + # OpenBao owner evidence. HTTP 403, cancellation or timeout never pass. + claim_refusal = b'claim "groups" does not match' in err or b'claim "groups" not found' in err + return None, 'group_refusal_candidate' if claim_refusal else 'unproved_login_failure' + try: + result = json.loads(raw) + if not isinstance(result, dict) or not isinstance(result.get('auth'), dict): + raise Refused('malformed_native_login') + if 'errors' in result: + raise RecoveredLoginRefused('native_error_envelope', result['auth']) + return result['auth'], 'login_succeeded' + except (ValueError, KeyError, TypeError): + raise Refused('malformed_native_login') from None + +def eso(api, runner=run): + # Existing owner SSH certificate, no kubeconfig/Secret reads. Separate + # TokenRequest (10m, openbao audience); no ESO controller session revoked. + env = {'PATH': '/usr/local/bin:/usr/bin:/bin', 'HOME': os.path.expanduser('~')} + rc, out, _ = runner(['ssh', '-oBatchMode=yes', 'railiance01', + 'kubectl get nodes -o json'], env) + if rc: + raise Transport('cluster_identity_unproved') + try: + nodes = json.loads(out)['items'] + addresses = {a['address'] for n in nodes for a in n['status']['addresses']} + if len(nodes) != 1 or '92.205.62.239' not in addresses: + raise Refused('wrong_cluster') + except (KeyError, TypeError, ValueError): + raise Refused('invalid_cluster_identity') from None + rc, jwt, _ = runner(['ssh', '-oBatchMode=yes', 'railiance01', + 'kubectl -n external-secrets create token external-secrets --audience=openbao --duration=10m'], env) + if rc or not jwt.strip(): + raise Transport('tokenrequest_failed') + return api.call('auth/kubernetes/login', body={'role': ESO_ROLE, + 'jwt': jwt.decode().strip()})['auth'] + +def lane_matrix(api, query, auth, mode): + token = auth.get('client_token') + if not isinstance(token, str) or not token or any(c.isspace() for c in token): + raise Refused('malformed_lane_token') + metadata = auth.get('metadata', {}) + expected_role = OP_ROLE if mode == 'operator' else ESO_ROLE + if metadata.get('role') != expected_role: + raise Refused('lane_role_mismatch') + if mode == 'eso' and (metadata.get('service_account_name') != 'external-secrets' or + metadata.get('service_account_namespace') != 'external-secrets'): + raise Refused('eso_identity_mismatch') + if auth.get('identity_policies'): + raise Refused('lane_identity_policy_additions') + expected_policy = POLICY + ('operator' if mode == 'operator' else 'eso') + if set(auth.get('policies', [])) != {expected_policy}: + raise Refused('lane_policy_mismatch') + if (type(auth.get('lease_duration')) is not int or not 0 < auth['lease_duration'] <= (300 if mode == 'operator' else 900)): + raise Refused('lane_ttl_mismatch') + expected = {p: ['deny'] for p in PATHS} + expected[DATA] = ['read'] + expected['auth/token/revoke-self'] = ['update'] + return checked_caps(api.call('sys/capabilities', query, + {'token': token, 'paths': PATHS}), expected) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('mode', choices=['operator', 'eso', 'nonmember']) + parser.add_argument('--survey-token-fd', type=int, required=True) + parser.add_argument('--approval-reference', required=True) + parser.add_argument('--approved-source-sha256', required=True) + parser.add_argument('--receipt', type=Path, required=True) + parser.add_argument('--attended', action='store_true') + args = parser.parse_args() + if not args.attended or not sys.stdin.isatty() or args.survey_token_fd < 3: + parser.error('founder terminal attendance and separate survey token FD required') + if any(os.environ.get(k) for k in ('BAO_TOKEN', 'VAULT_TOKEN', 'BAO_NAMESPACE', 'VAULT_NAMESPACE')): + parser.error('ambient authority refused') + source_sha256 = hashlib.sha256(Path(__file__).read_bytes()).hexdigest() + if args.approved_source_sha256 != source_sha256: + parser.error('approved source digest differs; no session requested') + # Reservation prevents overwrites/symlink substitution before credentials. + fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + receipt = {'schema': 'mason-client-custody-native/v1', 'task': 'MASON-WP-0009-T05', + 'mode': args.mode, 'source_sha256': source_sha256, + 'approval_reference': args.approval_reference, 'at': int(time.time()), + 'secret_reads': 0, 'result': 'unproved', 'owner_acceptance': 'pending', + 'query_revoke': False, 'lane_revoke': False} + api = Api() + query = '' + query_valid = False + lane = '' + try: + with os.fdopen(args.survey_token_fd, 'r') as handle: + query = handle.read(4097).strip() + if not query or len(query) > 4096 or any(c.isspace() for c in query): + raise Refused('malformed_query_token') + query_info = survey(api, query) + query_valid = True + receipt['query_accessor_sha256'] = accessor_digest(query_info.get('accessor')) + if args.mode == 'eso': + auth = eso(api) + else: + import tempfile + with tempfile.TemporaryDirectory(prefix='mason-oidc-') as home: + auth, result = oidc(safe_env(home)) + receipt['native_login_result'] = result + if auth is None: + # Never pronounce the nonmember check passed from CLI text. + raise Refused(result) + lane = auth.get('client_token', '') + receipt['lane_accessor_sha256'] = accessor_digest(auth.get('accessor')) + if args.mode == 'nonmember': + raise Refused('nonmember_obtained_token') + receipt['capabilities'] = lane_matrix(api, query, auth, args.mode) + receipt['result'] = 'capability_matrix_observed' + except RecoveredLoginRefused as error: + lane = error.token + receipt['failure_class'] = str(error) + except (Refused, Transport) as error: + receipt['failure_class'] = str(error) + except Exception: + receipt['failure_class'] = 'invalid_or_unexpected_response' + finally: + if lane: + try: + api.call('auth/token/revoke-self', lane, {}) + receipt['lane_revoke'] = True + except Exception: + receipt['result'] = 'unproved' + if query_valid: + try: + api.call('auth/token/revoke-self', query, {}) + receipt['query_revoke'] = True + except Exception: + receipt['result'] = 'unproved' + query = lane = '' + with os.fdopen(fd, 'w') as handle: + json.dump(receipt, handle, indent=2, sort_keys=True) + handle.write('\n') + return 0 if receipt['result'] == 'capability_matrix_observed' else 1 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/test_keycape_client_custody_verifier.py b/tests/test_keycape_client_custody_verifier.py new file mode 100644 index 0000000..6f70ab5 --- /dev/null +++ b/tests/test_keycape_client_custody_verifier.py @@ -0,0 +1,298 @@ +import importlib.util +from pathlib import Path +import unittest + +SCRIPT = Path(__file__).resolve().parents[1] / 'scripts/verify-keycape-client-custody.py' +spec = importlib.util.spec_from_file_location('custody', SCRIPT) +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +NATIVE_API = m.Api + +class Fake: + def __init__(self, info=None, changes=None): + self.calls = [] + self.info = info or {'policies': ['ops-mason-survey', 'default'], + 'display_name': 'token-claude-MASON-WP-0009-T05', 'ttl': 2700, + 'accessor': 'test-query-accessor'} + self.changes = changes or {} + def call(self, endpoint, token='', body=None): + self.calls.append((endpoint, token, body)) + if endpoint == 'auth/token/lookup-self': + return {'data': self.info} + if endpoint == 'sys/capabilities-self': + result = {m.DATA: ['deny'], 'sys/policies/acl/ops-mason-survey': ['deny'], + 'auth/token/create': ['deny'], 'sys/capabilities': ['update']} + else: + result = {p: ['deny'] for p in m.PATHS} + result[m.DATA] = ['read'] + result['auth/token/revoke-self'] = ['update'] + result.update(self.changes) + return {'data': result} + +class Tests(unittest.TestCase): + def auth(self, mode='operator'): + return {'client_token': 'test-only-not-a-credential', 'accessor': 'test-accessor', + 'policies': [m.POLICY + mode], 'lease_duration': 300, + 'metadata': {'role': m.OP_ROLE if mode == 'operator' else m.ESO_ROLE, + 'service_account_name': 'external-secrets', + 'service_account_namespace': 'external-secrets'}} + def test_query_identity_and_data_deny(self): + self.assertEqual(m.survey(Fake(), 'test-query')['accessor'], 'test-query-accessor') + def test_broad_ambient_query_refused(self): + for policies in [['root'], ['ops-mason-survey', 'platform-admin']]: + with self.subTest(policies=policies), self.assertRaises(m.Refused): + m.survey(Fake(info={'policies': policies}), 'test-query') + def test_wrong_task_and_unbounded_grant_refused(self): + for field, value in [('display_name', 'claude-another-task'), ('ttl', 2701), ('ttl', 0)]: + fake = Fake() + fake.info[field] = value + with self.subTest(field=field, value=value), self.assertRaises(m.Refused): + m.survey(fake, 'test-query') + def test_query_cannot_read_value_or_create_token(self): + for path in [m.DATA, 'auth/token/create', 'sys/policies/acl/ops-mason-survey']: + with self.subTest(path=path), self.assertRaises(m.Refused): + m.survey(Fake(changes={path: ['read']}), 'test-query') + def test_operator_and_eso_native_matrix(self): + for mode in ['operator', 'eso']: + fake = Fake() + result = m.lane_matrix(fake, 'test-query', self.auth(mode), mode) + self.assertEqual(result[m.DATA], ['read']) + self.assertEqual(fake.calls[0][0], 'sys/capabilities') + self.assertEqual(fake.calls[0][1], 'test-query') + def test_data_write_and_missing_read_refused(self): + for caps in [['read', 'update'], ['deny'], ['sudo', 'read']]: + with self.subTest(caps=caps), self.assertRaises(m.Refused): + m.lane_matrix(Fake(changes={m.DATA: caps}), 'test-query', self.auth(), 'operator') + def test_parent_sibling_metadata_and_policy_escalation_refused(self): + for path in m.PATHS[1:-1]: + with self.subTest(path=path), self.assertRaises(m.Refused): + m.lane_matrix(Fake(changes={path: ['read']}), 'test-query', self.auth(), 'operator') + def test_incomplete_matrix_refused(self): + with self.assertRaises(m.Refused): + m.checked_caps({'data': {m.DATA: ['read']}}, {m.DATA: ['read'], 'other': ['deny']}) + def test_wrong_policy_default_identity_role_ttl_refused(self): + for field, value in [('policies', ['root']), ('policies', [m.POLICY+'operator', 'default']), + ('metadata', {'role': 'platform-admin'}), ('lease_duration', 301), + ('client_token', 'invalid token')]: + auth = self.auth() + auth[field] = value + with self.subTest(field=field, value=value), self.assertRaises(m.Refused): + m.lane_matrix(Fake(), 'test-query', auth, 'operator') + def test_wrong_eso_namespace_refused(self): + auth = self.auth('eso') + auth['metadata']['service_account_namespace'] = 'sso' + with self.assertRaises(m.Refused): + m.lane_matrix(Fake(), 'test-query', auth, 'eso') + def test_login_failure_is_never_negative_acceptance(self): + for err in [b'connection refused', b'HTTP 403 permission denied', b'user cancelled', + b'claim "groups" does not match any associated bound claim values']: + result, category = m.oidc({}, lambda *args: (1, b'', err)) + self.assertIsNone(result) + self.assertIn(category, ['unproved_login_failure', 'group_refusal_candidate']) + self.assertNotIn('accepted', category) + def test_no_store_and_fixed_native_role(self): + calls = [] + def runner(argv, env): + calls.append(argv) + return 1, b'', b'cancelled' + m.oidc({}, runner) + self.assertIn('-no-store', calls[0]) + self.assertIn('role=' + m.OP_ROLE, calls[0]) + def test_api_rejects_secret_gets_role_writes_and_generic_endpoints(self): + api = m.Api() + for endpoint in [m.DATA, m.DATA.replace('/data/', '/metadata/'), + 'sys/policies/acl/new', 'auth/netkingdom/role/new', 'auth/token/create']: + with self.subTest(endpoint=endpoint), self.assertRaises(m.Refused): + api.call(endpoint, 'test-only') + def test_credentials_not_in_child_env(self): + import os + from unittest.mock import patch + with patch.dict(os.environ, {'BAO_TOKEN': 'test', 'VAULT_TOKEN': 'test', + 'HTTPS_PROXY': 'https://wrong', 'BAO_NAMESPACE': 'other'}): + env = m.safe_env('/tmp/test-only') + for name in ['BAO_TOKEN', 'VAULT_TOKEN', 'HTTPS_PROXY', 'BAO_NAMESPACE']: + self.assertNotIn(name, env) + self.assertEqual(env['HOME'], '/tmp/test-only') + def test_eso_cluster_mismatch_stops_before_tokenrequest(self): + calls = [] + def runner(argv, env): + calls.append(argv) + return 0, b'{"items": [{"status": {"addresses": [{"address": "10.0.0.1"}]}}]}', b'' + with self.assertRaises(m.Refused): + m.eso(Fake(), runner) + self.assertEqual(len(calls), 1) + def exercise_main(self, auth, failure=None, mode='operator', query_info=None, bad_revoke_response=None, login_error=None): + import tempfile + import os + from unittest.mock import patch + with tempfile.TemporaryDirectory() as directory: + credential = Path(directory) / 'test-fixture' + credential.write_text('test-only-query') + token_fd = os.open(credential, os.O_RDONLY) + receipt = Path(directory) / 'receipt.json' + fake = Fake() + if query_info is not None: + fake.info.update(query_info) + old_call = fake.call + def call(endpoint, token='', body=None): + if endpoint == 'auth/token/revoke-self' and bad_revoke_response is not None: + native = NATIVE_API() + native.opener = self.opener(bad_revoke_response) + return native.call(endpoint, token, body) + if endpoint == 'auth/token/revoke-self' and token == failure: + raise m.Transport('test_cleanup_transport') + return old_call(endpoint, token, body) + fake.call = call + argv = ['verifier', mode, '--survey-token-fd', str(token_fd), + '--approval-reference', 'test-not-an-approval', '--approved-source-sha256', + m.hashlib.sha256(SCRIPT.read_bytes()).hexdigest(), '--attended', '--receipt', str(receipt)] + with patch.object(m.sys, 'argv', argv), patch.object(m.sys.stdin, 'isatty', return_value=True), \ + patch.dict(os.environ, {}, clear=True), patch.object(m, 'Api', return_value=fake), \ + patch.object(m, 'oidc', return_value=(auth, 'login_succeeded'), side_effect=login_error): + code = m.main() + return code, m.json.loads(receipt.read_text()), fake.calls + def test_cleanup_attempted_even_when_policy_check_fails(self): + auth = self.auth() + auth['policies'] = ['root'] + code, receipt, calls = self.exercise_main(auth) + self.assertEqual(code, 1) + self.assertTrue(receipt['lane_revoke']) + self.assertTrue(receipt['query_revoke']) + self.assertNotIn('test-only-not-a-credential', m.json.dumps(receipt)) + def test_cleanup_transport_invalidates_positive_matrix(self): + for token in ['test-only-query', 'test-only-not-a-credential']: + code, receipt, calls = self.exercise_main(self.auth(), failure=token) + self.assertEqual(code, 1) + self.assertEqual(receipt['result'], 'unproved') + self.assertEqual(len([c for c in calls if c[0] == 'auth/token/revoke-self']), 1) + def test_unexpected_nonmember_success_is_revoked_and_fails(self): + code, receipt, calls = self.exercise_main(self.auth(), mode='nonmember') + self.assertEqual(code, 1) + self.assertEqual(receipt['failure_class'], 'nonmember_obtained_token') + self.assertTrue(receipt['lane_revoke']) + self.assertEqual(receipt['owner_acceptance'], 'pending') + def response(self, status=200, content='application/json', raw=b'{"data":{}}', length=None): + from email.message import Message + headers = Message() + if content: + headers['Content-Type'] = content + if length is not None: + headers['Content-Length'] = length + class Response: + def __enter__(self): return self + def __exit__(self, *args): pass + def read(self, count): return raw[:count] + response = Response() + response.status = status + response.headers = headers + return response + def opener(self, response): + from types import SimpleNamespace + return SimpleNamespace(open=lambda *args, **kwargs: response) + def test_mixed_login_error_preserves_recovered_token_cleanup(self): + raw=m.json.dumps({'errors':['private-synthetic'], 'auth':self.auth()}).encode() + with self.assertRaises(m.RecoveredLoginRefused) as error: + m.oidc({},lambda *args:(0,raw,b'')) + code, receipt, _ = self.exercise_main(self.auth(), login_error=error.exception) + self.assertEqual(code,1) + self.assertTrue(receipt['lane_revoke']) + self.assertTrue(receipt['query_revoke']) + self.assertNotIn('private-synthetic',m.json.dumps(receipt)) + native=m.Api();native.opener=self.opener(self.response(raw=raw)) + with self.assertRaises(m.RecoveredLoginRefused) as error: + native.call('auth/kubernetes/login',body={}) + self.assertEqual(error.exception.token,'test-only-not-a-credential') + def test_endpoint_http_success_contracts(self): + for endpoint, raw in [('auth/token/lookup-self', b'{"data":{}}'), + ('sys/capabilities', b'{"data":{}}'), + ('auth/kubernetes/login', b'{"auth":{}}')]: + api = m.Api();api.opener = self.opener(self.response(raw=raw)) + self.assertIsInstance(api.call(endpoint, 'test', {}), dict) + api = m.Api();api.opener = self.opener(self.response(status=204, content='', raw=b'')) + self.assertEqual(api.call('auth/token/revoke-self', 'test', {}), {}) + def test_bad_metadata_status_content_size_shape_errors_refused(self): + responses = [self.response(status=status) for status in [201,202,206,301,302,307,308]] + responses += [self.response(content='text/plain'), self.response(length='65537'), + self.response(length='-1'), self.response(raw=b'x'*65537), + self.response(raw=b'private-synthetic-invalid-json'), + self.response(raw=b'{"data":[]}'), self.response(raw=b'{}'), + self.response(raw=b'{"errors":[],"data":{}}'), + self.response(raw=b'{"errors":["private-synthetic"],"data":{}}')] + for response in responses: + api=m.Api();api.opener=self.opener(response) + with self.subTest(response=response), self.assertRaises((m.Refused,m.Transport)): + api.call('auth/token/lookup-self', 'synthetic') + def test_revoke202_wrong_content_error_body_never_confirmed_in_receipt(self): + responses = [self.response(status=status) for status in [200,201,202,206]] + responses += [self.response(status=204, raw=b'error')] + for response in responses: + code, receipt, _ = self.exercise_main(self.auth(), bad_revoke_response=response) + self.assertEqual(code, 1) + self.assertEqual(receipt['result'], 'unproved') + self.assertFalse(receipt['lane_revoke']) + self.assertFalse(receipt['query_revoke']) + def test_valid204_main_cleanup_confirmation(self): + code, receipt, _ = self.exercise_main(self.auth(), bad_revoke_response=self.response(status=204, content='', raw=b'')) + self.assertEqual(code, 0) + self.assertTrue(receipt['lane_revoke']) + self.assertTrue(receipt['query_revoke']) + def test_missing_invalid_accessors_fail_with_recovered_token_cleanup(self): + for accessor in [None, '', 123, ' ', 'x'*257]: + auth=self.auth();auth['accessor']=accessor + code, receipt, _ = self.exercise_main(auth) + self.assertEqual(code, 1) + self.assertTrue(receipt['lane_revoke']) + self.assertTrue(receipt['query_revoke']) + self.assertNotIn('lane_accessor_sha256',receipt) + code, receipt, _ = self.exercise_main(self.auth(), query_info={'accessor':accessor}) + self.assertEqual(code, 1) + self.assertTrue(receipt['query_revoke']) + self.assertNotIn('query_accessor_sha256',receipt) + def test_ttl_boolean_zero_negative_overbudget_refused(self): + for ttl in [True,False,0,-1,301,'300']: + auth=self.auth();auth['lease_duration']=ttl + with self.subTest(ttl=ttl), self.assertRaises(m.Refused): + m.lane_matrix(Fake(), 'test-query', auth, 'operator') + def test_approved_revision_mismatch_precedes_receipt_token_or_native(self): + from unittest.mock import patch + import os + argv=['verifier','operator','--survey-token-fd','99','--approval-reference','test', + '--approved-source-sha256','wrong','--attended','--receipt','/tmp/must-not-exist-mason-test'] + with patch.object(m.sys,'argv',argv), patch.object(m.sys.stdin,'isatty',return_value=True), \ + patch.dict(os.environ,{},clear=True), patch.object(m.os,'open',side_effect=AssertionError('no IO')): + with self.assertRaises(SystemExit) as error: + m.main() + self.assertEqual(error.exception.code,2) + def test_subprocess_capture_is_bounded_while_running(self): + import sys + with self.assertRaises(m.Refused): + m.run([sys.executable,'-c', 'import sys; sys.stdout.write("x"*1000000)'], {'PATH':'/usr/bin:/bin'}) + result=m.run([sys.executable,'-c','import sys; print("synthetic"); sys.stderr.write("synthetic error")'],{'PATH':'/usr/bin:/bin'}) + self.assertEqual(result,(0,b'synthetic\n',b'synthetic error')) + def test_real_relative_redirect_and_hostile_proxy_refused(self): + import threading + import os + from unittest.mock import patch + from http.server import BaseHTTPRequestHandler, HTTPServer + calls=[] + class Handler(BaseHTTPRequestHandler): + def do_GET(self): + calls.append(self.path) + self.send_response(302);self.send_header('Location','/private');self.end_headers() + def log_message(self,*args):pass + server=HTTPServer(('127.0.0.1',0),Handler) + thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() + try: + with patch.object(m,'BASE','http://127.0.0.1:'+str(server.server_port)), \ + patch.dict(os.environ, {'HTTP_PROXY':'http://127.0.0.1:1','http_proxy':'http://127.0.0.1:1','NO_PROXY':'','no_proxy':''}): + with self.assertRaises(m.Refused): + m.Api().call('auth/token/lookup-self','synthetic-not-a-credential') + finally: + server.shutdown();server.server_close();thread.join() + self.assertEqual(calls,['/v1/auth/token/lookup-self']) + def test_redirect_refused(self): + with self.assertRaises(m.Refused): + m.NoRedirect().redirect_request(None, None, None, None, None, None) + +if __name__ == '__main__': + unittest.main() diff --git a/workplans/ADHOC-2026-10-11.md b/workplans/ADHOC-2026-10-11.md index 921c5bc..f27fd9b 100644 --- a/workplans/ADHOC-2026-10-11.md +++ b/workplans/ADHOC-2026-10-11.md @@ -84,3 +84,33 @@ This bounded review task is complete. Corrections and native proof remain live in MASON-WP-0009-T05; no plan completion or route activation is claimed and no new residual is parked in prose. No native action, credential request, value access, approval disposition, commit, push or consistency sync was performed. + + +### T03 corrected-source re-review — same review trial + +Completed independent re-review preparation before the refined formal entry. +Original request-changes review and frozen refinement01 are preserved. Exact +corrected manifest SHA256 4dbf808e2d79c2e6600a163e147db7d15b075e59a8edf8cd236f31fdce9b5155, +verifier SHA256 f7685646dca48db5874174940c1826db186e1ed73e167814d8c0f37af3ad619c. +Verified all five immutable hashes and actual existing WP0009 baseline. + +Corrected source now accepts only HTTP200 application/json object envelopes with +endpoint data/auth shape and no errors member. Self-revoke requires HTTP204 and +empty body; stale or error responses cannot confirm cleanup. Nonempty typed +bounded accessors are mandatory; recovered query/lane tokens still revoke after +missing accessor or other later verification refusal. TTL types/limits, mixed +errors plus auth recovery, source digest preflight, incremental 64KiB native +stdout/stderr capture and redirect/proxy guards are covered. All 29 maker tests +pass independently. Four additional independent counterexamples pass: cross-origin +redirect target gets no token/request; absent revoke response and failed cleanup +remain unproved; expired TTL/missing accessor retain cleanup with no correlation +success; oversized native stderr is refused while child runs. + +INTENT Mission/Responsibility boundary remains respected: no KV GET, no widening, +source preparation only, exact founder go still required. Nonmember acceptance +continues to need issuer-authenticated human nonmember/aal2 and correlated native +platform groups refusal for the same attempted subject/flow; callback text alone +never passes. Source acceptance does not attest native isolation or finish T05. +Corrections to the first review's two blocking defects are complete at source; +existing MASON-WP-0009-T05 retains all native matrices, nonmember correlation, +owner acceptance and route activation. No new residual or native act is claimed. diff --git a/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md b/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md index 5f7237a..f626a74 100644 --- a/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md +++ b/workplans/MASON-WP-0009-keycape-secrets-engine-openbao-client-custody.md @@ -9,7 +9,7 @@ flavor: planning owner: claude topic_slug: netkingdom created: "2026-10-06" -updated: "2026-10-07" +updated: "2026-10-11" state_hub_workstream_id: "d9be08f5-a0f5-5cd1-8f87-0a6868661d77" --- @@ -182,3 +182,35 @@ only, 5m, 8 uses, no default policy). ops-mason's checks are complete. Pointer entry proposed to ops-warden as draft, non-resolvable (message `8ad8ed4e`). Left owner-run: capability checks with live operator and ESO tokens and a non-member refusal. + + +2026-10-11 (preparation only, no native action): a guarded founder-run verifier +and packet are prepared for T05: `scripts/verify-keycape-client-custody.py`, +`docs/keycape-client-custody-attended-verification.md`, source preparation record +`docs/evidence/2026-10-11-keycape-client-custody-native-preparation.json`. +The existing read-only survey query authority is reused because neither lane +policy grants sys/capabilities-self; lane tokens are queried in memory via +sys/capabilities without any KV GET. The query grant is checked for exact task +identity, policies, bounded TTL and explicit data denial. Native OIDC uses +-no-store; ESO uses a separately minted, 10m Kubernetes TokenRequest for the +existing ServiceAccount, never a Secret/controller-token read. Failed cleanup +invalidates observations. Nonmember CLI failure cannot pass: independently +verified key-cape identity/groups/aal2 and platform's correlated groups refusal +are required before owner acceptance. 29 offline tests passed, including broad +query authority, wrong role/ESO identity, unsafe matrix, endpoint secret-read +refusal, transport versus claim failure, unexpected nonmember success and +cleanup failures. Formal ops-mason source review, exact-revision founder +attendance approval, native receipts and nonmember owner correlation remain +unresolved. T05 and route activation remain open; no foreign owner review, +authority grant, workplan completion or live acceptance is asserted. + + +2026-10-11 source refinement (same T05 trial, no native action): independent +review of the initial immutable stage requested HTTP revoke/status/error-shape +and native accessor corrections. Corrected source requires endpoint-specific +HTTP200 JSON or empty HTTP204 self-revoke, rejects mixed errors, requires actual +nonempty native accessors, preserves recovered-token cleanup on malformed login, +uses positive integer TTLs and bounded incremental native stdout/stderr capture. +29 tests pass; corrected manifest is separate and preserves the rejected input. +Formal review of corrected bytes and all founder/native acceptance gates remain +open; this refinement is not a second scored success or an authority change.