Skip to content
Merged
9 changes: 8 additions & 1 deletion bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3535,7 +3535,14 @@ fm_backend_herdr_wait_transition() { # <session> <timeout_secs> <state_dir> <pa
fm_backend_herdr_control_exec rm -rf "$fifo_dir" 2>/dev/null || true
return 2
fi
fm_backend_herdr_scrubbed_exec "${reader[@]}" "$sock" "$timeout" "${pane_ids[@]}" > "$fifo" 2>/dev/null &
(
export LD_PRELOAD='' LD_LIBRARY_PATH='' LD_AUDIT='' LD_DEBUG=''
export DYLD_INSERT_LIBRARIES='' DYLD_LIBRARY_PATH='' DYLD_FRAMEWORK_PATH=''
export DYLD_FALLBACK_LIBRARY_PATH='' DYLD_FALLBACK_FRAMEWORK_PATH=''
export PERL5OPT='' PERL5LIB='' PERLLIB='' NODE_OPTIONS='' NODE_PATH=''
export PYTHONHOME='' PYTHONPATH='' RUBYOPT='' RUBYLIB='' BASH_ENV='' ENV='' GCONV_PATH=''
exec "${reader[@]}" "$sock" "$timeout" "${pane_ids[@]}"
) > "$fifo" 2>/dev/null &
reader_pid=$!
if ! exec 9< "$fifo"; then
kill "$reader_pid" 2>/dev/null || true
Expand Down
30 changes: 24 additions & 6 deletions bin/fm-herdr-lab.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
# loudly on a stopped or unreadable default.
# Provision records the running default session, its workspace/tab/pane
# topology, and its agent identities as a fleet-state tripwire. Teardown
# requires that record to be identical afterward.
# refuses default-session identity drift and topology drift attributable to the
# named lab, while auditing unrelated same-operator fleet churn.
# FM_HERDR_LAB_PROVISION_TIMEOUT_SECONDS is a whole number from 1 through 600.
# It defaults to 120 so a loaded fleet gets a fair but bounded startup window.
set -u
Expand Down Expand Up @@ -401,20 +402,37 @@ fm_herdr_lab_provision() { # <session>
}

fm_herdr_lab_check_tripwire() { # <session>
local name=$1 tripwire before after
local name=$1 tripwire before after verdict
tripwire=$(fm_herdr_lab_tripwire_path "$name")
[ -f "$tripwire" ] || {
fm_herdr_lab_error "missing fleet-state tripwire for '$name'; refusing unverified teardown"
return 1
}
before=$(cat "$tripwire")
after=$(fm_herdr_lab_fleet_state "$name") || return 1
[ "$before" = "$after" ] || {
fm_herdr_lab_error "FLEET-STATE TRIPWIRE FAILED: default session changed during lab work"
fm_herdr_lab_error "before: $before"
fm_herdr_lab_error "after: $after"
[ "$before" = "$after" ] && return 0
verdict=$(jq -nrce --argjson before "$before" --argjson after "$after" --arg name "$name" '
def topology: [.workspaces, .tabs, .panes, .agents];
if $before.session != $after.session then "identity"
elif (($before | topology | tojson | contains($name)) or
($after | topology | tojson | contains($name))) then "attributed"
else "external"
end
' 2>/dev/null) || {
fm_herdr_lab_error "FLEET-STATE TRIPWIRE FAILED: cannot classify default-session drift"
return 1
}
case "$verdict" in
external)
fm_herdr_lab_error "fleet-state audit: unrelated default-session topology changed during lab work; guarded teardown may continue"
;;
*)
fm_herdr_lab_error "FLEET-STATE TRIPWIRE FAILED: default-session drift is attributable to the lab or changed its identity"
fm_herdr_lab_error "before: $before"
fm_herdr_lab_error "after: $after"
return 1
;;
esac
}

fm_herdr_lab_verify_tripwire() { # <session>
Expand Down
190 changes: 154 additions & 36 deletions bin/fm-provision-lib.sh

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ A lane launched into that state cannot run the project's own tests, formatters,
This section records the operator-facing behavior only.

Detection is driven by what the worktree declares, never by a project name.
A directory holding `uv.lock` provisions with `uv sync --frozen`; one holding `requirements.txt` gets a `uv venv` virtual environment plus its `requirements.txt` and any conventional `requirements-dev.txt` / `requirements-test.txt` companions; `package-lock.json` runs `npm ci`; `pnpm-lock.yaml` runs `pnpm install --frozen-lockfile`.
A `requirements.txt` without Python project metadata is treated as a dependency list: provisioning creates a `uv venv` environment and installs the listed requirements without installing the directory as a package.
A standalone `uv.lock` is handled the same way only when its root dependency set is unambiguous; otherwise it records a capability gap and launches unprovisioned.
JavaScript lockfiles require a `package.json`; a lockfile-only directory records a capability gap instead of being passed to a package manager.
Python always goes through uv, never pip or venv directly.
A directory whose `pyproject.toml` declares a project - a `[project]`, `[build-system]`, or `[tool.poetry]` table - with neither of those two Python manifests is enumerated and reported as a capability gap rather than installed from a guess; a uv workspace member is excused, because its root's `uv sync --all-packages` already installs it, and a `pyproject.toml` holding only tool configuration (`[tool.ruff]`, `[tool.black]`, `[tool.pytest.ini_options]`) declares nothing to provision and is a clean no-op.
A pip component's fingerprint covers the requirements files it reaches through `-r` / `-c` includes as well as the ones named directly, so editing an included file is a cache miss rather than a false hit.
Expand Down
4 changes: 4 additions & 0 deletions tests/fm-arm-pretool-check.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ set -u
CHECK="$ROOT/bin/fm-arm-pretool-check.sh"
POLICY="$ROOT/bin/fm-arm-command-policy.mjs"

# The executable transport resolves approved absolute setup paths against the
# active firstmate home. Keep that input owned by this fixture, not the caller.
export FM_HOME="$ROOT"

# --- full cross-harness acceptance matrix ----------------------------------

MATRIX_IDS=()
Expand Down
9 changes: 7 additions & 2 deletions tests/fm-azure-runner.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ shared_allocator_bridge_unit() {
python3 - "$HOST" <<'PY' || fail "shared allocator runner bridge failed"
import importlib.util, json, pathlib, subprocess, tempfile, sys
spec=importlib.util.spec_from_file_location("runner_shared",sys.argv[1]); m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
fixture_home=pathlib.Path(tempfile.mkdtemp())
env={"subscription":"sub","resource_group":"rg","prefix":"prefix","budget_limit":1500,"state_dir":pathlib.Path(tempfile.mkdtemp()),"azure_operation_count":0}
limits={**m.RESOURCE_CLASSES["behavior-heavy"],"sku":"Standard_D4as_v7","sku_family":"StandardDasv7Family"}
state={"schema":m.SCHEMA,"invocation":"azr-aaaaaaaaaaaa","resources":{},"request":{"fence":"sha256:"+"a"*64,"resource_class":"behavior-heavy","limits":limits}}
Expand All @@ -568,10 +569,12 @@ calls=[]
def completed(value):
return subprocess.CompletedProcess(["python"],0,stdout=json.dumps(value),stderr="")
def allocator_run(command,**kwargs):
assert kwargs["env"]["FM_HOME"]==str(m.ROOT)
assert kwargs["env"]["FM_AZURE_WORKER_STATE_DIR"]==str(m.ROOT/"state"/"azure-workers")
assert kwargs["env"]["FM_HOME"]==str(fixture_home)
assert kwargs["env"]["FM_AZURE_WORKER_STATE_DIR"]==str((fixture_home/"state"/"azure-workers").resolve())
calls.append(command)
return completed({"reservation_id":"azr-aaaaaaaaaaaa","status":"reserved","reason":"","actual_usd":100.0,"forecast_usd":200.0,"admission_limit_usd":1500.0})
old_home=m.os.environ.get("FM_HOME")
m.os.environ["FM_HOME"]=str(fixture_home)
m.run=allocator_run
cost={"max_increment":25.0}
result=m.shared_capacity_reserve(env,state,cost)
Expand All @@ -594,6 +597,8 @@ state["shared_capacity_reservation"]={"status":"reserved"}
m.shared_capacity_release(env,state)
assert "capacity-release" in calls[-1] and len(calls[-1][calls[-1].index("--cleanup-receipt")+1])==64
assert state["shared_capacity_reservation"]["status"]=="released"
if old_home is None:m.os.environ.pop("FM_HOME",None)
else:m.os.environ["FM_HOME"]=old_home
PY
pass "runner queues behind the shared allocator and requires actual/forecast evidence before compute"
}
Expand Down
3 changes: 3 additions & 0 deletions tests/fm-backend.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ fm_git_identity fmtest fmtest@example.invalid
. "$ROOT/bin/fm-backend.sh"

fm_test_tmproot_into TMP_ROOT fm-backend-tests
FM_HOME="$TMP_ROOT/fm-home"
export FM_HOME
mkdir -p "$FM_HOME"

# fm_backend_detect's cmux fallback (bundle id + process ancestry,
# docs/cmux-backend.md "Runtime auto-detection") consults uname, lsappinfo,
Expand Down
29 changes: 22 additions & 7 deletions tests/fm-herdr-lab.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -258,17 +258,31 @@ test_changed_default_trips_after_teardown() {
pass "fm-herdr-lab: changed default fleet state is a hard failure"
}

test_changed_default_fleet_members_trip_after_teardown() {
local name="fm-lab-tripwire-members-$$" status=0
test_external_default_fleet_churn_is_audited() {
local name="fm-lab-tripwire-members-$$" output
: > "$FAKE_LOG"
run_with_fake fm_herdr_lab_provision "$name" || fail "fleet-member tripwire fixture provision failed"
printf '%s\n' captain > "$FAKE_STATE/default-agents"
output=$(run_with_fake fm_herdr_lab_teardown "$name" 2>&1) \
|| fail "unrelated default fleet churn blocked guarded teardown"
printf '%s\n' "$output" | grep -F "fleet-state audit: unrelated default-session topology changed" >/dev/null \
|| fail "unrelated default fleet churn was not audited"
assert_absent "$TRIPWIRES/$name.fleet-state.json" "audited external churn retained owned lab state"
printf '%s\n' captain crewmate-1 crewmate-2 > "$FAKE_STATE/default-agents"
pass "fm-herdr-lab: unrelated same-operator fleet churn is audited without blocking teardown"
}

test_lab_attributed_default_fleet_drift_trips() {
local name="fm-lab-attributed-drift-$$" status=0
: > "$FAKE_LOG"
run_with_fake fm_herdr_lab_provision "$name" || fail "attributed-drift fixture provision failed"
printf '%s\n' captain crewmate-1 crewmate-2 "$name" > "$FAKE_STATE/default-agents"
run_with_fake fm_herdr_lab_teardown "$name" >/dev/null 2>&1 || status=$?
expect_code 1 "$status" "lost default fleet members must fail teardown"
assert_present "$TRIPWIRES/$name.fleet-state.json" "fleet-member tripwire failure should retain evidence"
expect_code 1 "$status" "lab-attributed default fleet drift must refuse teardown"
assert_present "$TRIPWIRES/$name.fleet-state.json" "attributed fleet drift discarded ownership evidence"
printf '%s\n' captain crewmate-1 crewmate-2 > "$FAKE_STATE/default-agents"
rm -f "$TRIPWIRES/$name.fleet-state.json"
pass "fm-herdr-lab: default pane and agent deaths trip the fleet-state guard"
run_with_fake fm_herdr_lab_teardown "$name" || fail "attributed-drift cleanup failed after default fleet repair"
pass "fm-herdr-lab: lab-attributed default fleet drift retains teardown refusal"
}

test_default_runtime_activity_does_not_trip() {
Expand Down Expand Up @@ -389,7 +403,8 @@ test_provision_run_and_guarded_teardown
test_missing_tripwire_blocks_destruction
test_agent_argv_inserts_session_before_separator
test_changed_default_trips_after_teardown
test_changed_default_fleet_members_trip_after_teardown
test_external_default_fleet_churn_is_audited
test_lab_attributed_default_fleet_drift_trips
test_default_runtime_activity_does_not_trip
test_stopped_default_refuses_provision
test_malformed_default_running_refuses_provision
Expand Down
2 changes: 1 addition & 1 deletion tests/fm-session-start.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ SH
run_session_start() {
local home=$1 root=$2 path=$3
env -u CLAUDECODE -u PI_CODING_AGENT -u GROK_AGENT \
FM_HOME="$home" FM_ROOT_OVERRIDE="$root" PATH="$path" \
FM_HOME="$home" FM_ROOT_OVERRIDE="$root" FM_CONFIG_OVERRIDE="$home/config" PATH="$path" \
FM_BACKEND_HERDR_TEST_LAB=firstmate-herdr-test-lab-v1 \
"$SESSION_START"
}
Expand Down
Loading
Loading