Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/export-workspace-file-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": patch
---

`WorkspaceFileStore` and `WORKSPACE_ROOT` are exported from `@cloudflare/computer`, so code outside the tools can read and write workspace files without its own helper.
2 changes: 2 additions & 0 deletions docs/09_tool_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ interface MutableFileStore extends FileStore {

`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.

Code outside the tools can import `WorkspaceFileStore` from `@cloudflare/computer` without loading a tool library: `readAll` returns `null` for a missing file, and `write` creates parent directories. `WORKSPACE_ROOT` is the `/workspace` directory the backends and tools default to.

## Conventions for agents

- Tools take absolute paths. Pass `root` to resolve relative paths against a directory and refuse paths outside it, or resolve user input yourself before calling them. See [01. VFS](./01_vfs.md).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
type WorkspaceEgressPolicy,
} from "../../runtime/egress.js";
import { WorkspaceTransportError } from "../../transport-failure.js";
import { WORKSPACE_ROOT } from "../../workspace-root.js";
import type { ILegacyWorkspaceContainerAPI, WorkspaceRef } from "./container-host.js";
import { probeComputerdHealth } from "./health-probe.js";

Expand Down Expand Up @@ -249,7 +250,7 @@ export class LegacyContainerBackend implements WorkspaceBackend {

const env = {
PORT: String(this.#options.containerPort),
MOUNT_POINT: "/workspace",
MOUNT_POINT: WORKSPACE_ROOT,
...this.#options.containerEnv,
};
let runtimeId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
type WorkspaceEgressPolicy,
} from "../../runtime/egress.js";
import { WorkspaceTransportError } from "../../transport-failure.js";
import { WORKSPACE_ROOT } from "../../workspace-root.js";
import type { IWorkspaceContainerAPI, WorkspaceRef } from "./container-host.js";
import type { ContainerInstanceSize, ContainerLaunchSpec } from "./container-launch-record.js";
import { probeComputerdHealth } from "./health-probe.js";
Expand Down Expand Up @@ -329,7 +330,7 @@ export class ContainerBackend implements WorkspaceBackend {

const env = {
PORT: String(this.#options.containerPort),
MOUNT_POINT: "/workspace",
MOUNT_POINT: WORKSPACE_ROOT,
...(this.#options.ignore !== undefined
? { MOUNT_IGNORE: this.#options.ignore.join(",") }
: {}),
Expand Down
4 changes: 3 additions & 1 deletion packages/computer/src/backends/container/ignore-assertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// Object -- the #179 symptom. A mismatch is a deployment error, and a
// loud one is cheaper than a slow one.

import { WORKSPACE_ROOT } from "../../workspace-root.js";

/** The `ignore` block computerd reports on /__computerd/info. */
export interface ComputerdIgnoreReport {
readonly supported?: boolean;
Expand Down Expand Up @@ -88,7 +90,7 @@ export function readIgnoreReport(info: unknown): ResolvedIgnore {
return {
patterns,
root: typeof typed.root === "string" ? typed.root : undefined,
mountPoint: typeof mountPoint === "string" && mountPoint !== "" ? mountPoint : "/workspace",
mountPoint: typeof mountPoint === "string" && mountPoint !== "" ? mountPoint : WORKSPACE_ROOT,
supported: true,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
WorkspaceRuntimeLoader,
WorkspaceRuntimeValue,
} from "../../runtime/types.js";
import { WORKSPACE_ROOT } from "../../workspace-root.js";
import { decodeRuntimeFrames, type RuntimeFrame } from "./frames.js";
import {
assertHostModuleExports,
Expand Down Expand Up @@ -277,7 +278,7 @@ export class WorkerJavaScriptBackend implements WorkspaceModuleBackend {
...backendOptions,
egress: resolvedEgress,
modules: parseModules(options.modules ?? {}),
root: options.root ?? "/workspace",
root: options.root ?? WORKSPACE_ROOT,
access: options.access ?? "read-write",
defaultTimeoutMs,
maxTimeoutMs,
Expand Down
3 changes: 2 additions & 1 deletion packages/computer/src/backends/worker-shell/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { WorkerEntrypoint } from "cloudflare:workers";
import { Bash, type CustomCommand, type SecureFetch } from "just-bash";

import { WORKSPACE_ROOT } from "../../workspace-root.js";
import { WorkspaceFsAdapter } from "./adapter.js";
import { type ArtifactsCommandHost, defineArtifactsCommand } from "./artifacts-command.js";
import { type AssetsCommandHost, defineAssetsCommand } from "./assets-command.js";
Expand Down Expand Up @@ -110,7 +111,7 @@ export interface HostWorkspaceStub extends GitCommandHost, AssetsCommandHost {
[Symbol.dispose]?: () => void;
}

const DEFAULT_CWD = "/workspace";
const DEFAULT_CWD = WORKSPACE_ROOT;
const MAX_OUTPUT_BYTES = 1024 * 1024;

// Wire event shape. Matches @cloudflare/computer-rpc's ExecEvent
Expand Down
31 changes: 31 additions & 0 deletions packages/computer/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SQLiteTestStorage } from "@cloudflare/dofs/testing";
import { describe, expect, it } from "vitest";

import { getWorkspace, Workspace, WORKSPACE_ROOT, WorkspaceFileStore } from "./index.js";

describe("the file helper from @cloudflare/computer", () => {
it("reads missing files as null and writes into new directories", async () => {
const workspace = new Workspace({ storage: new SQLiteTestStorage() });
const files = new WorkspaceFileStore(workspace);
const path = `${WORKSPACE_ROOT}/notes/today.md`;

await expect(files.readAll(path)).resolves.toBeNull();
await files.write(path, new TextEncoder().encode("ship it"));
const written = await files.readAll(path);
expect(written && new TextDecoder().decode(written)).toBe("ship it");
await workspace.close();
});

it("works on a client that has not loaded yet", async () => {
const workspace = new Workspace({ storage: new SQLiteTestStorage() });
const client = getWorkspace({ __getWorkspaceStub: () => Promise.resolve(workspace.stub()) });
const files = new WorkspaceFileStore(client);
const path = `${WORKSPACE_ROOT}/a/b.txt`;

await expect(files.readAll(path)).resolves.toBeNull();
await files.write(path, new TextEncoder().encode("hello"));
await expect(files.stat(path)).resolves.toMatchObject({ size: 5 });
client[Symbol.dispose]();
await workspace.close();
});
});
3 changes: 3 additions & 0 deletions packages/computer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,7 @@ export {
type WorkspaceStubHost,
withWorkspace,
} from "./with-workspace.js";
export { WorkspaceFileStore } from "./tools/common/fs/store.js";
export type { FileStat, FileStore, MutableFileStore } from "./tools/common/fs/types.js";
Comment on lines +134 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New public exports lack example coverage

After adding WorkspaceFileStore and WORKSPACE_ROOT to the public API, no runnable example uses either export. The repository requires example updates for public API changes.

Learn more

Add a runnable example that imports WorkspaceFileStore and WORKSPACE_ROOT from the package entrypoint and uses the store with a workspace. Keep the existing source-level test; an example exercises the public integration separately.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

export { Workspace, type WorkspaceGitFactory, type WorkspaceOptions } from "./workspace.js";
export { WORKSPACE_ROOT } from "./workspace-root.js";
5 changes: 3 additions & 2 deletions packages/computer/src/tools/common/fs/find.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { WORKSPACE_ROOT } from "../../../workspace-root.js";
import { globList } from "./globs.js";

interface FoundEntry {
Expand All @@ -24,7 +25,7 @@ const DEFAULT_LIMIT = 200;
const MAX_LIMIT = 1000;

export const findInputSchema = z.object({
path: z.string().default("/workspace").describe("Absolute directory to search."),
path: z.string().default(WORKSPACE_ROOT).describe("Absolute directory to search."),
pattern: z
.string()
.describe('Glob pattern relative to path, for example "**/*.ts" or "src/?.js".'),
Expand Down Expand Up @@ -70,7 +71,7 @@ export async function findInWorkspace(
workspace: FindWorkspaceLike,
{ path, pattern, exclude, limit, offset }: FindInput,
): Promise<FindResult> {
const directory = path ?? "/workspace";
const directory = path ?? WORKSPACE_ROOT;
try {
const pageSize = limit ?? DEFAULT_LIMIT;
const pageOffset = offset ?? 0;
Expand Down
5 changes: 3 additions & 2 deletions packages/computer/src/tools/common/fs/grep.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { WORKSPACE_ROOT } from "../../../workspace-root.js";
import { globList, globOne } from "./globs.js";

interface GrepContextLine {
Expand Down Expand Up @@ -38,7 +39,7 @@ const DEFAULT_LIMIT = 200;
const MAX_LIMIT = 1000;

export const grepInputSchema = z.object({
path: z.string().default("/workspace").describe("Absolute file or directory to search."),
path: z.string().default(WORKSPACE_ROOT).describe("Absolute file or directory to search."),
query: z.string().describe("Literal string or regular expression to search for."),
include: z
.union([z.string(), z.array(z.string())])
Expand Down Expand Up @@ -95,7 +96,7 @@ export async function grepInWorkspace(
workspace: GrepWorkspaceLike,
{ path, query, include, exclude, regex, ignoreCase, context, limit, offset }: GrepInput,
): Promise<GrepResult> {
const target = path ?? "/workspace";
const target = path ?? WORKSPACE_ROOT;
try {
const pageSize = limit ?? DEFAULT_LIMIT;
const pageOffset = offset ?? 0;
Expand Down
2 changes: 2 additions & 0 deletions packages/computer/src/workspace-root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** The directory Computer's backends and tools treat as the working root. */
export const WORKSPACE_ROOT = "/workspace";
Loading