Skip to content

Fix the two silent capture failures in orca record opencode - #36

Merged
yi-here merged 3 commits into
Continuum-AI-Corp:mainfrom
askdkc:fix/opencode-capture
Sep 6, 2026
Merged

Fix the two silent capture failures in orca record opencode#36
yi-here merged 3 commits into
Continuum-AI-Corp:mainfrom
askdkc:fix/opencode-capture

Conversation

@askdkc

@askdkc askdkc commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Orca-Code-Review — push 1

Severity Count
P0 0
P1 0
P2 0
P3 0

✅ no blocking findings

The failure, met on a real recording

orca record opencode on an opencode-go model: the agent answered happily, exited zero, and the trace held two events (run.start, fs.snapshot). No model.request, no model.response, and an empty shell-frames.jsonl — while lsof showed the agent's one established connection going to 172.65.90.21:443, the provider, and nothing to the proxy. Two capture layers were silently absent:

  1. OpenCode resolves its API origin per model, and only two origins can be named with an environment variable. OPENAI_BASE_URL and ANTHROPIC_BASE_URL cover api.openai.com and api.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.
  2. OpenCode's shell tool resolves its interpreter from $SHELL and execs it by absolute path. The PATH shim in front of bash/sh never engaged; the log said shell tool using shell /bin/zsh and 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, undefined on 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:

  • a live relay (record, or --loose continuation) goes to the base the request named — a gateway from orca setup is 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 back 404 — the gateway's website HTML, recorded faithfully in the trace;
  • 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 (/chat/completions/v1/chat/completions), because a gateway handed a version-less path answers 404;
  • the gateway's credential goes only where the gateway is: upstreamHeaders now 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 own options.baseURL for 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-set OPENCODE_CONFIG_CONTENT is relayed untouched. And SHELL points 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: zsh joins the default shims, and resolveRealBinary now 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-flash under the fixed proxy: 3/3 model exchanges at status 200 (title generator → tool_useend_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 with verified_at set to that day, per the fixture's own rule.

Tests

  • proxy 200/200, adapters 274/274 (new: forward decoding/routing/credential-gating suite, JSONC stripper suite, opencode overlay + SHELL tests, nested-shim refusal).
  • Full suite compared per test against main: 11 failures on both, identical lists — pre-existing and environment-dependent (network/toolchain), none in the proxy, adapters or shell-shim.

…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.

@orcacode-review orcacode-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐳 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

@yi-here
yi-here merged commit 22dba3c into Continuum-AI-Corp:main Sep 6, 2026
@askdkc
askdkc deleted the fix/opencode-capture branch September 6, 2026 09:52
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.

2 participants