feat: make opencode web embeddable in iframes at a subpath#23912
Open
csillag wants to merge 1 commit intoanomalyco:devfrom
Open
feat: make opencode web embeddable in iframes at a subpath#23912csillag wants to merge 1 commit intoanomalyco:devfrom
csillag wants to merge 1 commit intoanomalyco:devfrom
Conversation
88acdbc to
77df993
Compare
csillag
added a commit
to deai-network/optio
that referenced
this pull request
Apr 23, 2026
Follow-ups from the branch-level code review. All low-risk fixes that came up during verification/review of the otherwise-ready feature. - RemoteHost.launch_opencode: read the opencode server password from a mode-0600 file in the workdir via `$(cat ...)` rather than inlining it into the command string. Previously any local user on the remote host could read the basic-auth password from the bash process's argv via `ps aux` during the launch window. Env variables (asyncssh create_process env=) would be cleaner but OpenSSH's default AcceptEnv won't forward arbitrary names, so a mode-0600 file the command substitutes at exec time is the portable fix. - RemoteHost.terminate_opencode(aggressive=True): use proc.kill() (SIGKILL) instead of proc.terminate() (SIGTERM). The spec's cancellation path expects SIGKILL; with SIGTERM an opencode that blocks on shutdown would eat the 5-second shutdown grace period. LocalHost was already correct. - LocalHost.tail_log / RemoteHost.tail_log: document why we use `tail -F -n +1` instead of the spec's `-n 0`. Fresh workdir + race- free at-least-once delivery (an earlier `-n 0` attempt silently dropped log lines that opencode wrote between subprocess spawn and our tail subscribing). - Remove unused `aiofiles>=23.0` dependency (the module docstring mentioned it but the implementation spawns a `tail` subprocess; aiofiles was never imported). Update host.py's module docstring. - AGENTS.md: add a "Where the fork lives" paragraph pointing at github.com/csillag/opencode #csillag/make-web-embeddable-in-iframes, the upstream PR anomalyco/opencode#23912, and the build command so someone picking up this branch cold can populate OPTIO_OPENCODE_BINARY_DIR without hunting for the right repo. Deferred review items (no change in this commit): remote-mode cancellation test (spec Section 9 coverage gap), routing install stderr through ctx.report_progress (nice-to-have), cosmetic file-touch idiom, demo-task inner.execute clarification, SFTP-over- SSH retry path not exercised by tests, negative-cache comment consolidation.
77df993 to
e83eb50
Compare
Author
|
Sorry for the rebase hickup, now fixed and the change is only 16 lines. |
4ff331f to
686a8dd
Compare
Enables a reverse-proxy / embedding host (e.g. a parent dashboard) to serve opencode web under an arbitrary URL prefix — the built SPA, the server's CSP, and the SDK's default-server-URL resolution all have to cooperate for a same-origin iframe mount to actually work. Four orthogonal changes: 1. packages/app/vite.config.ts — `base: './'` Emits relative asset paths in the built index.html and chunk imports (e.g. `./assets/foo.js` instead of `/assets/foo.js`), so a document loaded under `/some/deep/iframe-prefix/` can resolve its own asset URLs against that prefix rather than against the origin root. No effect on direct-serve at `/`; every Vite-base subpath story just works from one source build. 2. packages/opencode/src/server/routes/ui.ts — add `'unsafe-eval'` to the embedded-UI CSP's script-src directive Zod 4 (pinned via the workspace catalog at `[email protected]`) JIT-compiles its validators at schema-definition time via `new Function(...)` — that's where v4's speed advantage over v3 comes from. Every `z.object({...})` in the bundle therefore needs `'unsafe-eval'`; the existing CSP only granted `'wasm-unsafe-eval'`, which Firefox correctly distinguishes from the broader keyword Zod actually needs. Confirmed by tracing: the first Function-construct trap at page load fires from `packages/app/src/context/global-sdk.tsx:12` (`z.object({ name: z.literal('AbortError') })`) and the stack walks through `zod/v4/core/{core,schemas,util}.js`, which is the v4 codegen pipeline. This relaxation is therefore not optional while we depend on zod 4 — the alternatives are switching the `app` package to `zod/mini` (no-codegen entry, restricted API) or downgrading to zod 3, both larger refactors. 3. packages/app/src/entry.tsx — `getCurrentUrl` honors the localStorage defaultServerUrl override `getCurrentUrl()` was previously hard-wired to `location.origin` (in production) for the initial `servers[0]` entry, while `getDefaultUrl()` would return the localStorage-set `defaultServerUrl` when present for the `defaultServer` key. The two disagreed: the server-context's `current` server resolves via `allServers().find(key === state.active) ?? allServers()[0]`, so if `state.active` pointed at the localStorage URL but that URL wasn't in `allServers()`, the code fell back to `allServers()[0]` — i.e. `location.origin` — and control-plane requests like `/global/config` and `/global/event` bypassed the override entirely. Having `getCurrentUrl` also honor the localStorage override keeps both entries aligned and makes the override globally effective. 4. packages/opencode/src/server/routes/ui.ts — extend `connect-src` to allow `https://opencode.ai` `packages/app/src/context/highlights.tsx` fetches the release changelog from `https://opencode.ai/changelog.json` to surface release highlights to the user. Under the previous `connect-src 'self' data:` the fetch was blocked once the SPA ran from a non-opencode.ai origin (i.e. exactly the embed case). Adding `https://opencode.ai` to `connect-src` lets the changelog feature keep working in embedded deployments and is consistent with the SPA's existing same-domain trust (the production fallback proxy and the `location.hostname.includes('opencode.ai')` logic in `entry.tsx` already treat that origin as canonical). Together these let opencode web embed inside an iframe served from a foreign origin / subpath: assets load, the SPA bundle executes, all SDK calls (including control-plane routes) honor the configured server URL, and the changelog fetch isn't blocked by CSP. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
686a8dd to
6488100
Compare
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.
Issue for this PR
No existing issue — self-motivated fix to enable embedding
opencode webinside an iframe under a reverse-proxy subpath. Happy to file one
retroactively if that's the workflow you prefer.
Type of change
What does this PR do?
Lets
opencode webwork inside an<iframe>served from a URL subpath(e.g.
/some/deep/path/<session-id>/) under a reverse proxy. Foursmall orthogonal changes, none of which affect a direct-at-root
deployment:
packages/app/vite.config.ts—base: './'. The builtindex.htmland chunk imports now use relative asset paths so a document loaded
under a subpath resolves assets against the current URL instead of
the origin root. Fixes "asset requests return the SPA fallback HTML
with the wrong MIME type" for subpath embeds.
packages/opencode/src/server/routes/ui.ts— add'unsafe-eval'tothe embedded-UI CSP's
script-src. The production bundle invokesnew Function(...)at runtime: Zod 4 ([email protected]via the workspacecatalog) JIT-compiles its validators at schema-definition time,
which is where v4's speed advantage over v3 comes from. Every
z.object({...})in the bundle therefore needs'unsafe-eval';the existing policy allowed
'wasm-unsafe-eval'but not thebroader keyword, so Firefox (stricter than Chromium here)
correctly blocked the bundle once it ran in an iframe. Confirmed
by tracing — the first
Function-construct trap at page loadfires from
packages/app/src/context/global-sdk.tsx:12(
z.object({ name: z.literal('AbortError') })) and the stackwalks through
zod/v4/core/{core,schemas,util}.js, the v4codegen pipeline. The relaxation is therefore not optional while
we depend on zod 4 — alternatives are switching the
apppackageto
zod/mini(no-codegen entry, restricted API) or downgradingto zod 3, both larger refactors out of scope here.
packages/app/src/entry.tsx—getCurrentUrl()now honors thelocalStorage.defaultServerUrloverride the same waygetDefaultUrl()already does. Before,servers[0].urlwashard-wired to
location.originwhiledefaultServer's key camefrom the override; the server-context resolver
(
allServers().find(key === state.active) ?? allServers()[0]) fellback to
allServers()[0]when the two keys didn't align, andcontrol-plane calls like
/global/configand/global/eventbypassed the override entirely. Aligning both entries fixes that.
packages/opencode/src/server/routes/ui.ts— extendconnect-srcto allow
https://opencode.ai.packages/app/src/context/highlights.tsxfetches the release changelog from
https://opencode.ai/changelog.jsonto surface release highlights. Under the previous
connect-src 'self' data:, the fetch was blocked once the SPA ranfrom a non-opencode.ai origin (i.e. exactly the embed case). The
expanded directive keeps the changelog feature working in
embedded deployments and is consistent with the SPA's existing
same-domain trust for that origin (
location.hostname.includes('opencode.ai')in
entry.tsx, and the production fallback proxy inui.ts).The commit message on
686a8dd63has the per-change rationale in moredepth.
How did you verify your code works?
Built from source (
bun run --cwd packages/opencode build) andinstalled
opencode-linux-x64/bin/opencodeon a Linux test box. Ranthe binary in two configurations:
works, session/prompt round-trip works, provider auth works. No
behavior change vs. unmodified opencode.
path. Before these changes: asset requests resolved to the proxy's
origin root (wrong MIME type, SPA-fallback HTML), CSP blocked
evalwhen the SPA bundled code ran, and
/global/*SDK calls wentdirectly to
location.originignoring the configured server URL.After these changes: iframe mounts, the SPA bundle executes, all
SDK calls (including control-plane) route through the configured
server URL via the proxy.
Screenshots / recordings
Not attached — happy to record if it'd help.
Checklist