@fungi.computer/computer 0.0.1 → 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 (41) hide show
  1. package/README.md +38 -10
  2. package/dist/computer-backends.d.ts +11 -12
  3. package/dist/computer-backends.js +4 -7
  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/backing.js +2 -2
  13. package/dist/fleet/index.d.ts +1 -1
  14. package/dist/fleet/index.js +1 -1
  15. package/dist/fleet/inventory-wire.d.ts +4 -0
  16. package/dist/fleet/inventory-wire.js +5 -0
  17. package/dist/fleet/inventory.js +4 -4
  18. package/dist/fleet/lifecycle.d.ts +7 -1
  19. package/dist/fleet/lifecycle.js +23 -6
  20. package/dist/fleet/wire.d.ts +28 -28
  21. package/dist/fleet/wire.js +9 -9
  22. package/dist/index.d.ts +33 -15
  23. package/dist/index.js +426 -109
  24. package/dist/internet-access.d.ts +4 -0
  25. package/dist/internet-access.js +14 -0
  26. package/dist/local-backend-replay.d.ts +10 -0
  27. package/dist/local-backend-replay.js +43 -0
  28. package/dist/local-backend.d.ts +2 -1
  29. package/dist/local-backend.js +4 -4
  30. package/dist/on-box-vm.d.ts +15 -4
  31. package/dist/on-box-vm.js +43 -20
  32. package/dist/operation-record.d.ts +1 -1
  33. package/dist/operation-record.js +1 -1
  34. package/dist/process-inventory.d.ts +3 -1
  35. package/dist/process-inventory.js +2 -2
  36. package/dist/team-candidate-builder.mjs +8428 -337
  37. package/package.json +17 -24
  38. package/dist/light-shell.d.ts +0 -44
  39. package/dist/light-shell.js +0 -376
  40. package/dist/operation-contract.d.ts +0 -456
  41. package/dist/operation-contract.js +0 -452
package/README.md CHANGED
@@ -13,6 +13,13 @@ from `@fungi.computer/computer/fleet/wire`, and backing descriptions from
13
13
  declarations. The `./fleet` lifecycle and main Computer entry require the host
14
14
  runtime's Cloudflare Workers types.
15
15
 
16
+ The packed stranger check exercises these Node-safe entries and the egress
17
+ helpers after a cold install of the runtime closure. The fork's companion check
18
+ parses the published shell-module groups used by the Computer image with Acorn
19
+ and checks their declarations. Neither check starts a Computer, executes a
20
+ Worker shell, or qualifies the Worker/Workspace/Git SDK in a Node type
21
+ environment.
22
+
16
23
  `@fungi.computer/computer/fleet` owns the host-neutral grant check, backing
17
24
  records and fact contracts. A host authenticates its caller and supplies a
18
25
  `ComputerGrant`: one Computer, explicit actions, an expiry, optional host-owned
@@ -82,11 +89,11 @@ publish an App or replace a production Computer image.
82
89
  SQLite provider. Files belong to that workspace, not to a running shell process.
83
90
  A file read can work with no available execution backend.
84
91
 
85
- [Operation contracts](src/operation-contract.ts) declare discovery, bounded file
86
- reads/writes, directories, command execution, processes and port exposure. The
87
- [Files adapter](src/files.ts) implements the same read and compare/replace
88
- contract for Agent tools. Failed containment, conflicting contents and reserved
89
- ports remain explicit errors.
92
+ [Operation contracts](../computer-contract/README.md), in their own package,
93
+ declare discovery, bounded file reads/writes, directories, command execution,
94
+ processes and port exposure. The [Files adapter](src/files.ts) implements the
95
+ same read and compare/replace contract for Agent tools. Failed containment,
96
+ conflicting contents and reserved ports remain explicit errors.
90
97
 
91
98
  An execution request carries a replay key. The
92
99
  [operation record](src/operation-record.ts) admits the immutable input and
@@ -130,6 +137,27 @@ registered host to the Workspace backend interface. Platform containers use the
130
137
  maintained Cloudflare container backend. Neither backend label nor a green
131
138
  container fixture certifies an installed VM host or production routing.
132
139
 
140
+ Local dial verification binds Seam's replay-admission port to the existing
141
+ Computer Durable Object's SQLite database. That database is the receiver
142
+ namespace across reconstruction, shared by its attached devices; nonce identity
143
+ inside it is nonce-only. One native transaction prunes only strictly expired
144
+ records, refuses duplicates or the private 4,096-record ceiling, and commits the
145
+ nonce before admission succeeds. Seam owns the inclusive retention deadline;
146
+ duplicates never renew it and capacity never evicts live history. Storage
147
+ failure refuses admission without a process-local fallback. Attachment,
148
+ device-key, signature, audience and revocation authority remain unchanged.
149
+
150
+ `createLocalBackendReplayStore(owner)` is exported for another native Local-dial
151
+ receiver that uses its own existing SQLite owner, including a Team machine-link
152
+ receiver. The caller owns pins, audience, membership and revocation; this store
153
+ owns only atomic, bounded nonce retention. It neither creates a Computer nor
154
+ shares nonce history across separate databases.
155
+
156
+ `test:unit` includes the `machine-link-reopen-replay` proof: two physical Node
157
+ processes call the actual local-dial verifier against the same SQLite file. This
158
+ proves reconstructed-process replay refusal, not a complete HTTP/WebSocket
159
+ route, deployed host or container-health acceptance.
160
+
133
161
  ## Processes, ports and lifetime
134
162
 
135
163
  Start requires a host-owned opaque `requestId` and `requestScope`, plus a
@@ -196,11 +224,11 @@ were saved in a hosted Computer. Do not copy its temporary output into source.
196
224
  ## Proof and reference
197
225
 
198
226
  [package.json](package.json) declares the private entrypoints `.` and `./files`,
199
- `./backing`, `./host-config`, `./operations`.
200
- [Runtime proof](test/runtime-proof.mjs) checks native workspace operations and
201
- reconstruction. [Process proof](test/process-proof.mjs) checks retained process
202
- identity against the real container.
203
- [Terminal proof](test/terminal-container-proof.mjs),
227
+ `./backing`, `./host-config`. Operation schemas come from
228
+ `@fungi.computer/computer-contract`. [Runtime proof](test/runtime-proof.mjs)
229
+ checks native workspace operations and reconstruction.
230
+ [Process proof](test/process-proof.mjs) checks retained process identity against
231
+ the real container. [Terminal proof](test/terminal-container-proof.mjs),
204
232
  [resource proof](test/container-resource-proof.mjs) and
205
233
  [backend tests](test/computer-backends.test.mjs) cover their separate
206
234
  boundaries. The declared nightly egress proof is separate from the ordinary
@@ -4,7 +4,7 @@ export declare const hostedBackingSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
4
4
  kind: z.ZodLiteral<"platform-container">;
5
5
  }, z.core.$strict>, z.ZodObject<{
6
6
  kind: z.ZodLiteral<"on-box-vm">;
7
- hostId: z.ZodString;
7
+ hostId: z.ZodLazy<z.ZodString>;
8
8
  }, z.core.$strict>], "kind">;
9
9
  export type HostedBacking = z.infer<typeof hostedBackingSchema>;
10
10
  /** A member's machine, attached with a device key that never leaves it. */
@@ -12,39 +12,38 @@ export declare const localBackendSchema: z.ZodObject<{
12
12
  kind: z.ZodLiteral<"local">;
13
13
  deviceId: z.ZodString;
14
14
  publicKey: z.ZodBase64;
15
- hostLabel: z.ZodString;
16
- folderLabel: z.ZodString;
15
+ hostLabel: z.ZodLazy<z.ZodString>;
16
+ folderLabel: z.ZodLazy<z.ZodString>;
17
17
  }, z.core.$strict>;
18
18
  export type LocalBackend = z.infer<typeof localBackendSchema>;
19
19
  export declare const attachedBackendSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
20
20
  kind: z.ZodLiteral<"platform-container">;
21
21
  }, z.core.$strict>, z.ZodObject<{
22
22
  kind: z.ZodLiteral<"on-box-vm">;
23
- hostId: z.ZodString;
23
+ hostId: z.ZodLazy<z.ZodString>;
24
24
  }, z.core.$strict>, z.ZodObject<{
25
25
  kind: z.ZodLiteral<"local">;
26
26
  deviceId: z.ZodString;
27
27
  publicKey: z.ZodBase64;
28
- hostLabel: z.ZodString;
29
- folderLabel: z.ZodString;
28
+ hostLabel: z.ZodLazy<z.ZodString>;
29
+ folderLabel: z.ZodLazy<z.ZodString>;
30
30
  }, z.core.$strict>], "kind">;
31
31
  export type AttachedBackend = z.infer<typeof attachedBackendSchema>;
32
32
  /** The stable Workspace backend id: its sync cursor is keyed by it. */
33
33
  export declare function attachedBackendId(backend: AttachedBackend): string;
34
34
  /** One Computer's attached backends, in attach order; ids are unique. */
35
- export declare const attachedBackendsSchema: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
35
+ export declare const attachedBackendsSchema: z.ZodLazy<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
36
36
  kind: z.ZodLiteral<"platform-container">;
37
37
  }, z.core.$strict>, z.ZodObject<{
38
38
  kind: z.ZodLiteral<"on-box-vm">;
39
- hostId: z.ZodString;
39
+ hostId: z.ZodLazy<z.ZodString>;
40
40
  }, z.core.$strict>, z.ZodObject<{
41
41
  kind: z.ZodLiteral<"local">;
42
42
  deviceId: z.ZodString;
43
43
  publicKey: z.ZodBase64;
44
- hostLabel: z.ZodString;
45
- folderLabel: z.ZodString;
46
- }, z.core.$strict>], "kind">>;
47
- export declare const attachedBackendIdSchema: z.ZodString;
44
+ hostLabel: z.ZodLazy<z.ZodString>;
45
+ folderLabel: z.ZodLazy<z.ZodString>;
46
+ }, z.core.$strict>], "kind">>>;
48
47
  export type AttachedBackendStatus = "ready" | "online" | "offline";
49
48
  /** What a Computer reports about one attached backend. */
50
49
  export type AttachedBackendView = Readonly<{
@@ -8,8 +8,8 @@ import * as z from "zod";
8
8
  * id at most; a local backend carries its device's public key and two display
9
9
  * labels, never a path on the member's machine.
10
10
  */
11
- const hostIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/);
12
- const labelSchema = z.string().trim().min(1).max(256);
11
+ const hostIdSchema = z.lazy(() => z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/));
12
+ const labelSchema = z.lazy(() => z.string().trim().min(1).max(256));
13
13
  /** A backend the platform runs: a Cloudflare container or an on-box VM. */
14
14
  export const hostedBackingSchema = z.discriminatedUnion("kind", [
15
15
  z.strictObject({ kind: z.literal("platform-container") }),
@@ -40,13 +40,10 @@ export function attachedBackendId(backend) {
40
40
  }
41
41
  }
42
42
  /** One Computer's attached backends, in attach order; ids are unique. */
43
- export const attachedBackendsSchema = z
43
+ export const attachedBackendsSchema = z.lazy(() => z
44
44
  .array(attachedBackendSchema)
45
45
  .max(16)
46
- .refine((backends) => new Set(backends.map(attachedBackendId)).size === backends.length, "attached backend ids must be unique");
47
- export const attachedBackendIdSchema = z
48
- .string()
49
- .regex(/^(container|vm-[a-z0-9][a-z0-9-]{0,62}|local-[a-z2-7]{16})$/);
46
+ .refine((backends) => new Set(backends.map(attachedBackendId)).size === backends.length, "attached backend ids must be unique"));
50
47
  export function describeAttachedBackend(backend, online) {
51
48
  const id = attachedBackendId(backend);
52
49
  switch (backend.kind) {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Keep one command stream within `maximumStreamBytes`. A cut stream ends with
3
+ * a visible marker line (counted inside the bound, excluded from the totals)
4
+ * and reports a typed entry.
5
+ */
6
+ export declare function boundExecStream(value: string, stream: "stdout" | "stderr"): {
7
+ value: string;
8
+ truncation: readonly {
9
+ stream: "stdout" | "stderr";
10
+ keptBytes: number;
11
+ totalBytes: number;
12
+ }[];
13
+ };
@@ -0,0 +1,24 @@
1
+ import { execTruncationMarker, maximumStreamBytes, truncationMarkerReserveBytes, utf8Prefix, } from "@fungi.computer/computer-contract";
2
+ const utf8 = new TextEncoder();
3
+ const noTruncation = [];
4
+ /**
5
+ * Keep one command stream within `maximumStreamBytes`. A cut stream ends with
6
+ * a visible marker line (counted inside the bound, excluded from the totals)
7
+ * and reports a typed entry.
8
+ */
9
+ export function boundExecStream(value, stream) {
10
+ const bytes = utf8.encode(value);
11
+ if (bytes.byteLength <= maximumStreamBytes) {
12
+ return { value, truncation: noTruncation };
13
+ }
14
+ const kept = utf8Prefix(bytes, maximumStreamBytes - truncationMarkerReserveBytes);
15
+ const entry = {
16
+ stream,
17
+ keptBytes: kept.byteLength,
18
+ totalBytes: bytes.byteLength,
19
+ };
20
+ return {
21
+ value: `${new TextDecoder().decode(kept)}${execTruncationMarker(entry)}`,
22
+ truncation: [entry],
23
+ };
24
+ }
@@ -0,0 +1,35 @@
1
+ import { type ComputerExecutionCloseReason, type ComputerExecutionInvocation, type ComputerValue } from "@fungi.computer/computer-contract";
2
+ type ExecutionStorage = Pick<DurableObjectStorage, "sql" | "transactionSync">;
3
+ type Phase = "opening" | "dispatching" | "open" | "closed";
4
+ type ComputerExecutionRecord = Readonly<{
5
+ invocation: ComputerExecutionInvocation;
6
+ phase: Phase;
7
+ backend: string;
8
+ nativeOpened: boolean;
9
+ closeReason: ComputerExecutionCloseReason | null;
10
+ context: ComputerValue;
11
+ }>;
12
+ /**
13
+ * Only public host context and native invocation identity are durable here.
14
+ * The operation/process owners continue to own admission, replay and retention;
15
+ * neither credentials nor a second process inventory belong in this table.
16
+ */
17
+ declare class ComputerExecutionLifecycle {
18
+ #private;
19
+ readonly reconstructed: readonly ComputerExecutionRecord[];
20
+ constructor(storage: ExecutionStorage);
21
+ get(id: string): ComputerExecutionRecord | undefined;
22
+ /** Called synchronously after the existing owner's atomic NEW admission. */
23
+ begin(invocation: ComputerExecutionInvocation, backend: string, context: ComputerValue): void;
24
+ /** The last synchronous fence before invoking the maintained native SDK. */
25
+ dispatch(id: string): void;
26
+ /** A native handle arrived; this phase alone is not proof of native liveness. */
27
+ opened(id: string): void;
28
+ /** Persist revocation before the caller attempts the remote host callback. */
29
+ close(id: string, reason: ComputerExecutionCloseReason): ComputerExecutionRecord | undefined;
30
+ /** Closed metadata cannot outlive the existing operation/process owner. */
31
+ prune(): void;
32
+ }
33
+ /** Construct the ComputerDO's internal lifecycle owner, not a package API. */
34
+ export declare function createComputerExecutionLifecycle(storage: Pick<DurableObjectStorage, "sql" | "transactionSync">): ComputerExecutionLifecycle;
35
+ export {};
@@ -0,0 +1,78 @@
1
+ import { computerValueSchema, } from "@fungi.computer/computer-contract";
2
+ function record(row) {
3
+ return {
4
+ invocation: { id: row.id, kind: row.kind },
5
+ phase: row.phase,
6
+ backend: row.backend,
7
+ nativeOpened: row.native_opened === 1,
8
+ closeReason: row.close_reason,
9
+ context: computerValueSchema.parse(JSON.parse(row.context_json)),
10
+ };
11
+ }
12
+ /**
13
+ * Only public host context and native invocation identity are durable here.
14
+ * The operation/process owners continue to own admission, replay and retention;
15
+ * neither credentials nor a second process inventory belong in this table.
16
+ */
17
+ class ComputerExecutionLifecycle {
18
+ #storage;
19
+ reconstructed;
20
+ constructor(storage) {
21
+ this.#storage = storage;
22
+ this.reconstructed = storage.transactionSync(() => {
23
+ // The first partial checkpoint's preview metadata is disposable. Keep
24
+ // only this current representation, never an old-shape reader or copy.
25
+ storage.sql.exec("DROP TABLE IF EXISTS computer_execution_lifecycle");
26
+ storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_execution_invocations (id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK (kind IN ('exec', 'process')), phase TEXT NOT NULL CHECK (phase IN ('opening', 'dispatching', 'open', 'closed')), backend TEXT NOT NULL, native_opened INTEGER NOT NULL DEFAULT 0 CHECK (native_opened IN (0, 1)), context_json TEXT NOT NULL, close_reason TEXT CHECK (close_reason IN ('refused_start', 'native_exit', 'unknown', 'reconstructed'))) STRICT");
27
+ const interrupted = storage.sql
28
+ .exec("SELECT id, kind, phase, backend, native_opened, context_json, close_reason FROM computer_execution_invocations WHERE phase <> 'closed' AND (kind = 'exec' OR phase = 'opening')")
29
+ .toArray()
30
+ .map(record);
31
+ // A cold exec never probes or dispatches again. Process reconciliation
32
+ // will require native proof; an unfinished issuer open has no such proof.
33
+ storage.sql.exec("UPDATE computer_execution_invocations SET phase = 'closed', close_reason = 'reconstructed' WHERE phase <> 'closed' AND (kind = 'exec' OR phase = 'opening')");
34
+ return interrupted;
35
+ });
36
+ this.prune();
37
+ }
38
+ get(id) {
39
+ const row = this.#storage.sql
40
+ .exec("SELECT id, kind, phase, backend, native_opened, context_json, close_reason FROM computer_execution_invocations WHERE id = ?", id)
41
+ .toArray()[0];
42
+ return row === undefined ? undefined : record(row);
43
+ }
44
+ /** Called synchronously after the existing owner's atomic NEW admission. */
45
+ begin(invocation, backend, context) {
46
+ const publicContext = computerValueSchema.parse(context);
47
+ this.prune();
48
+ this.#storage.sql.exec("INSERT INTO computer_execution_invocations (id, kind, phase, backend, context_json) VALUES (?, ?, 'opening', ?, ?)", invocation.id, invocation.kind, backend, JSON.stringify(publicContext));
49
+ }
50
+ /** The last synchronous fence before invoking the maintained native SDK. */
51
+ dispatch(id) {
52
+ if (this.get(id)?.phase !== "opening")
53
+ throw new Error("Computer execution is no longer open for dispatch");
54
+ this.#storage.sql.exec("UPDATE computer_execution_invocations SET phase = 'dispatching' WHERE id = ?", id);
55
+ }
56
+ /** A native handle arrived; this phase alone is not proof of native liveness. */
57
+ opened(id) {
58
+ this.#storage.sql.exec("UPDATE computer_execution_invocations SET native_opened = 1, phase = CASE WHEN phase = 'dispatching' THEN 'open' ELSE phase END WHERE id = ?", id);
59
+ }
60
+ /** Persist revocation before the caller attempts the remote host callback. */
61
+ close(id, reason) {
62
+ return this.#storage.transactionSync(() => {
63
+ const current = this.get(id);
64
+ if (current === undefined || current.phase === "closed")
65
+ return undefined;
66
+ this.#storage.sql.exec("UPDATE computer_execution_invocations SET phase = 'closed', close_reason = ? WHERE id = ?", reason, id);
67
+ return current;
68
+ });
69
+ }
70
+ /** Closed metadata cannot outlive the existing operation/process owner. */
71
+ prune() {
72
+ this.#storage.sql.exec("DELETE FROM computer_execution_invocations WHERE phase = 'closed' AND ((kind = 'exec' AND NOT EXISTS (SELECT 1 FROM computer_operations WHERE 'exec:' || request_id = computer_execution_invocations.id)) OR (kind = 'process' AND NOT EXISTS (SELECT 1 FROM computer_processes WHERE id = computer_execution_invocations.id)))");
73
+ }
74
+ }
75
+ /** Construct the ComputerDO's internal lifecycle owner, not a package API. */
76
+ export function createComputerExecutionLifecycle(storage) {
77
+ return new ComputerExecutionLifecycle(storage);
78
+ }
@@ -3,17 +3,25 @@ import type { Workspace } from "@fungi.computer/cloudflare-computer";
3
3
  export declare function applyWorkspacePatch(workspace: Pick<Workspace, "runtime">, backend: string, input: {
4
4
  patch: string;
5
5
  }): Promise<{
6
- status: "completed" | "failed" | "cancelled";
6
+ status: "failed" | "completed" | "cancelled";
7
7
  exitCode: number;
8
8
  stdout: string;
9
9
  stderr: string;
10
- truncated: boolean;
10
+ truncated: false | {
11
+ stream: "stdout" | "stderr";
12
+ keptBytes: number;
13
+ totalBytes: number;
14
+ }[];
11
15
  } | {
12
- status: "completed" | "failed" | "cancelled";
16
+ status: "failed" | "completed" | "cancelled";
13
17
  exitCode: number;
14
18
  stdout: string;
15
19
  stderr: string;
16
- truncated: boolean;
20
+ truncated: false | {
21
+ stream: "stdout" | "stderr";
22
+ keptBytes: number;
23
+ totalBytes: number;
24
+ }[];
17
25
  error: {
18
26
  kind: "mismatch";
19
27
  };
@@ -50,7 +50,8 @@ 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 { patchInputSchema, patchOutputSchema, maximumStreamBytes, } from "./operation-contract.js";
53
+ import { patchInputSchema, patchOutputSchema, } from "@fungi.computer/computer-contract";
54
+ import { boundExecStream } from "./exec-output.js";
54
55
  /** The packaged, pinned Codex engine owns patch parsing and the Workspace jail. */
55
56
  const nativePatch = {
56
57
  executable: "/opt/fungi/fungi-apply-patch",
@@ -58,15 +59,6 @@ const nativePatch = {
58
59
  timeoutMs: 30_000,
59
60
  mismatchExitCode: 23,
60
61
  };
61
- function bounded(value) {
62
- const bytes = new TextEncoder().encode(value);
63
- return bytes.length <= maximumStreamBytes
64
- ? { value, truncated: false }
65
- : {
66
- value: new TextDecoder().decode(bytes.slice(0, maximumStreamBytes)),
67
- truncated: true,
68
- };
69
- }
70
62
  /** Await Workspace synchronization even when the native engine reports a partial failure. */
71
63
  export async function applyWorkspacePatch(workspace, backend, input) {
72
64
  const env_1 = { stack: [], error: void 0, hasError: false };
@@ -81,10 +73,10 @@ export async function applyWorkspacePatch(workspace, backend, input) {
81
73
  }), false);
82
74
  const result = await run.result();
83
75
  const synchronizationFailure = result.sync.status === "pending" ? result.sync.error : null;
84
- const stdout = bounded(result.stdout);
85
- const stderr = bounded(synchronizationFailure !== null
76
+ const stdout = boundExecStream(result.stdout, "stdout");
77
+ const stderr = boundExecStream(synchronizationFailure !== null
86
78
  ? `${result.stderr}\nPatch filesystem synchronization failed: ${synchronizationFailure}. Earlier patch writes may remain.`
87
- : result.stderr);
79
+ : result.stderr, "stderr");
88
80
  const output = {
89
81
  status: synchronizationFailure !== null ? "failed" : result.status,
90
82
  exitCode: synchronizationFailure !== null && result.exitCode === 0
@@ -92,7 +84,9 @@ export async function applyWorkspacePatch(workspace, backend, input) {
92
84
  : result.exitCode,
93
85
  stdout: stdout.value,
94
86
  stderr: stderr.value,
95
- truncated: stdout.truncated || stderr.truncated,
87
+ truncated: stdout.truncation.length + stderr.truncation.length === 0
88
+ ? false
89
+ : [...stdout.truncation, ...stderr.truncation],
96
90
  };
97
91
  return patchOutputSchema.parse(result.exitCode === nativePatch.mismatchExitCode
98
92
  ? { ...output, error: { kind: "mismatch" } }
package/dist/files.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { SQLiteWorkspaceProvider, WorkspaceClient } from "@fungi.computer/cloudflare-computer";
2
2
  /** Files need only the Workspace filesystem, local or leased through a stub. */
3
3
  export type WorkspaceFiles = Pick<WorkspaceClient, "fs">;
4
- import { type ComputerFileReplacement, type ComputerFileReplacementResult } from "./operation-contract.js";
4
+ import type { FilesPort } from "@fungi.computer/knapsack";
5
+ import { type ComputerFileGlobInput, type ComputerFileGrepInput, type ComputerFileListInput, type ComputerFileReplacement, type ComputerFileReplacementResult } from "@fungi.computer/computer-contract";
5
6
  export type WorkspaceFilesOwner = Readonly<{
6
7
  replaceFileIfUnchanged: (input: ComputerFileReplacement) => Promise<ComputerFileReplacementResult>;
7
8
  }>;
@@ -15,11 +16,48 @@ export declare function decodeUtf8Page(bytes: Uint8Array, path: string, moreByte
15
16
  export declare function readBoundedWorkspaceText(provider: SQLiteWorkspaceProvider, path: string, maximumBytes: number): string;
16
17
  /** Adapt native Workspace.fs at the one real file boundary. */
17
18
  export declare function createWorkspaceFilesPort(workspace: WorkspaceFiles, owner: WorkspaceFilesOwner): Readonly<{
19
+ rest: Readonly<{
20
+ list: (input: ComputerFileListInput) => Promise<{
21
+ path: string;
22
+ entries: {
23
+ name: string;
24
+ size: number;
25
+ modifiedAt: number;
26
+ type: "file" | "directory" | "symlink";
27
+ }[];
28
+ nextOffset: number | null;
29
+ }>;
30
+ glob(input: ComputerFileGlobInput): Promise<{
31
+ path: string;
32
+ matches: string[];
33
+ truncated: boolean;
34
+ }>;
35
+ grep(input: ComputerFileGrepInput): Promise<{
36
+ path: string;
37
+ matches: {
38
+ path: string;
39
+ line: number;
40
+ text: string;
41
+ }[];
42
+ truncated: boolean;
43
+ }>;
44
+ }>;
18
45
  read(input: {
19
46
  path: string;
20
47
  offset: number;
21
48
  limit: number;
22
49
  }): Promise<{
50
+ path: string;
51
+ image: {
52
+ mimeType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
53
+ data: string;
54
+ bytes: number;
55
+ };
56
+ file?: {
57
+ computerId: string;
58
+ path: string;
59
+ };
60
+ } | {
23
61
  path: string;
24
62
  content: string;
25
63
  offset: number;
@@ -29,25 +67,25 @@ export declare function createWorkspaceFilesPort(workspace: WorkspaceFiles, owne
29
67
  list(input: {
30
68
  path: string;
31
69
  limit: number;
32
- }): Promise<{
33
- path: string;
34
- entries: {
70
+ }, context?: Parameters<FilesPort["list"]>[1]): Promise<{
71
+ results: {
35
72
  path: string;
36
- name: string;
37
73
  type: "file" | "directory" | "symlink";
38
- size: number;
39
- modifiedAt: number;
40
74
  }[];
41
- truncated: boolean;
75
+ truncated: "limit" | "scan" | null;
76
+ path: string;
42
77
  }>;
43
78
  glob(input: {
44
79
  path: string;
45
80
  pattern: string;
46
81
  limit: number;
47
- }): Promise<{
82
+ }, context?: Parameters<FilesPort["glob"]>[1]): Promise<{
83
+ results: {
84
+ path: string;
85
+ type: "file" | "directory";
86
+ }[];
87
+ truncated: "limit" | "scan" | null;
48
88
  path: string;
49
- matches: string[];
50
- truncated: boolean;
51
89
  }>;
52
90
  grep(input: {
53
91
  path: string;
@@ -55,14 +93,15 @@ export declare function createWorkspaceFilesPort(workspace: WorkspaceFiles, owne
55
93
  regex: boolean;
56
94
  ignoreCase: boolean;
57
95
  limit: number;
58
- }): Promise<{
59
- path: string;
60
- matches: {
96
+ include?: string | undefined;
97
+ }, context?: Parameters<FilesPort["grep"]>[1]): Promise<{
98
+ results: {
61
99
  path: string;
62
100
  line: number;
63
101
  text: string;
64
102
  }[];
65
- truncated: boolean;
103
+ truncated: "limit" | "scan" | null;
104
+ path: string;
66
105
  }>;
67
106
  replace(input: Readonly<{
68
107
  path: string;
@@ -73,3 +112,14 @@ export declare function createWorkspaceFilesPort(workspace: WorkspaceFiles, owne
73
112
  bytes: number;
74
113
  }>;
75
114
  }>;
115
+ /** Shared REST listing: offsets count visible entries, never ignored candidates. */
116
+ export declare function listWorkspaceFiles(workspace: WorkspaceFiles, input: ComputerFileListInput): Promise<{
117
+ path: string;
118
+ entries: {
119
+ name: string;
120
+ size: number;
121
+ modifiedAt: number;
122
+ type: "file" | "directory" | "symlink";
123
+ }[];
124
+ nextOffset: number | null;
125
+ }>;