|
1 | | -/** |
2 | | - * @createos/opencode — run OpenCode's tools inside a remote, ephemeral CreateOS Sandbox. |
3 | | - * |
4 | | - * CLI-only: every operation goes through `createos` CLI. No HTTP client, |
5 | | - * no API key env vars — just `createos login` and go. |
6 | | - */ |
7 | | - |
8 | | -import type { Plugin } from "@opencode-ai/plugin"; |
9 | | -import * as cli from "./src/cli.ts"; |
10 | | -import { createTools, type ToolSandbox } from "./src/tools.ts"; |
11 | | -import { shortId } from "./src/util.ts"; |
12 | | - |
13 | | -interface ActiveSandbox { |
14 | | - sandboxId: string; |
15 | | - cwd: string; |
16 | | -} |
17 | | - |
18 | | -export const CreateOSPlugin: Plugin = async ({ project, client, $, directory }) => { |
19 | | - let active: ActiveSandbox | null = null; |
20 | | - let initPromise: Promise<void> | null = null; |
21 | | - const hostCwd = directory; |
22 | | - |
23 | | - await client.app.log({ |
24 | | - body: { service: "createos", level: "info", message: "Plugin initialized" }, |
25 | | - }); |
26 | | - |
27 | | - if (process.env.CREATEOS_ENABLED === "false") { |
28 | | - await client.app.log({ |
29 | | - body: { service: "createos", level: "info", message: "Disabled via CREATEOS_ENABLED=false" }, |
30 | | - }); |
31 | | - return {}; |
32 | | - } |
33 | | - |
34 | | - // Lazy sandbox init — triggered on first tool call |
35 | | - async function ensureSandbox(): Promise<ActiveSandbox> { |
36 | | - if (active) return active; |
37 | | - |
38 | | - if (!initPromise) { |
39 | | - initPromise = (async () => { |
40 | | - if (!(await cli.isCreateOSInstalled($))) { |
41 | | - await client.app.log({ |
42 | | - body: { service: "createos", level: "info", message: "CLI not found — installing..." }, |
43 | | - }); |
44 | | - if (!(await cli.autoInstallCLI($))) { |
45 | | - throw new Error( |
46 | | - "Failed to install CreateOS CLI. Run: curl -sfL https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh | sh", |
47 | | - ); |
48 | | - } |
49 | | - } |
50 | | - |
51 | | - if (!(await cli.isLoggedIn($))) { |
52 | | - throw new Error("Not logged in to CreateOS. Run: createos login"); |
53 | | - } |
54 | | - |
55 | | - const shape = process.env.CREATEOS_SHAPE ?? "s-2vcpu-2gb"; |
56 | | - const rootfs = process.env.CREATEOS_ROOTFS; |
57 | | - const networkFlag = process.env.CREATEOS_NETWORKS; |
58 | | - const networks = networkFlag |
59 | | - ? networkFlag |
60 | | - .split(",") |
61 | | - .map((n) => n.trim()) |
62 | | - .filter(Boolean) |
63 | | - : undefined; |
64 | | - |
65 | | - await client.app.log({ |
66 | | - body: { service: "createos", level: "info", message: `Creating sandbox (${shape})...` }, |
67 | | - }); |
68 | | - |
69 | | - const sandbox = await cli.createSandbox($, { |
70 | | - shape, |
71 | | - rootfs, |
72 | | - ingress: true, |
73 | | - networks, |
74 | | - name: `opencode-${shortId(project?.id ?? "session")}`, |
75 | | - }); |
76 | | - |
77 | | - const cwd = "/root/workspace"; |
78 | | - await cli.sandboxExec($, sandbox.id, `mkdir -p ${cwd}`); |
79 | | - |
80 | | - active = { sandboxId: sandbox.id, cwd }; |
81 | | - |
82 | | - await client.app.log({ |
83 | | - body: { |
84 | | - service: "createos", |
85 | | - level: "info", |
86 | | - message: `Sandbox ready: ${shortId(sandbox.id)} (${shape})${sandbox.ingress_url_template ? ` · ingress: ${sandbox.ingress_url_template}` : ""}`, |
87 | | - }, |
88 | | - }); |
89 | | - })(); |
90 | | - } |
91 | | - |
92 | | - await initPromise; |
93 | | - if (!active) throw new Error("Sandbox initialization failed"); |
94 | | - return active; |
95 | | - } |
96 | | - |
97 | | - // Build tools with lazy init wrapper |
98 | | - const getActive = (): ToolSandbox | null => active; |
99 | | - const baseTools = createTools($, getActive); |
100 | | - |
101 | | - const tools: Record<string, any> = {}; |
102 | | - for (const [name, def] of Object.entries(baseTools)) { |
103 | | - const original = (def as any).execute; |
104 | | - tools[name] = { |
105 | | - ...def, |
106 | | - execute: async (args: any, ctx: any) => { |
107 | | - await ensureSandbox(); |
108 | | - return original(args, ctx); |
109 | | - }, |
110 | | - }; |
111 | | - } |
112 | | - |
113 | | - return { |
114 | | - "experimental.session.compacting": async (_input: any, output: any) => { |
115 | | - if (!active) return; |
116 | | - output.context.push( |
117 | | - `## CreateOS Sandbox Environment\n` + |
118 | | - `Sandbox: ${active.sandboxId}\n` + |
119 | | - `Cwd: ${active.cwd}\n` + |
120 | | - `Host dir: ${hostCwd}\n` + |
121 | | - `All tools run remotely in this sandbox.\n` + |
122 | | - `\n` + |
123 | | - `Quick rules:\n` + |
124 | | - `- Work with a finish line (a build, a test suite, a script) → sandbox_offload dir="${hostCwd}" command="…". ` + |
125 | | - `ONE call: it creates the box, stages the dir, runs, and destroys the box. Do not hand-roll that out of ` + |
126 | | - `sandbox_create + sandbox_exec — that drops egress restriction, the keepalive, and the guaranteed destroy.\n` + |
127 | | - `- Several variants of that at once (shards, a config matrix) → sandbox_fanout\n` + |
128 | | - `- Untrusted code or any ad-hoc script/snippet you would otherwise run locally → sandbox_run_code (code + lang)\n` + |
129 | | - `- "mount/sync this dir" → sandbox_sync local_dir="${hostCwd}" remote_dir="/root/project"\n` + |
130 | | - `- Port access → sandbox_preview_url (public URL) > sandbox_tunnel (localhost) > device VPN (last resort)\n` + |
131 | | - `- Multi-node → sandbox_network_create + sandbox_create with network + sandbox_exec on other sandboxes\n` + |
132 | | - `- Questions about CreateOS Sandbox itself (REST API, SDKs, CLI, limits) → fetch the matching page listed in ` + |
133 | | - `https://createos.sh/docs/llms.txt (under /Sandbox/); every page is raw markdown at https://createos.sh/docs<path>.md`, |
134 | | - ); |
135 | | - }, |
136 | | - |
137 | | - event: async ({ event }: { event: { type: string } }) => { |
138 | | - if (event.type === "session.deleted" && active) { |
139 | | - try { |
140 | | - await cli.cleanupTempKey($); |
141 | | - } catch {} |
142 | | - try { |
143 | | - await cli.destroySandbox($, active.sandboxId); |
144 | | - } catch {} |
145 | | - active = null; |
146 | | - } |
147 | | - }, |
148 | | - |
149 | | - tool: tools, |
150 | | - }; |
151 | | -}; |
| 1 | +export { default } from "./src/plugin.ts"; |
0 commit comments