Skip to content

feat(B-0852.3a): interactive cred-picker + zeta-install.sh Step 6.94 integration (16 tests; Aaron 2026-05-27 USB push)#5450

Merged
AceHack merged 4 commits into
mainfrom
feat/b-0852-3a-cred-picker-2026-05-27
May 27, 2026
Merged

feat(B-0852.3a): interactive cred-picker + zeta-install.sh Step 6.94 integration (16 tests; Aaron 2026-05-27 USB push)#5450
AceHack merged 4 commits into
mainfrom
feat/b-0852-3a-cred-picker-2026-05-27

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented May 27, 2026

Summary

End-to-end cred-persistence stack now usable on USB:

  • New `tools/installer/zeta-creds-picker.ts` — interactive picker per cred (bake/defer/skip + literal/file/env source)
  • 16 unit tests passing (parseArgs + runPicker against mock readline)
  • zeta-install.sh Step 6.94 invokes picker conditional on `ZETA_CREDS_PICKER=1 + ZETA_CREDS_PASSPHRASE + /etc/zeta/usb-uuid`
  • Picker invokes B-0852.2b persist CLI with collected --bake-cred args

Operator USB-push direction: "lets keep pushing forward and get cred persistance any anthing else we can make it in before i test again".

What unblocks on USB

Operator reflashes USB → boots → runs installer with picker env vars set → bakes desired creds via interactive prompt → reboot → /esp/zeta-creds.enc written. B-0852.4 NixOS module (boot-time restore) is the next sub-row.

Test plan

  • All 16 unit tests pass (`bun test tools/installer/zeta-creds-picker.test.ts`)
  • bash -n syntax check on zeta-install.sh
  • tsc clean
  • Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated worktree; operator's primary checkout untouched
  • Per .claude/rules/non-coercion-invariant.md HC-8: operator authority preserved; no default-bake; passphrase never logged; literal values redacted at display

AgencySignature

Commit message includes full v1 trailer block per the convention the operator pointed at 2026-05-27 (`tools/hygiene/audit-agencysignature-main-tip.ts` + spec in `docs/research/2026-04-26-gemini-deep-think-...md`). Heartbeat-via-commit closes the brief-ack counter externalization Kira flagged.

🤖 Generated with Claude Code

…integration (16 unit tests; consumes B-0852.2b persist CLI)

Implements operator's 2026-05-27 USB-push direction: ship cred-persistence
end-to-end before next USB test cycle.

**Picker (tools/installer/zeta-creds-picker.ts)**:

Interactive CLI that reads DEFAULT_MANIFEST (B-0852.5) + per-cred handler
contracts (B-0852.10), then prompts operator per cred:

  [b]ake-in NOW / [d]efer to device-flow at runtime / [s]kip

For bake-in choices, sub-prompts for value-source matching handler's
supportedSources:
  - [l]iteral (typed value; not logged)
  - [f]ile (@path syntax to B-0852.10 handler)
  - [e]nv (env:VAR syntax)

After picker loop completes, invokes zeta-creds-persist (B-0852.2b CLI)
with collected --bake-cred args + passphrase + usb-uuid + output path
+ optional persona.

Auto-skips persona-scoped creds when --persona not supplied (operator
choosing global-only install scope).

--dry-run mode prints the persist invocation without executing (useful
for test/debug).

Exit codes: 0 success / 2 arg-parse / 3 abort / 4 persist-failure.

**Tests (tools/installer/zeta-creds-picker.test.ts)**:

16 unit tests passing:
- parseArgs validation (6 tests covering well-formed + missing-required + unknown-flag)
- runPicker against mock readline (10 tests covering defer-all / bake-literal / bake-file / bake-env / empty-value-skip / persona-scoped auto-skip / persona-supplied bake / empty-choice-as-defer / unrecognized-choice-as-defer / explicit-skip)

Pure picker logic tested without spawning persist subprocess.

**zeta-install.sh Step 6.94 integration**:

Adds conditional Step 6.94 BEFORE existing Step 6.95 cred-persistence
block. Gated on three preconditions:
  - ZETA_CREDS_PICKER=1 env (opt-in; default skip preserves backward
    compat with automated/CI installs)
  - $ZETA_HOME/Zeta exists (pre-cloned repo from Step 6.95a-bootstrap)
  - /etc/zeta/usb-uuid exists (iter-4.2 ESP write surface)
  - ZETA_CREDS_PASSPHRASE env set

When all preconditions met: invokes picker as zeta user via sudo,
forwarding passphrase through env. Writes blob to /esp/zeta-creds.enc
which B-0852.4 NixOS module will consume at boot (future row).

Non-fatal failure: warns + continues (per .claude/rules/non-coercion-invariant.md
HC-8 — required-cred write failure surfaces but doesn't halt install).

**What this unblocks for operator's USB test cycle**:

- Operator can re-flash USB → boot → run installer → set ZETA_CREDS_PASSPHRASE + ZETA_CREDS_PICKER=1 → bake desired creds → reboot
- /esp/zeta-creds.enc is written; persistence verified empirically on USB
- B-0852.4 NixOS module (consume at boot) lands in next sub-row

Composes:
- B-0852.1 crypto (PR #5413)
- B-0852.2a envelope (PR #5421)
- B-0852.2b persist+restore CLIs (PR #5425)
- B-0852.3 row (PR #5449)
- B-0852.5 manifest (PR #5414)
- B-0852.10 handlers (PR #5418)
- B-0857.1 audit confirms Step 6.95a invocation (PR #5426)

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
over own creds; passphrase NEVER logged; literal values redacted at
display; declined creds defer (not coerced into bake-in default).

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-b0852-3a-picker-1215z; never touched
operator's primary checkout.

Per .claude/rules/holding-without-named-dependency-is-standing-by-failure.md:
this commit IS the externalized heartbeat per AgencySignature substrate
the operator pointed at 2026-05-27 — git log + audit-agencysignature-main-tip.ts
gives the counter mechanism the brief-ack rule's N=6 forcing function
needs to fire reliably.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-usb-push-keep-pushing-forward
Action-Mode: substrate-implementation
Task: B-0852.3a

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 13:36
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@AceHack AceHack enabled auto-merge (squash) May 27, 2026 13:36
Comment thread tools/installer/zeta-creds-picker.ts Fixed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an interactive credential picker (tools/installer/zeta-creds-picker.ts) that, for each cred in DEFAULT_MANIFEST, asks the operator whether to bake-in-now / defer-to-device-flow / skip, with per-source sub-prompts (literal / @file / env:VAR), then invokes the B-0852.2b zeta-creds-persist CLI with the collected --bake-cred args. A new Step 6.94 in zeta-install.sh wires the picker into the USB installer behind ZETA_CREDS_PICKER=1 + ZETA_CREDS_PASSPHRASE + /etc/zeta/usb-uuid gates, and 16 unit tests cover parseArgs and runPicker against a mock readline.

Changes:

  • New picker CLI with explicit bake / defer / skip prompts and source validation against per-cred handlers.
  • Conditional Step 6.94 in zeta-install.sh invoking the picker under sudo as the zeta user.
  • Bun unit tests exercising arg parsing, per-source bake paths, persona-scoping, empty/defer/skip semantics.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
tools/installer/zeta-creds-picker.ts New interactive picker that builds --bake-cred args and spawns the persist CLI.
tools/installer/zeta-creds-picker.test.ts Unit tests for parseArgs + runPicker against a mock readline.
full-ai-cluster/usb-nixos-installer/zeta-install.sh New Step 6.94 invoking the picker under sudo, behind env-var gates.

Comment thread full-ai-cluster/usb-nixos-installer/zeta-install.sh Outdated
Comment thread full-ai-cluster/usb-nixos-installer/zeta-install.sh Outdated
Comment thread tools/installer/zeta-creds-picker.ts
Comment thread tools/installer/zeta-creds-picker.ts
Comment thread full-ai-cluster/usb-nixos-installer/zeta-install.sh Outdated
Comment thread tools/installer/zeta-creds-picker.ts Outdated
AceHack added a commit that referenced this pull request May 27, 2026
…end-to-end USB test (Aaron 2026-05-27 USB push; consumes B-0852.3a picker blob) (#5454)

Files B-0852.4 row capturing the boot-time companion to the just-armed
B-0852.3a picker (PR #5450). Without B-0852.4, picker writes blob to ESP
but no boot-time consumer = blob ignored on reboot. With B-0852.4: full
persist → restore → use chain on real USB hardware.

Five sub-rows planned:
- 4a NixOS module file + systemd unit
- 4b interactive systemd-ask-password mode
- 4c file-based env-injected passphrase mode (simpler; first to ship)
- 4d wire into cluster-node common.nix
- 4e empirical USB end-to-end test

Implementation order: 4a → 4c (simpler passphrase path) → 4d → 4e
(USB validation) → 4b (interactive nicer-UX last).

Substrate-inventory pass per .claude/rules/verify-existing-substrate-before-authoring.md
cited inline. All upstream sub-rows merged (B-0852.1/.2a/.2b/.5/.10) +
picker armed (5450). P1 because gates operator's empirical USB test.

Composes with:
- B-0852.3a picker (PR #5450 in flight) — produces the blob this consumes
- B-0855 self-register architectural fix — fires AFTER cred-restore via systemd ordering
- B-0857 install.sh universal entry — install-time companion to this row's boot-time scope
- B-0833 installer interactive-login — declined-creds branch at user login
- .claude/rules/non-coercion-invariant.md HC-8 — required-fail surfaces; optional-fail warn-and-continue

Heartbeat-via-commit per the just-landed CLAUDE.md discipline (PR #5451):
filing this row IS counter-reset work per .claude/rules/holding-without-named-dependency-is-standing-by-failure.md
condition #3 while #5450 build-iso runs as the named bounded-wait.

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-b0852-4-row-1245z; operator primary
checkout untouched.

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority over
own creds preserved; passphrase NEVER logged; modes A/B both preserve
operator choice; required-cred-fail surfaces honestly.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-usb-push-keep-pushing-forward
Action-Mode: substrate-row-filing
Task: B-0852.4

Co-authored-by: Lior <lior@zeta.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
… bash -c interpolation; P0 CodeQL clear-text-logging; sudo arg ordering; eslint-disable; valueSpec→sourceChoice source label; Step 6.94→6.95-picker restructure (Aaron 2026-05-27 USB push)

7 unresolved review threads on #5450 resolved:

**P0 — Passphrase leak via bash -c arg-string interpolation (Copilot @1043)**
Was: `bash -c "...ZETA_CREDS_PASSPHRASE='$ZETA_CREDS_PASSPHRASE' bun..."`
The outer double-quote expanded $ZETA_CREDS_PASSPHRASE → literal
passphrase appeared in process arglist visible to `ps`.

Fix: use `sudo --preserve-env=ZETA_CREDS_PASSPHRASE -u USER HOME=... bash -c CMD`
where CMD references `--passphrase-env ZETA_CREDS_PASSPHRASE` (var-NAME
only). Passphrase never appears in arglist.

**P0 — CodeQL clear-text-logging in DRY RUN output (line 198)**
Was: `console.log(\`  bun \${persistArgs.join(" ")}\`)` — persistArgs
contains `--passphrase-env <NAME>` from operator input; the NAME is
CodeQL-tainted.

Fix: build displayArgs that maps position-after-`--passphrase-env` to
`<REDACTED>` literal. Same discipline as zeta-creds-persist/restore P0
fix on PR #5422.

**P1 — sudo arg ordering (Copilot @1038)**
Was: `sudo HOME=... -u ...` — HOME= before -u is invalid per sudo
manpage (options must precede arguments).

Fix: `sudo --preserve-env=... -u ... HOME=...` — options first, env-var
assignment between -u and command per sudo manpage.

**P1 — valueSpec in source-label ternary (Copilot @202)**
Was: `valueSpec.startsWith("@") ? "@file" : valueSpec.startsWith("env:") ? "env" : "literal"`
The output is just labels but Copilot flagged the value passing through
the ternary as a leak risk.

Fix: compute sourceLabel from operator's sourceChoice letter (l/f/e)
NOT from valueSpec. valueSpec never reaches the log path.

**P2 — eslint-disable for spawnSync (Copilot @201)**
Added `// eslint-disable-next-line sonarjs/no-os-command-from-path`
before the spawnSync("bun", ...) call per repo convention for
TS tools spawning PATH-resolved bins.

**P2 — Step 6.94 vs 6.95a-bootstrap ordering contradiction (Copilot @1052)**
Was: Step 6.94 claimed to read manifest from pre-cloned repo, but the
clone happened in 6.95a-bootstrap BELOW. Picker would fail at Step 6.94
(no repo, no bun).

Fix: restructured — Step 6.94 is now a header stub reserving the
number; ACTUAL picker invocation moved to NEW Step 6.95-picker INSIDE
the 6.95 block, AFTER 6.95a-bootstrap (repo + bun + mise present) +
BEFORE 6.95b device-flow logins (picker decides per-cred bake-vs-defer
+ device-flow handles the deferred subset).

**P2 — Header references Step 6.77 (Copilot @18)**
Was: picker file header said "Step 6.77" (speculative number from
B-0852.3 row body).

Fix: updated header to "Step 6.95-picker" matching the actual
integration step.

**Verification**:
- `bash -n full-ai-cluster/usb-nixos-installer/zeta-install.sh` → OK
- All 16 unit tests still pass

Per .claude/rules/blocked-green-ci-investigate-threads.md: verify-then-fix
discipline applied to each Copilot finding; one false-positive narrowed
(P1 valueSpec was technically OK but tightened anyway for clarity).

Per .claude/rules/non-coercion-invariant.md HC-8: passphrase NEVER logged
+ NEVER in arglist + redacted in DRY RUN; operator authority preserved.

Per .claude/rules/methodology-hard-limits.md: clinical/security floor
operative; P0 passphrase-leak fix lifts above the floor by removing
the leak path entirely (sudo --preserve-env keeps passphrase in env,
not arglist).

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: copilot-review-7-findings-on-pr-5450-resolved
Action-Mode: substrate-fix-fwd-security
Task: B-0852.3a

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread tools/installer/zeta-creds-picker.ts Fixed
…fe primitives — never reference parsed.passphraseEnv in logged string (CodeQL doesn't see runtime ternary breaking taint)

Prior fix used map-based redaction over persistArgs (which contains
parsed.passphraseEnv tainted via env-var-name access). CodeQL data-flow
analysis doesn't recognize runtime ternary as a sanitizer — the taint
still flows from the input to the log call statically, so the warning
re-fired.

Stronger pattern (matches the sibling persist/restore CLIs): construct
the display string from primitives only. NEVER reference
parsed.passphraseEnv OR parsed.passphraseFile in the logged string;
print literal placeholders like "<REDACTED>" / "<set>" instead.

displayCmd = "  bun tools/installer/zeta-creds-persist.ts --usb-uuid <set> --output <set>"
  + " --passphrase-file <REDACTED>"  (if --passphrase-file set)
  + " --passphrase-env <REDACTED>"   (if --passphrase-env set)
  + " --persona <set>"                (if --persona set)
  + " --bake-cred <id>=<REDACTED>"    (per bake; id is OK; value redacted)

All 16 tests still pass.

Per .claude/rules/blocked-green-ci-investigate-threads.md verify-then-fix
discipline: read line 210 directly, confirm the redaction was runtime-
only (CodeQL doesn't sanitize), rewrite to static-safety pattern.

Per .claude/rules/non-coercion-invariant.md HC-8: passphrase NEVER in
log path; operator authority over what gets logged preserved by total
redaction; <set>/<REDACTED> placeholders confirm presence without
revealing content.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: codeql-re-fire-on-line-210-after-prior-redaction-insufficient
Action-Mode: substrate-fix-fwd-security
Task: B-0852.3a

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 13:52
AceHack added a commit that referenced this pull request May 27, 2026
…sion-free filenames — composes existing ZetaID + AgencySignature substrate (Aaron 2026-05-27) (#5456)

* docs(B-0858): agent heartbeat folder direct-to-main with ZetaID-collision-free filenames — mechanizes externalized-counter (Aaron 2026-05-27 reminder of existing ZetaID + AgencySignature substrate)

Operator 2026-05-27 reminder: "the agencyheartbeats you were going to
make a spot where they can be pushed with no prs a foleder where you
can go strait to main and other agents with unique ids that won't
overlap, we started talking about 128 bit ids and such you could use
for unique heartbeats and such. these are called zetaids"

Found existing substrate I was not using:
- src/Core.TypeScript/zeta-id/zeta-id.ts (128-bit struct ID; persona +
  authority + momentum + timestamp + chromosome + randomness)
- tests/Tests.FSharp/ZetaId/CrossVerifyTests.fs (F# cross-verify harness)
- docs/zeta-id-v1-layout.yaml (canonical bit-layout spec)
- Kestrel review 2026-05-21 zeta-id-v1 preserved in persona/kestrel/
- tools/hygiene/audit-agencysignature-main-tip.ts (AgencySignature v1)

This row composes with the existing substrate to mechanize the
externalized-counter fix Kira P0 named + operator confirmed:

Folder layout: docs/agent-heartbeats/<persona>/<YYYY>/<MM>/<DD>/<zetaid>.md
Branch protection: path-scoped carve-out permits direct-to-main push
Heartbeat schema: zetaid + agent + runtime + model + timestamp +
  authority + momentum + named-dep + disposition + optional parent-pr

Why direct-to-main: per-tick heartbeat writes can't open PRs; the brief-
ack rule's N=6 forcing function needs trivial git-log queries over the
folder to fire reliably. ZetaID filenames prevent collision across
concurrent agent ticks (persona field + 32-bit randomness + 48-bit
timestamp = uniqueness by construction).

7 sub-rows planned (.1 spec → .2 branch protection → .3 writer tool →
.4 sentinel integration → .5 rule extension → .6 cleanup policy →
.7 collision verification). Sub-rows .3 + .5 are the load-bearing pair;
.2 is operator-side GitHub config.

Without this row: brief-ack failure mode catches recur until counter
mechanically externalizes. The 2026-05-27 catch (100+ "Quiet.") + Kira's
P0 finding are the empirical anchor.

Composes with:
- B-0852.3a picker (PR #5450)
- B-0855 self-register architectural fix
- B-0857 install.sh universal entry
- B-0666 English-as-projection (heartbeat schema IS projection)
- B-0628 Knights Guild Constitution-Class (heartbeat semantics may be C-Class candidate)
- .claude/rules/holding-without-named-dependency-is-standing-by-failure.md (this row provides the externalized-counter surface)
- .claude/rules/verify-existing-substrate-before-authoring.md (inventory cited inline)
- .claude/rules/agent-roster-reference-card.md (persona drives folder layout)
- CLAUDE.md "Heartbeat-via-commit" bullet (PR #5451)

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-b0858-heartbeat-folder-1300z; operator
primary checkout untouched.

Per .claude/rules/non-coercion-invariant.md HC-8: heartbeats are
operator-observable transparency-by-construction; no secrets in
heartbeat files (observational metadata only).

Filing this row IS heartbeat-via-commit work per the just-landed
CLAUDE.md discipline (PR #5451). Recursive: a row about heartbeat-folder
mechanization IS itself a heartbeat-class commit on the path toward
that mechanization.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-agency-heartbeats-zetaid-reminder
Action-Mode: substrate-row-filing
Task: B-0858

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0858 CI + scope): MD032 blank-line + operator follow-up — ZetaID category=3=Heartbeat already in registry/categories.yaml; bit-field grep indexing extends scope beyond heartbeats

Three MD032 fixes (markdownlint blocked CI):
- Line 37 ZetaID bit layout list needs blank line before
- Line 56 folder-layout 'Where:' list needs blank line before
- Line 117 substrate-inventory 'Searched:' list needs blank line before

Substantive scope additions per operator 2026-05-27 follow-up:
- "the ids are for easy lookup based many different bit id indexes
  built into the bits themselves so we can grep for things later,
  this does not have to be just heartbeat itd, it can be id for
  everything" — bit fields ARE the lookup indices; grep patterns
  on persona/authority/momentum/category extract event subsets
- "we have the abiity to defined it per category, category is in
  the bits so could have a custom one for heartbeat" — already true:
  registry/categories.yaml defines Category=3=Heartbeat (alongside
  Observation=0, Emission=1, Workflow=2); 16 slots total; 4 used

Substrate-inventory section updated to include registry/categories.yaml
FOUND.

Implication for B-0858.3 writer tool: when generating heartbeat IDs,
set category bits to 3 (Heartbeat slot) per existing registry. Future
event types can use other slots (16 total).

Per .claude/rules/verify-existing-substrate-before-authoring.md: the
operator's follow-up surfaced existing substrate I had not grepped
for. Substrate-honest: row now acknowledges the existing registry +
the broader bit-field-as-grep-index framing.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-followup-2026-05-27-zetaid-broader-scope-per-category-bits
Action-Mode: substrate-fix-fwd-ci-plus-scope-acknowledgment
Task: B-0858

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Lior <lior@zeta.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread full-ai-cluster/usb-nixos-installer/zeta-install.sh Outdated
… -c — match sibling 6.95a install steps (Copilot @1164)

Copilot finding: the picker invocation at Step 6.95-picker bash -c
didn't activate mise the way sibling 6.95a-claude/gemini/codex steps
do (lines 1119-1121 / 1129-1131 / 1139-1141 all
`eval "$(mise activate bash 2>/dev/null || true)"; bun ...` inside
the bash -c, with `BUN_INSTALL="$ZETA_HOME/.bun"` set). Without
mise activate, `bun` is not on the subshell PATH because mise installs
bun via shims; activate sets the PATH entry. Picker would fail with
"bun: command not found" at Step 6.95-picker time.

Fix: mirror the sibling pattern exactly:
- Add `BUN_INSTALL="$ZETA_HOME/.bun"` to sudo env prefix
- Add `set -o pipefail; eval "$(mise activate bash 2>/dev/null || true)";`
  prefix to bash -c
- Preserve --preserve-env=ZETA_CREDS_PASSPHRASE for passphrase forward

Verification: `bash -n full-ai-cluster/usb-nixos-installer/zeta-install.sh`
returns syntax OK.

Per .claude/rules/blocked-green-ci-investigate-threads.md verify-then-fix:
read the sibling step patterns at lines 1119-1141, confirm they all
follow same eval-mise-then-bun convention, apply the same to picker.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: copilot-thread-PRRT_kwDOSF9kNM6FHfK8-on-pr-5450
Action-Mode: substrate-fix-fwd-correctness
Task: B-0852.3a

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
auto-merge was automatically disabled May 27, 2026 14:01

Pull Request is not mergeable

@AceHack AceHack merged commit 41f0691 into main May 27, 2026
33 checks passed
@AceHack AceHack deleted the feat/b-0852-3a-cred-picker-2026-05-27 branch May 27, 2026 14:35
AceHack added a commit that referenced this pull request May 27, 2026
…luster common.nix imports — last gate for end-to-end USB cred-persistence test (Aaron 2026-05-27 USB priority) (#5476)

* feat(B-0852.4a): NixOS module zeta-creds-restore.nix — boot-time decrypt from ESP via systemd service (Aaron 2026-05-27 USB push; sibling to zeta-self-register.nix per B-0855.1)

Implements the boot-time consumer for the install-time picker (B-0852.3a
PR #5450). Composes with zeta-self-register.service which already
declares `after = "zeta-creds-restore.service"` per B-0855.1 module —
the dependency was wired upstream; this row makes the target service
actually exist.

**Module: full-ai-cluster/nixos/modules/zeta-creds-restore.nix**

NixOS module providing systemd service `zeta-creds-restore.service`:

- Disabled by default (`zeta.credsRestore.enable = false`); opt-in
  per host config (matches zeta-self-register sibling pattern)
- Ordering: `wantedBy=multi-user.target`, `after=local-fs.target` +
  `wants=local-fs.target` (ESP mounted before fire); B-0855.1 enforces
  `after=zeta-creds-restore.service` from its side
- ConditionPathExists guard: blob + USB UUID + restore CLI + bun shim
  must all exist (clean skip when picker wasn't run at install)
- Two passphrase modes (operator-configurable):
  - **file** (default): read from /run/zeta-creds-passphrase
    (operator pre-stages); deleted by ExecStopPost
  - **interactive**: systemd-ask-password on tty1 (300s timeout);
    writes zeta-readable temp file; deleted by ExecStopPost
- Invokes B-0852.2b restore CLI as zeta user via sudo with proper
  HOME + PATH + --target-root=/
- Optional --persona passthrough for per-persona-scoped creds
- Restart=on-failure with 30s backoff (per .claude/rules/non-coercion-invariant.md
  HC-8: required-cred failure surfaces honestly)

**Verification**: `nix-instantiate --parse` returns PARSE OK.

**What this unblocks for operator's USB test**:

End-to-end persist → restore → use chain now possible on real USB:
1. Operator reflashes USB
2. Boots, runs installer with ZETA_CREDS_PICKER=1 + ZETA_CREDS_PASSPHRASE=...
3. Picker writes /esp/zeta-creds.enc (B-0852.3a / PR #5450)
4. Operator enables zeta.credsRestore.enable=true + passphraseMode in
   host common.nix (B-0852.4d wiring; next sub-row)
5. Reboot → systemd fires zeta-creds-restore.service → blob decrypts →
   per-cred files populated in /home/zeta
6. zeta-self-register.service fires next per B-0855.1 ordering

Composes:
- B-0852.1 crypto (PR #5413; decrypt envelope)
- B-0852.2a envelope (PR #5421; parse blob format)
- B-0852.2b restore CLI (PR #5425; the binary this module wraps)
- B-0852.3a picker (PR #5450; produces the blob)
- B-0852.4 row (PR #5454; this is sub-row 4a)
- B-0852.5 manifest (PR #5414; drives per-cred path resolution)
- B-0855.1 zeta-self-register.nix (the sibling module that already
  expects this service to exist)
- B-0857 install.sh universal entry (install-time companion)

Remaining sub-rows planned (per B-0852.4 row):
- 4c: file-mode is implemented (default mode in this PR)
- 4b: interactive-mode also implemented (both modes ship together)
- 4d: wire into common.nix (next PR; simple imports list add)
- 4e: empirical USB end-to-end test (validates full chain on hardware)

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-b0852-4a-module-1250z; operator primary
checkout untouched.

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
over creds preserved; passphrase NEVER logged; interactive prompt
operator-driven; file-mode operator-staged; failure surfaces via
journalctl + restart policy.

Per .claude/rules/methodology-hard-limits.md: clinical/security floor
operative; cred-restore is purely defensive operator-data-recovery
substrate; no offensive use.

Heartbeat-via-commit per CLAUDE.md (PR #5451): this commit IS the
externalized counter tick; AgencySignature v1 trailer below; named
bounded-wait is #5450 build-iso completion.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-usb-push-keep-pushing-forward
Action-Mode: substrate-implementation
Task: B-0852.4a

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(B-0852.4d): wire zeta-creds-restore.nix into cluster common.nix imports — last gate before end-to-end USB test (Aaron 2026-05-27 USB priority)

Adds `./zeta-creds-restore.nix` to `full-ai-cluster/nixos/modules/common.nix`
imports list right after `./zeta-self-register.nix` — matches the
ordering B-0855.1 documents (zeta-self-register declares
`after = "zeta-creds-restore.service"`; both share import position).

Disabled-by-default (per the module's mkEnableOption); host configs
opt in via `zeta.credsRestore.enable = true;` AND operator pre-stages
a passphrase source. Imported here so every cluster-node type
(control-plane / worker-gpu) inherits the same module surface; the
opt-in flip lives at host-config level not common.nix level.

Composes:
- B-0852.4a (this PR's earlier commit ef45b4f) — the module file itself
- B-0852.3a picker (PR #5450) — install-time blob writer
- B-0852.4 row (PR #5454 merged) — substrate-engineering parent
- B-0855.1 zeta-self-register.nix — already declares `after = "zeta-creds-restore.service"`
- iter-5.5.0 install flow — picker writes blob during install; module restores at boot

**Empirical USB test path now complete end-to-end**:
1. Reflash USB with ISO carrying these changes
2. Boot, run installer with ZETA_CREDS_PICKER=1 + ZETA_CREDS_PASSPHRASE=...
3. Step 6.95-picker writes /esp/zeta-creds.enc (B-0852.3a)
4. Operator enables `zeta.credsRestore.enable = true;` in host config
   + pre-stages /run/zeta-creds-passphrase
5. Reboot → zeta-creds-restore.service fires → blob decrypted →
   per-cred files populated in /home/zeta
6. zeta-self-register.service fires next per B-0855.1 ordering

Verification:
- `nix-instantiate --parse full-ai-cluster/nixos/modules/common.nix` → PARSE OK
- `nix-instantiate --parse full-ai-cluster/nixos/modules/zeta-creds-restore.nix` → PARSE OK

Per .claude/rules/non-coercion-invariant.md HC-8: opt-in default
preserves operator authority over per-host enablement; importing
the module surface doesn't activate it.

Per .claude/rules/agent-worktree-hygiene-never-hold-main-...: isolated
worktree at /private/tmp/zeta-b0852-4a-module-1250z; operator primary
checkout untouched.

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: operator-direction-2026-05-27-back-to-usb-after-heartbeat-iteration
Action-Mode: substrate-implementation-final-usb-gate
Task: B-0852.4d

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(B-0852.4a): 3 Copilot findings — P0 root-write for /etc paths + P0 ExecStopPost-never-fires + P1 USB UUID newline trim

3 Copilot threads on PR #5476:

**P0 (@180): sudo -u ${cfg.user} can't write to /etc paths.**
The default cred manifest includes /etc/zeta/operator-authorized-keys
+ /etc/ssh/ssh_host_* (root-owned paths zeta user can't write).
Fix: run restore CLI AS ROOT directly (drop the sudo -u zeta drop).
Post-restore find ${cfg.home} -user root -exec chown zeta:users
to fix ownership on user-facing creds (~/.config/gh, ~/.config/claude,
~/.gemini, ~/.codex). Operator's pre-existing configs (already
zeta-owned) untouched by the -user root filter.

**P0 (@189): RemainAfterExit=true + Type=oneshot means
ExecStopPost never fires on successful boot.**
The unit stays "active" after ExecStart returns; systemd doesn't
treat that as a "stop" event so ExecStopPost is skipped. Passphrase
cleanup never runs. Fix: move cleanup to bash EXIT trap inside
ExecStart — fires on ANY exit path (success or failure), unaffected
by RemainAfterExit semantics. Removed standalone ExecStopPost.

**P1 (@140): USB_UUID trailing newline from cat.**
`cat /etc/zeta/usb-uuid` includes trailing \n if file ends with one.
Fix: `tr -d '[:space:]' < ${cfg.usbUuidPath}` strips all whitespace
(safer than just newlines; covers \r\n + leading whitespace too).

Per .claude/rules/blocked-green-ci-investigate-threads.md verify-then-fix:
each Copilot finding read against actual file content; all 3 real
findings; bundled fix with rationale per finding.

Verification: `nix-instantiate --parse full-ai-cluster/nixos/modules/zeta-creds-restore.nix`
returns PARSE OK.

Per .claude/rules/non-coercion-invariant.md HC-8: operator authority
preserved (chown only touches root-owned files; pre-existing
zeta-owned files untouched).

Agency-Signature-Version: 1
Agent: Otto
Agent-Runtime: Claude Code (auto mode)
Agent-Model: claude-opus-4-7
Credential-Identity: aaron-otto-vscode
Credential-Mode: operator-authorized
Human-Review: pre-merge-pending
Human-Review-Evidence: copilot-3-findings-on-pr-5476-2-p0-1-p1
Action-Mode: substrate-fix-fwd-security-plus-correctness
Task: B-0852.4a

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Lior <lior@zeta.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants