@fungi.computer/computer 0.0.0-stage → 0.1.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.
Files changed (91) hide show
  1. package/Dockerfile +85 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/container/computer-init.mjs +119 -0
  5. package/container/computer-start.sh +24 -0
  6. package/container/package.json +14 -0
  7. package/container/pnpm-lock.yaml +378 -0
  8. package/container/pnpm-workspace.yaml +5 -0
  9. package/container/resource-bounds.mjs +54 -0
  10. package/container/runtime-privileges.cc +37 -0
  11. package/container/runtime-privileges.mjs +11 -0
  12. package/container/terminal-assets/README.md +35 -0
  13. package/container/terminal-assets/bashrc +49 -0
  14. package/container/terminal-assets/git-prompt.sh +586 -0
  15. package/container/terminal-protocol.mjs +58 -0
  16. package/container/terminal-runtime.mjs +10 -0
  17. package/container/terminal-server.mjs +243 -0
  18. package/dist/apply-patch/LICENSE.codex +201 -0
  19. package/dist/apply-patch/build.mjs +98 -0
  20. package/dist/apply-patch/cache.mjs +168 -0
  21. package/dist/apply-patch/compile.mjs +199 -0
  22. package/dist/apply-patch/docker/darwin.Dockerfile +15 -0
  23. package/dist/apply-patch/docker/linux-arm64.Dockerfile +38 -0
  24. package/dist/apply-patch/docker/linux-x64.Dockerfile +23 -0
  25. package/dist/apply-patch/main.rs +320 -0
  26. package/dist/apply-patch/notices.mjs +44 -0
  27. package/dist/apply-patch/pin.mjs +8 -0
  28. package/dist/apply-patch/toolchain.mjs +39 -0
  29. package/dist/computer-backends.d.ts +94 -0
  30. package/dist/computer-backends.js +103 -0
  31. package/dist/computerd.cjs +18010 -0
  32. package/dist/container-policy.d.ts +7 -0
  33. package/dist/container-policy.js +24 -0
  34. package/dist/container-system-trust.d.ts +8 -0
  35. package/dist/container-system-trust.js +66 -0
  36. package/dist/egress-policy.d.ts +89 -0
  37. package/dist/egress-policy.js +240 -0
  38. package/dist/execution-result.d.ts +3 -0
  39. package/dist/execution-result.js +28 -0
  40. package/dist/files-patch.d.ts +20 -0
  41. package/dist/files-patch.js +108 -0
  42. package/dist/files.d.ts +75 -0
  43. package/dist/files.js +136 -0
  44. package/dist/fleet/admission.d.ts +14 -0
  45. package/dist/fleet/admission.js +10 -0
  46. package/dist/fleet/backing.d.ts +52 -0
  47. package/dist/fleet/backing.js +242 -0
  48. package/dist/fleet/computers.d.ts +62 -0
  49. package/dist/fleet/computers.js +35 -0
  50. package/dist/fleet/index.d.ts +11 -0
  51. package/dist/fleet/index.js +11 -0
  52. package/dist/fleet/inventory-wire.d.ts +29 -0
  53. package/dist/fleet/inventory-wire.js +15 -0
  54. package/dist/fleet/inventory.d.ts +24 -0
  55. package/dist/fleet/inventory.js +152 -0
  56. package/dist/fleet/lifecycle.d.ts +99 -0
  57. package/dist/fleet/lifecycle.js +418 -0
  58. package/dist/fleet/runtime-usage.d.ts +50 -0
  59. package/dist/fleet/runtime-usage.js +145 -0
  60. package/dist/fleet/slots.d.ts +30 -0
  61. package/dist/fleet/slots.js +46 -0
  62. package/dist/fleet/wire.d.ts +228 -0
  63. package/dist/fleet/wire.js +127 -0
  64. package/dist/host-config.d.ts +12 -0
  65. package/dist/host-config.js +10 -0
  66. package/dist/index.d.ts +282 -0
  67. package/dist/index.js +1237 -0
  68. package/dist/light-shell.d.ts +44 -0
  69. package/dist/light-shell.js +376 -0
  70. package/dist/local-backend.d.ts +52 -0
  71. package/dist/local-backend.js +143 -0
  72. package/dist/metered-container-host.d.ts +26 -0
  73. package/dist/metered-container-host.js +48 -0
  74. package/dist/on-box-vm.d.ts +119 -0
  75. package/dist/on-box-vm.js +294 -0
  76. package/dist/operation-contract.d.ts +456 -0
  77. package/dist/operation-contract.js +450 -0
  78. package/dist/operation-record.d.ts +50 -0
  79. package/dist/operation-record.js +162 -0
  80. package/dist/preview.d.ts +55 -0
  81. package/dist/preview.js +151 -0
  82. package/dist/process-inventory.d.ts +80 -0
  83. package/dist/process-inventory.js +427 -0
  84. package/dist/pull-pending.d.ts +26 -0
  85. package/dist/pull-pending.js +35 -0
  86. package/dist/team-candidate-builder.mjs +28220 -0
  87. package/dist/transport-backend.d.ts +3 -0
  88. package/dist/transport-backend.js +99 -0
  89. package/dist/workspace-stream.d.ts +2 -0
  90. package/dist/workspace-stream.js +31 -0
  91. package/package.json +126 -3
@@ -0,0 +1,450 @@
1
+ import { replayKey, defineIdFamily } from "@fungi.computer/cairn";
2
+ import * as z from "zod";
3
+ import { attachedBackendIdSchema } from "./computer-backends.js";
4
+ const utf8 = new TextEncoder();
5
+ export const maximumFileBytes = 128 * 1_024;
6
+ export const maximumListEntries = 256;
7
+ export const maximumPatternCharacters = 512;
8
+ export const maximumCommandCharacters = 16 * 1_024;
9
+ export const maximumStreamBytes = 64 * 1_024;
10
+ /** The bound for ordinary Computer commands; hosts own any admitted job budget. */
11
+ export const maximumExecutionTimeMs = 120_000;
12
+ export const genericComputerExecutionBudget = Object.freeze({ maximumTimeMs: maximumExecutionTimeMs });
13
+ export const computerValueSchema = z.json();
14
+ export const capabilityNameSchema = z.lazy(() => z
15
+ .string()
16
+ .min(1)
17
+ .max(128)
18
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/));
19
+ /** One valid TCP port number, used only with this Computer's container. */
20
+ export const computerPortSchema = z.lazy(() => z.number().int().min(1).max(65_535));
21
+ export const maximumExposedPorts = 64;
22
+ /** computerd's Workspace RPC port, pinned by the Cloudflare container backend. */
23
+ export const computerDaemonPort = 8080;
24
+ /** The Computer-owned terminal server inside the container image. */
25
+ export const computerTerminalPort = 7681;
26
+ /**
27
+ * Ports served by the Computer's own in-container daemons. An admitted
28
+ * preview must never reach a control plane.
29
+ */
30
+ export const computerReservedPorts = new Set([
31
+ computerDaemonPort,
32
+ computerTerminalPort,
33
+ ]);
34
+ export function isComputerReservedPort(port) {
35
+ return computerReservedPorts.has(port);
36
+ }
37
+ export const computerPortInputSchema = z.strictObject({
38
+ port: computerPortSchema,
39
+ });
40
+ export const computerPortOutputSchema = z.lazy(() => z.strictObject({
41
+ port: computerPortSchema,
42
+ exposed: z.boolean(),
43
+ }));
44
+ export const computerPortRemovalOutputSchema = z.lazy(() => z.strictObject({
45
+ port: computerPortSchema,
46
+ removed: z.boolean(),
47
+ }));
48
+ export const computerPortsOutputSchema = z.lazy(() => z.strictObject({
49
+ ports: z.array(computerPortSchema).max(maximumExposedPorts),
50
+ }));
51
+ /** The port belongs to a Computer daemon and cannot be admitted for preview. */
52
+ export class ComputerPortReservedError extends Error {
53
+ constructor(port) {
54
+ super(`Computer port ${String(port)} is reserved`);
55
+ this.name = "ComputerPortReservedError";
56
+ }
57
+ }
58
+ /** A requested port cannot be reserved; no other process identity is disclosed. */
59
+ export class ComputerPortConflictError extends Error {
60
+ constructor() {
61
+ super("Computer port is unavailable");
62
+ this.name = "ComputerPortConflictError";
63
+ }
64
+ }
65
+ /** This Computer already admits the maximum number of preview ports. */
66
+ export class ComputerPortLimitError extends Error {
67
+ constructor() {
68
+ super(`Computer can expose at most ${String(maximumExposedPorts)} ports`);
69
+ this.name = "ComputerPortLimitError";
70
+ }
71
+ }
72
+ // Schema keywords are opaque transport data. Zod still validates every value
73
+ // as JSON before the description leaves or enters the Computer boundary.
74
+ const schemaDocument = z.lazy(() => z
75
+ .record(z.string(), z.unknown())
76
+ .refine((value) => computerValueSchema.safeParse(value).success, "Capability schema must contain JSON values"));
77
+ /** Native operation descriptions shared by discovery and its OpenAPI document. */
78
+ export const computerCapabilitiesSchema = z.lazy(() => z.array(z.strictObject({
79
+ id: capabilityNameSchema,
80
+ name: capabilityNameSchema,
81
+ kind: z.string(),
82
+ description: z.string(),
83
+ parameters: schemaDocument,
84
+ output: schemaDocument,
85
+ })));
86
+ export const pathSchema = z
87
+ .string()
88
+ .min(1)
89
+ .max(4_096)
90
+ .startsWith("/")
91
+ .refine((path) => !path.includes("\0"), "Path cannot contain null bytes");
92
+ export const boundedTextSchema = (minimumLength = 0) => z
93
+ .string()
94
+ .min(minimumLength)
95
+ .refine((content) => utf8.encode(content).byteLength <= maximumFileBytes, `Text exceeds the ${String(maximumFileBytes)}-byte Computer bound`);
96
+ /** Complete bounded UTF-8 text reads. Binary reads use the same path input. */
97
+ export const readInputSchema = z.strictObject({ path: pathSchema });
98
+ export const readOutputSchema = z.lazy(() => z.strictObject({
99
+ path: pathSchema,
100
+ content: boundedTextSchema(),
101
+ }));
102
+ /** Optional byte window; text reads remain complete and independently bounded. */
103
+ export const readBytesInputSchema = z.strictObject({
104
+ path: pathSchema,
105
+ byteOffset: z.number().int().nonnegative().optional(),
106
+ byteLength: z.number().int().positive().max(maximumFileBytes).optional(),
107
+ });
108
+ /** Finite byte-read reply used by admitted App frame bindings. */
109
+ export const computerByteReadReplySchema = z.lazy(() => z.strictObject({
110
+ path: readBytesInputSchema.shape.path,
111
+ byteOffset: z.number().int().nonnegative(),
112
+ content: z.string(),
113
+ encoding: z.literal("base64"),
114
+ truncated: z.boolean(),
115
+ }));
116
+ /** Host-admitted App method preview; parsing does not grant port access. */
117
+ export const appMethodPreviewResultSchema = z.lazy(() => z.strictObject({
118
+ previewUrl: z.url(),
119
+ expiresAt: z.number().int().positive(),
120
+ }));
121
+ export const listLimitSchema = z
122
+ .number()
123
+ .int()
124
+ .min(1)
125
+ .max(maximumListEntries)
126
+ .default(100);
127
+ export const listOffsetSchema = z.number().int().nonnegative().default(0);
128
+ export const listInputSchema = z.strictObject({
129
+ path: pathSchema.default("/"),
130
+ limit: listLimitSchema,
131
+ offset: listOffsetSchema,
132
+ });
133
+ const listEntrySchema = z.lazy(() => z.strictObject({
134
+ name: z.string(),
135
+ size: z.number().int().nonnegative(),
136
+ modifiedAt: z.number(),
137
+ type: z.enum(["file", "directory", "symlink"]),
138
+ }));
139
+ export const listOutputSchema = z.lazy(() => z.strictObject({
140
+ path: pathSchema,
141
+ entries: z.array(listEntrySchema).max(maximumListEntries),
142
+ nextOffset: z.number().int().nonnegative().nullable(),
143
+ }));
144
+ export const fileContentSchema = z.lazy(() => z
145
+ .string()
146
+ .refine((content) => utf8.encode(content).byteLength <= maximumFileBytes, "File exceeds the Computer write bound"));
147
+ export const writeInputSchema = z.strictObject({
148
+ path: pathSchema,
149
+ content: fileContentSchema,
150
+ });
151
+ export const writeOutputSchema = z.lazy(() => z.strictObject({
152
+ path: pathSchema,
153
+ bytes: z.number().int().nonnegative(),
154
+ }));
155
+ export const writeBytesInputSchema = readInputSchema;
156
+ export const replaceInputSchema = z.strictObject({
157
+ path: pathSchema,
158
+ expectedContent: fileContentSchema,
159
+ content: fileContentSchema,
160
+ });
161
+ export const replaceOutputSchema = writeOutputSchema;
162
+ export const globInputSchema = z.lazy(() => z.strictObject({
163
+ path: pathSchema,
164
+ pattern: z.string().min(1).max(maximumPatternCharacters),
165
+ limit: z
166
+ .number()
167
+ .int()
168
+ .min(1)
169
+ .max(maximumListEntries)
170
+ .default(maximumListEntries),
171
+ }));
172
+ export const globOutputSchema = z.lazy(() => z.strictObject({
173
+ path: pathSchema,
174
+ matches: z.array(pathSchema).max(maximumListEntries),
175
+ truncated: z.boolean(),
176
+ }));
177
+ export const grepInputSchema = z.lazy(() => z.strictObject({
178
+ path: pathSchema,
179
+ pattern: z.string().min(1).max(maximumPatternCharacters),
180
+ regex: z.boolean().default(false),
181
+ ignoreCase: z.boolean().default(false),
182
+ limit: z
183
+ .number()
184
+ .int()
185
+ .min(1)
186
+ .max(maximumListEntries)
187
+ .default(maximumListEntries),
188
+ }));
189
+ export const grepOutputSchema = z.lazy(() => z.strictObject({
190
+ path: pathSchema,
191
+ matches: z
192
+ .array(z.strictObject({
193
+ path: pathSchema,
194
+ line: z.number().int().positive(),
195
+ text: z.string(),
196
+ }))
197
+ .max(maximumListEntries),
198
+ truncated: z.boolean(),
199
+ }));
200
+ export const editInputSchema = z.lazy(() => z.strictObject({
201
+ path: pathSchema,
202
+ oldText: boundedTextSchema(1),
203
+ newText: boundedTextSchema(),
204
+ }));
205
+ export const editOutputSchema = z.lazy(() => z.strictObject({
206
+ path: pathSchema,
207
+ bytes: z.number().int().nonnegative(),
208
+ diff: z.string(),
209
+ firstChangedLine: z.number().int().positive().nullable(),
210
+ }));
211
+ // Native Files tools and Computer HTTP decode the same patch contract.
212
+ export { patchInputSchema, patchOutputSchema, } from "@fungi.computer/knapsack/files-patch";
213
+ export const mkdirInputSchema = z.lazy(() => z.strictObject({
214
+ path: pathSchema,
215
+ recursive: z.boolean().default(true),
216
+ }));
217
+ export const mkdirOutputSchema = z.strictObject({ path: pathSchema });
218
+ /**
219
+ * Host-owned idempotency key for one `exec` or `bash` tool invocation. The
220
+ * host mints it once per invocation and keeps it across its own retries of that
221
+ * invocation; the model never sees or supplies it.
222
+ */
223
+ export const computerRequestIdSchema = z.uuid({ version: "v7" });
224
+ /** Mint the request ID for one host tool invocation. */
225
+ export function mintComputerRequestId() {
226
+ return computerRequestIdSchema.parse(replayKey());
227
+ }
228
+ /** Model-facing command input. It carries no request identity. */
229
+ function executionInputSchema(budget) {
230
+ return z.strictObject({
231
+ command: z.string().min(1).max(maximumCommandCharacters),
232
+ cwd: pathSchema.optional(),
233
+ /** An attached backend id; omitted routes by the Computer's default order. */
234
+ backend: attachedBackendIdSchema.optional(),
235
+ timeoutMs: z
236
+ .number()
237
+ .int()
238
+ .min(1)
239
+ .max(budget.maximumTimeMs)
240
+ .default(10_000),
241
+ });
242
+ }
243
+ export const execInputSchema = executionInputSchema(genericComputerExecutionBudget);
244
+ /** Resource hosts select a budget only after admitting their job principal. */
245
+ export function computerExecutionRequestSchema(budget) {
246
+ return executionInputSchema(budget).extend({
247
+ requestId: computerRequestIdSchema,
248
+ });
249
+ }
250
+ /** Host-to-Computer command request: the model input plus the host's ID. */
251
+ export const execRequestSchema = execInputSchema.extend({
252
+ requestId: computerRequestIdSchema,
253
+ });
254
+ export const execOutputSchema = z.strictObject({
255
+ status: z.enum(["completed", "failed", "cancelled"]),
256
+ exitCode: z.number().int(),
257
+ stdout: z.string(),
258
+ stderr: z.string(),
259
+ truncated: z.boolean(),
260
+ });
261
+ /** One long-running container process owned by this Computer. */
262
+ const processIds = defineIdFamily({
263
+ family: "computer-process",
264
+ version: 1,
265
+ kinds: { process: { prefix: "computer_process" } },
266
+ }).process;
267
+ export const computerProcessIdSchema = z.lazy(() => z.string().refine((value) => processIds.is(value)));
268
+ /** At most this many processes may be running in one host scope at once. */
269
+ export const maximumRunningProcesses = 16;
270
+ /** At most this many process records, running or exited, are kept per host scope. */
271
+ export const maximumProcessRecords = 64;
272
+ /** Mint the durable identity of one started process. */
273
+ export function mintComputerProcessId() {
274
+ return processIds.generate();
275
+ }
276
+ export const processStartInputSchema = z.strictObject({
277
+ requestId: z.string().min(1).max(256),
278
+ requestScope: z.string().min(1).max(512),
279
+ port: computerPortSchema.optional(),
280
+ command: z.string().min(1).max(maximumCommandCharacters),
281
+ cwd: pathSchema.optional(),
282
+ });
283
+ /** Model input excludes the host's replay identity and authority scope. */
284
+ export const processStartToolInputSchema = processStartInputSchema.omit({
285
+ requestId: true,
286
+ requestScope: true,
287
+ });
288
+ /** A trusted App host binds replay identity to its admitted installation and caller. */
289
+ export function installationProcessScope(installation, method, actor) {
290
+ return processStartInputSchema.shape.requestScope.parse(`installation:${JSON.stringify([installation, method, `${actor.kind}:${actor.id}`])}`);
291
+ }
292
+ export const processIdInputSchema = z.strictObject({
293
+ id: computerProcessIdSchema,
294
+ });
295
+ /**
296
+ * `running`: computerd still reports the execution live. `exited`: it ended;
297
+ * `exitCode` is null when the runtime that ran it was replaced or its record
298
+ * expired before the exit code was observed. `uncertain`: the Computer could
299
+ * not reach its container to check, so neither is claimed.
300
+ */
301
+ export const computerProcessStateSchema = z.enum([
302
+ "running",
303
+ "exited",
304
+ "uncertain",
305
+ ]);
306
+ /**
307
+ * `synced`: the drain that recorded this exit read all the way to EOF,
308
+ * which is also the read that runs computerd's post-command pull.
309
+ * `pending`: that drain gave up waiting at its own deadline (a
310
+ * backgrounded child, for example `node server.js &`, can hold the
311
+ * transport open long after its parent exits) -- a background
312
+ * continuation keeps watching and upgrades this row to `synced` once it
313
+ * reaches that real EOF, so `pending` rows stay updatable. `unknown`:
314
+ * this exit was recorded with no pull evidence at all (a replaced
315
+ * runtime, an expired record, or any other exit path that never drained
316
+ * for one). Never claim `synced` without the specific evidence for this
317
+ * exit; default to `unknown`, not `synced`.
318
+ */
319
+ export const computerProcessSyncSchema = z.enum([
320
+ "synced",
321
+ "pending",
322
+ "unknown",
323
+ ]);
324
+ export const computerProcessSchema = z.strictObject({
325
+ id: computerProcessIdSchema,
326
+ port: computerPortSchema.nullable(),
327
+ command: z.string(),
328
+ cwd: z.string().nullable(),
329
+ state: computerProcessStateSchema,
330
+ exitCode: z.number().int().nullable(),
331
+ sync: computerProcessSyncSchema,
332
+ startedAt: z.number().int(),
333
+ endedAt: z.number().int().nullable(),
334
+ });
335
+ export const processListOutputSchema = z.strictObject({
336
+ processes: z.array(computerProcessSchema).max(maximumProcessRecords),
337
+ });
338
+ export const processKillOutputSchema = z.lazy(() => z.strictObject({
339
+ process: computerProcessSchema,
340
+ signalled: z.boolean(),
341
+ }));
342
+ /** No process with this id was started on this Computer. */
343
+ export class ComputerProcessNotFoundError extends Error {
344
+ constructor(id) {
345
+ super(`Computer process not found: ${id}`);
346
+ this.name = "ComputerProcessNotFoundError";
347
+ }
348
+ }
349
+ /** Every process slot is occupied by a running process. */
350
+ export class ComputerProcessLimitError extends Error {
351
+ constructor() {
352
+ super(`Computer process scope can run at most ${String(maximumRunningProcesses)} processes`);
353
+ this.name = "ComputerProcessLimitError";
354
+ }
355
+ }
356
+ export const computerOperationStatusSchema = z.enum([
357
+ "accepted",
358
+ "running",
359
+ "succeeded",
360
+ "failed",
361
+ "unknown",
362
+ "expired",
363
+ ]);
364
+ /** Durable operation status returned to the host that owns the request ID. */
365
+ export const computerOperationResultSchema = z.strictObject({
366
+ requestId: computerRequestIdSchema,
367
+ status: computerOperationStatusSchema,
368
+ output: execOutputSchema.nullable(),
369
+ });
370
+ /** Model-facing command result: the durable status without the host's ID. */
371
+ export const computerToolResultSchema = computerOperationResultSchema.omit({
372
+ requestId: true,
373
+ });
374
+ /** Project one host operation result onto the model-facing tool result. */
375
+ export function computerToolResult(result) {
376
+ return computerToolResultSchema.parse({
377
+ status: result.status,
378
+ output: result.output,
379
+ });
380
+ }
381
+ /** Public operation vocabulary; transport adapters map these to routes. */
382
+ export const computerOperationSchema = z.enum([
383
+ "files.read",
384
+ "files.readBytes",
385
+ "files.write",
386
+ "files.writeBytes",
387
+ "files.list",
388
+ "files.glob",
389
+ "files.grep",
390
+ "files.edit",
391
+ "files.patch",
392
+ "mkdir",
393
+ "bash",
394
+ "exec",
395
+ ]);
396
+ /** The expected file contents no longer match at atomic replacement. */
397
+ export class ComputerFileConflict extends Error {
398
+ constructor() {
399
+ super("Files edit conflict: contents changed before replacement");
400
+ this.name = "ComputerFileConflict";
401
+ }
402
+ }
403
+ /** A byte window starts beyond the current file extent. */
404
+ export class ComputerFileRangeError extends Error {
405
+ size;
406
+ constructor(size) {
407
+ super("Byte offset exceeds file size");
408
+ this.size = size;
409
+ this.name = "ComputerFileRangeError";
410
+ }
411
+ }
412
+ /** The requested file bytes exceed the admitted read bound. */
413
+ export class ComputerFileTooLargeError extends Error {
414
+ constructor() {
415
+ super("File exceeds the Computer read bound");
416
+ this.name = "ComputerFileTooLargeError";
417
+ }
418
+ }
419
+ function capability(id, kind, description, input, output) {
420
+ const parameters = structuredClone(z.toJSONSchema(input));
421
+ const result = structuredClone(z.toJSONSchema(output));
422
+ return computerValueSchema.parse({
423
+ id,
424
+ name: id,
425
+ kind,
426
+ description,
427
+ parameters,
428
+ output: result,
429
+ });
430
+ }
431
+ let capabilities;
432
+ /**
433
+ * The operations every Computer advertises; static, so hosts can describe them
434
+ * without a call. Built on first read: deriving JSON Schema for every operation
435
+ * is real work that a Worker should not pay at startup.
436
+ */
437
+ export function computerCapabilities() {
438
+ capabilities ??= computerCapabilitiesSchema.parse([
439
+ capability("read", "fileStore", "Read one bounded UTF-8 file from this Computer.", readInputSchema, readOutputSchema),
440
+ capability("list", "fileStore", "List one bounded page of a directory on this Computer.", listInputSchema, listOutputSchema),
441
+ capability("mkdir", "fileStore", "Create a directory on this Computer.", mkdirInputSchema, mkdirOutputSchema),
442
+ capability("write", "fileStore", "Write one bounded UTF-8 file without creating its parent directory.", writeInputSchema, writeOutputSchema),
443
+ capability("bash", "runtime", "Run one bounded shell command in this Computer's Linux container, with node, npm and the programs installed there.", execInputSchema, computerToolResultSchema),
444
+ capability("exec", "runtime", "Run one bounded command in a lightweight Worker shell over this Computer's files. It has no node, npm or installed programs; use bash to run programs.", execInputSchema, computerToolResultSchema),
445
+ capability("start_process", "runtime", "Start one long-running command, such as a dev server, in this Computer's container. Returns its durable process id.", processStartToolInputSchema, computerProcessSchema),
446
+ capability("list_processes", "runtime", "List this Computer's started processes as running, exited or uncertain.", z.strictObject({}), processListOutputSchema),
447
+ capability("kill_process", "runtime", "Send SIGTERM to one running process on this Computer.", processIdInputSchema, processKillOutputSchema),
448
+ ]);
449
+ return capabilities;
450
+ }
@@ -0,0 +1,50 @@
1
+ import { type ComputerExecOutput, type ComputerOperationResult, type ComputerRequestId } from "./operation-contract.js";
2
+ /** Terminal status records are retained for 7 days. */
3
+ export declare const operationRetentionMs: number;
4
+ /** Command output may contain secrets, so it is cleared after 24 hours. */
5
+ export declare const operationOutputRetentionMs: number;
6
+ /** At most this many status records are stored per Computer. */
7
+ export declare const maximumOperationRecords = 500;
8
+ /**
9
+ * A request ID is admitted as new work only when its UUIDv7 time is within this
10
+ * distance of the owner's clock. Records stay protected from capacity eviction
11
+ * until both their acceptance time and their ID time are older than the window,
12
+ * so a missing in-window ID was never admitted and an out-of-window ID expires.
13
+ */
14
+ export declare const operationAdmissionWindowMs: number;
15
+ export declare class ComputerOperationConflictError extends Error {
16
+ constructor();
17
+ }
18
+ export declare class ComputerOperationLimitError extends Error {
19
+ constructor();
20
+ }
21
+ export declare class ComputerRequestInvalidError extends Error {
22
+ constructor();
23
+ }
24
+ /** The Durable Object storage surface this record owns. */
25
+ export type ComputerOperationStorage = Pick<DurableObjectStorage, "sql" | "transactionSync">;
26
+ /**
27
+ * Durable `exec`/`bash` operation outcomes for one Computer. Every retention,
28
+ * eviction and expiry decision uses the owner's clock; a caller's UUIDv7 time
29
+ * only selects whether an unseen ID is inside the admission window.
30
+ */
31
+ export declare class ComputerOperationRecord {
32
+ #private;
33
+ constructor(storage: ComputerOperationStorage, now: () => number);
34
+ /**
35
+ * Atomically return the recorded state of a known ID, or admit a new one as
36
+ * `accepted`. `undefined` means the caller now owns dispatching it once.
37
+ */
38
+ admit(requestIdInput: string, inputHash: string, accept: () => void): ComputerOperationResult | undefined;
39
+ /** Record entry into the Workspace command. */
40
+ markRunning(requestId: ComputerRequestId): void;
41
+ /** Record the one terminal command result. */
42
+ settle(requestId: ComputerRequestId, output: ComputerExecOutput): void;
43
+ /**
44
+ * The command may have started, but no terminal result is established. The
45
+ * cause is logged so capacity or start failures stay diagnosable.
46
+ */
47
+ settleUnknown(requestId: ComputerRequestId, cause: string): void;
48
+ /** Durable status lookup; an unseen ID older than the window is expired. */
49
+ status(requestIdInput: string, accept: () => void): ComputerOperationResult;
50
+ }
@@ -0,0 +1,162 @@
1
+ import { computerOperationResultSchema, computerRequestIdSchema, execOutputSchema, } from "./operation-contract.js";
2
+ /** Terminal status records are retained for 7 days. */
3
+ export const operationRetentionMs = 7 * 24 * 60 * 60 * 1_000;
4
+ /** Command output may contain secrets, so it is cleared after 24 hours. */
5
+ export const operationOutputRetentionMs = 24 * 60 * 60 * 1_000;
6
+ /** At most this many status records are stored per Computer. */
7
+ export const maximumOperationRecords = 500;
8
+ /**
9
+ * A request ID is admitted as new work only when its UUIDv7 time is within this
10
+ * distance of the owner's clock. Records stay protected from capacity eviction
11
+ * until both their acceptance time and their ID time are older than the window,
12
+ * so a missing in-window ID was never admitted and an out-of-window ID expires.
13
+ */
14
+ export const operationAdmissionWindowMs = 5 * 60 * 1_000;
15
+ export class ComputerOperationConflictError extends Error {
16
+ constructor() {
17
+ super("Computer request ID is already bound to another operation");
18
+ this.name = "ComputerOperationConflictError";
19
+ }
20
+ }
21
+ export class ComputerOperationLimitError extends Error {
22
+ constructor() {
23
+ super("Computer operation record capacity is occupied by recent or active work");
24
+ this.name = "ComputerOperationLimitError";
25
+ }
26
+ }
27
+ export class ComputerRequestInvalidError extends Error {
28
+ constructor() {
29
+ super("Computer request ID is outside the allowed clock-skew window");
30
+ this.name = "ComputerRequestInvalidError";
31
+ }
32
+ }
33
+ /** The embedded UUIDv7 millisecond time; the ID is already Zod-validated. */
34
+ function requestIdTime(requestId) {
35
+ return Number.parseInt(requestId.replaceAll("-", "").slice(0, 12), 16);
36
+ }
37
+ /**
38
+ * Durable `exec`/`bash` operation outcomes for one Computer. Every retention,
39
+ * eviction and expiry decision uses the owner's clock; a caller's UUIDv7 time
40
+ * only selects whether an unseen ID is inside the admission window.
41
+ */
42
+ export class ComputerOperationRecord {
43
+ #storage;
44
+ #now;
45
+ constructor(storage, now) {
46
+ this.#storage = storage;
47
+ this.#now = now;
48
+ const sql = storage.sql;
49
+ sql.exec("CREATE TABLE IF NOT EXISTS computer_operations (request_id TEXT PRIMARY KEY, input_hash TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('accepted', 'running', 'succeeded', 'failed', 'unknown')), created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, output_expires_at INTEGER NOT NULL, output_json TEXT) STRICT");
50
+ // Reopening never reinterprets unfinished work as permission to rerun it.
51
+ storage.transactionSync(() => {
52
+ sql.exec("UPDATE computer_operations SET status = 'unknown', updated_at = ? WHERE status IN ('accepted', 'running')", now());
53
+ });
54
+ }
55
+ #admissionTime(requestIdInput) {
56
+ const requestId = computerRequestIdSchema.parse(requestIdInput);
57
+ const requestTime = requestIdTime(requestId);
58
+ const now = this.#now();
59
+ if (requestTime > now + operationAdmissionWindowMs) {
60
+ throw new ComputerRequestInvalidError();
61
+ }
62
+ return { requestId, requestTime, now };
63
+ }
64
+ /**
65
+ * Atomically return the recorded state of a known ID, or admit a new one as
66
+ * `accepted`. `undefined` means the caller now owns dispatching it once.
67
+ */
68
+ admit(requestIdInput, inputHash, accept) {
69
+ const { requestId, requestTime, now } = this.#admissionTime(requestIdInput);
70
+ return this.#storage.transactionSync(() => {
71
+ accept();
72
+ this.#sweep(now);
73
+ const existing = this.#row(requestId);
74
+ if (existing !== undefined) {
75
+ if (existing.input_hash !== inputHash)
76
+ throw new ComputerOperationConflictError();
77
+ return this.#result(requestId, existing, now);
78
+ }
79
+ if (requestTime < now - operationAdmissionWindowMs) {
80
+ return this.#result(requestId, undefined, now);
81
+ }
82
+ this.#makeRoom(now);
83
+ this.#storage.sql.exec("INSERT INTO computer_operations (request_id, input_hash, status, created_at, updated_at, output_expires_at, output_json) VALUES (?, ?, 'accepted', ?, ?, ?, NULL)", requestId, inputHash, now, now, now + operationOutputRetentionMs);
84
+ return undefined;
85
+ });
86
+ }
87
+ /** Record entry into the Workspace command. */
88
+ markRunning(requestId) {
89
+ this.#storage.sql.exec("UPDATE computer_operations SET status = 'running', updated_at = ? WHERE request_id = ?", this.#now(), requestId);
90
+ }
91
+ /** Record the one terminal command result. */
92
+ settle(requestId, output) {
93
+ const finishedAt = this.#now();
94
+ this.#storage.sql.exec("UPDATE computer_operations SET status = ?, updated_at = ?, output_expires_at = ?, output_json = ? WHERE request_id = ?", output.status === "completed" ? "succeeded" : "failed", finishedAt, finishedAt + operationOutputRetentionMs, JSON.stringify(execOutputSchema.parse(output)), requestId);
95
+ }
96
+ /**
97
+ * The command may have started, but no terminal result is established. The
98
+ * cause is logged so capacity or start failures stay diagnosable.
99
+ */
100
+ settleUnknown(requestId, cause) {
101
+ console.error("Computer operation outcome unknown", { requestId, cause });
102
+ this.#storage.sql.exec("UPDATE computer_operations SET status = 'unknown', updated_at = ?, output_json = NULL WHERE request_id = ?", this.#now(), requestId);
103
+ }
104
+ /** Durable status lookup; an unseen ID older than the window is expired. */
105
+ status(requestIdInput, accept) {
106
+ const { requestId, now } = this.#admissionTime(requestIdInput);
107
+ return this.#storage.transactionSync(() => {
108
+ accept();
109
+ this.#sweep(now);
110
+ return this.#result(requestId, this.#row(requestId), now);
111
+ });
112
+ }
113
+ #row(requestId) {
114
+ return this.#storage.sql
115
+ .exec("SELECT request_id, input_hash, status, created_at, output_expires_at, output_json FROM computer_operations WHERE request_id = ?", requestId)
116
+ .toArray()[0];
117
+ }
118
+ #result(requestId, row, now) {
119
+ if (row !== undefined) {
120
+ return computerOperationResultSchema.parse({
121
+ requestId,
122
+ status: row.status,
123
+ output: row.output_json !== null && row.output_expires_at > now
124
+ ? execOutputSchema.parse(JSON.parse(row.output_json))
125
+ : null,
126
+ });
127
+ }
128
+ return computerOperationResultSchema.parse({
129
+ requestId,
130
+ status: requestIdTime(requestId) < now - operationAdmissionWindowMs
131
+ ? "expired"
132
+ : "unknown",
133
+ output: null,
134
+ });
135
+ }
136
+ #sweep(now) {
137
+ const sql = this.#storage.sql;
138
+ sql.exec("UPDATE computer_operations SET output_json = NULL WHERE output_json IS NOT NULL AND output_expires_at <= ?", now);
139
+ // Admission bounds an ID's time to its acceptance time plus the window, so
140
+ // a record accepted 7 days ago is always outside the admission window.
141
+ sql.exec("DELETE FROM computer_operations WHERE status IN ('succeeded', 'failed', 'unknown') AND created_at <= ?", now - operationRetentionMs);
142
+ }
143
+ /** Evict the oldest terminal record that no in-window retry can address. */
144
+ #makeRoom(now) {
145
+ const sql = this.#storage.sql;
146
+ const count = sql
147
+ .exec("SELECT COUNT(*) AS count FROM computer_operations")
148
+ .toArray()[0]?.count;
149
+ if (count === undefined)
150
+ throw new ComputerOperationLimitError();
151
+ if (count < maximumOperationRecords)
152
+ return;
153
+ const horizon = now - operationAdmissionWindowMs;
154
+ const evictable = sql
155
+ .exec("SELECT request_id FROM computer_operations WHERE status IN ('succeeded', 'failed', 'unknown') AND created_at < ? ORDER BY created_at ASC, request_id ASC", horizon)
156
+ .toArray()
157
+ .find(({ request_id }) => requestIdTime(request_id) < horizon);
158
+ if (evictable === undefined)
159
+ throw new ComputerOperationLimitError();
160
+ sql.exec("DELETE FROM computer_operations WHERE request_id = ?", evictable.request_id);
161
+ }
162
+ }