Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/secondmate-provisioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ That no-fetch path is a purely local fast-forward of tracked files, never an ori
A remote launch and the deferred bootstrap sweep ask the configured host to fast-forward its persistent home to that host's code-root commit under the same clean and ancestry guards.
`/updatefirstmate` first updates the remote code root from its own origin, then runs that guarded home sync.
SSH exit 255 preserves the route and reports unknown completion; it never triggers local respawn or failover.
The same placement-specific launch and deferred bootstrap sweep also propagate the primary's declared inherited local material: `config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`, and the one shared captain-preference file `data/captain-shared.md`.
The same placement-specific launch and deferred bootstrap sweep also propagate the primary's declared inherited local material: `config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`, `config/av-inject`, and the one shared captain-preference file `data/captain-shared.md`.
Because these paths are gitignored, that propagation is a separate, primary-authoritative copy independent of the tracked-files fast-forward: it re-converges every live home whether or not its tracked files advanced, and it touches only the declared items.
Propagation failures warn without blocking secondmate launch or session-start continuation, and the destination keeps whatever safely validated state the helper left behind.
Inheritance copies the literal `config/crew-harness` file, so a secondmate's own crewmates use the primary's crewmate harness only when it names a concrete adapter such as `codex`; an unset or `default` value has nothing concrete to inherit, and the secondmate's own crewmates fall back to the secondmate's own or detected harness instead.
Expand All @@ -125,7 +125,7 @@ Keep every `data/learnings.md` fully local by captain decision; route fleet-gene
No AGENTS.md reread nudge is needed at spawn or respawn because the agent reads instructions fresh on launch; only the bootstrap sweep's running-home instruction-surface advance needs that AGENTS.md re-read.
Bootstrap reports successful AGENTS.md re-read sends as `BOOTSTRAP_INFO:` and only emits `NUDGE_SECONDMATES:` when that send fails and needs retry.
A separate, literal-content config reread is required whenever inherited `config/*` material changes under an already-running secondmate.
For a local home, after each successful allowlisted config write, both the locked bootstrap convergence path and mid-session `bin/fm-config-push.sh` use the shared propagation report to build one per-home generation-specific private instruction file from the validated destination post-write bytes for only the allowlisted config items that actually changed for that home (`config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`), in deterministic allowlist order.
For a local home, after each successful allowlisted config write, both the locked bootstrap convergence path and mid-session `bin/fm-config-push.sh` use the shared propagation report to build one per-home generation-specific private instruction file from the validated destination post-write bytes for only the allowlisted config items that actually changed for that home (`config/crew-dispatch.json`, `config/crew-harness`, `config/backlog-backend`, `config/backend`, `config/herdr-presentation-spaces`, `config/startup-memory-budget`, `config/av-inject`), in deterministic allowlist order.
Each changed path is printed with clear begin/end delimiters and the destination file's full exact new bytes unparsed, or the explicit token `ABSENT` when propagation removed the destination copy.
The instruction uses only minimal framing that these are defaults/rules and do not remove judgment; it never includes SHA values, selected profiles, parsed summaries, or any other generated interpretation.
`data/captain-shared.md` is not a config file and is never inlined into this instruction file or message.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ config/calm Pi Calm presentation preference; LOCAL, gitignored, and not inhe
config/startup-memory-budget primary-authoritative per-home startup-memory budget; LOCAL, gitignored, materialized as 7,500 estimated tokens by locked primary bootstrap and inherited into secondmate homes; see docs/configuration.md "Startup memory budget"
config/herdr-presentation-spaces optional "off" opt-out from, or "on" opt-in to, Herdr's default-on disposable single-task visual projection, which is unconfigured-default-on only at or above a Herdr version floor; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "Presentation spaces"
config/trace-context optional presence flag enabling default-off native W3C trace-context propagation to spawned agents; LOCAL, gitignored; inherited by secondmate homes; see docs/configuration.md "Trace context propagation" and docs/trace-context.md
config/av-inject optional "on" toggle wrapping every worker launch in `av inject +KEY... -- <launch>` so workers receive the captain's static API keys from Automic Vault as environment variables; LOCAL, gitignored; OFF by default, inherited by secondmate homes; see docs/configuration.md "Automic Vault secret injection"
config/cmux-socket-password optional cmux control-socket password; LOCAL, gitignored; read fresh on every cmux CLI call and passed through without ever overriding an operator's own ambient CMUX_SOCKET_PASSWORD when absent (docs/cmux-backend.md "Setup")
config/wedge-alarm optional away-mode wedge-alarm active-alert directives; LOCAL, gitignored; absent means auto (macOS Notification Center when available); see docs/wedge-alarm.md
config/x-mode.env generated Relay watcher cadence; LOCAL, gitignored; source before arming watcher when present
Expand Down
124 changes: 124 additions & 0 deletions bin/fm-av-inject-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# shellcheck shell=bash
# Automic Vault secret-injection primitives.
# Usage: . bin/fm-av-inject-lib.sh
#
# This library owns the contract for wrapping a crewmate/secondmate launch in
# `av inject +KEY... -- <launch>` so the worker receives the captain's static
# API keys from Automic Vault as environment variables, never on disk and never
# printed. bin/fm-spawn.sh assembles the launch command and calls
# fm_av_inject_prefix once per spawn to obtain the wrapper text.
#
# Automic Vault ("av") is a macOS-only local secrets manager: `av inject` copies
# the full ambient environment plus the named secrets and execs the target, so
# every env prefix firstmate already sets (FM_HOME, CLAUDE_CONFIG_DIR,
# TRACEPARENT, ...) is preserved as long as it appears BEFORE `av inject` on the
# launch line. The value reaches the worker's environment only; `av inject`
# itself prints nothing. See data/automic-vault-r1/report.md for the researched
# model and the migration prerequisites (verified launcher, per-secret Direct
# Access Rules, availability), and data/automic-vault-r1/decisions.md for the
# captain's approvals.
#
# Enablement is OFF by default because the shared template runs on homes and CI
# that have no Automic Vault app, and because the secrets must be migrated with
# an attended `av save` copy BEFORE injection can succeed (turning it on before
# the keys exist would make every `av inject` fail closed and break every
# spawn). Turn it on per home only after the migration is verified.

# The static API keys approved for injection (data/automic-vault-r1 plan, key
# names exactly as stored by `av save`). Key NAMES are not secret (they are what
# `av list` returns); the VALUES live only in the macOS keychain and are never
# handled here. Override for tests with FM_AV_INJECT_KEYS.
FM_AV_INJECT_DEFAULT_KEYS="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"

FM_AV_INJECT_FILE="av-inject"
FM_AV_INJECT_ERROR=""

# Read the enablement decision. FM_AV_INJECT (env) wins over the local,
# gitignored config/<FM_AV_INJECT_FILE> file; absent/empty/off/false/no/0 all
# mean disabled (the default), on/true/yes/1 mean enabled. An unrecognized value
# is treated as disabled so a typo fails safe rather than breaking every spawn.
# Prints "on" or "off".
# Args: <config-dir>
fm_av_inject_mode() { # <config-dir>
local raw="" file="$1/$FM_AV_INJECT_FILE"
if [ -n "${FM_AV_INJECT:-}" ]; then
raw=$FM_AV_INJECT
elif [ -f "$file" ]; then
# `read` takes the first line with leading/trailing whitespace trimmed; a
# missing final newline still yields the value, so `|| true` guards the
# non-zero read at EOF. Builtins only, so the check works even when PATH is
# too restricted to resolve coreutils.
IFS= read -r raw < "$file" 2>/dev/null || true
raw=${raw#"${raw%%[![:space:]]*}"}
raw=${raw%"${raw##*[![:space:]]}"}
fi
# Case-insensitive bracket patterns avoid a lowercasing external command
# (portable to bash 3.2, and works under a PATH too restricted for coreutils).
case "$raw" in
[Oo][Nn]|[Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|1) printf 'on\n' ;;
*) printf 'off\n' ;;
esac
}

# Resolve the `av` executable to an absolute path, mirroring resolve_pi_executable
# in bin/fm-spawn.sh so the pane runs the same signed CLI firstmate resolved.
# Prints the absolute path on success; returns non-zero when not found.
fm_av_inject_bin() {
local candidate dir
candidate=$(type -P -- av 2>/dev/null) || return 1
[ -x "$candidate" ] || return 1
case "$candidate" in
/*) printf '%s\n' "$candidate" ;;
*)
dir=$(cd "$(dirname "$candidate")" 2>/dev/null && pwd -P) || return 1
printf '%s/%s\n' "$dir" "$(basename "$candidate")"
;;
esac
}

# Compute the launch-command prefix that wraps a worker launch in `av inject`.
# When injection is disabled, prints nothing and succeeds (the launch is
# unchanged). When enabled, resolves `av` and every key name and prints
# `<av> inject +KEY1 +KEY2 ... -- ` (with a trailing space) so the caller can
# splice it immediately before the agent binary. Fails closed with a message in
# FM_AV_INJECT_ERROR when enabled but `av` is missing or a key name is invalid,
# so a misconfigured home refuses to spawn rather than launching without the
# secrets it was told to inject.
# Args: <config-dir>
fm_av_inject_prefix() { # <config-dir>
local config_dir=$1 av key keys quoted
FM_AV_INJECT_ERROR=""
[ "$(fm_av_inject_mode "$config_dir")" = on ] || { printf '%s' ''; return 0; }
if ! av=$(fm_av_inject_bin); then
FM_AV_INJECT_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"
return 1
fi
keys=${FM_AV_INJECT_KEYS:-$FM_AV_INJECT_DEFAULT_KEYS}
# `av` validates key names as [A-Za-z_][A-Za-z0-9_]* (src/cli/inject.rs); mirror
# that here so an invalid name is a loud refusal rather than a mangled launch.
quoted=""
for key in $keys; do
case "$key" in
[A-Za-z_]*)
case "$key" in
*[!A-Za-z0-9_]*)
FM_AV_INJECT_ERROR="av-inject key name '$key' is not a valid secret name ([A-Za-z_][A-Za-z0-9_]*)"
return 1
;;
esac
;;
*)
FM_AV_INJECT_ERROR="av-inject key name '$key' is not a valid secret name ([A-Za-z_][A-Za-z0-9_]*)"
return 1
;;
esac
quoted="$quoted +$key"
done
if [ -z "$quoted" ]; then
FM_AV_INJECT_ERROR="av-inject is enabled but no secret key names are configured"
return 1
fi
# <av> inject +K1 +K2 -- <agent...>. av path is single-quoted; key names passed
# this validation and need no quoting.
printf "'%s' inject%s -- " "$(printf '%s' "$av" | sed "s/'/'\\\\''/g")" "$quoted"
}
6 changes: 4 additions & 2 deletions bin/fm-config-inherit-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
# spawn on codex too, primary config/backlog-backend=manual makes that home
# hand-edit backlog files too, primary config/backend pins that home's local
# runtime-backend default for future spawns, primary config/startup-memory-budget
# bounds that home's startup-memory curation, and primary
# bounds that home's startup-memory curation, primary config/av-inject makes that
# home wrap its own worker launches in the same Automic Vault secret injection,
# and primary
# config/herdr-presentation-spaces carries the same Herdr presentation-projection
# preference - an absent primary file and an absent destination file both mean
# the same unconfigured default, so the generic absence mirror below converges
Expand Down Expand Up @@ -63,7 +65,7 @@ FM_SHARED_CAPTAIN_MODE="444"
# The declared inheritable set (space-separated, config-dir-relative item paths).
# Extend here to inherit more of the primary's local config; override via the
# environment only in tests. Items must not contain whitespace.
FM_INHERITABLE_CONFIG="${FM_INHERITABLE_CONFIG:-crew-dispatch.json crew-harness backlog-backend backend herdr-presentation-spaces startup-memory-budget trace-context}"
FM_INHERITABLE_CONFIG="${FM_INHERITABLE_CONFIG:-crew-dispatch.json crew-harness backlog-backend backend herdr-presentation-spaces startup-memory-budget trace-context av-inject}"

# Items whose value is a home-SESSION enablement decision rather than durable
# local configuration. They are inherited at the launch convergence point, where
Expand Down
Loading