Conversation
PR #70 validated an explicit `--model` for OpenCode launches, but only in the launcher's own directory and only when a model was named. Two gaps followed from that, both of which let a worker start on a model nobody checked: - An omitted or `--model default` launch validated nothing, and OpenCode then chose its own default from whatever config the worker directory holds. The gate was bypassed rather than failed. - `opencode models` is project-scoped, so a model supplied by a provider the worktree config declares is listed in the worktree and not in the launcher. Validating an explicit model in the launcher's directory refuses a model the worker can actually run. Both paths now resolve and validate in the settled task worktree, inside the destination pane, under the same environment expression the launch itself gets, so the binary, working directory, allowlisted credentials, and OpenCode server route are the ones the worker will have. `opencode debug config` is read as the source inventory it is (v2) or the resolved object it is (v1), ranked by the documented precedence, and the resulting root model is pinned on the launch. An unresolvable source, an equal-precedence disagreement, or no root model at all fails closed rather than deferring to OpenCode's default. Availability and zero cost are checked the same way for both paths, and `opencode-go/space-bunny-free` is not privileged: any model that clears the gate is accepted. Two things the validation could not see before it saw them: - `opencode models` prints only base ids, so it cannot confirm a `#variant`. The only declaring source is provider metadata in the resolved config, and an available base does not carry an arbitrary variant: the exact `base#variant` must be one the config SELECTS. Selectable variants are resolved through the same precedence order as the root model - union within one precedence step, replaced across steps, including by an empty set, which is how a higher-precedence document disabling every variant clears the model. Comparing declared sets alone would admit two same-step documents that both declare `low` and disagree on whether it is enabled. - The raw-launch escape hatch classified on the executable it could name, so `command opencode`, `exec opencode`, a quoted executable, or a `sh -c opencode` all reached the worker with every model guard skipped. Commands whose whole job is running another program are now walked through to the executable they would run, and a command naming OpenCode anywhere outside the resolved executable position fails closed. A launch that merely mentions OpenCode in a prompt is still an unrelated launch, and `cd <dir> && ./probe` is still a legitimate raw command. The shared spawn fixture executes the preflight the way a real pane does, so a suite that spawns OpenCode no longer fails on a timeout instead of on the behavior it means to test, and suites share one OpenCode preflight stub rather than each writing its own. Regression coverage spans the omitted and explicit paths, unavailable and nonzero-cost metadata, declared, disabled-by-precedence, and equal-precedence-conflicting variants, v1 and v2 config shapes, custom config-location overrides, the wrapper and quoting forms above, and the bounded preflight actually cutting off a hung catalog rather than only passing the bound as an argument. Verified against a live OpenCode 2.0.18: `debug config` is a source array carrying `info.model` in object form, `OPENCODE_CONFIG_CONTENT` does not register as a config source, and `opencode models` lists base ids with no `#` forms.
…ination-pane config resolution
Ivory2024
deleted the
fm/firstmate-pr70-opencode-model-default-validation-20261001
branch
October 1, 2026 11:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
70번 GitHub 고치기
What Changed
bin/fm-opencode-probe.shto query a live OpenCode pane and report the model it is actually running.bin/fm-spawn.shto resolve the expected model from destination-pane config and verify it against the probed actual model before/after dispatch, replacing the prior validation approach..agents/skills/harness-adapters/references/harness/opencode.mdanddocs/configuration.mdto document the destination-pane config resolution behavior.tests/fixtures.sh,tests/fm-busy-adapter-wiring.test.sh, andtests/fm-spawn-dispatch-profile.test.shwith fixtures and cases covering the new model-validation paths.Risk Assessment
🚨 High: The raw-launch escape hatch's new shell-interpreter exemption (bin/fm-spawn.sh:2426) has a confirmed, reproduced bypass that lets an OpenCode worker launch with a completely unvalidated/unpaid-unchecked model through
/bin/sh|bash|zsh|dash -c \"...opencode...\", defeating the exact cost/model gate this change (and the PR #70 gap it fixes) exists to enforce.Testing
Live-drove probe fix against real OpenCode 1.18.33 CLI: before fix, pipe truncates at 65536 bytes and jq parse fails; after fix, file-redirect captures full 110466 bytes, probe writes config.status=0 with valid complete JSON, no leaked config.raw.json. Scenarios 3 and 4 (model-resolution and adapter-wiring regression suites) were run but against fixture-mocked binaries, not the live product, so per validation they are downgraded to untested rather than reported as pass.
Evidence: Live probe run output (config.status=0, valid JSON) vs raw pipe truncation at 65536 bytes
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-spawn.sh:2426- The raw-launch classifier'sraw_shell_scriptexemption is meant only for<interpreter-path> <script-path>(e.g./bin/sh '/path/to/probe.sh', the legitimate form tested at tests/fm-spawn-dispatch-profile.test.sh:2123). It is implemented as a plain substring match on*/bin/sh\ * | */bin/bash\ * | */bin/zsh\ * | */bin/dash\ *, which also matches/bin/bash -c "...",/bin/sh -c "...",/bin/zsh -c "...", and/bin/dash -c "..."(with or without a leading wrapper likeexec/timeout). I traced this concretely: for ARG3 =/bin/bash -c "opencode --model gpt-4-paid --prompt hi", raw_shell_script=1, which suppresses raw_shell; the embeddedopencodetoken sits inside the quoted string so the bare-word scan (raw_expanded) also misses it (quoted spans are stripped before that scan); the executable walk resolves HARNESS=bash(notopencode), so RAW_OPENCODE never becomes 1. None of the four refusal branches at bin/fm-spawn.sh:2576-2588 fire, and because HARNESS != opencode, the entireif [ "$HARNESS" = opencode ]validation block (bin/fm-spawn.sh:4471+, including the explicit raw-launch pinning refusal) is skipped entirely. The raw command launches as-is, running an arbitrary/paid/unverified OpenCode model with zero model, cost, or availability checking — exactly the bypass class this PR exists to close (and the same class the embedded comments explicitly reason about for the no-absolute-path case, calling the baresh -c opencodeform 'unresolvable by construction'). Reproduced by extracting and executing the exact classification block from bin/fm-spawn.sh in isolation with that ARG3 value: HARNESS=bash, RAW_OPENCODE=0, raw_shell=0, raw_shell_script=1, raw_expanded=0, raw_quoted=0, raw_ambiguous=0. No existing test exercises the absolute-path-cform (tests only cover the no-pathbash -c "opencode..."form at tests/fm-spawn-dispatch-profile.test.sh:1300, which IS correctly refused because it lacks the absolute-path exemption). The same gap applies uniformly to all four listed shells at bin/fm-spawn.sh:2426, and to any wrapper-prefixed form (e.g.exec /bin/bash -c "opencode ...",timeout 30 /bin/bash -c "opencode ...") since the wrapper walk reaches the same absolute path before the exemption check. Smallest fix: narrow the case pattern (or add an explicit check) so the exemption only fires when the token following the interpreter path is not-c/-lc/-icetc. — i.e., require the next word to look like a path, not an option.bin/fm-spawn.sh:1124- spawn_abort_cleanup only removes the OpenCode probe directory when$OPENCODE_PROBE_DIR/doneexists (bin/fm-spawn.sh:1124). If opencode_worker_probe times out waiting for the destination pane (bin/fm-spawn.sh around the 'did not return ... within Ns' error),donewas never written, so the private probe directory (containing run.sh and any partial config/models output) is left behind under the task worktree on every abort path, including the global EXIT trap. The in-pane background process may also still be running and could still write to that leaked directory after the script has exited. Smallest fix: in spawn_abort_cleanup, remove the probe directory unconditionally when OPENCODE_PROBE_DIR is set (not gated on the done marker), since on abort the directory and its contents are disposable either way.bin/fm-opencode-probe.sh:110- fm-opencode-probe.sh pipes"$bin" debug configstraight intojq(bin/fm-opencode-probe.sh:110). Against the real OpenCode CLI actually installed on this machine (1.18.33, on PATH as /opt/homebrew/bin/opencode), that pipe silently truncates at exactly 65536 bytes:opencode debug config 2>/dev/null | wc -c-> 65536, even read one byte at a time withdd bs=1(so it is not a slow-reader artifact), whileopencode debug config > filecaptures the full 110466 bytes. The truncated JSON failsjq -cwith a parse error, soopencode_resolve_effective_modelsees config.status=5 and refuses the launch with 'could not verify the effective OpenCode model ... (exit 5)' (bin/fm-spawn.sh:1987) for EVERY omitted/default-model or#variantOpenCode spawn in a worktree whose resolved config exceeds 64KB — a bar a normal multi-provider opencode.json clears easily (this repo's did, at 110KB). I confirmed the ranking/jq logic itself is correct by feeding the untruncated file through the probe's own normalize_filter: it parses fine and correctly reports no root model declared here. So the only defect is the raw pipe losing data from this real CLI version, not the validation logic. This breaks the centerpiece behavior the PR adds (resolving and validating an omitted/default model in the worker worktree) against a CLI version actually in use, though the commit message notes verification was done against a newer 2.0.18 where this pipe behavior may differ.bash tests/fm-spawn-dispatch-profile.test.sh — 59/59 OpenCode/raw-launch/variant/secondmate/pi/claude dispatch-profile cases passed, exit 0bash bin/fm-opencode-probe.sh "$(command -v opencode)" "$PWD" <result-dir> 20 1 run directly against the real installed opencode 1.18.33 binary and this actual worktree — reproduced config.status=5 (jq parse failure)opencode debug config 2>/dev/null | wc -c (65536) vs opencode debug config 2>/dev/null > file; wc -c file (110466) and ... | dd bs=1 | wc -c (65536) — isolated the truncation to the pipe path the probe uses, independent of consumption speedFed the untruncated debug-config JSON through the probe's embedded normalize_filter directly with jq to confirm the ranking/parsing logic is correct and the defect is isolated to the pipe truncation🔧 Fix applied.
1 warning still open:
bin/fm-opencode-probe.sh <opencode> <worktree> <result-dir> 15 1 (direct live run against real installed opencode CLI)opencode debug config 2>/dev/null | wc -c (reproduces 65536-byte pipe truncation)opencode debug config > file; wc -c; jq -c . file (confirms file-redirect captures full 110466 bytes and parses)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.