Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .no-mistakes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# no-mistakes review/fix/document/test/lint/pr/rebase/ci agent never adopts that
# identity or drives the fleet. Trusted-only: a pushed branch cannot turn this off,
# so it is honored only from the default-branch copy of this file. Layered above
# the NO_MISTAKES_GATE lifecycle refusal (bin/fm-gate-refuse-lib.sh) and the
# gate-context lifecycle boundary (bin/fm-gate-refuse-lib.sh) and the
# HEAD-continuity guard; see docs/architecture.md "No-mistakes gate authority boundary."
disable_project_settings: true

Expand Down Expand Up @@ -40,6 +40,7 @@ test:
Run live Herdr scenarios only through bin/fm-herdr-lab.sh with a named non-default fm-lab-* session, following that helper's prepare, provision, run, and teardown contract exactly.
Never touch the live default Herdr session or fleet panes.
Prefer a throwaway lab for spawn, long-launch, and Claude-path proofs, and tear it down in the same evidence turn.
Lifecycle calls against a throwaway firstmate home proceed inside the gate only when the home was minted by `bin/fm-lab-home.sh create <dir>` and driven as plain `FM_HOME=<dir>` with no FM_*_OVERRIDE relocations; every other home stays refused.
Do not mutate the operator primary checkout, real fleet FM_HOME state, or production credentials, and keep git changes otherwise inside the run worktree.
Read docs/herdr-backend.md and the bin/fm-herdr-lab.sh header as the owners of Herdr lab mechanics rather than reproducing that manual here.
Ship or scout briefs that will drive Herdr lifecycle still require --herdr-lab at scaffold time; these Test-agent instructions are not a substitute for that brief flag.
Expand Down
95 changes: 75 additions & 20 deletions bin/fm-gate-refuse-lib.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# fm-gate-refuse-lib.sh - fail-closed refusal that keeps a no-mistakes GATE agent
# out of firstmate's fleet lifecycle.
# fm-gate-refuse-lib.sh - refuse no-mistakes gate lifecycle calls against the
# real fleet while allowing marked disposable lab homes.
#
# The hazard (data/nm-gate-ambient-authority-containment-c3/report.md): a
# no-mistakes gate agent runs inside a firstmate checkout with a free shell, so
Expand All @@ -11,34 +11,48 @@
#
# no-mistakes owns the authority-removal half (it neutralizes the project
# instructions and stamps NO_MISTAKES_GATE into the gate agent's environment).
# THIS is the firstmate capability-removal half: an enforceable script refusal,
# not a prose rule the neutralized agent would never read. It is sourced at the
# top of the three fleet-lifecycle entrypoints and called before any fleet
# mutation, so a gate agent that still reaches for the fleet is stopped cold.
# THIS is the firstmate capability boundary: an enforceable script check,
# not a prose rule the neutralized agent would never read. It is sourced by the
# four fleet-lifecycle entrypoints and called before their fleet mutation, so
# a gate agent that reaches for the real fleet is stopped cold.
#
# Two independent signals, either of which refuses (fail closed):
# Two independent gate-context signals, either of which triggers the check:
#
# 1. NO_MISTAKES_GATE set - the durable env marker no-mistakes stamps into every
# gate agent. This is the primary signal and covers a relocated NM_HOME.
# 2. The current worktree's git-common-dir resolves under a no-mistakes gate
# repo (.../.no-mistakes/repos/*.git) - the UNSPOOFABLE backstop. It derives
# from the checkout's real filesystem location, which the agent cannot
# relocate without breaking the gate's own git operations, so it still
# refuses even if the agent tampered NO_MISTAKES_GATE away. Its limit: the
# detects a gate even if the agent tampered NO_MISTAKES_GATE away. Its limit: the
# literal-path match only fires for the default NM_HOME (~/.no-mistakes); a
# relocated NM_HOME is covered by signal 1.
#
# A NORMAL firstmate session - a real primary checkout, a real treehouse/Orca
# crew worktree - has NEITHER signal and is COMPLETELY unaffected: the function
# returns 0 and the lifecycle proceeds exactly as before.
#
# This mirrors the unspoofable-marker precedent in bin/fm-marker-lib.sh: a signal
# the agent cannot forge, keyed on at a chokepoint, keeping the pattern familiar
# to firstmate maintainers. It layers ABOVE no-mistakes' separately-shipping
# HEAD-continuity guard, which remains the adversarial/residual backstop.
# THE ONE AUTHORIZED EXCEPTION - a disposable lab home: a gate agent may drive
# lifecycle against an FM_HOME that carries the FM_GATE_LAB_MARKER file, because
# bin/fm-lab-home.sh stamps it only on an empty directory
# (fm_gate_lab_mark refuses a populated dir, so the helper cannot mark a real home).
# The allowance additionally requires every FM_*_OVERRIDE to be empty or unset,
# so the lab call uses the marked home's stock layout and no override can split
# part of the "lab" back onto the real fleet. The threat model stays a CONFUSED
# agent: a hostile agent that would hand-forge the marker file is the
# adversarial case no-mistakes' neutral-execution-context and the
# HEAD-continuity guard already own, so the check is a plain token file, not a
# bound record. This is an allowance on the CAPABILITY side only:
# fm_is_gate_agent still reports the gate context, so the sessionstart
# stand-downs that read it directly are unaffected by the marker.
#
# The gate-context backstop mirrors the unspoofable-marker precedent in
# bin/fm-marker-lib.sh; the lab-home marker is deliberately not unspoofable.
# This boundary layers above no-mistakes' separately-shipping HEAD-continuity
# guard, which remains the adversarial/residual backstop.
#
# TEST-HARNESS ESCAPE HATCH (FM_GATE_REFUSE_BYPASS=1): firstmate's own test suite
# must exercise the REAL fm-spawn/fm-send/fm-teardown, but the no-mistakes gate
# must exercise the real fleet entrypoints, but the no-mistakes gate
# runs that suite FROM a gate worktree (cwd git-common-dir under
# .no-mistakes/repos/*.git, and possibly NO_MISTAKES_GATE set) - the exact
# environment this guard refuses. So both signals would fire during firstmate's
Expand All @@ -53,16 +67,52 @@
# neutral-execution-context and the HEAD-continuity guard. The dedicated
# tests/fm-gate-refuse.test.sh strips the bypass so it still verifies real refusal.
#
# Sourced by bin/fm-spawn.sh, bin/fm-send.sh, bin/fm-teardown.sh,
# bin/fm-sessionstart-nudge.sh, and the tests.
# Sourced by the fleet lifecycle entrypoints, session-start hooks,
# bin/fm-lab-home.sh, and the tests.
# No side effects on source. set -u / set -e safe. The refusal is a hard exit,
# not a return, because there is no safe way to continue a fleet mutation from a
# gate context.
# not a return, because an unpermitted gate call cannot safely mutate the fleet.

# The exit code every refusal uses, distinct enough to recognize in a caller or
# test as "the gate refusal fired" rather than an ordinary usage error.
FM_GATE_REFUSE_EXIT=3

# The disposable-lab-home marker file and the token line it must carry. The
# format is owned here; bin/fm-lab-home.sh is the supported writer.
FM_GATE_LAB_MARKER='.fm-lab-home'
FM_GATE_LAB_TOKEN='fm-lab-home v1'

# fm_gate_lab_home <dir>: return 0 when <dir> is a marked disposable lab home.
fm_gate_lab_home() {
local home=${1:-}
[ -n "$home" ] || return 1
[ -f "$home/$FM_GATE_LAB_MARKER" ] || return 1
[ "$(sed -n '1p' "$home/$FM_GATE_LAB_MARKER" 2>/dev/null || true)" = "$FM_GATE_LAB_TOKEN" ]
}

# fm_gate_lab_mark <dir>: stamp <dir> as a disposable lab home. Fails closed on
# any dir that is not empty, so this can never mark a populated real home.
fm_gate_lab_mark() {
local home=${1:-} listing
[ -n "$home" ] && [ -d "$home" ] || return 1
listing=$(find "$home" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null) || return 1
[ -z "$listing" ] || return 1
printf '%s\n' "$FM_GATE_LAB_TOKEN" > "$home/$FM_GATE_LAB_MARKER"
}

# fm_gate_lab_permitted: return 0 when the current call targets a marked lab
# home through a stock layout - $FM_HOME carries the marker and no
# FM_*_OVERRIDE relocation has a nonempty value.
fm_gate_lab_permitted() {
local v
fm_gate_lab_home "${FM_HOME:-}" || return 1
for v in "${!FM_@}"; do
case "$v" in
*_OVERRIDE) [ -z "${!v}" ] || return 1 ;;
esac
done
return 0
}

# fm_is_gate_agent: return 0 without output when this process looks like a
# no-mistakes gate agent. An optional root anchors the git-common-dir check;
# callers that omit it retain the historical current-worktree behavior.
Expand All @@ -88,11 +138,16 @@ fm_is_gate_agent() {
}

# fm_refuse_if_gate_agent: exit FM_GATE_REFUSE_EXIT with a clear stderr message if
# this process looks like a no-mistakes gate agent. Call before any fleet
# mutation. No-ops (returns 0) for a normal firstmate session, or when firstmate's
# own test harness sets FM_GATE_REFUSE_BYPASS=1 (see the header).
# this process looks like a no-mistakes gate agent without a permitted lab home.
# Call before any fleet mutation. No-ops (returns 0) for a normal firstmate
# session, a permitted lab home, or when firstmate's own test harness sets
# FM_GATE_REFUSE_BYPASS=1 (see the header).
fm_refuse_if_gate_agent() {
fm_is_gate_agent "${1:-.}" || return 0
if fm_gate_lab_permitted; then
echo "fm-gate-refuse: gate agent lifecycle permitted only against lab home $FM_HOME" >&2
return 0
fi
if [ "$FM_GATE_REFUSE_REASON" = env ]; then
echo "error: no-mistakes gate agent must not drive the fleet (NO_MISTAKES_GATE set)" >&2
else
Expand Down
47 changes: 47 additions & 0 deletions bin/fm-lab-home.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# fm-lab-home.sh - mint a disposable firstmate "lab" home.
#
# A lab home is a throwaway FM_HOME that a no-mistakes GATE agent may drive
# through the fleet lifecycle entrypoints: bin/fm-gate-refuse-lib.sh refuses
# those calls inside a gate agent unless FM_HOME carries the marker file this
# helper writes (the lib owns the marker format and authorization decision;
# this script is the supported writer).
#
# Usage:
# fm-lab-home.sh create <dir> make <dir> a marked lab home and print it;
# refused on any existing non-empty dir
#
# A lab home is the stock layout only - state/, data/, config/, projects/ - and
# callers remove it with ordinary rm -rf when done. Drive it with plain
# FM_HOME=<dir>; any FM_*_OVERRIDE relocation defeats the allowance.
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=bin/fm-gate-refuse-lib.sh
. "$SCRIPT_DIR/fm-gate-refuse-lib.sh"

fm_lab_home_error() {
echo "fm-lab-home: $*" >&2
}

case "${1:-}" in
create)
dir=${2:-}
[ -n "$dir" ] || { fm_lab_home_error "create requires a directory path"; exit 2; }
if [ -e "$dir" ] && [ ! -d "$dir" ]; then
fm_lab_home_error "refusing '$dir': exists and is not a directory"
exit 1
fi
mkdir -p "$dir" || exit 1
fm_gate_lab_mark "$dir" || {
fm_lab_home_error "refusing '$dir': a lab marker is only ever stamped on a fresh empty dir"
exit 1
}
mkdir -p "$dir/state" "$dir/data" "$dir/config" "$dir/projects" || exit 1
printf '%s\n' "$dir"
;;
*)
fm_lab_home_error "usage: fm-lab-home.sh create <dir>"
exit 2
;;
esac
6 changes: 5 additions & 1 deletion bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2295,7 +2295,11 @@ require_exclusive_worktree_slot_record() {
for state_dir in "${TREEHOUSE_OWNER_STATES[@]}"; do
for other in "$state_dir"/*.meta; do
[ -f "$other" ] && [ ! -L "$other" ] || continue
[ "$other" != "$record_meta" ] || continue
# Identity, not spelling: the same record reached through a differently
# resolved state dir (e.g. a symlinked $FM_HOME) is still this record. A
# differently named hardlink is another task's record, so the name must
# match too.
[ "${other##*/}" = "${record_meta##*/}" ] && [ "$other" -ef "$record_meta" ] && continue
other_id=$(basename "$other" .meta)
for field in worktree home; do
other_path=$(fm_meta_get "$other" "$field")
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,9 @@ Placement is proven only at launch, so `bin/fm-spawn.sh` also exports the task i

Firstmate's own no-mistakes gate runs agents inside a checkout that also contains the fleet-captain identity in `AGENTS.md`, so gate execution needs an authority boundary separate from ordinary crewmate worktree isolation.
The tracked `.no-mistakes.yaml` sets `disable_project_settings: true`; no-mistakes honors that setting only from the trusted default-branch copy, so a pushed branch cannot enable its own project instructions during validation.
Independently, `fm-spawn.sh`, `fm-send.sh`, `fm-control.sh`, and `fm-teardown.sh` source `bin/fm-gate-refuse-lib.sh` and exit with status 3 before fleet mutation when the gate environment marker is present or the current checkout matches the default no-mistakes gate-repository topology.
A normal primary checkout or crewmate worktree has neither signal and remains unaffected.
The helper's header owns the exact signal detection, relocated-home limitation, test-harness bypass, and relationship to no-mistakes' HEAD-continuity guard.
Independently, the fleet lifecycle entrypoints use `bin/fm-gate-refuse-lib.sh` to refuse gate calls against the real fleet, while permitting validation against a disposable lab home minted by `bin/fm-lab-home.sh`.
A normal primary checkout or crewmate worktree remains unaffected.
The refusal library's header owns the gate detection, lab-home exception, test-harness bypass, and relationship to no-mistakes' HEAD-continuity guard; the lab helper's header owns its usage.

## Two task shapes

Expand Down
5 changes: 3 additions & 2 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The first mate drives these; interactive entrypoints work by hand too, while `*-lib.sh` files are sourced helpers.
Each row is one purpose clause only: the script's own header comment is the authoritative description of its behavior, flags, and contracts, so read the header before first use.
If you have changed away from the firstmate home in an interactive shell, invoke these scripts by absolute path through the repo's `bin/` directory; the scripts self-locate internally after they start.
The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarized in [architecture.md](architecture.md#no-mistakes-gate-authority-boundary), while `docs/sessionstart-nudge.md` covers the silent session-open hook use; `fm-gate-refuse-lib.sh`'s header owns its exact contract.
The shared no-mistakes gate lifecycle boundary is summarized in [architecture.md](architecture.md#no-mistakes-gate-authority-boundary), while `docs/sessionstart-nudge.md` covers the silent session-open hook use; `fm-gate-refuse-lib.sh`'s header owns its exact contract.

| Script | Purpose |
| ------------------------ | ------------------------------------------------------------------------------------ |
Expand Down Expand Up @@ -38,6 +38,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-brief-heading-lib.sh` | Single owner of reading a brief's sections, shared by the `--intent` contract, spawn and promotion validation, and `fm-dispatch-resolve.sh` |
| `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session |
| `fm-herdr-lab-viewer.py` | The pty engine behind `fm-herdr-lab.sh viewer`: one real foreground Herdr client on a non-zero window grid |
| `fm-lab-home.sh` | Mint a disposable lab home for gate lifecycle validation |
| `fm-install-herdr.sh` | Install CI's exact-version Herdr pin with official asset URL, SHA-256, and protocol checks |
| `fm-install-treehouse.sh`| Install CI's exact-version Treehouse pin for real-Herdr E2E that needs spawn worktrees |
| `fm-herdr-ci-cleanup.sh` | Snapshot and tear down only job-owned `fm-lab-*` sessions in the Herdr CI lane |
Expand Down Expand Up @@ -85,7 +86,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-procevent-remote-reply.sh` | Relay the remote-secondmate status stream through non-destructive process-event deltas |
| `fm-procevent-quota.sh` | Wake Firstmate when tracked quota drops below a threshold, is exhausted, or cannot be polled |
| `fm-procevent-when.sh` | Fire a trust-bound deterministic action at most once when its registered condition holds, then wake with the outcome |
| `fm-gate-refuse-lib.sh` | Shared no-mistakes gate-context refusal for fleet lifecycle entrypoints |
| `fm-gate-refuse-lib.sh` | Shared gate-context lifecycle boundary for real and lab homes |
| `fm-watch-arm.sh` | Verified home-scoped watcher arm wrapper with loud cycle endings and bounded lifecycle ledger |
| `fm-watch-checkpoint.sh` | Run one bounded foreground watcher checkpoint for Codex-style supervision |
| `fm-watch.sh` | Singleton-safe watcher: absorb benign wakes, detect stalled local-secondmate wake queues, and exit on actionable ones |
Expand Down
Loading
Loading