Record reviewed owner implementation and consumer compatibility (1)

This commit is contained in:
codex 2026-10-17 23:06:00 +02:00
parent 05df7c3ee4
commit 518369dbee
18 changed files with 1990 additions and 0 deletions

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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/<client_id> 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

View file

@ -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.yaml> \\
--case-ref forgejo-netkingdom-oidc@sha256:<hex64> --receipt <file>
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())

View file

@ -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))

View file

@ -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()

View file

@ -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"
}

View file

@ -0,0 +1 @@
git apply --check against exact four baseline files: 0

View file

@ -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(

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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))

View file

@ -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."
]
}

View file

@ -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)}))