Fix the two silent capture failures in orca record opencode - #36
Merged
Conversation
…t names Some harnesses resolve each provider's API origin themselves — OpenCode picks a base URL per model out of its catalog — so no single base-URL variable names where the traffic was headed. An adapter can only redirect the two origins the dialects default to, and a run on anything else talked straight to its provider while the proxy saw nothing: on a real recording, the agent answered happily with zero established connections to the proxy, exited zero, and filed an empty trace. The mechanism is a path prefix. An adapter rewrites the provider's base URL to `<proxy>/forward/<encodeURIComponent(base)>`, so every request arrives carrying its own destination. `decodeForwardPath` decodes that strictly — http(s), no credentials, query or fragment, undefined on anything else — and the dialect reads the real path behind the prefix, so the trace records the conversation the agent had, not the envelope orca wrapped it in. Where the call goes, in order of who decided it: - A live call orca is merely relaying (record, or a loose replay continuing past its recording) goes to the base the request named. A gateway picked up from `orca setup` is a default for calls orca would otherwise have to guess at, not a licence to readdress one that announces where it was headed — the first version let the configured gateway win and every turn came back the gateway's website 404 page, recorded faithfully. - A fork substitutes the model, which is the operator choosing where answers come from: the fork's explicit upstream decides, the forwarded base is dropped, and the path is normalized to the dialect's own — a bare base url reaches this proxy as `/chat/completions`, and a gateway handed that without the version segment answers 404. The gateway's credential goes only where the gateway is: `upstreamHeaders` now name their origin (`headersOrigin` on the upstream plan), and a request headed elsewhere carries the caller's own auth, not the gateway's key. Callers that set headers without an origin keep the old attach-everywhere behaviour. Verified against the real harness, not the stub: `opencode run --agent glm53-flash -m opencode-go/glm-5.3-flash` under this proxy completed 3/3 model exchanges with status 200 — the title generator, a tool_use turn, and the final answer — where the same command without the fix recorded one 404 HTML page per turn and never reached the model. proxy 200/200. Full suite compared per test against main: 11 failures on both, identical lists — pre-existing, environment-dependent (network and toolchain), none in the proxy or adapters.
…ts shell through the shim Two silent capture failures, both met on a real recording of `orca record opencode` running an opencode-go model: - OpenCode resolves its API origin per model out of its catalog, and the only origins an environment variable can name are api.openai.com and api.anthropic.com. A run on `opencode-go` streamed straight to its provider while the proxy saw nothing — the agent answered, the exit code was zero, and the trace held two events. The adapter now writes a config overlay carried in `OPENCODE_CONFIG_CONTENT`, which OpenCode merges last, pointing its first-party providers (`opencode`, `opencode-go`) at the proxy with their real base encoded into the path — the origin-prefixed forwarding the proxy grew in the previous commit. The overlay honours routing the user configured: their own `options.baseURL` for the same provider is carried through the redirect rather than replaced, read from the same files OpenCode reads (JSONC — comments and trailing commas included, both present in real configs). A config that will not parse disables the overlay entirely — an unreadable intent is not a licence to reroute someone's provider — and an `OPENCODE_CONFIG_CONTENT` the user already set is relayed untouched, because two sources of one variable cannot be merged and clobbering theirs would change more than the capture. - OpenCode's shell tool resolves its interpreter from `$SHELL` and execs that binary by absolute path, so the PATH shim in front of `bash` and `sh` never engaged: on macOS every command ran under `/bin/zsh` and the frames file stayed empty while the trace showed shell tool calls. `SHELL` now points at a run shim named after a shell OpenCode would resolve anyway — the basename of the user's own when it is one OpenCode accepts, the platform fallback otherwise — and only after confirming the real binary is findable on PATH, because a shim whose real binary is missing answers 127 for every command and would break the run instead of under-recording it. With `--no-shell` the directory does not exist, OpenCode's own resolution falls back exactly as it would unrecorded, and the wrong variable costs nothing. The harness fixture is updated in the same change, per its own rule, with `verified_at` the day the adapter was confirmed against a running harness — the run described above, which completed 3/3 model exchanges at 200 and a zsh-frame for the shell command. The fixture test also isolates HOME now: the credential file `opencode auth login` writes decides the placeholder branch, so a fixture recorded on a machine that has signed in described a different contract from one that has not. adapters 274/274.
`zsh` joins the default shims. It is the shell OpenCode's tool resolves to on macOS, where it execs `$SHELL` by absolute path — capture only engages there because a shim named `zsh` exists to point at. The other run's PATH is untouched by this: the extra shim fires only for something that resolves `zsh` through PATH, which is what shims are for. The second half was found by the test suite hanging, not by reading code. Resolving the real binary excluded one directory — the shim dir this run installed — but a run inside a run has *another* run's shim directory on PATH, and a `sh` found there is a shim script, not a binary. Spawning it re-runs the runner, which resolves again: a hundred-deep exec loop inside the user's agent run, until something kills it. It is not hypothetical — it is what happens when an agent that records things for a living is itself recorded, which is exactly the session where the first nested run was met. The shim scripts carry a marker comment, so anything carrying it is refused whichever directory it sits in, by reading the head of each candidate file. Binaries elsewhere on PATH are safe to read and discard; a file that cannot be read is not a shim, and the exec attempt will say what is wrong with it.
There was a problem hiding this comment.
🐳 OrcaCode Review
✅ No findings — nothing to flag in this PR. Great work!
OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 262 calls · 16M tokens · 97% cached
❤️ Share · Install OrcaCode Review
Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.
Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter
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.
Orca-Code-Review — push 1
✅ no blocking findings
The failure, met on a real recording
orca record opencodeon anopencode-gomodel: the agent answered happily, exited zero, and the trace held two events (run.start,fs.snapshot). Nomodel.request, nomodel.response, and an emptyshell-frames.jsonl— whilelsofshowed the agent's one established connection going to172.65.90.21:443, the provider, and nothing to the proxy. Two capture layers were silently absent:OPENAI_BASE_URLandANTHROPIC_BASE_URLcoverapi.openai.comandapi.anthropic.com; OpenCode's own first-party providers (opencode,opencode-go) resolve their base from the catalog (https://opencode.ai/zen/go/v1) and no variable names it, so the traffic never reached the proxy. The adapter's own comment claimed "whichever protocol the chosen model speaks, the traffic lands on the proxy" — the assumption behind that is false for any third origin.$SHELLand execs it by absolute path. The PATH shim in front ofbash/shnever engaged; the log saidshell tool using shell /bin/zshand the frames file stayed empty while every command ran.The fix
Origin-prefixed forwarding (
proxy): an adapter rewrites a provider base to<proxy>/forward/<encodeURIComponent(base)>, so every request arrives naming its own destination. The decoder is strict (http(s), no credentials/query/fragment,undefinedon anything else, so the prefix never fires on a path that merely looks like one). Dialect selection and the recorded trace use the real path behind the prefix.Routing, by who decided it:
--loosecontinuation) goes to the base the request named — a gateway fromorca setupis a default, not a readdressing of a request that announces its own destination. This ordering is not theoretical: the first version let the configured gateway win, and every turn came back404— the gateway's website HTML, recorded faithfully in the trace;/chat/completions→/v1/chat/completions), because a gateway handed a version-less path answers 404;upstreamHeadersnow name their origin and are attached only on matching calls, so a forwarded request to a third origin never carries the gateway's key.The opencode adapter: writes a config overlay in
OPENCODE_CONFIG_CONTENT(merged last by OpenCode) pointing the first-party providers at the proxy through the mechanism above. It honours routing the user configured — their ownoptions.baseURLfor the same provider is carried through the redirect rather than replaced, read from the same JSONC files OpenCode reads; an unparseable config disables the overlay entirely; a user-setOPENCODE_CONFIG_CONTENTis relayed untouched. AndSHELLpoints at a run shim named after a shell OpenCode would resolve anyway, guarded by resolvability — a shim whose real binary is missing would 127 every command instead of under-recording.shell-shim:
zshjoins the default shims, andresolveRealBinarynow refuses any run's shim found on PATH, not only its own — found because the test suite hung: a run inside a run has another run's shim directory on PATH, and resolving a shim script as a binary is a hundred-deep exec loop inside the user's agent run.Verified against the running harness
opencode run --agent glm53-flash -m opencode-go/glm-5.3-flashunder the fixed proxy: 3/3 model exchanges at status 200 (title generator →tool_use→end_turn), the shell command captured in a zsh frame (argv, exit code, duration, byte counts), exit 0 — where the same command without the fix recorded one 404 HTML page per turn and never reached the model. The harness fixture is updated in the same change withverified_atset to that day, per the fixture's own rule.Tests
main: 11 failures on both, identical lists — pre-existing and environment-dependent (network/toolchain), none in the proxy, adapters or shell-shim.