CUST-WP-0075: tenant-engine sender seed helper; re-qualify RPF-WP-0035-T02/T08
- scripts/tenant_engine_audit_core_sender_seed.py: silent attended row-7 seed for MASON-WP-0007-T05 (wrap-migrate technique, CAS 0, never overwrites, metadata-only --dry-run, documented exit codes), with offline fake-bao tests. - Lane review doc with the exact founder sequence and the read-only ESO token-create can-i check for MASON-WP-0007-T06. - RPF-WP-0035-T02: SECRETS-WP-0007-T04 is done; real blocker is live KeyCape registration and client custody of secrets-engine-openbao (blocked_on key-cape). Design stays proposed. - RPF-WP-0035-T08: receiver rollout with evidence_kind already accepted 2026-09-11; moved the misplaced D1 correction note here from T02. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 3775@bnt-lap001 Assistant-Session: cfacb6a5-ad5b-4f79-ac67-053e3b0d34a3
This commit is contained in:
parent
401765f184
commit
e7eb9db57e
6 changed files with 512 additions and 3 deletions
|
|
@ -120,3 +120,12 @@ then perform the positive/negative/expiry/use-limit/revocation checks above.
|
|||
Do not print login responses. Publish the consumer contract only after those
|
||||
checks pass. A conflicting existing mount/role requires review, not overwrite.
|
||||
No service-client secret is created, moved or requested by this source return.
|
||||
|
||||
## Gate check — 2026-10-03 (CUST-WP-0075)
|
||||
|
||||
Still proposed. SECRETS-WP-0007-T04 is done (2026-09-27), so the gate named
|
||||
above is open, but the "before writing" preconditions are not: KeyCape has not
|
||||
claimed a live registration of `secrets-engine-openbao`, and its client-credential
|
||||
custody has no owning task (KeyCape contract: "a separate approved custody
|
||||
action"). No reviewed child is written. RPF-WP-0035-T02 waits on key-cape naming
|
||||
that task.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
# tenant-engine audit-core sender: attended seed
|
||||
|
||||
Status: ready for review, not run. Owner: railiance-platform (OpenBao side) for
|
||||
ops-mason MASON-WP-0007-T05 (plan row 7). Intake RPF-IN-0002. Prepared
|
||||
2026-10-03 under CUST-WP-0075.
|
||||
|
||||
## What the founder approves
|
||||
|
||||
One attended sitting, run from a railiance-platform checkout at the reviewed
|
||||
commit, with these three children in order. Each child prints nothing; read
|
||||
warden's printed line, not its exit code (orientation §5).
|
||||
|
||||
```sh
|
||||
cd ~/railiance-platform
|
||||
export BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200
|
||||
|
||||
# 1. policy + Kubernetes-auth role (commit 8c44256). Exit 3 = existing object differs.
|
||||
warden access openbao-platform-admin-login --exec -- \
|
||||
sh scripts/openbao-tenant-engine-audit-core-sender-eso-attended.sh
|
||||
|
||||
# 2. seed dry-run: KV metadata only, no value read or written.
|
||||
warden access openbao-platform-admin-login --exec -- \
|
||||
python3 scripts/tenant_engine_audit_core_sender_seed.py --dry-run \
|
||||
--confirm 'SEED tenant-engine audit-core-sender' \
|
||||
--receipt docs/evidence/tenant-engine-sender-seed-dry-run.json
|
||||
|
||||
# 3. seed: only if step 2's receipt says outcome dry_run_would_write (exit 0).
|
||||
warden access openbao-platform-admin-login --exec -- \
|
||||
python3 scripts/tenant_engine_audit_core_sender_seed.py \
|
||||
--confirm 'SEED tenant-engine audit-core-sender' \
|
||||
--receipt docs/evidence/tenant-engine-sender-seed.json
|
||||
```
|
||||
|
||||
Each `warden access` is its own login. Receipts are created mode 0600, refused
|
||||
if the file exists, and hold only paths, versions, booleans and an outcome
|
||||
code. Commit them as evidence.
|
||||
|
||||
## What the seed does
|
||||
|
||||
`scripts/tenant_engine_audit_core_sender_seed.py` is the user-engine /
|
||||
audit-core wrap-migrate technique (ops-mason
|
||||
`plans/user-engine-openbao-runtime-custody.md` §6; audit-core runbook,
|
||||
"wrap-migrated on 2026-08-14"): one platform-admin process reads the source and
|
||||
writes the target, and the value travels only through process memory and the
|
||||
`bao` child's stdin. Never argv, stdout, stderr, a file or the receipt.
|
||||
|
||||
1. Refuses unless `--confirm` is exact, HOME is a warden attended envelope, no
|
||||
`BAO_TOKEN`/`VAULT_TOKEN` is set, and the identity has `platform-admin` and
|
||||
not `root`.
|
||||
2. Reads `platform/workloads/audit-core/senders` (KV v2, field `senders.json`)
|
||||
and selects the one row named `tenant-engine`. Refuses if the row is
|
||||
missing or duplicated, holds zero or more than one token (a rotation
|
||||
overlap is in progress: finish it first), or is not write-only
|
||||
(`may_write` true, `may_read` false).
|
||||
3. If `platform/workloads/tenant-engine/audit-core-sender` exists: exit 0 when
|
||||
it holds exactly `{token: <that token>}`, otherwise exit 3. Never overwrites.
|
||||
4. Otherwise writes `{token}` with `cas=0`, reads it back (must be version 1 and
|
||||
equal), and checks that the registry version did not move during the run.
|
||||
|
||||
The registry is never written. Exit codes: 0 ok (written or already
|
||||
identical; dry-run: would write), 1 contained internal error, 2 precondition
|
||||
refused before any write, 3 target exists and differs, 4 write failed,
|
||||
5 verify failed, 6 dry-run found the target already present.
|
||||
|
||||
Tests: `tests/test_tenant_engine_audit_core_sender_seed.py` runs the helper as
|
||||
a subprocess against a fake `bao` and asserts empty stdout/stderr, the token
|
||||
never in `bao` argv or the receipt, CAS 0, idempotence, refusal of a differing
|
||||
or deleted target, every precondition, and distinct write/verify codes.
|
||||
|
||||
## Operator check for MASON-WP-0007-T06 (read-only, not run)
|
||||
|
||||
Before the tenant-engine ClusterSecretStore goes live, the ESO controller must
|
||||
be allowed to create ServiceAccount tokens in `tenant-engine`. Confirm the node
|
||||
IP is `92.205.62.239` first (`kubectl get nodes -o wide`), then:
|
||||
|
||||
```sh
|
||||
kubectl auth can-i create serviceaccounts --subresource=token \
|
||||
-n tenant-engine --as=system:serviceaccount:external-secrets:external-secrets
|
||||
```
|
||||
|
||||
`yes` is required. This is a SubjectAccessReview: it reads no object. A `no`
|
||||
is a railiance-cluster RBAC matter, not a reason to widen the OpenBao role.
|
||||
|
||||
## Follow-ups outside the seed
|
||||
|
||||
- `openbao/policies/agent-high-risk-boundary.hcl` denies coding agents the
|
||||
sender registry, but not the new
|
||||
`platform/{data,metadata}/workloads/tenant-engine/audit-core-sender` paths.
|
||||
Adding those deny lines is a separate reviewed policy change.
|
||||
- Rotation stays overlap-first (plan §4): rotate in the registry, finish the
|
||||
overlap so the sender holds one token, then re-seed. Because this helper
|
||||
never overwrites, a rotated token needs a reviewed CAS-update variant. That
|
||||
variant is not built.
|
||||
|
|
@ -48,6 +48,9 @@ description: >-
|
|||
audience openbao, 15m ttl/max/explicit max, no default policy). Not applied.
|
||||
Open: attended apply, then the founder-attended KV seed (plan row 7).
|
||||
No RBAC change needed here (see message).
|
||||
2026-10-03 (CUST-WP-0075): row-7 seed helper scripts/tenant_engine_audit_core_sender_seed.py
|
||||
(silent, CAS 0, never overwrites, metadata-only --dry-run) and lane review
|
||||
docs/credential-lane-designs/tenant-engine-audit-core-sender-seed.md added; not run.
|
||||
state_hub_intake_id: "01a0fedd-7af5-7126-a263-647d438b068d"
|
||||
```
|
||||
|
||||
|
|
|
|||
216
scripts/tenant_engine_audit_core_sender_seed.py
Executable file
216
scripts/tenant_engine_audit_core_sender_seed.py
Executable file
|
|
@ -0,0 +1,216 @@
|
|||
#!/usr/bin/env python3
|
||||
"""MASON-WP-0007-T05 row 7: attended, silent seed of tenant-engine's sender token.
|
||||
|
||||
Copies the one token of sender ``tenant-engine`` from the audit-core sender
|
||||
registry (KV v2 ``platform/workloads/audit-core/senders``, field
|
||||
``senders.json``) to the dedicated KV v2 path
|
||||
``platform/workloads/tenant-engine/audit-core-sender`` (one field ``token``),
|
||||
which the ESO role ``tenant-engine-audit-core-sender-eso`` reads. This is the
|
||||
user-engine / audit-core wrap-migrate technique: one platform-admin process
|
||||
reads the source and writes the target; the value only ever travels through
|
||||
this process's memory and the ``bao`` child's stdin, never argv, stdout,
|
||||
stderr, a file or the receipt.
|
||||
|
||||
Run from a railiance-platform checkout at the reviewed commit, after
|
||||
``scripts/openbao-tenant-engine-audit-core-sender-eso-attended.sh``:
|
||||
|
||||
BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 \\
|
||||
warden access openbao-platform-admin-login --exec -- \\
|
||||
python3 scripts/tenant_engine_audit_core_sender_seed.py \\
|
||||
--confirm 'SEED tenant-engine audit-core-sender' \\
|
||||
--receipt "$HOME/te-sender-seed-receipt.json"
|
||||
|
||||
Add ``--dry-run`` to touch only KV metadata (no value is read or written).
|
||||
|
||||
The process prints nothing: stdout and stderr are redirected to /dev/null at
|
||||
start (warden fails closed on any child output). The optional ``--receipt``
|
||||
(created mode 0600, refused if it already exists) holds only versions,
|
||||
booleans and a fixed outcome code.
|
||||
|
||||
Exit codes:
|
||||
0 seed: written at version 1, or target already holds exactly this token
|
||||
dry-run: target absent, registry present; a seed would write
|
||||
1 unexpected internal error (contained; nothing is printed)
|
||||
2 precondition refused before any write: missing/wrong --confirm, not in a
|
||||
warden attended envelope, identity not platform-admin (or is root),
|
||||
registry absent or malformed, sender missing, sender holding zero or more
|
||||
than one token (rotation overlap in progress), sender read-capable,
|
||||
absence not provable, receipt path exists
|
||||
3 target exists and differs (other value, extra fields, deleted/destroyed
|
||||
current version); never overwritten
|
||||
4 write failed (CAS 0 refused or bao error)
|
||||
5 verify failed: readback differs or registry changed during the run
|
||||
6 dry-run only: target already exists (a seed run compares the value)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
SOURCE = 'workloads/audit-core/senders'
|
||||
TARGET = 'workloads/tenant-engine/audit-core-sender'
|
||||
SENDER = 'tenant-engine'
|
||||
FIELD = 'token'
|
||||
CONFIRM = 'SEED tenant-engine audit-core-sender'
|
||||
SCHEMA = 'platform.tenant-engine-audit-core-sender-seed.v1'
|
||||
|
||||
|
||||
class Exit(Exception):
|
||||
def __init__(self, code: int, reason: str):
|
||||
super().__init__(reason)
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def bao(args, payload=None):
|
||||
# Output is always captured; nothing from bao reaches the terminal.
|
||||
return subprocess.run(['bao', *args],
|
||||
input=None if payload is None else json.dumps(payload).encode(),
|
||||
capture_output=True, timeout=60)
|
||||
|
||||
|
||||
def read_json(path, *, absent_ok, code=2):
|
||||
"""Return parsed ``data`` of a read, None if provably absent, else refuse."""
|
||||
result = bao(['read', '-format=json', path])
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
return json.loads(result.stdout)['data']
|
||||
except (ValueError, KeyError, TypeError):
|
||||
raise Exit(code, 'unparseable_read') from None
|
||||
if absent_ok and (b'404' in result.stderr or b'No value found' in result.stderr):
|
||||
return None
|
||||
raise Exit(code, 'read_failed_or_absence_not_proven')
|
||||
|
||||
|
||||
def live_version(meta):
|
||||
"""Current version number if it is live, else None (deleted/destroyed)."""
|
||||
cv = meta.get('current_version')
|
||||
if type(cv) is not int or cv < 1:
|
||||
return None
|
||||
entry = (meta.get('versions') or {}).get(str(cv)) or {}
|
||||
if entry.get('destroyed') or entry.get('deletion_time'):
|
||||
return None
|
||||
return cv
|
||||
|
||||
|
||||
def registry_token():
|
||||
raw = read_json('platform/data/' + SOURCE, absent_ok=False)
|
||||
try:
|
||||
version = raw['metadata']['version']
|
||||
rows = json.loads(raw['data']['senders.json'])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
raise Exit(2, 'registry_shape_invalid') from None
|
||||
if type(version) is not int or not isinstance(rows, list):
|
||||
raise Exit(2, 'registry_shape_invalid')
|
||||
match = [r for r in rows if isinstance(r, dict) and r.get('name') == SENDER]
|
||||
if len(match) != 1:
|
||||
raise Exit(2, 'sender_missing_or_duplicate')
|
||||
row = match[0]
|
||||
tokens = row.get('tokens') or ([row['token']] if row.get('token') else [])
|
||||
if not isinstance(tokens, list) or len(tokens) != 1:
|
||||
raise Exit(2, 'sender_token_count_not_one')
|
||||
token = tokens[0]
|
||||
if not isinstance(token, str) or not token or token != token.strip():
|
||||
raise Exit(2, 'sender_token_shape')
|
||||
if row.get('may_read') is not False or row.get('may_write') is not True:
|
||||
raise Exit(2, 'sender_not_write_only')
|
||||
return version, token
|
||||
|
||||
|
||||
def registry_version():
|
||||
meta = read_json('platform/metadata/' + SOURCE, absent_ok=True)
|
||||
if meta is None or live_version(meta) is None:
|
||||
raise Exit(2, 'registry_absent')
|
||||
return meta['current_version']
|
||||
|
||||
|
||||
def run(args, receipt):
|
||||
if args.confirm != CONFIRM:
|
||||
raise Exit(2, 'exact_confirmation_required')
|
||||
if (Path.home().parent.name != '.warden-attended-login'
|
||||
or os.environ.get('BAO_TOKEN') or os.environ.get('VAULT_TOKEN')):
|
||||
raise Exit(2, 'attended_warden_envelope_required')
|
||||
ident = read_json('auth/token/lookup-self', absent_ok=False)
|
||||
policies = ident.get('policies') or []
|
||||
if 'platform-admin' not in policies or 'root' in policies:
|
||||
raise Exit(2, 'attended_platform_admin_required')
|
||||
|
||||
receipt['registry_version_before'] = registry_version()
|
||||
target_meta = read_json('platform/metadata/' + TARGET, absent_ok=True)
|
||||
receipt['target_present_before'] = target_meta is not None
|
||||
if args.dry_run:
|
||||
receipt['value_reads'] = 0
|
||||
if target_meta is not None:
|
||||
receipt['target_version_before'] = target_meta.get('current_version')
|
||||
raise Exit(6, 'dry_run_target_present')
|
||||
return 'dry_run_would_write'
|
||||
|
||||
version, token = registry_token()
|
||||
if version != receipt['registry_version_before']:
|
||||
raise Exit(5, 'registry_changed_during_run')
|
||||
if target_meta is not None:
|
||||
if live_version(target_meta) is None:
|
||||
raise Exit(3, 'target_current_version_not_live')
|
||||
current = read_json('platform/data/' + TARGET, absent_ok=False)
|
||||
if (current or {}).get('data') != {FIELD: token}:
|
||||
raise Exit(3, 'target_differs')
|
||||
receipt['target_version'] = target_meta['current_version']
|
||||
return 'already_identical'
|
||||
|
||||
result = bao(['write', '-format=json', 'platform/data/' + TARGET, '-'],
|
||||
payload={'options': {'cas': 0}, 'data': {FIELD: token}})
|
||||
if result.returncode != 0:
|
||||
raise Exit(4, 'write_failed')
|
||||
|
||||
back = read_json('platform/data/' + TARGET, absent_ok=False, code=5)
|
||||
if (back or {}).get('data') != {FIELD: token} or back['metadata'].get('version') != 1:
|
||||
raise Exit(5, 'readback_mismatch')
|
||||
if registry_version() != version:
|
||||
raise Exit(5, 'registry_changed_during_run')
|
||||
receipt['target_version'] = 1
|
||||
return 'written'
|
||||
|
||||
|
||||
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('--confirm', default='')
|
||||
parser.add_argument('--receipt', type=Path)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
try:
|
||||
args = parser.parse_args(argv)
|
||||
except SystemExit:
|
||||
return 2
|
||||
receipt = {'schema': SCHEMA, 'mode': 'dry-run' if args.dry_run else 'seed',
|
||||
'source': 'platform/' + SOURCE, 'target': 'platform/' + TARGET,
|
||||
'sender': SENDER, '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
|
||||
try:
|
||||
receipt['outcome'] = run(args, receipt)
|
||||
code = 0
|
||||
except Exit as exc:
|
||||
receipt['outcome'] = exc.reason
|
||||
code = exc.code
|
||||
except Exception:
|
||||
receipt['outcome'] = 'contained_internal_error'
|
||||
code = 1
|
||||
receipt['exit_code'] = code
|
||||
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())
|
||||
183
tests/test_tenant_engine_audit_core_sender_seed.py
Normal file
183
tests/test_tenant_engine_audit_core_sender_seed.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"""Offline tests for the tenant-engine sender seed helper against a fake ``bao``.
|
||||
|
||||
The helper runs as a real subprocess with a fake ``bao`` first on PATH, so the
|
||||
silence contract (empty stdout and stderr) is tested end to end.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / 'scripts/tenant_engine_audit_core_sender_seed.py'
|
||||
CONFIRM = 'SEED tenant-engine audit-core-sender'
|
||||
TOKEN = 'te-' + 'A1b2' * 12
|
||||
OTHER = 'op-' + 'Z9y8' * 12
|
||||
SRC = 'workloads/audit-core/senders'
|
||||
DST = 'workloads/tenant-engine/audit-core-sender'
|
||||
|
||||
FAKE_BAO = textwrap.dedent('''\
|
||||
#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
sf = os.environ['FAKE_BAO_STATE']
|
||||
st = json.load(open(sf))
|
||||
st['calls'].append(sys.argv[1:])
|
||||
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)
|
||||
a = sys.argv[1:]
|
||||
print('Success! noise', file=sys.stderr)
|
||||
if a[0] == 'read':
|
||||
p = a[-1]
|
||||
if p == 'auth/token/lookup-self':
|
||||
out({'data': st['identity']})
|
||||
mount, kind, rest = p.split('/', 2)
|
||||
e = st['kv'].get(rest)
|
||||
if not e: missing()
|
||||
cv = len(e['versions'])
|
||||
if kind == 'metadata':
|
||||
out({'data': {'current_version': cv, 'versions': {str(i + 1): v['meta'] for i, v in enumerate(e['versions'])}}})
|
||||
v = e['versions'][-1]
|
||||
if v['meta'].get('deletion_time'): missing()
|
||||
out({'data': {'data': v['data'], 'metadata': {'version': cv}}})
|
||||
if a[0] == 'write':
|
||||
p = a[-2].split('/', 2)[2]
|
||||
body = json.loads(sys.stdin.read())
|
||||
st['stdin'].append(body)
|
||||
if st.get('fail_write'): save(); sys.exit(2)
|
||||
e = st['kv'].setdefault(p, {'versions': []})
|
||||
if body['options']['cas'] != len(e['versions']): save(); sys.exit(2)
|
||||
e['versions'].append({'data': body['data'], 'meta': {}})
|
||||
if st.get('corrupt_after_write'): e['versions'][-1]['data'] = {'token': 'x'}
|
||||
out({'data': {'version': len(e['versions'])}})
|
||||
save(); sys.exit(9)
|
||||
''')
|
||||
|
||||
|
||||
def registry(rows):
|
||||
return {'versions': [{'data': {'senders.json': json.dumps(rows), 'other': 'keep'}, 'meta': {}}] * 9}
|
||||
|
||||
|
||||
def sender(name, tokens, may_read=False):
|
||||
return {'name': name, 'tokens': tokens, 'sources': [name], 'may_write': True, 'may_read': may_read}
|
||||
|
||||
|
||||
class SeedHelper(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
(self.tmp / 'bin').mkdir()
|
||||
fake = self.tmp / 'bin/bao'
|
||||
fake.write_text(FAKE_BAO)
|
||||
fake.chmod(0o755)
|
||||
self.home = self.tmp / '.warden-attended-login' / 'session'
|
||||
self.home.mkdir(parents=True)
|
||||
self.state = {'calls': [], 'stdin': [], 'identity': {'policies': ['default', 'platform-admin']},
|
||||
'kv': {SRC: registry([sender('operator', [OTHER], may_read=True),
|
||||
sender('tenant-engine', [TOKEN])])}}
|
||||
|
||||
def run_helper(self, *extra, confirm=CONFIRM, home=None, env_extra=None, receipt=None):
|
||||
sf = self.tmp / 'state.json'
|
||||
sf.write_text(json.dumps(self.state))
|
||||
preexisting = receipt is not None
|
||||
receipt = receipt or self.tmp / f'receipt-{len(list(self.tmp.glob("receipt-*")))}.json'
|
||||
env = {'PATH': f'{self.tmp / "bin"}:/usr/bin:/bin', 'HOME': str(home or self.home),
|
||||
'FAKE_BAO_STATE': str(sf), **(env_extra or {})}
|
||||
args = [sys.executable, str(SCRIPT), '--receipt', str(receipt), *extra]
|
||||
if confirm is not None:
|
||||
args += ['--confirm', confirm]
|
||||
r = subprocess.run(args, capture_output=True, env=env, timeout=60)
|
||||
self.assertEqual((r.stdout, r.stderr), (b'', b''), 'helper must be silent')
|
||||
self.state = json.loads(sf.read_text())
|
||||
text = receipt.read_text() if receipt.exists() and not preexisting else '{}'
|
||||
self.assertNotIn(TOKEN, text)
|
||||
self.assertNotIn(OTHER, text)
|
||||
for call in self.state['calls']:
|
||||
self.assertNotIn(TOKEN, ' '.join(call), 'value must never be in bao argv')
|
||||
if receipt.exists() and not preexisting:
|
||||
self.assertEqual(receipt.stat().st_mode & 0o777, 0o600)
|
||||
return r.returncode, json.loads(text)
|
||||
|
||||
def test_seed_writes_cas0_and_is_idempotent(self):
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome'], rec['target_version']), (0, 'written', 1))
|
||||
self.assertEqual(self.state['stdin'], [{'options': {'cas': 0}, 'data': {'token': TOKEN}}])
|
||||
self.assertEqual(self.state['kv'][DST]['versions'][0]['data'], {'token': TOKEN})
|
||||
self.assertEqual(len(self.state['kv'][SRC]['versions']), 9, 'registry untouched')
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (0, 'already_identical'))
|
||||
self.assertEqual(len(self.state['stdin']), 1, 'no second write')
|
||||
|
||||
def test_dry_run_reads_metadata_only(self):
|
||||
code, rec = self.run_helper('--dry-run')
|
||||
self.assertEqual((code, rec['outcome'], rec['value_reads']), (0, 'dry_run_would_write', 0))
|
||||
paths = [c[-1] for c in self.state['calls']]
|
||||
self.assertTrue(all(p.startswith('platform/metadata/') or p == 'auth/token/lookup-self' for p in paths), paths)
|
||||
self.assertFalse(any(c[0] == 'write' for c in self.state['calls']))
|
||||
self.state['kv'][DST] = {'versions': [{'data': {'token': TOKEN}, 'meta': {}}]}
|
||||
code, rec = self.run_helper('--dry-run')
|
||||
self.assertEqual((code, rec['outcome']), (6, 'dry_run_target_present'))
|
||||
|
||||
def test_existing_different_target_is_refused_not_overwritten(self):
|
||||
for data in ({'token': OTHER}, {'token': TOKEN, 'extra': '1'}):
|
||||
self.state['kv'][DST] = {'versions': [{'data': data, 'meta': {}}]}
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (3, 'target_differs'))
|
||||
self.assertEqual(self.state['stdin'], [])
|
||||
|
||||
def test_deleted_current_version_is_refused(self):
|
||||
self.state['kv'][DST] = {'versions': [{'data': {'token': TOKEN}, 'meta': {'deletion_time': '2026-10-01T00:00:00Z'}}]}
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (3, 'target_current_version_not_live'))
|
||||
|
||||
def test_preconditions_refuse_before_any_bao_call_or_write(self):
|
||||
code, rec = self.run_helper(confirm='yes')
|
||||
self.assertEqual((code, rec['outcome'], self.state['calls']), (2, 'exact_confirmation_required', []))
|
||||
code, rec = self.run_helper(home=self.tmp)
|
||||
self.assertEqual((code, rec['outcome']), (2, 'attended_warden_envelope_required'))
|
||||
code, rec = self.run_helper(env_extra={'BAO_TOKEN': 'x'})
|
||||
self.assertEqual((code, rec['outcome']), (2, 'attended_warden_envelope_required'))
|
||||
for policies in (['default'], ['platform-admin', 'root']):
|
||||
self.state['identity'] = {'policies': policies}
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (2, 'attended_platform_admin_required'))
|
||||
self.assertFalse(any(c[0] == 'write' for c in self.state['calls']))
|
||||
|
||||
def test_sender_shape_refusals(self):
|
||||
cases = {
|
||||
'sender_missing_or_duplicate': [sender('operator', [OTHER], may_read=True)],
|
||||
'sender_token_count_not_one': [sender('tenant-engine', [TOKEN, TOKEN + 'b'])],
|
||||
'sender_not_write_only': [sender('tenant-engine', [TOKEN], may_read=True)],
|
||||
}
|
||||
for reason, rows in cases.items():
|
||||
self.state['kv'][SRC] = registry(rows)
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (2, reason))
|
||||
self.assertNotIn(DST, self.state['kv'])
|
||||
del self.state['kv'][SRC]
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (2, 'registry_absent'))
|
||||
|
||||
def test_write_and_verify_failures_have_distinct_codes(self):
|
||||
self.state['fail_write'] = True
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (4, 'write_failed'))
|
||||
self.state['fail_write'] = False
|
||||
self.state['corrupt_after_write'] = True
|
||||
code, rec = self.run_helper()
|
||||
self.assertEqual((code, rec['outcome']), (5, 'readback_mismatch'))
|
||||
|
||||
def test_existing_receipt_is_never_overwritten(self):
|
||||
receipt = self.tmp / 'receipt-0.json'
|
||||
receipt.write_text('keep')
|
||||
code, _ = self.run_helper(receipt=receipt)
|
||||
self.assertEqual(code, 2)
|
||||
self.assertEqual(receipt.read_text(), 'keep')
|
||||
self.assertEqual(self.state['calls'], [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -46,7 +46,8 @@ id: RPF-WP-0035-T02
|
|||
status: wait
|
||||
needs_human: true
|
||||
decision_id: 62aa95e4-3ccc-4867-8806-5e3db005b483
|
||||
blocking_reason: "KEY-WP-0009 finished; remaining gate is human: attended platform provisioning of the exact JWT mount/role/policy; KeyCape issuer/JWKS and service registration under KEY-WP-0009."
|
||||
blocked_on: message-from:key-cape
|
||||
blocking_reason: "SECRETS-WP-0007-T04 done 2026-09-27, so that gate is open. Real blocker: live KeyCape registration of client secrets-engine-openbao and its protected client-credential custody (contract: separate approved custody action; no owning task id yet, asked key-cape 2026-10-03). The design requires both before any write; then a reviewed child and the attended founder apply."
|
||||
priority: high
|
||||
state_hub_task_id: "e0c82ea9-05a5-537e-8fbf-67534263572e"
|
||||
```
|
||||
|
|
@ -72,7 +73,7 @@ contract, and rollback/cleanup receipts contain no secret material.
|
|||
|
||||
2026-10-03: human gate routed to hub decision 62aa95e4-3ccc-4867-8806-5e3db005b483 (D1: Attended OpenBao provisioning window: one session clears the pending credential lanes) by CUST-WP-0075; resolving that decision raises C-40 here.
|
||||
|
||||
2026-10-03 correction (CUST-WP-0075, informed-decision sitting draft `55fb078`): the D1 routing was wrong. First provision and delivery already ran on 2026-09-11, with seed and delivery receipts, and the review packet says not to repeat them. The task is not a provisioning-window act. It was taken off decision D1, and its blocking_reason now names what actually remains. railiance-platform should re-qualify it further if the receiver rollout has since happened.
|
||||
2026-10-03 (CUST-WP-0075): gate check. SECRETS-WP-0007-T04 is done (2026-09-27), so the gate named in the design is open. It is not the last one: the design says to confirm the exact live client registration and protected client-side custody before writing, and positive-login proof needs a real KeyCape service JWT. KeyCape's contract (`openbao-service-auth-contract.md`) and KEY-WP-0009 claim source registration only; the live registration of `secrets-engine-openbao` and its client-credential custody are "a separate approved custody action" that no workplan task owns yet (SECRETS-WP-0008-T06 waits on this task). Re-qualified with `blocked_on: message-from:key-cape`; key-cape was asked for the owning task id, which will replace it as `depends_on`. The design stays proposed and no child command was written. Not approvable in D1 yet.
|
||||
|
||||
## Implement the platform operator-write CCR contract and Fluid lane
|
||||
|
||||
|
|
@ -439,13 +440,17 @@ into this client-identity grant.
|
|||
id: RPF-WP-0035-T08
|
||||
status: wait
|
||||
needs_human: true
|
||||
blocking_reason: "Provisioned and delivered 2026-09-11 (CCR-2026-0021/0022 applied); do not repeat. Remaining: native delivery/receiver-ingestion acceptance (receiver image must carry evidence_kind; Approval Engine namespace is its owner prerequisite), archive readback, and revocation of the interim audit bearer tokens."
|
||||
blocking_reason: "Provisioned and delivered 2026-09-11 (CCR-2026-0021/0022 applied); do not repeat. Receiver rollout with evidence_kind and native producer acceptance passed 2026-09-11. Remaining human acceptance: independent archive readback and revocation of the interim audit bearer tokens."
|
||||
priority: high
|
||||
assignee: railiance-platform
|
||||
intervention_note: null
|
||||
state_hub_task_id: "0ef52c26-2669-5cd1-8149-b1f6fcdda6cb"
|
||||
```
|
||||
|
||||
2026-10-03 correction (CUST-WP-0075, informed-decision sitting draft `55fb078`): the D1 routing was wrong. First provision and delivery already ran on 2026-09-11, with seed and delivery receipts, and the review packet says not to repeat them. The task is not a provisioning-window act. It was taken off decision D1, and its blocking_reason now names what actually remains. railiance-platform should re-qualify it further if the receiver rollout has since happened.
|
||||
|
||||
2026-10-03 (CUST-WP-0075): re-qualified. The receiver rollout already happened: Audit Core c82e0442de0f carries evidence_kind and native producer accepted/duplicate and scope/read-refusal checks passed 2026-09-11 10:42 UTC (`docs/credential-lane-designs/factory-audit-senders-review.md`, `factory-native-acceptance.md`). Remaining: independent archive readback and audit bearer-revocation acceptance. Still not a provisioning-window act.
|
||||
|
||||
Consume AUDIT-WP-0009-T09/T11's exact declarations for approval-engine and
|
||||
informed-decision: tenant:platform, write-only, load-bearing, redact. Source
|
||||
preparation supplies two approved CCRs, independent exact-path policies/ESO
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue