@warlock.js/ai-workspace 4.6.1 → 4.7.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.
- package/cjs/index.cjs +15 -15
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +12 -12
- package/esm/backends/local.mjs.map +1 -1
- package/esm/ops.mjs +5 -5
- package/esm/ops.mjs.map +1 -1
- package/package.json +3 -3
package/cjs/index.cjs
CHANGED
|
@@ -365,7 +365,7 @@ var Ops = class {
|
|
|
365
365
|
async readFile(inputPath, opts) {
|
|
366
366
|
const { absolutePath } = await resolveInJail(this.policy, inputPath);
|
|
367
367
|
const raw = await this.backend.readFile(absolutePath);
|
|
368
|
-
const hash =
|
|
368
|
+
const hash = _warlock_js_fs.fs.hash.string(raw);
|
|
369
369
|
const lines = raw.split("\n");
|
|
370
370
|
const totalLines = lines.length;
|
|
371
371
|
const offset = Math.max(1, opts?.offset ?? 1);
|
|
@@ -383,14 +383,14 @@ var Ops = class {
|
|
|
383
383
|
await this.backend.mkdir(parent);
|
|
384
384
|
await this.backend.writeFile(absolutePath, content);
|
|
385
385
|
return {
|
|
386
|
-
hash:
|
|
386
|
+
hash: _warlock_js_fs.fs.hash.string(content),
|
|
387
387
|
bytesWritten: Buffer.byteLength(content, "utf8")
|
|
388
388
|
};
|
|
389
389
|
}
|
|
390
390
|
async editFile(input) {
|
|
391
391
|
const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
|
|
392
392
|
const current = await this.backend.readFile(absolutePath);
|
|
393
|
-
const currentHash =
|
|
393
|
+
const currentHash = _warlock_js_fs.fs.hash.string(current);
|
|
394
394
|
if (input.expectHash !== void 0 && input.expectHash !== currentHash) throw new WorkspaceEditError(`File "${input.path}" changed since it was read; the edit is stale.`, {
|
|
395
395
|
type: "stale-hash",
|
|
396
396
|
path: relativePath || input.path,
|
|
@@ -413,7 +413,7 @@ var Ops = class {
|
|
|
413
413
|
return {
|
|
414
414
|
path: relativePath || input.path,
|
|
415
415
|
replacements: input.replaceAll ? occurrences : 1,
|
|
416
|
-
hash:
|
|
416
|
+
hash: _warlock_js_fs.fs.hash.string(updated)
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
419
|
async exec(command, opts) {
|
|
@@ -622,7 +622,7 @@ function killTree(pid, child) {
|
|
|
622
622
|
var LocalBackend = class {
|
|
623
623
|
/** Read a file's full UTF-8 content at an absolute path. */
|
|
624
624
|
async readFile(absPath) {
|
|
625
|
-
return
|
|
625
|
+
return _warlock_js_fs.fs.files.get(absPath);
|
|
626
626
|
}
|
|
627
627
|
/**
|
|
628
628
|
* Write full content to an absolute path. Uses `atomicWriteAsync`, so a
|
|
@@ -630,39 +630,39 @@ var LocalBackend = class {
|
|
|
630
630
|
* directories are created.
|
|
631
631
|
*/
|
|
632
632
|
async writeFile(absPath, content) {
|
|
633
|
-
await
|
|
633
|
+
await _warlock_js_fs.fs.files.put(absPath, content, { atomic: true });
|
|
634
634
|
}
|
|
635
635
|
/** Whether anything (file or directory) exists at an absolute path. */
|
|
636
636
|
async exists(absPath) {
|
|
637
|
-
return
|
|
637
|
+
return _warlock_js_fs.fs.exists(absPath);
|
|
638
638
|
}
|
|
639
639
|
/** Create a directory (and any missing parents) at an absolute path; idempotent. */
|
|
640
640
|
async mkdir(absPath) {
|
|
641
|
-
await
|
|
641
|
+
await _warlock_js_fs.fs.dirs.ensure(absPath);
|
|
642
642
|
}
|
|
643
643
|
/**
|
|
644
644
|
* Remove a file or directory tree at an absolute path. Stats the target to
|
|
645
|
-
* pick the right
|
|
646
|
-
*
|
|
647
|
-
*
|
|
645
|
+
* pick the right op — `fs.dirs.remove` (recursive) for a directory,
|
|
646
|
+
* `fs.files.remove` for anything else. A path that does not exist is a no-op
|
|
647
|
+
* (the stat's ENOENT short-circuits, and both removes swallow `ENOENT`).
|
|
648
648
|
*/
|
|
649
649
|
async remove(absPath) {
|
|
650
650
|
let isDirectory = false;
|
|
651
651
|
try {
|
|
652
|
-
isDirectory = (await
|
|
652
|
+
isDirectory = (await _warlock_js_fs.fs.files.stats(absPath)).type === "directory";
|
|
653
653
|
} catch (error) {
|
|
654
654
|
if (error?.code === "ENOENT") return;
|
|
655
655
|
throw error;
|
|
656
656
|
}
|
|
657
657
|
if (isDirectory) {
|
|
658
|
-
await
|
|
658
|
+
await _warlock_js_fs.fs.dirs.remove(absPath);
|
|
659
659
|
return;
|
|
660
660
|
}
|
|
661
|
-
await
|
|
661
|
+
await _warlock_js_fs.fs.files.remove(absPath);
|
|
662
662
|
}
|
|
663
663
|
/** List immediate children of an absolute directory as absolute paths. */
|
|
664
664
|
async list(absDir) {
|
|
665
|
-
return
|
|
665
|
+
return _warlock_js_fs.fs.dirs.list(absDir);
|
|
666
666
|
}
|
|
667
667
|
/**
|
|
668
668
|
* Resolve symlinks and `..` segments to a canonical absolute path — the
|
package/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["AIError","canonicalize","path","globToRegExp","path","platform","DEFAULT_NAME","inputSchema","DEFAULT_NAME","inputSchema","path"],"sources":["../../../../../../@warlock.js/ai-workspace/src/errors.ts","../../../../../../@warlock.js/ai-workspace/src/policy/policy.ts","../../../../../../@warlock.js/ai-workspace/src/ops.ts","../../../../../../@warlock.js/ai-workspace/src/backends/local.ts","../../../../../../@warlock.js/ai-workspace/src/backends/mock.ts","../../../../../../@warlock.js/ai-workspace/src/tools/schema.ts","../../../../../../@warlock.js/ai-workspace/src/tools/edit-file.ts","../../../../../../@warlock.js/ai-workspace/src/tools/glob.ts","../../../../../../@warlock.js/ai-workspace/src/tools/grep.ts","../../../../../../@warlock.js/ai-workspace/src/tools/read-file.ts","../../../../../../@warlock.js/ai-workspace/src/tools/run-shell.ts","../../../../../../@warlock.js/ai-workspace/src/tools/run-tests.ts","../../../../../../@warlock.js/ai-workspace/src/tools/write-file.ts","../../../../../../@warlock.js/ai-workspace/src/workspace.ts"],"sourcesContent":["import { AIError, type AIErrorOptions } from \"@warlock.js/ai\";\n\n/**\n * Why a workspace policy check rejected an operation.\n *\n * - `\"path-escape\"` — a resolved path fell outside the `cwd` jail (or an\n * `allowPaths` root), or matched a `denyPaths` glob.\n * - `\"denied-command\"` — a shell command's leading executable basename\n * was not in `shell.allow`, or was explicitly in `shell.deny`.\n */\nexport type WorkspacePolicyViolation = \"path-escape\" | \"denied-command\";\n\n/**\n * Options for {@link WorkspacePolicyError} — the structured `type`\n * discriminator plus, where relevant, the offending path or command for\n * branchable diagnostics without parsing the message.\n */\nexport type WorkspacePolicyErrorOptions = AIErrorOptions & {\n /** Which policy rule was violated. */\n type: WorkspacePolicyViolation;\n /** The offending workspace-relative path (for `\"path-escape\"`). */\n path?: string;\n /** The offending command line (for `\"denied-command\"`). */\n command?: string;\n};\n\n/**\n * The workspace policy engine refused an operation — a path escaped the\n * jail (or hit a deny glob), or a shell command's executable was not\n * allowed.\n *\n * **Surface.** This is returned to the agent as tool-error *data*, never\n * a thrown run-killer — the agent reads the failure and self-corrects.\n * Extends the framework `AIError` (category `\"tool\"`, code\n * `TOOL_EXEC_FAILED`) so it flows through the same typed error contract\n * as every other AI error; branch on `error.type` for the specific\n * violation.\n *\n * @example\n * if (error instanceof WorkspacePolicyError && error.type === \"denied-command\") {\n * console.warn(`Blocked command: ${error.command}`);\n * }\n */\nexport class WorkspacePolicyError extends AIError {\n /** Which policy rule was violated. */\n public readonly type: WorkspacePolicyViolation;\n /** The offending path, when the violation was a path escape. */\n public readonly path?: string;\n /** The offending command, when the violation was a denied command. */\n public readonly command?: string;\n\n public constructor(message: string, options: WorkspacePolicyErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspacePolicyError\";\n this.type = options.type;\n this.path = options.path;\n this.command = options.command;\n }\n}\n\n/**\n * Why an edit was rejected.\n *\n * - `\"not-found\"` — the `oldString` did not appear in the file.\n * - `\"not-unique\"` — `oldString` matched more than once and `replaceAll`\n * was not set, so the edit is ambiguous.\n * - `\"stale-hash\"` — the file's current hash did not match the supplied\n * `expectHash`; the file changed since it was read.\n */\nexport type WorkspaceEditFailure = \"not-found\" | \"not-unique\" | \"stale-hash\";\n\n/**\n * Options for {@link WorkspaceEditError} — the structured `type`\n * discriminator plus optional match-count / hash context for the\n * `\"not-unique\"` and `\"stale-hash\"` cases.\n */\nexport type WorkspaceEditErrorOptions = AIErrorOptions & {\n /** Why the edit was rejected. */\n type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n path: string;\n /** How many times `oldString` matched (for `\"not-unique\"`). */\n matches?: number;\n /** The hash the caller expected (for `\"stale-hash\"`). */\n expectedHash?: string;\n /** The file's actual current hash (for `\"stale-hash\"`). */\n actualHash?: string;\n};\n\n/**\n * An `editFile` operation was rejected by the read-before-edit guard:\n * the `oldString` was absent, matched non-uniquely without `replaceAll`,\n * or the file's hash no longer matched the supplied `expectHash`.\n *\n * **Surface.** Like {@link WorkspacePolicyError}, returned to the agent\n * as tool-error *data* so it can re-read and retry. Extends `AIError`\n * (category `\"tool\"`, code `TOOL_EXEC_FAILED`); branch on `error.type`.\n *\n * @example\n * if (error instanceof WorkspaceEditError && error.type === \"stale-hash\") {\n * // re-read the file and retry the edit with the fresh hash\n * }\n */\nexport class WorkspaceEditError extends AIError {\n /** Why the edit was rejected. */\n public readonly type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n public readonly path: string;\n /** How many times `oldString` matched, for the `\"not-unique\"` case. */\n public readonly matches?: number;\n /** The hash the caller expected, for the `\"stale-hash\"` case. */\n public readonly expectedHash?: string;\n /** The file's actual current hash, for the `\"stale-hash\"` case. */\n public readonly actualHash?: string;\n\n public constructor(message: string, options: WorkspaceEditErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspaceEditError\";\n this.type = options.type;\n this.path = options.path;\n this.matches = options.matches;\n this.expectedHash = options.expectedHash;\n this.actualHash = options.actualHash;\n }\n}\n","import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type { WorkspacePolicy } from \"../contracts\";\n\n/**\n * The outcome of resolving a workspace-relative (or absolute) input path\n * against the jail — the canonical absolute location the backend should\n * touch, plus the workspace-relative form (POSIX-style, `/`-separated)\n * the agent and tool results echo back.\n */\nexport interface ResolvedPath {\n /** Canonical absolute path (symlinks in existing ancestors collapsed). */\n absolutePath: string;\n /**\n * The path relative to `policy.cwd`, `/`-separated regardless of OS,\n * used in tool results so the agent always sees stable workspace paths.\n * Empty string when the resolved path IS the jail root.\n */\n relativePath: string;\n}\n\n/**\n * Resolve the canonical absolute form of `target`, collapsing any\n * symlinks. `target` may not yet exist (a fresh `writeFile`/`mkdir`),\n * so we realpath the deepest **existing** ancestor and re-attach the\n * non-existent tail — a symlinked ancestor still cannot smuggle the\n * path out of the jail, while genuinely new leaves stay creatable.\n */\nasync function canonicalize(target: string): Promise<string> {\n let resolvedTarget = path.resolve(target);\n const tail: string[] = [];\n\n // Walk up until an existing ancestor is found (or we hit the root).\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n const real = await realpath(resolvedTarget);\n\n return tail.length > 0 ? path.join(real, ...tail) : real;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n\n // Only ENOENT means \"this segment doesn't exist yet\" — keep\n // walking up. Any other error (EACCES, ELOOP, …) is a real fault.\n if (code !== \"ENOENT\") {\n throw error;\n }\n\n const parent = path.dirname(resolvedTarget);\n\n // Reached the filesystem root without finding an existing\n // ancestor — give back the lexically-resolved path unchanged.\n if (parent === resolvedTarget) {\n return path.join(resolvedTarget, ...tail);\n }\n\n tail.unshift(path.basename(resolvedTarget));\n resolvedTarget = parent;\n }\n }\n}\n\n/**\n * Whether `child` is contained within `root` (or equals it), comparing\n * canonical absolute paths. Guards against the `/srv/app-evil` vs\n * `/srv/app` prefix-collision by anchoring on a path separator.\n */\nfunction isInside(child: string, root: string): boolean {\n const relative = path.relative(root, child);\n\n return (\n relative === \"\" ||\n (!relative.startsWith(\"..\") && !path.isAbsolute(relative))\n );\n}\n\n/**\n * Translate a glob (the small subset used by `denyPaths` — `*`, `**`,\n * `?`) into an anchored `RegExp` over a `/`-separated relative path.\n * `**` spans path separators; a single `*` does not.\n */\nfunction globToRegExp(glob: string): RegExp {\n let source = \"\";\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index];\n\n if (char === \"*\") {\n if (glob[index + 1] === \"*\") {\n // `**` — match across segments (and an optional trailing slash).\n source += \".*\";\n index++;\n\n if (glob[index + 1] === \"/\") {\n index++;\n }\n } else {\n // `*` — match within a single segment.\n source += \"[^/]*\";\n }\n\n continue;\n }\n\n if (char === \"?\") {\n source += \"[^/]\";\n\n continue;\n }\n\n // Escape everything else so it matches literally.\n source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * Whether a workspace-relative (`/`-separated) path matches any of the\n * policy's `denyPaths` globs. A deny glob also blocks everything beneath\n * a matched directory (`\".git/**\"` blocks `.git/config`).\n */\nfunction matchesDeny(relativePath: string, denyPaths: string[]): boolean {\n return denyPaths.some((glob) => {\n if (globToRegExp(glob).test(relativePath)) {\n return true;\n }\n\n // A bare directory glob (`\".git\"`, `\"node_modules\"`) should also\n // block its contents, mirroring how `\".git/**\"` would behave.\n if (!glob.includes(\"*\") && !glob.includes(\"?\")) {\n const prefix = glob.endsWith(\"/\") ? glob : `${glob}/`;\n\n return relativePath.startsWith(prefix);\n }\n\n return false;\n });\n}\n\n/**\n * Resolve and jail a single input path against a {@link WorkspacePolicy}.\n *\n * The input is resolved against `policy.cwd`, canonicalized (symlinks in\n * existing ancestors collapsed so a symlinked directory cannot escape\n * the jail), then accepted **only** when it sits under `cwd` or one of\n * the `allowPaths` roots. A path that escapes, or that matches any\n * `denyPaths` glob even while inside `cwd`, is rejected with a\n * {@link WorkspacePolicyError} of type `\"path-escape\"`.\n *\n * @param policy - The bounding policy (its `cwd` is the jail root).\n * @param inputPath - A workspace-relative or absolute path to resolve.\n * @returns The canonical absolute path plus its `/`-separated relative form.\n * @throws {WorkspacePolicyError} When the path escapes the jail or hits a deny glob.\n *\n * @example\n * const { absolutePath } = await resolveInJail(policy, \"src/index.ts\");\n */\nexport async function resolveInJail(\n policy: WorkspacePolicy,\n inputPath: string,\n): Promise<ResolvedPath> {\n const jailRoot = await canonicalize(policy.cwd);\n const requested = path.isAbsolute(inputPath)\n ? inputPath\n : path.join(policy.cwd, inputPath);\n const absolutePath = await canonicalize(requested);\n\n const insideCwd = isInside(absolutePath, jailRoot);\n const allowRoots = policy.allowPaths ?? [];\n let insideAllow = false;\n\n if (!insideCwd) {\n for (const root of allowRoots) {\n const canonicalRoot = await canonicalize(root);\n\n if (isInside(absolutePath, canonicalRoot)) {\n insideAllow = true;\n\n break;\n }\n }\n }\n\n if (!insideCwd && !insideAllow) {\n throw new WorkspacePolicyError(\n `Path \"${inputPath}\" resolves outside the workspace jail.`,\n { type: \"path-escape\", path: inputPath },\n );\n }\n\n // `denyPaths` is evaluated relative to the jail root and wins even\n // when the path is comfortably inside `cwd`.\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, absolutePath).split(path.sep).join(\"/\")\n : \"\";\n\n if (insideCwd && policy.denyPaths && policy.denyPaths.length > 0) {\n if (matchesDeny(relativeToJail, policy.denyPaths)) {\n throw new WorkspacePolicyError(\n `Path \"${inputPath}\" is blocked by the workspace deny list.`,\n { type: \"path-escape\", path: inputPath },\n );\n }\n }\n\n return { absolutePath, relativePath: relativeToJail };\n}\n\n/**\n * Extract the leading executable basename from a command line — the\n * token the shell allow/deny policy is keyed on. `\"npm run build\"` →\n * `\"npm\"`; `\"/usr/bin/node app.js\"` → `\"node\"`; `\"node.exe app\"` →\n * `\"node\"` (the `.exe`/`.cmd`/`.bat` Windows extension is stripped).\n */\nfunction leadingExecutable(command: string): string {\n const trimmed = command.trim();\n const firstToken = trimmed.split(/\\s+/)[0] ?? \"\";\n const base = path.basename(firstToken);\n\n return base.replace(/\\.(exe|cmd|bat|com)$/i, \"\");\n}\n\n/**\n * Whether a shell command is permitted by the policy's `shell` sub-policy.\n *\n * The command's leading executable basename is matched against\n * `shell.deny` then `shell.allow`. **Deny always wins.** When\n * `shell.allow` is set, the executable MUST appear in it (fail-closed\n * allowlist); when `allow` is absent/empty, any non-denied command is\n * permitted. An absent `shell` block means no command may run at all.\n *\n * Returns a plain `boolean` rather than throwing — the ops layer raises\n * the {@link WorkspacePolicyError} so the thrown context (`command`)\n * lives next to the call site.\n *\n * @example\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test\"); // true\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"rm -rf /\"); // false\n */\nexport function isCommandAllowed(\n policy: WorkspacePolicy,\n command: string,\n): boolean {\n const shell = policy.shell;\n\n // No shell sub-policy ⇒ fail-closed: nothing may run.\n if (!shell) {\n return false;\n }\n\n const executable = leadingExecutable(command);\n\n if (executable === \"\") {\n return false;\n }\n\n // Deny wins over everything else.\n if (shell.deny && shell.deny.includes(executable)) {\n return false;\n }\n\n // An allowlist, when present, is exhaustive.\n if (shell.allow && shell.allow.length > 0) {\n return shell.allow.includes(executable);\n }\n\n // No allowlist: anything not explicitly denied is permitted.\n return true;\n}\n\n/**\n * Build the exact environment a spawned process receives — `process.env`\n * is **never** inherited wholesale. The result is\n * `{ ...pick(process.env, inheritEnv), ...shell.env }`, so a command\n * cannot see `PATH` (and thus often cannot find `node`/`npm`) unless the\n * policy opts in via `shell.inheritEnv: [\"PATH\"]`. Explicit `shell.env`\n * values override inherited ones on key collision.\n *\n * @example\n * buildEnv({ cwd, shell: { inheritEnv: [\"PATH\"], env: { CI: \"1\" } } });\n * // → { PATH: <process PATH>, CI: \"1\" }\n */\nexport function buildEnv(policy: WorkspacePolicy): Record<string, string> {\n const shell = policy.shell;\n const env: Record<string, string> = {};\n\n if (!shell) {\n return env;\n }\n\n for (const key of shell.inheritEnv ?? []) {\n const value = process.env[key];\n\n if (value !== undefined) {\n env[key] = value;\n }\n }\n\n if (shell.env) {\n for (const [key, value] of Object.entries(shell.env)) {\n env[key] = value;\n }\n }\n\n return env;\n}\n","import path from \"node:path\";\nimport { hashString } from \"@warlock.js/fs\";\nimport { WorkspaceEditError, WorkspacePolicyError } from \"./errors\";\nimport { buildEnv, isCommandAllowed, resolveInJail } from \"./policy/policy\";\nimport type {\n EditFileInput,\n EditFileResult,\n GrepMatch,\n GrepResult,\n RunShellResult,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n} from \"./contracts\";\n\n/** Default line window a read returns when the policy sets no `defaultLines`. */\nconst DEFAULT_READ_LINES = 2000;\n/** Hard ceiling on grep matches returned, so a broad pattern can't flood. */\nconst DEFAULT_MAX_GREP_MATCHES = 1000;\n/** Default per-command output byte cap when the policy sets none. */\nconst DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;\n\n/**\n * Number the lines of `content` `cat -n` style: a right-aligned line\n * number (min width 6), a tab, then the line. `startLine` is the 1-based\n * number of the first line in the window.\n */\nfunction numberLines(content: string, startLine: number): string {\n const lines = content.split(\"\\n\");\n\n return lines\n .map((line, index) => {\n const lineNumber = startLine + index;\n\n return `${String(lineNumber).padStart(6, \" \")}\\t${line}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * Clip a captured stream at `maxBytes` (measured in UTF-8 bytes).\n * Returns the possibly-clipped string plus whether clipping occurred.\n */\nfunction capOutput(value: string, maxBytes: number): { value: string; truncated: boolean } {\n const bytes = Buffer.from(value, \"utf8\");\n\n if (bytes.byteLength <= maxBytes) {\n return { value, truncated: false };\n }\n\n // Slice on a byte boundary; `toString` tolerates a split multi-byte\n // char at the tail by emitting the replacement character, which is\n // acceptable for a truncated diagnostic stream.\n return { value: bytes.subarray(0, maxBytes).toString(\"utf8\"), truncated: true };\n}\n\n/**\n * Minimal glob match over a `/`-separated relative path. Supports `**`\n * (spans separators), `*` (within a segment), and `?` (one non-separator\n * char) — enough for the workspace's `glob`/grep narrowing without\n * pulling in a runtime dependency.\n */\nfunction globToRegExp(glob: string): RegExp {\n let source = \"\";\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index];\n\n if (char === \"*\") {\n if (glob[index + 1] === \"*\") {\n source += \".*\";\n index++;\n\n if (glob[index + 1] === \"/\") {\n index++;\n }\n } else {\n source += \"[^/]*\";\n }\n\n continue;\n }\n\n if (char === \"?\") {\n source += \"[^/]\";\n\n continue;\n }\n\n source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * The internal, single-instance implementation of {@link WorkspaceOps}.\n * Holds the backend + policy and is the one place the jail, command\n * gating, read caps, and the read-before-edit guard are enforced — both\n * the agent-facing tools and the human-facing direct methods funnel\n * through this object, so there is exactly one set of rules.\n */\nclass Ops implements WorkspaceOps {\n public constructor(\n private readonly backend: WorkspaceBackend,\n private readonly policy: WorkspacePolicy,\n ) {}\n\n /**\n * Recursively collect every file under `absDir` as absolute paths,\n * via the backend's `list` (so it works over disk or the in-memory\n * mock). Directories are descended; files are accumulated.\n */\n private async walkFiles(absDir: string): Promise<string[]> {\n const found: string[] = [];\n const entries = await this.backend.list(absDir);\n\n await Promise.all(\n entries.map(async (entry) => {\n // A child is a directory iff listing it succeeds; the backend\n // throws/returns for a file. Probe via `exists` + a list guard.\n const isDir = await this.isDirectory(entry);\n\n if (isDir) {\n const nested = await this.walkFiles(entry);\n found.push(...nested);\n } else {\n found.push(entry);\n }\n }),\n );\n\n return found;\n }\n\n /** Whether an absolute path is a directory, by attempting to list it. */\n private async isDirectory(absPath: string): Promise<boolean> {\n try {\n await this.backend.list(absPath);\n\n return true;\n } catch {\n return false;\n }\n }\n\n public async readFile(\n inputPath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<{ content: string; hash: string; totalLines: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n const raw = await this.backend.readFile(absolutePath);\n\n // Hash the content the backend returned (SHA-256 via @warlock.js/fs),\n // not the on-disk file — the mock backend keeps content in memory,\n // and hashing the returned bytes is what the stale-edit guard later\n // compares against, so it must be the SAME source of truth.\n const hash = hashString(raw);\n\n const lines = raw.split(\"\\n\");\n const totalLines = lines.length;\n\n const offset = Math.max(1, opts?.offset ?? 1);\n const limit = opts?.limit ?? this.policy.read?.defaultLines ?? DEFAULT_READ_LINES;\n\n const startIndex = offset - 1;\n const window = lines.slice(startIndex, startIndex + limit);\n const content = numberLines(window.join(\"\\n\"), offset);\n\n return { content, hash, totalLines };\n }\n\n public async writeFile(\n inputPath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n // Ensure the parent directory exists before writing the leaf.\n const parent = path.dirname(absolutePath);\n await this.backend.mkdir(parent);\n\n await this.backend.writeFile(absolutePath, content);\n\n return {\n hash: hashString(content),\n bytesWritten: Buffer.byteLength(content, \"utf8\"),\n };\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n const current = await this.backend.readFile(absolutePath);\n const currentHash = hashString(current);\n\n // Stale-hash guard: if the caller pinned a hash and the file moved\n // underneath them, refuse so they re-read before clobbering.\n if (input.expectHash !== undefined && input.expectHash !== currentHash) {\n throw new WorkspaceEditError(\n `File \"${input.path}\" changed since it was read; the edit is stale.`,\n {\n type: \"stale-hash\",\n path: relativePath || input.path,\n expectedHash: input.expectHash,\n actualHash: currentHash,\n },\n );\n }\n\n const occurrences = countOccurrences(current, input.oldString);\n\n if (occurrences === 0) {\n throw new WorkspaceEditError(\n `The text to replace was not found in \"${input.path}\".`,\n { type: \"not-found\", path: relativePath || input.path, matches: 0 },\n );\n }\n\n if (occurrences > 1 && !input.replaceAll) {\n throw new WorkspaceEditError(\n `The text to replace is not unique in \"${input.path}\" (${occurrences} matches); ` +\n `pass replaceAll or include more surrounding context.`,\n { type: \"not-unique\", path: relativePath || input.path, matches: occurrences },\n );\n }\n\n const updated = input.replaceAll\n ? current.split(input.oldString).join(input.newString)\n : replaceFirst(current, input.oldString, input.newString);\n\n await this.backend.writeFile(absolutePath, updated);\n\n return {\n path: relativePath || input.path,\n replacements: input.replaceAll ? occurrences : 1,\n hash: hashString(updated),\n };\n }\n\n public async exec(\n command: string,\n opts?: { timeoutMs?: number },\n ): Promise<RunShellResult> {\n if (!isCommandAllowed(this.policy, command)) {\n throw new WorkspacePolicyError(\n `Command is not permitted by the workspace shell policy: ${command}`,\n { type: \"denied-command\", command },\n );\n }\n\n const shell = this.policy.shell;\n const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;\n const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;\n\n const result = await this.backend.exec(command, {\n cwd: this.policy.cwd,\n timeoutMs,\n env: buildEnv(this.policy),\n });\n\n const stdout = capOutput(result.stdout, maxOutputBytes);\n const stderr = capOutput(result.stderr, maxOutputBytes);\n\n return {\n exitCode: result.exitCode,\n stdout: stdout.value,\n stderr: stderr.value,\n truncated: stdout.truncated || stderr.truncated,\n timedOut: result.timedOut,\n };\n }\n\n public async grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const flags = opts?.ignoreCase ? \"i\" : \"\";\n const regex = new RegExp(pattern, flags);\n const globRegex = opts?.glob ? globToRegExp(opts.glob) : undefined;\n\n const files = await this.walkFiles(jailRoot);\n const matches: GrepMatch[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (globRegex && !globRegex.test(relativePath)) {\n continue;\n }\n\n // Skip files the deny list would block (e.g. `.git/**`).\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n let content: string;\n\n try {\n content = await this.backend.readFile(absFile);\n } catch {\n // Unreadable entry (vanished, binary handle) — skip, don't fail.\n continue;\n }\n\n const lines = content.split(\"\\n\");\n\n for (let index = 0; index < lines.length; index++) {\n if (regex.test(lines[index])) {\n matches.push({ path: relativePath, line: index + 1, text: lines[index] });\n\n if (matches.length >= DEFAULT_MAX_GREP_MATCHES) {\n return { matches, total: matches.length };\n }\n }\n }\n }\n\n return { matches, total: matches.length };\n }\n\n public async glob(pattern: string): Promise<string[]> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const regex = globToRegExp(pattern);\n\n const files = await this.walkFiles(jailRoot);\n const matched: string[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n if (regex.test(relativePath)) {\n matched.push(relativePath);\n }\n }\n\n return matched.sort();\n }\n\n public async exists(inputPath: string): Promise<boolean> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n return this.backend.exists(absolutePath);\n }\n\n public async mkdir(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.mkdir(absolutePath);\n }\n\n public async remove(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.remove(absolutePath);\n }\n\n /** Whether a `/`-separated relative path hits the policy deny list. */\n private isDenied(relativePath: string): boolean {\n const denyPaths = this.policy.denyPaths;\n\n if (!denyPaths || denyPaths.length === 0) {\n return false;\n }\n\n return denyPaths.some((glob) => {\n if (globToRegExp(glob).test(relativePath)) {\n return true;\n }\n\n if (!glob.includes(\"*\") && !glob.includes(\"?\")) {\n const prefix = glob.endsWith(\"/\") ? glob : `${glob}/`;\n\n return relativePath.startsWith(prefix);\n }\n\n return false;\n });\n }\n}\n\n/** Count non-overlapping occurrences of `needle` in `haystack`. */\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle === \"\") {\n return 0;\n }\n\n let count = 0;\n let from = 0;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const index = haystack.indexOf(needle, from);\n\n if (index === -1) {\n break;\n }\n\n count++;\n from = index + needle.length;\n }\n\n return count;\n}\n\n/** Replace the first occurrence of `needle` with `replacement`. */\nfunction replaceFirst(haystack: string, needle: string, replacement: string): string {\n const index = haystack.indexOf(needle);\n\n if (index === -1) {\n return haystack;\n }\n\n return haystack.slice(0, index) + replacement + haystack.slice(index + needle.length);\n}\n\n/**\n * Create the policy-enforced operation layer over a backend.\n *\n * The returned {@link WorkspaceOps} is the single seam both the\n * agent-facing `.tools.*` factories and the human-facing direct methods\n * delegate to — one jail, one command-gate, one read-before-edit guard,\n * regardless of caller. Path inputs are workspace-relative and resolved\n * against `policy.cwd`; escapes and denied commands surface as typed\n * {@link WorkspacePolicyError} / {@link WorkspaceEditError}.\n *\n * @param backend - The dumb IO executor (local disk or in-memory mock).\n * @param policy - The policy that bounds every operation.\n *\n * @example\n * const ops = createOps(localBackend, { cwd: \"/srv/api\", shell: { allow: [\"npm\"] } });\n * const { content, hash } = await ops.readFile(\"src/index.ts\");\n */\nexport function createOps(\n backend: WorkspaceBackend,\n policy: WorkspacePolicy,\n): WorkspaceOps {\n return new Ops(backend, policy);\n}\n","import { spawn } from \"node:child_process\";\nimport { realpath } from \"node:fs/promises\";\nimport { platform } from \"node:process\";\nimport {\n atomicWriteAsync,\n ensureDirectoryAsync,\n getFileAsync,\n listAsync,\n pathExistsAsync,\n removeDirectoryAsync,\n statsAsync,\n unlinkAsync,\n} from \"@warlock.js/fs\";\nimport type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n} from \"../contracts/workspace-backend.contract\";\n\n/**\n * Hard ceiling on captured `stdout`/`stderr` per stream, in bytes. The raw\n * executor truncates each stream once this many bytes have accumulated so a\n * runaway command cannot exhaust memory; the ops layer applies its own\n * policy-driven cap (and the `truncated` flag) on top. Generous enough that\n * ordinary command output is never clipped here.\n */\nconst MAX_STREAM_BYTES = 10 * 1024 * 1024;\n\n/**\n * Append a chunk to a capped list of buffers, tracking the running byte\n * total and stopping once {@link MAX_STREAM_BYTES} is reached. Trailing bytes\n * past the cap are dropped rather than buffered.\n */\nfunction pushCapped(chunks: Buffer[], total: number, chunk: Buffer): number {\n if (total >= MAX_STREAM_BYTES) {\n return total;\n }\n\n const remaining = MAX_STREAM_BYTES - total;\n\n if (chunk.length <= remaining) {\n chunks.push(chunk);\n\n return total + chunk.length;\n }\n\n chunks.push(chunk.subarray(0, remaining));\n\n return MAX_STREAM_BYTES;\n}\n\n/**\n * Force-kill a spawned command and its entire process tree.\n *\n * With `shell: true` the command runs under an intermediary shell\n * (`cmd.exe` on Windows, `/bin/sh` elsewhere), so signalling the direct\n * child only reaps the shell — a long-running grandchild (e.g. `node`)\n * would survive, leaving the `exec` promise unsettled. We therefore kill\n * the whole group:\n * - **Windows** — `taskkill /T /F` walks and terminates the PID's tree.\n * - **POSIX** — the child is spawned `detached`, becoming its own process\n * group leader, so `process.kill(-pid)` SIGKILLs the group.\n */\nfunction killTree(pid: number | undefined, child: { kill(signal: NodeJS.Signals): boolean }): void {\n if (pid === undefined) {\n child.kill(\"SIGKILL\");\n\n return;\n }\n\n if (platform === \"win32\") {\n spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n\n return;\n }\n\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n // The group may already be gone; fall back to the direct child.\n child.kill(\"SIGKILL\");\n }\n}\n\n/**\n * The real-disk executor: every filesystem method delegates to\n * `@warlock.js/fs` (never `node:fs`), and {@link LocalBackend.exec} spawns a\n * process via `node:child_process`. It is deliberately **policy-agnostic** —\n * it receives already-resolved absolute paths and an already-resolved\n * environment + timeout from the ops layer, and just performs the side\n * effect. See {@link WorkspaceBackend} for the contract this implements.\n *\n * Constructed via {@link createLocalBackend}; the class itself is internal.\n */\nclass LocalBackend implements WorkspaceBackend {\n /** Read a file's full UTF-8 content at an absolute path. */\n public async readFile(absPath: string): Promise<string> {\n return getFileAsync(absPath);\n }\n\n /**\n * Write full content to an absolute path. Uses `atomicWriteAsync`, so a\n * concurrent reader never observes a half-written file, and missing parent\n * directories are created.\n */\n public async writeFile(absPath: string, content: string): Promise<void> {\n await atomicWriteAsync(absPath, content);\n }\n\n /** Whether anything (file or directory) exists at an absolute path. */\n public async exists(absPath: string): Promise<boolean> {\n return pathExistsAsync(absPath);\n }\n\n /** Create a directory (and any missing parents) at an absolute path; idempotent. */\n public async mkdir(absPath: string): Promise<void> {\n await ensureDirectoryAsync(absPath);\n }\n\n /**\n * Remove a file or directory tree at an absolute path. Stats the target to\n * pick the right primitive — `removeDirectoryAsync` (recursive) for a\n * directory, `unlinkAsync` for anything else. A path that does not exist is\n * a no-op (both primitives swallow `ENOENT`).\n */\n public async remove(absPath: string): Promise<void> {\n let isDirectory = false;\n\n try {\n isDirectory = (await statsAsync(absPath)).isDirectory();\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code === \"ENOENT\") {\n return;\n }\n\n throw error;\n }\n\n if (isDirectory) {\n await removeDirectoryAsync(absPath);\n\n return;\n }\n\n await unlinkAsync(absPath);\n }\n\n /** List immediate children of an absolute directory as absolute paths. */\n public async list(absDir: string): Promise<string[]> {\n return listAsync(absDir);\n }\n\n /**\n * Resolve symlinks and `..` segments to a canonical absolute path — the\n * primitive the ops-layer jail uses to detect escapes. Delegates to\n * `node:fs/promises` `realpath`, the one filesystem operation `@warlock.js/fs`\n * does not expose.\n */\n public async realpath(absPath: string): Promise<string> {\n return realpath(absPath);\n }\n\n /**\n * Run a command and capture its outcome. The command line is executed\n * through the platform shell (`shell: true`) so pipes/operators behave as a\n * caller would expect; `cwd`, `env`, and the timeout are taken verbatim from\n * the ops layer (the environment is NOT merged with `process.env`). On\n * timeout the process is SIGKILLed and `timedOut` is set. `stdout`/`stderr`\n * are captured and byte-capped per {@link MAX_STREAM_BYTES}.\n *\n * Never rejects for a non-zero exit, a missing executable, or a timeout —\n * those are reported through the resolved {@link WorkspaceBackendExecResult}\n * so the ops layer can surface them as tool-error data.\n */\n public exec(\n command: string,\n opts: WorkspaceBackendExecOptions = {},\n ): Promise<WorkspaceBackendExecResult> {\n return new Promise<WorkspaceBackendExecResult>((resolve) => {\n const child = spawn(command, {\n cwd: opts.cwd,\n env: opts.env,\n shell: true,\n windowsHide: true,\n // POSIX: own process group so a timeout SIGKILL reaps the whole\n // shell tree, not just the shell. Harmless on Windows (ignored;\n // there we tree-kill via taskkill instead).\n detached: platform !== \"win32\",\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let timedOut = false;\n let settled = false;\n\n const timer =\n opts.timeoutMs !== undefined && opts.timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true;\n killTree(child.pid, child);\n }, opts.timeoutMs)\n : undefined;\n\n const settle = (exitCode: number) => {\n if (settled) {\n return;\n }\n\n settled = true;\n\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n\n resolve({\n exitCode,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut,\n });\n };\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n stdoutBytes = pushCapped(stdoutChunks, stdoutBytes, chunk);\n });\n\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrBytes = pushCapped(stderrChunks, stderrBytes, chunk);\n });\n\n // A spawn failure (e.g. the shell itself is missing) surfaces as an\n // error event with no exit; report it as a conventional shell\n // \"command not found\" exit code rather than rejecting.\n child.on(\"error\", () => {\n settle(127);\n });\n\n child.on(\"close\", (code, signal) => {\n // A null code means the process was terminated by a signal (our\n // timeout SIGKILL, or an external kill). Map that to the POSIX\n // 128 + signal-number convention so callers see a non-zero exit.\n if (code === null) {\n const signalNumber = signal === \"SIGKILL\" ? 9 : 1;\n settle(128 + signalNumber);\n\n return;\n }\n\n settle(code);\n });\n });\n }\n}\n\n/**\n * Create the **local** workspace backend — the default executor that runs the\n * workspace over the real disk (`@warlock.js/fs`) and the local shell\n * (`node:child_process`).\n *\n * The returned object is policy-agnostic: it expects already-jail-resolved\n * absolute paths and an already-resolved environment/timeout from the ops\n * layer. Pair it with {@link WorkspaceOps} for the actual cwd jail,\n * allow/deny lists, hashing, and output policy.\n *\n * @example\n * const backend = createLocalBackend();\n * await backend.writeFile(\"/srv/app/src/index.ts\", \"export const x = 1;\");\n * const { exitCode } = await backend.exec(\"node -v\", { cwd: \"/srv/app\" });\n */\nexport function createLocalBackend(): WorkspaceBackend {\n return new LocalBackend();\n}\n","import type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n} from \"../contracts/workspace-backend.contract\";\n\n/**\n * A scripted outcome for the mock backend's `exec`. Mirrors\n * {@link WorkspaceBackendExecResult} but every field is optional so a\n * registration can specify only what it cares about — the rest fall back\n * to a successful, empty-output, non-timed-out run.\n */\nexport type MockExecResult = Partial<WorkspaceBackendExecResult>;\n\n/**\n * The two seedable inputs of a mock backend:\n *\n * - `files` — initial file tree as `absolutePath -> content`. Parent\n * directories of each seeded file are implicitly created.\n * - `commands` — scripted `exec` outcomes keyed on the **exact** command\n * line. Any command not registered here resolves to a `0`-exit no-op.\n */\nexport interface MockBackendSeed {\n /** Initial file contents, keyed by absolute path. */\n files?: Record<string, string>;\n /** Scripted `exec` results, keyed by exact command line. */\n commands?: Record<string, MockExecResult>;\n}\n\n/**\n * Normalize an absolute path to a stable in-memory key: forward slashes,\n * collapsed duplicate separators, and resolved `.` / `..` segments. There\n * are no symlinks in memory, so this is a pure lexical canonicalization —\n * exactly what the backend's `realpath` promises.\n */\nfunction canonicalize(absPath: string): string {\n const unified = absPath.replace(/\\\\/g, \"/\");\n // Preserve a leading slash (POSIX root) or a `C:`-style Windows drive\n // prefix; everything else is a normal segment.\n const driveMatch = unified.match(/^([a-zA-Z]:)?\\/?/);\n const prefix = driveMatch ? driveMatch[0] : \"\";\n const rest = unified.slice(prefix.length);\n\n const resolved: string[] = [];\n\n for (const segment of rest.split(\"/\")) {\n if (segment === \"\" || segment === \".\") {\n continue;\n }\n\n if (segment === \"..\") {\n resolved.pop();\n\n continue;\n }\n\n resolved.push(segment);\n }\n\n const joined = resolved.join(\"/\");\n\n // A trailing slash on the prefix means it was absolute; keep it so the\n // result stays absolute even when the body is empty (the root itself).\n const normalizedPrefix = prefix.endsWith(\"/\") ? prefix : `${prefix}/`;\n\n return joined.length > 0 ? `${normalizedPrefix}${joined}` : normalizedPrefix;\n}\n\n/** All ancestor directory keys of a canonical path, root-first. */\nfunction ancestorsOf(canonicalPath: string): string[] {\n const lastSlash = canonicalPath.lastIndexOf(\"/\");\n\n if (lastSlash <= 0) {\n return [];\n }\n\n const parent = canonicalPath.slice(0, lastSlash);\n const result = ancestorsOf(parent);\n\n result.push(parent);\n\n return result;\n}\n\n/**\n * The in-memory executor behind {@link createMockBackend}. Holds the file\n * tree in a `Map`, the directory set in a `Set`, and the scripted command\n * table in a second `Map` — no disk, no child processes, fully\n * deterministic. Construct it via the factory, never directly.\n */\nclass MockBackend implements WorkspaceBackend {\n /** Canonical file path -> content. */\n private readonly files = new Map<string, string>();\n\n /** Canonical directory paths that exist (including implicit parents). */\n private readonly directories = new Set<string>();\n\n /** Exact command line -> scripted outcome. */\n private readonly commands = new Map<string, MockExecResult>();\n\n public constructor(seed?: MockBackendSeed) {\n for (const [path, content] of Object.entries(seed?.files ?? {})) {\n const canonical = canonicalize(path);\n\n this.files.set(canonical, content);\n this.registerAncestorDirectories(canonical);\n }\n\n for (const [command, result] of Object.entries(seed?.commands ?? {})) {\n this.commands.set(command, result);\n }\n }\n\n /** Record every ancestor directory of a path as existing. */\n private registerAncestorDirectories(canonicalPath: string): void {\n for (const ancestor of ancestorsOf(canonicalPath)) {\n this.directories.add(ancestor);\n }\n }\n\n public async readFile(absPath: string): Promise<string> {\n const canonical = canonicalize(absPath);\n const content = this.files.get(canonical);\n\n if (content === undefined) {\n throw new Error(`Mock backend: no such file: ${canonical}`);\n }\n\n return content;\n }\n\n public async writeFile(absPath: string, content: string): Promise<void> {\n const canonical = canonicalize(absPath);\n\n this.files.set(canonical, content);\n this.registerAncestorDirectories(canonical);\n }\n\n public async exists(absPath: string): Promise<boolean> {\n const canonical = canonicalize(absPath);\n\n return this.files.has(canonical) || this.directories.has(canonical);\n }\n\n public async mkdir(absPath: string): Promise<void> {\n const canonical = canonicalize(absPath);\n\n this.directories.add(canonical);\n this.registerAncestorDirectories(canonical);\n }\n\n public async remove(absPath: string): Promise<void> {\n const canonical = canonicalize(absPath);\n const prefix = `${canonical}/`;\n\n // Drop the entry itself and any descendant files/directories — a\n // recursive tree removal, matching the contract's \"file or directory\n // tree\" wording.\n for (const file of [...this.files.keys()]) {\n if (file === canonical || file.startsWith(prefix)) {\n this.files.delete(file);\n }\n }\n\n for (const directory of [...this.directories]) {\n if (directory === canonical || directory.startsWith(prefix)) {\n this.directories.delete(directory);\n }\n }\n }\n\n public async list(absDir: string): Promise<string[]> {\n const canonical = canonicalize(absDir);\n const prefix = canonical === \"/\" ? \"/\" : `${canonical}/`;\n const children = new Set<string>();\n\n const collect = (key: string): void => {\n if (!key.startsWith(prefix) || key === canonical) {\n return;\n }\n\n const remainder = key.slice(prefix.length);\n const nextSlash = remainder.indexOf(\"/\");\n const childName =\n nextSlash === -1 ? remainder : remainder.slice(0, nextSlash);\n\n if (childName.length > 0) {\n children.add(`${prefix}${childName}`);\n }\n };\n\n for (const file of this.files.keys()) {\n collect(file);\n }\n\n for (const directory of this.directories) {\n collect(directory);\n }\n\n return [...children].sort();\n }\n\n public async realpath(absPath: string): Promise<string> {\n return canonicalize(absPath);\n }\n\n public async exec(\n command: string,\n _opts?: WorkspaceBackendExecOptions,\n ): Promise<WorkspaceBackendExecResult> {\n const scripted = this.commands.get(command);\n\n return {\n exitCode: scripted?.exitCode ?? 0,\n stdout: scripted?.stdout ?? \"\",\n stderr: scripted?.stderr ?? \"\",\n timedOut: scripted?.timedOut ?? false,\n };\n }\n}\n\n/**\n * Create an in-memory {@link WorkspaceBackend} for fast, disk-free tests.\n *\n * Every IO method operates on a `Map` of `absolutePath -> content` (with a\n * companion directory set), so reads, writes, existence checks, `mkdir`,\n * recursive `remove`, `list`, and `realpath` all run synchronously in\n * memory with no filesystem access. `exec` is **scripted**: a registered\n * `command -> result` table is consulted by exact command line, and any\n * unregistered command resolves to a successful `0`-exit no-op with empty\n * output.\n *\n * `realpath` is a pure lexical canonicalization (forward slashes,\n * collapsed separators, resolved `.`/`..`) — there are no symlinks in\n * memory — which is exactly what the jail resolver expects.\n *\n * The single positional `seed` accepts either the shorthand\n * `Record<string, string>` of file contents (matching the design's\n * `ai.workspace.mock(seed)` signature) or the richer {@link MockBackendSeed}\n * with both `files` and scripted `commands`.\n *\n * @example\n * // Shorthand: seed files only.\n * const backend = createMockBackend({ \"/srv/app/src/index.ts\": \"export const x = 1;\" });\n * await backend.readFile(\"/srv/app/src/index.ts\"); // \"export const x = 1;\"\n *\n * @example\n * // Full seed: files plus a scripted command.\n * const backend = createMockBackend({\n * files: { \"/srv/app/package.json\": \"{}\" },\n * commands: { \"npm test\": { exitCode: 1, stderr: \"1 failing\" } },\n * });\n * await backend.exec(\"npm test\"); // { exitCode: 1, stderr: \"1 failing\", ... }\n * await backend.exec(\"echo hi\"); // { exitCode: 0, stdout: \"\", ... } (no-op)\n */\nexport function createMockBackend(\n seed?: Record<string, string> | MockBackendSeed,\n): WorkspaceBackend {\n return new MockBackend(normalizeSeed(seed));\n}\n\n/** Coerce the dual-shaped `seed` argument into a {@link MockBackendSeed}. */\nfunction normalizeSeed(\n seed?: Record<string, string> | MockBackendSeed,\n): MockBackendSeed | undefined {\n if (seed === undefined) {\n return undefined;\n }\n\n if (isMockBackendSeed(seed)) {\n return seed;\n }\n\n return { files: seed };\n}\n\n/**\n * Whether `seed` is the structured {@link MockBackendSeed} (has a `files`\n * or `commands` key) rather than the flat `path -> content` shorthand. A\n * plain shorthand map whose only key happens to be named `files` is\n * treated as structured — callers wanting that literal path should use the\n * explicit `{ files: { files: \"...\" } }` form.\n */\nfunction isMockBackendSeed(\n seed: Record<string, string> | MockBackendSeed,\n): seed is MockBackendSeed {\n return (\n typeof (seed as MockBackendSeed).files === \"object\" ||\n typeof (seed as MockBackendSeed).commands === \"object\"\n );\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the workspace tools' input validation. The package pins\n * only `@warlock.js/ai` and `@warlock.js/fs` as runtime dependencies, so\n * rather than pull in a schema library we hand-roll the few shapes the\n * file tools need — exactly the pattern `@warlock.js/ai`'s own `tool()`\n * tests use. Each builder returns a `StandardSchemaV1`, which is what\n * `tool({ input })` validates against before calling `execute`.\n *\n * These intentionally cover only the primitive cases the FILE tools\n * require (`string`, `optional string`, `optional number`, `optional\n * boolean`, and an `object` of fields). They are not a general-purpose\n * validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-workspace\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional string field — accepts `undefined` (the property absent or\n * explicitly undefined) or a string, and rejects every other type.\n */\nexport function optionalStringField(): FieldValidator<string | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"string\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a string when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [\n { message: `\"${key}\" must be a finite number when provided`, path: [key] },\n ],\n };\n };\n}\n\n/** Optional boolean field — accepts `undefined` or a boolean. */\nexport function optionalBooleanField(): FieldValidator<boolean | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"boolean\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a boolean when provided`, path: [key] }],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ path: string; limit?: number }>({\n * path: stringField(),\n * limit: optionalNumberField(),\n * });\n */\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n","import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport {\n objectSchema,\n optionalBooleanField,\n optionalStringField,\n stringField,\n} from \"./schema\";\nimport type { EditFileInput, EditFileResult, WorkspaceOps } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"edit_file\";\n\n/** Input schema for the `edit_file` tool. */\nconst inputSchema = objectSchema<EditFileInput>({\n path: stringField(),\n oldString: stringField(),\n newString: stringField(),\n replaceAll: optionalBooleanField(),\n expectHash: optionalStringField(),\n});\n\n/**\n * Build the agent-facing `edit_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, oldString, newString, replaceAll?,\n * expectHash? }` against a Standard Schema, then delegates to\n * `ops.editFile`, which applies the exact-string replacement under the\n * read-before-edit guard and returns the replacement count plus the\n * post-edit `hash`.\n *\n * **Errors flow as data.** A non-unique `oldString` (without\n * `replaceAll`), a missing `oldString`, or a stale `expectHash` cause\n * `ops` to throw a `WorkspaceEditError`; the `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field — `invoke()` never\n * throws — so the agent can re-read and retry.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const editTool = makeEditFileTool(ops);\n * const { data, error } = await editTool.invoke({\n * path: \"src/index.ts\",\n * oldString: \"const a = 1;\",\n * newString: \"const a = 2;\",\n * expectHash,\n * });\n * if (error) console.warn(error.message); // e.g. stale-hash → re-read\n */\nexport function makeEditFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<EditFileInput, EditFileResult> {\n return tool<EditFileInput, EditFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Replace an exact substring in a workspace file. oldString must match \" +\n \"uniquely unless replaceAll is set. Pass expectHash (from read_file) to \" +\n \"reject the edit if the file changed since you read it. Returns the \" +\n \"number of replacements and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n return ops.editFile(input);\n },\n });\n}\n","import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type { GlobInput, GlobResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGlobTool} to customize the vended tool. */\nexport interface MakeGlobToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"glob\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * path-match surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GlobInput} — a single required `pattern`\n * string. Built on the package's shared, dependency-free schema builders\n * (no schema library, matching the validator idiom the `@warlock.js/ai`\n * tool runtime expects).\n */\nconst globInputSchema = objectSchema<GlobInput>({\n pattern: stringField(),\n});\n\n/**\n * Build the agent-facing `glob` tool — resolve a glob pattern to the\n * matching workspace-relative paths within the jail. The returned\n * {@link ToolContract} validates the LLM's arguments, delegates to\n * {@link WorkspaceOps.glob} (which returns a bare sorted `string[]`), and\n * wraps the result in a {@link GlobResult} so the agent always reads a\n * stable `{ paths }` envelope. The jail and `denyPaths` filtering are\n * enforced in the shared ops layer; a policy violation surfaces as typed\n * tool-error *data* via the runtime's `invoke()` wrapper.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `glob`.\n *\n * @example\n * const glob = makeGlobTool(ops);\n * const { data } = await glob.invoke({ pattern: \"src/models/**\\/*.ts\" });\n * console.log(data?.paths);\n */\nexport function makeGlobTool(\n ops: WorkspaceOps,\n options?: MakeGlobToolOptions,\n): ToolContract<GlobInput, GlobResult> {\n return tool<GlobInput, GlobResult>({\n name: options?.name ?? \"glob\",\n description:\n \"Find files in the workspace whose path matches a glob pattern \" +\n \"(supports `*`, `**`, and `?`). Returns the matching \" +\n \"workspace-relative paths, sorted.\",\n action: (input) => `Finding files matching ${input.pattern}`,\n input: globInputSchema,\n async execute(input) {\n const paths = await ops.glob(input.pattern);\n\n return { paths };\n },\n });\n}\n","import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport {\n objectSchema,\n optionalBooleanField,\n optionalStringField,\n stringField,\n} from \"./schema\";\nimport type { GrepInput, GrepResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGrepTool} to customize the vended tool. */\nexport interface MakeGrepToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"grep\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * search surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GrepInput} — `pattern` is a required string;\n * `glob` and `ignoreCase` are optional. Built on the package's shared,\n * dependency-free schema builders (no schema library, matching the\n * validator idiom the `@warlock.js/ai` tool runtime expects).\n */\nconst grepInputSchema = objectSchema<GrepInput>({\n pattern: stringField(),\n glob: optionalStringField(),\n ignoreCase: optionalBooleanField(),\n});\n\n/**\n * Build the agent-facing `grep` tool — a regex content search across the\n * jailed file set. The returned {@link ToolContract} validates the LLM's\n * arguments, then delegates verbatim to {@link WorkspaceOps.grep}, so the\n * policy jail, `denyPaths` filtering, and match cap are enforced in the\n * single shared ops layer rather than duplicated here. A policy violation\n * (e.g. a jail-resolution failure) surfaces as typed tool-error *data*\n * via the runtime's `invoke()` wrapper, never as a thrown run-killer.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `grep`.\n *\n * @example\n * const grep = makeGrepTool(ops);\n * const { data } = await grep.invoke({ pattern: \"TODO\", glob: \"src/*.ts\" });\n * console.log(data?.total, data?.matches);\n */\nexport function makeGrepTool(\n ops: WorkspaceOps,\n options?: MakeGrepToolOptions,\n): ToolContract<GrepInput, GrepResult> {\n return tool<GrepInput, GrepResult>({\n name: options?.name ?? \"grep\",\n description:\n \"Search file contents across the workspace for a regular-expression \" +\n \"pattern. Optionally narrow the scanned files with a glob and match \" +\n \"case-insensitively. Returns every matching line with its file path \" +\n \"and 1-based line number.\",\n action: (input) => `Searching for /${input.pattern}/`,\n input: grepInputSchema,\n async execute(input) {\n return ops.grep(input.pattern, {\n glob: input.glob,\n ignoreCase: input.ignoreCase,\n });\n },\n });\n}\n","import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, optionalNumberField, stringField } from \"./schema\";\nimport type { ReadFileInput, ReadFileResult, WorkspaceOps } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"read_file\";\n\n/** Input schema for the `read_file` tool. */\nconst inputSchema = objectSchema<ReadFileInput>({\n path: stringField(),\n startLine: optionalNumberField(),\n limit: optionalNumberField(),\n});\n\n/**\n * Build the agent-facing `read_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, startLine?, limit? }` against a Standard\n * Schema, then delegates to `ops.readFile`, mapping the result into the\n * agent wire shape {@link ReadFileResult} — the `hash` an agent must\n * carry into a later `edit_file` (read-before-edit), plus the `startLine`\n * / `endLine` / `truncated` window metadata derived from the requested\n * range and the file's `totalLines`.\n *\n * **Errors flow as data.** Policy violations (a jail escape) are thrown\n * by `ops`; the `tool()` wrapper catches them and surfaces them in the\n * returned `{ error }` field — `invoke()` never throws — so the agent can\n * read the failure and self-correct.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const readTool = makeReadFileTool(ops);\n * const { data, error } = await readTool.invoke({ path: \"src/index.ts\" });\n * if (!error) console.log(data.hash); // feed into edit_file's expectHash\n */\nexport function makeReadFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<ReadFileInput, ReadFileResult> {\n return tool<ReadFileInput, ReadFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Read a file from the workspace, returning a numbered line window plus \" +\n \"the file's content hash. Pass the hash to edit_file's expectHash to \" +\n \"guard against editing a stale version. Use startLine/limit to page \" +\n \"through large files.\",\n input: inputSchema,\n async execute(input) {\n const startLine = input.startLine !== undefined ? Math.max(1, input.startLine) : 1;\n const { content, hash, totalLines } = await ops.readFile(input.path, {\n offset: startLine,\n limit: input.limit,\n });\n\n // The window's last line is the start plus however many lines the\n // ops layer actually returned (it caps at `limit` / the policy\n // default), bounded by the file's end.\n const returnedLines = content.length === 0 ? 0 : content.split(\"\\n\").length;\n const endLine = Math.min(totalLines, startLine + Math.max(returnedLines, 1) - 1);\n const truncated = endLine < totalLines;\n\n return { content, startLine, endLine, totalLines, truncated, hash };\n },\n });\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n RunShellInput,\n RunShellResult,\n WorkspaceOps,\n} from \"../contracts\";\n\n/** The default tool name `run_shell` is exposed to the LLM under. */\nconst DEFAULT_RUN_SHELL_TOOL_NAME = \"run_shell\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunShellInput}. We validate the\n * model's arguments without a runtime schema dependency: `command` must be\n * a non-empty string, and `timeoutMs` (when present) a positive number.\n * Invalid args surface as a `SchemaValidationError` in the tool result's\n * `error` field rather than reaching `ops.exec`.\n */\nconst runShellInputSchema: StandardSchemaV1<RunShellInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n if (typeof value !== \"object\" || value === null) {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.command !== \"string\" || candidate.command.length === 0) {\n return { issues: [{ message: \"command must be a non-empty string\", path: [\"command\"] }] };\n }\n\n if (\n candidate.timeoutMs !== undefined &&\n (typeof candidate.timeoutMs !== \"number\" || candidate.timeoutMs <= 0)\n ) {\n return {\n issues: [{ message: \"timeoutMs must be a positive number\", path: [\"timeoutMs\"] }],\n };\n }\n\n const result: RunShellInput = { command: candidate.command };\n\n if (candidate.timeoutMs !== undefined) {\n result.timeoutMs = candidate.timeoutMs as number;\n }\n\n return { value: result };\n },\n },\n};\n\n/** Options for {@link makeRunShellTool}. */\nexport interface MakeRunShellToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_shell\"`). */\n name?: string;\n}\n\n/**\n * Build the `run_shell` tool — a {@link ToolContract} that runs a single\n * shell command through the policy-enforced {@link WorkspaceOps} layer.\n *\n * The command's leading executable basename is gated against the shell\n * allow/deny policy by `ops.exec`; a blocked command throws a\n * `WorkspacePolicyError` which the `tool()` runtime catches and surfaces\n * in the result's `error` field (never a thrown run-killer), so the agent\n * reads the refusal as tool data and self-corrects. A command that runs\n * but exits non-zero is *not* an error — its `exitCode`/`stderr` come back\n * in `data` for the agent to inspect.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Optional tool-name override.\n *\n * @example\n * const runShell = makeRunShellTool(ops);\n * const { data, error } = await runShell.invoke({ command: \"npm run build\" });\n * if (error) handleDenied(error);\n * else console.log(data.exitCode, data.stdout);\n */\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult> {\n return tool<RunShellInput, RunShellResult>({\n name: options?.name ?? DEFAULT_RUN_SHELL_TOOL_NAME,\n description:\n \"Run a single shell command inside the workspace. The command's \" +\n \"executable must be permitted by the shell policy; output is \" +\n \"byte-capped and the run is time-limited. A non-zero exit code is \" +\n \"returned as data, not an error.\",\n action: (input) => `Running \\`${input.command}\\``,\n input: runShellInputSchema,\n execute: (input) => ops.exec(input.command, { timeoutMs: input.timeoutMs }),\n });\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n RunShellResult,\n RunTestsInput,\n WorkspaceOps,\n} from \"../contracts\";\n\n/** The default tool name `run_tests` is exposed to the LLM under. */\nconst DEFAULT_RUN_TESTS_TOOL_NAME = \"run_tests\";\n\n/** The default command run when no `command` override is configured. */\nconst DEFAULT_TEST_COMMAND = \"npm test\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunTestsInput}. `pattern` is the\n * only field and is optional; when present it must be a string. Validation\n * happens without a runtime schema dependency, mirroring the wider tool\n * layer.\n */\nconst runTestsInputSchema: StandardSchemaV1<RunTestsInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n // A no-argument call (the common case) is valid and runs the bare\n // test command.\n if (value === undefined || value === null) {\n return { value: {} };\n }\n\n if (typeof value !== \"object\") {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (candidate.pattern !== undefined && typeof candidate.pattern !== \"string\") {\n return { issues: [{ message: \"pattern must be a string\", path: [\"pattern\"] }] };\n }\n\n const result: RunTestsInput = {};\n\n if (candidate.pattern !== undefined) {\n result.pattern = candidate.pattern as string;\n }\n\n return { value: result };\n },\n },\n};\n\n/** Options for {@link makeRunTestsTool}. */\nexport interface MakeRunTestsToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_tests\"`). */\n name?: string;\n /**\n * The base test command to run (default `\"npm test\"`). When the model\n * supplies a `pattern`, it is appended to this command.\n */\n command?: string;\n}\n\n/**\n * Build the `run_tests` tool — a {@link ToolContract} convenience over\n * `run_shell` that runs the workspace's configured test command through\n * the policy-enforced {@link WorkspaceOps} layer.\n *\n * The base command defaults to `\"npm test\"` and can be overridden via\n * `options.command`. When the model passes a `pattern`, it is appended to\n * the command as a path/suite filter forwarded to the runner (e.g.\n * `\"npm test src/cart\"`). Like `run_shell`, the resolved command's\n * executable is gated by the shell policy — a denial surfaces in the\n * result's `error` field — and a non-zero exit (failing tests) comes back\n * as `data` for the agent to read and fix.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Optional tool-name and base-command overrides.\n *\n * @example\n * const runTests = makeRunTestsTool(ops, { command: \"pnpm test\" });\n * const { data } = await runTests.invoke({ pattern: \"cart-total\" });\n * if (data.exitCode !== 0) inspect(data.stderr);\n */\nexport function makeRunTestsTool(\n ops: WorkspaceOps,\n options?: MakeRunTestsToolOptions,\n): ToolContract<RunTestsInput, RunShellResult> {\n const baseCommand = options?.command ?? DEFAULT_TEST_COMMAND;\n\n return tool<RunTestsInput, RunShellResult>({\n name: options?.name ?? DEFAULT_RUN_TESTS_TOOL_NAME,\n description:\n \"Run the workspace's test suite, optionally narrowed to a path or \" +\n \"name pattern forwarded to the test runner. Failing tests return a \" +\n \"non-zero exit code as data, not an error.\",\n action: (input) =>\n input.pattern ? `Running tests matching \"${input.pattern}\"` : \"Running tests\",\n input: runTestsInputSchema,\n execute: (input) => {\n const command = input.pattern ? `${baseCommand} ${input.pattern}` : baseCommand;\n\n return ops.exec(command);\n },\n });\n}\n","import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type { WorkspaceOps, WriteFileInput, WriteFileResult } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"write_file\";\n\n/** Input schema for the `write_file` tool. */\nconst inputSchema = objectSchema<WriteFileInput>({\n path: stringField(),\n content: stringField(),\n});\n\n/**\n * Build the agent-facing `write_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, content }` against a Standard Schema, then\n * delegates to `ops.writeFile`, which atomically writes the full content\n * (creating parent directories) and returns the byte count and content\n * `hash`. The tool re-attaches the workspace-relative `path` so the\n * result matches the {@link WriteFileResult} wire shape.\n *\n * **Errors flow as data.** A jail escape is thrown by `ops`; the\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field — `invoke()` never throws.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const writeTool = makeWriteFileTool(ops);\n * const { data } = await writeTool.invoke({ path: \"src/new.ts\", content: \"export {};\" });\n * console.log(data.bytesWritten, data.hash);\n */\nexport function makeWriteFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<WriteFileInput, WriteFileResult> {\n return tool<WriteFileInput, WriteFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Write full content to a workspace file, creating it (and any parent \" +\n \"directories) if absent and overwriting it otherwise. The write is \" +\n \"atomic. Returns the bytes written and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n const { hash, bytesWritten } = await ops.writeFile(input.path, input.content);\n\n return { path: input.path, bytesWritten, hash };\n },\n });\n}\n","import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n EditFileInput,\n EditFileResult,\n GrepResult,\n RunShellResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"writeFile\",\n \"runShell\",\n \"runTests\",\n \"grep\",\n \"glob\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * non-mutating tools only. `editFile` / `writeFile` / `runShell` /\n * `runTests` are deliberately omitted so a reviewer agent has no path to\n * change the tree.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"grep\",\n \"glob\",\n];\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy);\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(\n contract: ToolContract<TInput, TOutput>,\n ): ToolContract => contract as unknown as ToolContract;\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n runShell: (opts) => erase(makeRunShellTool(this.ops, opts)),\n runTests: (opts) => erase(makeRunTestsTool(this.ops, opts)),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n return {\n all: () =>\n ALL_TOOL_NAMES.filter((name) => this.allowedTools.has(name)).map((name) =>\n build(name),\n ),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<{ content: string; hash: string; totalLines: number }> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async exec(command: string, opts?: { timeoutMs?: number }): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string): Promise<string[]> {\n return this.ops.glob(pattern);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read/grep/glob\n * tools are vended and every mutating direct method rejects with a\n * {@link WorkspacePolicyError}. A fresh ops/backend is built from the\n * identical policy, so the projection sees the same jailed tree.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(this.policy, READONLY_TOOL_NAMES);\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the seven agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `writeFile` / `runShell` /\n * `runTests` / `grep` / `glob`, plus `all()` (every tool) and\n * `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` / `exec` /\n * `grep` / `glob` / `exists` / `mkdir` / `remove`, each delegating 1:1\n * to the shared ops layer.\n * - **`readonly()`** — a projection that vends only read/grep/glob and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["AIError","canonicalize","path","globToRegExp","fs","path","platform","fs","DEFAULT_NAME","inputSchema","DEFAULT_NAME","inputSchema","path"],"sources":["../../../../../../@warlock.js/ai-workspace/src/errors.ts","../../../../../../@warlock.js/ai-workspace/src/policy/policy.ts","../../../../../../@warlock.js/ai-workspace/src/ops.ts","../../../../../../@warlock.js/ai-workspace/src/backends/local.ts","../../../../../../@warlock.js/ai-workspace/src/backends/mock.ts","../../../../../../@warlock.js/ai-workspace/src/tools/schema.ts","../../../../../../@warlock.js/ai-workspace/src/tools/edit-file.ts","../../../../../../@warlock.js/ai-workspace/src/tools/glob.ts","../../../../../../@warlock.js/ai-workspace/src/tools/grep.ts","../../../../../../@warlock.js/ai-workspace/src/tools/read-file.ts","../../../../../../@warlock.js/ai-workspace/src/tools/run-shell.ts","../../../../../../@warlock.js/ai-workspace/src/tools/run-tests.ts","../../../../../../@warlock.js/ai-workspace/src/tools/write-file.ts","../../../../../../@warlock.js/ai-workspace/src/workspace.ts"],"sourcesContent":["import { AIError, type AIErrorOptions } from \"@warlock.js/ai\";\n\n/**\n * Why a workspace policy check rejected an operation.\n *\n * - `\"path-escape\"` — a resolved path fell outside the `cwd` jail (or an\n * `allowPaths` root), or matched a `denyPaths` glob.\n * - `\"denied-command\"` — a shell command's leading executable basename\n * was not in `shell.allow`, or was explicitly in `shell.deny`.\n */\nexport type WorkspacePolicyViolation = \"path-escape\" | \"denied-command\";\n\n/**\n * Options for {@link WorkspacePolicyError} — the structured `type`\n * discriminator plus, where relevant, the offending path or command for\n * branchable diagnostics without parsing the message.\n */\nexport type WorkspacePolicyErrorOptions = AIErrorOptions & {\n /** Which policy rule was violated. */\n type: WorkspacePolicyViolation;\n /** The offending workspace-relative path (for `\"path-escape\"`). */\n path?: string;\n /** The offending command line (for `\"denied-command\"`). */\n command?: string;\n};\n\n/**\n * The workspace policy engine refused an operation — a path escaped the\n * jail (or hit a deny glob), or a shell command's executable was not\n * allowed.\n *\n * **Surface.** This is returned to the agent as tool-error *data*, never\n * a thrown run-killer — the agent reads the failure and self-corrects.\n * Extends the framework `AIError` (category `\"tool\"`, code\n * `TOOL_EXEC_FAILED`) so it flows through the same typed error contract\n * as every other AI error; branch on `error.type` for the specific\n * violation.\n *\n * @example\n * if (error instanceof WorkspacePolicyError && error.type === \"denied-command\") {\n * console.warn(`Blocked command: ${error.command}`);\n * }\n */\nexport class WorkspacePolicyError extends AIError {\n /** Which policy rule was violated. */\n public readonly type: WorkspacePolicyViolation;\n /** The offending path, when the violation was a path escape. */\n public readonly path?: string;\n /** The offending command, when the violation was a denied command. */\n public readonly command?: string;\n\n public constructor(message: string, options: WorkspacePolicyErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspacePolicyError\";\n this.type = options.type;\n this.path = options.path;\n this.command = options.command;\n }\n}\n\n/**\n * Why an edit was rejected.\n *\n * - `\"not-found\"` — the `oldString` did not appear in the file.\n * - `\"not-unique\"` — `oldString` matched more than once and `replaceAll`\n * was not set, so the edit is ambiguous.\n * - `\"stale-hash\"` — the file's current hash did not match the supplied\n * `expectHash`; the file changed since it was read.\n */\nexport type WorkspaceEditFailure = \"not-found\" | \"not-unique\" | \"stale-hash\";\n\n/**\n * Options for {@link WorkspaceEditError} — the structured `type`\n * discriminator plus optional match-count / hash context for the\n * `\"not-unique\"` and `\"stale-hash\"` cases.\n */\nexport type WorkspaceEditErrorOptions = AIErrorOptions & {\n /** Why the edit was rejected. */\n type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n path: string;\n /** How many times `oldString` matched (for `\"not-unique\"`). */\n matches?: number;\n /** The hash the caller expected (for `\"stale-hash\"`). */\n expectedHash?: string;\n /** The file's actual current hash (for `\"stale-hash\"`). */\n actualHash?: string;\n};\n\n/**\n * An `editFile` operation was rejected by the read-before-edit guard:\n * the `oldString` was absent, matched non-uniquely without `replaceAll`,\n * or the file's hash no longer matched the supplied `expectHash`.\n *\n * **Surface.** Like {@link WorkspacePolicyError}, returned to the agent\n * as tool-error *data* so it can re-read and retry. Extends `AIError`\n * (category `\"tool\"`, code `TOOL_EXEC_FAILED`); branch on `error.type`.\n *\n * @example\n * if (error instanceof WorkspaceEditError && error.type === \"stale-hash\") {\n * // re-read the file and retry the edit with the fresh hash\n * }\n */\nexport class WorkspaceEditError extends AIError {\n /** Why the edit was rejected. */\n public readonly type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n public readonly path: string;\n /** How many times `oldString` matched, for the `\"not-unique\"` case. */\n public readonly matches?: number;\n /** The hash the caller expected, for the `\"stale-hash\"` case. */\n public readonly expectedHash?: string;\n /** The file's actual current hash, for the `\"stale-hash\"` case. */\n public readonly actualHash?: string;\n\n public constructor(message: string, options: WorkspaceEditErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspaceEditError\";\n this.type = options.type;\n this.path = options.path;\n this.matches = options.matches;\n this.expectedHash = options.expectedHash;\n this.actualHash = options.actualHash;\n }\n}\n","import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type { WorkspacePolicy } from \"../contracts\";\n\n/**\n * The outcome of resolving a workspace-relative (or absolute) input path\n * against the jail — the canonical absolute location the backend should\n * touch, plus the workspace-relative form (POSIX-style, `/`-separated)\n * the agent and tool results echo back.\n */\nexport interface ResolvedPath {\n /** Canonical absolute path (symlinks in existing ancestors collapsed). */\n absolutePath: string;\n /**\n * The path relative to `policy.cwd`, `/`-separated regardless of OS,\n * used in tool results so the agent always sees stable workspace paths.\n * Empty string when the resolved path IS the jail root.\n */\n relativePath: string;\n}\n\n/**\n * Resolve the canonical absolute form of `target`, collapsing any\n * symlinks. `target` may not yet exist (a fresh `writeFile`/`mkdir`),\n * so we realpath the deepest **existing** ancestor and re-attach the\n * non-existent tail — a symlinked ancestor still cannot smuggle the\n * path out of the jail, while genuinely new leaves stay creatable.\n */\nasync function canonicalize(target: string): Promise<string> {\n let resolvedTarget = path.resolve(target);\n const tail: string[] = [];\n\n // Walk up until an existing ancestor is found (or we hit the root).\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n const real = await realpath(resolvedTarget);\n\n return tail.length > 0 ? path.join(real, ...tail) : real;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n\n // Only ENOENT means \"this segment doesn't exist yet\" — keep\n // walking up. Any other error (EACCES, ELOOP, …) is a real fault.\n if (code !== \"ENOENT\") {\n throw error;\n }\n\n const parent = path.dirname(resolvedTarget);\n\n // Reached the filesystem root without finding an existing\n // ancestor — give back the lexically-resolved path unchanged.\n if (parent === resolvedTarget) {\n return path.join(resolvedTarget, ...tail);\n }\n\n tail.unshift(path.basename(resolvedTarget));\n resolvedTarget = parent;\n }\n }\n}\n\n/**\n * Whether `child` is contained within `root` (or equals it), comparing\n * canonical absolute paths. Guards against the `/srv/app-evil` vs\n * `/srv/app` prefix-collision by anchoring on a path separator.\n */\nfunction isInside(child: string, root: string): boolean {\n const relative = path.relative(root, child);\n\n return (\n relative === \"\" ||\n (!relative.startsWith(\"..\") && !path.isAbsolute(relative))\n );\n}\n\n/**\n * Translate a glob (the small subset used by `denyPaths` — `*`, `**`,\n * `?`) into an anchored `RegExp` over a `/`-separated relative path.\n * `**` spans path separators; a single `*` does not.\n */\nfunction globToRegExp(glob: string): RegExp {\n let source = \"\";\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index];\n\n if (char === \"*\") {\n if (glob[index + 1] === \"*\") {\n // `**` — match across segments (and an optional trailing slash).\n source += \".*\";\n index++;\n\n if (glob[index + 1] === \"/\") {\n index++;\n }\n } else {\n // `*` — match within a single segment.\n source += \"[^/]*\";\n }\n\n continue;\n }\n\n if (char === \"?\") {\n source += \"[^/]\";\n\n continue;\n }\n\n // Escape everything else so it matches literally.\n source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * Whether a workspace-relative (`/`-separated) path matches any of the\n * policy's `denyPaths` globs. A deny glob also blocks everything beneath\n * a matched directory (`\".git/**\"` blocks `.git/config`).\n */\nfunction matchesDeny(relativePath: string, denyPaths: string[]): boolean {\n return denyPaths.some((glob) => {\n if (globToRegExp(glob).test(relativePath)) {\n return true;\n }\n\n // A bare directory glob (`\".git\"`, `\"node_modules\"`) should also\n // block its contents, mirroring how `\".git/**\"` would behave.\n if (!glob.includes(\"*\") && !glob.includes(\"?\")) {\n const prefix = glob.endsWith(\"/\") ? glob : `${glob}/`;\n\n return relativePath.startsWith(prefix);\n }\n\n return false;\n });\n}\n\n/**\n * Resolve and jail a single input path against a {@link WorkspacePolicy}.\n *\n * The input is resolved against `policy.cwd`, canonicalized (symlinks in\n * existing ancestors collapsed so a symlinked directory cannot escape\n * the jail), then accepted **only** when it sits under `cwd` or one of\n * the `allowPaths` roots. A path that escapes, or that matches any\n * `denyPaths` glob even while inside `cwd`, is rejected with a\n * {@link WorkspacePolicyError} of type `\"path-escape\"`.\n *\n * @param policy - The bounding policy (its `cwd` is the jail root).\n * @param inputPath - A workspace-relative or absolute path to resolve.\n * @returns The canonical absolute path plus its `/`-separated relative form.\n * @throws {WorkspacePolicyError} When the path escapes the jail or hits a deny glob.\n *\n * @example\n * const { absolutePath } = await resolveInJail(policy, \"src/index.ts\");\n */\nexport async function resolveInJail(\n policy: WorkspacePolicy,\n inputPath: string,\n): Promise<ResolvedPath> {\n const jailRoot = await canonicalize(policy.cwd);\n const requested = path.isAbsolute(inputPath)\n ? inputPath\n : path.join(policy.cwd, inputPath);\n const absolutePath = await canonicalize(requested);\n\n const insideCwd = isInside(absolutePath, jailRoot);\n const allowRoots = policy.allowPaths ?? [];\n let insideAllow = false;\n\n if (!insideCwd) {\n for (const root of allowRoots) {\n const canonicalRoot = await canonicalize(root);\n\n if (isInside(absolutePath, canonicalRoot)) {\n insideAllow = true;\n\n break;\n }\n }\n }\n\n if (!insideCwd && !insideAllow) {\n throw new WorkspacePolicyError(\n `Path \"${inputPath}\" resolves outside the workspace jail.`,\n { type: \"path-escape\", path: inputPath },\n );\n }\n\n // `denyPaths` is evaluated relative to the jail root and wins even\n // when the path is comfortably inside `cwd`.\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, absolutePath).split(path.sep).join(\"/\")\n : \"\";\n\n if (insideCwd && policy.denyPaths && policy.denyPaths.length > 0) {\n if (matchesDeny(relativeToJail, policy.denyPaths)) {\n throw new WorkspacePolicyError(\n `Path \"${inputPath}\" is blocked by the workspace deny list.`,\n { type: \"path-escape\", path: inputPath },\n );\n }\n }\n\n return { absolutePath, relativePath: relativeToJail };\n}\n\n/**\n * Extract the leading executable basename from a command line — the\n * token the shell allow/deny policy is keyed on. `\"npm run build\"` →\n * `\"npm\"`; `\"/usr/bin/node app.js\"` → `\"node\"`; `\"node.exe app\"` →\n * `\"node\"` (the `.exe`/`.cmd`/`.bat` Windows extension is stripped).\n */\nfunction leadingExecutable(command: string): string {\n const trimmed = command.trim();\n const firstToken = trimmed.split(/\\s+/)[0] ?? \"\";\n const base = path.basename(firstToken);\n\n return base.replace(/\\.(exe|cmd|bat|com)$/i, \"\");\n}\n\n/**\n * Whether a shell command is permitted by the policy's `shell` sub-policy.\n *\n * The command's leading executable basename is matched against\n * `shell.deny` then `shell.allow`. **Deny always wins.** When\n * `shell.allow` is set, the executable MUST appear in it (fail-closed\n * allowlist); when `allow` is absent/empty, any non-denied command is\n * permitted. An absent `shell` block means no command may run at all.\n *\n * Returns a plain `boolean` rather than throwing — the ops layer raises\n * the {@link WorkspacePolicyError} so the thrown context (`command`)\n * lives next to the call site.\n *\n * @example\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test\"); // true\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"rm -rf /\"); // false\n */\nexport function isCommandAllowed(\n policy: WorkspacePolicy,\n command: string,\n): boolean {\n const shell = policy.shell;\n\n // No shell sub-policy ⇒ fail-closed: nothing may run.\n if (!shell) {\n return false;\n }\n\n const executable = leadingExecutable(command);\n\n if (executable === \"\") {\n return false;\n }\n\n // Deny wins over everything else.\n if (shell.deny && shell.deny.includes(executable)) {\n return false;\n }\n\n // An allowlist, when present, is exhaustive.\n if (shell.allow && shell.allow.length > 0) {\n return shell.allow.includes(executable);\n }\n\n // No allowlist: anything not explicitly denied is permitted.\n return true;\n}\n\n/**\n * Build the exact environment a spawned process receives — `process.env`\n * is **never** inherited wholesale. The result is\n * `{ ...pick(process.env, inheritEnv), ...shell.env }`, so a command\n * cannot see `PATH` (and thus often cannot find `node`/`npm`) unless the\n * policy opts in via `shell.inheritEnv: [\"PATH\"]`. Explicit `shell.env`\n * values override inherited ones on key collision.\n *\n * @example\n * buildEnv({ cwd, shell: { inheritEnv: [\"PATH\"], env: { CI: \"1\" } } });\n * // → { PATH: <process PATH>, CI: \"1\" }\n */\nexport function buildEnv(policy: WorkspacePolicy): Record<string, string> {\n const shell = policy.shell;\n const env: Record<string, string> = {};\n\n if (!shell) {\n return env;\n }\n\n for (const key of shell.inheritEnv ?? []) {\n const value = process.env[key];\n\n if (value !== undefined) {\n env[key] = value;\n }\n }\n\n if (shell.env) {\n for (const [key, value] of Object.entries(shell.env)) {\n env[key] = value;\n }\n }\n\n return env;\n}\n","import path from \"node:path\";\nimport { fs } from \"@warlock.js/fs\";\nimport { WorkspaceEditError, WorkspacePolicyError } from \"./errors\";\nimport { buildEnv, isCommandAllowed, resolveInJail } from \"./policy/policy\";\nimport type {\n EditFileInput,\n EditFileResult,\n GrepMatch,\n GrepResult,\n RunShellResult,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n} from \"./contracts\";\n\n/** Default line window a read returns when the policy sets no `defaultLines`. */\nconst DEFAULT_READ_LINES = 2000;\n/** Hard ceiling on grep matches returned, so a broad pattern can't flood. */\nconst DEFAULT_MAX_GREP_MATCHES = 1000;\n/** Default per-command output byte cap when the policy sets none. */\nconst DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;\n\n/**\n * Number the lines of `content` `cat -n` style: a right-aligned line\n * number (min width 6), a tab, then the line. `startLine` is the 1-based\n * number of the first line in the window.\n */\nfunction numberLines(content: string, startLine: number): string {\n const lines = content.split(\"\\n\");\n\n return lines\n .map((line, index) => {\n const lineNumber = startLine + index;\n\n return `${String(lineNumber).padStart(6, \" \")}\\t${line}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * Clip a captured stream at `maxBytes` (measured in UTF-8 bytes).\n * Returns the possibly-clipped string plus whether clipping occurred.\n */\nfunction capOutput(value: string, maxBytes: number): { value: string; truncated: boolean } {\n const bytes = Buffer.from(value, \"utf8\");\n\n if (bytes.byteLength <= maxBytes) {\n return { value, truncated: false };\n }\n\n // Slice on a byte boundary; `toString` tolerates a split multi-byte\n // char at the tail by emitting the replacement character, which is\n // acceptable for a truncated diagnostic stream.\n return { value: bytes.subarray(0, maxBytes).toString(\"utf8\"), truncated: true };\n}\n\n/**\n * Minimal glob match over a `/`-separated relative path. Supports `**`\n * (spans separators), `*` (within a segment), and `?` (one non-separator\n * char) — enough for the workspace's `glob`/grep narrowing without\n * pulling in a runtime dependency.\n */\nfunction globToRegExp(glob: string): RegExp {\n let source = \"\";\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index];\n\n if (char === \"*\") {\n if (glob[index + 1] === \"*\") {\n source += \".*\";\n index++;\n\n if (glob[index + 1] === \"/\") {\n index++;\n }\n } else {\n source += \"[^/]*\";\n }\n\n continue;\n }\n\n if (char === \"?\") {\n source += \"[^/]\";\n\n continue;\n }\n\n source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * The internal, single-instance implementation of {@link WorkspaceOps}.\n * Holds the backend + policy and is the one place the jail, command\n * gating, read caps, and the read-before-edit guard are enforced — both\n * the agent-facing tools and the human-facing direct methods funnel\n * through this object, so there is exactly one set of rules.\n */\nclass Ops implements WorkspaceOps {\n public constructor(\n private readonly backend: WorkspaceBackend,\n private readonly policy: WorkspacePolicy,\n ) {}\n\n /**\n * Recursively collect every file under `absDir` as absolute paths,\n * via the backend's `list` (so it works over disk or the in-memory\n * mock). Directories are descended; files are accumulated.\n */\n private async walkFiles(absDir: string): Promise<string[]> {\n const found: string[] = [];\n const entries = await this.backend.list(absDir);\n\n await Promise.all(\n entries.map(async (entry) => {\n // A child is a directory iff listing it succeeds; the backend\n // throws/returns for a file. Probe via `exists` + a list guard.\n const isDir = await this.isDirectory(entry);\n\n if (isDir) {\n const nested = await this.walkFiles(entry);\n found.push(...nested);\n } else {\n found.push(entry);\n }\n }),\n );\n\n return found;\n }\n\n /** Whether an absolute path is a directory, by attempting to list it. */\n private async isDirectory(absPath: string): Promise<boolean> {\n try {\n await this.backend.list(absPath);\n\n return true;\n } catch {\n return false;\n }\n }\n\n public async readFile(\n inputPath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<{ content: string; hash: string; totalLines: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n const raw = await this.backend.readFile(absolutePath);\n\n // Hash the content the backend returned (SHA-256 via @warlock.js/fs),\n // not the on-disk file — the mock backend keeps content in memory,\n // and hashing the returned bytes is what the stale-edit guard later\n // compares against, so it must be the SAME source of truth.\n const hash = fs.hash.string(raw);\n\n const lines = raw.split(\"\\n\");\n const totalLines = lines.length;\n\n const offset = Math.max(1, opts?.offset ?? 1);\n const limit = opts?.limit ?? this.policy.read?.defaultLines ?? DEFAULT_READ_LINES;\n\n const startIndex = offset - 1;\n const window = lines.slice(startIndex, startIndex + limit);\n const content = numberLines(window.join(\"\\n\"), offset);\n\n return { content, hash, totalLines };\n }\n\n public async writeFile(\n inputPath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n // Ensure the parent directory exists before writing the leaf.\n const parent = path.dirname(absolutePath);\n await this.backend.mkdir(parent);\n\n await this.backend.writeFile(absolutePath, content);\n\n return {\n hash: fs.hash.string(content),\n bytesWritten: Buffer.byteLength(content, \"utf8\"),\n };\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n const current = await this.backend.readFile(absolutePath);\n const currentHash = fs.hash.string(current);\n\n // Stale-hash guard: if the caller pinned a hash and the file moved\n // underneath them, refuse so they re-read before clobbering.\n if (input.expectHash !== undefined && input.expectHash !== currentHash) {\n throw new WorkspaceEditError(\n `File \"${input.path}\" changed since it was read; the edit is stale.`,\n {\n type: \"stale-hash\",\n path: relativePath || input.path,\n expectedHash: input.expectHash,\n actualHash: currentHash,\n },\n );\n }\n\n const occurrences = countOccurrences(current, input.oldString);\n\n if (occurrences === 0) {\n throw new WorkspaceEditError(\n `The text to replace was not found in \"${input.path}\".`,\n { type: \"not-found\", path: relativePath || input.path, matches: 0 },\n );\n }\n\n if (occurrences > 1 && !input.replaceAll) {\n throw new WorkspaceEditError(\n `The text to replace is not unique in \"${input.path}\" (${occurrences} matches); ` +\n `pass replaceAll or include more surrounding context.`,\n { type: \"not-unique\", path: relativePath || input.path, matches: occurrences },\n );\n }\n\n const updated = input.replaceAll\n ? current.split(input.oldString).join(input.newString)\n : replaceFirst(current, input.oldString, input.newString);\n\n await this.backend.writeFile(absolutePath, updated);\n\n return {\n path: relativePath || input.path,\n replacements: input.replaceAll ? occurrences : 1,\n hash: fs.hash.string(updated),\n };\n }\n\n public async exec(\n command: string,\n opts?: { timeoutMs?: number },\n ): Promise<RunShellResult> {\n if (!isCommandAllowed(this.policy, command)) {\n throw new WorkspacePolicyError(\n `Command is not permitted by the workspace shell policy: ${command}`,\n { type: \"denied-command\", command },\n );\n }\n\n const shell = this.policy.shell;\n const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;\n const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;\n\n const result = await this.backend.exec(command, {\n cwd: this.policy.cwd,\n timeoutMs,\n env: buildEnv(this.policy),\n });\n\n const stdout = capOutput(result.stdout, maxOutputBytes);\n const stderr = capOutput(result.stderr, maxOutputBytes);\n\n return {\n exitCode: result.exitCode,\n stdout: stdout.value,\n stderr: stderr.value,\n truncated: stdout.truncated || stderr.truncated,\n timedOut: result.timedOut,\n };\n }\n\n public async grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const flags = opts?.ignoreCase ? \"i\" : \"\";\n const regex = new RegExp(pattern, flags);\n const globRegex = opts?.glob ? globToRegExp(opts.glob) : undefined;\n\n const files = await this.walkFiles(jailRoot);\n const matches: GrepMatch[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (globRegex && !globRegex.test(relativePath)) {\n continue;\n }\n\n // Skip files the deny list would block (e.g. `.git/**`).\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n let content: string;\n\n try {\n content = await this.backend.readFile(absFile);\n } catch {\n // Unreadable entry (vanished, binary handle) — skip, don't fail.\n continue;\n }\n\n const lines = content.split(\"\\n\");\n\n for (let index = 0; index < lines.length; index++) {\n if (regex.test(lines[index])) {\n matches.push({ path: relativePath, line: index + 1, text: lines[index] });\n\n if (matches.length >= DEFAULT_MAX_GREP_MATCHES) {\n return { matches, total: matches.length };\n }\n }\n }\n }\n\n return { matches, total: matches.length };\n }\n\n public async glob(pattern: string): Promise<string[]> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const regex = globToRegExp(pattern);\n\n const files = await this.walkFiles(jailRoot);\n const matched: string[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n if (regex.test(relativePath)) {\n matched.push(relativePath);\n }\n }\n\n return matched.sort();\n }\n\n public async exists(inputPath: string): Promise<boolean> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n return this.backend.exists(absolutePath);\n }\n\n public async mkdir(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.mkdir(absolutePath);\n }\n\n public async remove(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.remove(absolutePath);\n }\n\n /** Whether a `/`-separated relative path hits the policy deny list. */\n private isDenied(relativePath: string): boolean {\n const denyPaths = this.policy.denyPaths;\n\n if (!denyPaths || denyPaths.length === 0) {\n return false;\n }\n\n return denyPaths.some((glob) => {\n if (globToRegExp(glob).test(relativePath)) {\n return true;\n }\n\n if (!glob.includes(\"*\") && !glob.includes(\"?\")) {\n const prefix = glob.endsWith(\"/\") ? glob : `${glob}/`;\n\n return relativePath.startsWith(prefix);\n }\n\n return false;\n });\n }\n}\n\n/** Count non-overlapping occurrences of `needle` in `haystack`. */\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle === \"\") {\n return 0;\n }\n\n let count = 0;\n let from = 0;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const index = haystack.indexOf(needle, from);\n\n if (index === -1) {\n break;\n }\n\n count++;\n from = index + needle.length;\n }\n\n return count;\n}\n\n/** Replace the first occurrence of `needle` with `replacement`. */\nfunction replaceFirst(haystack: string, needle: string, replacement: string): string {\n const index = haystack.indexOf(needle);\n\n if (index === -1) {\n return haystack;\n }\n\n return haystack.slice(0, index) + replacement + haystack.slice(index + needle.length);\n}\n\n/**\n * Create the policy-enforced operation layer over a backend.\n *\n * The returned {@link WorkspaceOps} is the single seam both the\n * agent-facing `.tools.*` factories and the human-facing direct methods\n * delegate to — one jail, one command-gate, one read-before-edit guard,\n * regardless of caller. Path inputs are workspace-relative and resolved\n * against `policy.cwd`; escapes and denied commands surface as typed\n * {@link WorkspacePolicyError} / {@link WorkspaceEditError}.\n *\n * @param backend - The dumb IO executor (local disk or in-memory mock).\n * @param policy - The policy that bounds every operation.\n *\n * @example\n * const ops = createOps(localBackend, { cwd: \"/srv/api\", shell: { allow: [\"npm\"] } });\n * const { content, hash } = await ops.readFile(\"src/index.ts\");\n */\nexport function createOps(\n backend: WorkspaceBackend,\n policy: WorkspacePolicy,\n): WorkspaceOps {\n return new Ops(backend, policy);\n}\n","import { spawn } from \"node:child_process\";\nimport { realpath } from \"node:fs/promises\";\nimport { platform } from \"node:process\";\nimport { fs } from \"@warlock.js/fs\";\nimport type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n} from \"../contracts/workspace-backend.contract\";\n\n/**\n * Hard ceiling on captured `stdout`/`stderr` per stream, in bytes. The raw\n * executor truncates each stream once this many bytes have accumulated so a\n * runaway command cannot exhaust memory; the ops layer applies its own\n * policy-driven cap (and the `truncated` flag) on top. Generous enough that\n * ordinary command output is never clipped here.\n */\nconst MAX_STREAM_BYTES = 10 * 1024 * 1024;\n\n/**\n * Append a chunk to a capped list of buffers, tracking the running byte\n * total and stopping once {@link MAX_STREAM_BYTES} is reached. Trailing bytes\n * past the cap are dropped rather than buffered.\n */\nfunction pushCapped(chunks: Buffer[], total: number, chunk: Buffer): number {\n if (total >= MAX_STREAM_BYTES) {\n return total;\n }\n\n const remaining = MAX_STREAM_BYTES - total;\n\n if (chunk.length <= remaining) {\n chunks.push(chunk);\n\n return total + chunk.length;\n }\n\n chunks.push(chunk.subarray(0, remaining));\n\n return MAX_STREAM_BYTES;\n}\n\n/**\n * Force-kill a spawned command and its entire process tree.\n *\n * With `shell: true` the command runs under an intermediary shell\n * (`cmd.exe` on Windows, `/bin/sh` elsewhere), so signalling the direct\n * child only reaps the shell — a long-running grandchild (e.g. `node`)\n * would survive, leaving the `exec` promise unsettled. We therefore kill\n * the whole group:\n * - **Windows** — `taskkill /T /F` walks and terminates the PID's tree.\n * - **POSIX** — the child is spawned `detached`, becoming its own process\n * group leader, so `process.kill(-pid)` SIGKILLs the group.\n */\nfunction killTree(pid: number | undefined, child: { kill(signal: NodeJS.Signals): boolean }): void {\n if (pid === undefined) {\n child.kill(\"SIGKILL\");\n\n return;\n }\n\n if (platform === \"win32\") {\n spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n\n return;\n }\n\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n // The group may already be gone; fall back to the direct child.\n child.kill(\"SIGKILL\");\n }\n}\n\n/**\n * The real-disk executor: every filesystem method delegates to\n * `@warlock.js/fs` (never `node:fs`), and {@link LocalBackend.exec} spawns a\n * process via `node:child_process`. It is deliberately **policy-agnostic** —\n * it receives already-resolved absolute paths and an already-resolved\n * environment + timeout from the ops layer, and just performs the side\n * effect. See {@link WorkspaceBackend} for the contract this implements.\n *\n * Constructed via {@link createLocalBackend}; the class itself is internal.\n */\nclass LocalBackend implements WorkspaceBackend {\n /** Read a file's full UTF-8 content at an absolute path. */\n public async readFile(absPath: string): Promise<string> {\n return fs.files.get(absPath);\n }\n\n /**\n * Write full content to an absolute path. Uses `atomicWriteAsync`, so a\n * concurrent reader never observes a half-written file, and missing parent\n * directories are created.\n */\n public async writeFile(absPath: string, content: string): Promise<void> {\n await fs.files.put(absPath, content, { atomic: true });\n }\n\n /** Whether anything (file or directory) exists at an absolute path. */\n public async exists(absPath: string): Promise<boolean> {\n return fs.exists(absPath);\n }\n\n /** Create a directory (and any missing parents) at an absolute path; idempotent. */\n public async mkdir(absPath: string): Promise<void> {\n await fs.dirs.ensure(absPath);\n }\n\n /**\n * Remove a file or directory tree at an absolute path. Stats the target to\n * pick the right op — `fs.dirs.remove` (recursive) for a directory,\n * `fs.files.remove` for anything else. A path that does not exist is a no-op\n * (the stat's ENOENT short-circuits, and both removes swallow `ENOENT`).\n */\n public async remove(absPath: string): Promise<void> {\n let isDirectory = false;\n\n try {\n isDirectory = (await fs.files.stats(absPath)).type === \"directory\";\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code === \"ENOENT\") {\n return;\n }\n\n throw error;\n }\n\n if (isDirectory) {\n await fs.dirs.remove(absPath);\n\n return;\n }\n\n await fs.files.remove(absPath);\n }\n\n /** List immediate children of an absolute directory as absolute paths. */\n public async list(absDir: string): Promise<string[]> {\n return fs.dirs.list(absDir);\n }\n\n /**\n * Resolve symlinks and `..` segments to a canonical absolute path — the\n * primitive the ops-layer jail uses to detect escapes. Delegates to\n * `node:fs/promises` `realpath`, the one filesystem operation `@warlock.js/fs`\n * does not expose.\n */\n public async realpath(absPath: string): Promise<string> {\n return realpath(absPath);\n }\n\n /**\n * Run a command and capture its outcome. The command line is executed\n * through the platform shell (`shell: true`) so pipes/operators behave as a\n * caller would expect; `cwd`, `env`, and the timeout are taken verbatim from\n * the ops layer (the environment is NOT merged with `process.env`). On\n * timeout the process is SIGKILLed and `timedOut` is set. `stdout`/`stderr`\n * are captured and byte-capped per {@link MAX_STREAM_BYTES}.\n *\n * Never rejects for a non-zero exit, a missing executable, or a timeout —\n * those are reported through the resolved {@link WorkspaceBackendExecResult}\n * so the ops layer can surface them as tool-error data.\n */\n public exec(\n command: string,\n opts: WorkspaceBackendExecOptions = {},\n ): Promise<WorkspaceBackendExecResult> {\n return new Promise<WorkspaceBackendExecResult>((resolve) => {\n const child = spawn(command, {\n cwd: opts.cwd,\n env: opts.env,\n shell: true,\n windowsHide: true,\n // POSIX: own process group so a timeout SIGKILL reaps the whole\n // shell tree, not just the shell. Harmless on Windows (ignored;\n // there we tree-kill via taskkill instead).\n detached: platform !== \"win32\",\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let timedOut = false;\n let settled = false;\n\n const timer =\n opts.timeoutMs !== undefined && opts.timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true;\n killTree(child.pid, child);\n }, opts.timeoutMs)\n : undefined;\n\n const settle = (exitCode: number) => {\n if (settled) {\n return;\n }\n\n settled = true;\n\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n\n resolve({\n exitCode,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut,\n });\n };\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n stdoutBytes = pushCapped(stdoutChunks, stdoutBytes, chunk);\n });\n\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrBytes = pushCapped(stderrChunks, stderrBytes, chunk);\n });\n\n // A spawn failure (e.g. the shell itself is missing) surfaces as an\n // error event with no exit; report it as a conventional shell\n // \"command not found\" exit code rather than rejecting.\n child.on(\"error\", () => {\n settle(127);\n });\n\n child.on(\"close\", (code, signal) => {\n // A null code means the process was terminated by a signal (our\n // timeout SIGKILL, or an external kill). Map that to the POSIX\n // 128 + signal-number convention so callers see a non-zero exit.\n if (code === null) {\n const signalNumber = signal === \"SIGKILL\" ? 9 : 1;\n settle(128 + signalNumber);\n\n return;\n }\n\n settle(code);\n });\n });\n }\n}\n\n/**\n * Create the **local** workspace backend — the default executor that runs the\n * workspace over the real disk (`@warlock.js/fs`) and the local shell\n * (`node:child_process`).\n *\n * The returned object is policy-agnostic: it expects already-jail-resolved\n * absolute paths and an already-resolved environment/timeout from the ops\n * layer. Pair it with {@link WorkspaceOps} for the actual cwd jail,\n * allow/deny lists, hashing, and output policy.\n *\n * @example\n * const backend = createLocalBackend();\n * await backend.writeFile(\"/srv/app/src/index.ts\", \"export const x = 1;\");\n * const { exitCode } = await backend.exec(\"node -v\", { cwd: \"/srv/app\" });\n */\nexport function createLocalBackend(): WorkspaceBackend {\n return new LocalBackend();\n}\n","import type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n} from \"../contracts/workspace-backend.contract\";\n\n/**\n * A scripted outcome for the mock backend's `exec`. Mirrors\n * {@link WorkspaceBackendExecResult} but every field is optional so a\n * registration can specify only what it cares about — the rest fall back\n * to a successful, empty-output, non-timed-out run.\n */\nexport type MockExecResult = Partial<WorkspaceBackendExecResult>;\n\n/**\n * The two seedable inputs of a mock backend:\n *\n * - `files` — initial file tree as `absolutePath -> content`. Parent\n * directories of each seeded file are implicitly created.\n * - `commands` — scripted `exec` outcomes keyed on the **exact** command\n * line. Any command not registered here resolves to a `0`-exit no-op.\n */\nexport interface MockBackendSeed {\n /** Initial file contents, keyed by absolute path. */\n files?: Record<string, string>;\n /** Scripted `exec` results, keyed by exact command line. */\n commands?: Record<string, MockExecResult>;\n}\n\n/**\n * Normalize an absolute path to a stable in-memory key: forward slashes,\n * collapsed duplicate separators, and resolved `.` / `..` segments. There\n * are no symlinks in memory, so this is a pure lexical canonicalization —\n * exactly what the backend's `realpath` promises.\n */\nfunction canonicalize(absPath: string): string {\n const unified = absPath.replace(/\\\\/g, \"/\");\n // Preserve a leading slash (POSIX root) or a `C:`-style Windows drive\n // prefix; everything else is a normal segment.\n const driveMatch = unified.match(/^([a-zA-Z]:)?\\/?/);\n const prefix = driveMatch ? driveMatch[0] : \"\";\n const rest = unified.slice(prefix.length);\n\n const resolved: string[] = [];\n\n for (const segment of rest.split(\"/\")) {\n if (segment === \"\" || segment === \".\") {\n continue;\n }\n\n if (segment === \"..\") {\n resolved.pop();\n\n continue;\n }\n\n resolved.push(segment);\n }\n\n const joined = resolved.join(\"/\");\n\n // A trailing slash on the prefix means it was absolute; keep it so the\n // result stays absolute even when the body is empty (the root itself).\n const normalizedPrefix = prefix.endsWith(\"/\") ? prefix : `${prefix}/`;\n\n return joined.length > 0 ? `${normalizedPrefix}${joined}` : normalizedPrefix;\n}\n\n/** All ancestor directory keys of a canonical path, root-first. */\nfunction ancestorsOf(canonicalPath: string): string[] {\n const lastSlash = canonicalPath.lastIndexOf(\"/\");\n\n if (lastSlash <= 0) {\n return [];\n }\n\n const parent = canonicalPath.slice(0, lastSlash);\n const result = ancestorsOf(parent);\n\n result.push(parent);\n\n return result;\n}\n\n/**\n * The in-memory executor behind {@link createMockBackend}. Holds the file\n * tree in a `Map`, the directory set in a `Set`, and the scripted command\n * table in a second `Map` — no disk, no child processes, fully\n * deterministic. Construct it via the factory, never directly.\n */\nclass MockBackend implements WorkspaceBackend {\n /** Canonical file path -> content. */\n private readonly files = new Map<string, string>();\n\n /** Canonical directory paths that exist (including implicit parents). */\n private readonly directories = new Set<string>();\n\n /** Exact command line -> scripted outcome. */\n private readonly commands = new Map<string, MockExecResult>();\n\n public constructor(seed?: MockBackendSeed) {\n for (const [path, content] of Object.entries(seed?.files ?? {})) {\n const canonical = canonicalize(path);\n\n this.files.set(canonical, content);\n this.registerAncestorDirectories(canonical);\n }\n\n for (const [command, result] of Object.entries(seed?.commands ?? {})) {\n this.commands.set(command, result);\n }\n }\n\n /** Record every ancestor directory of a path as existing. */\n private registerAncestorDirectories(canonicalPath: string): void {\n for (const ancestor of ancestorsOf(canonicalPath)) {\n this.directories.add(ancestor);\n }\n }\n\n public async readFile(absPath: string): Promise<string> {\n const canonical = canonicalize(absPath);\n const content = this.files.get(canonical);\n\n if (content === undefined) {\n throw new Error(`Mock backend: no such file: ${canonical}`);\n }\n\n return content;\n }\n\n public async writeFile(absPath: string, content: string): Promise<void> {\n const canonical = canonicalize(absPath);\n\n this.files.set(canonical, content);\n this.registerAncestorDirectories(canonical);\n }\n\n public async exists(absPath: string): Promise<boolean> {\n const canonical = canonicalize(absPath);\n\n return this.files.has(canonical) || this.directories.has(canonical);\n }\n\n public async mkdir(absPath: string): Promise<void> {\n const canonical = canonicalize(absPath);\n\n this.directories.add(canonical);\n this.registerAncestorDirectories(canonical);\n }\n\n public async remove(absPath: string): Promise<void> {\n const canonical = canonicalize(absPath);\n const prefix = `${canonical}/`;\n\n // Drop the entry itself and any descendant files/directories — a\n // recursive tree removal, matching the contract's \"file or directory\n // tree\" wording.\n for (const file of [...this.files.keys()]) {\n if (file === canonical || file.startsWith(prefix)) {\n this.files.delete(file);\n }\n }\n\n for (const directory of [...this.directories]) {\n if (directory === canonical || directory.startsWith(prefix)) {\n this.directories.delete(directory);\n }\n }\n }\n\n public async list(absDir: string): Promise<string[]> {\n const canonical = canonicalize(absDir);\n const prefix = canonical === \"/\" ? \"/\" : `${canonical}/`;\n const children = new Set<string>();\n\n const collect = (key: string): void => {\n if (!key.startsWith(prefix) || key === canonical) {\n return;\n }\n\n const remainder = key.slice(prefix.length);\n const nextSlash = remainder.indexOf(\"/\");\n const childName =\n nextSlash === -1 ? remainder : remainder.slice(0, nextSlash);\n\n if (childName.length > 0) {\n children.add(`${prefix}${childName}`);\n }\n };\n\n for (const file of this.files.keys()) {\n collect(file);\n }\n\n for (const directory of this.directories) {\n collect(directory);\n }\n\n return [...children].sort();\n }\n\n public async realpath(absPath: string): Promise<string> {\n return canonicalize(absPath);\n }\n\n public async exec(\n command: string,\n _opts?: WorkspaceBackendExecOptions,\n ): Promise<WorkspaceBackendExecResult> {\n const scripted = this.commands.get(command);\n\n return {\n exitCode: scripted?.exitCode ?? 0,\n stdout: scripted?.stdout ?? \"\",\n stderr: scripted?.stderr ?? \"\",\n timedOut: scripted?.timedOut ?? false,\n };\n }\n}\n\n/**\n * Create an in-memory {@link WorkspaceBackend} for fast, disk-free tests.\n *\n * Every IO method operates on a `Map` of `absolutePath -> content` (with a\n * companion directory set), so reads, writes, existence checks, `mkdir`,\n * recursive `remove`, `list`, and `realpath` all run synchronously in\n * memory with no filesystem access. `exec` is **scripted**: a registered\n * `command -> result` table is consulted by exact command line, and any\n * unregistered command resolves to a successful `0`-exit no-op with empty\n * output.\n *\n * `realpath` is a pure lexical canonicalization (forward slashes,\n * collapsed separators, resolved `.`/`..`) — there are no symlinks in\n * memory — which is exactly what the jail resolver expects.\n *\n * The single positional `seed` accepts either the shorthand\n * `Record<string, string>` of file contents (matching the design's\n * `ai.workspace.mock(seed)` signature) or the richer {@link MockBackendSeed}\n * with both `files` and scripted `commands`.\n *\n * @example\n * // Shorthand: seed files only.\n * const backend = createMockBackend({ \"/srv/app/src/index.ts\": \"export const x = 1;\" });\n * await backend.readFile(\"/srv/app/src/index.ts\"); // \"export const x = 1;\"\n *\n * @example\n * // Full seed: files plus a scripted command.\n * const backend = createMockBackend({\n * files: { \"/srv/app/package.json\": \"{}\" },\n * commands: { \"npm test\": { exitCode: 1, stderr: \"1 failing\" } },\n * });\n * await backend.exec(\"npm test\"); // { exitCode: 1, stderr: \"1 failing\", ... }\n * await backend.exec(\"echo hi\"); // { exitCode: 0, stdout: \"\", ... } (no-op)\n */\nexport function createMockBackend(\n seed?: Record<string, string> | MockBackendSeed,\n): WorkspaceBackend {\n return new MockBackend(normalizeSeed(seed));\n}\n\n/** Coerce the dual-shaped `seed` argument into a {@link MockBackendSeed}. */\nfunction normalizeSeed(\n seed?: Record<string, string> | MockBackendSeed,\n): MockBackendSeed | undefined {\n if (seed === undefined) {\n return undefined;\n }\n\n if (isMockBackendSeed(seed)) {\n return seed;\n }\n\n return { files: seed };\n}\n\n/**\n * Whether `seed` is the structured {@link MockBackendSeed} (has a `files`\n * or `commands` key) rather than the flat `path -> content` shorthand. A\n * plain shorthand map whose only key happens to be named `files` is\n * treated as structured — callers wanting that literal path should use the\n * explicit `{ files: { files: \"...\" } }` form.\n */\nfunction isMockBackendSeed(\n seed: Record<string, string> | MockBackendSeed,\n): seed is MockBackendSeed {\n return (\n typeof (seed as MockBackendSeed).files === \"object\" ||\n typeof (seed as MockBackendSeed).commands === \"object\"\n );\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Tiny, dependency-free [Standard Schema](https://standardschema.dev)\n * builders for the workspace tools' input validation. The package pins\n * only `@warlock.js/ai` and `@warlock.js/fs` as runtime dependencies, so\n * rather than pull in a schema library we hand-roll the few shapes the\n * file tools need — exactly the pattern `@warlock.js/ai`'s own `tool()`\n * tests use. Each builder returns a `StandardSchemaV1`, which is what\n * `tool({ input })` validates against before calling `execute`.\n *\n * These intentionally cover only the primitive cases the FILE tools\n * require (`string`, `optional string`, `optional number`, `optional\n * boolean`, and an `object` of fields). They are not a general-purpose\n * validator.\n */\n\n/** The vendor tag stamped on every issue these builders produce. */\nconst VENDOR = \"ai-workspace\";\n\n/**\n * A single field validator inside {@link objectSchema}: given a value,\n * return either the coerced value or a list of issues. Field validators\n * receive the raw property and the property name (for issue messages).\n */\ntype FieldValidator<T> = (\n value: unknown,\n key: string,\n) => { value: T } | { issues: StandardSchemaV1.Issue[] };\n\n/** Required string field — rejects anything that is not a string. */\nexport function stringField(): FieldValidator<string> {\n return (value, key) => {\n if (typeof value === \"string\") {\n return { value };\n }\n\n return { issues: [{ message: `\"${key}\" must be a string`, path: [key] }] };\n };\n}\n\n/**\n * Optional string field — accepts `undefined` (the property absent or\n * explicitly undefined) or a string, and rejects every other type.\n */\nexport function optionalStringField(): FieldValidator<string | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"string\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a string when provided`, path: [key] }],\n };\n };\n}\n\n/**\n * Optional finite-number field — accepts `undefined` or a finite number,\n * rejecting `NaN`/`Infinity` and non-number types.\n */\nexport function optionalNumberField(): FieldValidator<number | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return { value };\n }\n\n return {\n issues: [\n { message: `\"${key}\" must be a finite number when provided`, path: [key] },\n ],\n };\n };\n}\n\n/** Optional boolean field — accepts `undefined` or a boolean. */\nexport function optionalBooleanField(): FieldValidator<boolean | undefined> {\n return (value, key) => {\n if (value === undefined) {\n return { value: undefined };\n }\n\n if (typeof value === \"boolean\") {\n return { value };\n }\n\n return {\n issues: [{ message: `\"${key}\" must be a boolean when provided`, path: [key] }],\n };\n };\n}\n\n/** The per-key field validator map describing an object schema's shape. */\ntype ObjectShape<T> = {\n [K in keyof T]-?: FieldValidator<T[K]>;\n};\n\n/**\n * Build a {@link StandardSchemaV1} for a flat object whose every property\n * is validated by a {@link FieldValidator}. The input must be a non-null\n * object; each declared field is validated and the (possibly coerced)\n * values are collected into the typed result. All field issues are merged\n * so the caller sees every problem at once.\n *\n * `T` is constrained to `object` rather than `Record<string, unknown>` so\n * the tool IO `interface`s (which carry no implicit string index\n * signature) satisfy it directly — only the declared keys in `shape` are\n * ever read, so a string index signature is never required.\n *\n * @example\n * const schema = objectSchema<{ path: string; limit?: number }>({\n * path: stringField(),\n * limit: optionalNumberField(),\n * });\n */\nexport function objectSchema<T extends object>(\n shape: ObjectShape<T>,\n): StandardSchemaV1<T> {\n return {\n \"~standard\": {\n version: 1,\n vendor: VENDOR,\n validate(input) {\n if (typeof input !== \"object\" || input === null || Array.isArray(input)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const source = input as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n const result: Record<string, unknown> = {};\n\n for (const key of Object.keys(shape) as (keyof T)[]) {\n const field = shape[key];\n const outcome = field(source[key as string], key as string);\n\n if (\"issues\" in outcome) {\n issues.push(...outcome.issues);\n\n continue;\n }\n\n // Only carry through keys that resolved to a defined value, so\n // optional-absent fields stay absent rather than becoming\n // explicit `undefined` properties.\n if (outcome.value !== undefined) {\n result[key as string] = outcome.value;\n }\n }\n\n if (issues.length > 0) {\n return { issues };\n }\n\n return { value: result as T };\n },\n },\n };\n}\n","import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport {\n objectSchema,\n optionalBooleanField,\n optionalStringField,\n stringField,\n} from \"./schema\";\nimport type { EditFileInput, EditFileResult, WorkspaceOps } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"edit_file\";\n\n/** Input schema for the `edit_file` tool. */\nconst inputSchema = objectSchema<EditFileInput>({\n path: stringField(),\n oldString: stringField(),\n newString: stringField(),\n replaceAll: optionalBooleanField(),\n expectHash: optionalStringField(),\n});\n\n/**\n * Build the agent-facing `edit_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, oldString, newString, replaceAll?,\n * expectHash? }` against a Standard Schema, then delegates to\n * `ops.editFile`, which applies the exact-string replacement under the\n * read-before-edit guard and returns the replacement count plus the\n * post-edit `hash`.\n *\n * **Errors flow as data.** A non-unique `oldString` (without\n * `replaceAll`), a missing `oldString`, or a stale `expectHash` cause\n * `ops` to throw a `WorkspaceEditError`; the `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field — `invoke()` never\n * throws — so the agent can re-read and retry.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const editTool = makeEditFileTool(ops);\n * const { data, error } = await editTool.invoke({\n * path: \"src/index.ts\",\n * oldString: \"const a = 1;\",\n * newString: \"const a = 2;\",\n * expectHash,\n * });\n * if (error) console.warn(error.message); // e.g. stale-hash → re-read\n */\nexport function makeEditFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<EditFileInput, EditFileResult> {\n return tool<EditFileInput, EditFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Replace an exact substring in a workspace file. oldString must match \" +\n \"uniquely unless replaceAll is set. Pass expectHash (from read_file) to \" +\n \"reject the edit if the file changed since you read it. Returns the \" +\n \"number of replacements and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n return ops.editFile(input);\n },\n });\n}\n","import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type { GlobInput, GlobResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGlobTool} to customize the vended tool. */\nexport interface MakeGlobToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"glob\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * path-match surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GlobInput} — a single required `pattern`\n * string. Built on the package's shared, dependency-free schema builders\n * (no schema library, matching the validator idiom the `@warlock.js/ai`\n * tool runtime expects).\n */\nconst globInputSchema = objectSchema<GlobInput>({\n pattern: stringField(),\n});\n\n/**\n * Build the agent-facing `glob` tool — resolve a glob pattern to the\n * matching workspace-relative paths within the jail. The returned\n * {@link ToolContract} validates the LLM's arguments, delegates to\n * {@link WorkspaceOps.glob} (which returns a bare sorted `string[]`), and\n * wraps the result in a {@link GlobResult} so the agent always reads a\n * stable `{ paths }` envelope. The jail and `denyPaths` filtering are\n * enforced in the shared ops layer; a policy violation surfaces as typed\n * tool-error *data* via the runtime's `invoke()` wrapper.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `glob`.\n *\n * @example\n * const glob = makeGlobTool(ops);\n * const { data } = await glob.invoke({ pattern: \"src/models/**\\/*.ts\" });\n * console.log(data?.paths);\n */\nexport function makeGlobTool(\n ops: WorkspaceOps,\n options?: MakeGlobToolOptions,\n): ToolContract<GlobInput, GlobResult> {\n return tool<GlobInput, GlobResult>({\n name: options?.name ?? \"glob\",\n description:\n \"Find files in the workspace whose path matches a glob pattern \" +\n \"(supports `*`, `**`, and `?`). Returns the matching \" +\n \"workspace-relative paths, sorted.\",\n action: (input) => `Finding files matching ${input.pattern}`,\n input: globInputSchema,\n async execute(input) {\n const paths = await ops.glob(input.pattern);\n\n return { paths };\n },\n });\n}\n","import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport {\n objectSchema,\n optionalBooleanField,\n optionalStringField,\n stringField,\n} from \"./schema\";\nimport type { GrepInput, GrepResult, WorkspaceOps } from \"../contracts\";\n\n/** Options accepted by {@link makeGrepTool} to customize the vended tool. */\nexport interface MakeGrepToolOptions {\n /**\n * Override the tool name the LLM sees. Defaults to `\"grep\"`. Use a\n * custom name when wiring several workspaces into one agent so each\n * search surface is addressable.\n */\n name?: string;\n}\n\n/**\n * Standard Schema for {@link GrepInput} — `pattern` is a required string;\n * `glob` and `ignoreCase` are optional. Built on the package's shared,\n * dependency-free schema builders (no schema library, matching the\n * validator idiom the `@warlock.js/ai` tool runtime expects).\n */\nconst grepInputSchema = objectSchema<GrepInput>({\n pattern: stringField(),\n glob: optionalStringField(),\n ignoreCase: optionalBooleanField(),\n});\n\n/**\n * Build the agent-facing `grep` tool — a regex content search across the\n * jailed file set. The returned {@link ToolContract} validates the LLM's\n * arguments, then delegates verbatim to {@link WorkspaceOps.grep}, so the\n * policy jail, `denyPaths` filtering, and match cap are enforced in the\n * single shared ops layer rather than duplicated here. A policy violation\n * (e.g. a jail-resolution failure) surfaces as typed tool-error *data*\n * via the runtime's `invoke()` wrapper, never as a thrown run-killer.\n *\n * @param ops - The policy-enforced operation layer to delegate to.\n * @param options - Optional `{ name }` override for the vended tool name.\n * @returns A {@link ToolContract} the agent can call as `grep`.\n *\n * @example\n * const grep = makeGrepTool(ops);\n * const { data } = await grep.invoke({ pattern: \"TODO\", glob: \"src/*.ts\" });\n * console.log(data?.total, data?.matches);\n */\nexport function makeGrepTool(\n ops: WorkspaceOps,\n options?: MakeGrepToolOptions,\n): ToolContract<GrepInput, GrepResult> {\n return tool<GrepInput, GrepResult>({\n name: options?.name ?? \"grep\",\n description:\n \"Search file contents across the workspace for a regular-expression \" +\n \"pattern. Optionally narrow the scanned files with a glob and match \" +\n \"case-insensitively. Returns every matching line with its file path \" +\n \"and 1-based line number.\",\n action: (input) => `Searching for /${input.pattern}/`,\n input: grepInputSchema,\n async execute(input) {\n return ops.grep(input.pattern, {\n glob: input.glob,\n ignoreCase: input.ignoreCase,\n });\n },\n });\n}\n","import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, optionalNumberField, stringField } from \"./schema\";\nimport type { ReadFileInput, ReadFileResult, WorkspaceOps } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"read_file\";\n\n/** Input schema for the `read_file` tool. */\nconst inputSchema = objectSchema<ReadFileInput>({\n path: stringField(),\n startLine: optionalNumberField(),\n limit: optionalNumberField(),\n});\n\n/**\n * Build the agent-facing `read_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, startLine?, limit? }` against a Standard\n * Schema, then delegates to `ops.readFile`, mapping the result into the\n * agent wire shape {@link ReadFileResult} — the `hash` an agent must\n * carry into a later `edit_file` (read-before-edit), plus the `startLine`\n * / `endLine` / `truncated` window metadata derived from the requested\n * range and the file's `totalLines`.\n *\n * **Errors flow as data.** Policy violations (a jail escape) are thrown\n * by `ops`; the `tool()` wrapper catches them and surfaces them in the\n * returned `{ error }` field — `invoke()` never throws — so the agent can\n * read the failure and self-correct.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const readTool = makeReadFileTool(ops);\n * const { data, error } = await readTool.invoke({ path: \"src/index.ts\" });\n * if (!error) console.log(data.hash); // feed into edit_file's expectHash\n */\nexport function makeReadFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<ReadFileInput, ReadFileResult> {\n return tool<ReadFileInput, ReadFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Read a file from the workspace, returning a numbered line window plus \" +\n \"the file's content hash. Pass the hash to edit_file's expectHash to \" +\n \"guard against editing a stale version. Use startLine/limit to page \" +\n \"through large files.\",\n input: inputSchema,\n async execute(input) {\n const startLine = input.startLine !== undefined ? Math.max(1, input.startLine) : 1;\n const { content, hash, totalLines } = await ops.readFile(input.path, {\n offset: startLine,\n limit: input.limit,\n });\n\n // The window's last line is the start plus however many lines the\n // ops layer actually returned (it caps at `limit` / the policy\n // default), bounded by the file's end.\n const returnedLines = content.length === 0 ? 0 : content.split(\"\\n\").length;\n const endLine = Math.min(totalLines, startLine + Math.max(returnedLines, 1) - 1);\n const truncated = endLine < totalLines;\n\n return { content, startLine, endLine, totalLines, truncated, hash };\n },\n });\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n RunShellInput,\n RunShellResult,\n WorkspaceOps,\n} from \"../contracts\";\n\n/** The default tool name `run_shell` is exposed to the LLM under. */\nconst DEFAULT_RUN_SHELL_TOOL_NAME = \"run_shell\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunShellInput}. We validate the\n * model's arguments without a runtime schema dependency: `command` must be\n * a non-empty string, and `timeoutMs` (when present) a positive number.\n * Invalid args surface as a `SchemaValidationError` in the tool result's\n * `error` field rather than reaching `ops.exec`.\n */\nconst runShellInputSchema: StandardSchemaV1<RunShellInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n if (typeof value !== \"object\" || value === null) {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.command !== \"string\" || candidate.command.length === 0) {\n return { issues: [{ message: \"command must be a non-empty string\", path: [\"command\"] }] };\n }\n\n if (\n candidate.timeoutMs !== undefined &&\n (typeof candidate.timeoutMs !== \"number\" || candidate.timeoutMs <= 0)\n ) {\n return {\n issues: [{ message: \"timeoutMs must be a positive number\", path: [\"timeoutMs\"] }],\n };\n }\n\n const result: RunShellInput = { command: candidate.command };\n\n if (candidate.timeoutMs !== undefined) {\n result.timeoutMs = candidate.timeoutMs as number;\n }\n\n return { value: result };\n },\n },\n};\n\n/** Options for {@link makeRunShellTool}. */\nexport interface MakeRunShellToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_shell\"`). */\n name?: string;\n}\n\n/**\n * Build the `run_shell` tool — a {@link ToolContract} that runs a single\n * shell command through the policy-enforced {@link WorkspaceOps} layer.\n *\n * The command's leading executable basename is gated against the shell\n * allow/deny policy by `ops.exec`; a blocked command throws a\n * `WorkspacePolicyError` which the `tool()` runtime catches and surfaces\n * in the result's `error` field (never a thrown run-killer), so the agent\n * reads the refusal as tool data and self-corrects. A command that runs\n * but exits non-zero is *not* an error — its `exitCode`/`stderr` come back\n * in `data` for the agent to inspect.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Optional tool-name override.\n *\n * @example\n * const runShell = makeRunShellTool(ops);\n * const { data, error } = await runShell.invoke({ command: \"npm run build\" });\n * if (error) handleDenied(error);\n * else console.log(data.exitCode, data.stdout);\n */\nexport function makeRunShellTool(\n ops: WorkspaceOps,\n options?: MakeRunShellToolOptions,\n): ToolContract<RunShellInput, RunShellResult> {\n return tool<RunShellInput, RunShellResult>({\n name: options?.name ?? DEFAULT_RUN_SHELL_TOOL_NAME,\n description:\n \"Run a single shell command inside the workspace. The command's \" +\n \"executable must be permitted by the shell policy; output is \" +\n \"byte-capped and the run is time-limited. A non-zero exit code is \" +\n \"returned as data, not an error.\",\n action: (input) => `Running \\`${input.command}\\``,\n input: runShellInputSchema,\n execute: (input) => ops.exec(input.command, { timeoutMs: input.timeoutMs }),\n });\n}\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n RunShellResult,\n RunTestsInput,\n WorkspaceOps,\n} from \"../contracts\";\n\n/** The default tool name `run_tests` is exposed to the LLM under. */\nconst DEFAULT_RUN_TESTS_TOOL_NAME = \"run_tests\";\n\n/** The default command run when no `command` override is configured. */\nconst DEFAULT_TEST_COMMAND = \"npm test\";\n\n/**\n * Hand-rolled Standard Schema for {@link RunTestsInput}. `pattern` is the\n * only field and is optional; when present it must be a string. Validation\n * happens without a runtime schema dependency, mirroring the wider tool\n * layer.\n */\nconst runTestsInputSchema: StandardSchemaV1<RunTestsInput> = {\n \"~standard\": {\n version: 1,\n vendor: \"@warlock.js/ai-workspace\",\n validate: (value) => {\n // A no-argument call (the common case) is valid and runs the bare\n // test command.\n if (value === undefined || value === null) {\n return { value: {} };\n }\n\n if (typeof value !== \"object\") {\n return { issues: [{ message: \"expected an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (candidate.pattern !== undefined && typeof candidate.pattern !== \"string\") {\n return { issues: [{ message: \"pattern must be a string\", path: [\"pattern\"] }] };\n }\n\n const result: RunTestsInput = {};\n\n if (candidate.pattern !== undefined) {\n result.pattern = candidate.pattern as string;\n }\n\n return { value: result };\n },\n },\n};\n\n/** Options for {@link makeRunTestsTool}. */\nexport interface MakeRunTestsToolOptions {\n /** Override the tool name exposed to the LLM (default `\"run_tests\"`). */\n name?: string;\n /**\n * The base test command to run (default `\"npm test\"`). When the model\n * supplies a `pattern`, it is appended to this command.\n */\n command?: string;\n}\n\n/**\n * Build the `run_tests` tool — a {@link ToolContract} convenience over\n * `run_shell` that runs the workspace's configured test command through\n * the policy-enforced {@link WorkspaceOps} layer.\n *\n * The base command defaults to `\"npm test\"` and can be overridden via\n * `options.command`. When the model passes a `pattern`, it is appended to\n * the command as a path/suite filter forwarded to the runner (e.g.\n * `\"npm test src/cart\"`). Like `run_shell`, the resolved command's\n * executable is gated by the shell policy — a denial surfaces in the\n * result's `error` field — and a non-zero exit (failing tests) comes back\n * as `data` for the agent to read and fix.\n *\n * @param ops - The policy-enforced operation layer to delegate `exec` to.\n * @param options - Optional tool-name and base-command overrides.\n *\n * @example\n * const runTests = makeRunTestsTool(ops, { command: \"pnpm test\" });\n * const { data } = await runTests.invoke({ pattern: \"cart-total\" });\n * if (data.exitCode !== 0) inspect(data.stderr);\n */\nexport function makeRunTestsTool(\n ops: WorkspaceOps,\n options?: MakeRunTestsToolOptions,\n): ToolContract<RunTestsInput, RunShellResult> {\n const baseCommand = options?.command ?? DEFAULT_TEST_COMMAND;\n\n return tool<RunTestsInput, RunShellResult>({\n name: options?.name ?? DEFAULT_RUN_TESTS_TOOL_NAME,\n description:\n \"Run the workspace's test suite, optionally narrowed to a path or \" +\n \"name pattern forwarded to the test runner. Failing tests return a \" +\n \"non-zero exit code as data, not an error.\",\n action: (input) =>\n input.pattern ? `Running tests matching \"${input.pattern}\"` : \"Running tests\",\n input: runTestsInputSchema,\n execute: (input) => {\n const command = input.pattern ? `${baseCommand} ${input.pattern}` : baseCommand;\n\n return ops.exec(command);\n },\n });\n}\n","import { tool, type ToolContract } from \"@warlock.js/ai\";\nimport { objectSchema, stringField } from \"./schema\";\nimport type { WorkspaceOps, WriteFileInput, WriteFileResult } from \"../contracts\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"write_file\";\n\n/** Input schema for the `write_file` tool. */\nconst inputSchema = objectSchema<WriteFileInput>({\n path: stringField(),\n content: stringField(),\n});\n\n/**\n * Build the agent-facing `write_file` tool over a workspace's policy-\n * enforced {@link WorkspaceOps}.\n *\n * The tool validates `{ path, content }` against a Standard Schema, then\n * delegates to `ops.writeFile`, which atomically writes the full content\n * (creating parent directories) and returns the byte count and content\n * `hash`. The tool re-attaches the workspace-relative `path` so the\n * result matches the {@link WriteFileResult} wire shape.\n *\n * **Errors flow as data.** A jail escape is thrown by `ops`; the\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field — `invoke()` never throws.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const writeTool = makeWriteFileTool(ops);\n * const { data } = await writeTool.invoke({ path: \"src/new.ts\", content: \"export {};\" });\n * console.log(data.bytesWritten, data.hash);\n */\nexport function makeWriteFileTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<WriteFileInput, WriteFileResult> {\n return tool<WriteFileInput, WriteFileResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Write full content to a workspace file, creating it (and any parent \" +\n \"directories) if absent and overwriting it otherwise. The write is \" +\n \"atomic. Returns the bytes written and the new content hash.\",\n input: inputSchema,\n async execute(input) {\n const { hash, bytesWritten } = await ops.writeFile(input.path, input.content);\n\n return { path: input.path, bytesWritten, hash };\n },\n });\n}\n","import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n EditFileInput,\n EditFileResult,\n GrepResult,\n RunShellResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"writeFile\",\n \"runShell\",\n \"runTests\",\n \"grep\",\n \"glob\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * non-mutating tools only. `editFile` / `writeFile` / `runShell` /\n * `runTests` are deliberately omitted so a reviewer agent has no path to\n * change the tree.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"grep\",\n \"glob\",\n];\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy);\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(\n contract: ToolContract<TInput, TOutput>,\n ): ToolContract => contract as unknown as ToolContract;\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n runShell: (opts) => erase(makeRunShellTool(this.ops, opts)),\n runTests: (opts) => erase(makeRunTestsTool(this.ops, opts)),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n return {\n all: () =>\n ALL_TOOL_NAMES.filter((name) => this.allowedTools.has(name)).map((name) =>\n build(name),\n ),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<{ content: string; hash: string; totalLines: number }> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async exec(command: string, opts?: { timeoutMs?: number }): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string): Promise<string[]> {\n return this.ops.glob(pattern);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read/grep/glob\n * tools are vended and every mutating direct method rejects with a\n * {@link WorkspacePolicyError}. A fresh ops/backend is built from the\n * identical policy, so the projection sees the same jailed tree.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(this.policy, READONLY_TOOL_NAMES);\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the seven agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `writeFile` / `runShell` /\n * `runTests` / `grep` / `glob`, plus `all()` (every tool) and\n * `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` / `exec` /\n * `grep` / `glob` / `exists` / `mkdir` / `remove`, each delegating 1:1\n * to the shared ops layer.\n * - **`readonly()`** — a projection that vends only read/grep/glob and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/backends/local.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"local.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/backends/local.ts"],"mappings":";;;;;AAsQA;;;;AAAsD;;;;;;;;;iBAAtC,kBAAA,IAAsB,gBAAgB"}
|
package/esm/backends/local.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { fs } from "@warlock.js/fs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { platform } from "node:process";
|
|
5
5
|
|
|
@@ -72,7 +72,7 @@ function killTree(pid, child) {
|
|
|
72
72
|
var LocalBackend = class {
|
|
73
73
|
/** Read a file's full UTF-8 content at an absolute path. */
|
|
74
74
|
async readFile(absPath) {
|
|
75
|
-
return
|
|
75
|
+
return fs.files.get(absPath);
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
78
|
* Write full content to an absolute path. Uses `atomicWriteAsync`, so a
|
|
@@ -80,39 +80,39 @@ var LocalBackend = class {
|
|
|
80
80
|
* directories are created.
|
|
81
81
|
*/
|
|
82
82
|
async writeFile(absPath, content) {
|
|
83
|
-
await
|
|
83
|
+
await fs.files.put(absPath, content, { atomic: true });
|
|
84
84
|
}
|
|
85
85
|
/** Whether anything (file or directory) exists at an absolute path. */
|
|
86
86
|
async exists(absPath) {
|
|
87
|
-
return
|
|
87
|
+
return fs.exists(absPath);
|
|
88
88
|
}
|
|
89
89
|
/** Create a directory (and any missing parents) at an absolute path; idempotent. */
|
|
90
90
|
async mkdir(absPath) {
|
|
91
|
-
await
|
|
91
|
+
await fs.dirs.ensure(absPath);
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* Remove a file or directory tree at an absolute path. Stats the target to
|
|
95
|
-
* pick the right
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* pick the right op — `fs.dirs.remove` (recursive) for a directory,
|
|
96
|
+
* `fs.files.remove` for anything else. A path that does not exist is a no-op
|
|
97
|
+
* (the stat's ENOENT short-circuits, and both removes swallow `ENOENT`).
|
|
98
98
|
*/
|
|
99
99
|
async remove(absPath) {
|
|
100
100
|
let isDirectory = false;
|
|
101
101
|
try {
|
|
102
|
-
isDirectory = (await
|
|
102
|
+
isDirectory = (await fs.files.stats(absPath)).type === "directory";
|
|
103
103
|
} catch (error) {
|
|
104
104
|
if (error?.code === "ENOENT") return;
|
|
105
105
|
throw error;
|
|
106
106
|
}
|
|
107
107
|
if (isDirectory) {
|
|
108
|
-
await
|
|
108
|
+
await fs.dirs.remove(absPath);
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
|
-
await
|
|
111
|
+
await fs.files.remove(absPath);
|
|
112
112
|
}
|
|
113
113
|
/** List immediate children of an absolute directory as absolute paths. */
|
|
114
114
|
async list(absDir) {
|
|
115
|
-
return
|
|
115
|
+
return fs.dirs.list(absDir);
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
118
|
* Resolve symlinks and `..` segments to a canonical absolute path — the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/backends/local.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { realpath } from \"node:fs/promises\";\nimport { platform } from \"node:process\";\nimport {\n atomicWriteAsync,\n ensureDirectoryAsync,\n getFileAsync,\n listAsync,\n pathExistsAsync,\n removeDirectoryAsync,\n statsAsync,\n unlinkAsync,\n} from \"@warlock.js/fs\";\nimport type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n} from \"../contracts/workspace-backend.contract\";\n\n/**\n * Hard ceiling on captured `stdout`/`stderr` per stream, in bytes. The raw\n * executor truncates each stream once this many bytes have accumulated so a\n * runaway command cannot exhaust memory; the ops layer applies its own\n * policy-driven cap (and the `truncated` flag) on top. Generous enough that\n * ordinary command output is never clipped here.\n */\nconst MAX_STREAM_BYTES = 10 * 1024 * 1024;\n\n/**\n * Append a chunk to a capped list of buffers, tracking the running byte\n * total and stopping once {@link MAX_STREAM_BYTES} is reached. Trailing bytes\n * past the cap are dropped rather than buffered.\n */\nfunction pushCapped(chunks: Buffer[], total: number, chunk: Buffer): number {\n if (total >= MAX_STREAM_BYTES) {\n return total;\n }\n\n const remaining = MAX_STREAM_BYTES - total;\n\n if (chunk.length <= remaining) {\n chunks.push(chunk);\n\n return total + chunk.length;\n }\n\n chunks.push(chunk.subarray(0, remaining));\n\n return MAX_STREAM_BYTES;\n}\n\n/**\n * Force-kill a spawned command and its entire process tree.\n *\n * With `shell: true` the command runs under an intermediary shell\n * (`cmd.exe` on Windows, `/bin/sh` elsewhere), so signalling the direct\n * child only reaps the shell — a long-running grandchild (e.g. `node`)\n * would survive, leaving the `exec` promise unsettled. We therefore kill\n * the whole group:\n * - **Windows** — `taskkill /T /F` walks and terminates the PID's tree.\n * - **POSIX** — the child is spawned `detached`, becoming its own process\n * group leader, so `process.kill(-pid)` SIGKILLs the group.\n */\nfunction killTree(pid: number | undefined, child: { kill(signal: NodeJS.Signals): boolean }): void {\n if (pid === undefined) {\n child.kill(\"SIGKILL\");\n\n return;\n }\n\n if (platform === \"win32\") {\n spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n\n return;\n }\n\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n // The group may already be gone; fall back to the direct child.\n child.kill(\"SIGKILL\");\n }\n}\n\n/**\n * The real-disk executor: every filesystem method delegates to\n * `@warlock.js/fs` (never `node:fs`), and {@link LocalBackend.exec} spawns a\n * process via `node:child_process`. It is deliberately **policy-agnostic** —\n * it receives already-resolved absolute paths and an already-resolved\n * environment + timeout from the ops layer, and just performs the side\n * effect. See {@link WorkspaceBackend} for the contract this implements.\n *\n * Constructed via {@link createLocalBackend}; the class itself is internal.\n */\nclass LocalBackend implements WorkspaceBackend {\n /** Read a file's full UTF-8 content at an absolute path. */\n public async readFile(absPath: string): Promise<string> {\n return getFileAsync(absPath);\n }\n\n /**\n * Write full content to an absolute path. Uses `atomicWriteAsync`, so a\n * concurrent reader never observes a half-written file, and missing parent\n * directories are created.\n */\n public async writeFile(absPath: string, content: string): Promise<void> {\n await atomicWriteAsync(absPath, content);\n }\n\n /** Whether anything (file or directory) exists at an absolute path. */\n public async exists(absPath: string): Promise<boolean> {\n return pathExistsAsync(absPath);\n }\n\n /** Create a directory (and any missing parents) at an absolute path; idempotent. */\n public async mkdir(absPath: string): Promise<void> {\n await ensureDirectoryAsync(absPath);\n }\n\n /**\n * Remove a file or directory tree at an absolute path. Stats the target to\n * pick the right primitive — `removeDirectoryAsync` (recursive) for a\n * directory, `unlinkAsync` for anything else. A path that does not exist is\n * a no-op (both primitives swallow `ENOENT`).\n */\n public async remove(absPath: string): Promise<void> {\n let isDirectory = false;\n\n try {\n isDirectory = (await statsAsync(absPath)).isDirectory();\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code === \"ENOENT\") {\n return;\n }\n\n throw error;\n }\n\n if (isDirectory) {\n await removeDirectoryAsync(absPath);\n\n return;\n }\n\n await unlinkAsync(absPath);\n }\n\n /** List immediate children of an absolute directory as absolute paths. */\n public async list(absDir: string): Promise<string[]> {\n return listAsync(absDir);\n }\n\n /**\n * Resolve symlinks and `..` segments to a canonical absolute path — the\n * primitive the ops-layer jail uses to detect escapes. Delegates to\n * `node:fs/promises` `realpath`, the one filesystem operation `@warlock.js/fs`\n * does not expose.\n */\n public async realpath(absPath: string): Promise<string> {\n return realpath(absPath);\n }\n\n /**\n * Run a command and capture its outcome. The command line is executed\n * through the platform shell (`shell: true`) so pipes/operators behave as a\n * caller would expect; `cwd`, `env`, and the timeout are taken verbatim from\n * the ops layer (the environment is NOT merged with `process.env`). On\n * timeout the process is SIGKILLed and `timedOut` is set. `stdout`/`stderr`\n * are captured and byte-capped per {@link MAX_STREAM_BYTES}.\n *\n * Never rejects for a non-zero exit, a missing executable, or a timeout —\n * those are reported through the resolved {@link WorkspaceBackendExecResult}\n * so the ops layer can surface them as tool-error data.\n */\n public exec(\n command: string,\n opts: WorkspaceBackendExecOptions = {},\n ): Promise<WorkspaceBackendExecResult> {\n return new Promise<WorkspaceBackendExecResult>((resolve) => {\n const child = spawn(command, {\n cwd: opts.cwd,\n env: opts.env,\n shell: true,\n windowsHide: true,\n // POSIX: own process group so a timeout SIGKILL reaps the whole\n // shell tree, not just the shell. Harmless on Windows (ignored;\n // there we tree-kill via taskkill instead).\n detached: platform !== \"win32\",\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let timedOut = false;\n let settled = false;\n\n const timer =\n opts.timeoutMs !== undefined && opts.timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true;\n killTree(child.pid, child);\n }, opts.timeoutMs)\n : undefined;\n\n const settle = (exitCode: number) => {\n if (settled) {\n return;\n }\n\n settled = true;\n\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n\n resolve({\n exitCode,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut,\n });\n };\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n stdoutBytes = pushCapped(stdoutChunks, stdoutBytes, chunk);\n });\n\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrBytes = pushCapped(stderrChunks, stderrBytes, chunk);\n });\n\n // A spawn failure (e.g. the shell itself is missing) surfaces as an\n // error event with no exit; report it as a conventional shell\n // \"command not found\" exit code rather than rejecting.\n child.on(\"error\", () => {\n settle(127);\n });\n\n child.on(\"close\", (code, signal) => {\n // A null code means the process was terminated by a signal (our\n // timeout SIGKILL, or an external kill). Map that to the POSIX\n // 128 + signal-number convention so callers see a non-zero exit.\n if (code === null) {\n const signalNumber = signal === \"SIGKILL\" ? 9 : 1;\n settle(128 + signalNumber);\n\n return;\n }\n\n settle(code);\n });\n });\n }\n}\n\n/**\n * Create the **local** workspace backend — the default executor that runs the\n * workspace over the real disk (`@warlock.js/fs`) and the local shell\n * (`node:child_process`).\n *\n * The returned object is policy-agnostic: it expects already-jail-resolved\n * absolute paths and an already-resolved environment/timeout from the ops\n * layer. Pair it with {@link WorkspaceOps} for the actual cwd jail,\n * allow/deny lists, hashing, and output policy.\n *\n * @example\n * const backend = createLocalBackend();\n * await backend.writeFile(\"/srv/app/src/index.ts\", \"export const x = 1;\");\n * const { exitCode } = await backend.exec(\"node -v\", { cwd: \"/srv/app\" });\n */\nexport function createLocalBackend(): WorkspaceBackend {\n return new LocalBackend();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"local.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-workspace/src/backends/local.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { realpath } from \"node:fs/promises\";\nimport { platform } from \"node:process\";\nimport { fs } from \"@warlock.js/fs\";\nimport type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n} from \"../contracts/workspace-backend.contract\";\n\n/**\n * Hard ceiling on captured `stdout`/`stderr` per stream, in bytes. The raw\n * executor truncates each stream once this many bytes have accumulated so a\n * runaway command cannot exhaust memory; the ops layer applies its own\n * policy-driven cap (and the `truncated` flag) on top. Generous enough that\n * ordinary command output is never clipped here.\n */\nconst MAX_STREAM_BYTES = 10 * 1024 * 1024;\n\n/**\n * Append a chunk to a capped list of buffers, tracking the running byte\n * total and stopping once {@link MAX_STREAM_BYTES} is reached. Trailing bytes\n * past the cap are dropped rather than buffered.\n */\nfunction pushCapped(chunks: Buffer[], total: number, chunk: Buffer): number {\n if (total >= MAX_STREAM_BYTES) {\n return total;\n }\n\n const remaining = MAX_STREAM_BYTES - total;\n\n if (chunk.length <= remaining) {\n chunks.push(chunk);\n\n return total + chunk.length;\n }\n\n chunks.push(chunk.subarray(0, remaining));\n\n return MAX_STREAM_BYTES;\n}\n\n/**\n * Force-kill a spawned command and its entire process tree.\n *\n * With `shell: true` the command runs under an intermediary shell\n * (`cmd.exe` on Windows, `/bin/sh` elsewhere), so signalling the direct\n * child only reaps the shell — a long-running grandchild (e.g. `node`)\n * would survive, leaving the `exec` promise unsettled. We therefore kill\n * the whole group:\n * - **Windows** — `taskkill /T /F` walks and terminates the PID's tree.\n * - **POSIX** — the child is spawned `detached`, becoming its own process\n * group leader, so `process.kill(-pid)` SIGKILLs the group.\n */\nfunction killTree(pid: number | undefined, child: { kill(signal: NodeJS.Signals): boolean }): void {\n if (pid === undefined) {\n child.kill(\"SIGKILL\");\n\n return;\n }\n\n if (platform === \"win32\") {\n spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n\n return;\n }\n\n try {\n process.kill(-pid, \"SIGKILL\");\n } catch {\n // The group may already be gone; fall back to the direct child.\n child.kill(\"SIGKILL\");\n }\n}\n\n/**\n * The real-disk executor: every filesystem method delegates to\n * `@warlock.js/fs` (never `node:fs`), and {@link LocalBackend.exec} spawns a\n * process via `node:child_process`. It is deliberately **policy-agnostic** —\n * it receives already-resolved absolute paths and an already-resolved\n * environment + timeout from the ops layer, and just performs the side\n * effect. See {@link WorkspaceBackend} for the contract this implements.\n *\n * Constructed via {@link createLocalBackend}; the class itself is internal.\n */\nclass LocalBackend implements WorkspaceBackend {\n /** Read a file's full UTF-8 content at an absolute path. */\n public async readFile(absPath: string): Promise<string> {\n return fs.files.get(absPath);\n }\n\n /**\n * Write full content to an absolute path. Uses `atomicWriteAsync`, so a\n * concurrent reader never observes a half-written file, and missing parent\n * directories are created.\n */\n public async writeFile(absPath: string, content: string): Promise<void> {\n await fs.files.put(absPath, content, { atomic: true });\n }\n\n /** Whether anything (file or directory) exists at an absolute path. */\n public async exists(absPath: string): Promise<boolean> {\n return fs.exists(absPath);\n }\n\n /** Create a directory (and any missing parents) at an absolute path; idempotent. */\n public async mkdir(absPath: string): Promise<void> {\n await fs.dirs.ensure(absPath);\n }\n\n /**\n * Remove a file or directory tree at an absolute path. Stats the target to\n * pick the right op — `fs.dirs.remove` (recursive) for a directory,\n * `fs.files.remove` for anything else. A path that does not exist is a no-op\n * (the stat's ENOENT short-circuits, and both removes swallow `ENOENT`).\n */\n public async remove(absPath: string): Promise<void> {\n let isDirectory = false;\n\n try {\n isDirectory = (await fs.files.stats(absPath)).type === \"directory\";\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code === \"ENOENT\") {\n return;\n }\n\n throw error;\n }\n\n if (isDirectory) {\n await fs.dirs.remove(absPath);\n\n return;\n }\n\n await fs.files.remove(absPath);\n }\n\n /** List immediate children of an absolute directory as absolute paths. */\n public async list(absDir: string): Promise<string[]> {\n return fs.dirs.list(absDir);\n }\n\n /**\n * Resolve symlinks and `..` segments to a canonical absolute path — the\n * primitive the ops-layer jail uses to detect escapes. Delegates to\n * `node:fs/promises` `realpath`, the one filesystem operation `@warlock.js/fs`\n * does not expose.\n */\n public async realpath(absPath: string): Promise<string> {\n return realpath(absPath);\n }\n\n /**\n * Run a command and capture its outcome. The command line is executed\n * through the platform shell (`shell: true`) so pipes/operators behave as a\n * caller would expect; `cwd`, `env`, and the timeout are taken verbatim from\n * the ops layer (the environment is NOT merged with `process.env`). On\n * timeout the process is SIGKILLed and `timedOut` is set. `stdout`/`stderr`\n * are captured and byte-capped per {@link MAX_STREAM_BYTES}.\n *\n * Never rejects for a non-zero exit, a missing executable, or a timeout —\n * those are reported through the resolved {@link WorkspaceBackendExecResult}\n * so the ops layer can surface them as tool-error data.\n */\n public exec(\n command: string,\n opts: WorkspaceBackendExecOptions = {},\n ): Promise<WorkspaceBackendExecResult> {\n return new Promise<WorkspaceBackendExecResult>((resolve) => {\n const child = spawn(command, {\n cwd: opts.cwd,\n env: opts.env,\n shell: true,\n windowsHide: true,\n // POSIX: own process group so a timeout SIGKILL reaps the whole\n // shell tree, not just the shell. Harmless on Windows (ignored;\n // there we tree-kill via taskkill instead).\n detached: platform !== \"win32\",\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let timedOut = false;\n let settled = false;\n\n const timer =\n opts.timeoutMs !== undefined && opts.timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true;\n killTree(child.pid, child);\n }, opts.timeoutMs)\n : undefined;\n\n const settle = (exitCode: number) => {\n if (settled) {\n return;\n }\n\n settled = true;\n\n if (timer !== undefined) {\n clearTimeout(timer);\n }\n\n resolve({\n exitCode,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut,\n });\n };\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n stdoutBytes = pushCapped(stdoutChunks, stdoutBytes, chunk);\n });\n\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrBytes = pushCapped(stderrChunks, stderrBytes, chunk);\n });\n\n // A spawn failure (e.g. the shell itself is missing) surfaces as an\n // error event with no exit; report it as a conventional shell\n // \"command not found\" exit code rather than rejecting.\n child.on(\"error\", () => {\n settle(127);\n });\n\n child.on(\"close\", (code, signal) => {\n // A null code means the process was terminated by a signal (our\n // timeout SIGKILL, or an external kill). Map that to the POSIX\n // 128 + signal-number convention so callers see a non-zero exit.\n if (code === null) {\n const signalNumber = signal === \"SIGKILL\" ? 9 : 1;\n settle(128 + signalNumber);\n\n return;\n }\n\n settle(code);\n });\n });\n }\n}\n\n/**\n * Create the **local** workspace backend — the default executor that runs the\n * workspace over the real disk (`@warlock.js/fs`) and the local shell\n * (`node:child_process`).\n *\n * The returned object is policy-agnostic: it expects already-jail-resolved\n * absolute paths and an already-resolved environment/timeout from the ops\n * layer. Pair it with {@link WorkspaceOps} for the actual cwd jail,\n * allow/deny lists, hashing, and output policy.\n *\n * @example\n * const backend = createLocalBackend();\n * await backend.writeFile(\"/srv/app/src/index.ts\", \"export const x = 1;\");\n * const { exitCode } = await backend.exec(\"node -v\", { cwd: \"/srv/app\" });\n */\nexport function createLocalBackend(): WorkspaceBackend {\n return new LocalBackend();\n}\n"],"mappings":"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"}
|
package/esm/ops.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
|
|
2
2
|
import { buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { fs } from "@warlock.js/fs";
|
|
5
5
|
|
|
6
6
|
//#region ../@warlock.js/ai-workspace/src/ops.ts
|
|
7
7
|
/** Default line window a read returns when the policy sets no `defaultLines`. */
|
|
@@ -102,7 +102,7 @@ var Ops = class {
|
|
|
102
102
|
async readFile(inputPath, opts) {
|
|
103
103
|
const { absolutePath } = await resolveInJail(this.policy, inputPath);
|
|
104
104
|
const raw = await this.backend.readFile(absolutePath);
|
|
105
|
-
const hash =
|
|
105
|
+
const hash = fs.hash.string(raw);
|
|
106
106
|
const lines = raw.split("\n");
|
|
107
107
|
const totalLines = lines.length;
|
|
108
108
|
const offset = Math.max(1, opts?.offset ?? 1);
|
|
@@ -120,14 +120,14 @@ var Ops = class {
|
|
|
120
120
|
await this.backend.mkdir(parent);
|
|
121
121
|
await this.backend.writeFile(absolutePath, content);
|
|
122
122
|
return {
|
|
123
|
-
hash:
|
|
123
|
+
hash: fs.hash.string(content),
|
|
124
124
|
bytesWritten: Buffer.byteLength(content, "utf8")
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
async editFile(input) {
|
|
128
128
|
const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
|
|
129
129
|
const current = await this.backend.readFile(absolutePath);
|
|
130
|
-
const currentHash =
|
|
130
|
+
const currentHash = fs.hash.string(current);
|
|
131
131
|
if (input.expectHash !== void 0 && input.expectHash !== currentHash) throw new WorkspaceEditError(`File "${input.path}" changed since it was read; the edit is stale.`, {
|
|
132
132
|
type: "stale-hash",
|
|
133
133
|
path: relativePath || input.path,
|
|
@@ -150,7 +150,7 @@ var Ops = class {
|
|
|
150
150
|
return {
|
|
151
151
|
path: relativePath || input.path,
|
|
152
152
|
replacements: input.replaceAll ? occurrences : 1,
|
|
153
|
-
hash:
|
|
153
|
+
hash: fs.hash.string(updated)
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
156
|
async exec(command, opts) {
|
package/esm/ops.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ops.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-workspace/src/ops.ts"],"sourcesContent":["import path from \"node:path\";\nimport { hashString } from \"@warlock.js/fs\";\nimport { WorkspaceEditError, WorkspacePolicyError } from \"./errors\";\nimport { buildEnv, isCommandAllowed, resolveInJail } from \"./policy/policy\";\nimport type {\n EditFileInput,\n EditFileResult,\n GrepMatch,\n GrepResult,\n RunShellResult,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n} from \"./contracts\";\n\n/** Default line window a read returns when the policy sets no `defaultLines`. */\nconst DEFAULT_READ_LINES = 2000;\n/** Hard ceiling on grep matches returned, so a broad pattern can't flood. */\nconst DEFAULT_MAX_GREP_MATCHES = 1000;\n/** Default per-command output byte cap when the policy sets none. */\nconst DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;\n\n/**\n * Number the lines of `content` `cat -n` style: a right-aligned line\n * number (min width 6), a tab, then the line. `startLine` is the 1-based\n * number of the first line in the window.\n */\nfunction numberLines(content: string, startLine: number): string {\n const lines = content.split(\"\\n\");\n\n return lines\n .map((line, index) => {\n const lineNumber = startLine + index;\n\n return `${String(lineNumber).padStart(6, \" \")}\\t${line}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * Clip a captured stream at `maxBytes` (measured in UTF-8 bytes).\n * Returns the possibly-clipped string plus whether clipping occurred.\n */\nfunction capOutput(value: string, maxBytes: number): { value: string; truncated: boolean } {\n const bytes = Buffer.from(value, \"utf8\");\n\n if (bytes.byteLength <= maxBytes) {\n return { value, truncated: false };\n }\n\n // Slice on a byte boundary; `toString` tolerates a split multi-byte\n // char at the tail by emitting the replacement character, which is\n // acceptable for a truncated diagnostic stream.\n return { value: bytes.subarray(0, maxBytes).toString(\"utf8\"), truncated: true };\n}\n\n/**\n * Minimal glob match over a `/`-separated relative path. Supports `**`\n * (spans separators), `*` (within a segment), and `?` (one non-separator\n * char) — enough for the workspace's `glob`/grep narrowing without\n * pulling in a runtime dependency.\n */\nfunction globToRegExp(glob: string): RegExp {\n let source = \"\";\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index];\n\n if (char === \"*\") {\n if (glob[index + 1] === \"*\") {\n source += \".*\";\n index++;\n\n if (glob[index + 1] === \"/\") {\n index++;\n }\n } else {\n source += \"[^/]*\";\n }\n\n continue;\n }\n\n if (char === \"?\") {\n source += \"[^/]\";\n\n continue;\n }\n\n source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * The internal, single-instance implementation of {@link WorkspaceOps}.\n * Holds the backend + policy and is the one place the jail, command\n * gating, read caps, and the read-before-edit guard are enforced — both\n * the agent-facing tools and the human-facing direct methods funnel\n * through this object, so there is exactly one set of rules.\n */\nclass Ops implements WorkspaceOps {\n public constructor(\n private readonly backend: WorkspaceBackend,\n private readonly policy: WorkspacePolicy,\n ) {}\n\n /**\n * Recursively collect every file under `absDir` as absolute paths,\n * via the backend's `list` (so it works over disk or the in-memory\n * mock). Directories are descended; files are accumulated.\n */\n private async walkFiles(absDir: string): Promise<string[]> {\n const found: string[] = [];\n const entries = await this.backend.list(absDir);\n\n await Promise.all(\n entries.map(async (entry) => {\n // A child is a directory iff listing it succeeds; the backend\n // throws/returns for a file. Probe via `exists` + a list guard.\n const isDir = await this.isDirectory(entry);\n\n if (isDir) {\n const nested = await this.walkFiles(entry);\n found.push(...nested);\n } else {\n found.push(entry);\n }\n }),\n );\n\n return found;\n }\n\n /** Whether an absolute path is a directory, by attempting to list it. */\n private async isDirectory(absPath: string): Promise<boolean> {\n try {\n await this.backend.list(absPath);\n\n return true;\n } catch {\n return false;\n }\n }\n\n public async readFile(\n inputPath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<{ content: string; hash: string; totalLines: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n const raw = await this.backend.readFile(absolutePath);\n\n // Hash the content the backend returned (SHA-256 via @warlock.js/fs),\n // not the on-disk file — the mock backend keeps content in memory,\n // and hashing the returned bytes is what the stale-edit guard later\n // compares against, so it must be the SAME source of truth.\n const hash = hashString(raw);\n\n const lines = raw.split(\"\\n\");\n const totalLines = lines.length;\n\n const offset = Math.max(1, opts?.offset ?? 1);\n const limit = opts?.limit ?? this.policy.read?.defaultLines ?? DEFAULT_READ_LINES;\n\n const startIndex = offset - 1;\n const window = lines.slice(startIndex, startIndex + limit);\n const content = numberLines(window.join(\"\\n\"), offset);\n\n return { content, hash, totalLines };\n }\n\n public async writeFile(\n inputPath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n // Ensure the parent directory exists before writing the leaf.\n const parent = path.dirname(absolutePath);\n await this.backend.mkdir(parent);\n\n await this.backend.writeFile(absolutePath, content);\n\n return {\n hash: hashString(content),\n bytesWritten: Buffer.byteLength(content, \"utf8\"),\n };\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n const current = await this.backend.readFile(absolutePath);\n const currentHash = hashString(current);\n\n // Stale-hash guard: if the caller pinned a hash and the file moved\n // underneath them, refuse so they re-read before clobbering.\n if (input.expectHash !== undefined && input.expectHash !== currentHash) {\n throw new WorkspaceEditError(\n `File \"${input.path}\" changed since it was read; the edit is stale.`,\n {\n type: \"stale-hash\",\n path: relativePath || input.path,\n expectedHash: input.expectHash,\n actualHash: currentHash,\n },\n );\n }\n\n const occurrences = countOccurrences(current, input.oldString);\n\n if (occurrences === 0) {\n throw new WorkspaceEditError(\n `The text to replace was not found in \"${input.path}\".`,\n { type: \"not-found\", path: relativePath || input.path, matches: 0 },\n );\n }\n\n if (occurrences > 1 && !input.replaceAll) {\n throw new WorkspaceEditError(\n `The text to replace is not unique in \"${input.path}\" (${occurrences} matches); ` +\n `pass replaceAll or include more surrounding context.`,\n { type: \"not-unique\", path: relativePath || input.path, matches: occurrences },\n );\n }\n\n const updated = input.replaceAll\n ? current.split(input.oldString).join(input.newString)\n : replaceFirst(current, input.oldString, input.newString);\n\n await this.backend.writeFile(absolutePath, updated);\n\n return {\n path: relativePath || input.path,\n replacements: input.replaceAll ? occurrences : 1,\n hash: hashString(updated),\n };\n }\n\n public async exec(\n command: string,\n opts?: { timeoutMs?: number },\n ): Promise<RunShellResult> {\n if (!isCommandAllowed(this.policy, command)) {\n throw new WorkspacePolicyError(\n `Command is not permitted by the workspace shell policy: ${command}`,\n { type: \"denied-command\", command },\n );\n }\n\n const shell = this.policy.shell;\n const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;\n const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;\n\n const result = await this.backend.exec(command, {\n cwd: this.policy.cwd,\n timeoutMs,\n env: buildEnv(this.policy),\n });\n\n const stdout = capOutput(result.stdout, maxOutputBytes);\n const stderr = capOutput(result.stderr, maxOutputBytes);\n\n return {\n exitCode: result.exitCode,\n stdout: stdout.value,\n stderr: stderr.value,\n truncated: stdout.truncated || stderr.truncated,\n timedOut: result.timedOut,\n };\n }\n\n public async grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const flags = opts?.ignoreCase ? \"i\" : \"\";\n const regex = new RegExp(pattern, flags);\n const globRegex = opts?.glob ? globToRegExp(opts.glob) : undefined;\n\n const files = await this.walkFiles(jailRoot);\n const matches: GrepMatch[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (globRegex && !globRegex.test(relativePath)) {\n continue;\n }\n\n // Skip files the deny list would block (e.g. `.git/**`).\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n let content: string;\n\n try {\n content = await this.backend.readFile(absFile);\n } catch {\n // Unreadable entry (vanished, binary handle) — skip, don't fail.\n continue;\n }\n\n const lines = content.split(\"\\n\");\n\n for (let index = 0; index < lines.length; index++) {\n if (regex.test(lines[index])) {\n matches.push({ path: relativePath, line: index + 1, text: lines[index] });\n\n if (matches.length >= DEFAULT_MAX_GREP_MATCHES) {\n return { matches, total: matches.length };\n }\n }\n }\n }\n\n return { matches, total: matches.length };\n }\n\n public async glob(pattern: string): Promise<string[]> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const regex = globToRegExp(pattern);\n\n const files = await this.walkFiles(jailRoot);\n const matched: string[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n if (regex.test(relativePath)) {\n matched.push(relativePath);\n }\n }\n\n return matched.sort();\n }\n\n public async exists(inputPath: string): Promise<boolean> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n return this.backend.exists(absolutePath);\n }\n\n public async mkdir(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.mkdir(absolutePath);\n }\n\n public async remove(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.remove(absolutePath);\n }\n\n /** Whether a `/`-separated relative path hits the policy deny list. */\n private isDenied(relativePath: string): boolean {\n const denyPaths = this.policy.denyPaths;\n\n if (!denyPaths || denyPaths.length === 0) {\n return false;\n }\n\n return denyPaths.some((glob) => {\n if (globToRegExp(glob).test(relativePath)) {\n return true;\n }\n\n if (!glob.includes(\"*\") && !glob.includes(\"?\")) {\n const prefix = glob.endsWith(\"/\") ? glob : `${glob}/`;\n\n return relativePath.startsWith(prefix);\n }\n\n return false;\n });\n }\n}\n\n/** Count non-overlapping occurrences of `needle` in `haystack`. */\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle === \"\") {\n return 0;\n }\n\n let count = 0;\n let from = 0;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const index = haystack.indexOf(needle, from);\n\n if (index === -1) {\n break;\n }\n\n count++;\n from = index + needle.length;\n }\n\n return count;\n}\n\n/** Replace the first occurrence of `needle` with `replacement`. */\nfunction replaceFirst(haystack: string, needle: string, replacement: string): string {\n const index = haystack.indexOf(needle);\n\n if (index === -1) {\n return haystack;\n }\n\n return haystack.slice(0, index) + replacement + haystack.slice(index + needle.length);\n}\n\n/**\n * Create the policy-enforced operation layer over a backend.\n *\n * The returned {@link WorkspaceOps} is the single seam both the\n * agent-facing `.tools.*` factories and the human-facing direct methods\n * delegate to — one jail, one command-gate, one read-before-edit guard,\n * regardless of caller. Path inputs are workspace-relative and resolved\n * against `policy.cwd`; escapes and denied commands surface as typed\n * {@link WorkspacePolicyError} / {@link WorkspaceEditError}.\n *\n * @param backend - The dumb IO executor (local disk or in-memory mock).\n * @param policy - The policy that bounds every operation.\n *\n * @example\n * const ops = createOps(localBackend, { cwd: \"/srv/api\", shell: { allow: [\"npm\"] } });\n * const { content, hash } = await ops.readFile(\"src/index.ts\");\n */\nexport function createOps(\n backend: WorkspaceBackend,\n policy: WorkspacePolicy,\n): WorkspaceOps {\n return new Ops(backend, policy);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ops.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-workspace/src/ops.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fs } from \"@warlock.js/fs\";\nimport { WorkspaceEditError, WorkspacePolicyError } from \"./errors\";\nimport { buildEnv, isCommandAllowed, resolveInJail } from \"./policy/policy\";\nimport type {\n EditFileInput,\n EditFileResult,\n GrepMatch,\n GrepResult,\n RunShellResult,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n} from \"./contracts\";\n\n/** Default line window a read returns when the policy sets no `defaultLines`. */\nconst DEFAULT_READ_LINES = 2000;\n/** Hard ceiling on grep matches returned, so a broad pattern can't flood. */\nconst DEFAULT_MAX_GREP_MATCHES = 1000;\n/** Default per-command output byte cap when the policy sets none. */\nconst DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;\n\n/**\n * Number the lines of `content` `cat -n` style: a right-aligned line\n * number (min width 6), a tab, then the line. `startLine` is the 1-based\n * number of the first line in the window.\n */\nfunction numberLines(content: string, startLine: number): string {\n const lines = content.split(\"\\n\");\n\n return lines\n .map((line, index) => {\n const lineNumber = startLine + index;\n\n return `${String(lineNumber).padStart(6, \" \")}\\t${line}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * Clip a captured stream at `maxBytes` (measured in UTF-8 bytes).\n * Returns the possibly-clipped string plus whether clipping occurred.\n */\nfunction capOutput(value: string, maxBytes: number): { value: string; truncated: boolean } {\n const bytes = Buffer.from(value, \"utf8\");\n\n if (bytes.byteLength <= maxBytes) {\n return { value, truncated: false };\n }\n\n // Slice on a byte boundary; `toString` tolerates a split multi-byte\n // char at the tail by emitting the replacement character, which is\n // acceptable for a truncated diagnostic stream.\n return { value: bytes.subarray(0, maxBytes).toString(\"utf8\"), truncated: true };\n}\n\n/**\n * Minimal glob match over a `/`-separated relative path. Supports `**`\n * (spans separators), `*` (within a segment), and `?` (one non-separator\n * char) — enough for the workspace's `glob`/grep narrowing without\n * pulling in a runtime dependency.\n */\nfunction globToRegExp(glob: string): RegExp {\n let source = \"\";\n\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index];\n\n if (char === \"*\") {\n if (glob[index + 1] === \"*\") {\n source += \".*\";\n index++;\n\n if (glob[index + 1] === \"/\") {\n index++;\n }\n } else {\n source += \"[^/]*\";\n }\n\n continue;\n }\n\n if (char === \"?\") {\n source += \"[^/]\";\n\n continue;\n }\n\n source += char.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * The internal, single-instance implementation of {@link WorkspaceOps}.\n * Holds the backend + policy and is the one place the jail, command\n * gating, read caps, and the read-before-edit guard are enforced — both\n * the agent-facing tools and the human-facing direct methods funnel\n * through this object, so there is exactly one set of rules.\n */\nclass Ops implements WorkspaceOps {\n public constructor(\n private readonly backend: WorkspaceBackend,\n private readonly policy: WorkspacePolicy,\n ) {}\n\n /**\n * Recursively collect every file under `absDir` as absolute paths,\n * via the backend's `list` (so it works over disk or the in-memory\n * mock). Directories are descended; files are accumulated.\n */\n private async walkFiles(absDir: string): Promise<string[]> {\n const found: string[] = [];\n const entries = await this.backend.list(absDir);\n\n await Promise.all(\n entries.map(async (entry) => {\n // A child is a directory iff listing it succeeds; the backend\n // throws/returns for a file. Probe via `exists` + a list guard.\n const isDir = await this.isDirectory(entry);\n\n if (isDir) {\n const nested = await this.walkFiles(entry);\n found.push(...nested);\n } else {\n found.push(entry);\n }\n }),\n );\n\n return found;\n }\n\n /** Whether an absolute path is a directory, by attempting to list it. */\n private async isDirectory(absPath: string): Promise<boolean> {\n try {\n await this.backend.list(absPath);\n\n return true;\n } catch {\n return false;\n }\n }\n\n public async readFile(\n inputPath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<{ content: string; hash: string; totalLines: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n const raw = await this.backend.readFile(absolutePath);\n\n // Hash the content the backend returned (SHA-256 via @warlock.js/fs),\n // not the on-disk file — the mock backend keeps content in memory,\n // and hashing the returned bytes is what the stale-edit guard later\n // compares against, so it must be the SAME source of truth.\n const hash = fs.hash.string(raw);\n\n const lines = raw.split(\"\\n\");\n const totalLines = lines.length;\n\n const offset = Math.max(1, opts?.offset ?? 1);\n const limit = opts?.limit ?? this.policy.read?.defaultLines ?? DEFAULT_READ_LINES;\n\n const startIndex = offset - 1;\n const window = lines.slice(startIndex, startIndex + limit);\n const content = numberLines(window.join(\"\\n\"), offset);\n\n return { content, hash, totalLines };\n }\n\n public async writeFile(\n inputPath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n // Ensure the parent directory exists before writing the leaf.\n const parent = path.dirname(absolutePath);\n await this.backend.mkdir(parent);\n\n await this.backend.writeFile(absolutePath, content);\n\n return {\n hash: fs.hash.string(content),\n bytesWritten: Buffer.byteLength(content, \"utf8\"),\n };\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n const current = await this.backend.readFile(absolutePath);\n const currentHash = fs.hash.string(current);\n\n // Stale-hash guard: if the caller pinned a hash and the file moved\n // underneath them, refuse so they re-read before clobbering.\n if (input.expectHash !== undefined && input.expectHash !== currentHash) {\n throw new WorkspaceEditError(\n `File \"${input.path}\" changed since it was read; the edit is stale.`,\n {\n type: \"stale-hash\",\n path: relativePath || input.path,\n expectedHash: input.expectHash,\n actualHash: currentHash,\n },\n );\n }\n\n const occurrences = countOccurrences(current, input.oldString);\n\n if (occurrences === 0) {\n throw new WorkspaceEditError(\n `The text to replace was not found in \"${input.path}\".`,\n { type: \"not-found\", path: relativePath || input.path, matches: 0 },\n );\n }\n\n if (occurrences > 1 && !input.replaceAll) {\n throw new WorkspaceEditError(\n `The text to replace is not unique in \"${input.path}\" (${occurrences} matches); ` +\n `pass replaceAll or include more surrounding context.`,\n { type: \"not-unique\", path: relativePath || input.path, matches: occurrences },\n );\n }\n\n const updated = input.replaceAll\n ? current.split(input.oldString).join(input.newString)\n : replaceFirst(current, input.oldString, input.newString);\n\n await this.backend.writeFile(absolutePath, updated);\n\n return {\n path: relativePath || input.path,\n replacements: input.replaceAll ? occurrences : 1,\n hash: fs.hash.string(updated),\n };\n }\n\n public async exec(\n command: string,\n opts?: { timeoutMs?: number },\n ): Promise<RunShellResult> {\n if (!isCommandAllowed(this.policy, command)) {\n throw new WorkspacePolicyError(\n `Command is not permitted by the workspace shell policy: ${command}`,\n { type: \"denied-command\", command },\n );\n }\n\n const shell = this.policy.shell;\n const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;\n const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;\n\n const result = await this.backend.exec(command, {\n cwd: this.policy.cwd,\n timeoutMs,\n env: buildEnv(this.policy),\n });\n\n const stdout = capOutput(result.stdout, maxOutputBytes);\n const stderr = capOutput(result.stderr, maxOutputBytes);\n\n return {\n exitCode: result.exitCode,\n stdout: stdout.value,\n stderr: stderr.value,\n truncated: stdout.truncated || stderr.truncated,\n timedOut: result.timedOut,\n };\n }\n\n public async grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const flags = opts?.ignoreCase ? \"i\" : \"\";\n const regex = new RegExp(pattern, flags);\n const globRegex = opts?.glob ? globToRegExp(opts.glob) : undefined;\n\n const files = await this.walkFiles(jailRoot);\n const matches: GrepMatch[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (globRegex && !globRegex.test(relativePath)) {\n continue;\n }\n\n // Skip files the deny list would block (e.g. `.git/**`).\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n let content: string;\n\n try {\n content = await this.backend.readFile(absFile);\n } catch {\n // Unreadable entry (vanished, binary handle) — skip, don't fail.\n continue;\n }\n\n const lines = content.split(\"\\n\");\n\n for (let index = 0; index < lines.length; index++) {\n if (regex.test(lines[index])) {\n matches.push({ path: relativePath, line: index + 1, text: lines[index] });\n\n if (matches.length >= DEFAULT_MAX_GREP_MATCHES) {\n return { matches, total: matches.length };\n }\n }\n }\n }\n\n return { matches, total: matches.length };\n }\n\n public async glob(pattern: string): Promise<string[]> {\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const regex = globToRegExp(pattern);\n\n const files = await this.walkFiles(jailRoot);\n const matched: string[] = [];\n\n for (const absFile of files) {\n const relativePath = path.relative(jailRoot, absFile).split(path.sep).join(\"/\");\n\n if (this.isDenied(relativePath)) {\n continue;\n }\n\n if (regex.test(relativePath)) {\n matched.push(relativePath);\n }\n }\n\n return matched.sort();\n }\n\n public async exists(inputPath: string): Promise<boolean> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n return this.backend.exists(absolutePath);\n }\n\n public async mkdir(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.mkdir(absolutePath);\n }\n\n public async remove(inputPath: string): Promise<void> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n\n await this.backend.remove(absolutePath);\n }\n\n /** Whether a `/`-separated relative path hits the policy deny list. */\n private isDenied(relativePath: string): boolean {\n const denyPaths = this.policy.denyPaths;\n\n if (!denyPaths || denyPaths.length === 0) {\n return false;\n }\n\n return denyPaths.some((glob) => {\n if (globToRegExp(glob).test(relativePath)) {\n return true;\n }\n\n if (!glob.includes(\"*\") && !glob.includes(\"?\")) {\n const prefix = glob.endsWith(\"/\") ? glob : `${glob}/`;\n\n return relativePath.startsWith(prefix);\n }\n\n return false;\n });\n }\n}\n\n/** Count non-overlapping occurrences of `needle` in `haystack`. */\nfunction countOccurrences(haystack: string, needle: string): number {\n if (needle === \"\") {\n return 0;\n }\n\n let count = 0;\n let from = 0;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const index = haystack.indexOf(needle, from);\n\n if (index === -1) {\n break;\n }\n\n count++;\n from = index + needle.length;\n }\n\n return count;\n}\n\n/** Replace the first occurrence of `needle` with `replacement`. */\nfunction replaceFirst(haystack: string, needle: string, replacement: string): string {\n const index = haystack.indexOf(needle);\n\n if (index === -1) {\n return haystack;\n }\n\n return haystack.slice(0, index) + replacement + haystack.slice(index + needle.length);\n}\n\n/**\n * Create the policy-enforced operation layer over a backend.\n *\n * The returned {@link WorkspaceOps} is the single seam both the\n * agent-facing `.tools.*` factories and the human-facing direct methods\n * delegate to — one jail, one command-gate, one read-before-edit guard,\n * regardless of caller. Path inputs are workspace-relative and resolved\n * against `policy.cwd`; escapes and denied commands surface as typed\n * {@link WorkspacePolicyError} / {@link WorkspaceEditError}.\n *\n * @param backend - The dumb IO executor (local disk or in-memory mock).\n * @param policy - The policy that bounds every operation.\n *\n * @example\n * const ops = createOps(localBackend, { cwd: \"/srv/api\", shell: { allow: [\"npm\"] } });\n * const { content, hash } = await ops.readFile(\"src/index.ts\");\n */\nexport function createOps(\n backend: WorkspaceBackend,\n policy: WorkspacePolicy,\n): WorkspaceOps {\n return new Ops(backend, policy);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"url": "https://github.com/warlockjs/ai-workspace"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@warlock.js/fs": "4.
|
|
21
|
+
"@warlock.js/fs": "4.7.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@warlock.js/ai": "4.
|
|
24
|
+
"@warlock.js/ai": "4.7.0"
|
|
25
25
|
},
|
|
26
|
-
"version": "4.
|
|
26
|
+
"version": "4.7.0",
|
|
27
27
|
"main": "./cjs/index.cjs",
|
|
28
28
|
"module": "./esm/index.mjs",
|
|
29
29
|
"types": "./esm/index.d.mts",
|