HOMELAB-2171/2172: docs(design): auditor charter posture + Console API-key fallback fragment #12

Open
aaron wants to merge 0 commits from plane/HOMELAB-2171-posture into main
Owner

Pre-LD5 W1 (epic HOMELAB-2194 / HOMELAB-2198), recording the accepted 2026-10-21 ruling on HOMELAB-1964. Docs only; no code, no runtime config, nothing deployed.

What this adds

design/ld5/auditor-charter.md (HOMELAB-2171, B1.2) — draft charter for the in-cluster auditor, seeded with the seven-limit subscription posture exactly as accepted: unmodified Claude Code binary · operator is the only beneficiary · no gateway, no resale · interactive herdr panes, never claude -p · at most one macro Claude session before E2 · failure reviews always carry a budget and an agent cap, one at a time · the Console API-key fallback is the documented response to any warning or restriction. Plus credential/containment rules (only the Claude token, Anthropic-only egress), modes and caps, output rules, change control (Q17), and a checklist for the W3 B18 (HOMELAB-1946) worker who extends it into the operative charter. Marked draft: nothing is in force until B18 lands and the operator reviews.

design/ld5/console-key-fallback.md (HOMELAB-2172, B1.3) — the fallback as a design fragment, because there is no macro container to patch yet (B6 / HOMELAB-1934 is wave 2). It carries the exact patch to apply in B6: LD_CLAUDE_CREDENTIAL toggle (setup-token default / api-key), the ld-credential-select init step, a file-backed apiKeyHelper so the key never enters an ANTHROPIC_* variable (preserving B6's guard and B18.1), an ld-credential-source diagnostic that prints source + fingerprint only, the ConfigMap toggle, and the B7 ExternalSecret property for the separate kv/ld/claude-apikey path. Fail closed with exit 78 on a missing/unknown source. Plus a dry run that needs no real key, and an explicit verification table: dry run blocked on B6, ESO wiring blocked on B7/B2.

No plaintext key anywhere — the key exists only in OpenBao and the mounted Secret.

Notes

  • design/ld5/ also appears on the unmerged plane/HOMELAB-1929-m0 branch (measurement.md, report.sql). The file sets are disjoint, so merge order is safe in either direction.
  • The operator-facing copy of the posture, the acknowledgement line, and the fallback runbook section live in Homelab/internal-docs → runbooks/ld-claude-subscription-posture.md (HOMELAB-2171). That repo's upstream currently serves no refs, so that change is committed locally and reported on the ticket rather than pushed.
  • HOMELAB-2172 is dangerous class: this PR is the design, not the secrets wiring. The B7 PR carrying the ExternalSecret change is the one that needs the operator's review.
Pre-LD5 W1 (epic HOMELAB-2194 / HOMELAB-2198), recording the accepted 2026-10-21 ruling on HOMELAB-1964. Docs only; no code, no runtime config, nothing deployed. ## What this adds **`design/ld5/auditor-charter.md`** (HOMELAB-2171, B1.2) — draft charter for the in-cluster auditor, seeded with the **seven-limit subscription posture** exactly as accepted: unmodified Claude Code binary · operator is the only beneficiary · no gateway, no resale · interactive herdr panes, never `claude -p` · at most one macro Claude session before E2 · failure reviews always carry a budget and an agent cap, one at a time · the Console API-key fallback is the documented response to any warning or restriction. Plus credential/containment rules (only the Claude token, Anthropic-only egress), modes and caps, output rules, change control (Q17), and a checklist for the W3 **B18 (HOMELAB-1946)** worker who extends it into the operative charter. Marked draft: nothing is in force until B18 lands and the operator reviews. **`design/ld5/console-key-fallback.md`** (HOMELAB-2172, B1.3) — the fallback as a design fragment, because **there is no macro container to patch yet** (B6 / HOMELAB-1934 is wave 2). It carries the exact patch to apply in B6: `LD_CLAUDE_CREDENTIAL` toggle (`setup-token` default / `api-key`), the `ld-credential-select` init step, a file-backed `apiKeyHelper` so the key never enters an `ANTHROPIC_*` variable (preserving B6's guard and B18.1), an `ld-credential-source` diagnostic that prints source + fingerprint only, the ConfigMap toggle, and the B7 ExternalSecret property for the separate `kv/ld/claude-apikey` path. Fail closed with exit 78 on a missing/unknown source. Plus a dry run that needs no real key, and an explicit verification table: dry run **blocked on B6**, ESO wiring **blocked on B7/B2**. No plaintext key anywhere — the key exists only in OpenBao and the mounted Secret. ## Notes - `design/ld5/` also appears on the unmerged `plane/HOMELAB-1929-m0` branch (`measurement.md`, `report.sql`). The file sets are disjoint, so merge order is safe in either direction. - The operator-facing copy of the posture, the acknowledgement line, and the fallback runbook section live in `Homelab/internal-docs` → `runbooks/ld-claude-subscription-posture.md` (HOMELAB-2171). That repo's upstream currently serves no refs, so that change is committed locally and reported on the ticket rather than pushed. - HOMELAB-2172 is dangerous class: this PR is the design, not the secrets wiring. The B7 PR carrying the ExternalSecret change is the one that needs the operator's review.
design/ld5/auditor-charter.md: draft charter seeded with the seven-limits
posture (HOMELAB-1964), credential/containment rules, modes and caps, output
rules and change control; marked as the draft W3 B18 (HOMELAB-1946) extends.

design/ld5/console-key-fallback.md: HOMELAB-2172 design fragment — the exact
B6 patch (credential-select helper, apiKeyHelper, ld-credential-source,
ConfigMap toggle, ExternalSecret property for kv/ld/claude-apikey) plus a
dry run that needs no real key, with the blocked-on-B6/B7 items stated.

Refs HOMELAB-2171 (B1.2), HOMELAB-2172 (B1.3), HOMELAB-1934, HOMELAB-1935.
Author
Owner

Review — REQUEST CHANGES (1 blocker, 4 concerns)

Fresh-context review from primary sources only: this diff, the acceptance checklists on HOMELAB-2171 and HOMELAB-2172, and the ruling on HOMELAB-1964 (2026-10-21). No CI is configured on this repo, so there is nothing to be green on. Scope is confirmed clean: exactly two new files under design/ld5/, additions only, no other path touched, and no credential-shaped string anywhere except the deliberately fake sk-ant-…-DRYRUN-… placeholders in the dry run.

What passes

  • HOMELAB-2171 acceptance (charter §1). All seven posture limits from the 1964 ruling are present and unaltered in meaning, as rows 1–7 of the §1 table (auditor-charter.md:28-44): unmodified binary · operator the only beneficiary · no gateway/resale · interactive herdr panes never claude -p · at most one macro session before E2 · failure reviews with a budget and agent cap, one at a time · the Console API-key fallback as the documented response to any warning or restriction (:44). The file is marked draft in frontmatter and in the status block (:2, :13-23) and names HOMELAB-1946 (B18) as the ticket that extends it (:6, :17, §7). Nothing claims the operative charter is done.
  • HOMELAB-2172 coverage. Where the key lives is a separate path kv/ld/claude-apikey through ESO (console-key-fallback.md:40, §3.7) with the setup token left intact; the config toggle is specified end to end (LD_CLAUDE_CREDENTIAL, ConfigMap key claude-credential, §2/§3.6); the hard spend cap is explicitly deferred to the runbook, not silently omitted (:299, runbook §3.3); rollback lives in the same runbook section. No plaintext or real credential appears.
  • Claim honesty. The header states the switch is "not ready" (:18) and the §5 table marks the dry run blocked on B6/HOMELAB-1934 (:290) and the ESO wiring blocked on B7/HOMELAB-1935 + B2 (:291). No step is described as performed. That matches the PR body.
  • Scope. No design decision beyond the recorded ruling is asserted as settled; the narrowing in §3 of the charter is attributed to HOMELAB-1946 rather than decided here.

Blocker

  1. console-key-fallback.md:60, :80, :174 — the setup-token credential cannot reach the pane as specified, and the only reading that works breaks the init's shell. Line 60 says the helper is "sourced/called"; §3.5 (:170-175) shows it invoked as a plain command. Run as a command, export CLAUDE_CODE_OAUTH_TOKEN at :80 is discarded when the subprocess exits, so the default path — the one in use today, setup-token — authenticates nothing in the herdr pane. The alternative reading (sourced) does work, but then set -euo pipefail at :67 mutates the macro init's own shell options and will abort it on the init's next unset-variable reference. Since this file is offered as "the exact patch to apply in B6" and its helper semantics are stated as the contract, the mechanism must be named, not left to the implementer: pick one (source it with scoped shell options, or have the init own the export and pass the value through the launcher that starts herdr), and add a dry-run step that asserts CLAUDE_CODE_OAUTH_TOKEN is actually visible to the child that starts claude in setup-token mode — today steps 2 and 4 only exercise the file path and apiKeyHelper, which is why this defect cannot fail the stated acceptance.

Concerns (posting, not blocking)

  1. console-key-fallback.md:146-148 — the toggle is one-directional. setup-token mode clears a stale apiKeyHelper (:85-90), but api-key mode neither clears nor rejects a pre-existing CLAUDE_CODE_OAUTH_TOKEN, and the diagnostic only greps ANTHROPIC_*. If anything in the init or Secret projection supplies the OAuth token, the fallback appears engaged while the subscription credential is still doing the work — the opposite of what limit 7 is for. Cheap fix in the same spirit as the existing guard: in api-key mode exit 78 with credential-source=api-key oauth-leak when the OAuth token is set, and assert its absence in ld-credential-source.
  2. console-key-fallback.md:271-272 — dry-run step 7 is vacuous. Each step is a fresh --rm container, so no settings.json carries over from step 4; setup-token mode trivially finds no apiKeyHelper and the step cannot fail. To make it real, pre-bake or seed a settings.json containing apiKeyHelper in the container and assert it is gone.
  3. console-key-fallback.md:288 — "Done" overstates a local-only artifact. The runbook section is a local commit on Homelab/internal-docs fa37f9c, unreachable because that repo's upstream serves an empty ref advertisement (documented on HOMELAB-2171). Suggest "written, not yet landed — internal-docs upstream broken (HOMELAB-2171)". The same applies to auditor-charter.md:48-50, which tells a reader the operator-facing copy "lives in" that path.
  4. auditor-charter.md:42 vs :111 (also :83) — an unsettled number presented as decided. §1 fixes "at most 20 agents" for LD5 while §7 asks the B18 worker to fill in "the budget and agent-cap values". The 20 is a cap-down from Q19's 32-agent envelope so it cannot relax the ruling, but the draft should say either "B18 confirms 20" or drop the number from §1. Related nit: §1's heading says "binding" (:26) under a status block that says nothing here is in force — clarity, not contradiction.

Also noted, not a change request

ld-credential-select and ld-credential-source depend on python3 and sha256sum in the fleet-macro image; B6 should state that requirement explicitly, since the fragment otherwise claims the helpers are self-contained.

Blocker 1 is a wording-plus-one-dry-run-assertion fix in console-key-fallback.md only; the charter half of this PR needs no substantive change. I am not merging until that delta lands and is re-checked.

## Review — REQUEST CHANGES (1 blocker, 4 concerns) Fresh-context review from primary sources only: this diff, the acceptance checklists on HOMELAB-2171 and HOMELAB-2172, and the ruling on HOMELAB-1964 (2026-10-21). No CI is configured on this repo, so there is nothing to be green on. Scope is confirmed clean: exactly two new files under `design/ld5/`, additions only, no other path touched, and no credential-shaped string anywhere except the deliberately fake `sk-ant-…-DRYRUN-…` placeholders in the dry run. ### What passes - **HOMELAB-2171 acceptance (charter §1).** All seven posture limits from the 1964 ruling are present and unaltered in meaning, as rows 1–7 of the §1 table (`auditor-charter.md:28-44`): unmodified binary · operator the only beneficiary · no gateway/resale · interactive herdr panes never `claude -p` · at most one macro session before E2 · failure reviews with a budget and agent cap, one at a time · the Console API-key fallback as the documented response to any warning or restriction (`:44`). The file is marked draft in frontmatter and in the status block (`:2`, `:13-23`) and names HOMELAB-1946 (B18) as the ticket that extends it (`:6`, `:17`, §7). Nothing claims the operative charter is done. - **HOMELAB-2172 coverage.** Where the key lives is a separate path `kv/ld/claude-apikey` through ESO (`console-key-fallback.md:40`, §3.7) with the setup token left intact; the config toggle is specified end to end (`LD_CLAUDE_CREDENTIAL`, ConfigMap key `claude-credential`, §2/§3.6); the hard spend cap is **explicitly deferred to the runbook**, not silently omitted (`:299`, runbook §3.3); rollback lives in the same runbook section. No plaintext or real credential appears. - **Claim honesty.** The header states the switch is "not ready" (`:18`) and the §5 table marks the dry run blocked on B6/HOMELAB-1934 (`:290`) and the ESO wiring blocked on B7/HOMELAB-1935 + B2 (`:291`). No step is described as performed. That matches the PR body. - **Scope.** No design decision beyond the recorded ruling is asserted as settled; the narrowing in §3 of the charter is attributed to HOMELAB-1946 rather than decided here. ### Blocker 1. **`console-key-fallback.md:60`, `:80`, `:174` — the setup-token credential cannot reach the pane as specified, and the only reading that works breaks the init's shell.** Line 60 says the helper is "sourced/called"; §3.5 (`:170-175`) shows it invoked as a plain command. Run as a command, `export CLAUDE_CODE_OAUTH_TOKEN` at `:80` is discarded when the subprocess exits, so the **default** path — the one in use today, `setup-token` — authenticates nothing in the herdr pane. The alternative reading (sourced) does work, but then `set -euo pipefail` at `:67` mutates the macro init's own shell options and will abort it on the init's next unset-variable reference. Since this file is offered as "the exact patch to apply in B6" and its helper semantics are stated as the contract, the mechanism must be named, not left to the implementer: pick one (source it with scoped shell options, or have the init own the export and pass the value through the launcher that starts herdr), and add a dry-run step that asserts `CLAUDE_CODE_OAUTH_TOKEN` is actually visible to the child that starts `claude` in setup-token mode — today steps 2 and 4 only exercise the file path and `apiKeyHelper`, which is why this defect cannot fail the stated acceptance. ### Concerns (posting, not blocking) 2. **`console-key-fallback.md:146-148` — the toggle is one-directional.** `setup-token` mode clears a stale `apiKeyHelper` (`:85-90`), but `api-key` mode neither clears nor rejects a pre-existing `CLAUDE_CODE_OAUTH_TOKEN`, and the diagnostic only greps `ANTHROPIC_*`. If anything in the init or Secret projection supplies the OAuth token, the fallback appears engaged while the subscription credential is still doing the work — the opposite of what limit 7 is for. Cheap fix in the same spirit as the existing guard: in `api-key` mode exit 78 with `credential-source=api-key oauth-leak` when the OAuth token is set, and assert its absence in `ld-credential-source`. 3. **`console-key-fallback.md:271-272` — dry-run step 7 is vacuous.** Each step is a fresh `--rm` container, so no `settings.json` carries over from step 4; setup-token mode trivially finds no `apiKeyHelper` and the step cannot fail. To make it real, pre-bake or seed a `settings.json` containing `apiKeyHelper` in the container and assert it is gone. 4. **`console-key-fallback.md:288` — "Done" overstates a local-only artifact.** The runbook section is a local commit on `Homelab/internal-docs` `fa37f9c`, unreachable because that repo's upstream serves an empty ref advertisement (documented on HOMELAB-2171). Suggest "written, not yet landed — internal-docs upstream broken (HOMELAB-2171)". The same applies to `auditor-charter.md:48-50`, which tells a reader the operator-facing copy "lives in" that path. 5. **`auditor-charter.md:42` vs `:111` (also `:83`) — an unsettled number presented as decided.** §1 fixes "at most 20 agents" for LD5 while §7 asks the B18 worker to fill in "the budget and agent-cap values". The 20 is a cap-down from Q19's 32-agent envelope so it cannot relax the ruling, but the draft should say either "B18 confirms 20" or drop the number from §1. Related nit: §1's heading says "binding" (`:26`) under a status block that says nothing here is in force — clarity, not contradiction. ### Also noted, not a change request `ld-credential-select` and `ld-credential-source` depend on `python3` and `sha256sum` in the `fleet-macro` image; B6 should state that requirement explicitly, since the fragment otherwise claims the helpers are self-contained. Blocker 1 is a wording-plus-one-dry-run-assertion fix in `console-key-fallback.md` only; the charter half of this PR needs no substantive change. I am not merging until that delta lands and is re-checked.
Blocker: the setup-token credential had no working path to the pane (an export
inside a command subprocess died with it; sourcing mutated the init's shell).
The handoff is now explicit: ld-credential-select is a command that exports
nothing and configures settings.json, and herdr starts each pane through
ld-claude-pane, which reads the selected file at exec time, sets the variable in
its own environment and execs the unmodified binary. Dry-run step 4 asserts with
a fake child that CLAUDE_CODE_OAUTH_TOKEN reaches the process that runs claude.

Concerns: api-key mode now refuses a live OAuth token (exit 78 oauth-leak),
deletes the stored OAuth login and strips the token from settings.json env;
ld-credential-source reports OAuth leftovers (env/store/settings) as well as
ANTHROPIC_*. Dry-run step 7 seeds both leftovers inside one container run
(--rm made the old step 7 vacuous). The runbook row is restated as written but
not landed (internal-docs upstream broken, HOMELAB-2171). Charter: the agent-cap
number is deferred to B18 instead of fixing 20 agents in §1.
Author
Owner

Review findings addressed — e89dd9b (Rev 2, 2026-10-22)

All five findings fixed in design/ld5/; both files keep their structure, the seven posture limits are unchanged in meaning, and no credential of any kind was added. The three helpers in §3 were extracted from the doc and executed locally (bash only, fake secrets) to confirm every dry-run expectation below is real rather than asserted.

Blocker — the setup-token credential could not reach the pane

Mechanism is now named, not left to the implementer: ld-claude-pane.

  • console-key-fallback.md:50-51 (§2 contract) — new rows: the selector is run as a command and exports nothing; ld-claude-pane owns the handoff. Setup-token exposure is now stated precisely: CLAUDE_CODE_OAUTH_TOKEN lives in one environment (the claude process), never in the init's env, never in herdr's env, never in an argv. ANTHROPIC_* stays untouched, so B6's guard and B18.1 are unbroken.
  • :174-224 (§3.3, new) — ld-claude-pane: reads the selected file at exec time, assigns to its own environment (so not in /proc/*/cmdline, unlike env VAR=x cmd), then execs the released binary — one process tree, no launcher left behind. Argument list pinned to --setting-sources user; -p/--print refused with exit 78 (limit 4 reinforced, not changed). Limit 1 addressed in the header: no request-path hook, no modified binary.
  • :314-345 (§3.6) — init runs ld-credential-select as a command (explicit "do not source it"), herdr's session command is ld-claude-pane, plus a table of the three rejected readings and why each fails: subprocess-local export loses the credential; sourcing imports set -euo pipefail/exit into the init; init-owned export puts the token where herdr and every runtime child inherits it.
  • :437-476 (§4 step 4, new) — the requested assertion. A fake child (/tmp/ld-dryrun/child) is the LD_CLAUDE_BIN the launcher execs; it prints the argv it got, the byte count and 12-hex fingerprint of CLAUDE_CODE_OAUTH_TOKEN, and the count of ANTHROPIC_* variables. Expected: child-argv=[--setting-sources user] oauth-bytes=<file bytes> oauth-sha256=<file sha> anthropic-vars=0; in api-key mode the same child gets oauth-bytes=0 and the helper returns the key. Verified locally: oauth-bytes=31 oauth-sha256=c823568425ee against a 31-byte fake token whose sha matches, anthropic-vars=0. Step 2 also asserts caller-env-oauth=<unset>, i.e. the export-leak reading is now a test failure.

Concern 1 — one-directional toggle

  • :141 (§3.1) — api-key mode refuses a live OAuth token: credential-source=api-key oauth-leak, exit 78, before any state is written.
  • :143-148 — api-key mode also deletes the stored OAuth login ($CLAUDE_CONFIG_DIR/.credentials.json, oauth-store-removed) and :110-124 strips CLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_* out of settings.json's env block in both directions.
  • :231-285 (§3.4) — ld-credential-source now reports leftovers rather than only ANTHROPIC_*: oauth-leak env / oauth-leak store / oauth-leak settings, no-api-key-helper in api-key mode, stale-api-key-helper in setup-token mode, plus env-leak anthropic in either mode; exit 1 on any of them. :513-524 (§4 step 8) seeds an OAuth store and expects oauth-leak store, diag-exit=1 (verified locally).

Concern 2 — vacuous step 7 under --rm

:479-511 (§4 step 7) now does the whole two-directional proof inside one container run with a writable HOME, seeding a stale apiKeyHelper, an OAuth token in the settings env block, and a stored OAuth login before flipping modes. Verified output: ok: apiKeyHelper removed, ok: settings env key removed, credential-source=api-key oauth-leak / leak-exit=78, ok: apiKeyHelper written, ok: oauth store removed, env-clean, diag-exit=0. Step 7 can now fail.

Concern 3 — "Done" overstated a local-only artifact

  • console-key-fallback.md:551 — row restated: "Written, not landed" — local commit fa37f9c on Homelab/internal-docs, upstream serves an empty ref advertisement, nothing pushed (recorded on HOMELAB-2171), pending operator repair.
  • auditor-charter.md:48-52 — the same correction applied to the sentence that told a reader the operator-facing copy "lives in" that path.

Concern 4 — "20 agents" in §1 vs §7

  • auditor-charter.md:42 — the number is gone from limit 6; the limit keeps its meaning ("budget and agent cap, one at a time") and the values are deferred to B18, with Q19's envelope (32/6/$4) kept as the ceiling.
  • :88 (§4 table) and :116-118 (§7) aligned to the same wording; :26 heading reworded so "binding" no longer collides with the "nothing here is in force" status block.

Also noted

console-key-fallback.md:308-311 — §3.5 now states the helpers are not self-contained and that B6 must provide bash, python3 and sha256sum, with the first dry-run execution as the check.

Not merged, per instructions; the branch is pushed and PR #12 is ready for the re-check.

## Review findings addressed — `e89dd9b` (Rev 2, 2026-10-22) All five findings fixed in `design/ld5/`; both files keep their structure, the seven posture limits are unchanged in meaning, and no credential of any kind was added. The three helpers in §3 were extracted from the doc and **executed locally** (bash only, fake secrets) to confirm every dry-run expectation below is real rather than asserted. ### Blocker — the setup-token credential could not reach the pane **Mechanism is now named, not left to the implementer: `ld-claude-pane`.** - `console-key-fallback.md:50-51` (§2 contract) — new rows: the selector is run as a **command** and **exports nothing**; `ld-claude-pane` owns the handoff. Setup-token exposure is now stated precisely: `CLAUDE_CODE_OAUTH_TOKEN` lives in **one** environment (the `claude` process), never in the init's env, never in herdr's env, never in an argv. `ANTHROPIC_*` stays untouched, so B6's guard and B18.1 are unbroken. - `:174-224` (§3.3, new) — `ld-claude-pane`: reads the selected file at exec time, assigns to its **own** environment (so not in `/proc/*/cmdline`, unlike `env VAR=x cmd`), then `exec`s the released binary — one process tree, no launcher left behind. Argument list pinned to `--setting-sources user`; `-p`/`--print` refused with exit 78 (limit 4 reinforced, not changed). Limit 1 addressed in the header: no request-path hook, no modified binary. - `:314-345` (§3.6) — init runs `ld-credential-select` as a command (explicit "do not source it"), herdr's session command is `ld-claude-pane`, plus a table of the three rejected readings and why each fails: subprocess-local export loses the credential; sourcing imports `set -euo pipefail`/`exit` into the init; init-owned export puts the token where herdr and every runtime child inherits it. - `:437-476` (§4 step 4, new) — **the requested assertion**. A fake child (`/tmp/ld-dryrun/child`) is the `LD_CLAUDE_BIN` the launcher execs; it prints the argv it got, the **byte count and 12-hex fingerprint** of `CLAUDE_CODE_OAUTH_TOKEN`, and the count of `ANTHROPIC_*` variables. Expected: `child-argv=[--setting-sources user] oauth-bytes=<file bytes> oauth-sha256=<file sha> anthropic-vars=0`; in api-key mode the same child gets `oauth-bytes=0` and the helper returns the key. Verified locally: `oauth-bytes=31 oauth-sha256=c823568425ee` against a 31-byte fake token whose sha matches, `anthropic-vars=0`. Step 2 also asserts `caller-env-oauth=<unset>`, i.e. the export-leak reading is now a test failure. ### Concern 1 — one-directional toggle - `:141` (§3.1) — api-key mode refuses a live OAuth token: `credential-source=api-key oauth-leak`, exit 78, before any state is written. - `:143-148` — api-key mode also **deletes** the stored OAuth login (`$CLAUDE_CONFIG_DIR/.credentials.json`, `oauth-store-removed`) and `:110-124` strips `CLAUDE_CODE_OAUTH_TOKEN`/`ANTHROPIC_*` out of `settings.json`'s `env` block in both directions. - `:231-285` (§3.4) — `ld-credential-source` now reports leftovers rather than only `ANTHROPIC_*`: `oauth-leak env` / `oauth-leak store` / `oauth-leak settings`, `no-api-key-helper` in api-key mode, `stale-api-key-helper` in setup-token mode, plus `env-leak anthropic` in either mode; exit 1 on any of them. `:513-524` (§4 step 8) seeds an OAuth store and expects `oauth-leak store`, `diag-exit=1` (verified locally). ### Concern 2 — vacuous step 7 under `--rm` `:479-511` (§4 step 7) now does the whole two-directional proof **inside one container run** with a writable `HOME`, seeding a stale `apiKeyHelper`, an OAuth token in the settings `env` block, and a stored OAuth login before flipping modes. Verified output: `ok: apiKeyHelper removed`, `ok: settings env key removed`, `credential-source=api-key oauth-leak` / `leak-exit=78`, `ok: apiKeyHelper written`, `ok: oauth store removed`, `env-clean`, `diag-exit=0`. Step 7 can now fail. ### Concern 3 — "Done" overstated a local-only artifact - `console-key-fallback.md:551` — row restated: **"Written, not landed"** — local commit `fa37f9c` on `Homelab/internal-docs`, upstream serves an empty ref advertisement, nothing pushed (recorded on HOMELAB-2171), pending operator repair. - `auditor-charter.md:48-52` — the same correction applied to the sentence that told a reader the operator-facing copy "lives in" that path. ### Concern 4 — "20 agents" in §1 vs §7 - `auditor-charter.md:42` — the number is gone from limit 6; the limit keeps its meaning ("budget and agent cap, one at a time") and the **values** are deferred to B18, with Q19's envelope (32/6/$4) kept as the ceiling. - `:88` (§4 table) and `:116-118` (§7) aligned to the same wording; `:26` heading reworded so "binding" no longer collides with the "nothing here is in force" status block. ### Also noted `console-key-fallback.md:308-311` — §3.5 now states the helpers are **not** self-contained and that B6 must provide `bash`, `python3` and `sha256sum`, with the first dry-run execution as the check. Not merged, per instructions; the branch is pushed and PR #12 is ready for the re-check.
Author
Owner

Re-review of e89dd9b — APPROVE (0 blockers)

Fresh context, primary sources only: git diff 68dd9b1..e89dd9b (the fix delta), the full tree at e89dd9b, PR #12's commit list, the HOMELAB-2171 acceptance block, and the previous review comment's findings as the acceptance criteria. I did not implement or review this before, and I did not take the "findings addressed" comment as evidence — I extracted the four helper scripts from design/ld5/console-key-fallback.md and ran the dry-run steps locally with the doc's own fake secrets.

Scope is unchanged and additive: two files, 723 insertions, 0 deletions, nothing outside design/ld5/, no CI config on the branch.

Verification method (why this is not a wording check)

Extracted §3.1–§3.4 verbatim, bash -n clean on all four, then executed steps 2, 3, 4a, 4b, 4c, 5, 6, 7, 8 against LD_CLAUDE_SECRET_DIR=/tmp/... with sk-ant-…-DRYRUN-… files.

STEP 2   credential-source=setup-token selected 8872d9925a73 / select-exit=0 / caller-env-oauth=<unset>
STEP 3   credential-source=api-key selected 93a680d3d61c / /usr/local/bin/ld-claude-apikey / 0
STEP 4a  child-argv=[--setting-sources user] oauth-bytes=31 oauth-sha256=c823568425ee anthropic-vars=0
         file truth:                          oauth-bytes=31 oauth-sha256=c823568425ee
STEP 4b  child-argv=[--setting-sources user] oauth-bytes=0  oauth-sha256=e3b0c44298fc anthropic-vars=0
         ld-claude-apikey|sha256 = 93a680d3d61c = api-key file fp
STEP 4c  exit=78 (both -p and --print)
STEP 5/6 credential-source=api-key missing / credential-source=console unknown, exit=78
STEP 7   ok: apiKeyHelper removed · ok: settings env key removed · oauth-leak leak-exit=78
         oauth-store-removed · ok: apiKeyHelper written · ok: oauth store removed · env-clean diag-exit=0
STEP 8   oauth-leak store · diag-exit=1

The step-4 numbers independently reproduce the fix worker's claimed oauth-bytes=31 / c823568425ee / anthropic-vars=0 — the child that becomes claude really does receive the token.

Blocker 1 — credential handoff: RESOLVED

  • Mechanism is named at console-key-fallback.md:50-51 (contract row: selector is a command, exports nothing, ld-claude-pane owns the handoff) and implemented at :174-224: the launcher reads the selected file at exec time, sets the variable in its own environment, then execs the binary — so the value survives into the claude process without a launcher left behind, unlike the export-in-a-subprocess reading.
  • The set -euo pipefail-into-the-init hazard is gone: :314-345 runs the selector as a command with "do not source it", and ld-credential-select (:77) no longer exports anything (verified: caller-env-oauth=<unset>). The three rejected readings plus the chosen one are enumerated at :346-350, so B6 does not have to invent semantics.
  • The requested dry-run assertion exists at :437-476: a fake child (LD_CLAUDE_BIN) prints argv, byte count, 12-hex fingerprint and ANTHROPIC_* count; setup-token mode expects the file's bytes/fingerprint and anthropic-vars=0, api-key mode expects oauth-bytes=0 and the helper's key. Step 2 additionally pins the export-leak reading as a failure (caller-env-oauth=<unset>).
  • Consistency: limit 4's claude --setting-sources user and limit 1's "no request-path hook" both hold in the launcher (:184-228); the launcher adds only an env assignment and an exec.

Concern 2 — one-directional toggle: RESOLVED

console-key-fallback.md:141 refuses a live OAuth token in api-key mode (credential-source=api-key oauth-leak, exit 78, before any state is written); :143-148 deletes the stored OAuth login; edit_settings (:110-127) strips CLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_* from the settings env block in both directions. The diagnostic is now leftover-oriented, not ANTHROPIC_*-only: oauth-leak env (:268), oauth-leak store (:272), oauth-leak settings (:276), no-api-key-helper (:280), stale-api-key-helper (:284), env-leak anthropic (:260), exit 1 on any. I exercised all six paths individually, including the setup-token-mode stale-helper direction — each fires with exit 1.

Concern 3 — vacuous step 7 under --rm: RESOLVED

console-key-fallback.md:479-511 now seeds a stale apiKeyHelper, an OAuth token in the settings env block and a stored .credentials.json inside a single container run with a writable HOME, then flips setup-token → api-key. Seeding and assertion are in the same run, so --rm is irrelevant and the step operates on real state — pre-fix behaviour (helper left in place / store not deleted) would omit the corresponding ok: line, so the expected block is falsifiable. Verified end to end above.

Nit (not blocking, not a regression): each assertion is ! grep … && echo "ok: …", and under set -euo pipefail a failing left operand of an AND-list is exempt from set -e — I confirmed a broken direction leaves the script exiting 0. The step is no longer vacuous (the ok: lines vanish), but a || { echo "FAIL: …"; exit 1; } form would let it fail mechanically rather than by eyeball.

Concern 4 — runbook row overstated: RESOLVED

console-key-fallback.md:551 now reads "Written, not landed" with fa37f9c, the empty ref advertisement, "nothing is pushed (recorded on HOMELAB-2171)" and "pending operator repair of internal-docs". The matching sentence in auditor-charter.md:48-52 was corrected to "a local commit only, not yet landed". I did not touch Homelab/internal-docs; that fix is explicitly left to the operator. The remaining internal-docs/... mentions (:563, :566, charter :44, :144) are path references, not landed-artifact claims.

Concern 5 — "20 agents" in §1 vs §7: RESOLVED

auditor-charter.md:42 no longer fixes a number; limit 6 keeps its meaning ("budget and agent cap, run one at a time") and defers the values to B18 with Q19's 32/6/$4 as ceiling. :88 (§4 table) and :116-118 (§7) use the same wording, so §1 and §7 no longer conflict. :26 is reworded to "binding on anything B18 writes", which scopes the binding to B18's authoring rather than claiming the draft is in force — the status block's "nothing here is in force" still stands. "the 20 LD5 tickets" at :58 is the ticket count, not an agent cap.

Regression checks

  • Seven posture limits — the delta touches row 6 only; rows 1–5 and 7 are byte-identical to 68dd9b1. Limit 6's meaning is preserved and the change removes a number rather than relaxing the ruling. All seven still match HOMELAB-1964 as recorded on the ticket.
  • No plaintext / real credentials — the only credential-shaped strings in either file are the two sk-ant-…-DRYRUN-… placeholders (:403-404). Everything else is a path, a variable name, or a 12-hex fingerprint. Nothing added by the fix changes that.
  • Both files still draft — status: draft in both frontmatters (auditor-charter.md:4, console-key-fallback.md:4), "draft" in both bodies, "not ready" retained at console-key-fallback.md:18, §5 still shows dry run blocked on B6 and ESO blocked on B7/B2 (:554-555), and the spend cap stays deferred to the runbook (:563). HOMELAB-1946 (B18) is named five times in the charter (:7, :16, :66, :109, :142).
  • Claim honesty — the new §5 row "Credential handoff specified — Done (design)" is accurate: it says design and cites the step that asserts it, and no step is described as performed against an image that does not exist yet.

Non-blocking suggestions

  1. console-key-fallback.md frontmatter (:8) lists HOMELAB-2172/1934/1935 but not HOMELAB-1946, even though the fragment relies on B18.1 and hands its helpers to B18. A W3 worker searching by ticket would miss it. Pre-existing, not introduced by this delta.
  2. Step 7's || exit 1 refinement above.
  3. Step 4a/4c invoke ld-claude-pane as the container command without --entrypoint, while steps 2/3/4b/7/8 override the entrypoint with bash. If the dry-run image keeps the macro init as its entrypoint, 4a/4c would run the init instead. Worth one line stating the dry-run image's entrypoint.
  4. The §3.5 runtime-dependency note (:308-311, bash/python3/sha256sum, "deliberately not self-contained") satisfies the reviewer's earlier footnote — no action, recording that it landed.

Merge note

APPROVED, and I am not merging. This repo's landing lock cannot pass for a main-targeted PR — CI only runs on live-targeted PRs, and there is no CI config on this branch — so the green-CI and post-commit-verdict legs are unreachable here by construction. This is a docs-only, 0-deletion change with no CI surface; the operator UI-merges in the Forgejo UI. No blocker findings remain open.

## Re-review of `e89dd9b` — APPROVE (0 blockers) Fresh context, primary sources only: `git diff 68dd9b1..e89dd9b` (the fix delta), the full tree at `e89dd9b`, PR #12's commit list, the HOMELAB-2171 acceptance block, and the previous review comment's findings as the acceptance criteria. I did not implement or review this before, and I did not take the "findings addressed" comment as evidence — I extracted the four helper scripts from `design/ld5/console-key-fallback.md` and **ran the dry-run steps locally** with the doc's own fake secrets. Scope is unchanged and additive: two files, 723 insertions, 0 deletions, nothing outside `design/ld5/`, no CI config on the branch. ### Verification method (why this is not a wording check) Extracted §3.1–§3.4 verbatim, `bash -n` clean on all four, then executed steps 2, 3, 4a, 4b, 4c, 5, 6, 7, 8 against `LD_CLAUDE_SECRET_DIR=/tmp/...` with `sk-ant-…-DRYRUN-…` files. ``` STEP 2 credential-source=setup-token selected 8872d9925a73 / select-exit=0 / caller-env-oauth=<unset> STEP 3 credential-source=api-key selected 93a680d3d61c / /usr/local/bin/ld-claude-apikey / 0 STEP 4a child-argv=[--setting-sources user] oauth-bytes=31 oauth-sha256=c823568425ee anthropic-vars=0 file truth: oauth-bytes=31 oauth-sha256=c823568425ee STEP 4b child-argv=[--setting-sources user] oauth-bytes=0 oauth-sha256=e3b0c44298fc anthropic-vars=0 ld-claude-apikey|sha256 = 93a680d3d61c = api-key file fp STEP 4c exit=78 (both -p and --print) STEP 5/6 credential-source=api-key missing / credential-source=console unknown, exit=78 STEP 7 ok: apiKeyHelper removed · ok: settings env key removed · oauth-leak leak-exit=78 oauth-store-removed · ok: apiKeyHelper written · ok: oauth store removed · env-clean diag-exit=0 STEP 8 oauth-leak store · diag-exit=1 ``` The step-4 numbers independently reproduce the fix worker's claimed `oauth-bytes=31 / c823568425ee / anthropic-vars=0` — the child that becomes `claude` really does receive the token. ### Blocker 1 — credential handoff: RESOLVED - Mechanism is named at `console-key-fallback.md:50-51` (contract row: selector is a **command**, **exports nothing**, `ld-claude-pane` owns the handoff) and implemented at `:174-224`: the launcher reads the selected file at exec time, sets the variable in its **own** environment, then `exec`s the binary — so the value survives into the `claude` process without a launcher left behind, unlike the `export`-in-a-subprocess reading. - The `set -euo pipefail`-into-the-init hazard is gone: `:314-345` runs the selector as a command with "do not source it", and `ld-credential-select` (`:77`) no longer exports anything (verified: `caller-env-oauth=<unset>`). The three rejected readings plus the chosen one are enumerated at `:346-350`, so B6 does not have to invent semantics. - The requested dry-run assertion exists at `:437-476`: a fake child (`LD_CLAUDE_BIN`) prints argv, byte count, 12-hex fingerprint and `ANTHROPIC_*` count; setup-token mode expects the file's bytes/fingerprint and `anthropic-vars=0`, api-key mode expects `oauth-bytes=0` and the helper's key. Step 2 additionally pins the export-leak reading as a failure (`caller-env-oauth=<unset>`). - Consistency: limit 4's `claude --setting-sources user` and limit 1's "no request-path hook" both hold in the launcher (`:184-228`); the launcher adds only an env assignment and an `exec`. ### Concern 2 — one-directional toggle: RESOLVED `console-key-fallback.md:141` refuses a live OAuth token in api-key mode (`credential-source=api-key oauth-leak`, exit 78, before any state is written); `:143-148` deletes the stored OAuth login; `edit_settings` (`:110-127`) strips `CLAUDE_CODE_OAUTH_TOKEN`/`ANTHROPIC_*` from the settings `env` block in **both** directions. The diagnostic is now leftover-oriented, not `ANTHROPIC_*`-only: `oauth-leak env` (`:268`), `oauth-leak store` (`:272`), `oauth-leak settings` (`:276`), `no-api-key-helper` (`:280`), `stale-api-key-helper` (`:284`), `env-leak anthropic` (`:260`), `exit 1` on any. I exercised all six paths individually, including the `setup-token`-mode stale-helper direction — each fires with exit 1. ### Concern 3 — vacuous step 7 under `--rm`: RESOLVED `console-key-fallback.md:479-511` now seeds a stale `apiKeyHelper`, an OAuth token in the settings `env` block **and** a stored `.credentials.json` inside a single container run with a writable `HOME`, then flips setup-token → api-key. Seeding and assertion are in the same run, so `--rm` is irrelevant and the step operates on real state — pre-fix behaviour (helper left in place / store not deleted) would omit the corresponding `ok:` line, so the expected block is falsifiable. Verified end to end above. Nit (not blocking, not a regression): each assertion is `! grep … && echo "ok: …"`, and under `set -euo pipefail` a failing left operand of an AND-list is exempt from `set -e` — I confirmed a broken direction leaves the script exiting 0. The step is no longer vacuous (the `ok:` lines vanish), but a `|| { echo "FAIL: …"; exit 1; }` form would let it fail mechanically rather than by eyeball. ### Concern 4 — runbook row overstated: RESOLVED `console-key-fallback.md:551` now reads **"Written, not landed"** with `fa37f9c`, the empty ref advertisement, "nothing is pushed (recorded on HOMELAB-2171)" and "pending operator repair of internal-docs". The matching sentence in `auditor-charter.md:48-52` was corrected to "a **local commit only, not yet landed**". I did not touch `Homelab/internal-docs`; that fix is explicitly left to the operator. The remaining `internal-docs/...` mentions (`:563`, `:566`, charter `:44`, `:144`) are path references, not landed-artifact claims. ### Concern 5 — "20 agents" in §1 vs §7: RESOLVED `auditor-charter.md:42` no longer fixes a number; limit 6 keeps its meaning ("budget and agent cap, run one at a time") and defers the **values** to B18 with Q19's 32/6/$4 as ceiling. `:88` (§4 table) and `:116-118` (§7) use the same wording, so §1 and §7 no longer conflict. `:26` is reworded to "binding on anything B18 writes", which scopes the binding to B18's authoring rather than claiming the draft is in force — the status block's "nothing here is in force" still stands. "the 20 LD5 tickets" at `:58` is the ticket count, not an agent cap. ### Regression checks - **Seven posture limits** — the delta touches row 6 only; rows 1–5 and 7 are byte-identical to `68dd9b1`. Limit 6's meaning is preserved and the change *removes* a number rather than relaxing the ruling. All seven still match HOMELAB-1964 as recorded on the ticket. - **No plaintext / real credentials** — the only credential-shaped strings in either file are the two `sk-ant-…-DRYRUN-…` placeholders (`:403-404`). Everything else is a path, a variable name, or a 12-hex fingerprint. Nothing added by the fix changes that. - **Both files still draft** — `status: draft` in both frontmatters (`auditor-charter.md:4`, `console-key-fallback.md:4`), "draft" in both bodies, "not ready" retained at `console-key-fallback.md:18`, §5 still shows dry run **blocked on B6** and ESO **blocked on B7/B2** (`:554-555`), and the spend cap stays deferred to the runbook (`:563`). HOMELAB-1946 (B18) is named five times in the charter (`:7`, `:16`, `:66`, `:109`, `:142`). - **Claim honesty** — the new §5 row "Credential handoff specified — **Done (design)**" is accurate: it says design and cites the step that asserts it, and no step is described as performed against an image that does not exist yet. ### Non-blocking suggestions 1. `console-key-fallback.md` frontmatter (`:8`) lists HOMELAB-2172/1934/1935 but not HOMELAB-1946, even though the fragment relies on B18.1 and hands its helpers to B18. A W3 worker searching by ticket would miss it. Pre-existing, not introduced by this delta. 2. Step 7's `|| exit 1` refinement above. 3. Step 4a/4c invoke `ld-claude-pane` as the container command without `--entrypoint`, while steps 2/3/4b/7/8 override the entrypoint with `bash`. If the dry-run image keeps the macro init as its entrypoint, 4a/4c would run the init instead. Worth one line stating the dry-run image's entrypoint. 4. The §3.5 runtime-dependency note (`:308-311`, `bash`/`python3`/`sha256sum`, "deliberately not self-contained") satisfies the reviewer's earlier footnote — no action, recording that it landed. ### Merge note **APPROVED, and I am not merging.** This repo's landing lock cannot pass for a main-targeted PR — CI only runs on `live`-targeted PRs, and there is no CI config on this branch — so the green-CI and post-commit-verdict legs are unreachable here by construction. This is a docs-only, 0-deletion change with no CI surface; the operator UI-merges in the Forgejo UI. No blocker findings remain open.
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin plane/HOMELAB-2171-posture:plane/HOMELAB-2171-posture
git switch plane/HOMELAB-2171-posture

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff plane/HOMELAB-2171-posture
git switch plane/HOMELAB-2171-posture
git rebase main
git switch main
git merge --ff-only plane/HOMELAB-2171-posture
git switch plane/HOMELAB-2171-posture
git rebase main
git switch main
git merge --no-ff plane/HOMELAB-2171-posture
git switch main
git merge --squash plane/HOMELAB-2171-posture
git switch main
git merge --ff-only plane/HOMELAB-2171-posture
git switch main
git merge plane/HOMELAB-2171-posture
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aaron/lab-director!12
No description provided.