diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/README.md b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/README.md new file mode 100644 index 00000000..af8e91ea --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/README.md @@ -0,0 +1,81 @@ +# Platform broker source candidate + +This is an isolated, uncommitted candidate against Platform +`104ed90f42a9a86bf9fda343b49b65a85c9ee4b9`, with NetKingdom +`befb0bc68cd25b625804fce559476eefd702521d` and Warden +`b593c879831378db7dc62adfabdfedf2d44ddd38` as review inputs. +No canonical Platform, NetKingdom or Warden files were changed. No owner assent, +1.3 acknowledgment, case acceptance, credential, lane or native grant is created. + +`platform-source.patch` modifies four existing Platform files; exact input and +output hashes are in `manifest.json`, and the full candidate files are under +`candidate/`. `workplan-source-draft.md` proposes a legitimate new Platform +workplan with origin NK-WP-0048-T16 and four tasks, mapping the owner's review +labels RPF-1.3-T1..T4 into source work records. RPF-WP-0060 was next free in the +frozen source; recheck allocation before publishing. No managed UUIDs are set. + +The broker uses an explicit owner-controlled revision map. Only the existing +1.1 revision is admitted. The 1.2 source acknowledgment does not extend broker +admission, and actual proposed 1.3 remains refused. Each accepted revision needs +an exact type-source path and byte digest, exact reviewed parameter fingerprint, +acknowledged/implemented status, matching id/version, and successful case/type +validation against the authenticated producer closure. The three producer module +pins match Warden BrowserInputs. No 1.3 parameter or source pin is introduced. +Unknown, unbound, wrong-source, symlinked, proposed and drifted sources fail +closed before native calls. The existing exact case-byte reference, materialized +status, confidential-client and client-id checks remain required. + +The producer is copied only after hash verification into a disposable tree and +runs under `python -I` with an empty environment, following the existing Warden +pattern. Sources come from the fixed sibling `net-kingdom` checkout. No CLI or +environment-supplied root can select type sources. An installation without those +exact source bytes now fails closed; source installation/layout must therefore +be reviewed before integration. This deliberately makes availability depend on +verified source rather than silently falling back to the old string check. + +Catalog actors become human-operator only and the subject names the exact +`auth/netkingdom/role/credential-broker-keycape-client-secret-generate` issuer +binding. The regression checks the actual CCR-2026-0043 role file's +net-kingdom-admins group and aal2 claim. No policy or issuer role changes. + +This packet completes candidate type/source hardening and the catalog correction +only. Review label RPF-1.3-T1 is not fully satisfied: authenticated admitted-case +verification remains open, beyond reference/schema validation. T01 stays todo +in the source draft, as do the other tasks pending owner integration/review. + +`tests.log`: 15 broker tests and 4 catalog tests pass. Tests use a fake bao; +bwrap supplies an isolated network namespace with no host home/config mounts, +empty HOME and allowlisted environment. No credential, provider, model or native +calls occur. The synthetic future-revision test uses owner-configuration mocks +and an explicitly synthetic acknowledged type, proving that a new revision needs +both admission bindings and acknowledgment; it is not a test of actual 1.3 +admission. The actual proposed 1.3 and source-only 1.2 refusal paths are tested. +`baseline-reproduction.log` retains two expected failures against the original +broker/catalog with the new regressions: the old broker accepts an unknown case +parameter without producer validation; the old catalog permits approved-agent. +The original hardcoded @1.1 check remains visible in the patch input and would +refuse any successor regardless of its genuine future acknowledgment. + +Reproduction uses archives extracted into `/tmp/platform-broker-r1/` with sibling +`railiance-platform`, `net-kingdom` and `ops-warden` trees, and an empty +`empty-home` directory. Apply the candidate patch to that Platform archive, then +run `sh run-tests.sh`. Paths in the runner are intentionally exact. The baseline +runner temporarily substitutes the two original Platform files in scratch and +restores candidate bytes in a finally block; it never modifies canonical sources. + +Limits and integration gates: + +- Self-hashed case bytes and validation do not authenticate an owner's case + acceptance. That existing semantic limit is unchanged; Warden/native evidence + and the operator session must establish actual authorization independently. +- A pinned acknowledged type and validation never authorize execution. There is + no actual 1.3 binding, acknowledgment, owner-descriptor-acceptance or admission. +- T3/T4 capabilities, attended destroy authority, evidence publication, + revocations and platform-secret-delivery lane remain unimplemented. Their + legitimate proposed task records are retained in the draft workplan. +- Before source integration, independently review this patch, the fixed source + deployment requirement, source pins and draft workplan. Recheck exact source + heads, allocate the new Platform workplan, apply the reviewed candidate in that + owner repository, rerun the offline checks, and use the owner's Repo Manager + publication/projection procedure. This packet authorizes none of those native + operations or later owner acceptance by itself. diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/SHA256SUMS b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/SHA256SUMS new file mode 100644 index 00000000..57661077 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/SHA256SUMS @@ -0,0 +1,13 @@ +fb652cd9c77ae0f270f7775a17e01bc57498db52df40dd867028e0ecc3957cc5 README.md +abcec396aee21d9f44dd949b3de58e91ff4c15452af62dc168354c7f5769dca2 baseline-reproduction.log +7cfe12970a57ab75cb91c7c5024a4dcfc28ffae4b31a1afaf4d0cedabbb22886 candidate/credential-grants/catalog.yaml +da80947a2c28df36b37e711d1e6d2f66b4c0434082e7bf187da3d3cb692cec28 candidate/scripts/keycape_client_secret_generate.py +5c0cfcb039c0220e59dac7986d960bb568593a3a9281ff2557bb6c38de31e68a candidate/tests/test_credential_grants.py +5906e32c7fe0db09c1f7db48ad61226497e062f38ebfa91e8a03be89272765c0 candidate/tests/test_keycape_client_secret_generate.py +61c15ae8d12b3665eb0e210cc0a1b0a238f52026258a59987657fd77f8d6c0ba manifest.json +3ecccb9cee2279b2f6d665fc7116421685262abbd7b0f82f59e3c8ed56892ba3 patch-check.log +b0b85153d04a28d4bb05ab39a9b54c782c9efa54bd80ca961e2bcc6348a9b546 platform-source.patch +f3d1feabe5124f882306b1b58485a86ac2072d93981d468dc746efdf4ed6b6b3 reproduce-baseline.py +28f077918a87437d76b14dc93a035b358a99f1a721735b189263cf6d3b27fdea run-tests.sh +2102b703320a633de8a5135a753020d4e8e0854947df41f400b2935562f38191 tests.log +88e329cb244818427717b9f28b26b2ef2610c20f6ad3dfd3ae0d83386b6c1b51 workplan-source-draft.md diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/baseline-reproduction.log b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/baseline-reproduction.log new file mode 100644 index 00000000..9ca503fa --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/baseline-reproduction.log @@ -0,0 +1,42 @@ +Baseline Platform 104ed90f with candidate regressions; expected two failures. +test_acknowledged_11_still_requires_its_exact_parameters (test_keycape_client_secret_generate.Generate.test_acknowledged_11_still_requires_its_exact_parameters) ... FAIL +test_keycape_generator_actor_matches_applied_issuer_binding (test_credential_grants.CredentialGrantBindTests.test_keycape_generator_actor_matches_applied_issuer_binding) ... FAIL + +====================================================================== +FAIL: test_acknowledged_11_still_requires_its_exact_parameters (test_keycape_client_secret_generate.Generate.test_acknowledged_11_still_requires_its_exact_parameters) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tmp/platform-broker-r1/railiance-platform/tests/test_keycape_client_secret_generate.py", line 188, in test_acknowledged_11_still_requires_its_exact_parameters + self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed')) +AssertionError: Tuples differ: (0, 'generated') != (2, 'case_source_validation_failed') + +First differing element 0: +0 +2 + +- (0, 'generated') ++ (2, 'case_source_validation_failed') + +====================================================================== +FAIL: test_keycape_generator_actor_matches_applied_issuer_binding (test_credential_grants.CredentialGrantBindTests.test_keycape_generator_actor_matches_applied_issuer_binding) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tmp/platform-broker-r1/railiance-platform/tests/test_credential_grants.py", line 36, in test_keycape_generator_actor_matches_applied_issuer_binding + self.assertEqual(grant['actors']['allowed_types'], ['human-operator']) +AssertionError: Lists differ: ['approved-agent', 'human-operator'] != ['human-operator'] + +First differing element 0: +'approved-agent' +'human-operator' + +First list contains 1 additional elements. +First extra element 1: +'human-operator' + +- ['approved-agent', 'human-operator'] ++ ['human-operator'] + +---------------------------------------------------------------------- +Ran 2 tests in 0.381s + +FAILED (failures=2) diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/credential-grants/catalog.yaml b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/credential-grants/catalog.yaml new file mode 100644 index 00000000..afb17dd2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/credential-grants/catalog.yaml @@ -0,0 +1,279 @@ +version: 1 +updated: "2026-08-13" +owner_repo: railiance-platform +owner_domain: financials +workplan_id: RAILIANCE-WP-0005 +state_hub_workstream_id: 2731fece-6c49-45b8-ab8a-4ea6c04ac603 + +delivery_modes: + allowed_known: + - exec-env + - response-wrap + - local-token-file + - kubernetes-auth + denied_known: + - chat + - state-hub-body + - git + - command-line-token-argument + - llm-prompt + +grant_classes: + - self-service + - approval-required + - break-glass + +grants: + - id: rapp-postgres/audit-core-runtime + title: audit-core runtime PostgreSQL lease + rapp_id: rapp-postgres + status: active + grant_class: self-service + credential_type: openbao-database-credential + issuer: openbao + audience: audit-core + description: Short-lived runtime connection to the audit-core database. + openbao: + namespace: openbao + token_role: audit-core-runtime-broker + issuer_policy: credential-broker-audit-core-runtime-issuer + policies: [credential-broker-audit-core-runtime] + disallowed_policies: [root, platform-admin] + mount_paths: [database/creds/audit-core-runtime] + database: + role: audit-core-runtime + host: platform-pg-rw.databases.svc.cluster.local + port: 5432 + name: audit_core + env: {PGUSER: username, PGPASSWORD: password, PGHOST: host, PGPORT: port, PGDATABASE: database} + ttl: {default: 15m, max: 1h, renewable: false, requires_human_above: 1h} + actors: + allowed_types: [human-operator, approved-agent, ci-runner] + required_subject_binding: keycape-or-kubernetes-service-account + authorization: + flex_auth_required: false + flex_auth_mode: optional-preflight + approval_required: false + purpose_required: true + allowed_purpose_examples: [audit-core-runtime] + delivery: + allowed: [exec-env] + preferred: exec-env + denied: [chat, state-hub-body, git, command-line-token-argument, llm-prompt] + exec_env: {child_only: true, redact_logs: true} + audit: {openbao_audit_required: true, state_hub_metadata_allowed: true, record_secret_values: false} + revocation: {required: true, by_accessor: true, on_exec_exit: true, on_denied_request: false} + + - id: rapp-postgres/audit-core-migration + title: audit-core migration PostgreSQL lease + rapp_id: rapp-postgres + status: active + grant_class: self-service + credential_type: openbao-database-credential + issuer: openbao + audience: audit-core + description: Short-lived migration connection to the audit-core database. + openbao: + namespace: openbao + token_role: audit-core-migration-broker + issuer_policy: credential-broker-audit-core-migration-issuer + policies: [credential-broker-audit-core-migration] + disallowed_policies: [root, platform-admin] + mount_paths: [database/creds/audit-core-migration] + database: + role: audit-core-migration + host: platform-pg-rw.databases.svc.cluster.local + port: 5432 + name: audit_core + env: {PGUSER: username, PGPASSWORD: password, PGHOST: host, PGPORT: port, PGDATABASE: database} + ttl: {default: 10m, max: 30m, renewable: false, requires_human_above: 30m} + actors: + allowed_types: [human-operator, approved-agent, ci-runner] + required_subject_binding: keycape-or-kubernetes-service-account + authorization: + flex_auth_required: false + flex_auth_mode: optional-preflight + approval_required: false + purpose_required: true + allowed_purpose_examples: [audit-core-migration] + delivery: + allowed: [exec-env] + preferred: exec-env + denied: [chat, state-hub-body, git, command-line-token-argument, llm-prompt] + exec_env: {child_only: true, redact_logs: true} + audit: {openbao_audit_required: true, state_hub_metadata_allowed: true, record_secret_values: false} + revocation: {required: true, by_accessor: true, on_exec_exit: true, on_denied_request: false} + + - id: keycape/client-secret-generate + title: KeyCape confidential-client secret generation in custody + status: pilot + grant_class: approval-required + credential_type: openbao-token + issuer: openbao + audience: railiance-platform-credential-broker + description: > + RPF-WP-0054 / CCR-2026-0040. Short-lived token the broker uses inside its + own process to create platform/workloads/keycape/ once (CAS 0) + for an accepted oidc-browser-relying-party case. Create-only: it cannot + update, read values, write metadata, list or delete. The generated value + never leaves the broker process and the bao child's stdin; the caller gets a + receipt only. Founder decision 2026-10-09 (RPF-WP-0054-T02 option A). + openbao: + namespace: openbao + token_role: keycape-client-secret-generate + issuer_policy: credential-broker-keycape-client-secret-generate-issuer + policies: + - keycape-client-secret-generate + disallowed_policies: + - root + - platform-admin + mount_paths: + - platform/data/workloads/keycape/+ + - platform/metadata/workloads/keycape/+ + ttl: + default: 5m + max: 5m + renewable: false + requires_human_above: 5m + actors: + allowed_types: + - human-operator + required_subject_binding: auth/netkingdom/role/credential-broker-keycape-client-secret-generate + # CCR-2026-0043: human net-kingdom-admins membership and /assurance/level=aal2. + authorization: + flex_auth_required: false + flex_auth_mode: optional-preflight + approval_required: true + purpose_required: true + allowed_purpose_examples: + - oidc-browser-relying-party-mint-secret + delivery: + allowed: + - exec-env + preferred: exec-env + denied: + - chat + - state-hub-body + - git + - command-line-token-argument + - llm-prompt + exec_env: + variable: VAULT_TOKEN + child_only: true + redact_logs: true + audit: + openbao_audit_required: true + state_hub_metadata_allowed: true + record_secret_values: false + metadata_fields: + - grant_id + - actor + - subject + - purpose + - case_ref + - client_id + - kv_version + - lease_accessor + - status + revocation: + required: true + by_accessor: true + on_exec_exit: true + on_denied_request: false + - id: ops-warden/warden-sign + title: Ops Warden OpenBao SSH signing smoke token + status: pilot + grant_class: self-service + credential_type: openbao-token + issuer: openbao + audience: ops-warden + description: > + Short-lived OpenBao child token for ops-warden SSH signing smoke tests. + The token may only use the warden-sign policy and must not be treated as + an ops-warden-owned secret. + openbao: + namespace: openbao + token_role: warden-sign + issuer_policy: credential-broker-warden-sign-issuer + policies: + - warden-sign + disallowed_policies: + - root + - platform-admin + mount_paths: + - ssh/sign/adm-role + - ssh/sign/agt-role + - ssh/sign/atm-role + - ssh/roles + ttl: + default: 15m + max: 1h + renewable: false + requires_human_above: 1h + actors: + allowed_types: + - human-operator + - approved-agent + - ci-runner + required_subject_binding: keycape-or-kubernetes-service-account + authorization: + flex_auth_required: false + flex_auth_mode: optional-preflight + approval_required: false + purpose_required: true + allowed_purpose_examples: + - flex-auth-openbao-smoke + - ops-warden-production-sign-smoke + delivery: + allowed: + - exec-env + - response-wrap + - local-token-file + - kubernetes-auth + preferred: exec-env + denied: + - chat + - state-hub-body + - git + - command-line-token-argument + - llm-prompt + exec_env: + variable: VAULT_TOKEN + child_only: true + redact_logs: true + response_wrap: + ttl: 5m + unwrap_once: true + local_token_file: + directory: .local/credential-leases + mode: "0600" + kubernetes_auth: + mount: auth/kubernetes + role: credential-broker-warden-sign + audience: openbao + service_account_names: + - credential-broker + - ops-warden-smoke + namespaces: + - openbao + - ops-warden + audit: + openbao_audit_required: true + state_hub_metadata_allowed: true + record_secret_values: false + metadata_fields: + - grant_id + - actor + - subject + - purpose + - requested_ttl + - issued_ttl + - delivery_mode + - lease_accessor + - decision_id + - status + revocation: + required: true + by_accessor: true + on_exec_exit: true + on_denied_request: false diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/scripts/keycape_client_secret_generate.py b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/scripts/keycape_client_secret_generate.py new file mode 100644 index 00000000..96bdedc7 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/scripts/keycape_client_secret_generate.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +"""RPF-WP-0054-T03: generate a KeyCape confidential-client secret inside custody. + +Entry point for the oidc-browser-relying-party ``mint-secret`` step (lane +``keycape-client-secret-generate``). Normally reached as +``credential.py generate ...``; ops-warden runs it as a child in the caller's +own environment and holds no token. + + credential.py generate --grant keycape/client-secret-generate \\ + --client-id forgejo --case-file \\ + --case-ref forgejo-netkingdom-oidc@sha256: --receipt + +Steps (every one fails closed): + +1. Refuse agents (``WARDEN_AGENT_ID``) and root. The caller's session must hold + the broker issuer policy for this grant. +2. Re-derive the case reference from ``--case-file`` (sha256 over its exact + bytes) and require: it equals ``--case-ref``; type + an exactly source-pinned acknowledged browser revision; status ``materialized``; client_type + ``confidential``; ``parameters.client_id`` equals ``--client-id``, which + matches the type's pattern and is not a reserved entry. +3. Mint a 5-minute grant token through token role + ``keycape-client-secret-generate``. All KV calls below use only that token. +4. Prove absence by metadata (404). An existing entry is never touched. +5. Generate the value in this process and write it with CAS 0 through the + ``bao`` child's stdin. +6. Verify by metadata only: current version 1, live. The data path is never read. +7. Revoke the grant token (revoke-self). + +The process prints nothing. The receipt (mode 0600, refused if it exists) and +the optional State Hub event hold the path, version, case reference, booleans and +an outcome code only. + +Exit codes: 0 generated (dry-run: would generate); 1 contained internal error; +2 precondition refused; 3 entry already exists (never overwritten, including a +lost CAS race); 4 token or write failure; 5 verification failure. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import secrets +import subprocess +import sys +import tempfile +import urllib.request + +import yaml + +GRANT = 'keycape/client-secret-generate' +TOKEN_ROLE = 'keycape-client-secret-generate' +GRANT_POLICY = 'keycape-client-secret-generate' +ISSUER_POLICY = 'credential-broker-keycape-client-secret-generate-issuer' +PREFIX = 'workloads/keycape' +RESERVED = {'secrets-engine-openbao'} +FIELD = 'CLIENT_SECRET' +# Owner configuration, never a caller-supplied root or a version search/fallback. +# The sibling source checkout must contain these exact reviewed bytes. +NETKINGDOM_ROOT = Path(__file__).resolve().parents[2] / 'net-kingdom' +ACKNOWLEDGED_TYPES = { + 'oidc-browser-relying-party@1.1': ( + 'security-case-types/oidc-browser-relying-party/type.yaml', + 'f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c'), +} +# Same authenticated producer and parameter projection as ops-warden case_inputs +# at b593c879, restricted to this broker's currently admitted 1.1. Source-only +# acknowledgment (including 1.2) never expands broker admission. A future 1.3 +# entry requires real acknowledgment and a separately reviewed admission binding. +# Neither source status nor caller-supplied YAML expands this set. +SOURCE_PINS = { + 'tools/security-case-types/security_case_validator.py': + '1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b', + 'tools/security-scenario-composer/security_scenario_composer.py': + '9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f', + 'tools/playbook-capability-contract/playbook_contract_validator.py': + '2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d', +} +PARAMETER_PINS = { + '1.1': '9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c', +} +_VALIDATE = """ +import json, sys +from pathlib import Path +sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types')) +import security_case_validator as v +case, typ = json.load(sys.stdin) +issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False) +print('valid' if not v.has_errors(issues) else 'invalid') +""" +# oidc-browser-relying-party@1.1 parameter client_id. +CLIENT_ID_RE = re.compile(r'^[a-z0-9][a-z0-9-]{1,62}$') +CASE_REF_RE = re.compile(r'^[a-z0-9][a-z0-9-]*@sha256:[0-9a-f]{64}$') +# 48 random bytes -> 64 URL-safe characters, confirmed by key-cape 2026-10-10 +# (message 4e0213b3; opaque constant-time comparison, no alphabet limit). +SECRET_BYTES = 48 +TOKEN_TTL = '5m' +SCHEMA = 'platform.keycape-client-secret-generate.v1' + + +class Exit(Exception): + def __init__(self, code: int, reason: str): + super().__init__(reason) + self.code = code + self.reason = reason + + +def bao(args, token=None, payload=None): + env = dict(os.environ) + if token is not None: + env['BAO_TOKEN'] = env['VAULT_TOKEN'] = token + return subprocess.run(['bao', *args], env=env, + input=None if payload is None else json.dumps(payload).encode(), + capture_output=True, timeout=60) + + +def absent(result): + return result.returncode != 0 and (b'404' in result.stderr or b'No value found' in result.stderr) + + +def read_json(result, code): + try: + return json.loads(result.stdout) + except ValueError: + raise Exit(code, 'unparseable_bao_output') from None + + +def authenticated_source(relative, expected): + path = NETKINGDOM_ROOT / relative + # Refuse symlinks at every selected component, including the configured root. + if any(part.is_symlink() for part in (path, *path.parents)): + raise ValueError + raw = path.read_bytes() + if hashlib.sha256(raw).hexdigest() != expected: + raise ValueError + return raw + + +def check_acknowledged_case(doc): + # Authenticates the selected TYPE/producer bytes and validates case schema. + # The caller's self-hashed case reference is not authenticated case admission + # or owner acceptance. Those require independent Warden/native evidence; + # this source-only hardening does not complete the real admission binding. + binding = ACKNOWLEDGED_TYPES.get(doc['spec'].get('type')) + if binding is None: + raise Exit(2, 'case_type_unacknowledged') + try: + typ = yaml.safe_load(authenticated_source(*binding)) + spec = typ['spec'] + version = str(spec['version']) + fingerprint = hashlib.sha256(json.dumps( + spec['parameters'], sort_keys=True, separators=(',', ':')).encode()).hexdigest() + if (spec['status'] not in ('acknowledged', 'implemented') + or f"{spec['id']}@{version}" != doc['spec']['type'] + or PARAMETER_PINS.get(version) != fingerprint): + raise ValueError + with tempfile.TemporaryDirectory(prefix='platform-case-validator-') as directory: + for relative, expected in SOURCE_PINS.items(): + raw = authenticated_source(relative, expected) + dest = Path(directory) / relative + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(raw) + result = subprocess.run( + [sys.executable, '-I', '-c', _VALIDATE, directory], + input=json.dumps([doc, typ]), text=True, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + env={}, timeout=10, check=True) + if result.stdout != 'valid\n': + raise ValueError + except (OSError, ValueError, TypeError, KeyError, yaml.YAMLError, + subprocess.SubprocessError): + raise Exit(2, 'case_source_validation_failed') from None + + +def check_case(args, receipt): + if not CLIENT_ID_RE.match(args.client_id) or args.client_id in RESERVED: + raise Exit(2, 'client_id_invalid_or_reserved') + if not CASE_REF_RE.match(args.case_ref): + raise Exit(2, 'case_ref_malformed') + try: + raw = Path(args.case_file).read_bytes() + doc = yaml.safe_load(raw) + except (OSError, yaml.YAMLError): + raise Exit(2, 'case_file_unreadable') from None + spec = (doc or {}).get('spec') if isinstance(doc, dict) else None + if not isinstance(spec, dict): + raise Exit(2, 'case_file_shape') + params = spec.get('parameters') or {} + derived = f"{spec.get('id')}@sha256:{hashlib.sha256(raw).hexdigest()}" + if derived != args.case_ref: + raise Exit(2, 'case_ref_mismatch') + if not isinstance(spec.get('type'), str): + raise Exit(2, 'case_type_unacknowledged') + if spec.get('status') != 'materialized': + raise Exit(2, 'case_not_materialized') + if params.get('client_type') != 'confidential': + raise Exit(2, 'case_client_not_confidential') + if params.get('client_id') != args.client_id: + raise Exit(2, 'case_client_id_mismatch') + check_acknowledged_case(doc) + receipt['case_verified'] = True + + +def run(args, receipt, state): + if args.grant != GRANT: + raise Exit(2, 'unknown_grant') + if os.environ.get('WARDEN_AGENT_ID'): + raise Exit(2, 'agent_callers_refused') + check_case(args, receipt) + + ident = bao(['token', 'lookup', '-format=json']) + if ident.returncode != 0: + raise Exit(2, 'caller_session_missing') + policies = (read_json(ident, 2).get('data') or {}).get('policies') or [] + if 'root' in policies: + raise Exit(2, 'root_refused') + if ISSUER_POLICY not in policies: + raise Exit(2, 'caller_lacks_issuer_policy') + + created = bao(['token', 'create', f'-role={TOKEN_ROLE}', f'-policy={GRANT_POLICY}', + f'-ttl={TOKEN_TTL}', '-format=json']) + if created.returncode != 0: + raise Exit(4, 'grant_token_create_failed') + try: + state['token'] = read_json(created, 4)['auth']['client_token'] + except (KeyError, TypeError): + raise Exit(4, 'grant_token_create_failed') from None + token = state['token'] + + meta_path = f'platform/metadata/{PREFIX}/{args.client_id}' + meta = bao(['read', '-format=json', meta_path], token=token) + if meta.returncode == 0: + raise Exit(3, 'already_present') + if not absent(meta): + raise Exit(2, 'absence_not_proven') + if args.dry_run: + return 'dry_run_would_generate' + + value = secrets.token_urlsafe(SECRET_BYTES) + written = bao(['write', '-format=json', f'platform/data/{PREFIX}/{args.client_id}', '-'], + token=token, payload={'options': {'cas': 0}, 'data': {FIELD: value}}) + del value + if written.returncode != 0: + if bao(['read', '-format=json', meta_path], token=token).returncode == 0: + raise Exit(3, 'already_present_lost_race') + raise Exit(4, 'write_failed') + + meta = bao(['read', '-format=json', meta_path], token=token) + if meta.returncode != 0: + raise Exit(5, 'verify_metadata_unreadable') + data = read_json(meta, 5).get('data') or {} + version = (data.get('versions') or {}).get('1') or {} + if data.get('current_version') != 1 or version.get('deletion_time') or version.get('destroyed'): + raise Exit(5, 'verify_version_mismatch') + receipt['kv_version'] = 1 + return 'generated' + + +def record_state_hub(url, receipt): + body = { + 'summary': (f"keycape-client-secret-generate {receipt['outcome']}: {receipt['path']} " + f"v{receipt.get('kv_version', '-')} case {receipt['case_ref']}"), + 'event_type': 'credential_change_evidence', + 'author': 'railiance-platform-credential-broker', + } + request = urllib.request.Request(url.rstrip('/') + '/progress/', data=json.dumps(body).encode(), + headers={'Content-Type': 'application/json'}, method='POST') + with urllib.request.urlopen(request, timeout=10) as response: + return 200 <= response.status < 300 + + +def main(argv=None): + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, 1) + os.dup2(devnull, 2) + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--grant', required=True) + parser.add_argument('--client-id', required=True) + parser.add_argument('--case-ref', required=True) + parser.add_argument('--case-file', required=True) + parser.add_argument('--receipt', type=Path) + parser.add_argument('--dry-run', action='store_true') + parser.add_argument('--record-state-hub', action='store_true') + parser.add_argument('--state-hub-url', default=os.environ.get('STATE_HUB_URL', 'http://127.0.0.1:8000')) + try: + args = parser.parse_args(argv) + except SystemExit: + return 2 + receipt = {'schema': SCHEMA, 'mode': 'dry-run' if args.dry_run else 'generate', + 'grant': args.grant, 'client_id': args.client_id, 'case_ref': args.case_ref, + 'path': f'platform/{PREFIX}/{args.client_id}', 'value_reads': 0, + 'credential_values_emitted': False} + fd = None + if args.receipt is not None: + try: + fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + except OSError: + return 2 + state = {} + try: + receipt['outcome'] = run(args, receipt, state) + code = 0 + except Exit as exc: + receipt['outcome'] = exc.reason + code = exc.code + except Exception: + receipt['outcome'] = 'contained_internal_error' + code = 1 + if state.get('token'): + receipt['grant_token_revoked'] = bao(['token', 'revoke', '-self'], token=state['token']).returncode == 0 + receipt['exit_code'] = code + if args.record_state_hub: + try: + receipt['state_hub_recorded'] = record_state_hub(args.state_hub_url, receipt) + except Exception: + receipt['state_hub_recorded'] = False + if fd is not None: + os.write(fd, (json.dumps(receipt, indent=2, sort_keys=True) + '\n').encode()) + os.close(fd) + return code + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/tests/test_credential_grants.py b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/tests/test_credential_grants.py new file mode 100644 index 00000000..7bd1364a --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/tests/test_credential_grants.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import importlib.util +import sys +import unittest +from pathlib import Path + +REPO_DIR = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "credential_grants_validate", REPO_DIR / "scripts/credential-grants-validate.py" +) +grants = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +sys.modules[SPEC.name] = grants +SPEC.loader.exec_module(grants) + + +class CredentialGrantBindTests(unittest.TestCase): + def test_catalog_validates(self) -> None: + errors: list[str] = [] + catalog = grants.yaml.safe_load( + (REPO_DIR / "credential-grants/catalog.yaml").read_text() + ) + for index, grant in enumerate(catalog["grants"]): + grants.validate_grant(grant, index, catalog, errors) + self.assertEqual(errors, []) + + def test_keycape_generator_actor_matches_applied_issuer_binding(self) -> None: + catalog = grants.yaml.safe_load( + (REPO_DIR / 'credential-grants/catalog.yaml').read_text()) + grant = next(g for g in catalog['grants'] if g['id'] == 'keycape/client-secret-generate') + ccr = grants.yaml.safe_load((REPO_DIR / 'credential-change-requests' / + 'CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml').read_text()) + role = grants.yaml.safe_load((REPO_DIR / ccr['openbao']['auth']['role_file']).read_text()) + auth = ccr['openbao']['auth'] + self.assertEqual(grant['actors']['allowed_types'], ['human-operator']) + self.assertEqual(grant['actors']['required_subject_binding'], + f"auth/{auth['mount']}/role/{auth['role']}") + self.assertEqual(role['bound_claims'], + {'groups': ['net-kingdom-admins'], '/assurance/level': 'aal2'}) + + def test_postgres_grants_bind_rapp(self) -> None: + catalog = grants.yaml.safe_load( + (REPO_DIR / "credential-grants/catalog.yaml").read_text() + ) + bound = { + grant["id"]: grant.get("rapp_id") + for grant in catalog["grants"] + if str(grant["id"]).startswith("rapp-") + } + self.assertEqual( + bound, + { + "rapp-postgres/audit-core-runtime": "rapp-postgres", + "rapp-postgres/audit-core-migration": "rapp-postgres", + }, + ) + + def test_rapp_prefix_requires_matching_rapp_id(self) -> None: + errors: list[str] = [] + grant = { + "id": "rapp-postgres/audit-core-runtime", + "title": "x", + "description": "x", + "status": "active", + "grant_class": "self-service", + "credential_type": "openbao-database-credential", + "issuer": "openbao", + "audience": "audit-core", + "openbao": { + "token_role": "audit-core-runtime-broker", + "issuer_policy": "credential-broker-audit-core-runtime-issuer", + "policies": ["credential-broker-audit-core-runtime"], + "disallowed_policies": ["root", "platform-admin"], + "mount_paths": ["database/creds/audit-core-runtime"], + }, + "ttl": {"default": "15m", "max": "1h", "renewable": False}, + "actors": {"allowed_types": ["human-operator"]}, + "authorization": { + "flex_auth_required": False, + "purpose_required": True, + }, + "delivery": { + "allowed": ["exec-env"], + "preferred": "exec-env", + "denied": [ + "chat", + "state-hub-body", + "git", + "command-line-token-argument", + "llm-prompt", + ], + }, + "audit": { + "openbao_audit_required": True, + "record_secret_values": False, + }, + "revocation": {"required": True, "by_accessor": True}, + } + grants.validate_grant(grant, 0, {}, errors) + self.assertTrue(any("rapp_id is required" in error for error in errors)) + errors.clear() + grant["rapp_id"] = "rapp-openbao" + grants.validate_grant(grant, 0, {}, errors) + self.assertTrue(any("must equal the grant id prefix" in error for error in errors)) diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/tests/test_keycape_client_secret_generate.py b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/tests/test_keycape_client_secret_generate.py new file mode 100644 index 00000000..bd34ffd2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/candidate/tests/test_keycape_client_secret_generate.py @@ -0,0 +1,302 @@ +"""Offline tests for credential.py generate (RPF-WP-0054-T03) against a fake ``bao``. + +Runs ``credential.py generate`` as a real subprocess, so the silence contract and +the delegation from credential.py are tested end to end. The fake records which +token each call used: KV calls must use the minted grant token, never the +caller's session. +""" +import hashlib +import importlib.util +import shutil +from unittest import mock + +import yaml +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import textwrap +import unittest + +ROOT = Path(__file__).resolve().parents[1] +CLI = ROOT / 'scripts/credential.py' +GRANT_TOKEN = 'hvs.grant-' + 'G7h8' * 6 + +FAKE_BAO = textwrap.dedent(f'''\ + #!/usr/bin/env python3 + import json, os, sys + sf = os.environ['FAKE_BAO_STATE'] + st = json.load(open(sf)) + a = sys.argv[1:] + tok = os.environ.get('BAO_TOKEN', 'caller-session') + st['calls'].append({{'args': a, 'token': tok}}) + def save(): json.dump(st, open(sf, 'w')) + def out(obj): save(); print(json.dumps(obj)); sys.exit(0) + def missing(): save(); sys.stderr.write('No value found at x\\n'); sys.exit(2) + if a[:2] == ['token', 'lookup']: out({{'data': {{'policies': st['caller_policies']}}}}) + if a[:2] == ['token', 'create']: + if st.get('fail_create'): save(); sys.exit(2) + out({{'auth': {{'client_token': '{GRANT_TOKEN}', 'accessor': 'acc'}}}}) + if a[:2] == ['token', 'revoke']: st['revoked'] = tok; save(); sys.exit(0) + if a[0] == 'read': + rest = a[-1].split('/', 2)[2] + if rest not in st['kv']: missing() + n = len(st['kv'][rest]) + out({{'data': {{'current_version': n, 'versions': {{str(i + 1): {{}} for i in range(n)}}}}}}) + if a[0] == 'write': + rest = a[-2].split('/', 2)[2] + body = json.loads(sys.stdin.read()) + st['written'] = body + if st.get('race'): st['kv'][rest] = [{{'x': 1}}]; save(); sys.exit(2) + if st.get('fail_write'): save(); sys.exit(2) + e = st['kv'].setdefault(rest, []) + if body['options']['cas'] != len(e): save(); sys.exit(2) + e.append(body['data']) + out({{'data': {{'version': len(e)}}}}) + save(); sys.exit(9) +''') + +# Exact accepted Forgejo @1.1 shape; materialized only in this fake test. +CASE = '# First KnownSecurityCase of oidc-browser-relying-party 1.1 (NK-WP-0048-T06).\n# Parameters are the request the founder accepted on 2026-10-08\n# (key-cape docs/forgejo-oidc-client-request.yaml, KEY-WP-0038-T01); that\n# acceptance carries over as this case\'s acceptance. One difference: custody is\n# derived by the type (platform/workloads/keycape/forgejo), not the request\'s\n# proposed platform/workloads/forgejo/netkingdom-oidc-client. The founder\n# acknowledged that derivation with the type (memo r4, 2026-10-09).\n# Re-pinned from 1.0 to 1.1 on 2026-10-10 on the founder\'s go, before any\n# apply, so no 1.0 run overlaps (minor change: consumer-enforces-groups).\napiVersion: netkingdom.io/security-case/v0.1\nkind: SecurityCase\nspec:\n id: forgejo-netkingdom-oidc\n type: oidc-browser-relying-party@1.1\n status: materialized\n acceptance:\n at: "2026-10-08"\n record: "key-cape docs/forgejo-oidc-client-request.yaml (status accepted-2026-10-08, KEY-WP-0038-T01); type acknowledged via informed-decision memo nk-wp-0048-t05-oidc-browser-relying-party-r4, hub decision e418263e"\n by:\n - {role: owner, name: "founder (for railiance-apps)"}\n - {role: founder, name: founder}\n owner:\n repo: railiance-apps\n person: founder\n parameters:\n client_id: forgejo\n display_name: Forgejo source forge\n redirect_uris: https://forgejo.coulomb.social/user/oauth2/netkingdom/callback\n allowed_scopes: openid,profile,email,groups\n client_type: confidential\n mfa_required: "true"\n access_groups: forge-admins,forge-coulomb-owners,forge-coulomb-readers\n initial_members: forge-admins=tegwick;forge-coulomb-owners=tegwick\n consuming_service: forgejo\n consumer_owner_repo: railiance-apps\n' + + +class Generate(unittest.TestCase): + def setUp(self): + self.tmp = Path(tempfile.mkdtemp()) + (self.tmp / 'bin').mkdir() + (self.tmp / 'bin/bao').write_text(FAKE_BAO) + (self.tmp / 'bin/bao').chmod(0o755) + self.state = {'calls': [], 'kv': {}, + 'caller_policies': ['default', 'credential-broker-keycape-client-secret-generate-issuer']} + self.write_case(CASE) + + def write_case(self, text): + self.case = self.tmp / 'case.yaml' + self.case.write_text(text) + self.ref = 'forgejo-netkingdom-oidc@sha256:' + hashlib.sha256(text.encode()).hexdigest() + + def run_cli(self, *extra, client_id='forgejo', ref=None, env_extra=None): + (self.tmp / 'state.json').write_text(json.dumps(self.state)) + receipt = self.tmp / f'r{len(list(self.tmp.glob("r*.json")))}.json' + empty_home = self.tmp / 'empty-home' + empty_home.mkdir(exist_ok=True) + env = {'PATH': os.defpath, 'HOME': str(empty_home)} + env.update(PATH=f'{self.tmp / "bin"}:{env["PATH"]}', FAKE_BAO_STATE=str(self.tmp / 'state.json'), + **(env_extra or {})) + proc = subprocess.run([sys.executable, str(CLI), 'generate', '--grant', 'keycape/client-secret-generate', + '--client-id', client_id, '--case-file', str(self.case), + '--case-ref', ref or self.ref, '--receipt', str(receipt), *extra], + env=env, capture_output=True, text=True) + self.assertEqual((proc.stdout, proc.stderr), ('', '')) + self.state = json.loads((self.tmp / 'state.json').read_text()) + text = receipt.read_text() + self.assertNotIn(GRANT_TOKEN, text) + written = self.state.get('written') + if written: + self.assertNotIn(written['data']['CLIENT_SECRET'], text) + return proc.returncode, json.loads(text) + + def kv_calls(self): + return [c for c in self.state['calls'] if c['args'][0] in ('read', 'write')] + + def test_generates_once_with_grant_token_and_revokes_it(self): + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (0, 'generated')) + self.assertEqual(receipt['kv_version'], 1) + self.assertTrue(receipt['grant_token_revoked']) + self.assertEqual(self.state['revoked'], GRANT_TOKEN) + self.assertTrue(all(c['token'] == GRANT_TOKEN for c in self.kv_calls())) + self.assertFalse(any(c['args'][0] == 'read' and '/data/' in c['args'][-1] for c in self.kv_calls())) + value = self.state['written']['data']['CLIENT_SECRET'] + self.assertEqual(len(value), 64) + self.assertEqual(self.state['written']['options'], {'cas': 0}) + + def test_existing_entry_is_never_touched(self): + self.state['kv']['workloads/keycape/forgejo'] = [{'CLIENT_SECRET': 'old'}] + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (3, 'already_present')) + self.assertNotIn('written', self.state) + self.assertTrue(receipt['grant_token_revoked']) + + def test_lost_cas_race_reports_already_present(self): + self.state['race'] = True + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (3, 'already_present_lost_race')) + + def test_dry_run_writes_nothing(self): + code, receipt = self.run_cli('--dry-run') + self.assertEqual((code, receipt['outcome']), (0, 'dry_run_would_generate')) + self.assertNotIn('written', self.state) + + def test_case_checks_refuse_before_any_bao_call(self): + cases = [ + ('case_ref_mismatch', {'ref': 'forgejo-netkingdom-oidc@sha256:' + '0' * 64}), + ('case_client_id_mismatch', {'client_id': 'gitea'}), + ('client_id_invalid_or_reserved', {'client_id': 'secrets-engine-openbao'}), + ('client_id_invalid_or_reserved', {'client_id': '../forgejo'}), + ('case_ref_malformed', {'ref': 'forgejo'}), + ] + for outcome, kwargs in cases: + with self.subTest(outcome=outcome, **kwargs): + self.state['calls'] = [] + code, receipt = self.run_cli(**kwargs) + self.assertEqual((code, receipt['outcome']), (2, outcome)) + self.assertEqual(self.state['calls'], []) + + def test_unmaterialized_or_public_case_refused(self): + for text, outcome in ((CASE.replace('materialized', 'accepted'), 'case_not_materialized'), + (CASE.replace('confidential', 'public'), 'case_client_not_confidential')): + with self.subTest(outcome=outcome): + self.write_case(text) + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, outcome)) + + def test_only_exact_acknowledged_type_version_is_admitted(self): + for type_ref in ('oidc-browser-relying-party@1.0', + 'oidc-browser-relying-party@1.2', + 'oidc-browser-relying-party@1.3', + 'oidc-browser-relying-party@99.0', + 'confidential-service-client@1.1'): + with self.subTest(type_ref=type_ref): + self.write_case(CASE.replace('oidc-browser-relying-party@1.1', type_ref)) + self.state['calls'] = [] + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged')) + self.assertEqual(self.state['calls'], []) + + def test_source_acknowledgment_alone_does_not_admit_12(self): + self.write_case(CASE.replace('oidc-browser-relying-party@1.1', + 'oidc-browser-relying-party@1.2') + + ' consumer_namespace: forgejo\n') + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged')) + self.assertEqual(self.state['calls'], []) + + def test_caller_environment_cannot_select_type_or_source_root(self): + code, receipt = self.run_cli(env_extra={ + 'NETKINGDOM_ROOT': '/nonexistent', + 'WARDEN_NETKINGDOM_ROOT': '/nonexistent', + 'PYTHONPATH': '/nonexistent', + }) + self.assertEqual((code, receipt['outcome']), (0, 'generated')) + + def test_acknowledged_11_still_requires_its_exact_parameters(self): + self.write_case(CASE + ' unexpected_target: forgejo\n') + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed')) + self.assertEqual(self.state['calls'], []) + + def test_future_revision_requires_explicit_owner_binding_and_acknowledgment(self): + # Synthetic owner configuration exercises the extension mechanism only. + # This is not the actual proposed 1.3 type, acknowledgment or parameter pin. + spec = importlib.util.spec_from_file_location( + 'future_fixture_generator', ROOT / 'scripts/keycape_client_secret_generate.py') + broker = importlib.util.module_from_spec(spec) + spec.loader.exec_module(broker) + source = self.tmp / 'synthetic-owner-root' + source.mkdir() + relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1'] + for rel in (relative, *broker.SOURCE_PINS): + dest = source / rel + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest) + typ = source / relative + original = typ.read_bytes() + doc = yaml.safe_load(CASE) + doc['spec']['type'] = 'oidc-browser-relying-party@1.3' + typ.write_bytes(original.replace(b'version: "1.1"', b'version: "1.3"')) + # YAML numeric spelling is owner-authored, never an inferred revision. + type_doc = yaml.safe_load(original) + type_doc['spec']['version'] = '1.3' + typ.write_text(yaml.safe_dump(type_doc)) + binding = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())} + with mock.patch.object(broker, 'NETKINGDOM_ROOT', source): + with self.assertRaises(broker.Exit): + broker.check_acknowledged_case(doc) # no explicit binding + with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, binding): + with self.assertRaises(broker.Exit): + broker.check_acknowledged_case(doc) # no parameter admission + with mock.patch.dict(broker.PARAMETER_PINS, {'1.3': broker.PARAMETER_PINS['1.1']}): + broker.check_acknowledged_case(doc) + type_doc['spec']['status'] = 'proposed' + typ.write_text(yaml.safe_dump(type_doc)) + proposed = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())} + with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, proposed): + with self.assertRaises(broker.Exit) as caught: + broker.check_acknowledged_case(doc) + self.assertEqual(caught.exception.reason, 'case_source_validation_failed') + + def test_wrong_sources_proposed_unknown_versions_and_symlinks_fail_closed(self): + spec = importlib.util.spec_from_file_location( + 'candidate_generator', ROOT / 'scripts/keycape_client_secret_generate.py') + broker = importlib.util.module_from_spec(spec) + spec.loader.exec_module(broker) + source = self.tmp / 'net-kingdom' + source.mkdir() + relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1'] + for rel in (relative, *broker.SOURCE_PINS): + dest = source / rel + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest) + doc = yaml.safe_load(CASE) + with mock.patch.object(broker, 'NETKINGDOM_ROOT', source): + broker.check_acknowledged_case(doc) + producer = source / next(iter(broker.SOURCE_PINS)) + raw = producer.read_bytes() + producer.write_bytes(raw + b'\n# drift\n') + with self.assertRaises(broker.Exit) as caught: + broker.check_acknowledged_case(doc) + self.assertEqual(caught.exception.reason, 'case_source_validation_failed') + producer.write_bytes(raw) + typ = source / relative + raw = typ.read_bytes() + typ.write_bytes(raw.replace(b'status: acknowledged', b'status: proposed')) + # Even a hypothetical owner pin update cannot admit proposed status. + with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, { + doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}): + with self.assertRaises(broker.Exit) as caught: + broker.check_acknowledged_case(doc) + self.assertEqual(caught.exception.reason, 'case_source_validation_failed') + typ.write_bytes(raw + b'\n# source drift\n') + with self.assertRaises(broker.Exit) as caught: + broker.check_acknowledged_case(doc) + self.assertEqual(caught.exception.reason, 'case_source_validation_failed') + typ.unlink() + typ.symlink_to(broker.__file__) + with self.assertRaises(broker.Exit) as caught: + broker.check_acknowledged_case(doc) + self.assertEqual(caught.exception.reason, 'case_source_validation_failed') + for ref in ('oidc-browser-relying-party@1.3', 'oidc-browser-relying-party@99.0'): + doc['spec']['type'] = ref + with self.assertRaises(broker.Exit) as caught: + broker.check_acknowledged_case(doc) + self.assertEqual(caught.exception.reason, 'case_type_unacknowledged') + + def test_exact_accepted_case_still_requires_materialization(self): + self.write_case(CASE.replace('status: materialized', 'status: accepted')) + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, 'case_not_materialized')) + self.assertEqual(self.state['calls'], []) + + def test_agents_root_and_unbound_callers_refused(self): + code, receipt = self.run_cli(env_extra={'WARDEN_AGENT_ID': 'codex'}) + self.assertEqual((code, receipt['outcome']), (2, 'agent_callers_refused')) + self.state['caller_policies'] = ['root'] + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, 'root_refused')) + self.state['caller_policies'] = ['default'] + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (2, 'caller_lacks_issuer_policy')) + self.assertFalse(any(c['args'][:2] == ['token', 'create'] for c in self.state['calls'])) + + def test_write_failure_is_reported_and_token_revoked(self): + self.state['fail_write'] = True + code, receipt = self.run_cli() + self.assertEqual((code, receipt['outcome']), (4, 'write_failed')) + self.assertTrue(receipt['grant_token_revoked']) + + +if __name__ == '__main__': + unittest.main() diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/manifest.json b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/manifest.json new file mode 100644 index 00000000..4843d8cf --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/manifest.json @@ -0,0 +1,115 @@ +{ + "schema": "custodian.platform-broker-candidate.v1", + "candidate_only": true, + "source_commits": { + "railiance-platform": "104ed90f42a9a86bf9fda343b49b65a85c9ee4b9", + "net-kingdom": "befb0bc68cd25b625804fce559476eefd702521d", + "ops-warden": "b593c879831378db7dc62adfabdfedf2d44ddd38" + }, + "files": [ + { + "path": "scripts/keycape_client_secret_generate.py", + "input_sha256": "9576cd211cfc06526ec4cc9bd0a5b06f0f59287a68931bb2d6e3592a951ddc7d", + "output_sha256": "da80947a2c28df36b37e711d1e6d2f66b4c0434082e7bf187da3d3cb692cec28" + }, + { + "path": "credential-grants/catalog.yaml", + "input_sha256": "cb70165f80c58245d4231b4e712a4b322c51a6b168513dc839ad4c0234027047", + "output_sha256": "7cfe12970a57ab75cb91c7c5024a4dcfc28ffae4b31a1afaf4d0cedabbb22886" + }, + { + "path": "tests/test_keycape_client_secret_generate.py", + "input_sha256": "86063ef86fb8f43203659752895906838dc572f22512336464da9c74306c09d0", + "output_sha256": "5906e32c7fe0db09c1f7db48ad61226497e062f38ebfa91e8a03be89272765c0" + }, + { + "path": "tests/test_credential_grants.py", + "input_sha256": "aec72c194df50fb37ba6a0ccc28685b041441f2bafd2c174a1a91eb0ae7220cc", + "output_sha256": "5c0cfcb039c0220e59dac7986d960bb568593a3a9281ff2557bb6c38de31e68a" + } + ], + "admitted_type_versions": [ + "1.1" + ], + "admitted_13": false, + "tests": { + "passed": 19, + "baseline_expected_failures": 2, + "isolation": "bwrap unshare-net; only /usr /lib /lib64 plus scratch; no host home/config; empty HOME; allowlisted env; fake bao only" + }, + "review_inputs": [ + { + "repo": "railiance-platform", + "path": "AGENTS.md", + "sha256": "8d2f1a165e3f4eecf2e6e1d6a19c4bac9e8dcb64a76650e7185d55722f233c8b" + }, + { + "repo": "railiance-platform", + "path": "INTENT.md", + "sha256": "12c6540735af591f7a4a9fe85ff10d3cd00eec55b5b6b1ddf148abf2d3ac9a9c" + }, + { + "repo": "railiance-platform", + "path": "credential-change-requests/CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml", + "sha256": "84e44202d7d2dfad9955f00b461b587995ba275b05ff29fef94c085810758560" + }, + { + "repo": "railiance-platform", + "path": "openbao/auth/credential-broker-keycape-client-secret-generate-oidc-role.json", + "sha256": "ffcf056bdd5d39797efd589197e75f880ecbf784c23eb0a1516f64fe4f430dfb" + }, + { + "repo": "net-kingdom", + "path": "security-case-types/oidc-browser-relying-party/type.yaml", + "sha256": "f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c" + }, + { + "repo": "net-kingdom", + "path": "security-case-type-revisions/1.2/oidc-browser-relying-party/type.yaml", + "sha256": "9e60ff569fb0d860a2bbde7bbb0991a54d4b0688f62b56df0fa5d161fd624578" + }, + { + "repo": "net-kingdom", + "path": "security-case-type-revisions/1.2/oidc-browser-relying-party/decision/acknowledgment.json", + "sha256": "2dcc220febf125a17b3769f7014f18b63979d7121e0f7056a2b0d8b1ae6031b0" + }, + { + "repo": "net-kingdom", + "path": "security-case-type-revisions/1.3/oidc-browser-relying-party/type.yaml", + "sha256": "ef02ddae3167bdd1558193a5f6db0379fb8cc84207531706d5ef35caabc3684f" + }, + { + "repo": "net-kingdom", + "path": "tools/security-case-types/security_case_validator.py", + "sha256": "1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b" + }, + { + "repo": "net-kingdom", + "path": "tools/security-scenario-composer/security_scenario_composer.py", + "sha256": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f" + }, + { + "repo": "net-kingdom", + "path": "tools/playbook-capability-contract/playbook_contract_validator.py", + "sha256": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d" + }, + { + "repo": "ops-warden", + "path": "src/warden/case_inputs.py", + "sha256": "1334b0cd9897e763cfea2d306e997c659f3f284ecc3f9612140c2104835da7c7" + }, + { + "repo": "ops-warden", + "path": "src/warden/case_plan.py", + "sha256": "9352ae8683d1181980c4ff38524582e4be448d33c85b558f60582c1e5bb5bbaa" + }, + { + "repo": "ops-warden", + "path": "src/warden/case_run.py", + "sha256": "1bc73e30ac718fd75bbaff2a739495fc051aeaec73802ac71f0583cb2d53ab0a" + } + ], + "admitted_12": false, + "rpf_13_t1_fully_satisfied": false, + "authenticated_case_admission": "open; reference/schema validation is insufficient" +} diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/patch-check.log b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/patch-check.log new file mode 100644 index 00000000..3a070c5c --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/patch-check.log @@ -0,0 +1 @@ +git apply --check against exact four baseline files: 0 diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/platform-source.patch b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/platform-source.patch new file mode 100644 index 00000000..24c69b00 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/platform-source.patch @@ -0,0 +1,319 @@ +--- a/scripts/keycape_client_secret_generate.py ++++ b/scripts/keycape_client_secret_generate.py +@@ -16,7 +16,7 @@ + the broker issuer policy for this grant. + 2. Re-derive the case reference from ``--case-file`` (sha256 over its exact + bytes) and require: it equals ``--case-ref``; type +- ``oidc-browser-relying-party@1.1``; status ``materialized``; client_type ++ an exactly source-pinned acknowledged browser revision; status ``materialized``; client_type + ``confidential``; ``parameters.client_id`` equals ``--client-id``, which + matches the type's pattern and is not a reserved entry. + 3. Mint a 5-minute grant token through token role +@@ -46,6 +46,7 @@ + import secrets + import subprocess + import sys ++import tempfile + import urllib.request + + import yaml +@@ -57,7 +58,39 @@ + PREFIX = 'workloads/keycape' + RESERVED = {'secrets-engine-openbao'} + FIELD = 'CLIENT_SECRET' +-CASE_TYPE = 'oidc-browser-relying-party@1.1' ++# Owner configuration, never a caller-supplied root or a version search/fallback. ++# The sibling source checkout must contain these exact reviewed bytes. ++NETKINGDOM_ROOT = Path(__file__).resolve().parents[2] / 'net-kingdom' ++ACKNOWLEDGED_TYPES = { ++ 'oidc-browser-relying-party@1.1': ( ++ 'security-case-types/oidc-browser-relying-party/type.yaml', ++ 'f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c'), ++} ++# Same authenticated producer and parameter projection as ops-warden case_inputs ++# at b593c879, restricted to this broker's currently admitted 1.1. Source-only ++# acknowledgment (including 1.2) never expands broker admission. A future 1.3 ++# entry requires real acknowledgment and a separately reviewed admission binding. ++# Neither source status nor caller-supplied YAML expands this set. ++SOURCE_PINS = { ++ 'tools/security-case-types/security_case_validator.py': ++ '1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b', ++ 'tools/security-scenario-composer/security_scenario_composer.py': ++ '9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f', ++ 'tools/playbook-capability-contract/playbook_contract_validator.py': ++ '2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d', ++} ++PARAMETER_PINS = { ++ '1.1': '9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c', ++} ++_VALIDATE = """ ++import json, sys ++from pathlib import Path ++sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types')) ++import security_case_validator as v ++case, typ = json.load(sys.stdin) ++issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False) ++print('valid' if not v.has_errors(issues) else 'invalid') ++""" + # oidc-browser-relying-party@1.1 parameter client_id. + CLIENT_ID_RE = re.compile(r'^[a-z0-9][a-z0-9-]{1,62}$') + CASE_REF_RE = re.compile(r'^[a-z0-9][a-z0-9-]*@sha256:[0-9a-f]{64}$') +@@ -93,6 +126,53 @@ + return json.loads(result.stdout) + except ValueError: + raise Exit(code, 'unparseable_bao_output') from None ++ ++ ++def authenticated_source(relative, expected): ++ path = NETKINGDOM_ROOT / relative ++ # Refuse symlinks at every selected component, including the configured root. ++ if any(part.is_symlink() for part in (path, *path.parents)): ++ raise ValueError ++ raw = path.read_bytes() ++ if hashlib.sha256(raw).hexdigest() != expected: ++ raise ValueError ++ return raw ++ ++ ++def check_acknowledged_case(doc): ++ # Authenticates the selected TYPE/producer bytes and validates case schema. ++ # The caller's self-hashed case reference is not authenticated case admission ++ # or owner acceptance. Those require independent Warden/native evidence; ++ # this source-only hardening does not complete the real admission binding. ++ binding = ACKNOWLEDGED_TYPES.get(doc['spec'].get('type')) ++ if binding is None: ++ raise Exit(2, 'case_type_unacknowledged') ++ try: ++ typ = yaml.safe_load(authenticated_source(*binding)) ++ spec = typ['spec'] ++ version = str(spec['version']) ++ fingerprint = hashlib.sha256(json.dumps( ++ spec['parameters'], sort_keys=True, separators=(',', ':')).encode()).hexdigest() ++ if (spec['status'] not in ('acknowledged', 'implemented') ++ or f"{spec['id']}@{version}" != doc['spec']['type'] ++ or PARAMETER_PINS.get(version) != fingerprint): ++ raise ValueError ++ with tempfile.TemporaryDirectory(prefix='platform-case-validator-') as directory: ++ for relative, expected in SOURCE_PINS.items(): ++ raw = authenticated_source(relative, expected) ++ dest = Path(directory) / relative ++ dest.parent.mkdir(parents=True, exist_ok=True) ++ dest.write_bytes(raw) ++ result = subprocess.run( ++ [sys.executable, '-I', '-c', _VALIDATE, directory], ++ input=json.dumps([doc, typ]), text=True, ++ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ++ env={}, timeout=10, check=True) ++ if result.stdout != 'valid\n': ++ raise ValueError ++ except (OSError, ValueError, TypeError, KeyError, yaml.YAMLError, ++ subprocess.SubprocessError): ++ raise Exit(2, 'case_source_validation_failed') from None + + + def check_case(args, receipt): +@@ -112,14 +192,15 @@ + derived = f"{spec.get('id')}@sha256:{hashlib.sha256(raw).hexdigest()}" + if derived != args.case_ref: + raise Exit(2, 'case_ref_mismatch') +- if spec.get('type') != CASE_TYPE: +- raise Exit(2, 'case_type_mismatch') ++ if not isinstance(spec.get('type'), str): ++ raise Exit(2, 'case_type_unacknowledged') + if spec.get('status') != 'materialized': + raise Exit(2, 'case_not_materialized') + if params.get('client_type') != 'confidential': + raise Exit(2, 'case_client_not_confidential') + if params.get('client_id') != args.client_id: + raise Exit(2, 'case_client_id_mismatch') ++ check_acknowledged_case(doc) + receipt['case_verified'] = True + + +--- a/credential-grants/catalog.yaml ++++ b/credential-grants/catalog.yaml +@@ -137,9 +137,9 @@ + requires_human_above: 5m + actors: + allowed_types: +- - approved-agent + - human-operator +- required_subject_binding: keycape-or-kubernetes-service-account ++ required_subject_binding: auth/netkingdom/role/credential-broker-keycape-client-secret-generate ++ # CCR-2026-0043: human net-kingdom-admins membership and /assurance/level=aal2. + authorization: + flex_auth_required: false + flex_auth_mode: optional-preflight +--- a/tests/test_keycape_client_secret_generate.py ++++ b/tests/test_keycape_client_secret_generate.py +@@ -6,6 +6,11 @@ + caller's session. + """ + import hashlib ++import importlib.util ++import shutil ++from unittest import mock ++ ++import yaml + import json + import os + from pathlib import Path +@@ -75,7 +80,9 @@ + def run_cli(self, *extra, client_id='forgejo', ref=None, env_extra=None): + (self.tmp / 'state.json').write_text(json.dumps(self.state)) + receipt = self.tmp / f'r{len(list(self.tmp.glob("r*.json")))}.json' +- env = {k: v for k, v in os.environ.items() if k not in ('BAO_TOKEN', 'VAULT_TOKEN', 'WARDEN_AGENT_ID')} ++ empty_home = self.tmp / 'empty-home' ++ empty_home.mkdir(exist_ok=True) ++ env = {'PATH': os.defpath, 'HOME': str(empty_home)} + env.update(PATH=f'{self.tmp / "bin"}:{env["PATH"]}', FAKE_BAO_STATE=str(self.tmp / 'state.json'), + **(env_extra or {})) + proc = subprocess.run([sys.executable, str(CLI), 'generate', '--grant', 'keycape/client-secret-generate', +@@ -149,14 +156,123 @@ + def test_only_exact_acknowledged_type_version_is_admitted(self): + for type_ref in ('oidc-browser-relying-party@1.0', + 'oidc-browser-relying-party@1.2', ++ 'oidc-browser-relying-party@1.3', + 'oidc-browser-relying-party@99.0', + 'confidential-service-client@1.1'): + with self.subTest(type_ref=type_ref): + self.write_case(CASE.replace('oidc-browser-relying-party@1.1', type_ref)) + self.state['calls'] = [] + code, receipt = self.run_cli() +- self.assertEqual((code, receipt['outcome']), (2, 'case_type_mismatch')) ++ self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged')) + self.assertEqual(self.state['calls'], []) ++ ++ def test_source_acknowledgment_alone_does_not_admit_12(self): ++ self.write_case(CASE.replace('oidc-browser-relying-party@1.1', ++ 'oidc-browser-relying-party@1.2') ++ + ' consumer_namespace: forgejo\n') ++ code, receipt = self.run_cli() ++ self.assertEqual((code, receipt['outcome']), (2, 'case_type_unacknowledged')) ++ self.assertEqual(self.state['calls'], []) ++ ++ def test_caller_environment_cannot_select_type_or_source_root(self): ++ code, receipt = self.run_cli(env_extra={ ++ 'NETKINGDOM_ROOT': '/nonexistent', ++ 'WARDEN_NETKINGDOM_ROOT': '/nonexistent', ++ 'PYTHONPATH': '/nonexistent', ++ }) ++ self.assertEqual((code, receipt['outcome']), (0, 'generated')) ++ ++ def test_acknowledged_11_still_requires_its_exact_parameters(self): ++ self.write_case(CASE + ' unexpected_target: forgejo\n') ++ code, receipt = self.run_cli() ++ self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed')) ++ self.assertEqual(self.state['calls'], []) ++ ++ def test_future_revision_requires_explicit_owner_binding_and_acknowledgment(self): ++ # Synthetic owner configuration exercises the extension mechanism only. ++ # This is not the actual proposed 1.3 type, acknowledgment or parameter pin. ++ spec = importlib.util.spec_from_file_location( ++ 'future_fixture_generator', ROOT / 'scripts/keycape_client_secret_generate.py') ++ broker = importlib.util.module_from_spec(spec) ++ spec.loader.exec_module(broker) ++ source = self.tmp / 'synthetic-owner-root' ++ source.mkdir() ++ relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1'] ++ for rel in (relative, *broker.SOURCE_PINS): ++ dest = source / rel ++ dest.parent.mkdir(parents=True, exist_ok=True) ++ shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest) ++ typ = source / relative ++ original = typ.read_bytes() ++ doc = yaml.safe_load(CASE) ++ doc['spec']['type'] = 'oidc-browser-relying-party@1.3' ++ typ.write_bytes(original.replace(b'version: "1.1"', b'version: "1.3"')) ++ # YAML numeric spelling is owner-authored, never an inferred revision. ++ type_doc = yaml.safe_load(original) ++ type_doc['spec']['version'] = '1.3' ++ typ.write_text(yaml.safe_dump(type_doc)) ++ binding = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())} ++ with mock.patch.object(broker, 'NETKINGDOM_ROOT', source): ++ with self.assertRaises(broker.Exit): ++ broker.check_acknowledged_case(doc) # no explicit binding ++ with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, binding): ++ with self.assertRaises(broker.Exit): ++ broker.check_acknowledged_case(doc) # no parameter admission ++ with mock.patch.dict(broker.PARAMETER_PINS, {'1.3': broker.PARAMETER_PINS['1.1']}): ++ broker.check_acknowledged_case(doc) ++ type_doc['spec']['status'] = 'proposed' ++ typ.write_text(yaml.safe_dump(type_doc)) ++ proposed = {doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())} ++ with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, proposed): ++ with self.assertRaises(broker.Exit) as caught: ++ broker.check_acknowledged_case(doc) ++ self.assertEqual(caught.exception.reason, 'case_source_validation_failed') ++ ++ def test_wrong_sources_proposed_unknown_versions_and_symlinks_fail_closed(self): ++ spec = importlib.util.spec_from_file_location( ++ 'candidate_generator', ROOT / 'scripts/keycape_client_secret_generate.py') ++ broker = importlib.util.module_from_spec(spec) ++ spec.loader.exec_module(broker) ++ source = self.tmp / 'net-kingdom' ++ source.mkdir() ++ relative, digest = broker.ACKNOWLEDGED_TYPES['oidc-browser-relying-party@1.1'] ++ for rel in (relative, *broker.SOURCE_PINS): ++ dest = source / rel ++ dest.parent.mkdir(parents=True, exist_ok=True) ++ shutil.copyfile(broker.NETKINGDOM_ROOT / rel, dest) ++ doc = yaml.safe_load(CASE) ++ with mock.patch.object(broker, 'NETKINGDOM_ROOT', source): ++ broker.check_acknowledged_case(doc) ++ producer = source / next(iter(broker.SOURCE_PINS)) ++ raw = producer.read_bytes() ++ producer.write_bytes(raw + b'\n# drift\n') ++ with self.assertRaises(broker.Exit) as caught: ++ broker.check_acknowledged_case(doc) ++ self.assertEqual(caught.exception.reason, 'case_source_validation_failed') ++ producer.write_bytes(raw) ++ typ = source / relative ++ raw = typ.read_bytes() ++ typ.write_bytes(raw.replace(b'status: acknowledged', b'status: proposed')) ++ # Even a hypothetical owner pin update cannot admit proposed status. ++ with mock.patch.dict(broker.ACKNOWLEDGED_TYPES, { ++ doc['spec']['type']: (relative, hashlib.sha256(typ.read_bytes()).hexdigest())}): ++ with self.assertRaises(broker.Exit) as caught: ++ broker.check_acknowledged_case(doc) ++ self.assertEqual(caught.exception.reason, 'case_source_validation_failed') ++ typ.write_bytes(raw + b'\n# source drift\n') ++ with self.assertRaises(broker.Exit) as caught: ++ broker.check_acknowledged_case(doc) ++ self.assertEqual(caught.exception.reason, 'case_source_validation_failed') ++ typ.unlink() ++ typ.symlink_to(broker.__file__) ++ with self.assertRaises(broker.Exit) as caught: ++ broker.check_acknowledged_case(doc) ++ self.assertEqual(caught.exception.reason, 'case_source_validation_failed') ++ for ref in ('oidc-browser-relying-party@1.3', 'oidc-browser-relying-party@99.0'): ++ doc['spec']['type'] = ref ++ with self.assertRaises(broker.Exit) as caught: ++ broker.check_acknowledged_case(doc) ++ self.assertEqual(caught.exception.reason, 'case_type_unacknowledged') + + def test_exact_accepted_case_still_requires_materialization(self): + self.write_case(CASE.replace('status: materialized', 'status: accepted')) +--- a/tests/test_credential_grants.py ++++ b/tests/test_credential_grants.py +@@ -24,6 +24,20 @@ + for index, grant in enumerate(catalog["grants"]): + grants.validate_grant(grant, index, catalog, errors) + self.assertEqual(errors, []) ++ ++ def test_keycape_generator_actor_matches_applied_issuer_binding(self) -> None: ++ catalog = grants.yaml.safe_load( ++ (REPO_DIR / 'credential-grants/catalog.yaml').read_text()) ++ grant = next(g for g in catalog['grants'] if g['id'] == 'keycape/client-secret-generate') ++ ccr = grants.yaml.safe_load((REPO_DIR / 'credential-change-requests' / ++ 'CCR-2026-0043-keycape-client-secret-generate-issuer-binding.yaml').read_text()) ++ role = grants.yaml.safe_load((REPO_DIR / ccr['openbao']['auth']['role_file']).read_text()) ++ auth = ccr['openbao']['auth'] ++ self.assertEqual(grant['actors']['allowed_types'], ['human-operator']) ++ self.assertEqual(grant['actors']['required_subject_binding'], ++ f"auth/{auth['mount']}/role/{auth['role']}") ++ self.assertEqual(role['bound_claims'], ++ {'groups': ['net-kingdom-admins'], '/assurance/level': 'aal2'}) + + def test_postgres_grants_bind_rapp(self) -> None: + catalog = grants.yaml.safe_load( diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/reproduce-baseline.py b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/reproduce-baseline.py new file mode 100644 index 00000000..4ec776ac --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/reproduce-baseline.py @@ -0,0 +1,16 @@ +from pathlib import Path +import subprocess +root=Path('/tmp/platform-broker-r1/railiance-platform') +files=['scripts/keycape_client_secret_generate.py','credential-grants/catalog.yaml'] +saved={p:(root/p).read_bytes() for p in files} +try: + for p in files: + raw=subprocess.check_output(['git','-C','/home/worsch/railiance-platform','show','104ed90f42a9a86bf9fda343b49b65a85c9ee4b9:'+p]) + (root/p).write_bytes(raw) + command=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--bind','/tmp/platform-broker-r1','/tmp/platform-broker-r1','--chdir',str(root/'tests'),'--','env','-i','PATH=/usr/local/bin:/usr/bin:/bin','HOME=/tmp/platform-broker-r1/empty-home','PYTHONDONTWRITEBYTECODE=1','python3','-m','unittest','test_keycape_client_secret_generate.Generate.test_acknowledged_11_still_requires_its_exact_parameters','test_credential_grants.CredentialGrantBindTests.test_keycape_generator_actor_matches_applied_issuer_binding','-v'] + result=subprocess.run(command,capture_output=True,text=True) + log=Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/baseline-reproduction.log') + log.write_text('Baseline Platform 104ed90f with candidate regressions; expected two failures.\n'+result.stdout+result.stderr) + assert result.returncode==1 and 'FAIL: test_acknowledged_11' in result.stderr and 'FAIL: test_keycape_generator_actor' in result.stderr +finally: + for p,raw in saved.items(): (root/p).write_bytes(raw) diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/run-tests.sh b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/run-tests.sh new file mode 100644 index 00000000..9bc6bba8 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/run-tests.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu +bwrap --unshare-net --ro-bind /usr /usr --ro-bind /lib /lib --ro-bind /lib64 /lib64 --symlink usr/bin /bin --proc /proc --dev /dev --tmpfs /tmp --bind /tmp/platform-broker-r1 /tmp/platform-broker-r1 --chdir /tmp/platform-broker-r1/railiance-platform -- env -i PATH=/usr/local/bin:/usr/bin:/bin HOME=/tmp/platform-broker-r1/empty-home PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_keycape_client_secret_generate.py -v +bwrap --unshare-net --ro-bind /usr /usr --ro-bind /lib /lib --ro-bind /lib64 /lib64 --symlink usr/bin /bin --proc /proc --dev /dev --tmpfs /tmp --bind /tmp/platform-broker-r1 /tmp/platform-broker-r1 --chdir /tmp/platform-broker-r1/railiance-platform -- env -i PATH=/usr/local/bin:/usr/bin:/bin HOME=/tmp/platform-broker-r1/empty-home PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_credential_grants.py -v diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/tests.log b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/tests.log new file mode 100644 index 00000000..80f912c2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/tests.log @@ -0,0 +1,29 @@ +test_acknowledged_11_still_requires_its_exact_parameters (test_keycape_client_secret_generate.Generate.test_acknowledged_11_still_requires_its_exact_parameters) ... ok +test_agents_root_and_unbound_callers_refused (test_keycape_client_secret_generate.Generate.test_agents_root_and_unbound_callers_refused) ... ok +test_caller_environment_cannot_select_type_or_source_root (test_keycape_client_secret_generate.Generate.test_caller_environment_cannot_select_type_or_source_root) ... ok +test_case_checks_refuse_before_any_bao_call (test_keycape_client_secret_generate.Generate.test_case_checks_refuse_before_any_bao_call) ... ok +test_dry_run_writes_nothing (test_keycape_client_secret_generate.Generate.test_dry_run_writes_nothing) ... ok +test_exact_accepted_case_still_requires_materialization (test_keycape_client_secret_generate.Generate.test_exact_accepted_case_still_requires_materialization) ... ok +test_existing_entry_is_never_touched (test_keycape_client_secret_generate.Generate.test_existing_entry_is_never_touched) ... ok +test_future_revision_requires_explicit_owner_binding_and_acknowledgment (test_keycape_client_secret_generate.Generate.test_future_revision_requires_explicit_owner_binding_and_acknowledgment) ... ok +test_generates_once_with_grant_token_and_revokes_it (test_keycape_client_secret_generate.Generate.test_generates_once_with_grant_token_and_revokes_it) ... ok +test_lost_cas_race_reports_already_present (test_keycape_client_secret_generate.Generate.test_lost_cas_race_reports_already_present) ... ok +test_only_exact_acknowledged_type_version_is_admitted (test_keycape_client_secret_generate.Generate.test_only_exact_acknowledged_type_version_is_admitted) ... ok +test_source_acknowledgment_alone_does_not_admit_12 (test_keycape_client_secret_generate.Generate.test_source_acknowledgment_alone_does_not_admit_12) ... ok +test_unmaterialized_or_public_case_refused (test_keycape_client_secret_generate.Generate.test_unmaterialized_or_public_case_refused) ... ok +test_write_failure_is_reported_and_token_revoked (test_keycape_client_secret_generate.Generate.test_write_failure_is_reported_and_token_revoked) ... ok +test_wrong_sources_proposed_unknown_versions_and_symlinks_fail_closed (test_keycape_client_secret_generate.Generate.test_wrong_sources_proposed_unknown_versions_and_symlinks_fail_closed) ... ok + +---------------------------------------------------------------------- +Ran 15 tests in 6.051s + +OK +test_catalog_validates (test_credential_grants.CredentialGrantBindTests.test_catalog_validates) ... ok +test_keycape_generator_actor_matches_applied_issuer_binding (test_credential_grants.CredentialGrantBindTests.test_keycape_generator_actor_matches_applied_issuer_binding) ... ok +test_postgres_grants_bind_rapp (test_credential_grants.CredentialGrantBindTests.test_postgres_grants_bind_rapp) ... ok +test_rapp_prefix_requires_matching_rapp_id (test_credential_grants.CredentialGrantBindTests.test_rapp_prefix_requires_matching_rapp_id) ... ok + +---------------------------------------------------------------------- +Ran 4 tests in 0.077s + +OK diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/workplan-source-draft.md b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/workplan-source-draft.md new file mode 100644 index 00000000..cc41ff3e --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-followthrough-r1/workplan-source-draft.md @@ -0,0 +1,94 @@ +--- +id: RPF-WP-0060 +type: workplan +title: "KeyCape 1.3 Platform capability and broker follow-through" +domain: financials +repo: railiance-platform +status: proposed +owner: codex +topic_slug: platform-secret-custody +created: "2026-10-17" +updated: "2026-10-17" +flavor: implementation +origin: NK-WP-0048-T16 +--- + +Source draft only. RPF-WP-0060 was the next free number inspected at Platform +104ed90f42a9a86bf9fda343b49b65a85c9ee4b9. The coordinator must recheck and +allocate it in Platform before publication. No Hub UUIDs are manually assigned. +The owner-review RPF-1.3-T1 through T4 strings were review labels, not task IDs. + +The review is +`the-custodian/docs/changes/netkingdom-target-2026-10-17/nk48-1.3-owner-review/owner-review-railiance-platform.json`. +The candidate for T01/T02 is retained alongside this draft in +`platform-broker-followthrough-r1/`. This draft does not record owner acceptance, +a 1.3 acknowledgment, a case acceptance, a native grant, or completion of source +integration. No live policy change belongs to T01/T02. + +## Exact acknowledged broker admission (review label RPF-1.3-T1) + +```task +id: RPF-WP-0060-T01 +status: todo +priority: high +``` + +Review and integrate the isolated broker candidate with exact known acknowledged +1.1 source selection, pinned validator closure and parameter fingerprints, +case validation, and refusal before native calls. Run the offline regressions. +1.2 remains refused despite source-only acknowledgment. +1.3 remains refused until its actual acknowledgment and exact source selection +are established and separately reviewed; no speculative 1.3 pin is present. +Maintain the case-bytes reference check and materialized/confidential target +binding. Independently establish authenticated case admission/owner acceptance; +self-hashed caller case bytes and schema validation do not establish it. That +real admission binding remains open and prevents claiming this task complete. Review the fixed sibling source-installation requirement before merge. + +## Correct the issuer catalog statement (review label RPF-1.3-T2) + +```task +id: RPF-WP-0060-T02 +status: todo +priority: high +``` + +Review and integrate the metadata-only catalog correction to human-operator and +`auth/netkingdom/role/credential-broker-keycape-client-secret-generate`, matching +applied CCR-2026-0043. Run the regression against the role source, issuer group +and aal2 binding. No role, policy, credential or live grant changes. + +## Real mint and rollback capability bindings (review label RPF-1.3-T3) + +```task +id: RPF-WP-0060-T03 +status: todo +priority: high +``` + +Design and implement the generator-to-custody-mint.v1 projection, authenticated +publication, grant accessor/revocation evidence and caller-session revocation. +Destroy requires attended platform-admin authority with exact version-1, +created-time equality and no-other-version compare-before-delete. Preserve +UNCERTAIN on unavailable authority or revocation failure. Obtain required owner +review and authority before any native operation. Use offline adapter tests. +The source candidate does not implement or satisfy this task. + +## Delivery lane and capability (review label RPF-1.3-T4) + +```task +id: RPF-WP-0060-T04 +status: todo +priority: high +``` + +Define platform-secret-delivery with human net-kingdom-admins/aal2 sessions and +contained named ExternalSecret authority in sso, no Secret reads. Implement +case-derived spec equality for NO_CHANGE, Ready=True status gate, exact metadata +receipts, and uid/resourceVersion preconditioned removal. ESO reads custody; +this human lane creates the ExternalSecret. Route the proposed catalog entry +to ops-warden through an explicitly authorized owner handoff. Native authority +and catalog acceptance remain separate gates. The candidate implements none. + +Owner-descriptor-acceptance is still gated on completed owner obligations and +review. Publishing this workplan does not satisfy those obligations. Any +unfinished actionable work must remain in these tasks or a live residual record. diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/baseline-reproduction.log b/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/baseline-reproduction.log new file mode 100644 index 00000000..fb087f63 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/baseline-reproduction.log @@ -0,0 +1,42 @@ +Baseline Platform 104ed90f with candidate regressions; expected two failures. +test_acknowledged_11_still_requires_its_exact_parameters (test_keycape_client_secret_generate.Generate.test_acknowledged_11_still_requires_its_exact_parameters) ... FAIL +test_keycape_generator_actor_matches_applied_issuer_binding (test_credential_grants.CredentialGrantBindTests.test_keycape_generator_actor_matches_applied_issuer_binding) ... FAIL + +====================================================================== +FAIL: test_acknowledged_11_still_requires_its_exact_parameters (test_keycape_client_secret_generate.Generate.test_acknowledged_11_still_requires_its_exact_parameters) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tmp/platform-broker-independent-r1/railiance-platform/tests/test_keycape_client_secret_generate.py", line 188, in test_acknowledged_11_still_requires_its_exact_parameters + self.assertEqual((code, receipt['outcome']), (2, 'case_source_validation_failed')) +AssertionError: Tuples differ: (0, 'generated') != (2, 'case_source_validation_failed') + +First differing element 0: +0 +2 + +- (0, 'generated') ++ (2, 'case_source_validation_failed') + +====================================================================== +FAIL: test_keycape_generator_actor_matches_applied_issuer_binding (test_credential_grants.CredentialGrantBindTests.test_keycape_generator_actor_matches_applied_issuer_binding) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tmp/platform-broker-independent-r1/railiance-platform/tests/test_credential_grants.py", line 36, in test_keycape_generator_actor_matches_applied_issuer_binding + self.assertEqual(grant['actors']['allowed_types'], ['human-operator']) +AssertionError: Lists differ: ['approved-agent', 'human-operator'] != ['human-operator'] + +First differing element 0: +'approved-agent' +'human-operator' + +First list contains 1 additional elements. +First extra element 1: +'human-operator' + +- ['approved-agent', 'human-operator'] ++ ['human-operator'] + +---------------------------------------------------------------------- +Ran 2 tests in 0.361s + +FAILED (failures=2) diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/independent-probes.py b/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/independent-probes.py new file mode 100644 index 00000000..b95527ef --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/independent-probes.py @@ -0,0 +1,58 @@ +"""Offline reviewer controls; fake Bao fixture only, run inside supplied bwrap boundary.""" +import hashlib, importlib.util, json, pathlib, sys +ROOT=pathlib.Path('/tmp/platform-broker-independent-r1') +sys.path.insert(0,str(ROOT/'railiance-platform/tests')) +import test_keycape_client_secret_generate as fixture +spec=importlib.util.spec_from_file_location('reviewed_broker',ROOT/'railiance-platform/scripts/keycape_client_secret_generate.py') +broker=importlib.util.module_from_spec(spec);spec.loader.exec_module(broker) +results=[] +def cli_probe(name, change=None, env=None, accepted=False): + t=fixture.Generate();t.setUp() + if change:t.write_case(change(t.case.read_text())) + code,receipt=t.run_cli(env_extra=env) + if accepted: + assert (code,receipt['outcome'])==(0,'generated'),receipt + else: + assert code!=0 and t.state['calls']==[],(code,receipt,t.state) + results.append({'name':name,'code':code,'outcome':receipt['outcome'],'bao_calls':len(t.state['calls'])}) +# Every executable producer pin is independently necessary at the real CLI boundary. +for relative in broker.SOURCE_PINS: + path=ROOT/'net-kingdom'/relative;raw=path.read_bytes() + try: + path.write_bytes(raw+b'\n# independent source mutation\n') + cli_probe('producer drift: '+relative) + path.unlink() + cli_probe('producer absent: '+relative) + finally:path.write_bytes(raw) +# Layout requirement and symlink root refusal, without any runtime root overrides. +nk=ROOT/'net-kingdom';saved=ROOT/'net-kingdom-saved';nk.rename(saved) +try: + cli_probe('fixed sibling missing') + nk.symlink_to(saved,target_is_directory=True) + cli_probe('fixed sibling symlink refused') +finally: + if nk.is_symlink():nk.unlink() + saved.rename(nk) +# No ambient producer import, site hook or YAML dependency from case directory. +poison=ROOT/'poison';poison.mkdir(exist_ok=True) +(poison/'yaml.py').write_text("raise RuntimeError('caller yaml imported')\n") +(poison/'sitecustomize.py').write_text("raise RuntimeError('caller site hook imported')\n") +cli_probe('honest case with hostile source-root environment',env={'NETKINGDOM_ROOT':str(poison),'WARDEN_NETKINGDOM_ROOT':str(poison)},accepted=True) +# The broker itself is launched normally; the pinned producer process is the isolated one. +# Prove -I and env={} on that actual subprocess, then let it run normally. +import unittest.mock as mock +original=broker.subprocess.run +calls=[] +def observe(argv,**kwargs): + calls.append((argv,kwargs)) + return original(argv,**kwargs) +with mock.patch.object(broker.subprocess,'run',side_effect=observe): + broker.check_acknowledged_case(fixture.yaml.safe_load(fixture.CASE)) +assert len(calls)==1 and calls[0][0][1:3]==['-I','-c'] and calls[0][1]['env']=={} +assert 'PYTHONPATH' not in calls[0][1]['env'] +results.append({'name':'validator exact interpreter isolation','result':'-I and empty env observed on actual invocation'}) +# Authenticated case acceptance remains absent: structurally plausible invented metadata passes. +cli_probe('fabricated acceptance record (known admission limit)',change=lambda s:s.replace('record: "key-cape docs/forgejo-oidc-client-request.yaml (status accepted-2026-10-08, KEY-WP-0038-T01); type acknowledged via informed-decision memo nk-wp-0048-t05-oidc-browser-relying-party-r4, hub decision e418263e"','record: "fabricated reviewer test; no owner acceptance"'),accepted=True) +cli_probe('missing acceptance metadata refused',change=lambda s:s.replace(' acceptance:\n',' removed_acceptance:\n')) +cli_probe('parameter pattern refused',change=lambda s:s.replace(' redirect_uris: https://forgejo.coulomb.social/user/oauth2/netkingdom/callback',' redirect_uris: http://invalid.example/callback')) +print(json.dumps({'controls':results,'count':len(results),'all_expected':True},indent=2)) diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/review.json b/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/review.json new file mode 100644 index 00000000..89e11647 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-broker-review-r1/review.json @@ -0,0 +1,83 @@ +{ + "schema": "custodian.platform-broker-independent-review.v1", + "reviewed_on": "2026-10-17", + "candidate_manifest_sha256": "61c15ae8d12b3665eb0e210cc0a1b0a238f52026258a59987657fd77f8d6c0ba", + "candidate_patch_sha256": "b0b85153d04a28d4bb05ab39a9b54c782c9efa54bd80ca961e2bcc6348a9b546", + "source_commits": { + "railiance-platform": "104ed90f42a9a86bf9fda343b49b65a85c9ee4b9", + "net-kingdom": "befb0bc68cd25b625804fce559476eefd702521d", + "ops-warden": "b593c879831378db7dc62adfabdfedf2d44ddd38" + }, + "manifest_all_preimages_postimages_review_inputs_verified": true, + "candidate_restored_after_baseline": true, + "tests": { + "candidate_broker_passed": 15, + "candidate_catalog_passed": 4, + "baseline_expected_failures_reproduced": 2, + "independent_controls_passed": 13, + "controls": [ + "each of three producer files drifted, CLI refuses before fake Bao", + "each of three producer files removed, CLI refuses before fake Bao", + "fixed sibling missing refuses before fake Bao", + "fixed sibling root symlink refuses before fake Bao", + "source-root environment override ignored", + "actual validator subprocess uses -I and empty environment", + "fabricated acceptance record still reaches fake generation (documented existing admission gap)", + "missing case acceptance refuses before fake Bao", + "invalid redirect URI refuses before fake Bao" + ], + "isolation": "bwrap unshare-net; allowlisted environment; no host home/config; fake Bao only" + }, + "verdict": "admissible for bounded owner source integration; no blocking source-candidate defect found", + "source_installation": "Compatible with the documented ~/railiance-platform + sibling ~/net-kingdom checkout invocation; same exact layout exercised in independent scratch. No packaged/copied broker installation proved. Missing, drifted or symlinked source fails closed. Dependency needs explicit retention in owner integration documentation.", + "producer_closure": "Three pinned imported modules close validate_type + validate_case. Warden additionally pins security_case_evidence.py, only imported by CLI check-evidence; this broker never calls that path. Standard library and system PyYAML remain runtime dependencies under the existing Warden interpreter pattern.", + "current_admission": [ + "oidc-browser-relying-party@1.1" + ], + "source_only_acknowledged_12_refused": true, + "proposed_13_refused": true, + "catalog": "Human-only exact issuer role binding matches CCR-2026-0043 applied source and role net-kingdom-admins + aal2. No live verification was made.", + "authenticated_case_admission": "OPEN: caller can replace acceptance.record with fabricated structurally valid text, rehash case bytes, and pass schema validation. Independent fake-only probe demonstrated this. Source hardening does not complete T1 or establish owner acceptance.", + "real_capability_gates": [ + "Actual 1.3 acknowledgment and exact owner reviewed admission bindings", + "Authenticated case admission/owner acceptance", + "Generator projection to exact custody-mint schema", + "Grant accessor and independently authenticated revocation evidence", + "Caller-session revocation and authenticated evidence publication", + "Attended exact minted version compare-before-delete destroy capability", + "Platform secret delivery lane, Warden catalog acceptance, deliver/remove capabilities with ready/spec/precondition checks", + "Formal owner-descriptor-acceptance after obligations and review" + ], + "formal_owner_assent_created": false, + "canonical_writes": false, + "credential_native_network_model_calls": false, + "correction_patch": "none; no new source defect found", + "workplan": "RPF-WP-0060 remains tentative; parent coordinator allocates/publishes legitimate owner records", + "source_integration_verdict": "accept", + "source_integration_scope": "Exactly the four candidate source postimages from the reviewed patch against Platform 104ed90f42a9a86bf9fda343b49b65a85c9ee4b9; source integration only, without authority or completion claims.", + "reviewed_outputs": [ + { + "path": "scripts/keycape_client_secret_generate.py", + "sha256": "da80947a2c28df36b37e711d1e6d2f66b4c0434082e7bf187da3d3cb692cec28" + }, + { + "path": "credential-grants/catalog.yaml", + "sha256": "7cfe12970a57ab75cb91c7c5024a4dcfc28ffae4b31a1afaf4d0cedabbb22886" + }, + { + "path": "tests/test_keycape_client_secret_generate.py", + "sha256": "5906e32c7fe0db09c1f7db48ad61226497e062f38ebfa91e8a03be89272765c0" + }, + { + "path": "tests/test_credential_grants.py", + "sha256": "5c0cfcb039c0220e59dac7986d960bb568593a3a9281ff2557bb6c38de31e68a" + } + ], + "t1_complete": false, + "integration_conditions": [ + "Preserve explicit fixed sibling net-kingdom checkout dependency and exact source pins; missing/drifted/symlinked source refusal is intentional.", + "Keep 1.2 and 1.3 refused.", + "Retain authenticated case admission as open; self-hashed reference/schema validation is insufficient.", + "Allocate legitimate owner work records separately; tentative WP60 is not approval evidence." + ] +} diff --git a/docs/changes/netkingdom-target-2026-10-17/platform-current-source-integration-r1/apply_source.py b/docs/changes/netkingdom-target-2026-10-17/platform-current-source-integration-r1/apply_source.py new file mode 100644 index 00000000..133395b2 --- /dev/null +++ b/docs/changes/netkingdom-target-2026-10-17/platform-current-source-integration-r1/apply_source.py @@ -0,0 +1,80 @@ +"""Apply only independently reviewed owner source, using the normal allocator.""" +from pathlib import Path +import hashlib +import json +import re +import subprocess +import sys + +HERE = Path(__file__).resolve().parent +ROOT = Path('/home/worsch/railiance-platform') +PACKET = HERE.parent / 'platform-broker-followthrough-r1' +REVIEW = HERE.parent / 'platform-broker-review-r1/review.json' +NEW_PLAN = 'workplans/RPF-WP-0060-browser-case-platform-bindings.md' +BASE = '104ed90f42a9a86bf9fda343b49b65a85c9ee4b9' + + +def digest(raw): + return hashlib.sha256(raw).hexdigest() + + +def git(*args): + return subprocess.check_output(['git', '-C', str(ROOT), *args], + stderr=subprocess.PIPE, timeout=40).decode().strip() + + +assert git('rev-parse', 'HEAD') == BASE +assert git('ls-remote', 'origin', 'refs/heads/main').split()[0] == BASE +assert digest(REVIEW.read_bytes()) == '05d5bf9c2b90a8562ce4675866b8b23c0e12d5d069b43b194948d384b0a553f5' +review = json.loads(REVIEW.read_text()) +assert review['source_integration_verdict'] == 'accept' +assert review['t1_complete'] is False +manifest = json.loads((PACKET / 'manifest.json').read_text()) +assert digest((PACKET / 'platform-source.patch').read_bytes()) == review['candidate_patch_sha256'] +assert {r['path']: r['output_sha256'] for r in manifest['files']} == { + r['path']: r['sha256'] for r in review['reviewed_outputs']} +for row in manifest['files']: + assert digest((ROOT / row['path']).read_bytes()) == row['input_sha256'] + assert digest((PACKET / 'candidate' / row['path']).read_bytes()) == row['output_sha256'] +assert not (ROOT / NEW_PLAN).exists() +old_plans = {str(p.relative_to(ROOT)): digest(p.read_bytes()) for p in (ROOT / 'workplans').rglob('*.md')} +assert not any(re.search(r'^id:\s*RPF-WP-0060\s*$', (ROOT / p).read_text(), re.M) for p in old_plans) +dirty = git('diff', '--name-only').splitlines() +staged = git('diff', '--cached', '--name-only').splitlines() +assert set(dirty + staged) == {'.custodian-brief.md', 'WORK-RECORDS.md'} +originals = {p: {'worktree': digest((ROOT / p).read_bytes()), 'index': git('ls-files', '--stage', '--', p)} + for p in set(dirty + staged)} +sys.path.insert(0, '/home/worsch/repo-manager/src') +from repo_manager.identifiers import ensure_missing_work_record_identifiers +preview = ensure_missing_work_record_identifiers(ROOT, execute=False) +assert preview['ok'] and not preview['assignments'] and not preview['files_changed'] +for row in manifest['files']: + (ROOT / row['path']).write_bytes((PACKET / 'candidate' / row['path']).read_bytes()) +(ROOT / NEW_PLAN).write_bytes((HERE / 'workplan.md').read_bytes()) +preview = ensure_missing_work_record_identifiers(ROOT, execute=False) +(HERE / 'identifier-preview.json').write_text(json.dumps(preview, indent=2) + '\n') +assert preview['ok'] and preview['files_changed'] == [NEW_PLAN] +assert len(preview['assignments']) == 5 +assigned = ensure_missing_work_record_identifiers(ROOT, execute=True) +(HERE / 'identifier-assignment.json').write_text(json.dumps(assigned, indent=2) + '\n') +assert assigned['ok'] and assigned['files_changed'] == [NEW_PLAN] +for p, value in old_plans.items(): + assert digest((ROOT / p).read_bytes()) == value +for p, value in originals.items(): + assert digest((ROOT / p).read_bytes()) == value['worktree'] + assert git('ls-files', '--stage', '--', p) == value['index'] +files = [dict(path=row['path'], sha256=row['output_sha256']) for row in manifest['files']] +files.append(dict(path=NEW_PLAN, sha256=digest((ROOT / NEW_PLAN).read_bytes()))) +cfg = {'owner': 'railiance-platform', 'base_revision': BASE, + 'message': 'Bind mint broker validation to exact reviewed sources and track successor obligations', + 'files': files} +(HERE / 'publication-inputs.json').write_text(json.dumps(cfg, indent=2) + '\n') +(HERE / 'source-application.json').write_text(json.dumps({ + 'base': BASE, 'review_sha256': 'sha256:' + digest(REVIEW.read_bytes()), + 'files': files, 'existing_workplans_preserved': len(old_plans), + 'unrelated_state': originals, 'native_actions': False, + 'tasks': {'RPF-WP-0060-T01': 'progress', 'RPF-WP-0060-T02': 'done', + 'RPF-WP-0060-T03': 'todo', 'RPF-WP-0060-T04': 'todo'}}, indent=2) + '\n') +print(json.dumps({'status': 'applied_source_only', 'files': len(files), + 'normal_allocator_assignments': len(assigned['assignments']), + 'existing_workplans_preserved': len(old_plans)}))