Skip to content

OpenCode

Image updates

Ready-made agent images are updated every morning to include the current CLI version. To use a fixed image version, pin a template version.

OpenCode is an AI agent that supports multiple models and providers. It works with files and runs commands inside an AgentBox sandbox. The ready-made opencode template already includes the agent CLI. Your application supplies the task, data, and credentials for the selected provider.

Connect

Install the AgentBox SDK and set AGENTBOX_API_KEY. This example calls a model through OpenRouter: it reads OPENROUTER_API_KEY and passes it into the sandbox. The selected model is openrouter/qwen/qwen3-coder-next. When switching providers, change the model and the provider's authentication configuration together.

To access the model, you can use the AgentBox HTTPS proxy at https://sandbox-proxy.agentbox.ru:65181.

The examples pass HTTPS_PROXY, HTTP_PROXY, and NO_PROXY only to the agent command and its child processes. These are process-wide network settings: other agent requests may also use the proxy. NO_PROXY keeps connections to localhost and 127.0.0.1 direct.

Triage the support queue

The inputs are three requests in tickets.csv and priority rules in policy.md. Duplicate charges have high priority and go to billing. Delivery delays have normal priority and go to logistics. The agent must not promise a refund or a delivery date without confirmation.

mjs
import { Sandbox } from "@abox-dev/sdk";

const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) throw new Error("OPENROUTER_API_KEY is required");
const proxy =
  process.env.SANDBOX_PROXY_URL ?? "https://sandbox-proxy.agentbox.ru:65181";

const sandbox = await Sandbox.create("opencode", {
  timeoutMs: 600_000,
  envs: {
    OPENROUTER_API_KEY: apiKey,
    OPENCODE_CONFIG_CONTENT: '{"autoupdate":false,"permission":"allow"}',
  },
});
try {
  await sandbox.files.makeDir("/home/user/work");
  await sandbox.files.write(
    "/home/user/work/tickets.csv",
    "id,topic,message\n101,delivery,Order arrived two days late\n102,payment,Card charged twice for one order\n103,delivery,Tracking has not changed for three days\n",
  );
  await sandbox.files.write(
    "/home/user/work/policy.md",
    "Payment duplicates: high priority, route to billing. Delivery delays: normal priority, ask logistics. Never promise a refund or a delivery date without confirmation.\n",
  );
  await sandbox.commands.run(
    "opencode run --auto --model openrouter/qwen/qwen3-coder-next 'Read tickets.csv. Write triage.md with one row per ticket: id, priority, reason, and a draft reply. Apply policy.md.'",
    {
      envs: {
        HTTPS_PROXY: proxy,
        HTTP_PROXY: proxy,
        NO_PROXY: "localhost,127.0.0.1",
      },
      cwd: "/home/user/work",
      timeoutMs: 300_000,
      onStderr: (chunk) => process.stderr.write(chunk),
    },
  );
  console.log(
    "triage.md\n" + (await sandbox.files.read("/home/user/work/triage.md")),
  );
} finally {
  await sandbox.kill();
}

opencode run processes a task without a terminal interface. permission: "allow" in OPENCODE_CONFIG_CONTENT allows all tools, and --auto enables automatic approval of permission requests. AgentBox provides isolation. The agent can read and change sandbox files. It writes triage.md, which your application reads before stopping the sandbox.

OPENCODE_CONFIG_CONTENT disables CLI updates during execution. Also pin a template version when you need a reproducible agent version.

Output and follow-up work

The example processes the tickets and displays events while the agent works. The application then supplies a billing update: the duplicate charge for ticket 102 is confirmed and case BILL-204 has been opened. The agent continues the same session and updates the reply in triage.md, preserving the other tickets.

mjs
import { Sandbox } from "@abox-dev/sdk";

const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) throw new Error("OPENROUTER_API_KEY is required");
const proxy =
  process.env.SANDBOX_PROXY_URL ?? "https://sandbox-proxy.agentbox.ru:65181";

const sandbox = await Sandbox.create("opencode", {
  timeoutMs: 600_000,
  envs: {
    OPENROUTER_API_KEY: apiKey,
    OPENCODE_CONFIG_CONTENT: '{"autoupdate":false,"permission":"allow"}',
  },
});
try {
  await sandbox.files.makeDir("/home/user/work");
  await sandbox.files.write(
    "/home/user/work/tickets.csv",
    "id,topic,message\n101,delivery,Order arrived two days late\n102,payment,Card charged twice for one order\n103,delivery,Tracking has not changed for three days\n",
  );
  await sandbox.files.write(
    "/home/user/work/policy.md",
    "Payment duplicates: high priority, route to billing. Delivery delays: normal priority, ask logistics. Never promise a refund or a delivery date without confirmation.\n",
  );
  async function execute(prompt, sessionId) {
    let pending = "";
    let result;
    let failure;
    let nextSessionId;
    function event(message) {
      if (message.sessionID) nextSessionId = message.sessionID;
      if (message.type === "text") console.log(message.part.text);
      if (message.type === "tool_use")
        console.log("Tool:", message.part.tool, message.part.state.status);
      if (message.type === "step_finish") result = message.part;
      if (message.type === "error")
        failure = new Error(
          message.error?.data?.message ?? JSON.stringify(message.error),
        );
    }
    function onStdout(chunk) {
      pending += chunk;
      let newline;
      while ((newline = pending.indexOf("\n")) !== -1) {
        const line = pending.slice(0, newline);
        pending = pending.slice(newline + 1);
        if (line.trim()) event(JSON.parse(line));
      }
    }
    const handle = await sandbox.commands.run(
      "opencode run --auto --model openrouter/qwen/qwen3-coder-next --format json" +
        (sessionId ? ' --session "$AGENT_SESSION_ID"' : ""),
      {
        cwd: "/home/user/work",
        timeoutMs: 300_000,
        background: true,
        stdin: true,
        envs: {
          OPENROUTER_API_KEY: apiKey,
          HTTPS_PROXY: proxy,
          HTTP_PROXY: proxy,
          NO_PROXY: "localhost,127.0.0.1",
          AGENT_SESSION_ID: sessionId ?? "",
        },
        onStdout,
        onStderr: (chunk) => process.stderr.write(chunk),
      },
    );
    await handle.sendStdin(prompt);
    await handle.closeStdin();
    await handle.wait();
    if (pending.trim()) event(JSON.parse(pending));
    if (failure) throw failure;
    if (result?.reason !== "stop")
      throw new Error("OpenCode did not finish normally: " + result?.reason);
    if (!nextSessionId) throw new Error("OpenCode did not return a session ID");
    console.log("\nSession:", nextSessionId);
    return nextSessionId;
  }
  const sessionId = await execute(
    "Read tickets.csv. Write triage.md with one row per ticket: id, priority, reason, and a draft reply. Apply policy.md.",
  );
  await execute(
    "Billing confirmed a duplicate charge for ticket 102 and opened case BILL-204. Update its draft reply in triage.md with this case number. Keep the other tickets and do not promise a refund date.",
    sessionId,
  );
  console.log(
    "triage.md\n" + (await sandbox.files.read("/home/user/work/triage.md")),
  );
} finally {
  await sandbox.kill();
}

--format json emits one event per line. text contains a completed text block in part.text. tool_use reports a finished tool call and its status. This CLI mode does not stream individual text tokens.

The handler saves sessionID from the events and passes it to --session for the second request. It also handles error and checks the reason in the last step_finish: stop means the model finished its answer. Intermediate steps can end with tool calls. The file is read after the command succeeds, then the sandbox and its local history are deleted.

See the OpenCode CLI reference for other options.

Errors and cleanup

Do not present an empty result as completed work after a key, model, or network failure. Handle command errors and retain diagnostics without secrets. A missing triage.md also means the expected deliverable was not produced.

The command has a five-minute limit and the sandbox a ten-minute lifetime. After reading the file, finally stops the sandbox and releases resources.