(opencode-workspaces-plugin): add Daytona workspace adaptor plugin for OpenCode - #38
Draft
jamesmurdza wants to merge 18 commits into
Draft
(opencode-workspaces-plugin): add Daytona workspace adaptor plugin for OpenCode#38jamesmurdza wants to merge 18 commits into
jamesmurdza wants to merge 18 commits into
Conversation
…io/daytona@106ca0a3 Seeds apps/opencode-workspaces-plugin with libs/opencode-plugin as it stood in daytonaio/daytona at 106ca0a33 (tree 1e19ef2), so that the workspace- adaptor rewrite commits that follow apply as their authors wrote them. This tree is the pre-rewrite plugin; the next commit replaces most of it. It exists only to give the replayed history a faithful base.
Replaces the previous custom tools/session-manager implementation with a workspace adaptor that runs opencode inside a Daytona sandbox. Adds a system-prompt instructions file scoped to Daytona sessions, generates unique opencode-prefixed sandbox names, and forwards env vars through workspace create. Follow-up improvements folded in: - Drop workspace-API workarounds, import upstream types - Clean up Daytona sandbox when create() fails partway - Exclude host .opencode/ from sandbox repo upload - Clean up repo tarball on sandbox after extraction - Derive preview URL template from getPreviewLink - Warn/error clearly when DAYTONA_API_KEY is missing - Add integration and adapter-registration tests; fix flakiness - Update README (troubleshooting, migration guide, instructions, workspace creation steps, OpenCode 1.14.x requirement, test docs) - Add bun.lock Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: James Murdza <james@jamesmurdza.com>
macOS bsdtar embeds Apple extended attributes (e.g. com.apple.provenance) as LIBARCHIVE.xattr.* pax headers and AppleDouble ._* entries when building the repo upload tarball. On extraction the sandbox's GNU tar floods stdout with "Ignoring unknown extended header keyword" warnings and litters the repo with ._* files. Pass --no-xattrs and COPYFILE_DISABLE=1 to suppress both. Both are no-ops on Linux hosts (COPYFILE_DISABLE is macOS-only; --no-xattrs is GNU tar's default), so the fix stays portable. Adds an optional env passthrough to spawnAsync to set COPYFILE_DISABLE for the tar invocation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Running against a local OpenCode build" subsection explaining how to test the plugin against a from-source OpenCode checkout: run `bun dev` from the OpenCode repo root with the test project as a trailing argument. Notes the JSX-config (jsxImportSource) reason it must run from packages/opencode, and the "Cannot find module 'react/jsx-dev-runtime'" failure that results from launching the entry point inside the test project instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…teps The source-parse and adaptor-registration snippets assumed different cwds without saying so. Add the cd lines (libs/opencode-plugin and /tmp/myproject) so each is unambiguous. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The create() run() helper wrote every sandbox command's stdout to the host terminal, flooding it with the opencode installer's progress bar/banner, tar output, etc. during workspace creation. Surface command output only on failure (already captured in the thrown error) so the happy path stays quiet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The installer's "latest release" lookup hits api.github.com, which is rate-limited (HTTP 429) and fails with "Failed to fetch version information" when many sandboxes install in a short window — aborting workspace creation. Pass VERSION to skip that lookup. Also moves the env vars onto bash (right of the pipe) where the installer actually reads them, instead of curl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
File logger to /tmp/daytona-plugin.log tracing each create()/remove()/target() step, so a stalled or failing step in the experimental-workspace flow is visible without streaming to the UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sandbox stops servicing new commands while opencode runs its one-time DB migration on first start, so a health poll issued during that window hangs indefinitely and never returns — wedging the poll loop, and thus workspace creation, forever (the TUI shows "Creating…" with no progress) even after the server is actually healthy. Wrap each poll (and the final log fetch) in a 5s client-side timeout: a stuck poll is abandoned, treated as not-ready-yet, and the loop continues. Once the migration finishes a later poll returns and creation completes normally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drives the real OpenCode TUI via tmux (open -> /warp -> select Daytona -> wait for sandbox -> send a message) against the released opencode binary. Hard-asserts the plugin's responsibilities: the workspace provisions (sandbox started, remote /global/health healthy, create() completes without hanging) and the remote answers (session contains an assistant reply echoing a magic token). Logs — but does not assert — whether the host TUI renders the reply, since that depends on OpenCode's experimental-workspace global sync (an intermittent upstream gap, not this plugin). Gated on DAYTONA_API_KEY + tmux + a released opencode binary; skipped otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…them as dead Idle Daytona sandboxes auto-stop after ~15 min. The workspace global-sync connects to the sandbox via target() (it dials <url>/global/event), so once a sandbox paused, that endpoint failed and the host logged a relentless "failed to connect to global sync" (HTTP 400) storm — which destabilized the host and broke chat rendering. The sandbox wasn't dead, just paused. - target(): if the sandbox isn't 'started', resume it (sandbox.start()) and relaunch opencode serve (the server process doesn't survive a stop/start), then return the preview link. Cache entries carry a short verification TTL so bursty target() calls stay fast. - create(): disable auto-delete (autoDeleteInterval: -1) so a timeout only pauses the sandbox (resumable) rather than deleting it (which orphans the OpenCode workspace entry and is what actually triggered the 400 storm). - Factor the server launch + health-wait into ensureServerRunning(), shared by create() and target(). Verified live: create -> stop (simulated timeout) -> access resumes the sandbox, relaunches the server, and /global/health is reachable through the proxy again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alone npm setup The plugin arrived from the Daytona monorepo carrying that repo's Nx wiring. None of it works here, so swap it for the same standalone layout the other Node projects in this repo use: - drop project.json (Nx build/test/set-version/publish targets) and tsconfig.lib.json (project references into ../../tsconfig.base.json, which does not exist here); fold the compiler options into a single tsconfig.json - drop bun.lock for package-lock.json — CI installs with `npm ci` - drop README.legacy.md and .npmignore, both artifacts of npm publishing - mark the package private: this is an app, not a published package - add scripts: build (tsc, emits in place), typecheck, test (bun) - add LICENSE, and ignore the in-place tsc output Also add `opencode-ai` as a devDependency. Every test file needs an `opencode` binary — @opencode-ai/sdk does not bundle one, it cross-spawns `opencode` from PATH — and only e2e-tui.test.ts degrades to a skip without it; the other two fail outright. The npm package ships the real binary as platform optionalDependencies, so `npm ci` now yields a runnable suite with nothing installed globally and no curl-pipe-bash anywhere. The `test` script points OPENCODE_BIN and PATH at node_modules/.bin so both CI and a fresh clone pick it up. Verified: `npm run typecheck` and `npm run build` pass with @opencode-ai/plugin resolving to 1.18.11 (the manifest floor is ^1.4.11), so the WorkspaceAdapter / experimental_workspace surface the plugin builds on is still intact upstream. `npm test` on a PATH stripped of any global opencode gives 1 pass / 10 skip / 0 fail — the skips are the live tests wanting DAYTONA_API_KEY.
The README arrived describing a package published to npm from the Daytona
monorepo. Neither is true here, so:
- installation now uses a `file://` plugin spec against a local checkout,
since this app is not published; the symlink recipe stays as the alternative
- development setup is `git clone integrations` + `npm ci`, replacing the
yarn + `npx nx run opencode-plugin:{test,build,publish}` commands
- the "Migrating from v0.167.0" section became "Relationship to
@daytona/opencode": the two plugins now coexist in this repo rather than
superseding each other by version, so the useful thing to document is how
they differ and which to reach for
- documented the /tmp/daytona-plugin.log debug file
- corrected paths (libs/opencode-plugin -> apps/opencode-workspaces-plugin)
and the project-structure tree
The testing section gets a per-file table of what each test needs and covers,
and states plainly that nothing runs the suite in CI. Missing prerequisites
make these tests skip rather than fail, so a green run proves very little on
its own — the table exists so the skip count can be read against something.
Single-file runs go through `npm test -- <file>` rather than bare `bun test`.
integration.test.ts and e2e-tui.test.ts launch the binary by absolute path
(OPENCODE_BIN, defaulting to ~/.opencode/bin/opencode), which only the `test`
script sets; calling bun directly makes them look for a global install a fresh
clone does not have, and both fail quietly when it is missing — one waits 60s
and blames the server, the other just skips.
Prose is unwrapped to one line per paragraph, matching the other three
READMEs in apps/.
Also lists the app in the root README's Apps table.
…test.ts Despite its name, this test never touched the plugin. It inlined a ~115-line copy of the workspace adaptor into a temp project and ran OpenCode against that. The copy had drifted from the real plugin and was missing two fixes that matter: - the create() error path, so a failure leaked its sandbox instead of deleting it - withTimeout() on the health polls, so a command issued during opencode's first-start DB migration hung instead of failing Together those produced the observed behaviour: a ~300s hang ending in a client-side fetch timeout, with a running sandbox left behind. A copy of the thing under test proves nothing about the thing under test. Load the real plugin through a file:// spec in the temp project's opencode.json, the way plugin.test.ts already does. The temp-project `npm install` goes with it — the plugin's imports resolve from this package's node_modules, since the spec points inside it. Two further fixes the switch exposed: - the repo was built on whatever init.defaultBranch gives (main on current git) while the workspace asked for `master`. The real plugin passes that to `git clone --branch`, so it could never have cloned. Both sides now use TEST_BRANCH, forced with `git branch -M` so the host's git config cannot change the outcome. - afterAll only knew about a sandbox once the create request came back, so a hang left nothing recording what to delete. It now also parses `create: start name=` out of the plugin's debug log, which is written before the sandbox is provisioned. Same trick e2e-tui.test.ts uses. Verified live: 2 pass / 0 fail in 25s, down from a 300s hang. Step 5 now lists README.md, index.ts, package.json, src/ and .git inside the sandbox — the repo upload only exists in the real plugin, the copy just made an empty directory. The plugin log shows create -> "server healthy; done" and a matching "remove: deleted sandbox", so create() and remove() are both covered now. Sandbox count returned to its pre-run baseline.
…e_modules The tests defaulted OPENCODE_BIN to ~/.opencode/bin/opencode — a global install — and the npm `test` script papered over that by exporting OPENCODE_BIN and PATH. So the suite only worked through one invocation path. Calling `bun test` directly, which is the obvious thing to do, broke on a fresh clone, and broke quietly: integration.test.ts waited 60s and blamed the server, e2e-tui.test.ts skipped its whole file and reported success. Invert it. `opencode-ai` is already a devDependency shipping the binary as platform optionalDependencies, so default to that and drop the env wiring: process.env.OPENCODE_BIN || resolve(import.meta.dir, '../node_modules/.bin/opencode') OPENCODE_BIN still wins when set, so pointing at a local OpenCode build is unchanged. plugin.test.ts needs nothing — it goes through PATH, which bun and npm both populate from node_modules/.bin. The `test` script collapses to `bun test --timeout 180000`, and the README loses its caveat paragraph rather than having to explain a footgun. Verified with no global opencode reachable at all (PATH stripped, HOME pointing at an empty dir): bun test test/integration.test.ts 2 pass / 0 fail (was: "Server did not start") bun test test/e2e-tui.test.ts runs the test (was: 0 pass / 3 skip) e2e-tui still fails its final magic-token assertion, unchanged by this and tracked separately — the point here is that it now runs instead of silently skipping. Both runs cleaned up their sandboxes; the account is back to zero opencode-* sandboxes.
Contributor
|
All contributors have signed the CLA. ✅ Thank you! |
… in workspaces The e2e test provisioned a healthy sandbox but the remote never produced an assistant reply. Two root causes: 1. Pinned OPENCODE_VERSION (1.15.13) ships no built-in "opencode" Zen provider models (e.g. opencode/big-pickle), so a session created with one silently produces no reply — the server is healthy but has no usable model. Bump the pin to 1.18.11 to match recent host builds. 2. opencode proxies every remote session request with `?directory=<host worktree>` in the URL, and the server resolves the directory from that query param before the x-opencode-directory header (searchParams "directory" || header || cwd). The host path (/tmp/…, /Users/…) does not exist in the sandbox, so the remote session dies immediately with `FileSystem.realPath ENOENT`. Symlink the host worktree path to REPO_PATH during create() so that query param resolves to the extracted repo. With both fixes the e2e test passes: the remote generates the reply and it even renders back in the host TUI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… symlink Reference opencode PR anomalyco/opencode#40136, which strips the host `directory` query param before proxying to the remote — the upstream fix for the bug the worktree->REPO_PATH symlink works around. Once it ships in a release and OPENCODE_VERSION is bumped to include it, the symlink can be removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
|
I have read the CLA Document and I hereby sign the CLA |
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.
Description
Adds
apps/opencode-workspaces-plugin: an OpenCode plugin that provisions Daytona sandboxes as remote workspaces via OpenCode's experimental workspace-adaptor API.Ported from daytonaio/daytona#4504, with Nx build config replaced by the standalone npm layout used elsewhere in this repo. Marked private — not published. Does not change
packages/opencode-plugin.Dependencies (app-only):
opencode-ai,@opencode-ai/sdk,@types/node,typescriptas devDependencies. Runtime deps unchanged from upstream.Upstream bugs in OpenCode
When OpenCode proxies a session request to a remote workspace, it forwards the host's directory (its cwd) as a query param. That path (e.g. /tmp/… on Linux, F:\proj on Windows) doesn't exist in the sandbox, so the remote resolves it against its own cwd, the session is created against a non-existent dir, and the first prompt crashes in realPath with ENOENT. The remote server stays healthy but never produces a reply — surfacing as an opaque 500 / "Failed to send prompt".
Related upstream PRs:
Until this is fixed, this PR uses the following workaround:
create()symlinks the host worktree path to the sandbox repo path so the forwarded directory resolves to the extracted repo.Related issue(s)
Scope
(e.g.
feat(pi-extension): ...,fix(adk-plugin): ...) — it becomes the squash-commitmessage that release-please reads.
Checks
plugin.test.ts,integration.test.tspass.e2e-tui.test.tspasses with the workaround mentioned above.Legal
DCO (
git commit -s); the sign-off matches thecommit author.
1 of 15 commits signed off — needs
git rebase --signoff main.Contributor License Agreement.
On my first PR, the CLA assistant will comment and I will reply to sign (once).