#!/usr/bin/env python3 """custody-inventory — what credentials exist, what each is for, who owns it. ./scripts/custody-inventory.py operators/ and platform/workloads/ ./scripts/custody-inventory.py operators one mount or prefix ./scripts/custody-inventory.py --undescribed only paths missing metadata Reads metadata only — never a value — so it runs under ops-mason-build and can be handed to anyone orienting themselves without granting them a single secret. A path with no description is a finding, not a formatting problem: it is a credential nobody can identify without reading it, which is how a store turns back into the drawer of unlabelled keys it was meant to replace. """ from __future__ import annotations import json import os import subprocess import sys REQUIRED = ("description", "owner", "used_by", "rotation", "on_loss") DEFAULT_ROOTS = ("operators", "platform/workloads") def bao(*args: str) -> dict | list | None: env = dict(os.environ) env.setdefault("BAO_ADDR", "https://bao.coulomb.social") grant = os.path.expanduser("~/.claude-bao-token") if "BAO_TOKEN" not in env and os.path.exists(grant): with open(grant) as f: env["BAO_TOKEN"] = f.read().strip() # The Vault/OpenBao CLI rejects flags placed after a positional argument, # so -format=json goes immediately before the path, not at the end. argv = ["bao", *args[:-1], "-format=json", args[-1]] p = subprocess.run(argv, capture_output=True, text=True, timeout=30, env=env) if p.returncode != 0: return None try: return json.loads(p.stdout) except json.JSONDecodeError: return None def walk(prefix: str) -> list[str]: keys = bao("kv", "list", prefix) if not isinstance(keys, list): return [] out: list[str] = [] for k in keys: child = f"{prefix.rstrip('/')}/{k.rstrip('/')}" out.extend(walk(child) if k.endswith("/") else [child]) return out def main() -> int: only_undescribed = "--undescribed" in sys.argv roots = [a for a in sys.argv[1:] if not a.startswith("-")] or list(DEFAULT_ROOTS) total = incomplete = 0 for root in roots: for path in walk(root): total += 1 meta = bao("kv", "metadata", "get", path) or {} d = meta.get("data", {}) if isinstance(meta, dict) else {} cm = d.get("custom_metadata") or {} missing = [k for k in REQUIRED if not cm.get(k)] if missing: incomplete += 1 if only_undescribed and not missing: continue flag = " " if not missing else "! " print(f"{flag}{path} (v{d.get('current_version', '?')}, " f"{str(d.get('created_time', ''))[:10]})") for label, key in (("", "description"), ("owner: ", "owner"), ("used by: ", "used_by"), ("if lost: ", "on_loss")): if cm.get(key): print(f" {label}{cm[key]}") if missing: print(f" MISSING: {', '.join(missing)}") print() print("─" * 45) print(f"{total} credential path(s), {incomplete} missing required metadata") if incomplete: print("Paths marked ! need describing — see platform-root-custody.md.") return 1 if incomplete else 0 if __name__ == "__main__": raise SystemExit(main())