codebay 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mochi/manifest.json +778 -646
- package/.mochi/svelte-client/HydratableIsland-6y9p903x.js +1 -0
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-nv6yzk0x.js +2 -0
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-ry83ck8a.js +14 -0
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-vjt4jscf.js +3 -0
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-1jdrk8x6.js +48 -0
- package/.mochi/svelte-client/{chunk-spk1b2r4.js → chunk-0vqvn81s.js} +1 -1
- package/.mochi/svelte-client/chunk-2dc6352a.js +1 -0
- package/.mochi/svelte-client/chunk-3cxaw5hr.js +2 -0
- package/.mochi/svelte-client/chunk-3jqc40sp.js +2 -0
- package/.mochi/svelte-client/chunk-3pgw6t5j.js +4 -0
- package/.mochi/svelte-client/chunk-7g3xt6ez.js +2 -0
- package/.mochi/svelte-client/chunk-b5s3mpg0.js +1 -0
- package/.mochi/svelte-client/chunk-bhseezf3.js +2 -0
- package/.mochi/svelte-client/chunk-bhwkq8er.js +1 -0
- package/.mochi/svelte-client/chunk-jm3ge348.js +1 -0
- package/.mochi/svelte-client/{chunk-0mc09cyb.js → chunk-sxcr9gzp.js} +1 -1
- package/.mochi/svelte-client/{chunk-ccfh8hxz.js → chunk-tss2473w.js} +1 -1
- package/.mochi/svelte-compile/{App-94xsdn0w.server.js → App-8mpgrxza.server.js} +470 -271
- package/.mochi/svelte-compile/{Avatars-fnccw47h.server.js → Avatars-y5nf6q25.server.js} +111 -110
- package/.mochi/svelte-compile/{DefaultError-je52k17d.server.js → DefaultError-rrjye0s2.server.js} +1 -2
- package/.mochi/svelte-compile/Instance-3sbzyp0g.server.js +695 -0
- package/.mochi/svelte-compile/{Settings-a7b11v90.server.js → Settings-ysvh5vhh.server.js} +838 -607
- package/.mochi/svelte-compile/chunk-7zaw9t5a.js +25 -0
- package/.mochi/svelte-compile/chunk-878xcmp4.js +65 -0
- package/.mochi/svelte-compile/chunk-d2z3qxxe.js +28 -0
- package/.mochi/svelte-compile/{chunk-60mpagr9.js → chunk-gnf5cnct.js} +661 -22
- package/.mochi/svelte-compile/{chunk-w0hca7va.js → chunk-jq7jj8jd.js} +18 -2
- package/.mochi/svelte-compile/chunk-vr0e32mv.js +24 -0
- package/.mochi/svelte-compile/{chunk-apdte4nh.js → chunk-xmygdsj4.js} +35 -612
- package/.mochi/svelte-compile/{chunk-pqb403fh.js → chunk-zk4a9zms.js} +28 -31
- package/.mochi/svelte-css/AgentLogBox-38adby5iygny7.css +1 -0
- package/.mochi/svelte-css/DropZone-1zeitd761uwyd.css +1 -0
- package/.mochi/svelte-css/IdeBar-d3un7kuigxc5.css +1 -0
- package/.mochi/svelte-css/{InstanceCard-25z7nas7y98zk.css → InstanceCard-3vxozrp9uoads.css} +1 -1
- package/.mochi/svelte-css/InstanceView-3ucotech5097w.css +1 -0
- package/.mochi/svelte-css/{SettingsView-bccw34agd2bj.css → SettingsView-2400myxb9ot56.css} +1 -1
- package/README.md +43 -3
- package/migrations/0009_agent_runs.sql +24 -0
- package/migrations/0010_agent_run_model.sql +2 -0
- package/migrations/0011_agent_run_rejected_structured_output.sql +2 -0
- package/package.json +13 -9
- package/src/__fixtures__/IdeBarStrip.svelte +41 -0
- package/src/avatars/dog.ts +1 -1
- package/src/avatars/lightning.ts +8 -9
- package/src/avatars/rocket.ts +4 -5
- package/src/components/AgentLogBox.svelte +458 -0
- package/src/components/AppShell.svelte +112 -1
- package/src/components/DropZone.svelte +77 -0
- package/src/components/FolderBrowser.svelte +13 -5
- package/src/components/HealthBox.svelte +6 -5
- package/src/components/IdeBar.svelte +222 -76
- package/src/components/InstanceCard.svelte +38 -0
- package/src/components/InstanceView.svelte +83 -2
- package/src/components/SettingsView.svelte +273 -0
- package/src/components/TerminalPane.svelte +54 -0
- package/src/components/UiShowcase.svelte +23 -3
- package/src/container-injections/claude-code-install.ts +1 -1
- package/src/container-injections/claude-history.ts +52 -0
- package/src/container-injections/claude-statusline.ts +4 -1
- package/src/container-injections/github-credentials.ts +12 -1
- package/src/container-injections/tmux.ts +6 -2
- package/src/index.ts +35 -7
- package/src/lib/agent-run-stream.ts +286 -0
- package/src/lib/agent-runs.server.ts +735 -0
- package/src/lib/auth.server.ts +11 -2
- package/src/lib/build-freshness.server.ts +71 -0
- package/src/lib/config.server.ts +36 -7
- package/src/lib/container-files.server.ts +1 -1
- package/src/lib/data-dir-lock.server.ts +78 -0
- package/src/lib/db.server.ts +124 -1
- package/src/lib/devcontainer.server.ts +93 -16
- package/src/lib/docker-client.server.ts +11 -5
- package/src/lib/drives.ts +11 -0
- package/src/lib/exec.server.ts +66 -5
- package/src/lib/injections.server.ts +5 -1
- package/src/lib/instances.server.ts +66 -7
- package/src/lib/log-capture.server.ts +63 -23
- package/src/lib/mcp-auth.server.ts +52 -0
- package/src/lib/osc52.ts +18 -0
- package/src/lib/picker.server.ts +23 -2
- package/src/lib/sandbox-ops.server.ts +263 -0
- package/src/lib/upload.ts +32 -0
- package/src/lib/uploads.server.ts +164 -0
- package/src/mcp/routes.server.ts +25 -0
- package/src/mcp/server.server.ts +55 -0
- package/src/mcp/tools.server.ts +660 -0
- package/src/pages/Instance.svelte +6 -2
- package/src/pages/Settings.svelte +15 -0
- package/src/routes.ts +79 -3
- package/src/types.ts +17 -0
- package/.mochi/svelte-client/HydratableIsland-zrdsrsmg.js +0 -1
- package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-v7y7dh53.js +0 -2
- package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-tmwd6fpb.js +0 -14
- package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-m7t64dbh.js +0 -1
- package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-4a7ann1f.js +0 -40
- package/.mochi/svelte-client/chunk-5jwfs4p9.js +0 -2
- package/.mochi/svelte-client/chunk-6qg4q98a.js +0 -2
- package/.mochi/svelte-client/chunk-phf7v5jj.js +0 -4
- package/.mochi/svelte-client/chunk-tmeh70wg.js +0 -2
- package/.mochi/svelte-client/chunk-vs1yjbqt.js +0 -2
- package/.mochi/svelte-client/chunk-x0m3k0rn.js +0 -1
- package/.mochi/svelte-client/chunk-y7f4jbdm.js +0 -2
- package/.mochi/svelte-compile/Instance-knnkcn2p.server.js +0 -377
- package/.mochi/svelte-compile/chunk-ed46m58e.js +0 -22
- package/.mochi/svelte-compile/chunk-jx6h1xzp.js +0 -20
- package/.mochi/svelte-css/IdeBar-1mtgemqfuvqzi.css +0 -1
- package/.mochi/svelte-css/InstanceView-32j12hyg8v9u0.css +0 -1
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
import type { McpServer } from 'tmcp';
|
|
3
|
+
import { tool } from 'tmcp/utils';
|
|
4
|
+
import {
|
|
5
|
+
addForwardedPort,
|
|
6
|
+
createInstance,
|
|
7
|
+
deleteInstance,
|
|
8
|
+
listInstances,
|
|
9
|
+
rebuildInstance,
|
|
10
|
+
removeForwardedPort,
|
|
11
|
+
renameInstance,
|
|
12
|
+
sanitizeInstance,
|
|
13
|
+
startInstance,
|
|
14
|
+
stopInstance,
|
|
15
|
+
subscribeLogs
|
|
16
|
+
} from '../lib/instances.server.ts';
|
|
17
|
+
import {
|
|
18
|
+
getInstance,
|
|
19
|
+
getRun,
|
|
20
|
+
listForwards,
|
|
21
|
+
listRuns,
|
|
22
|
+
openRunFor,
|
|
23
|
+
type AgentRunRow,
|
|
24
|
+
type InstanceRow
|
|
25
|
+
} from '../lib/db.server.ts';
|
|
26
|
+
import {
|
|
27
|
+
PROMPT_MAX_BYTES,
|
|
28
|
+
pollRunNow,
|
|
29
|
+
readRunLog,
|
|
30
|
+
runDetail,
|
|
31
|
+
startRun,
|
|
32
|
+
stopRun
|
|
33
|
+
} from '../lib/agent-runs.server.ts';
|
|
34
|
+
import {
|
|
35
|
+
WRITE_FILE_MAX_BYTES,
|
|
36
|
+
createPr,
|
|
37
|
+
execCommand,
|
|
38
|
+
gitDiff,
|
|
39
|
+
gitPush,
|
|
40
|
+
readWorkspaceFile,
|
|
41
|
+
writeWorkspaceFile
|
|
42
|
+
} from '../lib/sandbox-ops.server.ts';
|
|
43
|
+
import { currentHealthSnapshots } from '../lib/health.server.ts';
|
|
44
|
+
import { proxyPathFor } from '../lib/proxy.server.ts';
|
|
45
|
+
import { PUBLIC_ORIGIN } from '../lib/config.server.ts';
|
|
46
|
+
import { CLAUDE_PERMISSION_MODES, normalizeMode } from '../types.ts';
|
|
47
|
+
|
|
48
|
+
/** Long-polling `get_run` past this would start tripping MCP clients' own request timeouts. */
|
|
49
|
+
const MAX_WAIT_SECONDS = 60;
|
|
50
|
+
|
|
51
|
+
const sandboxId = v.pipe(v.string(), v.minLength(1), v.description('The sandbox id.'));
|
|
52
|
+
|
|
53
|
+
const containerPort = v.pipe(
|
|
54
|
+
v.number(),
|
|
55
|
+
v.integer(),
|
|
56
|
+
v.minValue(1),
|
|
57
|
+
v.maxValue(65535),
|
|
58
|
+
v.description('The port inside the container.')
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
function requireInstance(id: string) {
|
|
62
|
+
const row = getInstance(id);
|
|
63
|
+
if (!row) throw new Error(`No sandbox with id ${id}`);
|
|
64
|
+
return row;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A boot owns its row until the last injection lands, so nothing else may move it before then. */
|
|
68
|
+
function requireSettled(id: string) {
|
|
69
|
+
const row = requireInstance(id);
|
|
70
|
+
if (row.status === 'creating') {
|
|
71
|
+
throw new Error('the sandbox is still building — poll get_sandbox until status is "running"');
|
|
72
|
+
}
|
|
73
|
+
return row;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Every tool that touches the container needs one that is actually up. */
|
|
77
|
+
function requireRunning(id: string) {
|
|
78
|
+
const row = requireSettled(id);
|
|
79
|
+
if (!row.container_id || row.status !== 'running') {
|
|
80
|
+
throw new Error(`the sandbox is ${row.status}, not running`);
|
|
81
|
+
}
|
|
82
|
+
return row;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The IDE link is what makes a sandbox inspectable by a human, so every sandbox payload carries it. */
|
|
86
|
+
const ideUrl = (id: string) => `${PUBLIC_ORIGIN}${proxyPathFor(id)}`;
|
|
87
|
+
|
|
88
|
+
const healthFor = (id: string) => currentHealthSnapshots().find((s) => s.id === id)?.health ?? null;
|
|
89
|
+
|
|
90
|
+
/** Forwards ride along because add/remove_port_forward hand back the sandbox as their receipt. */
|
|
91
|
+
function sandboxPayload(row: InstanceRow) {
|
|
92
|
+
const open = new Set(healthFor(row.id)?.openPorts ?? []);
|
|
93
|
+
return {
|
|
94
|
+
...sanitizeInstance(row),
|
|
95
|
+
ide_url: ideUrl(row.id),
|
|
96
|
+
forwarded_ports: listForwards(row.id).map((f) => ({
|
|
97
|
+
container_port: f.container_port,
|
|
98
|
+
host_port: f.host_port,
|
|
99
|
+
open: open.has(f.container_port)
|
|
100
|
+
}))
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The prompt is left out: the caller wrote it, and it can be as large as the carrier allows. */
|
|
105
|
+
function runPayload(run: AgentRunRow) {
|
|
106
|
+
const { id, instance_id, prompt: _prompt, ...detail } = runDetail(run);
|
|
107
|
+
return { run_id: id, sandbox_id: instance_id, ...detail };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The run a stop/rebuild settled, re-read after the fact so the caller sees it as cancelled. */
|
|
111
|
+
function cancelledRun(open: AgentRunRow | null) {
|
|
112
|
+
return open ? runPayload(getRun(open.id)!) : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A failed call reaches the model as readable content rather than a JSON-RPC protocol error: the
|
|
117
|
+
* caller is an agent, and "that sandbox is still building" is information it can act on.
|
|
118
|
+
*/
|
|
119
|
+
function safe<T>(run: (input: T) => unknown) {
|
|
120
|
+
return async (input: T) => {
|
|
121
|
+
try {
|
|
122
|
+
return tool.text(JSON.stringify(await run(input), null, 2));
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return tool.error((err as Error).message);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const runOptions = {
|
|
130
|
+
resume_session_id: v.optional(
|
|
131
|
+
v.pipe(
|
|
132
|
+
v.string(),
|
|
133
|
+
v.description('Continue a previous run’s Claude session instead of starting fresh.')
|
|
134
|
+
)
|
|
135
|
+
),
|
|
136
|
+
model: v.optional(
|
|
137
|
+
v.pipe(
|
|
138
|
+
v.string(),
|
|
139
|
+
v.description(
|
|
140
|
+
'Model alias or id, e.g. "sonnet". Omit for the sandbox default. The run reports the id ' +
|
|
141
|
+
'Claude actually used as `model` once it starts.'
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
),
|
|
145
|
+
max_turns: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
|
|
146
|
+
json_schema: v.optional(
|
|
147
|
+
v.pipe(
|
|
148
|
+
v.string(),
|
|
149
|
+
v.description(
|
|
150
|
+
'A JSON Schema (as a string). When set, the run’s structured_output matches it. If no ' +
|
|
151
|
+
'payload ever validates the run fails with error "structured_output_failed", and its ' +
|
|
152
|
+
'result holds Claude’s plain-text answer with rejected_structured_output the last ' +
|
|
153
|
+
'payload the schema refused.'
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
),
|
|
157
|
+
permission_mode: v.optional(
|
|
158
|
+
v.pipe(
|
|
159
|
+
v.picklist(CLAUDE_PERMISSION_MODES),
|
|
160
|
+
v.description(
|
|
161
|
+
'Tightens what Claude may do. Omit for full autonomy, which is the default for MCP runs.'
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
),
|
|
165
|
+
timeout_minutes: v.optional(v.pipe(v.number(), v.minValue(1), v.maxValue(240)))
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
type RunOptionInput = v.InferInput<v.ObjectSchema<typeof runOptions, undefined>>;
|
|
169
|
+
|
|
170
|
+
// The byte limits are enforced at runtime (startRun, writeWorkspaceFile) and only documented here:
|
|
171
|
+
// a `v.check` can't be rendered into the JSON Schema an MCP client reads.
|
|
172
|
+
const promptSchema = (description: string) =>
|
|
173
|
+
v.pipe(
|
|
174
|
+
v.string(),
|
|
175
|
+
v.minLength(1),
|
|
176
|
+
v.description(
|
|
177
|
+
`${description} At most ${PROMPT_MAX_BYTES} bytes of UTF-8; refer to a workspace file for anything larger.`
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
function toStartOptions(input: RunOptionInput) {
|
|
182
|
+
return {
|
|
183
|
+
resumeSessionId: input.resume_session_id,
|
|
184
|
+
model: input.model,
|
|
185
|
+
maxTurns: input.max_turns,
|
|
186
|
+
jsonSchema: input.json_schema,
|
|
187
|
+
permissionMode: input.permission_mode,
|
|
188
|
+
timeoutMs: input.timeout_minutes ? input.timeout_minutes * 60_000 : undefined
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
193
|
+
|
|
194
|
+
export function registerTools(server: McpServer<v.GenericSchema>): void {
|
|
195
|
+
/** Registers a tool whose handler returns a plain payload; `safe` serialises it and traps errors. */
|
|
196
|
+
function define<E extends v.ObjectEntries>(
|
|
197
|
+
options: { name: string; description: string; schema: v.ObjectSchema<E, undefined> },
|
|
198
|
+
run: (input: v.InferInput<v.ObjectSchema<E, undefined>>) => unknown
|
|
199
|
+
): void {
|
|
200
|
+
server.tool(options, safe(run));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
define(
|
|
204
|
+
{
|
|
205
|
+
name: 'create_sandbox',
|
|
206
|
+
description:
|
|
207
|
+
'Create an isolated devcontainer sandbox from a Git repo URL or a local folder path on ' +
|
|
208
|
+
'the Codebay host. Returns immediately while the container builds in the background; poll ' +
|
|
209
|
+
'get_sandbox until status is "running". Pass a prompt to queue an agent run that starts ' +
|
|
210
|
+
'as soon as the sandbox is up.',
|
|
211
|
+
schema: v.object({
|
|
212
|
+
source: v.pipe(
|
|
213
|
+
v.string(),
|
|
214
|
+
v.minLength(1),
|
|
215
|
+
v.description('A Git repo URL (cloned) or an absolute local folder path (copied).')
|
|
216
|
+
),
|
|
217
|
+
name: v.optional(v.string()),
|
|
218
|
+
branch: v.optional(v.pipe(v.string(), v.description('Branch to check out after cloning.'))),
|
|
219
|
+
mode: v.optional(v.picklist(['ide', 'terminal'] as const)),
|
|
220
|
+
prompt: v.optional(promptSchema('Queues an agent run once the sandbox is running.')),
|
|
221
|
+
...runOptions
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
async (input) => {
|
|
225
|
+
const instance = await createInstance(input.source, input.name, {
|
|
226
|
+
branch: input.branch,
|
|
227
|
+
mode: input.mode ? normalizeMode(input.mode) : undefined
|
|
228
|
+
});
|
|
229
|
+
// The sandbox exists once createInstance returns, so a failed run must not swallow its id —
|
|
230
|
+
// the caller would retry and leak the first one.
|
|
231
|
+
let run: AgentRunRow | null = null;
|
|
232
|
+
let runError: string | undefined;
|
|
233
|
+
try {
|
|
234
|
+
if (input.prompt) run = startRun(instance, input.prompt, toStartOptions(input));
|
|
235
|
+
} catch (err) {
|
|
236
|
+
runError = err instanceof Error ? err.message : String(err);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
sandbox: sandboxPayload(instance),
|
|
240
|
+
run: run ? runPayload(run) : null,
|
|
241
|
+
...(runError ? { run_error: runError } : {}),
|
|
242
|
+
note: 'The sandbox is building. Poll get_sandbox until status is "running".'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
define(
|
|
248
|
+
{
|
|
249
|
+
name: 'list_sandboxes',
|
|
250
|
+
description:
|
|
251
|
+
'List every sandbox Codebay is managing, with its status and any run currently in flight.',
|
|
252
|
+
schema: v.object({
|
|
253
|
+
status: v.optional(v.picklist(['creating', 'running', 'stopped', 'error'] as const))
|
|
254
|
+
})
|
|
255
|
+
},
|
|
256
|
+
async (input) => {
|
|
257
|
+
const all = await listInstances();
|
|
258
|
+
const rows = input.status ? all.filter((i) => i.status === input.status) : all;
|
|
259
|
+
return {
|
|
260
|
+
sandboxes: rows.map((row) => ({ ...row, ide_url: ideUrl(row.id) }))
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
define(
|
|
266
|
+
{
|
|
267
|
+
name: 'get_sandbox',
|
|
268
|
+
description:
|
|
269
|
+
'Full detail for one sandbox: status, health probes, forwarded ports and its active run.',
|
|
270
|
+
schema: v.object({ sandbox_id: sandboxId })
|
|
271
|
+
},
|
|
272
|
+
async (input) => {
|
|
273
|
+
const row = requireInstance(input.sandbox_id);
|
|
274
|
+
const runs = listRuns(row.id, 5);
|
|
275
|
+
return {
|
|
276
|
+
sandbox: sandboxPayload(row),
|
|
277
|
+
health: healthFor(row.id),
|
|
278
|
+
runs: runs.map(runPayload)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
define(
|
|
284
|
+
{
|
|
285
|
+
name: 'delete_sandbox',
|
|
286
|
+
description:
|
|
287
|
+
'Destroy a sandbox: cancels any active run, removes the container and its workspace copy. ' +
|
|
288
|
+
'Mirrored run transcripts are kept.',
|
|
289
|
+
schema: v.object({ sandbox_id: sandboxId })
|
|
290
|
+
},
|
|
291
|
+
async (input) => {
|
|
292
|
+
requireInstance(input.sandbox_id);
|
|
293
|
+
await deleteInstance(input.sandbox_id);
|
|
294
|
+
return { deleted: input.sandbox_id };
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
define(
|
|
299
|
+
{
|
|
300
|
+
name: 'stop_sandbox',
|
|
301
|
+
description:
|
|
302
|
+
'Stop a sandbox’s container without destroying it: the workspace, its files and its run ' +
|
|
303
|
+
'history are kept, and start_sandbox brings it back. Any active run is cancelled first, ' +
|
|
304
|
+
'the same way the dashboard’s Stop does, and comes back as cancelled_run so the ' +
|
|
305
|
+
'cancellation is never silent. Synchronous: the returned sandbox already has its new ' +
|
|
306
|
+
'status ("stopped", or "error" with the reason if Docker refused).',
|
|
307
|
+
schema: v.object({ sandbox_id: sandboxId })
|
|
308
|
+
},
|
|
309
|
+
async (input) => {
|
|
310
|
+
const row = requireSettled(input.sandbox_id);
|
|
311
|
+
if (!row.container_id) throw new Error('the sandbox has no container to stop');
|
|
312
|
+
const open = openRunFor(row.id);
|
|
313
|
+
const sandbox = sandboxPayload(await stopInstance(row.id));
|
|
314
|
+
return { sandbox, cancelled_run: cancelledRun(open) };
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
define(
|
|
319
|
+
{
|
|
320
|
+
name: 'start_sandbox',
|
|
321
|
+
description:
|
|
322
|
+
'Start a stopped sandbox’s container again. Synchronous, unlike create_sandbox: it ' +
|
|
323
|
+
'returns once Docker has started the container, with status already "running" (or ' +
|
|
324
|
+
'"error", with the reason in `error`), so there is nothing to poll for the status itself. ' +
|
|
325
|
+
'The editor surface inside takes a few more seconds to come up; get_sandbox’s ' +
|
|
326
|
+
'health.codeServerAccessible reports when it has. run_agent, exec_command and the file ' +
|
|
327
|
+
'tools work as soon as status is "running".',
|
|
328
|
+
schema: v.object({ sandbox_id: sandboxId })
|
|
329
|
+
},
|
|
330
|
+
async (input) => {
|
|
331
|
+
const row = requireSettled(input.sandbox_id);
|
|
332
|
+
if (!row.container_id) throw new Error('the sandbox has no container to start');
|
|
333
|
+
return { sandbox: sandboxPayload(await startInstance(row.id)) };
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
define(
|
|
338
|
+
{
|
|
339
|
+
name: 'rebuild_sandbox',
|
|
340
|
+
description:
|
|
341
|
+
'Recreate a sandbox’s container from its devcontainer config while keeping the workspace ' +
|
|
342
|
+
'copy, so files edited inside survive. This is what applies add_port_forward / ' +
|
|
343
|
+
'remove_port_forward and re-runs the Codebay injections (credentials, hooks). Any active ' +
|
|
344
|
+
'run is cancelled and comes back as cancelled_run. Returns immediately with status ' +
|
|
345
|
+
'"creating" while the build runs in the background; poll get_sandbox until status is ' +
|
|
346
|
+
'"running" (or "error" — get_logs kind "boot" says why).',
|
|
347
|
+
schema: v.object({
|
|
348
|
+
sandbox_id: sandboxId,
|
|
349
|
+
no_cache: v.optional(
|
|
350
|
+
v.pipe(v.boolean(), v.description('Rebuild the image without Docker’s build cache.'))
|
|
351
|
+
)
|
|
352
|
+
})
|
|
353
|
+
},
|
|
354
|
+
(input) => {
|
|
355
|
+
const row = requireSettled(input.sandbox_id);
|
|
356
|
+
const open = openRunFor(row.id);
|
|
357
|
+
const sandbox = sandboxPayload(rebuildInstance(row.id, { noCache: input.no_cache }));
|
|
358
|
+
return {
|
|
359
|
+
sandbox,
|
|
360
|
+
cancelled_run: cancelledRun(open),
|
|
361
|
+
note: 'The sandbox is rebuilding. Poll get_sandbox until status is "running".'
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
define(
|
|
367
|
+
{
|
|
368
|
+
name: 'rename_sandbox',
|
|
369
|
+
description:
|
|
370
|
+
'Rename a sandbox. Names are unique, so a clash gets a " #2"-style suffix; the returned ' +
|
|
371
|
+
'sandbox carries the name actually applied.',
|
|
372
|
+
schema: v.object({ sandbox_id: sandboxId, name: v.pipe(v.string(), v.minLength(1)) })
|
|
373
|
+
},
|
|
374
|
+
(input) => {
|
|
375
|
+
requireInstance(input.sandbox_id);
|
|
376
|
+
return { sandbox: sandboxPayload(renameInstance(input.sandbox_id, input.name)) };
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
define(
|
|
381
|
+
{
|
|
382
|
+
name: 'add_port_forward',
|
|
383
|
+
description:
|
|
384
|
+
'Publish a port inside the sandbox (e.g. a dev server an agent started) on a host port of ' +
|
|
385
|
+
'the Codebay host, so a human can open it; Codebay picks the host port. Only persisted ' +
|
|
386
|
+
'here: the mapping goes live on the next rebuild_sandbox, and forwarded_ports[].open on ' +
|
|
387
|
+
'get_sandbox shows once the mapping is published (after a rebuild).',
|
|
388
|
+
schema: v.object({ sandbox_id: sandboxId, port: containerPort })
|
|
389
|
+
},
|
|
390
|
+
async (input) => {
|
|
391
|
+
requireInstance(input.sandbox_id);
|
|
392
|
+
const sandbox = sandboxPayload(await addForwardedPort(input.sandbox_id, input.port));
|
|
393
|
+
return {
|
|
394
|
+
sandbox,
|
|
395
|
+
forward: sandbox.forwarded_ports.find((f) => f.container_port === input.port) ?? null,
|
|
396
|
+
note: 'Forwards are applied by rebuild_sandbox; the port is not published until then.'
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
define(
|
|
402
|
+
{
|
|
403
|
+
name: 'remove_port_forward',
|
|
404
|
+
description:
|
|
405
|
+
'Drop a forwarded port. Like add_port_forward this only updates the persisted set; the ' +
|
|
406
|
+
'mapping is actually unpublished by the next rebuild_sandbox.',
|
|
407
|
+
schema: v.object({ sandbox_id: sandboxId, port: containerPort })
|
|
408
|
+
},
|
|
409
|
+
(input) => {
|
|
410
|
+
requireInstance(input.sandbox_id);
|
|
411
|
+
return {
|
|
412
|
+
sandbox: sandboxPayload(removeForwardedPort(input.sandbox_id, input.port)),
|
|
413
|
+
note: 'Forwards are applied by rebuild_sandbox; the port stays published until then.'
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
define(
|
|
419
|
+
{
|
|
420
|
+
name: 'run_agent',
|
|
421
|
+
description:
|
|
422
|
+
'Run Claude Code non-interactively against the sandbox and return a run handle. The run ' +
|
|
423
|
+
'continues in the background; poll get_run for progress and the final result. Only one ' +
|
|
424
|
+
'run at a time per sandbox.',
|
|
425
|
+
schema: v.object({
|
|
426
|
+
sandbox_id: sandboxId,
|
|
427
|
+
prompt: promptSchema('What Claude should do.'),
|
|
428
|
+
...runOptions
|
|
429
|
+
})
|
|
430
|
+
},
|
|
431
|
+
(input) => {
|
|
432
|
+
const row = requireInstance(input.sandbox_id);
|
|
433
|
+
return { run: runPayload(startRun(row, input.prompt, toStartOptions(input))) };
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
define(
|
|
438
|
+
{
|
|
439
|
+
name: 'get_run',
|
|
440
|
+
description:
|
|
441
|
+
'Status and result of an agent run. While it is still going you get the live session id, ' +
|
|
442
|
+
'model, turn count, cost and what Claude is doing right now. Set wait_seconds to block until it ' +
|
|
443
|
+
'finishes instead of polling.',
|
|
444
|
+
schema: v.object({
|
|
445
|
+
run_id: v.pipe(v.string(), v.minLength(1)),
|
|
446
|
+
wait_seconds: v.optional(
|
|
447
|
+
v.pipe(
|
|
448
|
+
v.number(),
|
|
449
|
+
v.integer(),
|
|
450
|
+
v.minValue(0),
|
|
451
|
+
v.maxValue(MAX_WAIT_SECONDS),
|
|
452
|
+
v.description('Block up to this long for the run to finish before answering.')
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
})
|
|
456
|
+
},
|
|
457
|
+
async (input) => {
|
|
458
|
+
if (!getRun(input.run_id)) throw new Error(`No run with id ${input.run_id}`);
|
|
459
|
+
const deadline = Date.now() + (input.wait_seconds ?? 0) * 1000;
|
|
460
|
+
let run = await pollRunNow(input.run_id);
|
|
461
|
+
while (
|
|
462
|
+
run &&
|
|
463
|
+
(run.status === 'running' || run.status === 'queued') &&
|
|
464
|
+
Date.now() < deadline
|
|
465
|
+
) {
|
|
466
|
+
await sleep(2000);
|
|
467
|
+
run = await pollRunNow(input.run_id);
|
|
468
|
+
}
|
|
469
|
+
return { run: runPayload(run!) };
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
define(
|
|
474
|
+
{
|
|
475
|
+
name: 'list_runs',
|
|
476
|
+
description: 'Run history for a sandbox, newest first.',
|
|
477
|
+
schema: v.object({
|
|
478
|
+
sandbox_id: sandboxId,
|
|
479
|
+
limit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100)))
|
|
480
|
+
})
|
|
481
|
+
},
|
|
482
|
+
(input) => {
|
|
483
|
+
requireInstance(input.sandbox_id);
|
|
484
|
+
return { runs: listRuns(input.sandbox_id, input.limit ?? 20).map(runPayload) };
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
define(
|
|
489
|
+
{
|
|
490
|
+
name: 'stop_run',
|
|
491
|
+
description:
|
|
492
|
+
'Cancel an agent run. Sends SIGINT first so Claude can end its turn cleanly, then ' +
|
|
493
|
+
'escalates if it does not exit.',
|
|
494
|
+
schema: v.object({ run_id: v.pipe(v.string(), v.minLength(1)) })
|
|
495
|
+
},
|
|
496
|
+
async (input) => ({ run: runPayload(await stopRun(input.run_id)) })
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
define(
|
|
500
|
+
{
|
|
501
|
+
name: 'get_diff',
|
|
502
|
+
description:
|
|
503
|
+
'The sandbox workspace’s uncommitted changes as a unified diff, plus a porcelain ' +
|
|
504
|
+
'status summary and the current branch. This is how you see what an agent run actually did.',
|
|
505
|
+
schema: v.object({
|
|
506
|
+
sandbox_id: sandboxId,
|
|
507
|
+
base: v.optional(
|
|
508
|
+
v.pipe(v.string(), v.description('Diff against this ref instead of the working tree.'))
|
|
509
|
+
),
|
|
510
|
+
staged: v.optional(v.boolean())
|
|
511
|
+
})
|
|
512
|
+
},
|
|
513
|
+
async (input) =>
|
|
514
|
+
await gitDiff(requireRunning(input.sandbox_id), {
|
|
515
|
+
base: input.base,
|
|
516
|
+
staged: input.staged
|
|
517
|
+
})
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
define(
|
|
521
|
+
{
|
|
522
|
+
name: 'read_file',
|
|
523
|
+
description:
|
|
524
|
+
'Read a file from the sandbox workspace, by path relative to the workspace root.',
|
|
525
|
+
schema: v.object({ sandbox_id: sandboxId, path: v.pipe(v.string(), v.minLength(1)) })
|
|
526
|
+
},
|
|
527
|
+
async (input) => ({
|
|
528
|
+
path: input.path,
|
|
529
|
+
content: await readWorkspaceFile(requireRunning(input.sandbox_id), input.path)
|
|
530
|
+
})
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
define(
|
|
534
|
+
{
|
|
535
|
+
name: 'write_file',
|
|
536
|
+
description:
|
|
537
|
+
'Write a file into the sandbox workspace, creating parent directories as needed. Useful ' +
|
|
538
|
+
`for seeding config or fixtures before prompting Claude. Content is capped at ${WRITE_FILE_MAX_BYTES} ` +
|
|
539
|
+
'bytes per call; build anything larger inside the sandbox with exec_command.',
|
|
540
|
+
schema: v.object({
|
|
541
|
+
sandbox_id: sandboxId,
|
|
542
|
+
path: v.pipe(v.string(), v.minLength(1)),
|
|
543
|
+
content: v.pipe(
|
|
544
|
+
v.string(),
|
|
545
|
+
v.description(`The file's full UTF-8 text, at most ${WRITE_FILE_MAX_BYTES} bytes.`)
|
|
546
|
+
)
|
|
547
|
+
})
|
|
548
|
+
},
|
|
549
|
+
async (input) => {
|
|
550
|
+
await writeWorkspaceFile(requireRunning(input.sandbox_id), input.path, input.content);
|
|
551
|
+
return { path: input.path, written: true };
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
define(
|
|
556
|
+
{
|
|
557
|
+
name: 'exec_command',
|
|
558
|
+
description:
|
|
559
|
+
'Run a shell command in the sandbox, from the workspace root, and capture its output. ' +
|
|
560
|
+
'Bounded by timeout_seconds (default 120).',
|
|
561
|
+
schema: v.object({
|
|
562
|
+
sandbox_id: sandboxId,
|
|
563
|
+
command: v.pipe(v.string(), v.minLength(1)),
|
|
564
|
+
cwd: v.optional(
|
|
565
|
+
v.pipe(v.string(), v.description('Run from here instead of the workspace root.'))
|
|
566
|
+
),
|
|
567
|
+
timeout_seconds: v.optional(
|
|
568
|
+
v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(3600))
|
|
569
|
+
)
|
|
570
|
+
})
|
|
571
|
+
},
|
|
572
|
+
async (input) =>
|
|
573
|
+
await execCommand(requireRunning(input.sandbox_id), input.command, {
|
|
574
|
+
timeoutSeconds: input.timeout_seconds,
|
|
575
|
+
cwd: input.cwd
|
|
576
|
+
})
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
define(
|
|
580
|
+
{
|
|
581
|
+
name: 'git_push',
|
|
582
|
+
description:
|
|
583
|
+
'Commit any pending changes and push the sandbox branch to origin. Git and gh are already ' +
|
|
584
|
+
'authenticated inside the container, so no credentials are needed here.',
|
|
585
|
+
schema: v.object({
|
|
586
|
+
sandbox_id: sandboxId,
|
|
587
|
+
branch: v.optional(
|
|
588
|
+
v.pipe(v.string(), v.description('Branch to push to; defaults to the current one.'))
|
|
589
|
+
),
|
|
590
|
+
commit_message: v.optional(
|
|
591
|
+
v.pipe(
|
|
592
|
+
v.string(),
|
|
593
|
+
v.description('Commit everything outstanding under this message first.')
|
|
594
|
+
)
|
|
595
|
+
),
|
|
596
|
+
force: v.optional(v.pipe(v.boolean(), v.description('Push with --force-with-lease.')))
|
|
597
|
+
})
|
|
598
|
+
},
|
|
599
|
+
async (input) =>
|
|
600
|
+
await gitPush(requireRunning(input.sandbox_id), {
|
|
601
|
+
branch: input.branch,
|
|
602
|
+
commitMessage: input.commit_message,
|
|
603
|
+
force: input.force
|
|
604
|
+
})
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
define(
|
|
608
|
+
{
|
|
609
|
+
name: 'create_pr',
|
|
610
|
+
description: 'Open a pull request from the sandbox branch using the container’s gh CLI.',
|
|
611
|
+
schema: v.object({
|
|
612
|
+
sandbox_id: sandboxId,
|
|
613
|
+
title: v.pipe(v.string(), v.minLength(1)),
|
|
614
|
+
body: v.optional(v.string()),
|
|
615
|
+
base: v.optional(v.string()),
|
|
616
|
+
draft: v.optional(v.boolean())
|
|
617
|
+
})
|
|
618
|
+
},
|
|
619
|
+
async (input) =>
|
|
620
|
+
await createPr(requireRunning(input.sandbox_id), {
|
|
621
|
+
title: input.title,
|
|
622
|
+
body: input.body,
|
|
623
|
+
base: input.base,
|
|
624
|
+
draft: input.draft
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
define(
|
|
629
|
+
{
|
|
630
|
+
name: 'get_logs',
|
|
631
|
+
description:
|
|
632
|
+
'Logs for a sandbox. kind "boot" is the provisioning log (why a build failed); kind "run" ' +
|
|
633
|
+
'is the mirrored event stream of an agent run.',
|
|
634
|
+
schema: v.object({
|
|
635
|
+
sandbox_id: sandboxId,
|
|
636
|
+
kind: v.picklist(['boot', 'run'] as const),
|
|
637
|
+
run_id: v.optional(v.string()),
|
|
638
|
+
tail: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(2000)))
|
|
639
|
+
})
|
|
640
|
+
},
|
|
641
|
+
(input) => {
|
|
642
|
+
requireInstance(input.sandbox_id);
|
|
643
|
+
const tail = input.tail ?? 200;
|
|
644
|
+
if (input.kind === 'run') {
|
|
645
|
+
if (!input.run_id) throw new Error('run_id is required for kind "run"');
|
|
646
|
+
// Resolve through the DB so the id is a known token, never a path fragment.
|
|
647
|
+
const run = getRun(input.run_id);
|
|
648
|
+
if (!run || run.instance_id !== input.sandbox_id) {
|
|
649
|
+
throw new Error(`No run ${input.run_id} in sandbox ${input.sandbox_id}`);
|
|
650
|
+
}
|
|
651
|
+
return { kind: 'run', run_id: run.id, log: readRunLog(run.id, tail) };
|
|
652
|
+
}
|
|
653
|
+
// subscribeLogs replays the buffer synchronously before it starts streaming, so
|
|
654
|
+
// unsubscribing straight away leaves us with exactly the captured boot log.
|
|
655
|
+
const lines: string[] = [];
|
|
656
|
+
subscribeLogs(input.sandbox_id, (chunk) => lines.push(chunk))();
|
|
657
|
+
return { kind: 'boot', log: lines.join('').split('\n').slice(-tail).join('\n') };
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
}
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
import '@fontsource-variable/jetbrains-mono';
|
|
4
4
|
import InstanceView from '../components/InstanceView.svelte';
|
|
5
5
|
|
|
6
|
-
let {
|
|
6
|
+
let {
|
|
7
|
+
id,
|
|
8
|
+
injectionChecks,
|
|
9
|
+
uploadEnabled
|
|
10
|
+
}: { id: string; injectionChecks: number; uploadEnabled: boolean } = $props();
|
|
7
11
|
</script>
|
|
8
12
|
|
|
9
|
-
<InstanceView {id} {injectionChecks} mochi:hydrate />
|
|
13
|
+
<InstanceView {id} {injectionChecks} {uploadEnabled} mochi:hydrate />
|