Handle actual Bao absence response and retain safe attempt phases
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-04 23:13:47 +02:00
parent a7bc33b867
commit f0c9a15235
4 changed files with 133 additions and 11 deletions

View file

@ -30,3 +30,12 @@ After successful metadata execution, continue owner-controlled ESO deployment,
exact workload positive/negative proof, mounted projection and requester exchange.
Keep the receipt-contract checks pending until observed. No credential rotation
is included. Checklist complete: false; no live metadata applied by the agent.
## Helper repair after exit 11
The first attended attempt reported child_failed. Installed CLI testing exposed
an absence-diagnostic mismatch, now fixed. The exact CCR digest and command
above remain valid. The child compares both current objects before any writes,
and now records fixed phase/exit metadata under
`docs/evidence/requester-metadata/`. No CLI output or credential data is retained.
Report only the Warden exit code; the agent can inspect the bounded local receipt.

View file

@ -3,6 +3,7 @@
Does not read/write KV values, change issuer custody, or deploy Kubernetes objects.
"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
@ -10,6 +11,7 @@ import os
from pathlib import Path
import subprocess
import sys
import uuid
ROOT = Path(__file__).resolve().parents[1]
CCR = ROOT / 'credential-change-requests/CCR-2026-0032.yaml'
@ -29,8 +31,12 @@ def cli(arguments, payload=None, *, missing_ok=False):
except (OSError, subprocess.TimeoutExpired):
raise Refused(4) from None
if result.returncode:
# Only a structured API 404 is absence. Denial/transport failure is not.
if missing_ok and result.returncode == 2 and b'Code: 404' in result.stderr:
# bao read turns an empty 404 into this exact path-specific diagnostic.
# Do not treat arbitrary stderr containing 404, denial or timeout as absence.
if (missing_ok and arguments[:2] == ['read', '-format=json']
and len(arguments) == 3 and result.returncode == 2
and not result.stdout
and result.stderr.strip() == ('No value found at ' + arguments[2]).encode()):
return None
raise Refused(4)
if arguments[0] == 'write' and not result.stdout.strip():
@ -86,8 +92,10 @@ def source(expected_digest):
return policy, payload
def apply(policy, payload, *, command=cli):
def apply(policy, payload, *, command=cli, phase=lambda value: None):
phase("read_policy")
current_policy = command(['read', '-format=json', 'sys/policies/acl/' + POLICY], missing_ok=True)
phase('read_role')
current_role = command(['read', '-format=json', ROLE], missing_ok=True)
# Check BOTH before mutating either; differing objects require a fresh review.
if current_policy is not None and current_policy.get('data', {}).get('policy') != policy:
@ -95,11 +103,15 @@ def apply(policy, payload, *, command=cli):
if current_role is not None and not role_matches(current_role.get('data')):
raise Refused(3)
if current_policy is None:
phase('write_policy')
command(['write', '-format=json', 'sys/policies/acl/' + POLICY, '-'],
json.dumps({'policy': policy}).encode())
if current_role is None:
phase('write_role')
command(['write', '-format=json', ROLE, '-'], json.dumps(payload).encode())
phase('verify_policy')
verified_policy = command(['read', '-format=json', 'sys/policies/acl/' + POLICY])
phase('verify_role')
verified_role = command(['read', '-format=json', ROLE])
if (verified_policy.get('data', {}).get('policy') != policy
or not role_matches(verified_role.get('data'))):
@ -107,6 +119,10 @@ def apply(policy, payload, *, command=cli):
def main():
receipt = {'schema': 'requester-metadata-attempt.v1', 'ccr': 'CCR-2026-0032',
'phase': 'arguments', 'exit_code': 2, 'native_delivery_verified': False}
def phase(value):
receipt['phase'] = value
try:
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--expected-ccr-sha256', required=True)
@ -118,20 +134,39 @@ def main():
args = parser.parse_args()
finally:
sys.stderr = original
phase('attended_boundary')
if (Path.home().parent.name != '.warden-attended-login'
or os.getenv('BAO_TOKEN') or os.getenv('VAULT_TOKEN')):
raise Refused(2)
phase('source')
policy, payload = source(args.expected_ccr_sha256)
phase('identity')
identity = cli(['token', 'lookup', '-format=json']).get('data', {})
policies = identity.get('policies', [])
if 'platform-admin' not in policies or 'root' in policies:
raise Refused(2)
apply(policy, payload)
apply(policy, payload, phase=phase)
phase('metadata_verified')
receipt['exit_code'] = 0
return 0
except Refused as exc:
receipt['exit_code'] = exc.code
return exc.code
except BaseException:
return 2
finally:
# Fixed metadata only. Never persist CLI bytes, exception text or identity data.
try:
directory = ROOT / 'docs/evidence/requester-metadata'
directory.mkdir(parents=True, exist_ok=True)
receipt['at'] = datetime.now(timezone.utc).isoformat()
path = directory / (uuid.uuid4().hex + '.json')
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, 'w') as stream:
json.dump(receipt, stream, sort_keys=True)
stream.write('\n')
except BaseException:
return 6 # Receipt could not be retained; metadata may have changed.
if __name__ == '__main__':

View file

@ -5,6 +5,11 @@ import json
from pathlib import Path
import subprocess
import unittest
import tempfile
import http.server
import threading
import shutil
import os
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('requester_metadata', Path(__file__).resolve().parents[1] / 'scripts/informed_decision_requester_metadata.py')
@ -89,8 +94,8 @@ class MetadataTests(unittest.TestCase):
self.assertEqual(caught.exception.code, 5)
def test_404_only_is_absence_and_failures_are_silent(self):
for code, stderr, missing in [(2, b'Code: 404', True), (2, b'Code: 403 SECRET-SENTINEL', False), (1, b'Code: 404 SECRET-SENTINEL', False)]:
with self.subTest(code=code, stderr=stderr), patch.object(m.subprocess, 'run', return_value=subprocess.CompletedProcess([], code, b'SECRET-SENTINEL', stderr)):
for code, stderr, missing in [(2, ('No value found at '+m.ROLE+'\n').encode(), True), (2, b'Code: 404', False), (2, b'Code: 403 SECRET-SENTINEL', False), (1, ('No value found at '+m.ROLE).encode(), False), (2, b'No value found at wrong-path', False)]:
with self.subTest(code=code, stderr=stderr), patch.object(m.subprocess, 'run', return_value=subprocess.CompletedProcess([], code, b'', stderr)):
output = io.StringIO()
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
if missing: self.assertIsNone(m.cli(['read', '-format=json', m.ROLE], missing_ok=True))
@ -103,17 +108,74 @@ class MetadataTests(unittest.TestCase):
self.assertEqual(m.cli(['write', '-format=json', m.ROLE, '-'], b'{}'), {})
def test_proposed_source_refuses_before_network(self):
digest = m.hashlib.sha256(m.CCR.read_bytes()).hexdigest()
with patch.object(m.subprocess, 'run', side_effect=AssertionError('must remain offline')):
with self.assertRaises(m.Refused) as caught: m.source(digest)
self.assertEqual(caught.exception.code, 2)
with tempfile.TemporaryDirectory() as tmp:
ccr = Path(tmp) / 'proposed.yaml'
ccr.write_text(m.CCR.read_text().replace('status: approved', 'status: proposed'))
digest = m.hashlib.sha256(ccr.read_bytes()).hexdigest()
with patch.object(m, 'CCR', ccr), patch.object(m.subprocess, 'run', side_effect=AssertionError('must remain offline')):
with self.assertRaises(m.Refused) as caught: m.source(digest)
self.assertEqual(caught.exception.code, 2)
@unittest.skipUnless(shutil.which('bao'), 'installed bao CLI required')
def test_installed_cli_missing_create_readback_and_idempotent_retry(self):
objects, writes = {}, []
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
value = objects.get(self.path)
self.send_response(200 if value else 404)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'data': value} if value else {'errors': []}).encode())
def do_PUT(self):
value = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
writes.append(self.path)
objects[self.path] = value if '/sys/' in self.path else role()
self.send_response(204)
self.end_headers()
do_POST = do_PUT
def log_message(self, *args): pass
server = http.server.HTTPServer(('127.0.0.1', 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
with tempfile.TemporaryDirectory() as tmp, patch.dict(os.environ, {
'PATH': os.environ['PATH'], 'HOME': tmp,
'BAO_ADDR': f'http://127.0.0.1:{server.server_port}',
'BAO_TOKEN': 'synthetic-test-only',
}, clear=True):
m.apply('exact policy', {'audience': 'openbao'})
m.apply('exact policy', {'audience': 'openbao'})
self.assertEqual(writes, ['/v1/sys/policies/acl/' + m.POLICY, '/v1/' + m.ROLE])
finally:
server.shutdown()
server.server_close()
thread.join()
def test_failed_step_receipt_contains_only_fixed_metadata(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(m, 'ROOT', Path(tmp)), \
patch.object(m.sys, 'argv', ['child', '--expected-ccr-sha256', 'fixture']), \
patch.object(m.Path, 'home', return_value=Path('/tmp/.warden-attended-login/fixture')), \
patch.dict(os.environ, {}, clear=True), \
patch.object(m, 'source', return_value=('policy', {})), \
patch.object(m, 'cli', side_effect=m.Refused(4)):
output = io.StringIO()
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
self.assertEqual(m.main(), 4)
self.assertEqual(output.getvalue(), '')
receipt = json.loads(next((Path(tmp)/'docs/evidence/requester-metadata').glob('*.json')).read_text())
self.assertEqual(receipt['phase'], 'identity')
self.assertEqual(receipt['exit_code'], 4)
self.assertEqual(set(receipt), {'schema', 'ccr', 'phase', 'exit_code', 'native_delivery_verified', 'at'})
def test_missing_arguments_silent(self):
with patch.object(m.sys, 'argv', ['metadata-child']):
with tempfile.TemporaryDirectory() as tmp, patch.object(m, 'ROOT', Path(tmp)), patch.object(m.sys, 'argv', ['metadata-child']):
output = io.StringIO()
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(output):
self.assertEqual(m.main(), 2)
self.assertEqual(output.getvalue(), '')
receipt = json.loads(next((Path(tmp)/'docs/evidence/requester-metadata').glob('*.json')).read_text())
self.assertEqual(receipt['phase'], 'arguments')
self.assertEqual(receipt['exit_code'], 2)
if __name__ == '__main__':

View file

@ -122,3 +122,19 @@ exact ESO identity, audience openbao, 900-second limits and read-only projection
The metadata child still requires the founder terminal OIDC/MFA. Native claims,
delivery, negative proofs and revocation remain unproven under T03. No broader
cutover or issuer rotation approval is inferred.
## Attended child failure and bounded repair — 2026-10-04
Founder reported Warden exit 11; the local audit confirms child_failed. This
does not prove that metadata stayed unchanged or that revocation succeeded.
The installed bao binary against a synthetic HTTP 404 reproduces a helper bug:
it emits exact path-specific No value found, not Code: 404. Updated handling
accepts only the exact read diagnostic with exit 2 and empty stdout; denials,
wrong paths and other failures refuse. Real installed-CLI tests cover missing
objects, both writes/readbacks and an idempotent repeat without another write.
Each child now retains only fixed phase/exit metadata in
docs/evidence/requester-metadata/*.json and remains silent. Twelve focused tests
pass. The approved CCR and permissions are unchanged. A fresh attended attempt
will compare both objects before any missing-only write and record its phase.
Native delivery remains pending; do not mark the earlier attempt successful.