@fungi.computer/computer 0.1.0 → 0.2.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 (39) hide show
  1. package/README.md +38 -10
  2. package/dist/computer-backends.d.ts +0 -1
  3. package/dist/computer-backends.js +0 -3
  4. package/dist/exec-output.d.ts +13 -0
  5. package/dist/exec-output.js +24 -0
  6. package/dist/execution-lifecycle.d.ts +35 -0
  7. package/dist/execution-lifecycle.js +78 -0
  8. package/dist/files-patch.d.ts +12 -4
  9. package/dist/files-patch.js +8 -14
  10. package/dist/files.d.ts +65 -15
  11. package/dist/files.js +152 -31
  12. package/dist/fleet/index.d.ts +1 -1
  13. package/dist/fleet/index.js +1 -1
  14. package/dist/fleet/inventory-wire.d.ts +6 -2
  15. package/dist/fleet/inventory-wire.js +7 -2
  16. package/dist/fleet/lifecycle.d.ts +7 -1
  17. package/dist/fleet/lifecycle.js +22 -0
  18. package/dist/fleet/wire.d.ts +12 -12
  19. package/dist/fleet/wire.js +1 -1
  20. package/dist/index.d.ts +33 -15
  21. package/dist/index.js +422 -105
  22. package/dist/internet-access.d.ts +4 -0
  23. package/dist/internet-access.js +14 -0
  24. package/dist/local-backend-replay.d.ts +10 -0
  25. package/dist/local-backend-replay.js +43 -0
  26. package/dist/local-backend.d.ts +2 -1
  27. package/dist/local-backend.js +4 -4
  28. package/dist/on-box-vm.d.ts +11 -0
  29. package/dist/on-box-vm.js +36 -13
  30. package/dist/operation-record.d.ts +1 -1
  31. package/dist/operation-record.js +1 -1
  32. package/dist/process-inventory.d.ts +3 -1
  33. package/dist/process-inventory.js +2 -2
  34. package/dist/team-candidate-builder.mjs +8375 -334
  35. package/package.json +17 -24
  36. package/dist/light-shell.d.ts +0 -44
  37. package/dist/light-shell.js +0 -376
  38. package/dist/operation-contract.d.ts +0 -456
  39. package/dist/operation-contract.js +0 -450
package/dist/index.js CHANGED
@@ -50,6 +50,9 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
+ import { boundExecStream } from "./exec-output.js";
54
+ export { createLocalBackendReplayStore } from "./local-backend-replay.js";
55
+ import { computerInternetAccess } from "./internet-access.js";
53
56
  import { getWorkspace, SQLiteWorkspaceProvider, Workspace, WorkspaceProxy, WorkspaceServiceProxy, } from "@fungi.computer/cloudflare-computer";
54
57
  import { WorkerShellBackend, } from "@fungi.computer/cloudflare-computer/backends/worker-shell";
55
58
  import jqCommands from "@fungi.computer/cloudflare-computer/shell/jq";
@@ -58,16 +61,19 @@ import xanCommands from "@fungi.computer/cloudflare-computer/shell/xan";
58
61
  import fileCommands from "@fungi.computer/cloudflare-computer/shell/file";
59
62
  import { withWorkspaceContainer, } from "@fungi.computer/cloudflare-computer/backends/container";
60
63
  import { DurableObject } from "cloudflare:workers";
64
+ import * as Effect from "effect/Effect";
65
+ import * as Redacted from "effect/Redacted";
61
66
  import { z } from "zod";
62
67
  import { SystemTrustContainerBackend } from "./container-system-trust.js";
63
68
  import { computerTransportBackend } from "./transport-backend.js";
64
69
  import { PullPendingTracker } from "./pull-pending.js";
65
- import { ComputerFileConflict, ComputerFileRangeError, ComputerFileTooLargeError, capabilityNameSchema, computerValueSchema, computerCapabilities, computerPortInputSchema, computerPortOutputSchema, computerPortRemovalOutputSchema, computerPortsOutputSchema, computerPortSchema, ComputerPortLimitError, ComputerPortReservedError, computerTerminalPort, isComputerReservedPort, maximumExposedPorts, maximumProcessRecords, execInputSchema, execOutputSchema, computerToolResult, computerExecutionRequestSchema, genericComputerExecutionBudget, mintComputerRequestId, listInputSchema, listOutputSchema, maximumFileBytes, maximumStreamBytes, mkdirInputSchema, mkdirOutputSchema, readInputSchema, readBytesInputSchema, readOutputSchema, replaceInputSchema, writeInputSchema, writeOutputSchema, processIdInputSchema, processKillOutputSchema, processListOutputSchema, processStartInputSchema, processStartToolInputSchema, ComputerProcessNotFoundError, } from "./operation-contract.js";
66
- import { createWorkspaceFilesPort, readBoundedWorkspaceText } from "./files.js";
70
+ import { ComputerFileConflict, ComputerFileRangeError, ComputerFileTooLargeError, capabilityNameSchema, computerValueSchema, computerCapabilities, computerPortInputSchema, computerPortOutputSchema, computerPortRemovalOutputSchema, computerPortsOutputSchema, computerPortSchema, ComputerPortLimitError, ComputerPortReservedError, computerTerminalPort, isComputerReservedPort, maximumExposedPorts, maximumProcessRecords, execInputSchema, execOutputSchema, computerToolResult, computerExecutionRequestSchema, genericComputerExecutionBudget, mintComputerRequestId, listInputSchema, listOutputSchema, maximumFileBytes, mkdirInputSchema, mkdirOutputSchema, readInputSchema, readBytesInputSchema, readOutputSchema, replaceInputSchema, writeInputSchema, writeOutputSchema, processIdInputSchema, processKillOutputSchema, processListOutputSchema, processStartInputSchema, processStartToolInputSchema, ComputerProcessNotFoundError, } from "@fungi.computer/computer-contract";
71
+ import { createWorkspaceFilesPort, readBoundedWorkspaceText, listWorkspaceFiles, } from "./files.js";
67
72
  import { computerExecutionResult } from "./execution-result.js";
68
73
  import { ComputerOperationRecord } from "./operation-record.js";
74
+ import { createComputerExecutionLifecycle } from "./execution-lifecycle.js";
69
75
  import { applyWorkspacePatch } from "./files-patch.js";
70
- import { ComputerProcessInventory, drainProcessExit, observeProcess, } from "./process-inventory.js";
76
+ import { ComputerProcessInventory, drainProcessExit, failedObservation, observeProcess, } from "./process-inventory.js";
71
77
  import { assertComputerContainersEnabled, computerContainerSleepAfterMs, ComputerContainersPausedError, configureComputerContainer, hostedComputerContainersPaused, } from "./container-policy.js";
72
78
  import { OnBoxVmContainerHost, onBoxVmPlacement, } from "./on-box-vm.js";
73
79
  import { attachedBackendId, describeAttachedBackend, routeComputerBackend, } from "./computer-backends.js";
@@ -96,23 +102,6 @@ const terminalQuerySchema = z.lazy(() => z.strictObject({
96
102
  cols: z.coerce.number().int().min(2).max(500).catch(terminalDefaultCols),
97
103
  rows: z.coerce.number().int().min(2).max(300).catch(terminalDefaultRows),
98
104
  }));
99
- function entryType(entry) {
100
- if (entry.isFile)
101
- return "file";
102
- if (entry.isDirectory)
103
- return "directory";
104
- return "symlink";
105
- }
106
- function truncate(value) {
107
- const bytes = utf8.encode(value);
108
- if (bytes.byteLength <= maximumStreamBytes) {
109
- return { value, truncated: false };
110
- }
111
- return {
112
- value: new TextDecoder().decode(bytes.slice(0, maximumStreamBytes)),
113
- truncated: true,
114
- };
115
- }
116
105
  async function read(workspace, input) {
117
106
  const { path } = readInputSchema.parse(input);
118
107
  const stat = await workspace.fs.stat(path);
@@ -127,21 +116,7 @@ async function read(workspace, input) {
127
116
  });
128
117
  }
129
118
  async function list(workspace, input) {
130
- const { path, limit, offset } = listInputSchema.parse(input);
131
- const rows = await workspace.fs.readdir(path, {
132
- limit: limit + 1,
133
- offset,
134
- });
135
- return listOutputSchema.parse({
136
- path,
137
- entries: rows.slice(0, limit).map((entry) => ({
138
- name: entry.name,
139
- size: entry.size,
140
- modifiedAt: entry.mtime,
141
- type: entryType(entry),
142
- })),
143
- nextOffset: rows.length > limit ? offset + limit : null,
144
- });
119
+ return listOutputSchema.parse(await listWorkspaceFiles(workspace, listInputSchema.parse(input)));
145
120
  }
146
121
  async function mkdir(workspace, input) {
147
122
  const { path, recursive } = mkdirInputSchema.parse(input);
@@ -156,7 +131,7 @@ async function write(workspace, input) {
156
131
  bytes: utf8.encode(content).byteLength,
157
132
  });
158
133
  }
159
- async function exec(workspace, input, backend, signal) {
134
+ async function exec(workspace, input, backend, signal, execution) {
160
135
  const env_1 = { stack: [], error: void 0, hasError: false };
161
136
  try {
162
137
  // The host request has already been decoded under its admitted budget.
@@ -165,15 +140,23 @@ async function exec(workspace, input, backend, signal) {
165
140
  const options = cwd === undefined
166
141
  ? { encoding: "utf8", timeoutMs, ...backendOption }
167
142
  : { encoding: "utf8", timeoutMs, cwd, ...backendOption };
168
- const run = __addDisposableResource(env_1, await workspace.runtime.exec(command, options), false);
143
+ const nativeOptions = {
144
+ ...options,
145
+ ...execution?.carriage,
146
+ };
147
+ if (execution !== undefined)
148
+ nativeOptions.id = execution.invocation.id;
149
+ execution?.dispatch();
150
+ const run = __addDisposableResource(env_1, await workspace.runtime.exec(command, nativeOptions), false);
151
+ execution?.opened();
169
152
  const result = await computerExecutionResult(run, signal);
170
153
  const synchronizationFailure = result.sync.status === "pending"
171
154
  ? `Computer command filesystem synchronization failed: ${result.sync.error}`
172
155
  : null;
173
- const stdout = truncate(result.stdout);
174
- const stderr = truncate(synchronizationFailure === null
156
+ const stdout = boundExecStream(result.stdout, "stdout");
157
+ const stderr = boundExecStream(synchronizationFailure === null
175
158
  ? result.stderr
176
- : `${synchronizationFailure}${result.stderr.length === 0 ? "" : `\n${result.stderr}`}`);
159
+ : `${synchronizationFailure}${result.stderr.length === 0 ? "" : `\n${result.stderr}`}`, "stderr");
177
160
  return execOutputSchema.parse({
178
161
  status: synchronizationFailure === null ? result.status : "failed",
179
162
  exitCode: synchronizationFailure !== null && result.exitCode === 0
@@ -181,7 +164,9 @@ async function exec(workspace, input, backend, signal) {
181
164
  : result.exitCode,
182
165
  stdout: stdout.value,
183
166
  stderr: stderr.value,
184
- truncated: stdout.truncated || stderr.truncated,
167
+ truncated: stdout.truncation.length + stderr.truncation.length === 0
168
+ ? false
169
+ : [...stdout.truncation, ...stderr.truncation],
185
170
  });
186
171
  }
187
172
  catch (e_1) {
@@ -295,7 +280,7 @@ class ContainerComputer extends ContainerComputerBase {
295
280
  attachedBackendId(candidate) === backendId);
296
281
  if (backend === undefined ||
297
282
  computerName !== this.state.id.name ||
298
- !(await verifyLocalBackendDial(request, computerName, backend)))
283
+ !(await verifyLocalBackendDial(request, computerName, backend, this.state.storage)))
299
284
  return new Response(null, { status: 404 });
300
285
  // A verified plain request lets the machine tell attached from detached.
301
286
  if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket")
@@ -323,9 +308,10 @@ class ContainerComputer extends ContainerComputerBase {
323
308
  * The hosted backend that terminals, processes and ports use: the first in
324
309
  * the default order, or none when only local machines are attached.
325
310
  */
326
- hostedRoute() {
311
+ hostedRoute(backend) {
327
312
  const route = routeComputerBackend(this.attachedBackends(), () => false, {
328
313
  need: "hosted",
314
+ backend,
329
315
  });
330
316
  if (route.kind !== "routed" || route.backend.kind === "local")
331
317
  return undefined;
@@ -456,9 +442,8 @@ class ContainerComputer extends ContainerComputerBase {
456
442
  }
457
443
  /**
458
444
  * The current Workspace once its mount index is ready and its guest mount
459
- * root exists. Every backend mounts `/workspace`, so a host write there
460
- * (such as a Workflow step's one-shot Git credential file) must not depend
461
- * on a guest having booted first.
445
+ * root exists. Every backend mounts `/workspace`, so an ordinary host file
446
+ * write there must not depend on a guest having booted first.
462
447
  */
463
448
  async readyWorkspace() {
464
449
  const workspace = this.workspace();
@@ -496,9 +481,20 @@ class ContainerComputer extends ContainerComputerBase {
496
481
  }
497
482
  /** One upstream Cloudflare Workspace that advertises and runs its capabilities. */
498
483
  export class ComputerDO extends ContainerComputer {
484
+ constructor(ctx, env) {
485
+ super(ctx, env);
486
+ // Reconstruction revokes unfinished exec authority before the first RPC.
487
+ void this.#executions;
488
+ }
489
+ /** Host-admitted settings write; egress reads the current durable value. */
490
+ internetAccess(enabled) {
491
+ return computerInternetAccess(this.state.storage.sql, enabled);
492
+ }
499
493
  #binaryFileTail = Promise.resolve(undefined);
500
494
  #operationRecord;
501
495
  #processRecord;
496
+ #executionRecord;
497
+ #processStarts = new Map();
502
498
  #terminalSync;
503
499
  get #processes() {
504
500
  this.#processRecord ??= new ComputerProcessInventory(this.state.storage, () => Date.now());
@@ -508,6 +504,128 @@ export class ComputerDO extends ContainerComputer {
508
504
  this.#operationRecord ??= new ComputerOperationRecord(this.state.storage, () => Date.now());
509
505
  return this.#operationRecord;
510
506
  }
507
+ /** Ordinary Computers have no credential host; resource hosts override it. */
508
+ executionHooks() {
509
+ return undefined;
510
+ }
511
+ get #executions() {
512
+ if (this.#executionRecord === undefined) {
513
+ // Initialize the existing retention owners before their dependent table.
514
+ void this.#operations;
515
+ void this.#processes;
516
+ const executions = createComputerExecutionLifecycle(this.state.storage);
517
+ this.#executionRecord = executions;
518
+ for (const interrupted of executions.reconstructed) {
519
+ if (interrupted.invocation.kind === "process") {
520
+ // An opening process never crossed the synchronous SDK fence.
521
+ const { id } = processIdInputSchema.parse({
522
+ id: interrupted.invocation.id,
523
+ });
524
+ this.#processes.release(id);
525
+ }
526
+ this.state.waitUntil(Promise.resolve().then(() => this.#deliverExecutionClose(interrupted, "reconstructed")));
527
+ }
528
+ }
529
+ return this.#executionRecord;
530
+ }
531
+ async #deliverExecutionClose(record, reason, hooks) {
532
+ try {
533
+ await (hooks ?? this.executionHooks())?.close(record.invocation, record.context, reason);
534
+ }
535
+ catch (cause) {
536
+ // A host error may contain credential-bearing transport data.
537
+ Effect.runSync(Effect.logError("Computer host execution close is unavailable", Redacted.make(cause)));
538
+ }
539
+ }
540
+ async #closeExecution(id, reason, hooks) {
541
+ const record = this.#executions.close(id, reason);
542
+ if (record !== undefined)
543
+ await this.#deliverExecutionClose(record, reason, hooks);
544
+ }
545
+ /**
546
+ * Admission observes the actual native subscriber, never App completion,
547
+ * output silence, an accepted signal or this isolate's warm state.
548
+ */
549
+ async executionIsLive(invocationId) {
550
+ const record = this.#executions.get(invocationId);
551
+ if (record === undefined ||
552
+ record.phase === "closed" ||
553
+ record.phase === "opening" ||
554
+ (record.phase === "open" && !record.nativeOpened) ||
555
+ (record.phase === "dispatching" &&
556
+ (record.invocation.kind !== "process" ||
557
+ this.#processStarts.has(invocationId))))
558
+ return false;
559
+ let route;
560
+ try {
561
+ const selected = this.hostedRoute(record.backend);
562
+ if (selected === undefined)
563
+ throw new ComputerBackendUnavailableError("no_backend");
564
+ route = selected;
565
+ }
566
+ catch {
567
+ await this.#closeExecution(invocationId, "unknown");
568
+ return false;
569
+ }
570
+ let workspace;
571
+ try {
572
+ if (!(await this.containerRunning(route.backing))) {
573
+ if (record.nativeOpened)
574
+ await this.#closeExecution(invocationId, "native_exit");
575
+ return false;
576
+ }
577
+ workspace = await this.readyWorkspace();
578
+ }
579
+ catch {
580
+ return false;
581
+ }
582
+ const runtime = processRuntime(workspace, route.id);
583
+ let observed;
584
+ let archivedExit = false;
585
+ try {
586
+ const env_2 = { stack: [], error: void 0, hasError: false };
587
+ try {
588
+ // Hold both handles unread. A live execution admits only one subscriber;
589
+ // two successful gets therefore prove archived native completion.
590
+ const first = __addDisposableResource(env_2, await runtime.getExec(invocationId, { resume: "tail" }), false);
591
+ void first;
592
+ const second = __addDisposableResource(env_2, await runtime.getExec(invocationId, { resume: "tail" }), false);
593
+ void second;
594
+ archivedExit = true;
595
+ observed = { state: "exited", exitCode: null, sync: "unknown" };
596
+ }
597
+ catch (e_2) {
598
+ env_2.error = e_2;
599
+ env_2.hasError = true;
600
+ }
601
+ finally {
602
+ __disposeResources(env_2);
603
+ }
604
+ }
605
+ catch (cause) {
606
+ observed = failedObservation(cause instanceof Error ? cause : undefined);
607
+ }
608
+ if (observed.state === "running") {
609
+ this.#executions.opened(invocationId);
610
+ return this.#executions.get(invocationId)?.phase === "open";
611
+ }
612
+ if (observed.state === "exited" && (record.nativeOpened || archivedExit))
613
+ await this.#closeExecution(invocationId, "native_exit");
614
+ return false;
615
+ }
616
+ /** Caller-driven renewal only; a Git HTTP liveness check never renews. */
617
+ async #renewExecution(invocationId) {
618
+ const record = this.#executions.get(invocationId);
619
+ if (record === undefined || !(await this.executionIsLive(invocationId)))
620
+ return;
621
+ try {
622
+ await this.executionHooks()?.renew(record.invocation, record.context);
623
+ }
624
+ catch (cause) {
625
+ await this.#closeExecution(invocationId, "unknown");
626
+ Effect.runSync(Effect.logError("Computer host execution renewal is unavailable", Redacted.make(cause)));
627
+ }
628
+ }
511
629
  #ensureExposedPorts() {
512
630
  void this.#processes;
513
631
  }
@@ -843,7 +961,7 @@ export class ComputerDO extends ContainerComputer {
843
961
  return await this.callAfterAcceptance(capabilityId, input, () => undefined);
844
962
  }
845
963
  /** Resource hosts may accept authority once the Workspace is ready to dispatch. */
846
- async callAfterAcceptance(capabilityId, input, accept) {
964
+ async callAfterAcceptance(capabilityId, input, accept, executionContext) {
847
965
  const selected = capabilityNameSchema.parse(capabilityId);
848
966
  if (selected === "bash" || selected === "exec") {
849
967
  accept();
@@ -851,11 +969,11 @@ export class ComputerDO extends ContainerComputer {
851
969
  return computerToolResult(await this.executeOperationAfterAcceptance({
852
970
  ...execInputSchema.parse(input),
853
971
  requestId: mintComputerRequestId(),
854
- }, () => undefined, selected));
972
+ }, () => undefined, selected, undefined, undefined, executionContext));
855
973
  }
856
974
  switch (selected) {
857
975
  case "start_process":
858
- return await this.startProcessAfterAcceptance(input, accept);
976
+ return await this.startProcessAfterAcceptance(input, accept, executionContext);
859
977
  case "list_processes":
860
978
  return await this.listProcessesAfterAcceptance(accept);
861
979
  case "kill_process":
@@ -888,15 +1006,22 @@ export class ComputerDO extends ContainerComputer {
888
1006
  * Execute a host request once, after the host accepts current authority.
889
1007
  * `exec` runs on the Workspace's built-in Worker shell unless it names an
890
1008
  * attached backend; `bash` always runs on an attached backend, the named one
891
- * or the first available in the default order.
1009
+ * or the first available in the default order. Host-only `hosted` execution
1010
+ * excludes local backends, including an explicitly named local backend.
892
1011
  */
893
- async executeOperationAfterAcceptance(input, accept, kind, budget = genericComputerExecutionBudget, signal) {
1012
+ async executeOperationAfterAcceptance(input, accept, kind, budget = genericComputerExecutionBudget, signal, executionContext) {
894
1013
  const { requestId, ...command } = computerExecutionRequestSchema(budget).parse(input);
895
1014
  const route = kind === "exec" && command.backend === undefined
896
1015
  ? undefined
897
- : this.routeBackend(command.backend, "exec");
1016
+ : this.routeBackend(command.backend, kind === "hosted" ? "hosted" : "exec");
1017
+ if (executionContext !== undefined) {
1018
+ if (route === undefined)
1019
+ throw new Error("Host execution requires a hosted native backend");
1020
+ // Keep the selected backend; never redirect credential-bearing work from
1021
+ // a local backend to a different hosted one.
1022
+ this.routeBackend(route.id, "hosted");
1023
+ }
898
1024
  const run = async () => {
899
- const workspace = await this.readyWorkspace();
900
1025
  const inputHash = await sha256(JSON.stringify({
901
1026
  kind,
902
1027
  command: command.command,
@@ -907,16 +1032,61 @@ export class ComputerDO extends ContainerComputer {
907
1032
  const operations = this.#operations;
908
1033
  signal?.throwIfAborted();
909
1034
  const recorded = operations.admit(requestId, inputHash, accept);
910
- if (recorded !== undefined)
1035
+ if (recorded !== undefined) {
1036
+ if (executionContext !== undefined)
1037
+ await this.#renewExecution(`exec:${requestId}`);
911
1038
  return recorded;
1039
+ }
912
1040
  operations.markRunning(requestId);
1041
+ let hooks;
1042
+ const invocation = {
1043
+ id: `exec:${requestId}`,
1044
+ kind: "exec",
1045
+ };
1046
+ let completed = false;
913
1047
  try {
914
- operations.settle(requestId, await exec(workspace, command, route?.id, signal));
1048
+ const workspace = await this.readyWorkspace();
1049
+ hooks =
1050
+ executionContext === undefined ? undefined : this.executionHooks();
1051
+ let execution;
1052
+ if (executionContext !== undefined) {
1053
+ if (hooks === undefined || route === undefined)
1054
+ throw new Error("Computer host execution is unavailable");
1055
+ this.#executions.begin(invocation, route.id, executionContext);
1056
+ // The host's last awaited authority fence follows NEW admission.
1057
+ const carriage = await hooks.open(invocation, executionContext);
1058
+ execution = {
1059
+ invocation,
1060
+ carriage,
1061
+ dispatch: () => {
1062
+ signal?.throwIfAborted();
1063
+ this.#executions.dispatch(invocation.id);
1064
+ },
1065
+ opened: () => {
1066
+ this.#executions.opened(invocation.id);
1067
+ },
1068
+ };
1069
+ }
1070
+ const output = await exec(workspace, command, route?.id, signal, execution);
1071
+ completed = true;
1072
+ operations.settle(requestId, output);
915
1073
  }
916
1074
  catch (cause) {
917
- operations.settleUnknown(requestId, cause instanceof Error
918
- ? `${cause.name}: ${cause.message}`
919
- : "Computer execution rejected without an Error");
1075
+ operations.settleUnknown(requestId, executionContext !== undefined
1076
+ ? "Computer hosted execution outcome is unavailable"
1077
+ : cause instanceof Error
1078
+ ? `${cause.name}: ${cause.message}`
1079
+ : "Computer execution rejected without an Error");
1080
+ }
1081
+ finally {
1082
+ if (hooks !== undefined && executionContext !== undefined) {
1083
+ const phase = this.#executions.get(invocation.id)?.phase;
1084
+ await this.#closeExecution(invocation.id, completed
1085
+ ? "native_exit"
1086
+ : phase === "dispatching" || phase === "open"
1087
+ ? "unknown"
1088
+ : "refused_start", hooks);
1089
+ }
920
1090
  }
921
1091
  return operations.status(requestId, () => undefined);
922
1092
  };
@@ -937,22 +1107,33 @@ export class ComputerDO extends ContainerComputer {
937
1107
  return await this.startProcessAfterAcceptance(input, () => undefined);
938
1108
  }
939
1109
  /** The host accepts authority when the running slot is reserved. */
940
- async startProcessAfterAcceptance(input, accept) {
1110
+ async startProcessAfterAcceptance(input, accept, executionContext) {
941
1111
  const start = processStartInputSchema.parse(input);
942
1112
  const { command, cwd } = start;
943
1113
  const inputHash = await sha256(JSON.stringify({ command, cwd: cwd ?? null, port: start.port ?? null }));
944
1114
  const replay = this.#processes.replay(start, inputHash, accept);
945
- if (replay !== undefined)
946
- return replay;
1115
+ if (replay !== undefined) {
1116
+ if (executionContext !== undefined)
1117
+ await this.#renewExecution(replay.id);
1118
+ return this.#processReceipt(replay, executionContext !== undefined);
1119
+ }
947
1120
  if (start.port !== undefined && isComputerReservedPort(start.port))
948
1121
  throw new ComputerPortReservedError(start.port);
949
1122
  const backend = this.requireHostedRoute().id;
950
1123
  const workspace = await this.readyWorkspace();
951
1124
  const admission = this.#processes.admit(start, inputHash, accept);
952
- if (!admission.created)
953
- return admission.process;
1125
+ if (!admission.created) {
1126
+ if (executionContext !== undefined)
1127
+ await this.#renewExecution(admission.process.id);
1128
+ return this.#processReceipt(admission.process, executionContext !== undefined);
1129
+ }
954
1130
  const admitted = admission.process;
955
1131
  const id = admitted.id;
1132
+ const invocation = { id, kind: "process" };
1133
+ const control = { cancelled: false };
1134
+ this.#processStarts.set(id, control);
1135
+ let hooks;
1136
+ let dispatched = false;
956
1137
  let run;
957
1138
  try {
958
1139
  // timeoutMs 0 disables computerd's default 320 s command deadline.
@@ -964,9 +1145,38 @@ export class ComputerDO extends ContainerComputer {
964
1145
  };
965
1146
  if (cwd !== undefined)
966
1147
  options.cwd = cwd;
967
- run = await workspace.runtime.exec(command, options);
1148
+ if (executionContext !== undefined) {
1149
+ hooks = this.executionHooks();
1150
+ if (hooks === undefined)
1151
+ throw new Error("Computer host execution is unavailable");
1152
+ this.#executions.begin(invocation, backend, executionContext);
1153
+ const carriage = await hooks.open(invocation, executionContext);
1154
+ if (carriage.env !== undefined)
1155
+ options.env = carriage.env;
1156
+ if (carriage.stdin !== undefined)
1157
+ options.stdin = carriage.stdin;
1158
+ }
1159
+ // No awaited work belongs between this fence and the SDK invocation.
1160
+ if (control.cancelled)
1161
+ throw new Error("Computer process start was cancelled");
1162
+ if (executionContext !== undefined)
1163
+ this.#executions.dispatch(id);
1164
+ dispatched = true;
1165
+ control.dispatch = workspace.runtime.exec(command, options);
1166
+ run = await control.dispatch;
1167
+ if (executionContext !== undefined)
1168
+ this.#executions.opened(id);
968
1169
  }
969
1170
  catch (error) {
1171
+ if (executionContext !== undefined) {
1172
+ await this.#closeExecution(id, dispatched ? "unknown" : "refused_start", hooks);
1173
+ if (!dispatched) {
1174
+ this.#processes.release(id);
1175
+ throw error;
1176
+ }
1177
+ // A failed dispatch reply is not proof that native spawn failed.
1178
+ return this.#settle(id, { state: "uncertain" });
1179
+ }
970
1180
  // A transport failure after dispatch may still have started it.
971
1181
  const observed = await observeProcess(processRuntime(workspace, backend), id);
972
1182
  if (observed.state === "exited" && observed.exitCode === null) {
@@ -975,9 +1185,20 @@ export class ComputerDO extends ContainerComputer {
975
1185
  }
976
1186
  return this.#settle(id, observed);
977
1187
  }
1188
+ finally {
1189
+ this.#processStarts.delete(id);
1190
+ }
978
1191
  this.state.waitUntil(this.#watchProcess(id, run));
979
1192
  return admitted;
980
1193
  }
1194
+ #processReceipt(process, requiresContext = false) {
1195
+ const execution = this.#executions.get(process.id);
1196
+ return process.state === "running" &&
1197
+ (execution?.phase === "closed" ||
1198
+ (requiresContext && execution === undefined))
1199
+ ? { ...process, state: "uncertain" }
1200
+ : process;
1201
+ }
981
1202
  /** List this Computer's processes, reconciling live ones with computerd. */
982
1203
  async listProcesses() {
983
1204
  return await this.listProcessesAfterAcceptance(() => undefined);
@@ -990,28 +1211,55 @@ export class ComputerDO extends ContainerComputer {
990
1211
  return processListOutputSchema.parse({
991
1212
  processes: rows.slice(0, maximumProcessRecords),
992
1213
  });
993
- const hosted = this.hostedRoute();
994
- if (hosted === undefined ||
995
- !(await this.containerRunning(hosted.backing))) {
996
- // No container means none of its processes survived; do not wake it.
997
- for (const row of running)
998
- this.#processes.exited(row.id, null);
999
- return processListOutputSchema.parse({
1000
- processes: this.#processes.list().slice(0, maximumProcessRecords),
1001
- });
1002
- }
1003
- const workspace = await this.readyWorkspace();
1004
- const runtime = processRuntime(workspace, hosted.id);
1005
- const observed = new Map(await Promise.all(running.map(async (row) => [
1006
- row.id,
1007
- this.#settle(row.id, await observeProcess(runtime, row.id)),
1008
- ])));
1214
+ const observed = new Map(await Promise.all(running.map(async (row) => [row.id, await this.#observeOwnedProcess(row)])));
1009
1215
  return processListOutputSchema.parse({
1010
1216
  processes: rows
1011
1217
  .slice(0, maximumProcessRecords)
1012
1218
  .map((row) => observed.get(row.id) ?? row),
1013
1219
  });
1014
1220
  }
1221
+ async #observeOwnedProcess(row) {
1222
+ // A reservation or unresolved SDK preparation is not a native exit.
1223
+ if (this.#processStarts.has(row.id))
1224
+ return { ...row, state: "uncertain" };
1225
+ const execution = this.#executions.get(row.id);
1226
+ let hosted = this.hostedRoute();
1227
+ if (execution !== undefined) {
1228
+ try {
1229
+ hosted = this.hostedRoute(execution.backend);
1230
+ if (hosted === undefined)
1231
+ throw new ComputerBackendUnavailableError("no_backend");
1232
+ }
1233
+ catch {
1234
+ await this.#closeExecution(row.id, "unknown");
1235
+ return { ...row, state: "uncertain" };
1236
+ }
1237
+ }
1238
+ if (hosted === undefined ||
1239
+ !(await this.containerRunning(hosted.backing))) {
1240
+ // Never wake a stopped backend. Absence before a known native handle
1241
+ // cannot settle a dispatch whose reply was lost.
1242
+ if (execution !== undefined && !execution.nativeOpened)
1243
+ return { ...row, state: "uncertain" };
1244
+ return this.#settle(row.id, {
1245
+ state: "exited",
1246
+ exitCode: null,
1247
+ sync: "unknown",
1248
+ });
1249
+ }
1250
+ const workspace = await this.readyWorkspace();
1251
+ const observed = await observeProcess(processRuntime(workspace, hosted.id), row.id);
1252
+ if (execution !== undefined &&
1253
+ !execution.nativeOpened &&
1254
+ observed.state === "exited" &&
1255
+ observed.exitCode === null &&
1256
+ observed.sync === "unknown")
1257
+ return { ...row, state: "uncertain" };
1258
+ const process = this.#settle(row.id, observed);
1259
+ if (observed.state === "running")
1260
+ await this.#renewExecution(row.id);
1261
+ return process;
1262
+ }
1015
1263
  /** Send SIGTERM to one running process; its exit is recorded when observed. */
1016
1264
  async killProcess(input) {
1017
1265
  return await this.killProcessAfterAcceptance(input, () => undefined);
@@ -1024,10 +1272,61 @@ export class ComputerDO extends ContainerComputer {
1024
1272
  throw new ComputerProcessNotFoundError(id);
1025
1273
  if (row.state !== "running")
1026
1274
  return processKillOutputSchema.parse({ process: row, signalled: false });
1027
- const hosted = this.hostedRoute();
1275
+ const pending = this.#processStarts.get(id);
1276
+ if (pending !== undefined) {
1277
+ pending.cancelled = true;
1278
+ const dispatch = pending.dispatch;
1279
+ if (dispatch === undefined) {
1280
+ await this.#closeExecution(id, "refused_start");
1281
+ this.#processes.release(id);
1282
+ return processKillOutputSchema.parse({
1283
+ process: this.#processes.get(id),
1284
+ signalled: false,
1285
+ });
1286
+ }
1287
+ // Preparation can still be in flight. Await its actual native handle,
1288
+ // then signal that same invocation; never infer exit from acceptance.
1289
+ pending.signal ??= (async () => {
1290
+ try {
1291
+ const native = await dispatch;
1292
+ await native.kill("SIGTERM");
1293
+ return true;
1294
+ }
1295
+ catch {
1296
+ return false;
1297
+ }
1298
+ })();
1299
+ const signalled = await pending.signal;
1300
+ return processKillOutputSchema.parse({
1301
+ process: this.#processReceipt(this.#processes.get(id) ?? row),
1302
+ signalled,
1303
+ });
1304
+ }
1305
+ const execution = this.#executions.get(id);
1306
+ let hosted = this.hostedRoute();
1307
+ if (execution !== undefined) {
1308
+ try {
1309
+ hosted = this.hostedRoute(execution.backend);
1310
+ if (hosted === undefined)
1311
+ throw new ComputerBackendUnavailableError("no_backend");
1312
+ }
1313
+ catch {
1314
+ await this.#closeExecution(id, "unknown");
1315
+ return processKillOutputSchema.parse({
1316
+ process: { ...row, state: "uncertain" },
1317
+ signalled: false,
1318
+ });
1319
+ }
1320
+ }
1028
1321
  if (hosted === undefined || !(await this.containerRunning(hosted.backing)))
1029
1322
  return processKillOutputSchema.parse({
1030
- process: this.#processes.exited(id, null),
1323
+ process: execution !== undefined && !execution.nativeOpened
1324
+ ? { ...row, state: "uncertain" }
1325
+ : this.#settle(id, {
1326
+ state: "exited",
1327
+ exitCode: null,
1328
+ sync: "unknown",
1329
+ }),
1031
1330
  signalled: false,
1032
1331
  });
1033
1332
  const workspace = await this.readyWorkspace();
@@ -1038,14 +1337,21 @@ export class ComputerDO extends ContainerComputer {
1038
1337
  });
1039
1338
  }
1040
1339
  catch (cause) {
1041
- console.error("Computer runtime operation failed", cause);
1042
- const observed = await observeProcess(processRuntime(workspace, hosted.id), id);
1340
+ if (execution === undefined)
1341
+ console.error("Computer runtime operation failed", cause);
1342
+ else
1343
+ console.error("Computer hosted process signal was not confirmed");
1043
1344
  return processKillOutputSchema.parse({
1044
- process: this.#settle(id, observed),
1345
+ process: execution === undefined
1346
+ ? this.#settle(id, await observeProcess(processRuntime(workspace, hosted.id), id))
1347
+ : await this.#observeOwnedProcess(row),
1045
1348
  signalled: false,
1046
1349
  });
1047
1350
  }
1048
- return processKillOutputSchema.parse({ process: row, signalled: true });
1351
+ return processKillOutputSchema.parse({
1352
+ process: this.#processReceipt(this.#processes.get(id) ?? row),
1353
+ signalled: true,
1354
+ });
1049
1355
  }
1050
1356
  /**
1051
1357
  * A "pending" verdict's background continuation keeps the host alive
@@ -1063,7 +1369,11 @@ export class ComputerDO extends ContainerComputer {
1063
1369
  if (observed.state === "exited") {
1064
1370
  if (observed.settleLater !== undefined)
1065
1371
  this.#watchResync(id, observed.settleLater);
1066
- return this.#processes.exited(id, observed.exitCode, observed.sync);
1372
+ const process = this.#processes.exited(id, observed.exitCode, observed.sync);
1373
+ const execution = this.#executions.close(id, "native_exit");
1374
+ if (execution !== undefined)
1375
+ this.state.waitUntil(this.#deliverExecutionClose(execution, "native_exit"));
1376
+ return process;
1067
1377
  }
1068
1378
  const row = this.#processes.get(id);
1069
1379
  if (row === undefined)
@@ -1082,13 +1392,21 @@ export class ComputerDO extends ContainerComputer {
1082
1392
  try {
1083
1393
  const settled = await drainProcessExit(run);
1084
1394
  if (settled !== undefined) {
1085
- if (settled.settleLater !== undefined)
1086
- this.#watchResync(id, settled.settleLater);
1087
- this.#processes.exited(id, settled.exitCode, settled.sync);
1395
+ this.#settle(id, {
1396
+ state: "exited",
1397
+ ...settled,
1398
+ });
1088
1399
  }
1400
+ else
1401
+ await this.#closeExecution(id, "unknown");
1089
1402
  }
1090
1403
  catch (cause) {
1091
- console.error("Computer runtime operation failed", cause);
1404
+ if (this.#executions.get(id) === undefined)
1405
+ console.error("Computer runtime operation failed", cause);
1406
+ else {
1407
+ await this.#closeExecution(id, "unknown");
1408
+ console.error("Computer hosted process observation is unavailable");
1409
+ }
1092
1410
  // The stream broke (eviction, transport); list reconciles later.
1093
1411
  }
1094
1412
  }
@@ -1116,10 +1434,10 @@ export class ComputerDO extends ContainerComputer {
1116
1434
  if (hosted === undefined)
1117
1435
  return Response.json({ error: "Computer container is unavailable" }, { status: 502 });
1118
1436
  try {
1119
- const env_2 = { stack: [], error: void 0, hasError: false };
1437
+ const env_3 = { stack: [], error: void 0, hasError: false };
1120
1438
  try {
1121
1439
  const workspace = await this.readyWorkspace();
1122
- const readiness = __addDisposableResource(env_2, await workspace.runtime.exec("true", {
1440
+ const readiness = __addDisposableResource(env_3, await workspace.runtime.exec("true", {
1123
1441
  backend: hosted.id,
1124
1442
  encoding: "utf8",
1125
1443
  timeoutMs: 10_000,
@@ -1129,12 +1447,12 @@ export class ComputerDO extends ContainerComputer {
1129
1447
  return Response.json({ error: "Computer container is unavailable" }, { status: 502 });
1130
1448
  }
1131
1449
  }
1132
- catch (e_2) {
1133
- env_2.error = e_2;
1134
- env_2.hasError = true;
1450
+ catch (e_3) {
1451
+ env_3.error = e_3;
1452
+ env_3.hasError = true;
1135
1453
  }
1136
1454
  finally {
1137
- __disposeResources(env_2);
1455
+ __disposeResources(env_3);
1138
1456
  }
1139
1457
  }
1140
1458
  catch (cause) {
@@ -1201,24 +1519,24 @@ export class ComputerDO extends ContainerComputer {
1201
1519
  // Runtime backends reach the authoritative Workspace through these loopback
1202
1520
  // entrypoints. The consuming Worker must re-export both from its main module.
1203
1521
  export { WorkspaceProxy, WorkspaceServiceProxy };
1204
- export { OnBoxVmContainerHost, onBoxVmPlacement, onBoxVmIngress, } from "./on-box-vm.js";
1522
+ export { OnBoxVmContainerHost, OnBoxVmTimeoutError, onBoxVmPlacement, onBoxVmIngress, } from "./on-box-vm.js";
1205
1523
  export { computerContainerSleepAfterMs } from "./container-policy.js";
1206
1524
  export * from "./computer-backends.js";
1207
1525
  export { LocalBackendOfflineError, localBackendAudience, localBackendDialPath, verifyLocalBackendDial, } from "./local-backend.js";
1208
1526
  /** Keep each native Workspace lease inside the operation that uses it. */
1209
1527
  export function createComputerFilesPort(computer) {
1210
1528
  async function withFiles(operation) {
1211
- const env_3 = { stack: [], error: void 0, hasError: false };
1529
+ const env_4 = { stack: [], error: void 0, hasError: false };
1212
1530
  try {
1213
- const workspace = __addDisposableResource(env_3, await getWorkspace(computer), false);
1531
+ const workspace = __addDisposableResource(env_4, await getWorkspace(computer), false);
1214
1532
  return await operation(createWorkspaceFilesPort(workspace, computer));
1215
1533
  }
1216
- catch (e_3) {
1217
- env_3.error = e_3;
1218
- env_3.hasError = true;
1534
+ catch (e_4) {
1535
+ env_4.error = e_4;
1536
+ env_4.hasError = true;
1219
1537
  }
1220
1538
  finally {
1221
- __disposeResources(env_3);
1539
+ __disposeResources(env_4);
1222
1540
  }
1223
1541
  }
1224
1542
  return {
@@ -1233,5 +1551,4 @@ export function createComputerFilesPort(computer) {
1233
1551
  };
1234
1552
  }
1235
1553
  export { createWorkspaceFilesPort } from "./files.js";
1236
- export * from "./operation-contract.js";
1237
1554
  export { ComputerOperationConflictError, ComputerOperationLimitError, ComputerRequestInvalidError, } from "./operation-record.js";