diff --git a/vendor/agents/.changeset/bounded-workspace-ranges.md b/vendor/agents/.changeset/bounded-workspace-ranges.md new file mode 100644 index 0000000..425261d --- /dev/null +++ b/vendor/agents/.changeset/bounded-workspace-ranges.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/shell": minor +--- + +Add optional bounded byte reads to WorkspaceFsLike and FileSystem. OPFS reads +native File slices under its existing locks; WorkspaceFileSystem validates ranges +and preserves compatibility with backends that only implement whole-file reads. diff --git a/vendor/agents/docs/fork-diff.md b/vendor/agents/docs/fork-diff.md index 86f5a0e..60b890a 100644 --- a/vendor/agents/docs/fork-diff.md +++ b/vendor/agents/docs/fork-diff.md @@ -104,6 +104,7 @@ upstreamable additions, not claims that Workers need browser polyfills. | Early model preparation and media: Think `prepareModel`, inference configuration and `tools/workspace.ts` materialize history/tool media for one selected model. | Rook model configuration; model-history-capability and read-tool wire/contract tests. | `beforeTurn` occurs too late to restore bytes excluded during history rendering. Keep the early owner seam until upstream supplies equivalent selection and per-render image/PDF capability controls. | | Inference callback context: `think/src/inference-context.ts` binds admitted-turn model callbacks and tool iterator operations. | Real AI SDK mock-model streams in Think Chromium tests overlap ordinary/streaming tools and cleanup. | Native stream callbacks can enter outside their creator's scope. Compiler-assisted awaits cannot restore missing callback-entry context. Retire only when upstream/native context covers those same entry points. | | Shell OPFS: `shell/src/browser/` implements host-selected directories, symlinks, Web Locks and staged writes; `filesystem.ts`, `extras.ts`, `helpers.ts`, `fs/mime-types.ts` share existing filesystem facts. | Rook shared workspace; real Worker OPFS tests cover failures, cancellation and concurrent writes. | Optional backend, not a second Rook filesystem. Runtime SQL/OPFS alone does not implement Shell's filesystem contract. Retire with a matching upstream backend. | +| Shell byte ranges: `WorkspaceFsLike`, `FileSystem`, `WorkspaceFileSystem` and `browser/opfs-workspace.ts` expose optional bounded reads. | Rook mounted-PDF shell performance; native Worker OPFS tests assert exact slices and legacy SQL adapter tests verify fallback. | Filesystem capability belongs in Shell. Existing implementations remain compatible; retire when upstream exposes equivalent range reads. | | Shell metadata export and Git scans: package/build export `state-methods.ts`; `shell/src/git/index.ts` uses three `refresh:false` scan options. | Rook workspace connector; real browser Git test checks index bytes and changed-content detection. | Pure metadata avoids loading Codemode. Browser handles lack full POSIX stat identity; scans must not rewrite the index. Retire with equivalent public metadata and configurable/native scan behavior. | | Voice flush/error/meter: `agents/src/voice/{types,audio-pipeline,voice-input}.ts` exposes optional `waitUntilClosed`, waits final audio before idle, propagates close failures and meters accepted PCM; background context keeps microtask ordering with `await Promise.resolve()` inside eager async entry. | Rook transcriber/root Agent; native Voice and browser dictation tests. | Final transcript must precede idle. `waitUntilReady` is already upstream. The implementation now lives in `agents/voice`; retire with equivalent upstream hooks; an unrelated response-settlement workaround cannot replace transcriber flush. | | Package identity and syntax: six build scripts use tsdown’s `deps.neverBundle`, preserve constructor names and ES2021 async syntax; manifests expose optional browser and metadata leaves. Think aligns Chat to 4.38 and declares optional Slack/Discord adapter peers for those leaves. | Actor/facet identities and runtime async transform; built-package integration and export checks. | A consumer cannot recover renamed constructors or lowered async code. Retire when upstream builds preserve these contracts; never replace with source aliases. | diff --git a/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts b/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts index a46ae4d..f37b3c6 100644 --- a/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts +++ b/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts @@ -152,3 +152,16 @@ it("serializes appends from separate Workers using the same OPFS root", async () .sort() ); }); + +it("reads bounded binary ranges through OPFS without reading a whole native file", async () => { + expect(await run(directory(), "byte-ranges")).toEqual({ + prefix: [0, 1, 2], + suffix: [254, 255], + empty: [], + pastEnd: [], + missing: null, + directory: "EISDIR", + invalid: "EINVAL", + reads: [3, 2, 0, 0] + }); +}); diff --git a/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts b/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts index ebcf486..cd7b0a1 100644 --- a/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts +++ b/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts @@ -39,6 +39,35 @@ async function run(name: string, operation: string, writer = "") { const root = await storage.getDirectoryHandle(name, { create: true }); const ws = new OpfsWorkspace({ root }); switch (operation) { + case "byte-ranges": { + await ws.writeFileBytes( + "/data", + Uint8Array.from({ length: 256 }, (_, i) => i) + ); + await ws.symlink("data", "/link"); + const fs = ws; + const reads: number[] = []; + const arrayBuffer = Blob.prototype.arrayBuffer; + Blob.prototype.arrayBuffer = function () { + reads.push(this.size); + if (this.size > 3) throw new Error("Full-file read forbidden"); + return arrayBuffer.call(this); + }; + try { + return { + prefix: Array.from((await fs.readFileRange("/link", 0, 3)) ?? []), + suffix: Array.from((await fs.readFileRange("/data", 254, 10)) ?? []), + empty: Array.from((await fs.readFileRange("/data", 0, 0)) ?? []), + pastEnd: Array.from((await fs.readFileRange("/data", 300, 1)) ?? []), + missing: await fs.readFileRange("/missing", 0, 0), + directory: await errorCode(fs.readFileRange("/", 0, 0)), + invalid: await errorCode(fs.readFileRange("/data", -1, 1)), + reads + }; + } finally { + Blob.prototype.arrayBuffer = arrayBuffer; + } + } case "contract": return { memory: await fileContract(new InMemoryFs()), diff --git a/vendor/agents/packages/shell/src/browser/opfs-workspace.ts b/vendor/agents/packages/shell/src/browser/opfs-workspace.ts index fc8129d..e52a9d9 100644 --- a/vendor/agents/packages/shell/src/browser/opfs-workspace.ts +++ b/vendor/agents/packages/shell/src/browser/opfs-workspace.ts @@ -1,3 +1,4 @@ +import { validateReadRange } from "../fs/read-range"; import type { FileInfo, Workspace, WorkspaceFsLike } from "../filesystem"; import { MAX_WORKSPACE_PATH_LENGTH, @@ -125,7 +126,23 @@ export class OpfsWorkspace implements WorkspaceFsLike { return bytes === null ? null : TEXT_DECODER.decode(bytes); } + async readFileRange( + path: string, + offset: number, + length: number + ): Promise { + validateReadRange(offset, length); + return this.readBytes(path, { offset, length }); + } + async readFileBytes(path: string): Promise { + return this.readBytes(path); + } + + private async readBytes( + path: string, + range?: { offset: number; length: number } + ): Promise { const normalized = normalizePath(path); return this.withSharedTree(async () => { if (normalized === "/") { @@ -139,7 +156,11 @@ export class OpfsWorkspace implements WorkspaceFsLike { if (!native) return null; if (native.kind !== "file") throw fsError("EISDIR", `${path} is a directory`); - return new Uint8Array(await (await native.getFile()).arrayBuffer()); + const file = await native.getFile(); + const blob = range + ? file.slice(range.offset, range.offset + range.length) + : file; + return new Uint8Array(await blob.arrayBuffer()); }); }); } diff --git a/vendor/agents/packages/shell/src/filesystem.ts b/vendor/agents/packages/shell/src/filesystem.ts index 6d299b4..d708fca 100644 --- a/vendor/agents/packages/shell/src/filesystem.ts +++ b/vendor/agents/packages/shell/src/filesystem.ts @@ -178,7 +178,17 @@ export type WorkspaceFsLike = Pick< | "symlink" | "readlink" | "glob" ->; +> & { + /** Optional bounded byte read; follows symlinks, returns null for missing files. + * Offset and length must be nonnegative safe integers with a safe sum. + * Reads past EOF return an empty buffer; short reads at EOF are allowed. + */ + readFileRange?( + path: string, + offset: number, + length: number + ): Promise; +}; // ── Constants ──────────────────────────────────────────────────────── diff --git a/vendor/agents/packages/shell/src/fs/interface.ts b/vendor/agents/packages/shell/src/fs/interface.ts index ade18d1..587fcbd 100644 --- a/vendor/agents/packages/shell/src/fs/interface.ts +++ b/vendor/agents/packages/shell/src/fs/interface.ts @@ -52,6 +52,12 @@ export interface CpOptions { export interface FileSystem { readFile(path: string): Promise; readFileBytes(path: string): Promise; + /** Optional bounded byte read. Nonnegative safe integer range; missing paths throw ENOENT. */ + readFileRange?( + path: string, + offset: number, + length: number + ): Promise; writeFile(path: string, content: string): Promise; writeFileBytes(path: string, content: Uint8Array): Promise; appendFile(path: string, content: string | Uint8Array): Promise; diff --git a/vendor/agents/packages/shell/src/fs/read-range.ts b/vendor/agents/packages/shell/src/fs/read-range.ts new file mode 100644 index 0000000..e4b180e --- /dev/null +++ b/vendor/agents/packages/shell/src/fs/read-range.ts @@ -0,0 +1,14 @@ +/** Validate byte ranges before calling APIs whose slicing rules accept negatives. */ +export function validateReadRange(offset: number, length: number): void { + if ( + !Number.isSafeInteger(offset) || + offset < 0 || + !Number.isSafeInteger(length) || + length < 0 || + !Number.isSafeInteger(offset + length) + ) { + throw Object.assign(new RangeError("EINVAL: invalid byte range"), { + code: "EINVAL" + }); + } +} diff --git a/vendor/agents/packages/shell/src/tests/agents/workspace.ts b/vendor/agents/packages/shell/src/tests/agents/workspace.ts index 5d58728..7a884dc 100644 --- a/vendor/agents/packages/shell/src/tests/agents/workspace.ts +++ b/vendor/agents/packages/shell/src/tests/agents/workspace.ts @@ -1,3 +1,4 @@ +import { WorkspaceFileSystem } from "../../workspace"; import { subscribe as dcSubscribe, unsubscribe as dcUnsubscribe @@ -31,6 +32,20 @@ export class TestWorkspaceAgent extends Agent { } }); + async byteRangeContract() { + const fs = new WorkspaceFileSystem(this.workspace); + await this.workspace.writeFileBytes( + "/ranges", + new Uint8Array([0, 128, 255, 4]) + ); + await this.workspace.symlink("ranges", "/range-link"); + return { + bytes: Array.from(await fs.readFileRange("/range-link", 1, 2)), + pastEnd: Array.from(await fs.readFileRange("/ranges", 8, 2)), + zero: Array.from(await fs.readFileRange("/ranges", 0, 0)) + }; + } + async stat(path: string): Promise { try { return await this.workspace.stat(path); diff --git a/vendor/agents/packages/shell/src/tests/workspace.test.ts b/vendor/agents/packages/shell/src/tests/workspace.test.ts index 40345af..97bb6c3 100644 --- a/vendor/agents/packages/shell/src/tests/workspace.test.ts +++ b/vendor/agents/packages/shell/src/tests/workspace.test.ts @@ -2161,3 +2161,12 @@ describe("workspace — observability", () => { expect(log).toHaveLength(0); }); }); + +it("reads byte ranges through legacy WorkspaceFsLike adapters", async () => { + const agent = await freshAgent("range-fallback"); + expect(await agent.byteRangeContract()).toEqual({ + bytes: [128, 255], + pastEnd: [], + zero: [] + }); +}); diff --git a/vendor/agents/packages/shell/src/workspace.ts b/vendor/agents/packages/shell/src/workspace.ts index 83f22b1..d4f83c7 100644 --- a/vendor/agents/packages/shell/src/workspace.ts +++ b/vendor/agents/packages/shell/src/workspace.ts @@ -1,3 +1,4 @@ +import { validateReadRange } from "./fs/read-range"; import type { WorkspaceFsLike } from "./filesystem"; import type { FileSystem, FileSystemDirent, FsStat } from "./fs/interface"; import { FileSystemStateBackend } from "./memory"; @@ -42,6 +43,21 @@ export class WorkspaceFileSystem implements FileSystem { return bytes; } + async readFileRange( + path: string, + offset: number, + length: number + ): Promise { + validateReadRange(offset, length); + const bytes = this.ws.readFileRange + ? await this.ws.readFileRange(path, offset, length) + : (await this.ws.readFileBytes(path))?.slice(offset, offset + length); + if (bytes == null) throw enoent(path); + if (bytes.byteLength > length) + throw new Error("EIO: byte range exceeded requested length"); + return bytes; + } + async writeFile(path: string, content: string): Promise { await this.ws.writeFile(path, content); }