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
|
Do not print login responses. Publish the consumer contract only after those
|
||||||
checks pass. A conflicting existing mount/role requires review, not overwrite.
|
checks pass. A conflicting existing mount/role requires review, not overwrite.
|
||||||
No service-client secret is created, moved or requested by this source return.
|
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.
|
audience openbao, 15m ttl/max/explicit max, no default policy). Not applied.
|
||||||
Open: attended apply, then the founder-attended KV seed (plan row 7).
|
Open: attended apply, then the founder-attended KV seed (plan row 7).
|
||||||
No RBAC change needed here (see message).
|
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"
|
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
|
status: wait
|
||||||
needs_human: true
|
needs_human: true
|
||||||
decision_id: 62aa95e4-3ccc-4867-8806-5e3db005b483
|
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
|
priority: high
|
||||||
state_hub_task_id: "e0c82ea9-05a5-537e-8fbf-67534263572e"
|
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: 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
|
## Implement the platform operator-write CCR contract and Fluid lane
|
||||||
|
|
||||||
|
|
@ -439,13 +440,17 @@ into this client-identity grant.
|
||||||
id: RPF-WP-0035-T08
|
id: RPF-WP-0035-T08
|
||||||
status: wait
|
status: wait
|
||||||
needs_human: true
|
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
|
priority: high
|
||||||
assignee: railiance-platform
|
assignee: railiance-platform
|
||||||
intervention_note: null
|
intervention_note: null
|
||||||
state_hub_task_id: "0ef52c26-2669-5cd1-8149-b1f6fcdda6cb"
|
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
|
Consume AUDIT-WP-0009-T09/T11's exact declarations for approval-engine and
|
||||||
informed-decision: tenant:platform, write-only, load-bearing, redact. Source
|
informed-decision: tenant:platform, write-only, load-bearing, redact. Source
|
||||||
preparation supplies two approved CCRs, independent exact-path policies/ESO
|
preparation supplies two approved CCRs, independent exact-path policies/ESO
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue