NK-WP-0058-T03 done: readiness command, executor-lanes status, R1 via the preflight source check
tools/bounded-execution-readiness/ready.py (make bounded-execution-ready): agent-identity checks from owner source (admission, approver chain at the Argo pin and main, access-engine profile, CCRs by the owner's RUNBOOK_ALLOWED_STATUSES, case and broker type pin), and three named read-only founder checks. executor-lanes.py status: write role, fence, cas_required; no write. A replay test catches 2026-11-04. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 943911@bnt-lap001 Assistant-Session: fc322940-1c8e-4bed-ab39-86cec99f23a0
This commit is contained in:
parent
64cec27c28
commit
a29d92ecda
8 changed files with 452 additions and 11 deletions
7
Makefile
7
Makefile
|
|
@ -212,8 +212,13 @@ bounded-execution-admission-check: ## Validate the example admissions (read-only
|
|||
execution-attribution-receipt-test: ## Receipt schema v0.1 validator; fixtures match the executor (NK-WP-0056-T15)
|
||||
python3 -m pytest tools/execution-attribution-receipt/tests
|
||||
|
||||
bounded-execution-ready: ## Readiness before a window, read-only (NK-WP-0058-T03): ADMISSION= APPROVAL_ID= RECEIPT=
|
||||
@test -n "$(ADMISSION)" -a -n "$(APPROVAL_ID)" -a -n "$(RECEIPT)" || (echo "ADMISSION=, APPROVAL_ID= and RECEIPT= are required" >&2; exit 2)
|
||||
python3 tools/bounded-execution-readiness/ready.py --admission $(ADMISSION) --approval-id $(APPROVAL_ID) --receipt $(RECEIPT)
|
||||
|
||||
bounded-executor-test: ## Bounded executor RBAC and declaration tests; the pilot admission inside its declared grants (NK-WP-0056-T03)
|
||||
python3 -m pytest sso-mfa/k8s/bounded-executor bounded-executor/tests tests/test_executor_lanes.py tests/test_write_role_audit.py
|
||||
python3 -m pytest sso-mfa/k8s/bounded-executor bounded-executor/tests tests/test_executor_lanes.py tests/test_write_role_audit.py \
|
||||
tools/bounded-execution-readiness/tests
|
||||
python3 tools/service-identity/validate.py tools/service-identity/declarations/bounded-executor.yaml \
|
||||
--admissions examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml
|
||||
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@
|
|||
| task | NK-WP-0057-T04 | todo | — | workplans/NK-WP-0057-decision-routing-by-responsibility.md |
|
||||
| task | NK-WP-0058-T01 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md |
|
||||
| task | NK-WP-0058-T02 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md |
|
||||
| task | NK-WP-0058-T03 | progress | — | workplans/NK-WP-0058-bounded-execution-operability.md |
|
||||
| task | NK-WP-0058-T03 | done | — | workplans/NK-WP-0058-bounded-execution-operability.md |
|
||||
| task | NK-WP-0058-T04 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md |
|
||||
| task | NK-WP-0058-T05 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md |
|
||||
| task | NK-WP-0058-T06 | todo | — | workplans/NK-WP-0058-bounded-execution-operability.md |
|
||||
|
|
|
|||
|
|
@ -189,10 +189,11 @@ resolve through the rapp config and must equal the routing recipient for the mem
|
|||
(`plan.source.files`).
|
||||
- **Gap:** no record links the digest to the commit to the admission pins, so "the admission's pins match the
|
||||
pinned image" cannot be checked.
|
||||
- **Proposal:**
|
||||
- the image workflow writes a value-free pin record (commit, file sha256s, digest);
|
||||
- rapp pins that record;
|
||||
- the renderer refuses when the admission's file hashes are not the image's (T03).
|
||||
- **Resolved without new records (T03, 2026-11-05):** the image is tagged `main-<commit>` by its workflow, and
|
||||
the executor's own `source` preflight check compares the admission's pinned file hashes with the files inside
|
||||
the running image. rapp `make preflight` runs that check in the pinned image before a window opens, so a pin
|
||||
mismatch shows as `preflight:source` ahead of the window. Remaining hand step: copying the digest into the rapp
|
||||
`Makefile`.
|
||||
|
||||
### R2: config, Job and exit codes
|
||||
|
||||
|
|
@ -215,7 +216,7 @@ resolve through the rapp config and must equal the routing recipient for the mem
|
|||
- E1: lane names and policy text, from the CCRs;
|
||||
- E5: the approver, compared until it has one source;
|
||||
- E8: the RBAC copy;
|
||||
- R1: the pin record;
|
||||
- R1: the pin chain, now checked by the preflight `source` check (T03);
|
||||
- R2: the config schema.
|
||||
- **delete:** nothing yet. The candidates are the hand copies named under merge, once they are derived or checked.
|
||||
- **owner decision** (to NK-WP-0057-T01 or the named owner):
|
||||
|
|
|
|||
|
|
@ -262,3 +262,20 @@ def test_lane_error_keeps_its_fixed_code(tmp_path, monkeypatch):
|
|||
assert m.reason_of(lane_error('command_failed')) == 'LaneError:command_failed'
|
||||
assert m.reason_of(lane_error('not a fixed code')) == 'LaneError'
|
||||
assert m.reason_of(ValueError('anything')) == 'ValueError'
|
||||
|
||||
|
||||
def test_status_is_read_only_and_reports_every_check(tmp_path, monkeypatch):
|
||||
m, fake, d = build(tmp_path, monkeypatch)
|
||||
run(m, 'setup', d)
|
||||
before = json.dumps(fake.store, sort_keys=True), fake.cas
|
||||
r = {}
|
||||
m.status(r)
|
||||
assert r == {'phase': 'done', 'status': 'status_read', 'write_role_absent': True, 'agent_fence_present': True,
|
||||
'cas_required': True, 'ready_for_window': True}
|
||||
assert (json.dumps(fake.store, sort_keys=True), fake.cas) == before # nothing written
|
||||
path, now = admission_at(tmp_path, m)
|
||||
m.window_open(d, path, {}, now=now)
|
||||
fake.cas = False
|
||||
r = {}
|
||||
m.status(r)
|
||||
assert (r['write_role_absent'], r['cas_required'], r['ready_for_window']) == (False, False, False)
|
||||
|
|
|
|||
234
tools/bounded-execution-readiness/ready.py
Normal file
234
tools/bounded-execution-readiness/ready.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Readiness check before a bounded-execution window opens (NK-WP-0058-T03). Read-only, value-free.
|
||||
|
||||
python3 tools/bounded-execution-readiness/ready.py --admission admissions/BEA-....yaml --approval-id ID \
|
||||
--receipt EVIDENCE/NN-readiness.json [--siblings DIR] [--now ISO]
|
||||
|
||||
What the agent identity can check, from the owners' committed source at the revisions that matter:
|
||||
|
||||
admission valid (the admission validator), executor named, window not closed
|
||||
approver_chain the admission's approvers resolve through the rapp config; informed-decision, both at the
|
||||
revision Argo pins and on main (live is one of the two), registers this executor and routes its
|
||||
review group to the first approver's subject (access-engine lists a memo only to that
|
||||
recipient, policy v14 recipient_ok)
|
||||
argo_pin the pinned informed-decision files equal its main branch (warning only: live may be newer)
|
||||
profile access-engine's present_existing profile for the executor matches informed-decision's registry
|
||||
ccrs CCR-2026-0058..0061 and -0063 are in railiance-platform's RUNBOOK_ALLOWED_STATUSES (approved,
|
||||
applied, verified, active), every bound_claims_confirmed true
|
||||
case the approval-client case is materialized or live; its type's bytes match the broker's hash pin
|
||||
|
||||
What it cannot see is named as one read-only founder command each (`founder_checks`): the write role and fence
|
||||
(OpenBao), the preflight Job (reachability, lanes, source pins in the pinned image, trust, window, approval) and
|
||||
the custody deliveries (ExternalSecret Ready). Exit 0 when every agent check passes, 1 otherwise.
|
||||
"""
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
CASE = 'security-cases/nk-bind-executor-approval.yaml'
|
||||
CCRS = ('CCR-2026-0058', 'CCR-2026-0059', 'CCR-2026-0060', 'CCR-2026-0061', 'CCR-2026-0063')
|
||||
ARGO_APP = 'argocd/railiance01/applications/informed-decision.yaml'
|
||||
POLICY = 'examples/informed-decision-present-existing-review/policy.md'
|
||||
BROKER = 'scripts/keycape_client_secret_generate.py'
|
||||
RAPP_CONFIG = 'examples/config.pilot.json'
|
||||
|
||||
|
||||
def load_validator():
|
||||
spec = importlib.util.spec_from_file_location('bea_validate_ready',
|
||||
REPO / 'tools/bounded-execution-admission/validate.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def when(text):
|
||||
return dt.datetime.fromisoformat(text.replace('Z', '+00:00'))
|
||||
|
||||
|
||||
class Report:
|
||||
def __init__(self):
|
||||
self.checks = []
|
||||
|
||||
def add(self, check, ok, reason=None, warn=False):
|
||||
result = 'pass' if ok else ('warn' if warn else 'fail')
|
||||
self.checks.append({'check': check, 'result': result, **({'reason': reason} if reason and not ok else {})})
|
||||
return ok
|
||||
|
||||
|
||||
def git_show(repo, rev, path):
|
||||
done = subprocess.run(['git', '-C', str(repo), 'show', f'{rev}:{path}'], capture_output=True)
|
||||
if done.returncode: raise FileNotFoundError(f'{repo.name}:{rev}:{path}')
|
||||
return done.stdout
|
||||
|
||||
|
||||
def check_admission(report, doc, now):
|
||||
bea = load_validator()
|
||||
result = bea.evaluate(doc, json.loads(bea.SCHEMA.read_text()))
|
||||
rules = ','.join(sorted({f['rule'] for f in result['findings']}))
|
||||
report.add('admission_valid', result['valid'], 'admission_invalid:' + rules)
|
||||
spec = doc['spec']
|
||||
report.add('window_not_closed', now < when(spec['window']['not_after']), 'window_closed')
|
||||
|
||||
|
||||
def check_approver_chain(report, spec, cfg, infd, rev, label='pinned'):
|
||||
names = spec['approval']['approvers']
|
||||
subjects = [cfg.get('approver_subjects', {}).get(n) for n in names]
|
||||
if label == 'pinned':
|
||||
if not report.add('approvers_resolve', all(subjects), 'approver_not_in_rapp_config'):
|
||||
return None
|
||||
report.add('single_approver', len(names) == 1, 'routing_has_one_recipient_per_group')
|
||||
elif not all(subjects):
|
||||
return None
|
||||
groups = json.loads(git_show(infd, rev, 'deploy/lifecycle/review-groups.json'))
|
||||
submitters = json.loads(git_show(infd, rev, 'deploy/lifecycle/intake-submitters.json'))
|
||||
row = submitters.get(cfg['runtime_principal'])
|
||||
if not report.add(f'executor_registered:{label}', row is not None, 'runtime_principal_not_in_intake_submitters'):
|
||||
return None
|
||||
actor = (row.get('present_existing') or {}).get('approval_actor')
|
||||
report.add(f'approval_actor_matches:{label}', actor == cfg['executor_identity'], 'approval_actor_differs')
|
||||
report.add(f'memo_class_admitted:{label}', 'bounded-execution' in row.get('memo_classes', []), 'memo_class_missing')
|
||||
recipients = {g: (groups.get(g) or {}).get('subject') for g in row.get('review_groups', [])}
|
||||
report.add(f'recipient_is_approver:{label}', bool(recipients) and set(recipients.values()) == {subjects[0]},
|
||||
'review_group_routes_elsewhere')
|
||||
return row
|
||||
|
||||
|
||||
def check_argo_pin(report, infd, rev):
|
||||
for name in ('review-groups.json', 'intake-submitters.json'):
|
||||
same = git_show(infd, rev, 'deploy/lifecycle/' + name) == git_show(infd, 'HEAD', 'deploy/lifecycle/' + name)
|
||||
report.add('argo_pin_matches_source:' + name, same, 'pinned_revision_differs_from_main', warn=True)
|
||||
|
||||
|
||||
def check_profile(report, row, cfg, access_engine):
|
||||
text = (access_engine / POLICY).read_text()
|
||||
block = re.search(r'present_existing_profiles := \{(.*?)\n\}', text, re.S)
|
||||
entry = block and re.search(r'"' + re.escape(cfg['executor_identity']) + r'": \{(.*?)\}', block.group(1), re.S)
|
||||
if not report.add('profile_present', bool(entry), 'executor_not_in_present_existing_profiles'):
|
||||
return
|
||||
fields = dict(re.findall(r'"(\w+)": "([^"]*)"', entry.group(1)))
|
||||
report.add('profile_submitter_matches', row is not None and fields.get('intake_submitter') == row['submitter_id'],
|
||||
'intake_submitter_differs')
|
||||
report.add('profile_memo_class_matches', fields.get('memo_class') == 'bounded-execution', 'memo_class_differs')
|
||||
|
||||
|
||||
def confirmations(node):
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
if key == 'bound_claims_confirmed': yield value
|
||||
else: yield from confirmations(value)
|
||||
elif isinstance(node, list):
|
||||
for value in node: yield from confirmations(value)
|
||||
|
||||
|
||||
def ccr_statuses(platform):
|
||||
"""The owner's own set of statuses under which a CCR may be acted on (credential-change.py), not a copy."""
|
||||
spec = importlib.util.spec_from_file_location('credential_change_ready', platform / 'scripts/credential-change.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return set(module.RUNBOOK_ALLOWED_STATUSES)
|
||||
|
||||
|
||||
def check_ccrs(report, platform):
|
||||
folder = platform / 'credential-change-requests'
|
||||
allowed = ccr_statuses(platform)
|
||||
for ccr in CCRS:
|
||||
files = sorted(folder.glob(ccr + '-*.yaml'))
|
||||
if not report.add(ccr + ':present', len(files) == 1, 'ccr_file_missing'):
|
||||
continue
|
||||
doc = yaml.safe_load(files[0].read_text())
|
||||
report.add(ccr + ':approved', doc.get('status') in allowed, 'status_' + str(doc.get('status')))
|
||||
report.add(ccr + ':bindings_confirmed', all(v is True for v in confirmations(doc)), 'binding_not_confirmed')
|
||||
|
||||
|
||||
def check_case(report, platform):
|
||||
case = yaml.safe_load((REPO / CASE).read_text())['spec']
|
||||
report.add('case_materialized', case.get('status') in ('materialized', 'live'), 'case_' + str(case.get('status')))
|
||||
pins = {ref: (path, digest) for ref, path, digest in re.findall(
|
||||
r"'([a-z0-9-]+@[0-9.]+)': \(\s*'(security-case-types/[^']+)',\s*'([0-9a-f]{64})'\)",
|
||||
(platform / BROKER).read_text())}
|
||||
pinned = pins.get(case.get('type'))
|
||||
if not report.add('case_type_acknowledged', pinned is not None, 'type_not_admitted_by_broker'):
|
||||
return
|
||||
path, digest = pinned
|
||||
report.add('case_type_bytes_match_broker', hashlib.sha256((REPO / path).read_bytes()).hexdigest() == digest,
|
||||
'type_yaml_changed_since_acknowledgment')
|
||||
|
||||
|
||||
def founder_checks(admission, approval_id):
|
||||
"""One read-only command each, for what the agent identity cannot read (NK-WP-0058-T05 status table)."""
|
||||
k = 'kubectl --kubeconfig ~/.kube/config-railiance01 -n sso'
|
||||
jsonpath = "-o jsonpath='{.status.conditions[?(@.type==\"Ready\")].status}'"
|
||||
return [
|
||||
{'id': 'write_role_and_fence', 'needs': 'platform-admin OpenBao login',
|
||||
'command': 'python3 tools/service-binds/executor-lanes.py status --receipt <evidence>/NN-status.json',
|
||||
'expect': 'ready_for_window: true'},
|
||||
{'id': 'preflight_job', 'needs': 'admin kubeconfig',
|
||||
'command': f'cd ~/rapp-bounded-executor && make preflight ADMISSION={admission} APPROVAL_ID={approval_id} '
|
||||
'CONFIG=<run config> KUBECTL="kubectl --kubeconfig ~/.kube/config-railiance01"',
|
||||
'expect': '"ready": true'},
|
||||
{'id': 'custody_delivery', 'needs': 'admin kubeconfig',
|
||||
'command': f'for e in keycape-nk-bind-executor-approval-client bounded-executor-tokens; do '
|
||||
f'{k} get externalsecret $e {jsonpath}; echo; done',
|
||||
'expect': 'True, twice'},
|
||||
]
|
||||
|
||||
|
||||
def run(admission_path, approval_id, siblings, now):
|
||||
report = Report()
|
||||
doc = yaml.safe_load(Path(admission_path).read_text())
|
||||
spec = doc['spec']
|
||||
check_admission(report, doc, now)
|
||||
platform, infd = siblings / 'railiance-platform', siblings / 'informed-decision'
|
||||
access_engine = siblings / 'access-engine'
|
||||
cfg = json.loads((siblings / 'rapp-bounded-executor' / RAPP_CONFIG).read_text())
|
||||
report.add('executor_named', spec['executor']['identity'] == cfg['executor_identity'], 'admission_names_another')
|
||||
rev = yaml.safe_load((platform / ARGO_APP).read_text())['spec']['source']['targetRevision']
|
||||
row = check_approver_chain(report, spec, cfg, infd, rev)
|
||||
check_argo_pin(report, infd, rev)
|
||||
# live is the Argo-pinned revision or a newer founder-run release (2026-11-04: the pin lagged): hold both
|
||||
check_approver_chain(report, spec, cfg, infd, 'HEAD', label='main')
|
||||
check_profile(report, row, cfg, access_engine)
|
||||
check_ccrs(report, platform)
|
||||
check_case(report, platform)
|
||||
failures = [c['check'] for c in report.checks if c['result'] == 'fail']
|
||||
return {'action': 'bounded-execution-readiness', 'admission': spec['id'], 'values_recorded': False,
|
||||
'at': now.strftime('%Y-%m-%dT%H:%M:%SZ'), 'argo_pin': rev[:12], 'agent_checks_pass': not failures,
|
||||
'failures': failures, 'warnings': [c['check'] for c in report.checks if c['result'] == 'warn'],
|
||||
'checks': report.checks, 'founder_checks': founder_checks(admission_path, approval_id)}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument('--admission', required=True)
|
||||
p.add_argument('--approval-id', required=True)
|
||||
p.add_argument('--receipt', required=True)
|
||||
p.add_argument('--siblings', type=Path, default=Path(os.environ.get('NK_SIBLINGS', REPO.parent)))
|
||||
p.add_argument('--now', type=when, default=None)
|
||||
a = p.parse_args(argv)
|
||||
now = a.now or dt.datetime.now(dt.timezone.utc)
|
||||
fd = os.open(a.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
r = run(a.admission, a.approval_id, a.siblings, now)
|
||||
except Exception as error: # value-free: a missing file or revision is named, nothing else is echoed
|
||||
r = {'action': 'bounded-execution-readiness', 'agent_checks_pass': False, 'values_recorded': False,
|
||||
'failures': ['error:' + type(error).__name__],
|
||||
'reason': str(error) if isinstance(error, FileNotFoundError) else type(error).__name__}
|
||||
with os.fdopen(fd, 'w') as out:
|
||||
json.dump(r, out, indent=2, sort_keys=True); out.write('\n')
|
||||
print(json.dumps({'agent_checks_pass': r['agent_checks_pass'], 'failures': r['failures'],
|
||||
'warnings': r.get('warnings', []),
|
||||
'founder_checks': [f['id'] for f in r.get('founder_checks', [])]}))
|
||||
return 0 if r['agent_checks_pass'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
137
tools/bounded-execution-readiness/tests/test_ready.py
Normal file
137
tools/bounded-execution-readiness/tests/test_ready.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""ready.py against synthetic sibling repositories (NK-WP-0058-T03). No network, no cluster, no secret values."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
spec = importlib.util.spec_from_file_location('ready', REPO / 'tools/bounded-execution-readiness/ready.py')
|
||||
ready = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ready)
|
||||
|
||||
ADMISSION = REPO / 'admissions/BEA-2026-0003.yaml'
|
||||
INSIDE = ready.when('2026-11-05T08:25:00Z')
|
||||
ADMIN = 'uid=admin,ou=people,dc=netkingdom,dc=local'
|
||||
ROOT_DN = 'uid=platform-root,ou=people,dc=netkingdom,dc=local'
|
||||
EXECUTOR = 'service:nk-bind-executor'
|
||||
SA = 'system:serviceaccount:sso:nk-bind-executor'
|
||||
TYPE = 'security-case-types/oidc-service-client/type.yaml'
|
||||
|
||||
|
||||
def git(repo, *args):
|
||||
subprocess.run(['git', '-C', str(repo), *args], check=True, capture_output=True)
|
||||
|
||||
|
||||
def lifecycle(repo, recipient, submitter='nk-bind-executor'):
|
||||
d = repo / 'deploy/lifecycle'
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
(d / 'review-groups.json').write_text(json.dumps({'net-kingdom-admins': {'subject': recipient}}))
|
||||
(d / 'intake-submitters.json').write_text(json.dumps({SA: {
|
||||
'submitter_id': submitter, 'memo_classes': ['bounded-execution'], 'review_groups': ['net-kingdom-admins'],
|
||||
'present_existing': {'approval_actor': EXECUTOR, 'actions': ['bounded-execution']}}}))
|
||||
git(repo, 'add', '-A')
|
||||
git(repo, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'lifecycle')
|
||||
return subprocess.run(['git', '-C', str(repo), 'rev-parse', 'HEAD'], capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def siblings(tmp_path, *, pin_recipient=ADMIN, main_recipient=None, ccr_status='approved', confirmed=True,
|
||||
type_digest=None, profile_submitter='nk-bind-executor'):
|
||||
root = tmp_path / 'siblings'
|
||||
infd = root / 'informed-decision'
|
||||
infd.mkdir(parents=True)
|
||||
git(infd, 'init', '-q')
|
||||
pin = lifecycle(infd, pin_recipient)
|
||||
if main_recipient:
|
||||
lifecycle(infd, main_recipient)
|
||||
plat = root / 'railiance-platform'
|
||||
app = plat / ready.ARGO_APP
|
||||
app.parent.mkdir(parents=True)
|
||||
app.write_text(yaml.safe_dump({'spec': {'source': {'targetRevision': pin, 'path': 'deploy/lifecycle'}}}))
|
||||
(plat / 'scripts').mkdir()
|
||||
(plat / 'scripts/credential-change.py').write_text(
|
||||
'APPLY_ALLOWED_STATUSES = {"approved"}\nPOST_APPLY_STATUSES = {"applied", "verified", "active"}\n'
|
||||
'RUNBOOK_ALLOWED_STATUSES = APPLY_ALLOWED_STATUSES | POST_APPLY_STATUSES\n')
|
||||
digest = type_digest or hashlib.sha256((REPO / TYPE).read_bytes()).hexdigest()
|
||||
(plat / ready.BROKER).write_text("ACKNOWLEDGED_TYPES = {\n 'oidc-service-client@0.2': (\n"
|
||||
f" '{TYPE}',\n '{digest}'),\n}}\n")
|
||||
ccrs = plat / 'credential-change-requests'
|
||||
ccrs.mkdir()
|
||||
for ccr in ready.CCRS:
|
||||
(ccrs / f'{ccr}-synthetic.yaml').write_text(yaml.safe_dump(
|
||||
{'id': ccr, 'status': ccr_status, 'openbao': {'auth': {'bound_claims_confirmed': confirmed}}}))
|
||||
ae = root / 'access-engine' / ready.POLICY
|
||||
ae.parent.mkdir(parents=True)
|
||||
ae.write_text('present_existing_profiles := {\n "service:nk-bind-executor": {\n'
|
||||
f' "intake_submitter": "{profile_submitter}",\n "memo_class": "bounded-execution",\n'
|
||||
' "approval_action": "bounded-execution",\n },\n}\n')
|
||||
rapp = root / 'rapp-bounded-executor' / ready.RAPP_CONFIG
|
||||
rapp.parent.mkdir(parents=True)
|
||||
rapp.write_text(json.dumps({'executor_identity': EXECUTOR, 'runtime_principal': SA,
|
||||
'approver_subjects': {'admin': ADMIN}}))
|
||||
return root
|
||||
|
||||
|
||||
def results(r):
|
||||
return {c['check']: c['result'] for c in r['checks']}
|
||||
|
||||
|
||||
def test_clean_chain_passes_and_names_the_founder_checks(tmp_path):
|
||||
r = ready.run(ADMISSION, 'apr-1', siblings(tmp_path), INSIDE)
|
||||
assert r['agent_checks_pass'] and r['failures'] == [] and r['warnings'] == []
|
||||
assert [f['id'] for f in r['founder_checks']] == ['write_role_and_fence', 'preflight_job', 'custody_delivery']
|
||||
assert 'APPROVAL_ID=apr-1' in r['founder_checks'][1]['command']
|
||||
assert r['values_recorded'] is False
|
||||
|
||||
|
||||
def test_replay_2026_11_04_routing_to_platform_root_on_main_is_caught(tmp_path):
|
||||
"""The Argo pin lagged a founder-run release that routed net-kingdom-admins to platform-root."""
|
||||
r = ready.run(ADMISSION, 'apr-1', siblings(tmp_path, main_recipient=ROOT_DN), INSIDE)
|
||||
assert not r['agent_checks_pass']
|
||||
assert 'recipient_is_approver:main' in r['failures']
|
||||
assert set(r['warnings']) == {'argo_pin_matches_source:review-groups.json'}
|
||||
assert results(r)['recipient_is_approver:pinned'] == 'pass'
|
||||
|
||||
|
||||
def test_routing_elsewhere_at_the_pin(tmp_path):
|
||||
r = ready.run(ADMISSION, 'apr-1', siblings(tmp_path, pin_recipient=ROOT_DN), INSIDE)
|
||||
assert {'recipient_is_approver:pinned', 'recipient_is_approver:main'} <= set(r['failures'])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kwargs, failed', [
|
||||
({'ccr_status': 'proposed'}, 'CCR-2026-0058:approved'),
|
||||
({'ccr_status': 'verified'}, None),
|
||||
({'confirmed': False}, 'CCR-2026-0061:bindings_confirmed'),
|
||||
({'type_digest': '0' * 64}, 'case_type_bytes_match_broker'),
|
||||
({'profile_submitter': 'someone-else'}, 'profile_submitter_matches'),
|
||||
])
|
||||
def test_each_owner_contract_is_checked(tmp_path, kwargs, failed):
|
||||
r = ready.run(ADMISSION, 'apr-1', siblings(tmp_path, **kwargs), INSIDE)
|
||||
if failed is None:
|
||||
assert r['agent_checks_pass'], r['failures']
|
||||
else:
|
||||
assert failed in r['failures']
|
||||
|
||||
|
||||
def test_closed_window_and_receipt_handling(tmp_path):
|
||||
root = siblings(tmp_path)
|
||||
r = ready.run(ADMISSION, 'apr-1', root, ready.when('2026-11-06T00:00:00Z'))
|
||||
assert 'window_not_closed' in r['failures']
|
||||
receipt = tmp_path / 'r.json'
|
||||
assert ready.main(['--admission', str(ADMISSION), '--approval-id', 'apr-1', '--receipt', str(receipt),
|
||||
'--siblings', str(root), '--now', '2026-11-05T08:25:00Z']) == 0
|
||||
assert json.loads(receipt.read_text())['agent_checks_pass'] is True
|
||||
with pytest.raises(FileExistsError):
|
||||
ready.main(['--admission', str(ADMISSION), '--approval-id', 'apr-1', '--receipt', str(receipt),
|
||||
'--siblings', str(root)])
|
||||
|
||||
|
||||
def test_a_missing_sibling_fails_by_name(tmp_path):
|
||||
receipt = tmp_path / 'r.json'
|
||||
assert ready.main(['--admission', str(ADMISSION), '--approval-id', 'a', '--receipt', str(receipt),
|
||||
'--siblings', str(tmp_path / 'none')]) == 1
|
||||
r = json.loads(receipt.read_text())
|
||||
assert r['failures'] == ['error:FileNotFoundError'] and r['agent_checks_pass'] is False
|
||||
|
|
@ -16,6 +16,8 @@ window-open --admission FILE: for one admitted run (T06): the admission is valid
|
|||
create the write role exactly as CCR-2026-0058 declares it.
|
||||
window-close --admission FILE: after the run's receipt or at window end, whichever comes first: delete the write role
|
||||
and prove it is gone. Always allowed, also late; needs no CCR digests.
|
||||
status: read-only (NK-WP-0058-T03): write role absent, coding-agent fence, cas_required; ready_for_window if all hold.
|
||||
No write and no CCR digests; safe to repeat.
|
||||
Receipts hold closed booleans only. The journey values are read into memory to check their field names, and never
|
||||
printed or written.
|
||||
"""
|
||||
|
|
@ -279,6 +281,17 @@ def window_open(digests, admission_path, receipt, now=None):
|
|||
ccr_sha256=digests)
|
||||
|
||||
|
||||
def status(receipt):
|
||||
"""Read-only (NK-WP-0058-T03 readiness): no write, no role change, no CCR digests. Every check is reported, not
|
||||
only the first that fails. `ready_for_window` means a window may be opened: the write role is absent, the fence
|
||||
holds and cas_required is set on the bind key."""
|
||||
receipt['phase'] = 'identity'; attended_platform_admin()
|
||||
receipt['phase'] = 'read'
|
||||
checks = {'write_role_absent': write_role_absent(), 'agent_fence_present': fence_present(),
|
||||
'cas_required': cas_required()}
|
||||
receipt.update(phase='done', status='status_read', **checks, ready_for_window=all(checks.values()))
|
||||
|
||||
|
||||
def window_close(admission_path, receipt):
|
||||
"""Delete the write role after the run's receipt or at window end (whichever comes first) and prove it is gone.
|
||||
Needs no open window: closing late must always be possible."""
|
||||
|
|
@ -312,7 +325,7 @@ def reason_of(error):
|
|||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument('action', choices=['setup', 'verify', 'window-open', 'window-close'])
|
||||
p.add_argument('action', choices=['setup', 'verify', 'window-open', 'window-close', 'status'])
|
||||
p.add_argument('--write-ccr-sha256', help='sha256 of the CCR-2026-0058 file (all but window-close)')
|
||||
p.add_argument('--journey-ccr-sha256', help='sha256 of the CCR-2026-0059 file (all but window-close)')
|
||||
p.add_argument('--kubeconfig', help='admin kubeconfig; required for verify')
|
||||
|
|
@ -321,7 +334,7 @@ def main():
|
|||
a = p.parse_args()
|
||||
if a.action == 'verify' and not a.kubeconfig: p.error('--kubeconfig is required for verify')
|
||||
if a.action.startswith('window-') and not a.admission: p.error('--admission is required for ' + a.action)
|
||||
if a.action != 'window-close' and not (a.write_ccr_sha256 and a.journey_ccr_sha256):
|
||||
if a.action not in ('window-close', 'status') and not (a.write_ccr_sha256 and a.journey_ccr_sha256):
|
||||
p.error('--write-ccr-sha256 and --journey-ccr-sha256 are required for ' + a.action)
|
||||
digests = {'write': a.write_ccr_sha256, 'journey': a.journey_ccr_sha256}
|
||||
fd = os.open(a.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
||||
|
|
@ -331,13 +344,15 @@ def main():
|
|||
if a.action == 'setup': setup(digests, r)
|
||||
elif a.action == 'verify': verify(digests, a.kubeconfig, r)
|
||||
elif a.action == 'window-open': window_open(digests, a.admission, r)
|
||||
elif a.action == 'status': status(r)
|
||||
else: window_close(a.admission, r)
|
||||
except Exception as error:
|
||||
r.update(status='failed', reason=reason_of(error))
|
||||
if r['reason'] in HINTS: r['hint'] = HINTS[r['reason']]
|
||||
finally:
|
||||
with os.fdopen(fd, 'w') as out: json.dump(r, out, indent=2, sort_keys=True); out.write('\n')
|
||||
print(json.dumps({k: r[k] for k in ('status', 'phase', 'reason', 'hint') if k in r}))
|
||||
print(json.dumps({k: r[k] for k in ('status', 'phase', 'reason', 'hint', 'ready_for_window', 'write_role_absent')
|
||||
if k in r}))
|
||||
return int(r['status'] == 'failed')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ duplicates in other repos, a removal request is sent to their owners and recorde
|
|||
|
||||
```task
|
||||
id: NK-WP-0058-T03
|
||||
status: progress
|
||||
status: done
|
||||
priority: high
|
||||
assignee: claude-code
|
||||
depends_on: [NK-WP-0058-T01, NK-WP-0058-T09]
|
||||
|
|
@ -307,6 +307,38 @@ read-only founder command that supplies it.
|
|||
**Done when:** the readiness check is tested on stubs, runs with the agent identity wherever possible, and
|
||||
`--preflight-only` reports every failure it finds by phase and endpoint.
|
||||
|
||||
**Result, 2026-11-05.** Three steps.
|
||||
|
||||
1. **Named causes.**
|
||||
- `executor-lanes.py`'s identity phase distinguishes `openbao_unreachable`, `openbao_sealed` and
|
||||
`platform_admin_login_required`, with a hint. `LaneError` keeps its fixed code.
|
||||
- `approval_entry:<approver_not_listed|not_human|not_aal2|self_approval>`.
|
||||
2. **`cli.py --preflight-only`.**
|
||||
- It checks reachability per endpoint, then sets up without the write role, Kubernetes or directory clients.
|
||||
- It runs all seven preflight checks by phase, with no consume, emit or write. Exit 0 ready, 4 not.
|
||||
- The real preflight uses the same named checks.
|
||||
3. **The readiness command.**
|
||||
- `tools/bounded-execution-readiness/ready.py` (`make bounded-execution-ready`) runs with the agent identity
|
||||
from the owners' source. It checks:
|
||||
- the admission;
|
||||
- the approver chain at both the Argo-pinned revision and main of informed-decision;
|
||||
- the access-engine profile against the registry;
|
||||
- the CCRs, against the owner's `RUNBOOK_ALLOWED_STATUSES`;
|
||||
- the case, and the type bytes against the broker's pin.
|
||||
- It names three read-only founder commands for the rest:
|
||||
- `executor-lanes.py status` (new: write role, fence, `cas_required`);
|
||||
- rapp `make preflight` (new: the Job runs `--preflight-only` in the pinned image, with only the openbao
|
||||
token);
|
||||
- the ExternalSecret `Ready` reads.
|
||||
- A replay test shows it catches 2026-11-04: routing to `platform-root` on main while the Argo pin lagged.
|
||||
- Against the real sources it passes for BEA-2026-0003.
|
||||
|
||||
Tests: 125 in `make bounded-executor-test`, 31 in rapp `make test`. R1 needs no pin record: the preflight's `source`
|
||||
check compares the admission's pins with the files in the pinned image.
|
||||
|
||||
**Before first live use,** the executor changed, so a new image build and a rapp re-pin are needed. That is a
|
||||
founder step for the next admission. The readiness check is not yet in CI; that is part of T02.
|
||||
|
||||
### Founder interface: few, short, stateful steps
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue