Skip to content

feat(spawn): inject Automic Vault secrets into worker launch templates - #2479

Closed
ansellchiu wants to merge 1 commit into
kunchenguid:mainfrom
ansellchiu:fm/fm-av-inject-s1
Closed

feat(spawn): inject Automic Vault secrets into worker launch templates#2479
ansellchiu wants to merge 1 commit into
kunchenguid:mainfrom
ansellchiu:fm/fm-av-inject-s1

Conversation

@ansellchiu

Copy link
Copy Markdown

Intent

Implement the Automic Vault (av) secret-injection wrapper in fm-spawn.sh's launch templates, per the automic-vault-r1 research plan (data/automic-vault-r1/report.md and decisions.md). Goal: workers (crewmates and secondmates) receive the captain's static API keys from Automic Vault as environment variables, injected via 'av inject +KEY... -- ', with the value never written to disk and never printed or logged.

Exact keys from the plan: EXA_API_KEY, PARALLEL_API_KEY, TAVILY_API_KEY, LINKUP_API_KEY, BRAVE_SEARCH_API_KEY, BRAVE_ANSWERS_API_KEY, DEEPSEEK_API_KEY, BUZZ_XYZ_KEY (buzz.xyz renamed because '.'/space are invalid av key names). av inject copies the full ambient env plus the named secrets and execs the child, so every existing env prefix must stay BEFORE 'av inject' on the launch line. Captain approvals (decisions.md): keep secrets 'When Unlocked', grant per-secret Direct Access Rules to the verified launcher, keep 1Password originals as fallback, and ship this fm-spawn.sh wrapper.

Deliberate design decisions I made:

  • Injection is OFF by default via a new opt-in local, gitignored 'config/av-inject' toggle (absent/off/false/no/0/unrecognized = off; on/true/yes/1 = on; FM_AV_INJECT env overrides the file, mainly for tests). This is intentional and required: the plan mandates an attended per-key 'av save' migration BEFORE injection can succeed, and turning it on before the keys exist would make every 'av inject' fail closed and break every spawn. It also keeps the shared template safe on homes and CI without Automic Vault (macOS-only app).
  • New bin/fm-av-inject-lib.sh owns the contract (mode parsing, av absolute-path resolution mirroring resolve_pi_executable, key-name validation matching av's [A-Za-z_][A-Za-z0-9_]* rule, and the ' inject +KEY... -- ' prefix). It fails closed: when enabled but 'av' is missing or a key name is invalid, the spawn refuses loudly rather than launching without the expected secrets. Mode parsing uses only bash builtins (case-insensitive glob patterns, no external tr) so it is portable to bash 3.2 and works even under a restricted PATH.
  • fm-spawn.sh places an AVINJECT placeholder immediately before the agent binary token in every verified launch template (claude, codex, opencode, pi/pi-signed, grok, cursor, kimi, muse), so all env prefixes (both template-internal like CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION and later-prepended like FM_HOME/FM_PI_HARNESS/CLAUDE_CONFIG_DIR/'unset TRACEPARENT;') remain before 'av inject'. The prefix is resolved once, early (right after harness/template resolution), so a misconfigured home fails fast before heavy spawn setup. The raw unverified-adapter escape hatch is deliberately left unwrapped (no placeholder) - injection applies to verified templates only; this is a documented boundary.
  • config/av-inject is primary-authoritative and inherited into secondmate homes by adding it to FM_INHERITABLE_CONFIG in fm-config-inherit-lib.sh, alongside the other local config toggles, so a secondmate's own workers inherit the same injection. Relaunch (fm-control.sh relaunch -> fm-spawn --relaunch) reuses the same template path, so it is covered automatically.
  • Documentation: a new owning section in docs/configuration.md, a one-line entry in AGENTS.md section 2 layout, and updates to the inheritance listings in the secondmate-provisioning skill and docs/configuration.md's config-push listing. Tracked docs deliberately do NOT link data/automic-vault-r1/report.md because data/ is gitignored (would be a broken local link); the lib header points to that private research record instead.
  • Tests: tests/fm-av-inject.test.sh covers the library units (mode precedence, disabled no-op, fail-closed when av missing, the wrapper string for the default key set, key-name rejection) plus a spawn-path regression with a fake tmux, fake av, and a real isolated git worktree proving a real claude launch is wrapped with env prefixes before 'av inject' only when opted in, and unwrapped by default.

This is a change to firstmate's shared tracked material, developed under firstmate-coding-guidelines (one-owner rule, inline-stub/trigger hygiene, one-sentence-per-line Markdown, plain dashes, colocated behavior-driven test).

What Changed

  • Added bin/fm-av-inject-lib.sh, which owns the av inject +KEY... -- contract: opt-in mode parsing (via a gitignored config/av-inject toggle, overridable by FM_AV_INJECT), absolute-path resolution of av, key-name validation, and construction of the injection prefix for the fixed key set (EXA, PARALLEL, TAVILY, LINKUP, BRAVE_SEARCH, BRAVE_ANSWERS, DEEPSEEK, BUZZ_XYZ). It fails closed when enabled but av is missing or a key name is invalid.
  • Wired an __AVINJECT__ placeholder into every verified launch template in bin/fm-spawn.sh (claude, codex, opencode, pi/pi-signed, grok, cursor, kimi, muse), resolved once early so all env prefixes stay before av inject; the raw unverified-adapter path is deliberately left unwrapped. Added config/av-inject to FM_INHERITABLE_CONFIG in bin/fm-config-inherit-lib.sh so secondmate homes inherit the toggle.
  • Documented the toggle in docs/configuration.md, AGENTS.md, and the secondmate-provisioning skill, and added tests/fm-av-inject.test.sh covering the library units plus a spawn-path regression proving launches are wrapped (env prefixes before av inject) only when opted in.

Risk Assessment

✅ Low: Well-bounded, opt-in, default-off change with correct env-prefix ordering across all templates, fail-closed resolution, and thorough colocated tests; no reachable defect or intent contradiction found.

Testing

Ran the focused suite tests/fm-av-inject.test.sh (7/7 pass) and produced product-level CLI transcripts showing the actual end-user surface: the full 8-key av inject +KEY... -- wrapper string, both fail-closed refusals with their exact operator-facing error messages, and the literal claude launch command fm-spawn.sh sends to the pane in both an opted-in home (env prefixes intact before av inject, keys in plan order, -- claude after) and a default home (unwrapped). This is a bash/CLI change with no rendered UI surface, so the appropriate reviewer-visible evidence is the captured launch-command transcripts rather than screenshots. All checks pass; worktree left clean; evidence written to the dedicated evidence directory.

Evidence: Real fm-spawn.sh claude launch line: opted-in (wrapped) vs default (unwrapped)

[OPTED-IN] env -u CURSOR_AGENT -u CURSOR_INVOKED_AS CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false '.../av' inject +EXA_API_KEY +PARALLEL_API_KEY +TAVILY_API_KEY +LINKUP_API_KEY +BRAVE_SEARCH_API_KEY +BRAVE_ANSWERS_API_KEY +DEEPSEEK_API_KEY +BUZZ_XYZ_KEY -- claude --dangerously-skip-permissions ... [DEFAULT] env -u CURSOR_AGENT -u CURSOR_INVOKED_AS CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions ... (no inject wrapper)

=== fm-spawn.sh end-to-end: literal claude launch line sent to the pane ===

[OPTED-IN home: config/av-inject=on] the worker is launched as:
-----------------------------------------------------------------
treehouse get
export GOTMPDIR=/tmp/fm-on-z1/gotmp
env -u CURSOR_AGENT -u CURSOR_INVOKED_AS CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false '/var/folders/52/xcqf00nn5kd1k_zk6377bs340000gn/T//fm-av-inject-evid.nWjm6x/spawn-on/fake/fakebin/av' inject +EXA_API_KEY +PARALLEL_API_KEY +TAVILY_API_KEY +LINKUP_API_KEY +BRAVE_SEARCH_API_KEY +BRAVE_ANSWERS_API_KEY +DEEPSEEK_API_KEY +BUZZ_XYZ_KEY -- claude --dangerously-skip-permissions "$('/Users/achiu/.no-mistakes/worktrees/d9c8a3404af5/01M05FTYK7NQBD6ASFRKGYV2MP/bin/fm-operational-input.sh' encode launch-brief < '/var/folders/52/xcqf00nn5kd1k_zk6377bs340000gn/T//fm-av-inject-evid.nWjm6x/spawn-on/home/data/on-z1/brief.md')"
-----------------------------------------------------------------

[DEFAULT home: no config/av-inject] the worker is launched UNCHANGED as:
-----------------------------------------------------------------
treehouse get
export GOTMPDIR=/tmp/fm-off-z1/gotmp
env -u CURSOR_AGENT -u CURSOR_INVOKED_AS CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions "$('/Users/achiu/.no-mistakes/worktrees/d9c8a3404af5/01M05FTYK7NQBD6ASFRKGYV2MP/bin/fm-operational-input.sh' encode launch-brief < '/var/folders/52/xcqf00nn5kd1k_zk6377bs340000gn/T//fm-av-inject-evid.nWjm6x/spawn-off/home/data/off-z1/brief.md')"
-----------------------------------------------------------------
Evidence: Wrapper string + fail-closed operator messages

[1] on -> '<av>' inject +EXA_API_KEY ... +BUZZ_XYZ_KEY -- [2] off -> prefix=[] rc=0 [3] enabled+no av -> rc=1 error: av-inject is enabled but the 'av' CLI (Automic Vault) was not found on PATH... [4] invalid key -> rc=1 error: av-inject key name 'bad-key' is not a valid secret name ([A-Za-z_][A-Za-z0-9_]*)

=== Automic Vault secret-injection wrapper: end-user launch evidence ===

[1] Opted-in home (config/av-inject=on) -> full wrapper prepended to the worker launch.
    All 8 captain-approved keys, resolved absolute av path, values never printed:
    '/var/folders/52/xcqf00nn5kd1k_zk6377bs340000gn/T/tmp.d0czevFYuE/bin/av' inject +EXA_API_KEY +PARALLEL_API_KEY +TAVILY_API_KEY +LINKUP_API_KEY +BRAVE_SEARCH_API_KEY +BRAVE_ANSWERS_API_KEY +DEEPSEEK_API_KEY +BUZZ_XYZ_KEY -- 

[2] Default home (no opt-in / config/av-inject=off) -> empty prefix, launch unchanged:
    prefix=[]  rc=0

[3] Fail-closed: enabled but the 'av' CLI is not installed -> spawn refuses loudly:
    rc=1  error: av-inject is enabled but the 'av' CLI (Automic Vault) was not found on PATH; install Automic Vault or set config/av-inject to off

[4] Fail-closed: an invalid secret key name is rejected:
    rc=1  error: av-inject key name 'bad-key' is not a valid secret name ([A-Za-z_][A-Za-z0-9_]*)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-spawn.sh:1242 - AV_INJECT_PREFIX is resolved unconditionally at bin/fm-spawn.sh:1242, before the harness type is known to be a verified template vs. the raw unverified-adapter escape hatch (LAUNCH=$ARG3 at line 1191, which carries no AVINJECT placeholder). Consequence: an av-inject-enabled home that spawns a raw unverified adapter while av is missing on PATH will fail closed and refuse to spawn, even though injection would never have been applied to that launch. This is fail-safe (a misconfigured enabled home shouldn't be missing av anyway) and the injection-applies-to-verified-only boundary is still honored when av is present, so no action is needed; noting the interaction only for transparency.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-av-inject.test.sh — all 7 checks pass (mode precedence, disabled no-op, fail-closed on missing av, default-key wrapper string, key-name rejection, opt-in spawn wrapping, default unwrapped spawn)
  • Manual: sourced bin/fm-av-inject-lib.sh under bash and ran fm_av_inject_prefix for on/off/missing-av/invalid-key cases, captured to av-inject-wrapper.txt
  • Manual e2e: drove bin/fm-spawn.sh with fake tmux/av and a real isolated git worktree to capture the literal claude launch line for an opted-in vs. default home, captured to av-inject-spawn-launch.txt
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

Add an opt-in `config/av-inject` toggle that wraps every verified worker
launch in `av inject +KEY... -- <launch>` so crewmates and secondmates
receive the captain's static API keys from Automic Vault as environment
variables, never written to disk and never printed.

- bin/fm-av-inject-lib.sh owns the mode parsing (default off; on/true/yes/1
  enable, FM_AV_INJECT overrides the file), av resolution, key-name
  validation, and the `<av> inject +KEY... -- ` wrapper. Enabled-but-missing
  av refuses the spawn rather than launching without the expected secrets.
- fm-spawn.sh places an __AVINJECT__ placeholder immediately before the
  agent binary in every launch template so all env prefixes stay before
  av inject; the raw unverified-adapter escape hatch is deliberately
  unwrapped. The prefix is resolved once, early, so a misconfigured home
  fails fast.
- config/av-inject is primary-authoritative and inherited into secondmate
  homes alongside the other local config toggles.
- Documented in AGENTS.md, docs/configuration.md, and secondmate-provisioning;
  covered by tests/fm-av-inject.test.sh (library units plus a spawn-path
  regression proving the real claude launch is wrapped only when opted in).

The keys and injection mechanics follow the automic-vault-r1 research plan.
Injection stays OFF by default because it must not be enabled before the
attended per-key `av save` migration is verified.
@ansellchiu ansellchiu closed this Aug 16, 2026
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.

1 participant