8000
Skip to content

Add opencode-sandbox example - #23

Draft
opencolin wants to merge 2 commits into
examples/codex-sandboxfrom
examples/opencode-sandbox
Draft

Add opencode-sandbox example#23
opencolin wants to merge 2 commits into
examples/codex-sandboxfrom
examples/opencode-sandbox

Conversation

@opencolin
Copy link
Copy Markdown
Collaborator

CreateOptions has shipped enableOpenCode and openCodeProvider for a while and the cookbook has never mentioned either. This documents them.

enableOpenCode: true bakes the OpenCode CLI into the guest before the sandbox reports ready, so unlike #20 and #22 this example has no install step at all. It starts opencode serve against a cloned repo and exposes it, so one public HTTPS URL is both OpenCode's web UI (/app) and its JSON API (/session, /agent, /project/current, /doc).

Verification

Ran node verify.mjs locally against the live API:

✓ opencode-sandbox: create + clone → opencode 1.17.20 serve → exposePort → 401 unauthed, 200 → /home/tenki/repo → dispose

Exit code 0, no model key needed. Asserts real values: opencode --version matches semver (proving the bake, since nothing installs it), an unauthenticated request to the public URL returns exactly 401, and the authenticated one returns worktree: "/home/tenki/repo" with vcs: "git" — which only holds if the clone, the server, the gateway, and the auth are all working.

Everything below was measured in a live sandbox

  • enableOpenCode: true puts opencode 1.17.20 at /usr/local/bin/opencode, and also starts a ttyd console on port 7681.
  • Port 7681 cannot be exposed. exposePort(7681) fails with [invalid_argument] port 7681 cannot be exposed as a preview — it's Tenki's own console. Any other port is fine; the example uses 4096.
  • --hostname 0.0.0.0 is required. opencode serve defaults to 127.0.0.1, which the gateway can't reach.
  • openCodeProvider: { apiKey, baseUrl } arrives in the guest as the env vars OPENCODE_API_KEY and OPENCODE_PROVIDER_BASE_URL — no config files are written.

The security bit, which is why this PR is worth a careful read

Started without a password, the server logs OPENCODE_SERVER_PASSWORD is not set; server is unsecured and answers every caller. Combined with exposePort, that publishes an unauthenticated coding agent with a shell to the internet. I hit that in testing before writing a line of the example.

So run.mjs generates a random 24-byte password, and verify.mjs asserts the 401 before it asserts anything else, so the example cannot quietly regress into shipping an open agent. Auth is HTTP Basic (any username, that password); a Bearer token is rejected — both confirmed live.

Scope

Adds one row to Developer tools, next to Claude Code and Codex. Touches nothing else outside examples/opencode-sandbox/.

Stacked on examples/codex-sandbox (#22) → #21#20, since all four add a row to the root README table. Review order: #20#21#22 → this.


Generated by Claude Code

claude added 2 commits August 17, 2026 23:59
Documents enableOpenCode, a first-party Tenki feature the cookbook had no
coverage of: it bakes the OpenCode CLI into the guest before the sandbox
reports ready, so this example has no install step at all.

Runs `opencode serve` against a cloned repo and exposes it on a public
preview URL, so the same URL is both OpenCode's web UI (/app) and its JSON
API. Measured gotchas are documented: port 7681 is reserved by Tenki's own
ttyd console and cannot be exposed, --hostname 0.0.0.0 is required, and
OPENCODE_SERVER_PASSWORD must be set or the server is open to the internet.

verify.mjs asserts an unauthenticated request gets 401 before asserting the
authenticated one returns the checkout, so the example cannot regress into
publishing an unsecured agent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uz85xGA4zWSge1BtiDNnSN
run.mjs printed a preview URL and told you to open it in a browser, then
`await using` terminated the sandbox as the scope ended — the URL 404'd
before anyone could click it. Caught by running run.mjs for the first time.

It now creates the sandbox without `await using`, holds the process open,
and disposes on SIGINT, so the URL stays reachable until Ctrl-C.
idleTimeoutMinutes: 30 reaps a sandbox that gets forgotten.

Holding the loop open needs a ref'd handle: an unsettled top-level await
makes Node exit 13 with "Detected unsettled top-level await".

Dropping `await using` also drops the Node 24 requirement for this example;
run.mjs now parses on Node 22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uz85xGA4zWSge1BtiDNnSN
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

0