@fungi.computer/shed 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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -2
  3. package/backend/gills.ts +74 -0
  4. package/backend/index.ts +15 -0
  5. package/backend/methods.ts +319 -0
  6. package/backend/process-page.ts +44 -0
  7. package/backend/runs.ts +580 -0
  8. package/backend/test/cloudflare-workers.fixture.ts +1 -0
  9. package/backend/test/methods.test.ts +662 -0
  10. package/backend/test/present.test.ts +40 -0
  11. package/backend/test/runs.node.test.ts +1770 -0
  12. package/backend/tsconfig.json +10 -0
  13. package/backend/vitest.config.ts +16 -0
  14. package/dist/assets/Anton-5.3.0-latin-Byf51wtH.woff2 +0 -0
  15. package/dist/assets/BricolageGrotesque-VF-latin-Cre6nC2_.woff2 +0 -0
  16. package/dist/assets/DepartureMono-Regular-2BZob_Zz.woff2 +0 -0
  17. package/dist/assets/Fraunces-Italic-VF-latin-D2JnYqd-.woff2 +0 -0
  18. package/dist/assets/Fraunces-VF-latin-CFFu7zhK.woff2 +0 -0
  19. package/dist/assets/GrenzeGotisch-VF-latin-BhUVw2FV.woff2 +0 -0
  20. package/dist/assets/InstrumentSans-Italic-VF-latin-BlcgWI5J.woff2 +0 -0
  21. package/dist/assets/InstrumentSans-VF-latin-BbzFLZTg.woff2 +0 -0
  22. package/dist/assets/MapleMono-VF-CCDrizxw.woff2 +0 -0
  23. package/dist/assets/NothingYouCouldDo-Regular-B7VfwEBS.woff2 +0 -0
  24. package/dist/assets/Nunito-VF-8c6a9bb-cyrillic-BsWUdSU9.woff2 +0 -0
  25. package/dist/assets/Nunito-VF-8c6a9bb-cyrillic-ext-Cb1j0nGJ.woff2 +0 -0
  26. package/dist/assets/Nunito-VF-8c6a9bb-latin-BYs3GYNf.woff2 +0 -0
  27. package/dist/assets/Nunito-VF-8c6a9bb-latin-ext-0RvtKGDy.woff2 +0 -0
  28. package/dist/assets/Nunito-VF-8c6a9bb-vietnamese-cS2dauUA.woff2 +0 -0
  29. package/dist/assets/__vite-browser-external-2447137e-BvRk9kiK.js +0 -0
  30. package/dist/assets/index-2RtcU_CN.css +1 -0
  31. package/dist/assets/index-bW5A7RuN.js +1769 -0
  32. package/dist/index.html +15 -0
  33. package/package.json +159 -3
@@ -0,0 +1,580 @@
1
+ import * as z from "zod";
2
+ import { Hex } from "effect/encoding";
3
+ import type { HostActor } from "@fungi.computer/puffball/app-method-runtime";
4
+ import {
5
+ workflowGitPassResultSchema,
6
+ type WorkflowGitPassResult,
7
+ } from "@fungi.computer/agar/computer-contracts";
8
+ import { resolvedGitAccessSchema } from "@fungi.computer/braid/git-access";
9
+ import { pending } from "@fungi.computer/agar/backend-definition";
10
+ import type { WhistleJsonValue } from "@fungi.computer/whistle";
11
+ import { AppMethodRefusal } from "@fungi.computer/agar/backend";
12
+ import {
13
+ computerProcessSchema,
14
+ maximumProcessRecords,
15
+ writeOutputSchema,
16
+ type ComputerFileWriteInput,
17
+ processStartInputSchema,
18
+ processListOutputSchema,
19
+ type ComputerProcess,
20
+ } from "@fungi.computer/computer/operations";
21
+ import {
22
+ runCommandInputSchema,
23
+ runCommandResultSchema,
24
+ shedComputerRefusalSchema,
25
+ } from "../src/light-shell/run-command-contract.js";
26
+
27
+ /** What a Git command needs from a pass: its remote and any header credential. */
28
+ type GitCredential = Pick<WorkflowGitPassResult, "remote" | "bearer">;
29
+ const processInputSchema = processStartInputSchema.omit({
30
+ requestId: true,
31
+ requestScope: true,
32
+ });
33
+ export const shedRunInputSchema = processInputSchema
34
+ .omit({ port: true })
35
+ .extend({
36
+ gitAccess: resolvedGitAccessSchema.optional(),
37
+ });
38
+ export const shedStartInputSchema = processInputSchema.extend({
39
+ gitAccess: resolvedGitAccessSchema.optional(),
40
+ });
41
+ export type ShedStartInput = z.output<typeof shedStartInputSchema>;
42
+ export type ShedRunInput = z.output<typeof shedRunInputSchema>;
43
+
44
+ /** Facet-owned records associate one trusted logical call with one native process. */
45
+ export type ShedSql = {
46
+ exec(
47
+ query: string,
48
+ ...values: (string | number | null)[]
49
+ ): { toArray(): unknown[] };
50
+ };
51
+ export type ShedComputer = Readonly<{
52
+ workflowGitPass?:
53
+ | ((input: Record<string, never>) => Promise<z.core.util.JSONType>)
54
+ | undefined;
55
+ write?:
56
+ | ((input: ComputerFileWriteInput) => Promise<z.core.util.JSONType>)
57
+ | undefined;
58
+ processStart(input: {
59
+ command: string;
60
+ cwd?: string | undefined;
61
+ port?: number | undefined;
62
+ }): Promise<z.core.util.JSONType>;
63
+ processes(input: Record<string, never>): Promise<z.core.util.JSONType>;
64
+ }>;
65
+ const rowSchema = z.strictObject({
66
+ requestKey: z.string(),
67
+ computerId: z.string(),
68
+ command: z.string(),
69
+ cwd: z.string().nullable(),
70
+ processId: z.string().nullable(),
71
+ phase: z.enum(["starting", "running", "exited"]),
72
+ startedAt: z.number().int().nullable(),
73
+ exitCode: z.number().int().nullable(),
74
+ logPath: z.string(),
75
+ gitAccess: z.string().nullable(),
76
+ passId: z.string().nullable(),
77
+ remote: z.string().nullable(),
78
+ expiresAt: z.number().int().nullable(),
79
+ });
80
+ // A header-admitted pass reaches git only as command-local
81
+ // http.<remote>.extraHeader config, read from a one-shot file the command
82
+ // deletes; neither the command nor any repository config ever holds it.
83
+ function gitCredentialFile(pass: GitCredential) {
84
+ return pass.bearer === undefined
85
+ ? `${pass.remote}\n`
86
+ : `${pass.remote}\n${pass.bearer}\n`;
87
+ }
88
+ function gitCredentialCommand(
89
+ remotePath: string,
90
+ pass: GitCredential,
91
+ command: string,
92
+ ) {
93
+ const read = `{ IFS= read -r _fungi_git_remote && IFS= read -r _fungi_git_bearer; } < ${quote(remotePath)}; rm -f -- ${quote(remotePath)}; test -n "$_fungi_git_remote"`;
94
+ return pass.bearer === undefined
95
+ ? `${read} && FUNGI_GIT_REMOTE="$_fungi_git_remote" bash -c ${quote(command)}`
96
+ : `${read} && test -n "$_fungi_git_bearer" && FUNGI_GIT_REMOTE="$_fungi_git_remote" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="http.$_fungi_git_remote.extraHeader" GIT_CONFIG_VALUE_0="Authorization: Bearer $_fungi_git_bearer" bash -c ${quote(command)}`;
97
+ }
98
+
99
+ export const shedRunCommandInputSchema = runCommandInputSchema.extend({
100
+ gitAccess: resolvedGitAccessSchema.optional(),
101
+ });
102
+ export type ShedRunCommandInput = z.output<typeof shedRunCommandInputSchema>;
103
+ export type ShedRunCommandResult = z.output<typeof runCommandResultSchema>;
104
+
105
+ /**
106
+ * One bounded exec that presents the step's Workflow Git pass exactly as
107
+ * `run` does: `$FUNGI_GIT_REMOTE` plus command-local header config. Without
108
+ * `gitAccess` it is the plain exec.
109
+ */
110
+ export async function runShedCommand(
111
+ input: ShedRunCommandInput,
112
+ computer: Readonly<{
113
+ exec(
114
+ input: Omit<ShedRunCommandInput, "gitAccess">,
115
+ ): Promise<WhistleJsonValue>;
116
+ workflowGitPass?: ShedComputer["workflowGitPass"];
117
+ write?: ShedComputer["write"];
118
+ }>,
119
+ now: () => number = Date.now,
120
+ ): Promise<ShedRunCommandResult> {
121
+ const { gitAccess, ...exec } = input;
122
+ if (gitAccess === undefined)
123
+ return runCommandResultSchema.parse(await computer.exec(exec));
124
+ if (!computer.workflowGitPass || !computer.write)
125
+ return shedComputerRefusalSchema.parse({ error: "not_granted" });
126
+ const minted = z
127
+ .union([workflowGitPassResultSchema, shedComputerRefusalSchema])
128
+ .parse(await computer.workflowGitPass({}));
129
+ if ("error" in minted) return minted;
130
+ if (minted.expiresAt <= now() + 5000)
131
+ throw new Error("Workflow Git pass expires before the command starts");
132
+ const remotePath = `/workspace/.fungi-git-${Hex.encode(crypto.getRandomValues(new Uint8Array(16)))}.remote`;
133
+ const content = gitCredentialFile(minted);
134
+ const written = await computer.write({ path: remotePath, content });
135
+ const refused = shedComputerRefusalSchema.safeParse(written);
136
+ if (refused.success) return refused.data;
137
+ const reply = writeOutputSchema.parse(written);
138
+ if (
139
+ reply.path !== remotePath ||
140
+ reply.bytes !== new TextEncoder().encode(content).byteLength
141
+ )
142
+ throw new Error("Invalid Computer credential file reply");
143
+ return runCommandResultSchema.parse(
144
+ await computer.exec({
145
+ ...exec,
146
+ command: gitCredentialCommand(remotePath, minted, exec.command),
147
+ }),
148
+ );
149
+ }
150
+
151
+ function claimKey(actor: HostActor, method: "start" | "run", key: string) {
152
+ return JSON.stringify([actor, method, key]);
153
+ }
154
+ const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
155
+ // A command that ran to completion is a normal result at any exit code,
156
+ // zero or not: the caller (a Workflow step, for example) tells "the command
157
+ // failed" from "the platform failed" only if a non-zero exit comes back as
158
+ // a value, never as a thrown dispatch failure. Only a genuinely missing
159
+ // exit status -- the Computer's own record of this process was replaced or
160
+ // expired before it observed the exit -- is a platform-level failure to
161
+ // report completion at all, and stays an exception.
162
+ function completion(exitCode: number | null, logPath: string) {
163
+ if (exitCode === null)
164
+ throw new Error(`Process exit status is unavailable. Log: ${logPath}`);
165
+ return { exitCode, logPath };
166
+ }
167
+
168
+ /** The Computer owns execution and exit state; Shed only remembers the call-to-process link. */
169
+ export function createShedRuns(sql: ShedSql, now: () => number = Date.now) {
170
+ // Unscoped records are not current claims; do not read or import the old ledger.
171
+ sql.exec(
172
+ "CREATE TABLE IF NOT EXISTS shed_process_claims (requestKey TEXT PRIMARY KEY, computerId TEXT NOT NULL, command TEXT NOT NULL, cwd TEXT, processId TEXT, phase TEXT NOT NULL, startedAt INTEGER, exitCode INTEGER, logPath TEXT NOT NULL, gitAccess TEXT, passId TEXT, remote TEXT, expiresAt INTEGER)",
173
+ );
174
+ sql.exec(
175
+ "CREATE TABLE IF NOT EXISTS shed_retired_claim_keys (requestKey TEXT PRIMARY KEY)",
176
+ );
177
+ sql.exec(
178
+ "DELETE FROM shed_process_claims WHERE requestKey IN (SELECT requestKey FROM shed_retired_claim_keys)",
179
+ );
180
+ const starting = new Map<string, number | null>();
181
+ function retired(requestKey: string) {
182
+ return (
183
+ sql
184
+ .exec(
185
+ "SELECT requestKey FROM shed_retired_claim_keys WHERE requestKey=?",
186
+ requestKey,
187
+ )
188
+ .toArray().length !== 0
189
+ );
190
+ }
191
+ function scopeRows(
192
+ actor: HostActor,
193
+ mode: "start" | "run",
194
+ computerId: string,
195
+ ) {
196
+ return z
197
+ .array(rowSchema)
198
+ .parse(
199
+ sql
200
+ .exec(
201
+ "SELECT * FROM shed_process_claims WHERE computerId=? AND json_extract(requestKey,'$[0]')=? AND json_extract(requestKey,'$[1]')=? ORDER BY startedAt ASC, processId ASC, rowid ASC",
202
+ computerId,
203
+ JSON.stringify(actor),
204
+ mode,
205
+ )
206
+ .toArray(),
207
+ );
208
+ }
209
+ function retire(requestKey: string, actor: HostActor, mode: "start" | "run") {
210
+ // Record rejection first, so an interruption cannot turn a retired key into a new start.
211
+ sql.exec(
212
+ "INSERT OR IGNORE INTO shed_retired_claim_keys (requestKey) VALUES (?)",
213
+ requestKey,
214
+ );
215
+ sql.exec("DELETE FROM shed_process_claims WHERE requestKey=?", requestKey);
216
+ // Replay refusal covers the latest 64 retired calls per member/method.
217
+ // Older opaque keys are outside the retained replay horizon, like native history.
218
+ sql.exec(
219
+ "DELETE FROM shed_retired_claim_keys WHERE rowid IN (SELECT rowid FROM shed_retired_claim_keys WHERE json_extract(requestKey,'$[0]')=? AND json_extract(requestKey,'$[1]')=? ORDER BY rowid DESC LIMIT -1 OFFSET ?)",
220
+ JSON.stringify(actor),
221
+ mode,
222
+ maximumProcessRecords,
223
+ );
224
+ }
225
+ function trimScope(
226
+ actor: HostActor,
227
+ mode: "start" | "run",
228
+ computerId: string,
229
+ current: string,
230
+ ) {
231
+ const rows = scopeRows(actor, mode, computerId);
232
+ let remaining = rows.length;
233
+ for (const row of rows) {
234
+ if (remaining <= maximumProcessRecords) break;
235
+ if (
236
+ row.requestKey === current ||
237
+ row.phase === "running" ||
238
+ starting.has(row.requestKey)
239
+ )
240
+ continue;
241
+ retire(row.requestKey, actor, mode);
242
+ remaining--;
243
+ }
244
+ }
245
+ async function refreshScope(
246
+ actor: HostActor,
247
+ mode: "start" | "run",
248
+ computerId: string,
249
+ computer: ShedComputer,
250
+ ) {
251
+ const rows = scopeRows(actor, mode, computerId);
252
+ if (rows.length < maximumProcessRecords) return null;
253
+ const reply = await computer.processes({});
254
+ const refused = shedComputerRefusalSchema.safeParse(reply);
255
+ if (refused.success) return refused.data;
256
+ const inventory = processListOutputSchema.parse(reply);
257
+ for (const row of rows) {
258
+ if (row.processId === null || starting.has(row.requestKey)) continue;
259
+ const process = inventory.processes.find(
260
+ (item) => item.id === row.processId,
261
+ );
262
+ if (process) observe(row.requestKey, process, row.logPath);
263
+ }
264
+ return null;
265
+ }
266
+ function read(requestKey: string) {
267
+ if (retired(requestKey)) return null;
268
+ const row = sql
269
+ .exec("SELECT * FROM shed_process_claims WHERE requestKey=?", requestKey)
270
+ .toArray()[0];
271
+ return row === undefined ? null : rowSchema.parse(row);
272
+ }
273
+ function observe(
274
+ requestKey: string,
275
+ process: ComputerProcess,
276
+ logPath: string,
277
+ ) {
278
+ sql.exec(
279
+ "UPDATE shed_process_claims SET processId=?,phase=?,exitCode=?,startedAt=? WHERE requestKey=?",
280
+ process.id,
281
+ process.state === "exited" ? "exited" : "running",
282
+ process.exitCode,
283
+ process.startedAt,
284
+ requestKey,
285
+ );
286
+ return process.port === null
287
+ ? { process, logPath }
288
+ : { process, logPath, port: process.port };
289
+ }
290
+ function verify(
291
+ prior: z.output<typeof rowSchema>,
292
+ input: ShedRunInput,
293
+ computerId: string,
294
+ ) {
295
+ if (
296
+ prior.computerId !== computerId ||
297
+ prior.command !== input.command ||
298
+ prior.cwd !== (input.cwd ?? null) ||
299
+ prior.gitAccess !==
300
+ (input.gitAccess === undefined ? null : JSON.stringify(input.gitAccess))
301
+ )
302
+ throw new AppMethodRefusal(
303
+ "conflict",
304
+ "Run identity conflicts with its original command, Git scope or Computer",
305
+ );
306
+ }
307
+ async function claim(
308
+ input: ShedStartInput,
309
+ computerId: string,
310
+ rawKey: string,
311
+ actor: HostActor,
312
+ computer: ShedComputer,
313
+ mode: "start" | "run",
314
+ ) {
315
+ if (actor.kind !== "member")
316
+ return shedComputerRefusalSchema.parse({ error: "not_granted" });
317
+ const requestKey = claimKey(actor, mode, rawKey);
318
+ if (retired(requestKey))
319
+ return shedComputerRefusalSchema.parse({ error: "not_found" });
320
+ const unavailable = await refreshScope(actor, mode, computerId, computer);
321
+ if (unavailable !== null) return unavailable;
322
+ if (retired(requestKey))
323
+ return shedComputerRefusalSchema.parse({ error: "not_found" });
324
+ const digest = await crypto.subtle.digest(
325
+ "SHA-256",
326
+ new TextEncoder().encode(requestKey),
327
+ );
328
+ const directory = `/workspace/.fungi/runs/call-${Hex.encode(new Uint8Array(digest))}`;
329
+ const remotePath = `/workspace/.fungi-git-${Hex.encode(new Uint8Array(digest))}.remote`;
330
+ const credentialFile = gitCredentialFile;
331
+ function prepare(pass: GitCredential | null) {
332
+ const logPath = `${directory}/out.log`;
333
+ const command =
334
+ pass === null
335
+ ? `bash -c ${quote(input.command)}`
336
+ : gitCredentialCommand(remotePath, pass, input.command);
337
+ const script = `mkdir -p ${quote(directory)} && ${command} 2>&1 | tee ${quote(logPath)}`;
338
+ const processInput: z.input<typeof processInputSchema> = {
339
+ command: `bash -o pipefail -c ${quote(script)}`,
340
+ };
341
+ if (input.cwd !== undefined) processInput.cwd = input.cwd;
342
+ if (input.port !== undefined) processInput.port = input.port;
343
+ return { start: processInputSchema.parse(processInput), logPath };
344
+ }
345
+ async function dispatch(
346
+ start: z.output<typeof processInputSchema>,
347
+ logPath: string,
348
+ pass: GitCredential | null,
349
+ expiresAt: number | null,
350
+ ) {
351
+ const remote = pass?.remote ?? null;
352
+ starting.set(requestKey, input.port ?? null);
353
+ try {
354
+ function checkLease() {
355
+ if (remote !== null && (expiresAt === null || expiresAt <= now()))
356
+ throw new Error(
357
+ `Workflow Git pass expired before process dispatch. Log: ${logPath}`,
358
+ );
359
+ }
360
+ checkLease();
361
+ if (pass !== null) {
362
+ if (!computer.write)
363
+ throw new Error("Computer credential file access is unavailable");
364
+ const content = credentialFile(pass);
365
+ const reply = await computer.write({
366
+ path: remotePath,
367
+ content,
368
+ });
369
+ const refused = shedComputerRefusalSchema.safeParse(reply);
370
+ if (refused.success) {
371
+ sql.exec(
372
+ "DELETE FROM shed_process_claims WHERE requestKey=?",
373
+ requestKey,
374
+ );
375
+ return refused.data;
376
+ }
377
+ const written = writeOutputSchema.parse(reply);
378
+ if (
379
+ written.path !== remotePath ||
380
+ written.bytes !== new TextEncoder().encode(content).byteLength
381
+ )
382
+ throw new Error("Invalid Computer credential file reply");
383
+ }
384
+ checkLease();
385
+ const reply = await computer.processStart(start);
386
+ const refused = shedComputerRefusalSchema.safeParse(reply);
387
+ if (refused.success) {
388
+ sql.exec(
389
+ "DELETE FROM shed_process_claims WHERE requestKey=?",
390
+ requestKey,
391
+ );
392
+ return refused.data;
393
+ }
394
+ const process = computerProcessSchema.parse(reply);
395
+ if (process.port !== (input.port ?? null))
396
+ throw new Error("Run identity conflicts with its declared port");
397
+ return observe(requestKey, process, logPath);
398
+ } finally {
399
+ starting.delete(requestKey);
400
+ trimScope(actor, mode, computerId, requestKey);
401
+ }
402
+ }
403
+ const gitAccess =
404
+ input.gitAccess === undefined ? null : JSON.stringify(input.gitAccess);
405
+ async function mint() {
406
+ if (!computer.workflowGitPass)
407
+ throw new Error("Workflow Git pass binding is unavailable");
408
+ const pass = workflowGitPassResultSchema.parse(
409
+ await computer.workflowGitPass({}),
410
+ );
411
+ if (pass.expiresAt <= now() + 5000)
412
+ throw new Error("Workflow Git pass expires before the next poll");
413
+ return pass;
414
+ }
415
+ async function resume(prior: z.output<typeof rowSchema>) {
416
+ verify(prior, input, computerId);
417
+ if (prior.phase === "starting") {
418
+ const activePort = starting.get(requestKey);
419
+ if (activePort !== undefined) {
420
+ if (activePort !== (input.port ?? null))
421
+ throw new AppMethodRefusal(
422
+ "conflict",
423
+ "Run identity conflicts with its declared port",
424
+ );
425
+ return pending(5000);
426
+ }
427
+ // The host binding retains this call's native idempotency key across retries.
428
+ if (mode === "start") {
429
+ // Renew the same selector rather than storing a header credential.
430
+ let pass: GitCredential | null = null;
431
+ if (prior.remote !== null) {
432
+ if (prior.expiresAt === null || prior.expiresAt <= now())
433
+ throw new Error(
434
+ `Workflow Git pass expired before process dispatch. Log: ${prior.logPath}`,
435
+ );
436
+ const renewed = await mint();
437
+ if (
438
+ renewed.passId !== prior.passId ||
439
+ renewed.remote !== prior.remote
440
+ )
441
+ throw new Error(
442
+ `Workflow Git renewal changed its original selector. Log: ${prior.logPath}`,
443
+ );
444
+ pass = renewed;
445
+ }
446
+ const prepared = prepare(pass);
447
+ return dispatch(prepared.start, prior.logPath, pass, prior.expiresAt);
448
+ }
449
+ throw new Error(
450
+ `Process start outcome is unavailable. Log: ${prior.logPath}`,
451
+ );
452
+ }
453
+ const { processes } = processListOutputSchema.parse(
454
+ await computer.processes({}),
455
+ );
456
+ const process = processes.find((item) => item.id === prior.processId);
457
+ if (!process)
458
+ throw new Error(
459
+ `Process is no longer in the Computer inventory. Log: ${prior.logPath}`,
460
+ );
461
+ if (process.port !== (input.port ?? null))
462
+ throw new AppMethodRefusal(
463
+ "conflict",
464
+ "Run identity conflicts with its declared port",
465
+ );
466
+ if (mode === "run" && process.state !== "exited" && gitAccess !== null) {
467
+ if (prior.expiresAt === null || prior.expiresAt <= now())
468
+ throw new Error(`Workflow Git pass expired. Log: ${prior.logPath}`);
469
+ const pass = await mint();
470
+ if (
471
+ prior.expiresAt <= now() ||
472
+ pass.passId !== prior.passId ||
473
+ pass.remote !== prior.remote ||
474
+ pass.expiresAt < prior.expiresAt
475
+ )
476
+ throw new Error(
477
+ `Workflow Git renewal changed or expired its original selector. Log: ${prior.logPath}`,
478
+ );
479
+ sql.exec(
480
+ "UPDATE shed_process_claims SET expiresAt=MAX(expiresAt,?) WHERE requestKey=?",
481
+ pass.expiresAt,
482
+ requestKey,
483
+ );
484
+ }
485
+ return observe(requestKey, process, prior.logPath);
486
+ }
487
+ const prior = read(requestKey);
488
+ if (prior) return resume(prior);
489
+ // Mint can yield; recheck the durable claim before starting a process.
490
+ const pass: WorkflowGitPassResult | null =
491
+ gitAccess === null ? null : await mint();
492
+ const claimed = read(requestKey);
493
+ if (claimed) return resume(claimed);
494
+ const { start, logPath } = prepare(pass);
495
+ sql.exec(
496
+ "INSERT INTO shed_process_claims (requestKey,computerId,command,cwd,phase,logPath,gitAccess,passId,remote,expiresAt) VALUES (?,?,?,?,'starting',?,?,?,?,?)",
497
+ requestKey,
498
+ computerId,
499
+ input.command,
500
+ input.cwd ?? null,
501
+ logPath,
502
+ gitAccess,
503
+ pass?.passId ?? null,
504
+ pass?.remote ?? null,
505
+ pass?.expiresAt ?? null,
506
+ );
507
+ return dispatch(start, logPath, pass, pass?.expiresAt ?? null);
508
+ }
509
+ return {
510
+ owns(processId: string, computerId: string, actor: HostActor) {
511
+ if (actor.kind !== "member") return false;
512
+ return (
513
+ sql
514
+ .exec(
515
+ "SELECT requestKey FROM shed_process_claims WHERE processId=? AND computerId=? AND json_extract(requestKey,'$[0]')=?",
516
+ processId,
517
+ computerId,
518
+ JSON.stringify(actor),
519
+ )
520
+ .toArray().length !== 0
521
+ );
522
+ },
523
+ logPath(
524
+ rawKey: string,
525
+ computerId: string,
526
+ actor: HostActor,
527
+ ): { path: string } | { error: "not_found" | "ambiguous" } {
528
+ const rows = [
529
+ read(claimKey(actor, "start", rawKey)),
530
+ read(claimKey(actor, "run", rawKey)),
531
+ ].filter((row) => row !== null && row.processId !== null);
532
+ if (rows.length > 1) return { error: "ambiguous" };
533
+ const row = rows[0];
534
+ return row?.computerId === computerId
535
+ ? { path: row.logPath }
536
+ : { error: "not_found" };
537
+ },
538
+ start(
539
+ input: ShedStartInput,
540
+ computerId: string,
541
+ requestKey: string,
542
+ actor: HostActor,
543
+ computer: ShedComputer,
544
+ ) {
545
+ return claim(input, computerId, requestKey, actor, computer, "start");
546
+ },
547
+ async run(
548
+ input: ShedRunInput,
549
+ computerId: string,
550
+ requestKey: string,
551
+ actor: HostActor,
552
+ computer: ShedComputer,
553
+ ) {
554
+ if (actor.kind !== "member")
555
+ return shedComputerRefusalSchema.parse({ error: "not_granted" });
556
+ if (retired(claimKey(actor, "run", requestKey)))
557
+ return shedComputerRefusalSchema.parse({ error: "not_found" });
558
+ const prior = read(claimKey(actor, "run", requestKey));
559
+ if (prior) {
560
+ verify(prior, input, computerId);
561
+ if (prior.phase === "exited")
562
+ return completion(prior.exitCode, prior.logPath);
563
+ }
564
+ const result = await claim(
565
+ input,
566
+ computerId,
567
+ requestKey,
568
+ actor,
569
+ computer,
570
+ "run",
571
+ );
572
+ if ("kind" in result) return result;
573
+ if ("error" in result) return result;
574
+ return result.process.state === "exited"
575
+ ? completion(result.process.exitCode, result.logPath)
576
+ : pending(5000);
577
+ },
578
+ };
579
+ }
580
+ export type ShedRuns = ReturnType<typeof createShedRuns>;
@@ -0,0 +1 @@
1
+ export { DurableObject } from "../../../../packages/shiitake/test/fixtures/cloudflare-workers.fixture.js";