From 6c9b4500436f1dd69403b1b4417595662e768742 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 11:41:31 +0200 Subject: [PATCH 1/3] custody-describe apply: receipt under the real home, default BAO_ADDR The warden wrapper runs with a temporary HOME and without BAO_ADDR, so the second founder run exited non-zero and its receipt vanished with the temporary directory. Failures now record bao's exit code in the receipt. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 327203@bnt-lap001 Assistant-Session: 55028ae3-4914-4bd1-8f5c-815f3527aef0 --- scripts/custody-describe.py | 13 +++++++++---- tests/test_custody_describe.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/custody-describe.py b/scripts/custody-describe.py index ea1cff3..9274771 100755 --- a/scripts/custody-describe.py +++ b/scripts/custody-describe.py @@ -121,7 +121,10 @@ def check(doc: dict) -> int: return 0 if bad == 0 else 1 -RECEIPT = os.path.join(HOME, ".local/state/railiance/receipts/custody-describe.json") +# The warden wrapper runs with a temporary HOME, so resolve the real one. +import pwd +REAL_HOME = pwd.getpwuid(os.getuid()).pw_dir +RECEIPT = os.path.join(REAL_HOME, ".local/state/railiance/receipts/custody-describe.json") def apply(doc: dict, receipt: str = RECEIPT) -> int: @@ -136,8 +139,10 @@ def apply(doc: dict, receipt: str = RECEIPT) -> int: 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 - results[path] = "patched" if ok else "failed" + env = dict(os.environ) + env.setdefault("BAO_ADDR", "http://127.0.0.1:18200") + rc = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env).returncode + results[path] = "patched" if rc == 0 else f"failed rc={rc}" os.makedirs(os.path.dirname(receipt), mode=0o700, exist_ok=True) old_umask = os.umask(0o077) try: @@ -145,7 +150,7 @@ def apply(doc: dict, receipt: str = RECEIPT) -> int: json.dump(results, f, indent=1, sort_keys=True) finally: os.umask(old_umask) - return 1 if "failed" in results.values() else 0 + return 1 if any(v.startswith("failed") for v 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 220cae8..d7a9cc9 100644 --- a/tests/test_custody_describe.py +++ b/tests/test_custody_describe.py @@ -49,3 +49,13 @@ def test_check_refuses_drafts_missing_and_overlong_fields(): def test_clip_respects_openbao_value_limit(): assert len(cd._clip("y" * 2000)) == cd.MAX_VALUE + + +def test_receipt_ignores_temporary_home_and_bao_addr_defaults(tmp_path, monkeypatch): + import pwd, os + assert cd.RECEIPT.startswith(pwd.getpwuid(os.getuid()).pw_dir) + monkeypatch.delenv("BAO_ADDR", raising=False) + doc = {"platform/workloads/a/b": {"review": "accepted", **FULL}} + with patch.object(cd.subprocess, "run", return_value=MagicMock(returncode=0)) as run: + cd.apply(doc, receipt=str(tmp_path / "r.json")) + assert run.call_args.kwargs["env"]["BAO_ADDR"] == "http://127.0.0.1:18200" From 6bab3bbfdff68ac5d3f89d2ca7ccc90cabb931e1 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 11:45:51 +0200 Subject: [PATCH 2/3] custody-describe apply: record HTTP code, merge-put fallback when patch is refused Third founder run: every write exited 2 (bao's generic error code), cause not visible. The receipt now keeps only the HTTP status from stderr; on 403/405 for patch it reads metadata (never data), merges and writes it back whole, preserving custom_metadata set by earlier CCRs. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 327203@bnt-lap001 Assistant-Session: 55028ae3-4914-4bd1-8f5c-815f3527aef0 --- scripts/custody-describe.py | 37 ++++++++++++++++++----- tests/test_custody_describe.py | 54 ++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/scripts/custody-describe.py b/scripts/custody-describe.py index 9274771..ae59e80 100755 --- a/scripts/custody-describe.py +++ b/scripts/custody-describe.py @@ -127,22 +127,45 @@ REAL_HOME = pwd.getpwuid(os.getuid()).pw_dir RECEIPT = os.path.join(REAL_HOME, ".local/state/railiance/receipts/custody-describe.json") +def _bao(args: list[str], env: dict) -> tuple[int, str, str]: + """Run bao; return (rc, stdout, http_code). stderr is reduced to its HTTP code.""" + r = subprocess.run(["bao", *args], capture_output=True, text=True, env=env) + m = re.search(r"Code: (\d{3})", r.stderr or "") + return r.returncode, r.stdout, (m.group(1) if m else ("" if r.returncode == 0 else "no-code")) + + +def _write(mount: str, rest: str, fields: dict, env: dict) -> str: + flags = [f"-custom-metadata={k}={_clip(v)}" for k, v in fields.items()] + rc, _, code = _bao(["kv", "metadata", "patch", f"-mount={mount}", *flags, rest], env) + if rc == 0: + return "patched" + if code not in ("403", "405"): + return f"failed patch http={code}" + # No patch capability: read metadata (never data), merge, write it back whole, + # so custom_metadata set by earlier CCRs survives. + import json + rc, out, code = _bao(["kv", "metadata", "get", "-format=json", f"-mount={mount}", rest], env) + if rc != 0: + return f"failed metadata-get http={code}" + current = ((json.loads(out or "{}").get("data") or {}).get("custom_metadata")) or {} + merged = {**current, **{k: _clip(v) for k, v in fields.items()}} + flags = [f"-custom-metadata={k}={v}" for k, v in merged.items()] + rc, _, code = _bao(["kv", "metadata", "put", f"-mount={mount}", *flags, rest], env) + return "merged" if rc == 0 else f"failed put http={code}" + + def apply(doc: dict, receipt: str = RECEIPT) -> int: """Silent: the warden wrapper refuses any output, so results go to a 0600 receipt.""" import json + env = dict(os.environ) + env.setdefault("BAO_ADDR", "http://127.0.0.1:18200") results = {} for path, e in doc.items(): if e.get("review") != "accepted" or any(not e.get(k) for k in REQUIRED): 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) - env = dict(os.environ) - env.setdefault("BAO_ADDR", "http://127.0.0.1:18200") - rc = subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env).returncode - results[path] = "patched" if rc == 0 else f"failed rc={rc}" + results[path] = _write(mount, rest, {k: e[k] for k in REQUIRED}, env) os.makedirs(os.path.dirname(receipt), mode=0o700, exist_ok=True) old_umask = os.umask(0o077) try: diff --git a/tests/test_custody_describe.py b/tests/test_custody_describe.py index d7a9cc9..d357afb 100644 --- a/tests/test_custody_describe.py +++ b/tests/test_custody_describe.py @@ -1,4 +1,8 @@ import importlib.util +import json +import os +import pwd +import stat from pathlib import Path from unittest.mock import patch, MagicMock @@ -9,32 +13,47 @@ _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(tmp_path, capsys): +def _r(rc=0, out="", err=""): + return MagicMock(returncode=rc, stdout=out, stderr=err) + + +def test_apply_patches_only_accepted_complete_entries_silently(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, 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 + with patch.object(cd.subprocess, "run", return_value=_r()) as run: + assert cd.apply(doc, receipt=str(tmp_path / "r.json")) == 0 + assert capsys.readouterr() == ("", "") assert run.call_count == 1 args = run.call_args.args[0] - assert args[:5] == ["bao", "kv", "metadata", "patch", "-mount=platform"] - assert args[-1] == "workloads/a/b" - assert not any(a in ("get", "put", "patch-data") or "/data/" in a for a in args) - assert run.call_args.kwargs["stdout"] is cd.subprocess.DEVNULL and run.call_args.kwargs["stderr"] is cd.subprocess.DEVNULL + assert args[:5] == ["bao", "kv", "metadata", "patch", "-mount=platform"] and args[-1] == "workloads/a/b" + assert not any(a in ("get", "put") or "/data/" in a for a in args) + 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 -def test_apply_reports_failure_without_output(tmp_path, capsys): +def test_patch_refused_falls_back_to_merge_keeping_existing_metadata(tmp_path): + existing = json.dumps({"data": {"custom_metadata": {"ccr": "CCR-2026-0033", "owner": "old"}}}) + calls = [_r(2, err="Error ... Code: 403. Errors: permission denied"), _r(0, out=existing), _r(0)] doc = {"platform/workloads/a/b": {"review": "accepted", **FULL}} - with patch.object(cd.subprocess, "run", return_value=MagicMock(returncode=2)): - assert cd.apply(doc, receipt=str(tmp_path / 'r.json')) == 1 - assert capsys.readouterr() == ('', '') + with patch.object(cd.subprocess, "run", side_effect=calls) as run: + assert cd.apply(doc, receipt=str(tmp_path / "r.json")) == 0 + get_args, put_args = run.call_args_list[1].args[0], run.call_args_list[2].args[0] + assert get_args[1:4] == ["kv", "metadata", "get"] + assert put_args[1:4] == ["kv", "metadata", "put"] + assert "-custom-metadata=ccr=CCR-2026-0033" in put_args and "-custom-metadata=owner=o" in put_args + assert json.load(open(tmp_path / "r.json")) == {"platform/workloads/a/b": "merged"} + + +def test_failure_records_only_http_code(tmp_path, capsys): + doc = {"platform/workloads/a/b": {"review": "accepted", **FULL}} + with patch.object(cd.subprocess, "run", return_value=_r(2, err="Code: 500. secret-ish detail")): + assert cd.apply(doc, receipt=str(tmp_path / "r.json")) == 1 + assert capsys.readouterr() == ("", "") + assert json.load(open(tmp_path / "r.json")) == {"platform/workloads/a/b": "failed patch http=500"} def test_check_refuses_drafts_missing_and_overlong_fields(): @@ -52,10 +71,9 @@ def test_clip_respects_openbao_value_limit(): def test_receipt_ignores_temporary_home_and_bao_addr_defaults(tmp_path, monkeypatch): - import pwd, os assert cd.RECEIPT.startswith(pwd.getpwuid(os.getuid()).pw_dir) monkeypatch.delenv("BAO_ADDR", raising=False) doc = {"platform/workloads/a/b": {"review": "accepted", **FULL}} - with patch.object(cd.subprocess, "run", return_value=MagicMock(returncode=0)) as run: + with patch.object(cd.subprocess, "run", return_value=_r()) as run: cd.apply(doc, receipt=str(tmp_path / "r.json")) assert run.call_args.kwargs["env"]["BAO_ADDR"] == "http://127.0.0.1:18200" From 84beefc406ccfa91e84b553f6bb350a6a69440fe Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 12:09:52 +0200 Subject: [PATCH 3/3] MASON-WP-0004-T02: 27/29 descriptions applied; clip to OpenBao's byte limit Founder run 4 merged 27 accepted descriptions. Two failed with HTTP 400: used_by was 514 bytes because clipping counted characters and appended a 3-byte ellipsis. Clip and check now count bytes; manifest re-clipped. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 327203@bnt-lap001 Assistant-Session: 55028ae3-4914-4bd1-8f5c-815f3527aef0 --- manifests/custody-descriptions.yaml | 4 ++-- scripts/custody-describe.py | 8 ++++++-- tests/test_custody_describe.py | 6 ++++-- .../MASON-WP-0004-credential-inventory-descriptions.md | 8 ++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/manifests/custody-descriptions.yaml b/manifests/custody-descriptions.yaml index 8c5639a..d561577 100644 --- a/manifests/custody-descriptions.yaml +++ b/manifests/custody-descriptions.yaml @@ -138,7 +138,7 @@ platform/workloads/core-hub/forgejo-source-read: feeding ExternalSecret core-hub/repo-manager-forgejo-source, which materializes Secret repo-manager-forgejo-source with the single key token (remoteRef workloads/core-hub/forgejo-source-read property FORGEJO_SOURCE_TOKEN), explicit empty target metadata template, creationPolicy Owner, deletionPolicy Retain. rapp-core-hub - then sets publisher.existingSecret to repo-manager-forgejo-source with tokenKey token. Both ma… + then sets publisher.existingSecret to repo-manager-forgejo-source with tokenKey token. Both ma platform/workloads/core-hub/runtime: description: Project the existing Core Hub protected-API token into the private railiance01 runtime through a namespace-limited ExternalSecret while preserving the token across the relocation window. @@ -458,7 +458,7 @@ platform/workloads/secrets-engine/approval-client: sso/keycape-secrets-engine-approval-client and Secret sso/keycape-secrets-engine-approval-client with key client-secret. KeyCape resolves it as KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET through a secretKeyRef, matching the live KEYCAPE_RAPP_QONTO_CLIENT_SECRET shape observed on image main-153258b. - Manifests: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstor…' + Manifests: argocd/platform-addons/openbao-secretstore/openbao-keycape-approval-clients.clustersecretstor' platform/workloads/secrets-engine/approval-requester: description: T03 create-only requester KeyCape verifier custody; execute only the three fixed T03 request records. diff --git a/scripts/custody-describe.py b/scripts/custody-describe.py index ae59e80..580f0c8 100755 --- a/scripts/custody-describe.py +++ b/scripts/custody-describe.py @@ -36,8 +36,12 @@ PROPOSALS = os.path.join(os.path.dirname(__file__), "..", "docs", "credential-ow def _clip(text: str | None) -> str: + """Fit OpenBao's custom_metadata limit, which counts bytes, not characters.""" text = " ".join(str(text or "").split()) - return text if len(text) <= MAX_VALUE else text[: MAX_VALUE - 1] + "…" + if len(text.encode()) <= MAX_VALUE: + return text + cut = text.encode()[: MAX_VALUE - 3].decode(errors="ignore") + return cut + "..." def _first_sentence(text: str | None) -> str: @@ -111,7 +115,7 @@ def check(doc: dict) -> int: bad = 0 for path, e in doc.items(): missing = [k for k in REQUIRED if not e.get(k)] - long = [k for k in REQUIRED if len(str(e.get(k, ""))) > MAX_VALUE] + long = [k for k in REQUIRED if len(str(e.get(k, "")).encode()) > MAX_VALUE] state = e.get("review") if missing or long or state != "accepted": bad += 1 diff --git a/tests/test_custody_describe.py b/tests/test_custody_describe.py index d357afb..58307ce 100644 --- a/tests/test_custody_describe.py +++ b/tests/test_custody_describe.py @@ -66,8 +66,10 @@ def test_check_refuses_drafts_missing_and_overlong_fields(): assert cd.check({"p/ok": doc["p/ok"]}) == 0 -def test_clip_respects_openbao_value_limit(): - assert len(cd._clip("y" * 2000)) == cd.MAX_VALUE +def test_clip_respects_openbao_byte_limit(): + assert len(cd._clip("y" * 2000).encode()) <= cd.MAX_VALUE + assert len(cd._clip("→" * 400).encode()) <= cd.MAX_VALUE + assert cd.check({"p/x": {"review": "accepted", **dict(FULL, used_by="y" * 510 + "…")}}) == 1 def test_receipt_ignores_temporary_home_and_bao_addr_defaults(tmp_path, monkeypatch): diff --git a/workplans/MASON-WP-0004-credential-inventory-descriptions.md b/workplans/MASON-WP-0004-credential-inventory-descriptions.md index 9564532..7036ea9 100644 --- a/workplans/MASON-WP-0004-credential-inventory-descriptions.md +++ b/workplans/MASON-WP-0004-credential-inventory-descriptions.md @@ -122,6 +122,14 @@ 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.) +2026-10-09 founder runs 3-4: the wrapper has a temporary HOME and the admin +policy has no `patch` capability, so apply now writes its receipt under the +real home and falls back to metadata get + merged put. Run 4 receipt: 27 of 29 +accepted entries `merged` (existing custom_metadata kept), 2 `failed put +http=400` (`core-hub/forgejo-source-read`, `secrets-engine/approval-client`): +`used_by` was 514 bytes, clipped by characters. Clipping is now by bytes; those +two need one more run. + ## Resolve the rest with their owners ```task