--all drains an injected process-substitution key after the first project #36

Closed
opened 2026-07-13 22:51:18 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-13 22:51:18 +00:00 (Migrated from github.com)

Follow-up from the review of #35 (which fixed #34).

The edge

decryptSecrets now reads the identity itself (readFileSync(keyFile)) and hands it to age -d -i - on stdin — correct, and it makes the documented CAST_AGE_KEY_FILE_<ENV>=<(pm read …) recipe work. But a process substitution is a read-once pipe, and runProject is called in a loop under diff --all / apply --all (cli.ts, the for (const [i, repo] of targets.entries()) loop). Each project with a secrets store re-reads the key file:

  • project 1 drains the pipe and decrypts fine;
  • project 2 gets an empty identity, age fails, and the failure is caught by the fleet loop's catch-all — misreported as unreachable in diff --all, and aborting the fleet in apply --all.

Why it's latent, not live

Only one registered project (heavy-duty/incubator) has a prod store today, so no current invocation hits it. And the old code never worked with <(…) at all, so this is a new capability's edge, not a regression. It becomes real the moment a second project gains a secrets store.

Suggested fix

Read the key bytes once per process — memoize by resolved key path (or read in main and thread the bytes through). Exposure is equivalent: the key already transits node memory on every call. A FIFO-based test would pin the read-once shape (the current regression test uses a regular file behind /proc/self/fd/N, which reproduces the path-resolution shape but re-opens at offset 0 on every read).

🤖 Generated with Claude Code

Follow-up from the review of #35 (which fixed #34). ## The edge `decryptSecrets` now reads the identity itself (`readFileSync(keyFile)`) and hands it to `age -d -i -` on stdin — correct, and it makes the documented `CAST_AGE_KEY_FILE_<ENV>=<(pm read …)` recipe work. But a process substitution is a **read-once pipe**, and `runProject` is called in a loop under `diff --all` / `apply --all` (cli.ts, the `for (const [i, repo] of targets.entries())` loop). Each project with a secrets store re-reads the key file: - project 1 drains the pipe and decrypts fine; - project 2 gets an **empty identity**, age fails, and the failure is caught by the fleet loop's catch-all — misreported as `unreachable` in `diff --all`, and aborting the fleet in `apply --all`. ## Why it's latent, not live Only one registered project (`heavy-duty/incubator`) has a prod store today, so no current invocation hits it. And the old code never worked with `<(…)` at all, so this is a new capability's edge, not a regression. It becomes real the moment a second project gains a secrets store. ## Suggested fix Read the key bytes **once per process** — memoize by resolved key path (or read in `main` and thread the bytes through). Exposure is equivalent: the key already transits node memory on every call. A FIFO-based test would pin the read-once shape (the current regression test uses a regular file behind `/proc/self/fd/N`, which reproduces the path-resolution shape but re-opens at offset 0 on every read). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
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: heavy-duty/cast#36
No description provided.