From 51c1c390c522d8da6f010a8835f693b9015c0eb1 Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:38:38 +0000 Subject: [PATCH 01/11] dofs, computer, docs: Document file tools Describe directory pagination, ranged reads, the complete AI tool set, multimodal output, continuation fields, and read-only behavior. --- docs/09_tool_interface.md | 260 +++++++++++++++--------------------- packages/computer/README.md | 21 ++- packages/dofs/README.md | 7 + 3 files changed, 125 insertions(+), 163 deletions(-) diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index eeec7ca2..c5155a3f 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -1,29 +1,30 @@ # 09. Tool interface (agents) -`@cloudflare/computer/tools` ships ready-made [AI SDK](https://github.com/vercel/ai) tools for agents that use a `Workspace`. The first provider target is the AI SDK because it is the tool layer used by the `agents` SDK and the Think example. +`@cloudflare/computer/tools` ships ready-made [AI SDK](https://github.com/vercel/ai) tools for agents that use a `Workspace`. -The tools are thin wrappers over the existing `Workspace` surfaces: +The tools wrap three Workspace surfaces: -- `workspace.fs` for file reads, writes, edits, and directory listing. -- `workspace.runtime.exec` for command execution when the caller opts in. +- `workspace.fs` for file reads, writes, edits, searches, listings, and deletion; +- `workspace.runtime.exec` for command execution when the caller opts in; - `workspace.assets` for publishing generated files when an assets publisher is configured. -Git access already ships through `workspace.git`, the third major surface on `Workspace` alongside `fs`, `runtime`, Assets, and Artifacts. AI SDK tool wrappers around that surface can land later against a stable target. See [`13_git_interface.md`](./13_git_interface.md). - ## What ships | Export | Purpose | | --- | --- | | `createAITools` | Create the default AI SDK `ToolSet` for a Workspace. | -| `createReadTool` | Memory-efficient, line-windowed file read. | -| `createWriteTool` | Whole-file write with a UTF-8 byte cap. | -| `createEditTool` | Exact targeted replacements with unified-diff preview. | -| `createListTool` | One-level directory listing. | -| `createExecTool` | Run a shell command through a configured Workspace backend. | +| `createReadTool` | Stream text by line and pass images or PDFs to capable models. | +| `createWriteTool` | Write a whole file with a UTF-8 byte cap. | +| `createEditTool` | Apply atomic targeted replacements and return a unified diff. | +| `createListTool` | Page through one directory with file metadata. | +| `createFindTool` | Find paths with `*`, `**`, and `?` globs. | +| `createGrepTool` | Search text with regular expressions or fixed strings. | +| `createDeleteTool` | Delete a file or directory. | +| `createExecTool` | Run a command through a configured Workspace backend. | | `createPublishTool` | Publish a workspace file through `workspace.assets`. | -| `WorkspaceFileStore` | Adapt `Workspace.fs` to the file-store shape used by the file tools. | +| `WorkspaceFileStore` | Adapt `workspace.fs` to the store used by file tools. | -The fixed tool names from `createAITools()` are `read`, `write`, `edit`, and `ls`. When present, the conditional tool names are also fixed: `exec` for shell commands and `publish` for asset publishing. +`createAITools()` always names its tools `read`, `ls`, `find`, `grep`, `write`, `edit`, and `delete`. `exec` appears when the caller supplies `shell` options. `publish` appears when assets are configured. In read-only mode the set is `read`, `ls`, `find`, and `grep`. ## Wiring up @@ -35,49 +36,40 @@ export class Agent { workspace: Workspace; constructor(ctx: DurableObjectState) { - this.workspace = new Workspace({ - storage: ctx.storage, - }); + this.workspace = new Workspace({ storage: ctx.storage }); } getTools() { return createAITools({ workspace: this.workspace, - read: { maxBytes: 32 * 1024, maxLines: 800 }, + read: { + maxBytes: 32 * 1024, + maxLines: 800, + includeLineNumbers: true, + lineTruncation: { chars: 2000 }, + }, }); } } ``` -When assigning the same instance to `Think.workspace`, construct it with `useThink: true` so Think's built-in workspace tools can use the compatibility filesystem methods. Pass `shell` only when the `Workspace` was constructed with matching backend ids: +Pass the returned AI SDK `ToolSet` to `generateText`, `streamText`, or an agent framework hook such as `getTools()`. -```ts -const workspace = new Workspace({ - storage: ctx.storage, - backends: [ - new WorkerShellBackend({ id: "shell", /* ... */ }), - new CloudflareContainerBackend({ id: "container", /* ... */ }), - ], -}); +Pass `shell` only when the Workspace has matching backend ids: +```ts const tools = createAITools({ workspace, shell: { defaultBackend: "shell", backends: { - shell: { - description: "Fast Worker shell with built-in textual commands.", - }, - container: { - description: "Full Linux userland in a Cloudflare Container.", - }, + shell: { description: "Fast Worker shell with built-in text commands." }, + container: { description: "Full Linux userland in a Cloudflare Container." }, }, }, }); ``` -The returned value is an AI SDK `ToolSet`. Pass it to `generateText`, `streamText`, or an agent framework hook such as `getTools()`. - ## `createAITools` ```ts @@ -88,215 +80,171 @@ createAITools({ read?, write?, edit?, + find?, + grep?, + delete?, shell?, }); ``` | Option | Default | Notes | | --- | --- | --- | -| `workspace` | required | A `Workspace` or structural equivalent with `fs`, and optionally `shell`, `assets`, and `sessionId`. | -| `readonly` | `false` | When true, return only `read` and `ls`. This omits mutation tools, `exec`, and `publish` even if other options are present. | -| `assets` | `true` | Set to `false` to omit `publish`. When not false, `publish` appears only if `workspace.assets` is configured. | +| `workspace` | required | A `Workspace` or structural equivalent. | +| `readonly` | `false` | Omit `write`, `edit`, `delete`, `exec`, and `publish`. Search remains available. | +| `assets` | `true` | Set to `false` to omit `publish`. | | `read` | default caps | Options passed to `createReadTool`. | -| `write` | default caps | Options passed to `createWriteTool`. Ignored when `readonly` is true. | -| `edit` | default caps | Options passed to `createEditTool`. Ignored when `readonly` is true. | -| `shell` | omitted | Options passed to `createExecTool`. `exec` appears only when this is present and `readonly` is not true. | - -`createAITools({ workspace, readonly: true })` is the safe mode for agents that should inspect a workspace but not change it or run commands. +| `write` | default caps | Options passed to `createWriteTool`. | +| `edit` | default caps | Options passed to `createEditTool`. | +| `find` | defaults | Options passed to `createFindTool`. | +| `grep` | defaults | Options passed to `createGrepTool`. | +| `delete` | defaults | Options passed to `createDeleteTool`. | +| `shell` | omitted | Options passed to `createExecTool`. | ## `read` ```ts -createReadTool({ store, maxLines?, maxBytes? }); +createReadTool({ + store, + maxLines?, + maxBytes?, + includeLineNumbers?, + lineTruncation?, + maxModelBytes?, + mediaSniffBytes?, +}); ``` | Option | Default | Notes | | --- | --- | --- | | `maxLines` | 2000 | Hard line cap per call. | -| `maxBytes` | 256 KiB | Hard byte cap per call. | +| `maxBytes` | 256 KiB | Hard UTF-8 output cap per call. | +| `includeLineNumbers` | `false` | Prefix text lines with `${lineNumber}\t`. | +| `lineTruncation` | omitted | Shorten each line by `{ bytes }` or `{ chars }` before applying `maxBytes`. | +| `maxModelBytes` | 3.5 MiB | Largest image or PDF encoded into model output. | +| `mediaSniffBytes` | 512 | Prefix read when the extension does not identify the file. | Schema: ```ts { path: string; - offset?: number; // 1-indexed start line - limit?: number; // max lines this call + offset?: number; // 1-indexed start line + byteOffset?: number; // byte continuation from the previous result + limit?: number; } ``` -Returns the line window plus `nextOffset` whenever the result was truncated, so the model can call `read` again to keep going. Reads stream through `store.readChunks(path)` and stop as soon as the line or byte cap is hit. +A truncated text result has `totalLines: null`, `nextOffset`, and `nextByteOffset`. Pass both continuations to the next call. `nextOffset` preserves line numbering; `nextByteOffset` prevents the store from transferring bytes already read. -## `ls` +Known image and PDF extensions are classified without reading the file. Unknown extensions use a bounded magic-byte sniff. The tool's `toModelOutput` hook emits AI SDK `file-data` parts for images and PDFs. It reads and base64-encodes the whole file only after its size passes `maxModelBytes`. Other binary files return an unsupported binary result. -```ts -createListTool({ workspace }); -``` - -Schema: - -```ts -{ - path: string; -} -``` - -Calls `workspace.fs.readdir(path)` and returns: +## `ls` ```ts { path: string; - entries: Array<{ - name: string; - isFile: boolean; - isDirectory: boolean; - }>; + limit?: number; // default 200, maximum 1000 + offset?: number; } ``` -## `write` - -```ts -createWriteTool({ store, maxBytes? }); -``` - -| Option | Default | -| --- | --- | -| `maxBytes` | 2 MiB | +`ls` returns at most `limit` entries in name order. Each entry includes `name`, `size`, `mtime`, `isFile`, `isDirectory`, and `isSymbolicLink`. A non-final page includes `nextOffset`. -Schema: +## `find` ```ts { - path: string; - content: string; + path?: string; // default /workspace + pattern: string; + limit?: number; // default 200, maximum 1000 + offset?: number; } ``` -Overwrites the file. Preserves an existing file's `mode` so executable scripts keep their executable bit. Rejects writes larger than `maxBytes` with a structured error pointing the model at the `edit` tool or a smaller write. +The pattern is relative to `path`. `*` stays within one path segment, `**` crosses directories, and `?` matches one non-separator character. Results contain `path` and `type`; a non-final page includes `nextOffset`. -## `edit` - -```ts -createEditTool({ store, maxBytes? }); -``` - -| Option | Default | -| --- | --- | -| `maxBytes` | 2 MiB | - -Schema: +## `grep` ```ts { - path: string; - edits: Array<{ oldText: string; newText: string }>; + path?: string; // default /workspace + query: string; + include?: string; // glob relative to path + fixedString?: boolean; // default false + caseSensitive?: boolean;// default false + contextLines?: number; // 0 through 10 + limit?: number; // default 200, maximum 1000 + offset?: number; } ``` -Each edit is matched against the original file content, not incrementally. Overlapping or nested edits are rejected. The tool normalizes line endings for matching, restores the original line ending style on write, preserves the existing file mode, and returns a unified patch for review. +The AI tool defaults to case-insensitive regular expressions to match Think's tool contract. Set `fixedString` when the query should be treated as plain text. Matches include path, line number, text, and optional numbered context. Invalid regular expressions return a structured error. A non-final page includes `nextOffset`. -## `exec` +The lower-level `workspace.fs.grep` keeps its existing defaults: literal and case-sensitive. Its options also accept `limit`, `offset`, `contextLines`, `fixedString`, and `caseSensitive`. + +## `write` ```ts -createExecTool({ - workspace, - backends, - defaultBackend, - maxBytes?, -}); +createWriteTool({ store, maxBytes? }); // default 2 MiB ``` -| Option | Default | Notes | -| --- | --- | --- | -| `backends` | required | Map of backend id to a model-facing description. | -| `defaultBackend` | required | Backend used when the model omits `backend`. Must be a key in `backends`. | -| `maxBytes` | 64 KiB | UTF-8 byte cap for each of stdout and stderr. | +The schema is `{ path, content }`. Writing overwrites the file and preserves its existing mode. The tool rejects content over `maxBytes`. -Schema: +## `edit` ```ts -{ - command: string; - cwd?: string; - backend?: string; -} +createEditTool({ store, maxBytes? }); // default 2 MiB ``` -Calls `workspace.runtime.exec(command, { cwd, encoding: "utf8", backend })`, waits for `result()`, and returns: +The schema is: ```ts { - command: string; - cwd: string | null; - backend: string; - exitCode: number; - stdout: string; - stderr: string; + path: string; + edits: Array<{ oldText: string; newText: string }>; } ``` -`exec` is opt-in. `createAITools()` includes it only when the caller passes `shell` options and `readonly` is not true. The backend descriptions are included in the tool description so the model can choose the cheapest backend that can run the command. +Every `oldText` must identify one unique, non-overlapping range in the original content. The tool applies the batch atomically, preserves the byte order mark, line ending style, and file mode, and returns a unified patch plus `firstChangedLine`. -Wire this tool up carefully: it executes arbitrary shell commands inside the configured backend. Use `readonly: true` for inspection-only agents, or omit `shell` when command execution is not part of the agent's job. +`edit`, `write`, and `delete` share a per-store, per-path lock. A write cannot land between edit's read and write phases, while unrelated workspaces and paths remain independent. -## `publish` - -```ts -createPublishTool({ workspace }); -``` - -Schema: +## `delete` ```ts { path: string; - expiresAfterMs?: number; + recursive?: boolean; } ``` -Calls `workspace.assets.share(path, { expiresAfter, prefix })` and returns either: - -```ts -{ ok: true; url: string } -``` +The tool uses forced removal, so deleting a missing path succeeds. Set `recursive` to remove a non-empty directory. `readonly: true` omits this tool. -or: +## `exec` -```ts -{ ok: false; error: string } -``` +`exec` is opt-in. It calls `workspace.runtime.exec` with the configured backend and streams bounded output. Backend descriptions are included in the model-facing tool description, so describe capabilities and startup cost in plain language. Omit `shell` or use `readonly: true` when command execution is not part of the agent's job. -The default expiry is one hour. When `workspace.sessionId` is non-empty, the prefix is `agent-${workspace.sessionId}` so generated links are grouped by workspace session. If no session id was configured, the tool leaves the prefix unset. +## `publish` -`createAITools()` includes `publish` by default when `readonly` is not true, `assets` is not false, and `workspace.assets` is configured. Pass `assets: false` to hide the tool even when credentials are present. +`publish` calls `workspace.assets.share`. It appears when assets are configured, `assets` is not `false`, and the tool set is not read-only. The default link expiry is one hour. ## `FileStore` -The file tools depend on this shape: - ```ts interface FileStore { stat(path: string): Promise; readAll(path: string): Promise; - readChunks(path: string, byteOffset?: number, byteLength?: number): AsyncIterable; + readChunks( + path: string, + byteOffset?: number, + byteLength?: number, + ): AsyncIterable; write(path: string, bytes: Uint8Array, options?: { mode?: number }): Promise; } -interface FileStat { - size: number; - mode?: number; - mtime: number; +interface MutableFileStore extends FileStore { + remove(path: string, options?: { recursive?: boolean; force?: boolean }): Promise; } ``` -`WorkspaceFileStore` adapts `Workspace.fs.stat`, `Workspace.fs.readFile`, `Workspace.fs.writeFile`, and `Workspace.fs.mkdir` to this contract. Custom stores can use the same tools against another filesystem-shaped backend. - -## Conventions for agents - -- Tools take absolute paths. Pre-resolve user input against the configured workspace root before calling. See [01. VFS](./01_vfs.md). -- The `read` tool returns continuation offsets. Feed them back to the model on truncation rather than asking for the whole file. -- Pair the `edit` tool with a system prompt that says edits apply against the original file. Models that incrementally update their mental model of the file will produce overlapping edits and get a rejection error. -- Describe every shell backend in plain language. The model reads those descriptions when deciding where a command should run. -- Treat `exec` output as untrusted text when feeding it back into the model. -- Use `readonly: true` for review, indexing, or support agents that should not modify the workspace. +`WorkspaceFileStore` adapts the corresponding `workspace.fs` methods. Its chunk iterator uses fixed-size `readRange` calls, so seeking to a byte continuation does not stream and discard the preceding file content. diff --git a/packages/computer/README.md b/packages/computer/README.md index bffba952..adf6136e 100644 --- a/packages/computer/README.md +++ b/packages/computer/README.md @@ -22,8 +22,9 @@ things in it. module. Pick a full Linux container, a fast in-Worker shell, or an isolated JavaScript runtime, all against the same files. - **Batteries for agents.** Ready-made [AI SDK](https://github.com/vercel/ai) - tools (`read`, `write`, `edit`, `ls`, `exec`), a git client, R2-backed - read-only mounts, and helpers for publishing files. + tools (`read`, `ls`, `find`, `grep`, `write`, `edit`, `delete`, and optional + `exec`), a git client, R2-backed read-only mounts, and helpers for publishing + files. The Workspace can also run with no execution backend at all, giving you just the filesystem. @@ -266,8 +267,10 @@ to a named one — see [Multiple backends](#multiple-backends). `@cloudflare/computer/tools` ships AI SDK tools that wrap the Workspace surfaces, ready to hand to `generateText`, `streamText`, or an agent -framework's `getTools()`. The default set is `read`, `write`, `edit`, -and `ls`; `exec` and `publish` are added when you configure them. +framework's `getTools()`. The default set is `read`, `ls`, `find`, +`grep`, `write`, `edit`, and `delete`; `exec` and `publish` are added +when you configure them. Read-only mode keeps `read`, `ls`, `find`, and +`grep`. ```ts import { createAITools } from "@cloudflare/computer/tools"; @@ -286,8 +289,12 @@ const tools = createAITools({ ``` The model reads each backend's `description` when deciding where a -command should run, so write them in plain language. See -[`docs/09_tool_interface.md`](../../docs/09_tool_interface.md). +command should run, so write them in plain language. Large text reads +return both line and byte continuations; pass both to the next call to +avoid transferring the same bytes again. Images and PDFs are returned +as AI SDK `file-data` model output after a size check. `ls`, `find`, and +`grep` return bounded pages with `nextOffset` when more results exist. +See [`docs/09_tool_interface.md`](../../docs/09_tool_interface.md). ## Git @@ -400,7 +407,7 @@ on a computerd instance. | `@cloudflare/computer/backends/container` | `CloudflareContainerBackend` and `withWorkspaceContainer`. Pulls in the computerd / capnweb sync plumbing. | | `@cloudflare/computer/backends/worker-shell` | `WorkerShellBackend` and the bundled just-bash runtime. | | `@cloudflare/computer/backends/worker-javascript` | `WorkerJavaScriptBackend`, configured libraries, durable imports, `node:fs/promises`, and trusted `ws:git` / `ws:artifacts`. | -| `@cloudflare/computer/tools` | AI SDK tools for agents: `read`, `write`, `edit`, `ls`, optional `exec` and `publish`. | +| `@cloudflare/computer/tools` | AI SDK tools for agents: `read`, `ls`, `find`, `grep`, `write`, `edit`, `delete`, and optional `exec` and `publish`. | | `@cloudflare/computer/git` | Opt-in `isomorphic-git` glue for checkouts inside the workspace. | | `@cloudflare/computer/assets` | `createAssets` — share a workspace file to R2 as a presigned URL. | | `@cloudflare/computer/artifacts` | `createArtifact` and its CLI, a session-scoped facade over the Cloudflare Artifacts binding. | diff --git a/packages/dofs/README.md b/packages/dofs/README.md index 8eacbf4a..0ef0cd25 100644 --- a/packages/dofs/README.md +++ b/packages/dofs/README.md @@ -45,6 +45,11 @@ export class WorkspaceDO extends DurableObject { - `incrementRev()` shared sequencer in place. FS writes stamp the returned value into `vfs_nodes.rev` and pass it to `sync/changes.ts` for tombstones. - `SQLiteTestStorage` (backed by `node:sqlite`) available from `./testing` for unit tests against a real in-memory database; `RecordingStorage` available from the package root for workerd-safe schema assertions. - All filesystem primitives listed above are implemented and unit-tested. + `readdir` returns size and modification time and supports stable + `limit`/`offset` pages, including files held in pending write buffers. + `find` supports `*`, `**`, and `?` globs. `grep` supports bounded pages, + regular expressions or fixed strings, explicit case handling, and numbered + context lines. - `SQLiteWorkspaceProvider` (the `@platformatic/vfs` adapter) implemented and exported from the package entrypoint; consumed by `@cloudflare/computerd`. - Buffered-write surface for the FUSE driver: `createFileSync`, `writeRangeSync`, `truncateFileSync`, `readRangeSync`, `chmodSync`, @@ -53,6 +58,8 @@ export class WorkspaceDO extends DurableObject { on FUSE create/open, mutates it through subsequent writes and truncates, and commits chunks in one transaction at release time. Reads against the same database see the buffered bytes immediately. +- `WorkspaceFilesystem.readRange` exposes bounded byte reads without + materializing the whole file. - Content-addressed blob cache: `readFile`, `readRangeSync`, `provider.readFileSync`, and the partial-chunk read-modify-write helper share a per-`Database` LRU keyed by `vfs_blob_bytes.hash`. From 2991391bfb883e20d911c63a0be42484dd8c5efe Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:10:48 +0000 Subject: [PATCH 02/11] docs: Correct bounded tool contracts --- docs/09_tool_interface.md | 23 ++++++++++++++++++----- packages/computer/README.md | 15 +++++++++------ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index c5155a3f..83022374 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -134,9 +134,9 @@ Schema: } ``` -A truncated text result has `totalLines: null`, `nextOffset`, and `nextByteOffset`. Pass both continuations to the next call. `nextOffset` preserves line numbering; `nextByteOffset` prevents the store from transferring bytes already read. +A truncated text result has `totalLines: null`, `nextOffset`, and `nextByteOffset`. Pass both continuations to the next call. `nextOffset` preserves line numbering; `nextByteOffset` prevents the store from transferring bytes already read. The AI SDK model output keeps this complete result as JSON when a read is truncated. A complete read remains plain text. -Known image and PDF extensions are classified without reading the file. Unknown extensions use a bounded magic-byte sniff. The tool's `toModelOutput` hook emits AI SDK `file-data` parts for images and PDFs. It reads and base64-encodes the whole file only after its size passes `maxModelBytes`. Other binary files return an unsupported binary result. +Known image and PDF extensions are classified without reading the file. Unknown extensions use a bounded magic-byte sniff. The tool's `toModelOutput` hook emits AI SDK `file-data` parts for images and PDFs. It checks the file size, then reads at most `maxModelBytes + 1` bytes before deciding whether to encode the file. This keeps the load bounded if the file grows after the size check. Other binary files return an unsupported binary result. ## `ls` @@ -161,7 +161,7 @@ Known image and PDF extensions are classified without reading the file. Unknown } ``` -The pattern is relative to `path`. `*` stays within one path segment, `**` crosses directories, and `?` matches one non-separator character. Results contain `path` and `type`; a non-final page includes `nextOffset`. +The pattern is relative to `path`. `*` stays within one path segment, `**` crosses directories, and `?` matches one non-separator character. Results contain `path` and `type`; a non-final page includes `nextOffset`. Pagination reaches `workspace.fs.find`, which walks directory children in fixed-size pages and stops after collecting the requested page instead of materializing every match. ## `grep` @@ -180,7 +180,9 @@ The pattern is relative to `path`. `*` stays within one path segment, `**` cross The AI tool defaults to case-insensitive regular expressions to match Think's tool contract. Set `fixedString` when the query should be treated as plain text. Matches include path, line number, text, and optional numbered context. Invalid regular expressions return a structured error. A non-final page includes `nextOffset`. -The lower-level `workspace.fs.grep` keeps its existing defaults: literal and case-sensitive. Its options also accept `limit`, `offset`, `contextLines`, `fixedString`, and `caseSensitive`. +The tool passes `include`, `limit`, and `offset` through one `workspace.fs.grep` call. The storage search pages matching files and stops after the requested matches, so an included search does not build the full file or match list in the tool layer. + +The lower-level `workspace.fs.grep` keeps its existing defaults: literal and case-sensitive. Its options also accept `limit`, `offset`, `include`, `contextLines`, `fixedString`, and `caseSensitive`. ## `write` @@ -207,7 +209,7 @@ The schema is: Every `oldText` must identify one unique, non-overlapping range in the original content. The tool applies the batch atomically, preserves the byte order mark, line ending style, and file mode, and returns a unified patch plus `firstChangedLine`. -`edit`, `write`, and `delete` share a per-store, per-path lock. A write cannot land between edit's read and write phases, while unrelated workspaces and paths remain independent. +`edit`, `write`, and `delete` share locks through the store's stable `lockIdentity`. Every `WorkspaceFileStore` over the same `workspace.fs` uses the same identity, including adapters created by separate `createAITools()` calls. A write cannot land between edit's read and write phases, while unrelated workspaces and paths remain independent. Recursive deletion also locks the whole subtree, so mutations to ancestors or descendants cannot interleave with it. ## `delete` @@ -231,7 +233,14 @@ The tool uses forced removal, so deleting a missing path succeeds. Set `recursiv ## `FileStore` ```ts +interface FileStat { + size: number; + mtime: number; + mode?: number; +} + interface FileStore { + readonly lockIdentity?: object; stat(path: string): Promise; readAll(path: string): Promise; readChunks( @@ -247,4 +256,8 @@ interface MutableFileStore extends FileStore { } ``` +`readChunks` must stream without loading the full file at once. It yields no bytes at or beyond end of file; otherwise it yields exactly `min(byteLength ?? size - byteOffset, size - byteOffset)` bytes and throws when the path is missing. `readAll` is the explicit whole-file operation used only where the caller applies its own size bound or needs all content for an edit. + +`lockIdentity` coordinates mutations across adapters that represent the same storage resource. Custom stores should share one identity when their instances can reach the same files. + `WorkspaceFileStore` adapts the corresponding `workspace.fs` methods. Its chunk iterator uses fixed-size `readRange` calls, so seeking to a byte continuation does not stream and discard the preceding file content. diff --git a/packages/computer/README.md b/packages/computer/README.md index adf6136e..76a280c1 100644 --- a/packages/computer/README.md +++ b/packages/computer/README.md @@ -289,12 +289,15 @@ const tools = createAITools({ ``` The model reads each backend's `description` when deciding where a -command should run, so write them in plain language. Large text reads -return both line and byte continuations; pass both to the next call to -avoid transferring the same bytes again. Images and PDFs are returned -as AI SDK `file-data` model output after a size check. `ls`, `find`, and -`grep` return bounded pages with `nextOffset` when more results exist. -See [`docs/09_tool_interface.md`](../../docs/09_tool_interface.md). +command should run, so write them in plain language. Truncated text +model output keeps both line and byte continuations; pass both to the +next call to avoid transferring the same bytes again. Images and PDFs +are returned as AI SDK `file-data` model output through a bounded read. +`ls`, `find`, and `grep` pass pagination through to the storage layer +and return `nextOffset` when more results exist. File mutations share +locks across tool sets for the same workspace, and recursive deletion +excludes mutations throughout its subtree. See +[`docs/09_tool_interface.md`](../../docs/09_tool_interface.md). ## Git From 3d9fef4ca91e2c5693dd4a5b6025aa2d3b0e8a24 Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:27:20 +0000 Subject: [PATCH 03/11] docs: Clarify reserved tool options --- docs/09_tool_interface.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 83022374..92756c5b 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -95,9 +95,7 @@ createAITools({ | `read` | default caps | Options passed to `createReadTool`. | | `write` | default caps | Options passed to `createWriteTool`. | | `edit` | default caps | Options passed to `createEditTool`. | -| `find` | defaults | Options passed to `createFindTool`. | -| `grep` | defaults | Options passed to `createGrepTool`. | -| `delete` | defaults | Options passed to `createDeleteTool`. | +| `find`, `grep`, `delete` | omitted | Reserved option bags with no configurable fields yet. | | `shell` | omitted | Options passed to `createExecTool`. | ## `read` From 892f73a84bbcc605f59e5e3845dcc2dc613aaaea Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:24:00 +0000 Subject: [PATCH 04/11] docs: Restore agent safety guidance --- docs/09_tool_interface.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 92756c5b..9a531cb6 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -222,7 +222,9 @@ The tool uses forced removal, so deleting a missing path succeeds. Set `recursiv ## `exec` -`exec` is opt-in. It calls `workspace.runtime.exec` with the configured backend and streams bounded output. Backend descriptions are included in the model-facing tool description, so describe capabilities and startup cost in plain language. Omit `shell` or use `readonly: true` when command execution is not part of the agent's job. +`exec` is opt-in. It calls `workspace.runtime.exec` with the configured backend and streams bounded output. Backend descriptions are included in the model-facing tool description, so describe capabilities and startup cost in plain language. + +Wire this tool carefully: it executes arbitrary shell commands inside the configured backend. Treat its output as untrusted text when including it in later model input. Omit `shell` or use `readonly: true` when command execution is not part of the agent's job. ## `publish` @@ -259,3 +261,12 @@ interface MutableFileStore extends FileStore { `lockIdentity` coordinates mutations across adapters that represent the same storage resource. Custom stores should share one identity when their instances can reach the same files. `WorkspaceFileStore` adapts the corresponding `workspace.fs` methods. Its chunk iterator uses fixed-size `readRange` calls, so seeking to a byte continuation does not stream and discard the preceding file content. + +## Conventions for agents + +- Tools take absolute paths. Resolve user input against the configured workspace root before calling them. See [01. VFS](./01_vfs.md). +- The `read` tool returns line and byte continuation offsets. Pass both back on the next call instead of asking for the whole file again. +- Tell the model that each `edit` batch applies against the original file content. Treating each edit as an incremental change can produce overlapping edits, which the tool rejects. +- Describe every shell backend in plain language. The model reads these descriptions when deciding where to run a command. +- Treat `exec` output as untrusted text when including it in later model input. +- Use `readonly: true` for review, indexing, or support agents that should not modify the workspace. From 946f60d7e1d667978f6bb53d13d93a292b0aff02 Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:37:56 +0000 Subject: [PATCH 05/11] docs: Define grep discovery order --- docs/09_tool_interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 9a531cb6..5509c49e 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -178,7 +178,7 @@ The pattern is relative to `path`. `*` stays within one path segment, `**` cross The AI tool defaults to case-insensitive regular expressions to match Think's tool contract. Set `fixedString` when the query should be treated as plain text. Matches include path, line number, text, and optional numbered context. Invalid regular expressions return a structured error. A non-final page includes `nextOffset`. -The tool passes `include`, `limit`, and `offset` through one `workspace.fs.grep` call. The storage search pages matching files and stops after the requested matches, so an included search does not build the full file or match list in the tool layer. +The tool passes `include`, `limit`, and `offset` through one `workspace.fs.grep` call. The storage search pages matching files and stops after the requested matches, so an included search does not build the full file or match list in the tool layer. Directory searches return matches in deterministic depth-first discovery order, then line order within each file. They are not globally sorted by full path. The lower-level `workspace.fs.grep` keeps its existing defaults: literal and case-sensitive. Its options also accept `limit`, `offset`, `include`, `contextLines`, `fixedString`, and `caseSensitive`. From dcb2cef6c726539e908165b17468849ec09bd988 Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:50:12 +0000 Subject: [PATCH 06/11] docs: Rename grep options --- docs/04_filesystem_interface.md | 59 +++++++++++++++++++++++---------- docs/09_tool_interface.md | 10 +++--- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/docs/04_filesystem_interface.md b/docs/04_filesystem_interface.md index 1e40b11c..6131558d 100644 --- a/docs/04_filesystem_interface.md +++ b/docs/04_filesystem_interface.md @@ -231,38 +231,61 @@ const paths = await fs.ls("/workspace/.agents/skills"); ### `grep` -Available on `Workspace.fs` for parity with the agent tools, and on -`Workspace.runtime` when you want it to run inside the container (faster -for large trees because it uses ripgrep). +`Workspace.fs.grep` accepts this interface: ```ts +interface GrepOptions { + regex?: boolean; + ignoreCase?: boolean; + context?: number; + limit?: number; + offset?: number; + include?: string; +} + +interface WorkspaceGrepContextLine { + line: number; + text: string; + isMatch: boolean; +} + +interface WorkspaceGrepMatch { + path: string; + line: number; + text: string; + context?: WorkspaceGrepContextLine[]; +} + grep( pattern: string, - path: string, - options?: { ignoreCase?: boolean } -): Promise<{ path: string; line: number; text: string }[]> + path: string, + options?: GrepOptions, +): Promise ``` -`pattern` is a **literal substring** — not a regex, not a glob. -`ignoreCase` lowercases both sides before comparing. - -`path` may be a directory **or a single file**. Directory walks return -matches in walk order. Each result row carries: +Matching is literal and case-sensitive by default. Set `regex: true` to +interpret `pattern` as a regular expression and `ignoreCase: true` to ignore +letter case. `context` adds that many lines before and after each match. +`include` is a glob relative to a searched directory. `limit` and `offset` +paginate matching lines. -- `path` — absolute path of the matching file. -- `line` — 1-indexed line number within that file. -- `text` — the entire matching line (without the trailing newline), not - just the matched substring. +`path` may be a directory or a single file. Directory searches return matches +in deterministic depth-first discovery order, then line order within each +file. Results are not globally sorted by full path. ```ts -const hits = await fs.grep("TODO", "/workspace/src", { ignoreCase: true }); +const hits = await fs.grep("TODO", "/workspace/src", { + ignoreCase: true, + include: "**/*.ts", +}); for (const hit of hits) { console.log(`${hit.path}:${hit.line}: ${hit.text}`); } ``` -See [05. Shell Interface](./05_runtime_interface.md) for the container-side -variant. +`Workspace.runtime` exposes a narrower container-side variant that accepts only +`ignoreCase` and treats its pattern as a literal string. See +[05. Shell Interface](./05_runtime_interface.md) for that variant. ## Error handling diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 5509c49e..42826cbe 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -168,19 +168,19 @@ The pattern is relative to `path`. `*` stays within one path segment, `**` cross path?: string; // default /workspace query: string; include?: string; // glob relative to path - fixedString?: boolean; // default false - caseSensitive?: boolean;// default false - contextLines?: number; // 0 through 10 + regex?: boolean; // default false + ignoreCase?: boolean; // default false + context?: number; // 0 through 10 limit?: number; // default 200, maximum 1000 offset?: number; } ``` -The AI tool defaults to case-insensitive regular expressions to match Think's tool contract. Set `fixedString` when the query should be treated as plain text. Matches include path, line number, text, and optional numbered context. Invalid regular expressions return a structured error. A non-final page includes `nextOffset`. +The AI tool defaults to literal, case-sensitive matching. Set `regex: true` to interpret `query` as a regular expression and `ignoreCase: true` to ignore letter case. Matches include path, line number, text, and optional numbered context. Invalid regular expressions return a structured error. A non-final page includes `nextOffset`. The tool passes `include`, `limit`, and `offset` through one `workspace.fs.grep` call. The storage search pages matching files and stops after the requested matches, so an included search does not build the full file or match list in the tool layer. Directory searches return matches in deterministic depth-first discovery order, then line order within each file. They are not globally sorted by full path. -The lower-level `workspace.fs.grep` keeps its existing defaults: literal and case-sensitive. Its options also accept `limit`, `offset`, `include`, `contextLines`, `fixedString`, and `caseSensitive`. +The lower-level `workspace.fs.grep` uses the same literal, case-sensitive defaults. Its options also accept `limit`, `offset`, `include`, `context`, `regex`, and `ignoreCase`. ## `write` From c85f757ca06c5d521c72cbe9dcf43ec8a03a5a8c Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:55:19 +0000 Subject: [PATCH 07/11] docs: Describe ranged streams and ls pages --- docs/04_filesystem_interface.md | 25 +++++++++++++++++++++++-- docs/09_tool_interface.md | 24 +++++++++++++++++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/docs/04_filesystem_interface.md b/docs/04_filesystem_interface.md index 6131558d..6f9a453b 100644 --- a/docs/04_filesystem_interface.md +++ b/docs/04_filesystem_interface.md @@ -37,19 +37,40 @@ method-by-method mapping against `node:fs/promises`. ### `readFile` ```ts +type ReadFileRange = { + byteOffset?: number; // default 0 + byteLength?: number; // default: remainder of the file +}; + readFile(path: string): Promise> readFile(path: string, encoding: "utf8"): Promise -readFile(path: string, options: { encoding?: "utf8" }): Promise +readFile(path: string, options: ReadFileRange): Promise> +readFile( + path: string, + options: ReadFileRange & { encoding: "utf8" }, +): Promise ``` Defaulting to a stream is deliberate — most reads in an agent context -are "send this file somewhere" and never need to be in memory. +are "send this file somewhere" and never need to be in memory. A ranged +stream resolves the file and captures its overlapping chunk rows once, +then lazily sends those content-addressed blobs. This preserves the existing +whole-file stream behavior across ordinary concurrent writes while avoiding +reads before `byteOffset`. The same single stream +crosses the Workers RPC boundary; callers do not issue one RPC invocation +per storage chunk. ```ts // Stream a large file straight to the client. const stream = await fs.readFile("/workspace/build/out.wasm"); return new Response(stream, { headers: { "content-type": "application/wasm" } }); +// Resume a stream at a byte continuation and cap the transfer. +const continuation = await fs.readFile("/workspace/build/out.wasm", { + byteOffset: 1_048_576, + byteLength: 262_144, +}); + // Read a small text file into a string. const todo = await fs.readFile("/workspace/notes/todo.md", "utf8"); diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 42826cbe..694c51d0 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -132,7 +132,7 @@ Schema: } ``` -A truncated text result has `totalLines: null`, `nextOffset`, and `nextByteOffset`. Pass both continuations to the next call. `nextOffset` preserves line numbering; `nextByteOffset` prevents the store from transferring bytes already read. The AI SDK model output keeps this complete result as JSON when a read is truncated. A complete read remains plain text. +A truncated text result has `totalLines: null`, `nextOffset`, and `nextByteOffset`. Pass both continuations to the next call. `nextOffset` preserves line numbering; `nextByteOffset` opens the next database-backed stream at that byte instead of transferring bytes already read. The workspace adapter uses one ranged stream per tool call, including across Workers RPC; it does not issue one eager range RPC per chunk. The AI SDK model output keeps this complete result as JSON when a read is truncated. A complete read remains plain text. Known image and PDF extensions are classified without reading the file. Unknown extensions use a bounded magic-byte sniff. The tool's `toModelOutput` hook emits AI SDK `file-data` parts for images and PDFs. It checks the file size, then reads at most `maxModelBytes + 1` bytes before deciding whether to encode the file. This keeps the load bounded if the file grows after the size check. Other binary files return an unsupported binary result. @@ -146,7 +146,25 @@ Known image and PDF extensions are classified without reading the file. Unknown } ``` -`ls` returns at most `limit` entries in name order. Each entry includes `name`, `size`, `mtime`, `isFile`, `isDirectory`, and `isSymbolicLink`. A non-final page includes `nextOffset`. +`ls` defaults to at most 200 entries and returns this shape: + +```ts +{ + path: string; + count: number; + entries: Array<{ + name: string; + size: number; + mtime: number; + isFile: boolean; + isDirectory: boolean; + isSymbolicLink: boolean; + }>; + nextOffset?: number; +} +``` + +Entries are in name order. A non-final page includes `nextOffset`; pass it as the next call's `offset`. ## `find` @@ -260,7 +278,7 @@ interface MutableFileStore extends FileStore { `lockIdentity` coordinates mutations across adapters that represent the same storage resource. Custom stores should share one identity when their instances can reach the same files. -`WorkspaceFileStore` adapts the corresponding `workspace.fs` methods. Its chunk iterator uses fixed-size `readRange` calls, so seeking to a byte continuation does not stream and discard the preceding file content. +`WorkspaceFileStore` adapts the corresponding `workspace.fs` methods. Its chunk iterator opens one ranged `readFile` stream, so seeking to a byte continuation neither transfers the preceding content nor issues one RPC invocation per chunk. ## Conventions for agents From 235c2d6f03bfd7784565a3b666ec3ca9ddb77ac2 Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:39:21 +0000 Subject: [PATCH 08/11] docs: Remove empty tool option bags --- docs/09_tool_interface.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 694c51d0..4dbf9c92 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -80,9 +80,6 @@ createAITools({ read?, write?, edit?, - find?, - grep?, - delete?, shell?, }); ``` @@ -95,7 +92,6 @@ createAITools({ | `read` | default caps | Options passed to `createReadTool`. | | `write` | default caps | Options passed to `createWriteTool`. | | `edit` | default caps | Options passed to `createEditTool`. | -| `find`, `grep`, `delete` | omitted | Reserved option bags with no configurable fields yet. | | `shell` | omitted | Options passed to `createExecTool`. | ## `read` From 37616d92a488d0c94e2b3cbb7fc6506457066747 Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:39:37 +0000 Subject: [PATCH 09/11] docs: Describe stable multimodal reads --- docs/09_tool_interface.md | 4 ++-- packages/computer/README.md | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/09_tool_interface.md b/docs/09_tool_interface.md index 4dbf9c92..64d049c7 100644 --- a/docs/09_tool_interface.md +++ b/docs/09_tool_interface.md @@ -128,9 +128,9 @@ Schema: } ``` -A truncated text result has `totalLines: null`, `nextOffset`, and `nextByteOffset`. Pass both continuations to the next call. `nextOffset` preserves line numbering; `nextByteOffset` opens the next database-backed stream at that byte instead of transferring bytes already read. The workspace adapter uses one ranged stream per tool call, including across Workers RPC; it does not issue one eager range RPC per chunk. The AI SDK model output keeps this complete result as JSON when a read is truncated. A complete read remains plain text. +A truncated text result has `totalLines: null`, `nextOffset`, and `nextByteOffset`. Pass both continuations to the next call. A positive `byteOffset` is valid only with `offset`; `byteOffset: 0` starts from the beginning. `nextOffset` preserves line numbering, while `nextByteOffset` opens the next database-backed stream at that byte instead of transferring bytes already read. The workspace adapter uses one ranged stream per tool call, including across Workers RPC; it does not issue one eager range RPC per chunk. The AI SDK model output keeps the complete result as JSON when a read is truncated, empty, or explicitly positioned. Other complete text reads remain plain text. -Known image and PDF extensions are classified without reading the file. Unknown extensions use a bounded magic-byte sniff. The tool's `toModelOutput` hook emits AI SDK `file-data` parts for images and PDFs. It checks the file size, then reads at most `maxModelBytes + 1` bytes before deciding whether to encode the file. This keeps the load bounded if the file grows after the size check. Other binary files return an unsupported binary result. +Known image and PDF extensions are classified without a prefix read. Unknown extensions use a bounded magic-byte and UTF-8 sniff. SVG source is returned as text rather than inline media. During execution, the tool reads at most `maxModelBytes + 1` bytes and captures eligible image or PDF data in the result. The `toModelOutput` hook performs no filesystem I/O and emits an AI SDK `file` part from those captured bytes, so regenerated prompt history cannot observe later file changes. Other binary files return an unsupported binary result. ## `ls` diff --git a/packages/computer/README.md b/packages/computer/README.md index 76a280c1..b0d5fa84 100644 --- a/packages/computer/README.md +++ b/packages/computer/README.md @@ -291,9 +291,10 @@ const tools = createAITools({ The model reads each backend's `description` when deciding where a command should run, so write them in plain language. Truncated text model output keeps both line and byte continuations; pass both to the -next call to avoid transferring the same bytes again. Images and PDFs -are returned as AI SDK `file-data` model output through a bounded read. -`ls`, `find`, and `grep` pass pagination through to the storage layer +next call to avoid transferring the same bytes again. Eligible image and +PDF bytes are captured once during the bounded tool execution and returned +as AI SDK `file` model output without re-reading the file. SVG source remains +text. `ls`, `find`, and `grep` pass pagination through to the storage layer and return `nextOffset` when more results exist. File mutations share locks across tool sets for the same workspace, and recursive deletion excludes mutations throughout its subtree. See From 64d924d23811f6bd55903f225e4c770785e9fb8d Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:07:33 +0000 Subject: [PATCH 10/11] docs: Remove the readRange facade --- packages/dofs/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/dofs/README.md b/packages/dofs/README.md index 0ef0cd25..2027226a 100644 --- a/packages/dofs/README.md +++ b/packages/dofs/README.md @@ -58,8 +58,6 @@ export class WorkspaceDO extends DurableObject { on FUSE create/open, mutates it through subsequent writes and truncates, and commits chunks in one transaction at release time. Reads against the same database see the buffered bytes immediately. -- `WorkspaceFilesystem.readRange` exposes bounded byte reads without - materializing the whole file. - Content-addressed blob cache: `readFile`, `readRangeSync`, `provider.readFileSync`, and the partial-chunk read-modify-write helper share a per-`Database` LRU keyed by `vfs_blob_bytes.hash`. From 47adb37d722bbb63945e0069afb557a6f07f2c71 Mon Sep 17 00:00:00 2001 From: aron <263346377+aron-cf@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:11:55 +0000 Subject: [PATCH 11/11] docs: Correct find and grep summaries --- docs/04_filesystem_interface.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/04_filesystem_interface.md b/docs/04_filesystem_interface.md index 6f9a453b..1ce88505 100644 --- a/docs/04_filesystem_interface.md +++ b/docs/04_filesystem_interface.md @@ -208,7 +208,11 @@ console.log(`${s.size} bytes, modified ${new Date(s.mtime).toISOString()}`); ```ts find( directory: string, - pattern?: string, // simple glob (`*.ts`, `**/*.md`) + pattern?: string, // simple glob (`*.ts`, `**/*.md`) + options?: { + limit?: number; + offset?: number; + }, ): Promise> ``` @@ -218,7 +222,7 @@ matched against each candidate's path **relative to `directory`**, not its absolute path — so `**/*.ts` under `/workspace/src` matches `a/b.ts`, not `/workspace/src/a/b.ts`. -Only `*`, `**`, and `**/` are honored; `?`, character classes, and +The glob supports `*`, `**`, `**/`, and `?`. Character classes and brace expansions are matched literally. ```ts @@ -386,8 +390,8 @@ maps to `Workspace.fs`: | `symlink` / `readlink` | — | Not on the public surface; see note below. | | `watch` | — | Low-level primitive in `fs/watch.ts` (`createWatcher`, `createWatchAsyncIterable`, `WatchHandle`, `WatchOptions`); not exposed on the `WorkspaceFilesystem` class. | | `open` / `FileHandle` | — | Use streams instead. | -| `glob` | `find` | Limited glob support (`*`, `**`, `**/` only). | -| — | `grep` | Not in `node:fs`; included here for agents. Substring match. | +| `glob` | `find` | Limited glob support (`*`, `**`, `**/`, and `?`). | +| — | `grep` | Not in `node:fs`; literal by default, with optional regular expressions. | | — | `find` | Recursive directory walk with an optional glob, relative-rooted. | | — | `ls` | Flat list of file paths under a directory (segment-aware). |