Skip to content

feat: integrate Docker Sandbox placement and Crabbox execution - #1

Open
chsong1 wants to merge 7 commits into
mainfrom
fm/docker-sandbox-crabbox
Open

feat: integrate Docker Sandbox placement and Crabbox execution#1
chsong1 wants to merge 7 commits into
mainfrom
fm/docker-sandbox-crabbox

Conversation

@chsong1

@chsong1 chsong1 commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Intent

Integrate Docker Sandboxes and Crabbox into the Firstmate harness as two independent, opt-in modules while preserving current host/direct/local defaults and all existing worker-runtime backends. Docker Sandbox owns workspace placement for ordinary workers and persistent secondmates, supports direct or clone workspace modes plus optional Docker Kits and official agent presets, keeps canonical task metadata and private bridge cursors on the host, synchronizes only bounded status/turn-end deltas, reuses exact recorded placements on relaunch, and releases only exact recorded placements on cleanup. Crabbox owns task-scoped command execution only: explicit configured profile or recorded lease routing, argv-preserving execution without eval, provider snapshot validation, run/lease/cost/expiry persistence, bounded logs, explicit release, and no role as a worker or secondmate carrier. Configuration is a strict optional inherited JSON file; missing optional tools stay silent unless selected, then bootstrap emits one actionable manual-install diagnostic. Preserve existing firstmate lifecycle, no-mistakes, trace-context, remote-secondmate, and host behavior. Add observable fake-tool behavioral coverage for placement, bridge safety, command execution, changed-test selection, and secondmate persistence/recovery. Do not install Docker Sandbox or Crabbox; their binaries are absent, so live provider smoke is not part of this change.

What Changed

  • Added strict optional inherited workspace/execution configuration and per-spawn overrides, preserving host/direct/local defaults while wiring Docker Sandbox placement and kits independently of runtime session backends.
  • Added task-scoped local/Crabbox execution with explicit profile-or-lease routing, argv-preserving runs, private journals, validated provider snapshots, and exact release handling.
  • Added host-private sandbox bridges, bounded status/turn-end sync, exact relaunch/teardown ownership, transactional backend cleanup, and fake-tool coverage for changed-test selection and secondmate recovery.

Risk Assessment

✅ Low: The reviewed delta contains no additional substantiated source risk beyond prior-round decisions.

Testing

Focused fake-tool, lifecycle, backend-preservation, changed-test-selection, bridge-safety, placement, and command-execution checks passed after serial confirmation; reviewer-visible CLI evidence was captured, the worktree remained clean, and live Docker Sandbox/Crabbox smoke was intentionally not attempted because their binaries are absent by design.

Evidence: Sandbox secondmate lifecycle transcript
ok - seed: registry scope+projects, charter copied, clones+origins, no-mistakes init in subhome only
warning: secondmate design sync skipped before launch: primary default-branch commit cannot be resolved
ok - spawn: launches in the subhome with persistent charter, records routing meta
ok - send: a bare fm-<id> secondmate routes to the meta window with the from-firstmate marker
ok - handoff: in-scope items move verbatim, out-of-scope stays, idempotent
ok - recovery: respawns from the durable registry and persistent home
ok - teardown: removes the home, then clears meta and the registry route
ok - sandbox seed: persistent secondmate home fixture is available
warning: secondmate sandbox sync skipped before launch: primary default-branch commit cannot be resolved
ok - sandbox spawn: exact home+bridge mounts, bridge metadata, and persistent charter launch
ok - sandbox sync: watcher bridge sync publishes canonical status and one-shot turn-end
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
warning: secondmate sandbox sync skipped before launch: primary default-branch commit cannot be resolved
ok - sandbox relaunch: exact recorded sandbox and bridge are reused without creation
ok - sandbox teardown: release is exact, bridge is removed after endpoint closure, home safeguards remain
Evidence: Crabbox command execution transcript
ok - command execution adapters and task-scoped Crabbox behavior
- Outcome: ⚠️ 2 warnings across 1 run (47m32s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 8 issues found → auto-fixed (30) ✅
  • 🚨 bin/fm-workspace-execution-config.sh:92 - The intent requires “supports direct or clone workspace modes,” but configuration accepts only workspaceMode=direct for workers and secondmates, while fresh Docker placements hard-code direct. The adapter’s clone implementation is unreachable through the supported lifecycle.
  • 🚨 bin/fm-teardown.sh:2317 - The intent requires “releases only exact recorded placements on cleanup,” but forced parent cleanup deletes child metadata and homes without releasing recorded Docker placements or bridges. A Docker-placed child can therefore leak its sandbox.
  • 🚨 bin/executors/crabbox.sh:43 - Profile execution invokes crabbox run --job, but the documented Crabbox interface uses --profile &lt;name&gt;. Selected profile execution will fail before running the command. See the Crabbox run command.
  • 🚨 bin/fm-exec.sh:334 - The intent requires “provider snapshot validation, run/lease/cost/expiry persistence,” but the adapter discards provider lifecycle output and fm-exec records only local run metadata; snapshots are validated only as arbitrary JSON objects. Provider runs therefore cannot be durably reconciled or released.
  • 🚨 bin/executors/crabbox.sh:60 - The intent requires “bounded logs,” but Crabbox logs are streamed without any byte or line cap. A noisy provider can make fm-exec logs emit unbounded output.
  • 🚨 bin/fm-spawn.sh:2318 - The intent requires preserving the existing firstmate lifecycle, but ordinary Docker workers skip busy-state arming. Their bridge turn-end hooks do not provide a busy verdict, so stable active turns can be classified as missing/unknown and enter the watcher’s stale path.
  • 🚨 bin/executors/crabbox.sh:42 - Crabbox is passed the task worktree but ignores it, invoking the provider from the caller’s ambient directory. Since Crabbox syncs the current checkout before execution, a task can run against the wrong repository. See the Crabbox run command.
  • ⚠️ bin/fm-sandbox-bridge-lib.sh:307 - Bridge status append and cursor advancement are separate operations. If append succeeds but cursor persistence fails, retrying re-appends the same delta and can duplicate wakes; use a durable transactional or idempotent boundary.

🔧 Fix: Correct Crabbox profile routing and make bridge sync retry-idempotent
7 issues (4 errors, 3 warnings) still open:

  • 🚨 bin/fm-spawn.sh:321 - The required criteria preserve trace-context and remote-secondmate, but the parser no longer consumes --traceparent; remote and local launches treat it as a positional argument and reject the spawn. Restore explicit option handling before positional validation.
  • 🚨 bin/fm-spawn.sh:1147 - Selecting Crabbox makes every fresh worker/secondmate spawn fail: this path unconditionally calls fm_command_execution_prepare, while Crabbox's prepare hook deliberately returns 1 because it has no prewarm operation. Crabbox is required to be task-scoped command execution only, so preparation must not block carrier creation.
  • ⚠️ bin/fm-workspace-execution-config.sh:94 - The strict JSON validator accepts kits: null and the loader silently converts it to [], despite kits being required to be an array. Reject explicit null values instead of silently discarding malformed configuration.
  • ⚠️ bin/fm-workspace-execution-config.sh:64 - jq -e . accepts multiple top-level JSON values, and the subsequent schema filter validates them independently. The “strict optional inherited JSON file” can therefore pass validation and fail later ambiguously; require exactly one JSON document.
  • 🚨 bin/fm-spawn.sh:2711 - Profile values are checked only for nonempty strings. A configured or explicit profile containing newline, carriage return, or tab is written directly into canonical task metadata at this path, allowing metadata-field injection or corruption. Reject control whitespace before publication.
  • ⚠️ bin/fm-sandbox-bridge-lib.sh:112 - Bridge validation canonicalizes the parent roots and exact lexical path but never verifies the task bridge directory itself is a real, non-symlink directory. Replacing &lt;state&gt;/sandbox-bridge/&lt;task-id&gt; with a symlink can redirect binding/status/runtime reads and cleanup outside the canonical bridge root, violating private host cursors and canonical host binding.
  • 🚨 tests/fm-test-run.test.sh:96 - The changed-test fixture no longer creates tests/fm-backend.test.sh or tests/fm-ask-user-authority.test.sh, but later assertions still require both paths in event-wait, non-bin-source, and changed-source selection cases. The behavioral test cannot pass with the fixture as written; restore the fixtures or update the expected selection contract.

🔧 Fix: Restore missing changed-selection fixture scripts
7 issues (4 errors, 3 warnings) still open:

  • 🚨 bin/fm-spawn.sh:2286 - The required behavior is “synchronizes only bounded status/turn-end deltas.” Fresh Docker spawns switch to the bridge copy of the brief at this line, but the generated brief still tells agents to append to the absolute host path $STATE/$ID.status (bin/fm-brief.sh:179,434). Only the worktree and bridge are mounted, so Docker workers and secondmates cannot publish status through bridge/status; watcher synchronization has nothing to consume.
  • 🚨 bin/fm-spawn.sh:2282 - The intent requires “canonical task metadata and private bridge cursors on the host” while crossing only bounded deltas. The bridge is mounted as an ordinary extra workspace here; Docker mounts extra workspaces read-write unless :ro is used (workspace mounting docs). A sandbox can therefore rewrite runtime-brief.md or fm-operational-input.sh, and relaunch trusts the modified brief while validation checks only regular-file safety and binding. Keep immutable bridge inputs read-only or outside the writable mount.
  • 🚨 bin/placements/docker-sandbox.sh:235 - The required criterion is “releases only exact recorded placements on cleanup,” but normal teardown invokes this release path without force. It unconditionally stops the verified sandbox and then removes it without --force; Docker documents that sbx rm requires confirmation and --force for non-interactive scripts or in-use sandboxes (sbx rm docs), while sbx stop only stops running sandboxes (sbx stop docs). A stopped or attached placement can therefore make teardown prompt or fail and leave resources behind; even forced teardown can fail before reaching forced removal if stop errors.
  • 🚨 bin/fm-teardown.sh:2628 - Crabbox is required to support “explicit release” and “run/lease/cost/expiry persistence.” fm-exec records explicit leases in state/&lt;id&gt;.execution, but teardown neither checks for nor releases an unreleased lease; this cleanup removes the task metadata while leaving the execution journal and snapshot. After run --lease lease-a followed by teardown, fm-exec release can no longer run because metadata is gone, leaking the provider lease; reusing the task ID can also make the stale journal’s lease current for the new task. Refuse teardown while a lease is active or release that exact lease before erasing the task identity.
  • ⚠️ bin/fm-spawn.sh:2581 - Docker OpenCode spawns create .opencode/plugins/fm-sandbox-turnend.js here, but ordinary teardown removes only fm-turn-end.js at bin/fm-teardown.sh:2514. The new ignored hook can survive treehouse return and be loaded by a later task using the pooled worktree, producing stale turn-ended notifications. Remove this exact generated file in the corresponding teardown and abort-cleanup paths.
  • ⚠️ bin/fm-watch.sh:317 - watch_meta_value rejects duplicate or malformed placement fields, but the watcher silently continues at this line. A corrupted Docker task metadata file is skipped before watch_sync_sandbox_bridge can emit its existing actionable check wake, so bridge status and turn-end deltas stop syncing without diagnosis. Route malformed Docker candidates through the failure wake instead of dropping them.
  • ⚠️ bin/placements/docker-sandbox.sh:27 - The existing task-ID contract accepts underscores (bin/fm-pr-lib.sh:93-109), but the Docker placement adapter rejects them when deriving the sandbox name. A valid task such as foo_bar therefore cannot opt into Docker Sandbox placement for a worker or secondmate. Use a collision-safe name encoding or explicitly narrow the task-ID contract before accepting this placement.

🔧 Fix: Clean Docker OpenCode hooks and surface malformed metadata
3 issues (2 errors, 1 warning) still open:

  • 🚨 bin/placements/docker-sandbox.sh:205 - The required criteria are “reuses exact recorded placements on relaunch” and “releases only exact recorded placements on cleanup,” but inspect/release identify a sandbox only by deterministic name. If the original sandbox is deleted and another with the same name is created, relaunch adopts the replacement and teardown can remove it even if its workspace or bridge differs. Persist and verify an immutable provider identity, or fail closed when it cannot be proven.
  • 🚨 bin/fm-spawn.sh:2284 - Fresh Docker spawns create the backend endpoint/worktree before bridge and placement setup. If bridge creation fails, abort cleanup is not armed; if placement setup fails, cleanup still never removes the backend endpoint, and a partial provider placement can remain unrecorded because its handle is assigned only after preparation succeeds. Register resources at creation and unwind all prepublication failures.
  • ⚠️ bin/fm-spawn.sh:1131 - Explicit per-axis overrides retain incompatible dependent configuration. With a Crabbox default/profile, --executor local leaves the profile populated and is rejected; with Docker kits, --placement host leaves the kits populated and is rejected. An operator cannot opt one task back to host/local defaults without editing config. Clear dependent values on override or explicitly define and document this rejection contract.

🔧 Fix: Make fresh Docker spawn cleanup transactional
6 issues (4 errors, 2 warnings) still open:

  • 🚨 bin/placements/docker-sandbox.sh:159 - sbx create failure cleanup uses only the deterministic name before ownership is proven, so a race can remove another sandbox; if cleanup fails, no acquired handle is exported. The clone path also returns without cleanup. This contradicts “releases only exact recorded placements on cleanup.”
  • 🚨 bin/fm-spawn.sh:739 - Abort cleanup clears its bookkeeping before resource release and only warns on failure. A prepublication abort can leave an endpoint, worktree, bridge, or placement live with no metadata or retry record, violating “Preserve existing firstmate lifecycle.”
  • 🚨 bin/fm-spawn.sh:2092 - Endpoint cleanup is registered only after backend creation fully returns. Zellij, cmux, and Herdr can allocate an endpoint before pane/surface parsing or verification fails, leaving partial endpoints outside the new transaction.
  • 🚨 bin/fm-spawn.sh:2297 - treehouse get can acquire a worktree before settling or validation completes, but registration occurs afterward. Timeout or validation failure therefore leaves no exact handle for treehouse return, violating “Preserve existing firstmate lifecycle.”
  • ⚠️ bin/fm-spawn.sh:2337 - The bridge path is recorded only after bridge creation succeeds. If creation fails after partial bridge/cursor creation and its best-effort cleanup also fails, the abort trap has no path to retry cleanup, conflicting with host-private bridge ownership.
  • ⚠️ bin/fm-watch.sh:321 - Any nonempty placement value other than docker-sandbox is silently skipped as if it were host. placement=bogus therefore stops bridge synchronization without an actionable wake, violating “synchronizes only bounded status/turn-end deltas.”

🔧 Fix: Harden transactional cleanup and exact resource ownership
3 errors still open:

  • 🚨 bin/placements/docker-sandbox.sh:108 - The required criterion is “releases only exact recorded placements on cleanup.” Inventory JSON with an unsupported or scalar wrapper is normalized to an empty array at this line instead of rejected. inspect then treats an existing sandbox as absent, allowing teardown to delete task metadata without releasing the provider placement; fresh creation can likewise leave an unrecorded sandbox.
  • 🚨 bin/fm-spawn.sh:2083 - The required criterion is “Preserve existing firstmate lifecycle” with exact resources registered at acquisition. Backend creation can succeed and produce an exact endpoint, but if acquisition-record persistence fails, the caller ignores the failure (spawn_register_endpoint_from_file || true) and the durable record remains unusable. EXIT cleanup then has no endpoint handle and cannot retry cleanup, affecting tmux, zellij, cmux, and Herdr paths.
  • 🚨 bin/fm-spawn.sh:2486 - The required criterion is “Preserve existing firstmate lifecycle.” After treehouse get acquires a worktree, registration occurs only after two consecutive matching path observations. If settlement times out or keeps changing, the worktree remains acquired but no cleanup record is written, so the abort path never returns it and no durable retry record exists.

🔧 Fix: Harden inventory and preserve exact acquisition cleanup
7 issues (5 errors, 2 warnings) still open:

  • 🚨 bin/fm-spawn.sh:3079 - After fresh metadata is written, this marks publication complete, but the EXIT trap still runs prepublication cleanup until later flags are cleared. An interruption in that window can release the published task’s endpoint, worktree, bridge, or placement while its metadata remains live, violating “Preserve existing firstmate lifecycle.”
  • 🚨 bin/fm-spawn.sh:2552 - Every tentative pane cwd is registered as the cleanup handle. If treehouse acquires worktree B but a stale/foreign pane observation A arrives before settlement, abort cleanup can return A and leak B, violating “Preserve existing firstmate lifecycle.” Keep settlement observations separate from the exact acquired worktree handle.
  • 🚨 bin/backends/cmux.sh:379 - new-workspace can succeed while the follow-up workspace-list lookup fails. This returns without writing an acquisition record or emitting an exact ID, so spawn cleanup cannot remove the created cmux workspace, violating lifecycle preservation and exact endpoint ownership.
  • 🚨 bin/fm-sandbox-bridge-lib.sh:448 - Teardown retains metadata when this removal fails, but the sequential deletion can partially succeed. If the bridge is removed and cursor deletion fails, the next retry rejects the missing bridge during validation and can never finish cleanup, violating “releases only exact recorded placements on cleanup.”
  • 🚨 bin/placements/docker-sandbox.sh:130 - The inventory wrapper is validated, but malformed members are silently reduced to empty fields and skipped. A live recorded sandbox with a malformed entry can therefore appear absent to inspect, allowing teardown to erase metadata without releasing the provider placement.
  • ⚠️ bin/fm-spawn.sh:887 - Endpoint acquisition creates an empty file before backend creation. If creation fails before an endpoint is recorded, this condition still writes a durable cleanup record with endpoint_cleanup=0, and the empty file remains; the watcher then reports a permanent unpublished-cleanup wake for a resource that was never acquired.
  • ⚠️ bin/fm-spawn.sh:2935 - The Docker OpenCode hook interpolates TURNEND directly into a JavaScript string. Valid paths containing quotes, backslashes, or tabs can produce invalid or altered hook code; escape the path as a JSON/JavaScript string before writing the plugin.

🔧 Fix: Harden spawn publication and exact cleanup ownership
5 errors still open:

  • 🚨 bin/fm-teardown.sh:500 - Teardown validates a complete bridge and existing worktree at this line. If bridge removal partially succeeds or later worktree/home cleanup fails, the retained task cannot be retried because the next run rejects the now-missing bridge/worktree before reaching idempotent cleanup. Make teardown validate the remaining cleanup phase or remove bridge/cursor before returning the worktree.
  • 🚨 bin/fm-spawn.sh:2611 - The non-lease path sends treehouse get at line 2571 but arms worktree cleanup only after two settled cwd observations here. If settling or path reads time out, an acquired worktree has no exact cleanup handle or durable retry record and leaks. Register the exact provider result before observation, or retain an exact fallback handle through timeout.
  • 🚨 bin/backends/cmux.sh:386 - cmux new-workspace has already created a workspace before this pending-record write or the subsequent title lookup can fail. The resulting cmux-pending record is not consumed by endpoint cleanup, and the failure path may emit no exact workspace ID, leaving the workspace live with only a non-actionable title record. Emit an exact ID or make pending records consumable by exact cleanup.
  • 🚨 bin/fm-spawn.sh:2566 - Cleanup is armed before validate_spawn_worktree proves isolation. If treehouse returns the primary checkout, a foreign worktree, or a non-git path, abort cleanup can still run treehouse return --force on that unverified path, potentially returning another task's worktree. Keep candidate observations separate and only arm destructive cleanup after ownership validation; retain a non-destructive retry record otherwise.
  • 🚨 bin/fm-spawn.sh:1275 - Remote secondmate handling begins here before the new placement/executor/profile axes are resolved or forwarded; the launch arguments later contain only legacy harness/model/effort/backend values. A request such as --secondmate --placement docker-sandbox or --executor crabbox --execution-profile p is silently accepted but launches with remote SSH/Herdr and remote defaults, contradicting the required Docker placement for persistent secondmates and the documented per-spawn overrides. Reject these flags explicitly on remote routes or propagate them through the remote control contract.

🔧 Fix: Harden phased cleanup and exact worktree recovery
1 error still open:

  • 🚨 bin/backends/cmux.sh:386 - The required contract says “Preserve existing firstmate lifecycle,” but after cmux new-workspace succeeds, a failure writing the pending acquisition record at this line returns without emitting the exact workspace ID. fm_spawn.sh then cannot register or durably clean up the created workspace, leaving it orphaned. Resolve and retain an exact ID before this write or make every post-create failure produce a consumable cleanup record.

🔧 Fix: Make cmux acquisition ownership transactional
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/backends/cmux.sh:388 - After cmux new-workspace succeeds but ID resolution fails, this persists only the workspace title; fm-spawn.sh:764 then resolves the first matching workspace and treats it as exact cleanup authority. A duplicate/title race can close an unrelated workspace while leaking the created one, violating “Preserve existing firstmate lifecycle.” Capture the exact ID before fallible publication or retain a non-destructive unresolved record; never clean up by title.
  • ⚠️ tests/fm-backend-cmux.test.sh:522 - The new failure-path test only rejects writes tagged cmux-pending, but its response sequence resolves a workspace ID and writes a normal cmux-workspace record. It therefore does not exercise the pending-record write failure it claims to cover.

🔧 Fix: Enforce exact cmux identity during cleanup
5 errors still open:

  • 🚨 bin/backends/cmux.sh:765 - Exact cleanup scans only the current cmux window. If the task workspace was created in window A and the current window is B, the missing ID is treated as already gone, so abort clears its cleanup record while the workspace remains live. Search all windows before declaring the exact workspace absent.
  • 🚨 bin/backends/zellij.sh:380 - fm_backend_zellij_session_exists returns failure both when the session is absent and when zellij list-sessions fails. This helper treats either case as successful cleanup, allowing a transient CLI/server error during abort to forget the exact tab and leak it. Distinguish verified absence from inspection failure and retain cleanup for retry.
  • 🚨 bin/fm-sandbox-bridge-lib.sh:280 - The runtime brief copy is unchecked. If the source brief disappears or cannot be read after validation, an empty or truncated runtime-brief.md can still pass metadata/mode validation and be published to a Docker task. Make the copy checked and atomic, failing and cleaning up on error.
  • 🚨 bin/fm-spawn.sh:2742 - Bridge creation acquires the bridge and cursor before the caller copies their exact IDs into PLACEMENT_* and enables bridge cleanup. An interruption between these assignments can leave the bridge unregistered; abort cleanup has a provider fallback but no equivalent bridge fallback. Publish the acquired bridge cleanup handle before the next fallible boundary.
  • 🚨 bin/fm-bootstrap.sh:1107 - The required criterion says “missing optional tools stay silent unless selected, then bootstrap emits one actionable manual-install diagnostic.” Selected Docker configuration checks only sbx here, although the Docker placement adapter also requires jq; with jq absent, config loading can fail before the manual-install diagnostic or proceed without reporting the missing dependency. Include all selected adapter dependencies in the diagnostic path.

🔧 Fix: Hardened exact cleanup and atomic bridge ownership
6 issues (5 errors, 1 warning) still open:

  • 🚨 bin/backends/tmux.sh:93 - The new transactional writers use GNU-only -- separators (mv -f --/rm -f -- here and in zellij:347, cmux:402, herdr:2000, spawn-cleanup:46; bridge cp/mv/rm at fm-sandbox-bridge-lib.sh:289-340). macOS BSD tools reject these options, so fresh backend acquisition and Docker bridge publication fail, while cleanup journals cannot be persisted.
  • 🚨 bin/fm-sandbox-bridge-lib.sh:257 - The required criteria are “Preserve existing firstmate lifecycle” and “releases only exact recorded placements on cleanup.” After creating the bridge, identity lookup can fail at this line or during cursor initialization at line 267, leaving an acquired bridge/cursor with empty IDs; removal requires nonempty matching identities at lines 327-334, so the resource has no exact retryable cleanup handle.
  • ⚠️ bin/fm-spawn-cleanup-lib.sh:32 - The cleanup-record validator sends lines containing newline, carriage-return, or tab into key parsing instead of rejecting them. A control-character value can therefore inject extra journal fields and corrupt durable cleanup state; reject control characters before parsing key=value.
  • 🚨 bin/fm-spawn.sh:912 - The required criterion is “Preserve existing firstmate lifecycle.” The shared gate treats an empty acquisition journal/result as proof that no endpoint exists at this line, but backend creation can fail or return no usable ID after creating a window, tab, or workspace. Abort then removes the empty journal and loses cleanup authority, leaking the endpoint; retain an unresolved create-attempt record until absence is proven.
  • 🚨 bin/fm-spawn.sh:2625 - The required criterion is “Preserve existing firstmate lifecycle.” Lease-mode treehouse get is invoked without arming cleanup or a durable unresolved record first. If the provider acquires a lease/worktree and then exits nonzero or returns no path, abort cannot return the worktree or retry cleanup.
  • 🚨 bin/backends/zellij.sh:596 - The required criterion is “Preserve existing firstmate lifecycle.” The new tri-state probe returns an inspection-error state, but the generic kill path still collapses that state to successful absence here. fm-teardown.sh:2607 then removes task metadata, so a transient zellij list-sessions failure can leave the exact tab live with no retry record.

🔧 Fix: Restore BSD-safe writers and reject unsafe duplicate cleanup fields
2 errors still open:

  • 🚨 bin/fm-exec.sh:362 - The required criterion is “Crabbox owns task-scoped command execution only,” but logs validates only an opaque string before invoking crabbox logs; it never proves that the ID belongs to a run recorded for this task. This permits retrieving another provider run’s logs. Reject unbound IDs or persist a verified task-to-run binding.
  • 🚨 bin/fm-control-lib.sh:201 - The new Docker OpenCode hook cleanup invokes rm -f --, which is not portable to the supported macOS BSD tools. Docker OpenCode teardown and abort paths can therefore fail while leaving the hook and durable cleanup state behind. Use a BSD-safe removal form.

🔧 Fix: Make Docker OpenCode hook cleanup BSD-safe and option-safe
4 issues (3 errors, 1 warning) still open:

  • 🚨 bin/fm-teardown.sh:485 - The intent requires “releases only exact recorded placements on cleanup” and “reuses exact recorded placements on relaunch.” This validates the handle/provider identity but never checks its embedded task ID against the current task; task A metadata containing task B’s valid handle can release or relaunch B’s sandbox. Bind inspect/release to the caller task ID and fail closed.
  • 🚨 bin/placements/docker-sandbox.sh:338 - The handle contains an immutable provider ID, but launch invokes sbx exec by deterministic name only. If that sandbox is replaced after inspection, relaunch can execute in the replacement; clone cwd discovery at line 312 has the same gap. Use the recorded provider identity for every provider execution.
  • 🚨 bin/fm-spawn.sh:1036 - “Preserve existing firstmate lifecycle” requires a durable retry record when cleanup fails. Failure to write the cleanup record is only warned about, after which abort cleanup proceeds; if resource cleanup also fails, later journal-write failures are ignored and the exact live resource has no standard retry record. Fail closed before destructive cleanup or retain an alternative durable journal.
  • ⚠️ bin/fm-test-run.sh:200 - The new tests/fm-transactional-writers.test.sh is absent from the backend-dispatch family. A changed-only run triggered solely by bin/backends/{tmux,zellij,cmux,herdr}.sh therefore omits this fake-tool regression; add the test to that family or map the source paths directly.

🔧 Fix: Map transactional writer coverage to backend dispatch
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/backends/tmux.sh:122 - Exact cleanup treats an empty provider label/name as verified absence. If a live endpoint with the recorded ID is listed with an empty label, tmux (line 122) and the analogous zellij/herdr paths return success, so abort or teardown clears the durable cleanup record while the endpoint remains live. Only zero matching IDs should prove absence; an empty or malformed matching entry must retain cleanup for retry.
  • ⚠️ bin/fm-exec.sh:283 - The required intent includes “provider snapshot validation,” but jq -e &#39;type == &#34;object&#34;&#39; accepts multiple top-level JSON documents (including a scalar followed by an object) and persists them as a validated snapshot. Require exactly one JSON document whose value is an object before committing the snapshot.

🔧 Fix: Harden exact endpoint and snapshot validation
3 issues (2 errors, 1 warning) still open:

  • 🚨 bin/fm-spawn.sh:799 - The required criterion is “Preserve existing firstmate lifecycle.” When Herdr creates a tab and pane but its full acquisition-record write fails, it emits the exact two-token fallback &lt;tab_id&gt; &lt;pane_id&gt; at bin/backends/herdr.sh:2040-2042. The guard at this line rejects any result containing a space before the backend-specific parser runs, so exact endpoint cleanup is never armed and the created tab can leak.
  • 🚨 bin/backends/zellij.sh:426 - The required criterion is “Preserve existing firstmate lifecycle.” The post-close absence checks here and at bin/backends/herdr.sh:3138 run jq -e over raw provider output without requiring one JSON document. A stale document containing the recorded tab followed by an empty document makes the final predicate false, so cleanup reports success while the tab remains live and its durable endpoint record may be cleared. Require exactly one complete response and zero matching IDs before declaring absence.
  • ⚠️ bin/placements/docker-sandbox.sh:32 - The required criteria are “reuses exact recorded placements on relaunch” and “releases only exact recorded placements on cleanup.” The Docker inventory normalizer uses jq -e -c without enforcing a single complete JSON document, so concatenated valid arrays are accepted and consumed as authoritative identity/absence data instead of failing closed. Reject multi-document provider listings before normalizing or selecting IDs.

🔧 Fix: Harden provider inventory and partial endpoint cleanup
1 error still open:

  • 🚨 bin/placements/docker-sandbox.sh:289 - The required criterion says “releases only exact recorded placements on cleanup,” but sbx create can create a sandbox and return nonzero at this hunk. Both create branches return without reconciling before_ids against a post-create inventory; fm-spawn.sh persists only placement_pending_name and the watcher merely reports it, leaving the live provider resource without an exact handle or supported retry release. Reconcile observable post-create identities and retain an exact or durable unresolved ownership record across failures.

🔧 Fix: Reconcile partial Docker Sandbox creation failures
2 errors still open:

  • 🚨 bin/placements/docker-sandbox.sh:252 - The required criterion is “releases only exact recorded placements on cleanup.” If another actor creates the same deterministic sandbox name after the pre-create snapshot, a failed sbx create leaves exactly one new ID; lines 252-277 adopt it solely because its ID is new and its name matches, so cleanup can release the other actor’s sandbox. Require provider-level ownership proof or retain unresolved state.
  • 🚨 bin/fm-sandbox-bridge-lib.sh:521 - The required criteria are “releases only exact recorded placements on cleanup” and “Preserve existing firstmate lifecycle.” If bridge removal partially deletes files before rm -rf fails at line 531, the retry re-enters full validation at line 521, which requires all bridge inputs and can never remove the remaining bridge/cursor. Make bridge cleanup phase-aware and retryable using the exact remaining identity.

🔧 Fix: Fail closed Docker ownership and retry bridge cleanup
1 error still open:

  • 🚨 bin/backends/cmux.sh:432 - fm_backend_cmux_create_task treats a one-ID before/after title diff as the exact workspace created by this call. If cmux creates A, A disappears before the post-list, and another actor creates same-title B, this code records B and later exact cleanup can close the foreign workspace. This violates the required “Preserve existing firstmate lifecycle” contract; use the provider’s creation ID or retain an unresolved record whenever ownership is not provable.

🔧 Fix: Require provider IDs for cmux workspace ownership
1 warning still open:

  • ⚠️ bin/backends/cmux.sh:440 - The required criterion is “Preserve existing firstmate lifecycle,” but this unresolved cmux acquisition record is written directly to the live handoff path. An interrupted write or path replacement can leave a truncated or redirected record before abort reconciliation consumes it; publish it via the existing private temp-file, chmod, and atomic-rename path.

🔧 Fix: Publish unresolved cmux records atomically
1 error still open:

  • 🚨 bin/backends/cmux.sh:487 - The atomic writer still serializes an unvalidated surface_id. A malformed or multi-document list-panes response can inject newlines/duplicate fields, replace the valid workspace-only record, and prevent endpoint registration, leaving the created cmux workspace without an exact cleanup handle. Validate one complete response and one control-free provider ID before publication.

🔧 Fix: Validate cmux surface identities before record publication
2 errors still open:

  • 🚨 bin/fm-config-inherit-lib.sh:69 - The new inherited allowlist includes workspace-execution.json, but fresh local secondmate spawning explicitly propagates only trace-context (bin/fm-spawn.sh:2521-2522). Because seeded homes start with an empty config/, a secondmate never receives the primary’s placement and command-execution defaults at launch; its future workers fall back to host/direct/local. This contradicts the required persistent-secondmate Docker placement and inherited configuration behavior.
  • 🚨 bin/backends/cmux.sh:371 - When a recorded cmux workspace ID is stale, recovery selects the first workspace with the expected title via head -1 without requiring a unique match or validating the provider ID. Since cmux permits duplicate titles, a foreign same-title workspace can receive sends/captures and normal teardown can close it, violating lifecycle preservation. Require exactly one validated match or fail closed.

🔧 Fix: Harden secondmate inheritance and cmux stale recovery
2 errors still open:

  • 🚨 bin/backends/cmux.sh:1036 - Published teardown dispatches cmux to this best-effort kill path, which swallows close failures and never verifies post-close absence. If the task is the only workspace and sibling creation fails (or close no-ops), teardown removes task metadata while the cmux workspace remains live with no retryable identity. Use the exact post-close-verifying cleanup boundary.
  • 🚨 bin/fm-spawn.sh:2522 - Secondmate inheritance failures are reduced to warnings and launch continues. If the selected primary workspace-execution config cannot be copied or stale destination config cannot be removed, future workers silently fall back to host/direct/local instead of inheriting the primary defaults. Fail before publication or retain a retryable pending state.

🔧 Fix: Harden cmux teardown and secondmate config publication
1 error still open:

  • 🚨 bin/backends/cmux.sh:1068 - Published teardown parses the recorded cmux target and treats a missing workspace ID as successful absence. cmux workspace IDs do not survive app relaunch, so a restored task with the same scoped title is left live while fm-teardown deletes its metadata, contradicting “Preserve existing firstmate lifecycle.” Recover the unique scoped workspace across all windows before accepting absence.

🔧 Fix: Recover cmux stale IDs through exact all-window cleanup
2 errors still open:

  • 🚨 bin/fm-sandbox-bridge-lib.sh:420 - The intent requires “Preserve existing firstmate lifecycle” and bridge cleanup to remain retryable after partial deletion. Prepublication abort calls fm_sandbox_bridge_remove_acquired directly without first creating the phase-aware removal journal. If deletion partially succeeds and then fails, only the generic spawn-cleanup record remains; later retry revalidates already-consumed bridge inputs and cannot finish cleanup.
  • 🚨 bin/backends/cmux.sh:791 - The intent requires preserving “all existing worker-runtime backends” and reliable stale-ID recovery. fm_backend_cmux_target_ready uses the current-window workspace inventory, so after restart or window changes it refuses sends/captures for a uniquely titled task located in another cmux window, even though the all-window inventory path can prove its identity. Use the same unique all-window recovery boundary or fail with a durable retry state.

🔧 Fix: Make bridge aborts retryable and cmux recovery all-window
2 errors still open:

  • 🚨 bin/fm-spawn.sh:2073 - The secondmate inheritance call still converts configuration or shared-preference copy failures into a warning and continues. The later check only retries workspace config after endpoint acquisition and never rechecks shared preferences, so a failed inheritance can still publish a secondmate with stale or missing inherited state. Make the complete inheritance transaction a pre-publication gate or retain durable pending state.
  • 🚨 bin/backends/herdr.sh:1998 - Herdr acquisition records serialize provider-derived tab, pane, session, and workspace values directly into a line-oriented cleanup handoff without requiring single control-free fields. A malformed provider response can corrupt the record; endpoint registration then rejects both the record and fallback result, leaving an already-created tab without exact abort cleanup. Validate provider identities before publishing and bind the consumed record to the requested backend/task.

🔧 Fix: Validate Herdr acquisition identities and task binding
3 issues (2 errors, 1 warning) still open:

  • 🚨 bin/backends/cmux.sh:719 - The required criterion is “Preserve existing firstmate lifecycle.” If cmux new-workspace creates a workspace but then returns nonzero, this branch discards the response and emits no exact or unresolved acquisition record. Spawn cleanup then has no handle to return or retry, leaking the workspace.
  • 🚨 bin/backends/herdr.sh:1859 - The required criterion is “Preserve existing firstmate lifecycle.” Herdr endpoint disappearance checks parse raw pane get output without requiring exactly one complete response. A live response followed by pane_not_found can be classified as absent, allowing teardown to remove metadata while the pane remains live.
  • ⚠️ bin/backends/cmux.sh:779 - The required criterion is “reuses exact recorded placements on relaunch.” cmux surface readiness still trusts raw list-panes JSON without single-document or structural validation. A malformed or multi-document response can make a stale/ambiguous surface appear ready and receive sends or captures.

🔧 Fix: Harden cmux and Herdr provider lifecycle validation
3 issues (2 errors, 1 warning) still open:

  • 🚨 bin/backends/zellij.sh:404 - Exact Zellij cleanup validates only the top-level array. A malformed tab member or wrong-typed ID is filtered out, yielding zero matches and falsely proving the tab absent; abort cleanup can then forget a live tab. Validate all members and fail closed.
  • 🚨 bin/backends/herdr.sh:3257 - Exact Herdr cleanup checks only that result.tabs is an array and does not validate each member. A malformed member lacking tab_id yields zero matches, so abort cleanup can forget a live tab despite an incomplete inventory. Retain cleanup on malformed or ambiguous output.
  • ⚠️ bin/backends/cmux.sh:850 - Stale-ID recovery updates only process-local cmux globals; task metadata remains stale. The next invocation must rediscover the target and loses durable recovery. Persist recovered IDs before sending or capturing, or fail closed.

🔧 Fix: Harden provider inventories and persist cmux recovery
1 error still open:

  • 🚨 bin/backends/cmux.sh:810 - The intent requires “preserving current host/direct/local defaults and all existing worker-runtime backends.” fm_backend_cmux_surface_id_for_workspace accepts a pane response containing only selected_surface_id, but fm_backend_cmux_surface_exists rejects that same response because it requires a nonempty surface_ids array. Creation or recovery can record the selected ID, then every later target-ready, send, or capture check fails. Normalize both paths consistently or reject the shape during acquisition.

🔧 Fix: Unify cmux pane normalization across lifecycle paths
1 warning still open:

  • ⚠️ bin/fm-bootstrap.sh:1095 - The required criterion says “missing optional tools stay silent unless selected.” The new bootstrap path uses GNU-style rm -f -- here and again at line 1100; stock macOS BSD rm rejects that separator, so config detection emits a cleanup error and leaves its temporary file even when no optional tool is selected. Use a BSD-safe exact-path removal form.

🔧 Fix: Make bootstrap temp cleanup BSD-safe
✅ Re-checked - no issues remain.

⚠️ **Test** - 2 warnings
  • ⚠️ tests/fm-spawn-worktree-settle.test.sh:175 - The settle timing assertion exceeded its threshold under batch load twice but passed two serial reruns; this appears scheduling-sensitive rather than a product failure.
  • ⚠️ tests/fm-bootstrap.test.sh:1008 - Bootstrap clone-timing attribution failed once in the parallel batch but passed serially; no product failure reproduced.
  • bash tests/fm-workspace-placement.test.sh
  • bash tests/fm-workspace-execution-config.test.sh
  • bash tests/fm-command-execution.test.sh
  • bash tests/fm-sandbox-bridge-lib.test.sh
  • bash tests/fm-watch-sandbox-bridge.test.sh
  • bash tests/fm-spawn-docker-transaction.test.sh
  • bash tests/fm-secondmate-lifecycle-e2e.test.sh
  • bash tests/fm-secondmate-harness.test.sh
  • bash tests/fm-test-run.test.sh
  • bash tests/fm-transactional-writers.test.sh
  • bash tests/fm-backend.test.sh
  • bash tests/fm-backend-cmux.test.sh
  • bash tests/fm-backend-herdr.test.sh
  • bash tests/fm-backend-zellij.test.sh
  • bash tests/fm-backend-tmux-smoke.test.sh
  • bash tests/fm-spawn-worktree-settle.test.sh (serial confirmation)
  • bash tests/fm-control-relaunch.test.sh (serial confirmation)
  • bash tests/fm-gate-refuse.test.sh
  • bash tests/fm-bootstrap.test.sh (serial confirmation)
  • Captured sandbox lifecycle and Crabbox transcripts with tee.
  • git status --short --untracked-files=all
⚠️ **Document** - 2 errors
  • 🚨 bin/fm-workspace-execution-config.sh:92 - Integrated config and spawn reject every workspace mode except direct, so required Docker Sandbox clone support remains unimplemented outside this document-only phase.
  • 🚨 bin/fm-spawn.sh:1589 - Crabbox selection reaches unconditional preparation, which always fails; normal spawns therefore cannot use the documented opt-in executor.

🔧 Fix: Updated authoritative inheritance and metadata documentation
2 errors still open:

  • 🚨 bin/fm-workspace-execution-config.sh:92 - Integrated configuration and spawn validation still reject every workspace mode except direct, so required Docker Sandbox clone-mode support for ordinary and secondmate spawns remains unimplemented outside this document-only phase.
  • 🚨 bin/fm-spawn.sh:1589 - Every fresh spawn unconditionally calls Crabbox preparation, which intentionally fails because no recordable prewarm lease exists, so opt-in Crabbox spawns cannot publish a worker or secondmate.
🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: Fix ShellCheck findings across backend and test scripts
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

@chsong1 chsong1 closed this Aug 12, 2026
@chsong1 chsong1 reopened this Aug 12, 2026
@chsong1
chsong1 force-pushed the fm/docker-sandbox-crabbox branch from 60a8c9c to 2dbafcc Compare August 12, 2026 09:46
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