diff --git a/scripts/custody-describe.py b/scripts/custody-describe.py index 0e8f210..ea1cff3 100755 --- a/scripts/custody-describe.py +++ b/scripts/custody-describe.py @@ -12,7 +12,7 @@ because a confidently wrong `used_by` is worse than an empty one. `apply` writes custom_metadata only (`bao kv metadata patch`), for entries marked `review: accepted` with all five fields filled. It never reads or writes -a value, prints nothing but paths and booleans, and runs under the founder's +a value, prints nothing (results go to a 0600 receipt), and runs under the founder's admin session (warden access openbao-platform-admin-login --exec), since ops-mason-survey cannot write metadata. """ @@ -121,20 +121,31 @@ def check(doc: dict) -> int: return 0 if bad == 0 else 1 -def apply(doc: dict) -> int: - failed = 0 +RECEIPT = os.path.join(HOME, ".local/state/railiance/receipts/custody-describe.json") + + +def apply(doc: dict, receipt: str = RECEIPT) -> int: + """Silent: the warden wrapper refuses any output, so results go to a 0600 receipt.""" + import json + results = {} for path, e in doc.items(): if e.get("review") != "accepted" or any(not e.get(k) for k in REQUIRED): - print(f"skip {path}") + results[path] = "skipped" continue mount, rest = path.split("/", 1) args = ["bao", "kv", "metadata", "patch", f"-mount={mount}"] args += [f"-custom-metadata={k}={_clip(e[k])}" for k in REQUIRED] args.append(rest) ok = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0 - failed += not ok - print(f"{'patched' if ok else 'FAILED '} {path}") - return 1 if failed else 0 + results[path] = "patched" if ok else "failed" + os.makedirs(os.path.dirname(receipt), mode=0o700, exist_ok=True) + old_umask = os.umask(0o077) + try: + with open(receipt, "w") as f: + json.dump(results, f, indent=1, sort_keys=True) + finally: + os.umask(old_umask) + return 1 if "failed" in results.values() else 0 def main(argv: list[str]) -> int: diff --git a/tests/test_custody_describe.py b/tests/test_custody_describe.py index 6591793..220cae8 100644 --- a/tests/test_custody_describe.py +++ b/tests/test_custody_describe.py @@ -9,14 +9,19 @@ _spec.loader.exec_module(cd) FULL = dict(description="d", owner="o", used_by="u", rotation="r", on_loss="l") -def test_apply_patches_only_accepted_complete_entries_and_only_metadata(): +def test_apply_patches_only_accepted_complete_entries_and_only_metadata(tmp_path, capsys): doc = { "platform/workloads/a/b": {"review": "accepted", **FULL}, "platform/workloads/c/d": {"review": "draft", **FULL}, "operators/e": {"review": "accepted", **dict(FULL, used_by="")}, } with patch.object(cd.subprocess, "run", return_value=MagicMock(returncode=0)) as run: - assert cd.apply(doc) == 0 + assert cd.apply(doc, receipt=str(tmp_path / 'r.json')) == 0 + assert capsys.readouterr() == ('', '') + import json, os, stat + r = tmp_path / 'r.json' + assert json.load(open(r)) == {'platform/workloads/a/b': 'patched', 'platform/workloads/c/d': 'skipped', 'operators/e': 'skipped'} + assert stat.S_IMODE(os.stat(r).st_mode) == 0o600 assert run.call_count == 1 args = run.call_args.args[0] assert args[:5] == ["bao", "kv", "metadata", "patch", "-mount=platform"] @@ -25,10 +30,11 @@ def test_apply_patches_only_accepted_complete_entries_and_only_metadata(): assert run.call_args.kwargs["stdout"] is cd.subprocess.DEVNULL and run.call_args.kwargs["stderr"] is cd.subprocess.DEVNULL -def test_apply_reports_failure_without_output(): +def test_apply_reports_failure_without_output(tmp_path, capsys): doc = {"platform/workloads/a/b": {"review": "accepted", **FULL}} with patch.object(cd.subprocess, "run", return_value=MagicMock(returncode=2)): - assert cd.apply(doc) == 1 + assert cd.apply(doc, receipt=str(tmp_path / 'r.json')) == 1 + assert capsys.readouterr() == ('', '') def test_check_refuses_drafts_missing_and_overlong_fields(): diff --git a/workplans/MASON-WP-0004-credential-inventory-descriptions.md b/workplans/MASON-WP-0004-credential-inventory-descriptions.md index e1d0ee0..9564532 100644 --- a/workplans/MASON-WP-0004-credential-inventory-descriptions.md +++ b/workplans/MASON-WP-0004-credential-inventory-descriptions.md @@ -115,9 +115,12 @@ have gaps left empty on purpose (nextcloud x2, keycape-factor-issuer, grafana-admin, smtp, and four `used_by`). Every entry is `review: draft`. `apply` writes custom_metadata only, for `review: accepted` entries, silently (`tests/test_custody_describe.py`, suite 157 passed). Founder run, after -review: `warden access openbao-platform-admin-login --exec -- python3 +review: `warden access openbao-platform-admin-login --exec -- sh -c 'python3 /home/worsch/ops-mason/scripts/custody-describe.py apply -/home/worsch/ops-mason/manifests/custody-descriptions.yaml`. +/home/worsch/ops-mason/manifests/custody-descriptions.yaml >/dev/null 2>&1'`, +then read `~/.local/state/railiance/receipts/custody-describe.json`. +(First attempt 2026-10-09 refused by the wrapper because apply printed status +lines; nothing written, all entries were still drafts. apply is now silent.) ## Resolve the rest with their owners