diff --git a/CLAUDE.md b/CLAUDE.md index 5efc2f0..b8e457f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,7 +84,15 @@ Full design rationale: `docs/decisions/` `encrypt FILE=` still exist for scripted/non-interactive edits.) - `make check-vault` validates the vault decrypts, is valid YAML, keeps secrets under the nested `vault:` map, and has no empty leaves — printing a structure view with values - masked. Needs `rbw` unlocked. + masked. Needs `rbw` unlocked. It also **flags any leaf still set to `CHANGEME`** (see + next bullet). +- **Stubbing a secret the operator must supply** (don't ping-pong over chat): when a new + secret is needed, the agent itself adds the vault entry with the sentinel value + **`CHANGEME`** plus a comment stating *what it is and how to obtain it*, wires the code + to `{{ vault.. }}`, and commits that. Then prompt the operator to run + `make edit-vault`, replace the `CHANGEME`(s) with the real value(s) — which never touch + the conversation — and re-encrypt. `make check-vault` lists any outstanding `CHANGEME` + placeholders so nothing is forgotten. The agent never handles the real secret. --- diff --git a/scripts/check-vault.py b/scripts/check-vault.py index 295a813..2816d1e 100755 --- a/scripts/check-vault.py +++ b/scripts/check-vault.py @@ -9,7 +9,9 @@ YAML, and checks: - it decrypts to valid YAML that is a mapping; - top-level keys are within the allowed set (``vault`` + the ``vault__confirm`` canary) — secrets belong under the nested ``vault:`` map (CLAUDE.md); - - ``vault..`` leaves are all non-empty strings. + - ``vault..`` leaves are all non-empty strings; + - reports leaves still set to the ``CHANGEME`` sentinel — a secret the agent stubbed + that the operator must fill via ``make edit-vault``. Prints a REDACTED view (comments + key tree, values masked) so a human can eyeball format and comments. Secret values are never printed. @@ -31,6 +33,7 @@ except ImportError: DEFAULT = "inventories/production/group_vars/all/vault.yml" ALLOWED_TOPLEVEL = {"vault", "vault__confirm"} +SENTINEL = "CHANGEME" # placeholder the agent writes for a secret the operator must fill def main() -> int: @@ -57,6 +60,7 @@ def main() -> int: sys.exit("check-vault: vault root is not a mapping") errors = [] + placeholders = [] extra = set(data) - ALLOWED_TOPLEVEL if extra: errors.append( @@ -74,6 +78,8 @@ def main() -> int: for k, v in kv.items(): if not isinstance(v, str) or not v.strip(): errors.append(f"vault.{svc}.{k} is empty or not a string") + elif v.strip() == SENTINEL: + placeholders.append(f"vault.{svc}.{k}") # Redacted structure (comments + masked values) for human review. print(f"# {path} — redacted structure (secret values masked)") @@ -88,11 +94,20 @@ def main() -> int: if isinstance(data.get("vault"), dict): print("\n# services under vault: " + ", ".join(sorted(data["vault"]))) + if placeholders: + print("\n# placeholders awaiting real values (see the comments above for how to obtain each):") + for ph in placeholders: + print(f"# - {ph}") + if errors: print("\ncheck-vault: FAIL", file=sys.stderr) for e in errors: print(f" - {e}", file=sys.stderr) return 1 + if placeholders: + print(f"\ncheck-vault: OK — {len(placeholders)} placeholder(s) still need real values " + f"(fill via `make edit-vault`): {', '.join(placeholders)}") + return 0 print("\ncheck-vault: OK") return 0