@warlock.js/ai-workspace 5.43.0 → 5.45.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/CHANGELOG.md +31 -0
- package/README.md +14 -1
- package/cjs/index.cjs +1847 -141
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +36 -22
- package/esm/backends/local.mjs.map +1 -1
- package/esm/contracts/index.d.mts +5 -1
- package/esm/contracts/shell-approval.type.d.mts +88 -0
- package/esm/contracts/shell-approval.type.d.mts.map +1 -0
- package/esm/contracts/tool-io.type.d.mts +45 -1
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +11 -1
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-edit.type.d.mts +66 -0
- package/esm/contracts/workspace-edit.type.d.mts.map +1 -0
- package/esm/contracts/workspace-ops.contract.d.mts +7 -1
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-output.type.d.mts +57 -0
- package/esm/contracts/workspace-output.type.d.mts.map +1 -0
- package/esm/contracts/workspace-policy.type.d.mts +38 -1
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +23 -1
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/edit/apply-replacement.mjs +58 -0
- package/esm/edit/apply-replacement.mjs.map +1 -0
- package/esm/edit/match.d.mts +16 -0
- package/esm/edit/match.d.mts.map +1 -0
- package/esm/edit/match.mjs +214 -0
- package/esm/edit/match.mjs.map +1 -0
- package/esm/errors.d.mts +12 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +1 -0
- package/esm/errors.mjs.map +1 -1
- package/esm/hooks/after-edit.mjs +36 -0
- package/esm/hooks/after-edit.mjs.map +1 -0
- package/esm/index.d.mts +7 -2
- package/esm/index.mjs +3 -2
- package/esm/ops.d.mts +3 -1
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +213 -75
- package/esm/ops.mjs.map +1 -1
- package/esm/output/spill-dir.mjs +15 -0
- package/esm/output/spill-dir.mjs.map +1 -0
- package/esm/output/spill.mjs +223 -0
- package/esm/output/spill.mjs.map +1 -0
- package/esm/policy/approval-helpers.mjs +22 -0
- package/esm/policy/approval-helpers.mjs.map +1 -0
- package/esm/policy/policy.d.mts +3 -1
- package/esm/policy/policy.d.mts.map +1 -1
- package/esm/policy/policy.mjs +12 -4
- package/esm/policy/policy.mjs.map +1 -1
- package/esm/policy/powershell-aliases.mjs +275 -0
- package/esm/policy/powershell-aliases.mjs.map +1 -0
- package/esm/policy/powershell-parser.mjs +420 -0
- package/esm/policy/powershell-parser.mjs.map +1 -0
- package/esm/policy/shell-check.d.mts +32 -2
- package/esm/policy/shell-check.d.mts.map +1 -1
- package/esm/policy/shell-check.mjs +183 -31
- package/esm/policy/shell-check.mjs.map +1 -1
- package/esm/policy/shell-interpreter.d.mts +63 -0
- package/esm/policy/shell-interpreter.d.mts.map +1 -0
- package/esm/policy/shell-interpreter.mjs +123 -0
- package/esm/policy/shell-interpreter.mjs.map +1 -0
- package/esm/policy/shell-parser.d.mts +33 -0
- package/esm/policy/shell-parser.d.mts.map +1 -0
- package/esm/policy/shell-parser.mjs +3 -0
- package/esm/policy/shell-parser.mjs.map +1 -1
- package/esm/tools/glob.mjs +1 -0
- package/esm/tools/glob.mjs.map +1 -1
- package/esm/tools/run-shell.d.mts +10 -0
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-shell.mjs +19 -1
- package/esm/tools/run-shell.mjs.map +1 -1
- package/esm/tools/run-tests.d.mts +7 -0
- package/esm/tools/run-tests.d.mts.map +1 -1
- package/esm/tools/run-tests.mjs +5 -4
- package/esm/tools/run-tests.mjs.map +1 -1
- package/esm/tools/write-file.mjs +3 -2
- package/esm/tools/write-file.mjs.map +1 -1
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +27 -5
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +70 -17
- package/package.json +3 -3
- package/skills/use-a-workspace/SKILL.md +70 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/backends/local.ts"],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"local.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/backends/local.ts"],"mappings":";;;;;;;;;;;;;;;;;wBA6lBgB,sBAAsB"}
|
package/esm/backends/local.mjs
CHANGED
|
@@ -108,25 +108,39 @@ const POSIX_SHELL = "/bin/sh";
|
|
|
108
108
|
* `/s` keeps the line verbatim inside the outer quotes). The ops layer has
|
|
109
109
|
* already checked every command in the line.
|
|
110
110
|
*/
|
|
111
|
-
function planSpawn(command, interpreter) {
|
|
112
|
-
if (interpreter === "
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
function planSpawn(command, interpreter, interpreterPath, hostPlatform = platform) {
|
|
112
|
+
if (interpreter === "powershell") return {
|
|
113
|
+
file: interpreterPath ?? (hostPlatform === "win32" ? "powershell.exe" : "pwsh"),
|
|
114
|
+
args: [
|
|
115
|
+
"-NoLogo",
|
|
116
|
+
"-NoProfile",
|
|
117
|
+
"-NonInteractive",
|
|
118
|
+
"-Command",
|
|
119
|
+
command
|
|
120
|
+
],
|
|
121
|
+
windowsVerbatimArguments: false
|
|
122
|
+
};
|
|
123
|
+
if (interpreter === "bash") return {
|
|
124
|
+
file: interpreterPath ?? "bash",
|
|
125
|
+
args: ["-c", command],
|
|
126
|
+
windowsVerbatimArguments: false
|
|
127
|
+
};
|
|
128
|
+
if (interpreter === "cmd" || interpreter === "sh" && hostPlatform === "win32") return {
|
|
129
|
+
file: interpreterPath ?? process.env.ComSpec ?? "cmd.exe",
|
|
130
|
+
args: [
|
|
131
|
+
"/d",
|
|
132
|
+
"/v:off",
|
|
133
|
+
"/s",
|
|
134
|
+
"/c",
|
|
135
|
+
`"${command}"`
|
|
136
|
+
],
|
|
137
|
+
windowsVerbatimArguments: true
|
|
138
|
+
};
|
|
139
|
+
if (interpreter === "sh") return {
|
|
140
|
+
file: interpreterPath ?? POSIX_SHELL,
|
|
141
|
+
args: ["-c", command],
|
|
142
|
+
windowsVerbatimArguments: false
|
|
143
|
+
};
|
|
130
144
|
const argv = tokenizeCommand(command);
|
|
131
145
|
const executable = argv?.[0];
|
|
132
146
|
if (argv === null || executable === void 0) return { refused: "Command was not executed: it is empty, has unbalanced quotes, or contains unquoted shell metacharacters (;, &, |, `, $, <, >, parentheses). Commands run without a shell — pass metacharacters inside quotes as literal arguments, or run one command at a time." };
|
|
@@ -282,7 +296,7 @@ var LocalBackend = class {
|
|
|
282
296
|
});
|
|
283
297
|
return;
|
|
284
298
|
}
|
|
285
|
-
const plan = planSpawn(command, opts.interpreter ?? "none");
|
|
299
|
+
const plan = planSpawn(command, opts.interpreter ?? "none", opts.interpreterPath);
|
|
286
300
|
if ("refused" in plan) {
|
|
287
301
|
refuse(plan.refused);
|
|
288
302
|
return;
|
|
@@ -356,7 +370,7 @@ var LocalBackend = class {
|
|
|
356
370
|
* still-running tree is also killed when the Node process exits.
|
|
357
371
|
*/
|
|
358
372
|
start(command, opts) {
|
|
359
|
-
const plan = planSpawn(command, opts.interpreter ?? "none");
|
|
373
|
+
const plan = planSpawn(command, opts.interpreter ?? "none", opts.interpreterPath);
|
|
360
374
|
if ("refused" in plan) {
|
|
361
375
|
opts.onOutput("stderr", plan.refused);
|
|
362
376
|
return {
|
|
@@ -438,5 +452,5 @@ function createLocalBackend() {
|
|
|
438
452
|
}
|
|
439
453
|
|
|
440
454
|
//#endregion
|
|
441
|
-
export { createLocalBackend };
|
|
455
|
+
export { createLocalBackend, planSpawn };
|
|
442
456
|
//# sourceMappingURL=local.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/backends/local.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { lstat, realpath } from \"node:fs/promises\";\nimport { platform } from \"node:process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { fs } from \"@warlock.js/fs\";\nimport { tokenizeCommand } from \"../policy/tokenize-command\";\nimport type { WorkspaceShellInterpreter } from \"../contracts/workspace-policy.type\";\nimport type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n WorkspaceBackendProcess,\n WorkspaceBackendStartOptions,\n WorkspaceBackendStat,\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 * The direct child may have grandchildren (on Windows it is the `cmd.exe`\n * wrapper around a batch shim; anywhere, an allowed `npm`/`node` can spawn\n * further processes), so signalling the direct child alone could leave a\n * long-running grandchild alive and the `exec` promise unsettled. We\n * therefore kill 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 const killer = spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n\n // Without a listener a failed spawn (taskkill missing from PATH) is an\n // unhandled 'error' event that crashes the process; fall back to the\n // direct child instead.\n killer.on(\"error\", () => {\n child.kill(\"SIGKILL\");\n });\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 * Argument bytes `cmd.exe` cannot carry safely even inside double quotes:\n * an embedded quote breaks out of the quoted span, `%` triggers variable\n * expansion regardless of quoting, and newlines end the command line. An\n * argv containing any of these is refused rather than risked (the\n * BatBadBut / CVE-2024-24576 class of `cmd.exe` argument smuggling).\n */\nconst WIN32_UNSAFE_ARGUMENT = /[\"%\\r\\n]/;\n\n/**\n * Build the `cmd.exe` invocation that runs an argv on Windows. Batch shims\n * (`npm.cmd`, `npx.cmd`, …) cannot be spawned directly without a shell\n * (Node rejects them since CVE-2024-27980), so the argv is run through\n * `cmd.exe /d /s /c` with every element individually double-quoted —\n * quoted spans are literal to cmd's parser, so pipes/ampersands inside an\n * argument stay argument data. Returns `null` when an element contains a\n * byte cmd cannot quote safely (see {@link WIN32_UNSAFE_ARGUMENT}).\n *\n * The caller must spawn with `windowsVerbatimArguments: true` so Node does\n * not re-quote the already-quoted command line.\n */\nfunction toWin32CmdInvocation(argv: string[]): { file: string; args: string[] } | null {\n if (argv.some((element) => WIN32_UNSAFE_ARGUMENT.test(element))) {\n return null;\n }\n\n const commandLine = argv.map((element) => `\"${element}\"`).join(\" \");\n\n return {\n file: process.env.ComSpec ?? \"cmd.exe\",\n args: [\"/d\", \"/s\", \"/c\", `\"${commandLine}\"`],\n };\n}\n\n/** The shell a POSIX shell-mode line runs through — an absolute path, never a PATH lookup. */\nconst POSIX_SHELL = \"/bin/sh\";\n\n/** How to spawn a command line, or why it cannot be spawned. */\ntype SpawnPlan =\n | { file: string; args: string[]; windowsVerbatimArguments: boolean }\n | { refused: string };\n\n/**\n * Work out the spawn for a command line.\n *\n * - argv mode: tokenize (no shell semantics) and spawn the argv directly —\n * on Windows through an individually-quoted `cmd.exe` wrapper.\n * - shell mode: `/bin/sh -c <line>`, or `cmd.exe /d /v:off /s /c \"<line>\"`\n * on Windows (`/d` skips AutoRun, `/v:off` disables delayed `!` expansion,\n * `/s` keeps the line verbatim inside the outer quotes). The ops layer has\n * already checked every command in the line.\n */\nfunction planSpawn(command: string, interpreter: WorkspaceShellInterpreter): SpawnPlan {\n if (interpreter === \"sh\") {\n if (platform === \"win32\") {\n return {\n file: process.env.ComSpec ?? \"cmd.exe\",\n args: [\"/d\", \"/v:off\", \"/s\", \"/c\", `\"${command}\"`],\n windowsVerbatimArguments: true,\n };\n }\n\n return { file: POSIX_SHELL, args: [\"-c\", command], windowsVerbatimArguments: false };\n }\n\n const argv = tokenizeCommand(command);\n const executable = argv?.[0];\n\n if (argv === null || executable === undefined) {\n return {\n refused:\n \"Command was not executed: it is empty, has unbalanced quotes, or \" +\n \"contains unquoted shell metacharacters (;, &, |, `, $, <, >, \" +\n \"parentheses). Commands run without a shell — pass metacharacters \" +\n \"inside quotes as literal arguments, or run one command at a time.\",\n };\n }\n\n if (platform === \"win32\") {\n const invocation = toWin32CmdInvocation(argv);\n\n if (invocation === null) {\n return {\n refused:\n 'Command was not executed: on Windows, arguments containing \", %, ' +\n \"or newlines cannot be passed to cmd.exe safely.\",\n };\n }\n\n return { ...invocation, windowsVerbatimArguments: true };\n }\n\n return { file: executable, args: argv.slice(1), windowsVerbatimArguments: false };\n}\n\n/**\n * Kill functions of every background process still running, so they can\n * be reaped if the Node process exits without the workspace being disposed\n * (detached process groups would otherwise outlive it).\n */\nconst liveBackgroundKills = new Set<() => void>();\n\nlet exitHookInstalled = false;\n\n/** Track a live background tree; installs (once) the exit hook that kills them. */\nfunction trackBackgroundProcess(kill: () => void): () => void {\n liveBackgroundKills.add(kill);\n\n if (!exitHookInstalled) {\n exitHookInstalled = true;\n process.on(\"exit\", () => {\n for (const killTreeNow of liveBackgroundKills) {\n killTreeNow();\n }\n });\n }\n\n return () => {\n liveBackgroundKills.delete(kill);\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 * Describe a path without following a final symlink (`lstat` — like\n * `realpath`, a filesystem operation `@warlock.js/fs` does not expose).\n */\n public async stat(absPath: string): Promise<WorkspaceBackendStat> {\n const stats = await lstat(absPath);\n const type = stats.isSymbolicLink()\n ? \"symlink\"\n : stats.isDirectory()\n ? \"directory\"\n : stats.isFile()\n ? \"file\"\n : \"other\";\n\n return { type, size: stats.size };\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. In argv mode (the default) the\n * command line is tokenized into an argv (quotes respected, NO shell\n * semantics — see `tokenizeCommand`) and spawned **without a shell**, so\n * metacharacters like `;`, `&&`, `|`, backticks, and `$()` can never\n * chain extra commands past the ops layer's allowlist; a command they\n * appear unquoted in is refused with exit code 127. On Windows the argv\n * runs through a `cmd.exe /d /s /c` wrapper (batch shims like `npm.cmd`\n * cannot be spawned shell-less) with every element individually quoted.\n * With `interpreter: \"sh\"` the line runs through `/bin/sh -c` (or\n * `cmd.exe /d /v:off /s /c` on Windows) — only after the ops layer has\n * checked every command in it.\n * `cwd`, `env`, and the timeout are taken verbatim from the ops layer\n * (the environment is NOT merged with `process.env`). On timeout the\n * process tree is SIGKILLed and `timedOut` is set. When `signal` aborts\n * mid-run the process tree is SIGKILLed the same way and `cancelled` is\n * set; an already-aborted `signal` resolves `cancelled` without spawning.\n * `stdout`/`stderr` are captured and byte-capped per {@link MAX_STREAM_BYTES}.\n *\n * Never rejects for a non-zero exit, a missing executable, a refused\n * command line, a timeout or a cancellation — those are reported through the resolved\n * {@link WorkspaceBackendExecResult} so the ops layer can surface them\n * 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 refuse = (stderr: string): void =>\n resolve({ exitCode: 127, stdout: \"\", stderr, timedOut: false });\n\n const signal = opts.signal;\n\n // Never spawn for an invocation that was cancelled before it started.\n if (signal?.aborted) {\n resolve({\n exitCode: 130,\n stdout: \"\",\n stderr: \"Command was not executed: the invocation was cancelled before it started.\",\n timedOut: false,\n cancelled: true,\n });\n\n return;\n }\n\n const plan = planSpawn(command, opts.interpreter ?? \"none\");\n\n if (\"refused\" in plan) {\n refuse(plan.refused);\n\n return;\n }\n\n const child = spawn(plan.file, plan.args, {\n cwd: opts.cwd,\n env: opts.env,\n shell: false,\n windowsHide: true,\n windowsVerbatimArguments: plan.windowsVerbatimArguments,\n // POSIX: own process group so a timeout SIGKILL reaps the whole\n // process tree, not just the direct child. Harmless on Windows\n // (ignored; 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 cancelled = false;\n let settled = false;\n // The direct child has exited; 'close' may still be pending while its\n // stdio drains (or a grandchild holds the pipes open).\n let exited = false;\n\n // Timeout and cancellation are mutually exclusive: whichever kills the\n // tree first owns the outcome, so a result is never both.\n const timer =\n opts.timeoutMs !== undefined && opts.timeoutMs > 0\n ? setTimeout(() => {\n if (cancelled) {\n return;\n }\n\n timedOut = true;\n killTree(child.pid, child);\n }, opts.timeoutMs)\n : undefined;\n\n const onAbort = (): void => {\n if (settled || timedOut) {\n return;\n }\n\n // The command already finished: report its real outcome, not a\n // cancellation. Still reap the tree so a grandchild holding the\n // pipes open cannot keep `close` (and this promise) pending.\n if (!exited) {\n cancelled = true;\n }\n\n killTree(child.pid, child);\n };\n\n // Attached synchronously after `spawn`, so there is no window in which\n // an abort could be missed; removed in `settle` so a long-lived signal\n // shared across many commands does not accumulate listeners.\n signal?.addEventListener(\"abort\", onAbort, { once: true });\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 signal?.removeEventListener(\"abort\", onAbort);\n\n resolve({\n exitCode,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut,\n cancelled,\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 executable cannot be found) surfaces as\n // an error event with no exit; report it as a conventional\n // \"command not found\" exit code rather than rejecting.\n child.on(\"error\", () => {\n settle(127);\n });\n\n child.on(\"exit\", () => {\n exited = true;\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 * Start a long-running command and return at once. Spawned like\n * {@link exec} (same argv/shell planning, own process group on POSIX), with\n * stdin closed. Output is decoded per stream with a `StringDecoder`, so a\n * chunk never ends inside a character. `kill()` kills the whole tree; a\n * still-running tree is also killed when the Node process exits.\n */\n public start(command: string, opts: WorkspaceBackendStartOptions): WorkspaceBackendProcess {\n const plan = planSpawn(command, opts.interpreter ?? \"none\");\n\n if (\"refused\" in plan) {\n opts.onOutput(\"stderr\", plan.refused);\n\n return { done: Promise.resolve({ exitCode: 127 }), kill: () => {} };\n }\n\n const child = spawn(plan.file, plan.args, {\n cwd: opts.cwd,\n env: opts.env,\n shell: false,\n windowsHide: true,\n windowsVerbatimArguments: plan.windowsVerbatimArguments,\n detached: platform !== \"win32\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const stdoutDecoder = new StringDecoder(\"utf8\");\n const stderrDecoder = new StringDecoder(\"utf8\");\n let finished = false;\n\n const kill = (): void => {\n if (!finished) {\n killTree(child.pid, child);\n }\n };\n\n const untrack = trackBackgroundProcess(kill);\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n const text = stdoutDecoder.write(chunk);\n\n if (text !== \"\") {\n opts.onOutput(\"stdout\", text);\n }\n });\n\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n const text = stderrDecoder.write(chunk);\n\n if (text !== \"\") {\n opts.onOutput(\"stderr\", text);\n }\n });\n\n const done = new Promise<{ exitCode: number }>((resolve) => {\n const finish = (exitCode: number): void => {\n if (finished) {\n return;\n }\n\n finished = true;\n untrack();\n\n const stdoutTail = stdoutDecoder.end();\n const stderrTail = stderrDecoder.end();\n\n if (stdoutTail !== \"\") {\n opts.onOutput(\"stdout\", stdoutTail);\n }\n\n if (stderrTail !== \"\") {\n opts.onOutput(\"stderr\", stderrTail);\n }\n\n resolve({ exitCode });\n };\n\n child.on(\"error\", (error) => {\n opts.onOutput(\"stderr\", `Command could not be started: ${error.message}`);\n finish(127);\n });\n\n child.on(\"close\", (code, signal) => {\n finish(code === null ? 128 + (signal === \"SIGKILL\" ? 9 : 1) : code);\n });\n });\n\n return { ...(child.pid !== undefined ? { pid: child.pid } : {}), done, kill };\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":";;;;;;;;;;;;;;;AAuBA,MAAM,mBAAmB;;;;;;AAOzB,SAAS,WAAW,QAAkB,OAAe,OAAuB;CAC1E,IAAI,SAAS,kBACX,OAAO;CAGT,MAAM,YAAY,mBAAmB;CAErC,IAAI,MAAM,UAAU,WAAW;EAC7B,OAAO,KAAK,KAAK;EAEjB,OAAO,QAAQ,MAAM;CACvB;CAEA,OAAO,KAAK,MAAM,SAAS,GAAG,SAAS,CAAC;CAExC,OAAO;AACT;;;;;;;;;;;;;AAcA,SAAS,SAAS,KAAyB,OAAwD;CACjG,IAAI,QAAQ,QAAW;EACrB,MAAM,KAAK,SAAS;EAEpB;CACF;CAEA,IAAI,aAAa,SAAS;EAMxB,AALe,MAAM,YAAY;GAAC;GAAQ,OAAO,GAAG;GAAG;GAAM;EAAI,GAAG,EAAE,aAAa,KAAK,CAKnF,CAAC,CAAC,GAAG,eAAe;GACvB,MAAM,KAAK,SAAS;EACtB,CAAC;EAED;CACF;CAEA,IAAI;EACF,QAAQ,KAAK,CAAC,KAAK,SAAS;CAC9B,QAAQ;EAEN,MAAM,KAAK,SAAS;CACtB;AACF;;;;;;;;AASA,MAAM,wBAAwB;;;;;;;;;;;;;AAc9B,SAAS,qBAAqB,MAAyD;CACrF,IAAI,KAAK,MAAM,YAAY,sBAAsB,KAAK,OAAO,CAAC,GAC5D,OAAO;CAGT,MAAM,cAAc,KAAK,KAAK,YAAY,IAAI,QAAQ,EAAE,CAAC,CAAC,KAAK,GAAG;CAElE,OAAO;EACL,MAAM,QAAQ,IAAI,WAAW;EAC7B,MAAM;GAAC;GAAM;GAAM;GAAM,IAAI,YAAY;EAAE;CAC7C;AACF;;AAGA,MAAM,cAAc;;;;;;;;;;;AAiBpB,SAAS,UAAU,SAAiB,aAAmD;CACrF,IAAI,gBAAgB,MAAM;EACxB,IAAI,aAAa,SACf,OAAO;GACL,MAAM,QAAQ,IAAI,WAAW;GAC7B,MAAM;IAAC;IAAM;IAAU;IAAM;IAAM,IAAI,QAAQ;GAAE;GACjD,0BAA0B;EAC5B;EAGF,OAAO;GAAE,MAAM;GAAa,MAAM,CAAC,MAAM,OAAO;GAAG,0BAA0B;EAAM;CACrF;CAEA,MAAM,OAAO,gBAAgB,OAAO;CACpC,MAAM,aAAa,OAAO;CAE1B,IAAI,SAAS,QAAQ,eAAe,QAClC,OAAO,EACL,SACE,mQAIJ;CAGF,IAAI,aAAa,SAAS;EACxB,MAAM,aAAa,qBAAqB,IAAI;EAE5C,IAAI,eAAe,MACjB,OAAO,EACL,SACE,oHAEJ;EAGF,OAAO;GAAE,GAAG;GAAY,0BAA0B;EAAK;CACzD;CAEA,OAAO;EAAE,MAAM;EAAY,MAAM,KAAK,MAAM,CAAC;EAAG,0BAA0B;CAAM;AAClF;;;;;;AAOA,MAAM,sCAAsB,IAAI,IAAgB;AAEhD,IAAI,oBAAoB;;AAGxB,SAAS,uBAAuB,MAA8B;CAC5D,oBAAoB,IAAI,IAAI;CAE5B,IAAI,CAAC,mBAAmB;EACtB,oBAAoB;EACpB,QAAQ,GAAG,cAAc;GACvB,KAAK,MAAM,eAAe,qBACxB,YAAY;EAEhB,CAAC;CACH;CAEA,aAAa;EACX,oBAAoB,OAAO,IAAI;CACjC;AACF;;;;;;;;;;;AAYA,IAAM,eAAN,MAA+C;;CAE7C,MAAa,SAAS,SAAkC;EACtD,OAAO,GAAG,MAAM,IAAI,OAAO;CAC7B;;;;;;CAOA,MAAa,UAAU,SAAiB,SAAgC;EACtE,MAAM,GAAG,MAAM,IAAI,SAAS,SAAS,EAAE,QAAQ,KAAK,CAAC;CACvD;;CAGA,MAAa,OAAO,SAAmC;EACrD,OAAO,GAAG,OAAO,OAAO;CAC1B;;CAGA,MAAa,MAAM,SAAgC;EACjD,MAAM,GAAG,KAAK,OAAO,OAAO;CAC9B;;;;;;;CAQA,MAAa,OAAO,SAAgC;EAClD,IAAI,cAAc;EAElB,IAAI;GACF,eAAe,MAAM,GAAG,MAAM,MAAM,OAAO,EAAC,CAAE,SAAS;EACzD,SAAS,OAAO;GACd,IAAK,OAAiC,SAAS,UAC7C;GAGF,MAAM;EACR;EAEA,IAAI,aAAa;GACf,MAAM,GAAG,KAAK,OAAO,OAAO;GAE5B;EACF;EAEA,MAAM,GAAG,MAAM,OAAO,OAAO;CAC/B;;CAGA,MAAa,KAAK,QAAmC;EACnD,OAAO,GAAG,KAAK,KAAK,MAAM;CAC5B;;;;;CAMA,MAAa,KAAK,SAAgD;EAChE,MAAM,QAAQ,MAAM,MAAM,OAAO;EASjC,OAAO;GAAE,MARI,MAAM,eAAe,IAC9B,YACA,MAAM,YAAY,IAChB,cACA,MAAM,OAAO,IACX,SACA;GAEO,MAAM,MAAM;EAAK;CAClC;;;;;;;CAQA,MAAa,SAAS,SAAkC;EACtD,OAAO,SAAS,OAAO;CACzB;;;;;;;;;;;;;;;;;;;;;;;;;CA0BA,AAAO,KACL,SACA,OAAoC,CAAC,GACA;EACrC,OAAO,IAAI,SAAqC,YAAY;GAC1D,MAAM,UAAU,WACd,QAAQ;IAAE,UAAU;IAAK,QAAQ;IAAI;IAAQ,UAAU;GAAM,CAAC;GAEhE,MAAM,SAAS,KAAK;GAGpB,IAAI,QAAQ,SAAS;IACnB,QAAQ;KACN,UAAU;KACV,QAAQ;KACR,QAAQ;KACR,UAAU;KACV,WAAW;IACb,CAAC;IAED;GACF;GAEA,MAAM,OAAO,UAAU,SAAS,KAAK,eAAe,MAAM;GAE1D,IAAI,aAAa,MAAM;IACrB,OAAO,KAAK,OAAO;IAEnB;GACF;GAEA,MAAM,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM;IACxC,KAAK,KAAK;IACV,KAAK,KAAK;IACV,OAAO;IACP,aAAa;IACb,0BAA0B,KAAK;IAI/B,UAAU,aAAa;GACzB,CAAC;GAED,MAAM,eAAyB,CAAC;GAChC,MAAM,eAAyB,CAAC;GAChC,IAAI,cAAc;GAClB,IAAI,cAAc;GAClB,IAAI,WAAW;GACf,IAAI,YAAY;GAChB,IAAI,UAAU;GAGd,IAAI,SAAS;GAIb,MAAM,QACJ,KAAK,cAAc,UAAa,KAAK,YAAY,IAC7C,iBAAiB;IACf,IAAI,WACF;IAGF,WAAW;IACX,SAAS,MAAM,KAAK,KAAK;GAC3B,GAAG,KAAK,SAAS,IACjB;GAEN,MAAM,gBAAsB;IAC1B,IAAI,WAAW,UACb;IAMF,IAAI,CAAC,QACH,YAAY;IAGd,SAAS,MAAM,KAAK,KAAK;GAC3B;GAKA,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;GAEzD,MAAM,UAAU,aAAqB;IACnC,IAAI,SACF;IAGF,UAAU;IAEV,IAAI,UAAU,QACZ,aAAa,KAAK;IAGpB,QAAQ,oBAAoB,SAAS,OAAO;IAE5C,QAAQ;KACN;KACA,QAAQ,OAAO,OAAO,YAAY,CAAC,CAAC,SAAS,MAAM;KACnD,QAAQ,OAAO,OAAO,YAAY,CAAC,CAAC,SAAS,MAAM;KACnD;KACA;IACF,CAAC;GACH;GAEA,MAAM,QAAQ,GAAG,SAAS,UAAkB;IAC1C,cAAc,WAAW,cAAc,aAAa,KAAK;GAC3D,CAAC;GAED,MAAM,QAAQ,GAAG,SAAS,UAAkB;IAC1C,cAAc,WAAW,cAAc,aAAa,KAAK;GAC3D,CAAC;GAKD,MAAM,GAAG,eAAe;IACtB,OAAO,GAAG;GACZ,CAAC;GAED,MAAM,GAAG,cAAc;IACrB,SAAS;GACX,CAAC;GAED,MAAM,GAAG,UAAU,MAAM,WAAW;IAIlC,IAAI,SAAS,MAAM;KAEjB,OAAO,OADc,WAAW,YAAY,IAAI,EACvB;KAEzB;IACF;IAEA,OAAO,IAAI;GACb,CAAC;EACH,CAAC;CACH;;;;;;;;CASA,AAAO,MAAM,SAAiB,MAA6D;EACzF,MAAM,OAAO,UAAU,SAAS,KAAK,eAAe,MAAM;EAE1D,IAAI,aAAa,MAAM;GACrB,KAAK,SAAS,UAAU,KAAK,OAAO;GAEpC,OAAO;IAAE,MAAM,QAAQ,QAAQ,EAAE,UAAU,IAAI,CAAC;IAAG,YAAY,CAAC;GAAE;EACpE;EAEA,MAAM,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM;GACxC,KAAK,KAAK;GACV,KAAK,KAAK;GACV,OAAO;GACP,aAAa;GACb,0BAA0B,KAAK;GAC/B,UAAU,aAAa;GACvB,OAAO;IAAC;IAAU;IAAQ;GAAM;EAClC,CAAC;EAED,MAAM,gBAAgB,IAAI,cAAc,MAAM;EAC9C,MAAM,gBAAgB,IAAI,cAAc,MAAM;EAC9C,IAAI,WAAW;EAEf,MAAM,aAAmB;GACvB,IAAI,CAAC,UACH,SAAS,MAAM,KAAK,KAAK;EAE7B;EAEA,MAAM,UAAU,uBAAuB,IAAI;EAE3C,MAAM,QAAQ,GAAG,SAAS,UAAkB;GAC1C,MAAM,OAAO,cAAc,MAAM,KAAK;GAEtC,IAAI,SAAS,IACX,KAAK,SAAS,UAAU,IAAI;EAEhC,CAAC;EAED,MAAM,QAAQ,GAAG,SAAS,UAAkB;GAC1C,MAAM,OAAO,cAAc,MAAM,KAAK;GAEtC,IAAI,SAAS,IACX,KAAK,SAAS,UAAU,IAAI;EAEhC,CAAC;EAED,MAAM,OAAO,IAAI,SAA+B,YAAY;GAC1D,MAAM,UAAU,aAA2B;IACzC,IAAI,UACF;IAGF,WAAW;IACX,QAAQ;IAER,MAAM,aAAa,cAAc,IAAI;IACrC,MAAM,aAAa,cAAc,IAAI;IAErC,IAAI,eAAe,IACjB,KAAK,SAAS,UAAU,UAAU;IAGpC,IAAI,eAAe,IACjB,KAAK,SAAS,UAAU,UAAU;IAGpC,QAAQ,EAAE,SAAS,CAAC;GACtB;GAEA,MAAM,GAAG,UAAU,UAAU;IAC3B,KAAK,SAAS,UAAU,iCAAiC,MAAM,SAAS;IACxE,OAAO,GAAG;GACZ,CAAC;GAED,MAAM,GAAG,UAAU,MAAM,WAAW;IAClC,OAAO,SAAS,OAAO,OAAO,WAAW,YAAY,IAAI,KAAK,IAAI;GACpE,CAAC;EACH,CAAC;EAED,OAAO;GAAE,GAAI,MAAM,QAAQ,SAAY,EAAE,KAAK,MAAM,IAAI,IAAI,CAAC;GAAI;GAAM;EAAK;CAC9E;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,qBAAuC;CACrD,OAAO,IAAI,aAAa;AAC1B"}
|
|
1
|
+
{"version":3,"file":"local.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/backends/local.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { lstat, realpath } from \"node:fs/promises\";\nimport { platform } from \"node:process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { fs } from \"@warlock.js/fs\";\nimport { tokenizeCommand } from \"../policy/tokenize-command\";\nimport type { WorkspaceShellInterpreter } from \"../contracts/workspace-policy.type\";\nimport type {\n WorkspaceBackend,\n WorkspaceBackendExecOptions,\n WorkspaceBackendExecResult,\n WorkspaceBackendProcess,\n WorkspaceBackendStartOptions,\n WorkspaceBackendStat,\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 * The direct child may have grandchildren (on Windows it is the `cmd.exe`\n * wrapper around a batch shim; anywhere, an allowed `npm`/`node` can spawn\n * further processes), so signalling the direct child alone could leave a\n * long-running grandchild alive and the `exec` promise unsettled. We\n * therefore kill 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 const killer = spawn(\"taskkill\", [\"/pid\", String(pid), \"/T\", \"/F\"], { windowsHide: true });\n\n // Without a listener a failed spawn (taskkill missing from PATH) is an\n // unhandled 'error' event that crashes the process; fall back to the\n // direct child instead.\n killer.on(\"error\", () => {\n child.kill(\"SIGKILL\");\n });\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 * Argument bytes `cmd.exe` cannot carry safely even inside double quotes:\n * an embedded quote breaks out of the quoted span, `%` triggers variable\n * expansion regardless of quoting, and newlines end the command line. An\n * argv containing any of these is refused rather than risked (the\n * BatBadBut / CVE-2024-24576 class of `cmd.exe` argument smuggling).\n */\nconst WIN32_UNSAFE_ARGUMENT = /[\"%\\r\\n]/;\n\n/**\n * Build the `cmd.exe` invocation that runs an argv on Windows. Batch shims\n * (`npm.cmd`, `npx.cmd`, …) cannot be spawned directly without a shell\n * (Node rejects them since CVE-2024-27980), so the argv is run through\n * `cmd.exe /d /s /c` with every element individually double-quoted —\n * quoted spans are literal to cmd's parser, so pipes/ampersands inside an\n * argument stay argument data. Returns `null` when an element contains a\n * byte cmd cannot quote safely (see {@link WIN32_UNSAFE_ARGUMENT}).\n *\n * The caller must spawn with `windowsVerbatimArguments: true` so Node does\n * not re-quote the already-quoted command line.\n */\nfunction toWin32CmdInvocation(argv: string[]): { file: string; args: string[] } | null {\n if (argv.some((element) => WIN32_UNSAFE_ARGUMENT.test(element))) {\n return null;\n }\n\n const commandLine = argv.map((element) => `\"${element}\"`).join(\" \");\n\n return {\n file: process.env.ComSpec ?? \"cmd.exe\",\n args: [\"/d\", \"/s\", \"/c\", `\"${commandLine}\"`],\n };\n}\n\n/** The shell a POSIX shell-mode line runs through — an absolute path, never a PATH lookup. */\nconst POSIX_SHELL = \"/bin/sh\";\n\n/** How to spawn a command line, or why it cannot be spawned. */\nexport type SpawnPlan =\n | { file: string; args: string[]; windowsVerbatimArguments: boolean }\n | { refused: string };\n\n/**\n * Work out the spawn for a command line.\n *\n * - argv mode: tokenize (no shell semantics) and spawn the argv directly —\n * on Windows through an individually-quoted `cmd.exe` wrapper.\n * - shell mode: `/bin/sh -c <line>`, or `cmd.exe /d /v:off /s /c \"<line>\"`\n * on Windows (`/d` skips AutoRun, `/v:off` disables delayed `!` expansion,\n * `/s` keeps the line verbatim inside the outer quotes). The ops layer has\n * already checked every command in the line.\n */\nexport function planSpawn(\n command: string,\n interpreter: WorkspaceShellInterpreter,\n interpreterPath?: string,\n hostPlatform: NodeJS.Platform = platform,\n): SpawnPlan {\n if (interpreter === \"powershell\") {\n return {\n file: interpreterPath ?? (hostPlatform === \"win32\" ? \"powershell.exe\" : \"pwsh\"),\n args: [\"-NoLogo\", \"-NoProfile\", \"-NonInteractive\", \"-Command\", command],\n windowsVerbatimArguments: false,\n };\n }\n\n if (interpreter === \"bash\") {\n return {\n file: interpreterPath ?? \"bash\",\n args: [\"-c\", command],\n windowsVerbatimArguments: false,\n };\n }\n\n if (interpreter === \"cmd\" || (interpreter === \"sh\" && hostPlatform === \"win32\")) {\n return {\n file: interpreterPath ?? process.env.ComSpec ?? \"cmd.exe\",\n args: [\"/d\", \"/v:off\", \"/s\", \"/c\", `\"${command}\"`],\n windowsVerbatimArguments: true,\n };\n }\n\n if (interpreter === \"sh\") {\n return {\n file: interpreterPath ?? POSIX_SHELL,\n args: [\"-c\", command],\n windowsVerbatimArguments: false,\n };\n }\n\n const argv = tokenizeCommand(command);\n const executable = argv?.[0];\n\n if (argv === null || executable === undefined) {\n return {\n refused:\n \"Command was not executed: it is empty, has unbalanced quotes, or \" +\n \"contains unquoted shell metacharacters (;, &, |, `, $, <, >, \" +\n \"parentheses). Commands run without a shell — pass metacharacters \" +\n \"inside quotes as literal arguments, or run one command at a time.\",\n };\n }\n\n if (platform === \"win32\") {\n const invocation = toWin32CmdInvocation(argv);\n\n if (invocation === null) {\n return {\n refused:\n 'Command was not executed: on Windows, arguments containing \", %, ' +\n \"or newlines cannot be passed to cmd.exe safely.\",\n };\n }\n\n return { ...invocation, windowsVerbatimArguments: true };\n }\n\n return { file: executable, args: argv.slice(1), windowsVerbatimArguments: false };\n}\n\n/**\n * Kill functions of every background process still running, so they can\n * be reaped if the Node process exits without the workspace being disposed\n * (detached process groups would otherwise outlive it).\n */\nconst liveBackgroundKills = new Set<() => void>();\n\nlet exitHookInstalled = false;\n\n/** Track a live background tree; installs (once) the exit hook that kills them. */\nfunction trackBackgroundProcess(kill: () => void): () => void {\n liveBackgroundKills.add(kill);\n\n if (!exitHookInstalled) {\n exitHookInstalled = true;\n process.on(\"exit\", () => {\n for (const killTreeNow of liveBackgroundKills) {\n killTreeNow();\n }\n });\n }\n\n return () => {\n liveBackgroundKills.delete(kill);\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 * Describe a path without following a final symlink (`lstat` — like\n * `realpath`, a filesystem operation `@warlock.js/fs` does not expose).\n */\n public async stat(absPath: string): Promise<WorkspaceBackendStat> {\n const stats = await lstat(absPath);\n const type = stats.isSymbolicLink()\n ? \"symlink\"\n : stats.isDirectory()\n ? \"directory\"\n : stats.isFile()\n ? \"file\"\n : \"other\";\n\n return { type, size: stats.size };\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. In argv mode (the default) the\n * command line is tokenized into an argv (quotes respected, NO shell\n * semantics — see `tokenizeCommand`) and spawned **without a shell**, so\n * metacharacters like `;`, `&&`, `|`, backticks, and `$()` can never\n * chain extra commands past the ops layer's allowlist; a command they\n * appear unquoted in is refused with exit code 127. On Windows the argv\n * runs through a `cmd.exe /d /s /c` wrapper (batch shims like `npm.cmd`\n * cannot be spawned shell-less) with every element individually quoted.\n * With `interpreter: \"sh\"` the line runs through `/bin/sh -c` (or\n * `cmd.exe /d /v:off /s /c` on Windows) — only after the ops layer has\n * checked every command in it.\n * `cwd`, `env`, and the timeout are taken verbatim from the ops layer\n * (the environment is NOT merged with `process.env`). On timeout the\n * process tree is SIGKILLed and `timedOut` is set. When `signal` aborts\n * mid-run the process tree is SIGKILLed the same way and `cancelled` is\n * set; an already-aborted `signal` resolves `cancelled` without spawning.\n * `stdout`/`stderr` are captured and byte-capped per {@link MAX_STREAM_BYTES}.\n *\n * Never rejects for a non-zero exit, a missing executable, a refused\n * command line, a timeout or a cancellation — those are reported through the resolved\n * {@link WorkspaceBackendExecResult} so the ops layer can surface them\n * 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 refuse = (stderr: string): void =>\n resolve({ exitCode: 127, stdout: \"\", stderr, timedOut: false });\n\n const signal = opts.signal;\n\n // Never spawn for an invocation that was cancelled before it started.\n if (signal?.aborted) {\n resolve({\n exitCode: 130,\n stdout: \"\",\n stderr: \"Command was not executed: the invocation was cancelled before it started.\",\n timedOut: false,\n cancelled: true,\n });\n\n return;\n }\n\n const plan = planSpawn(command, opts.interpreter ?? \"none\", opts.interpreterPath);\n\n if (\"refused\" in plan) {\n refuse(plan.refused);\n\n return;\n }\n\n const child = spawn(plan.file, plan.args, {\n cwd: opts.cwd,\n env: opts.env,\n shell: false,\n windowsHide: true,\n windowsVerbatimArguments: plan.windowsVerbatimArguments,\n // POSIX: own process group so a timeout SIGKILL reaps the whole\n // process tree, not just the direct child. Harmless on Windows\n // (ignored; 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 cancelled = false;\n let settled = false;\n // The direct child has exited; 'close' may still be pending while its\n // stdio drains (or a grandchild holds the pipes open).\n let exited = false;\n\n // Timeout and cancellation are mutually exclusive: whichever kills the\n // tree first owns the outcome, so a result is never both.\n const timer =\n opts.timeoutMs !== undefined && opts.timeoutMs > 0\n ? setTimeout(() => {\n if (cancelled) {\n return;\n }\n\n timedOut = true;\n killTree(child.pid, child);\n }, opts.timeoutMs)\n : undefined;\n\n const onAbort = (): void => {\n if (settled || timedOut) {\n return;\n }\n\n // The command already finished: report its real outcome, not a\n // cancellation. Still reap the tree so a grandchild holding the\n // pipes open cannot keep `close` (and this promise) pending.\n if (!exited) {\n cancelled = true;\n }\n\n killTree(child.pid, child);\n };\n\n // Attached synchronously after `spawn`, so there is no window in which\n // an abort could be missed; removed in `settle` so a long-lived signal\n // shared across many commands does not accumulate listeners.\n signal?.addEventListener(\"abort\", onAbort, { once: true });\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 signal?.removeEventListener(\"abort\", onAbort);\n\n resolve({\n exitCode,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut,\n cancelled,\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 executable cannot be found) surfaces as\n // an error event with no exit; report it as a conventional\n // \"command not found\" exit code rather than rejecting.\n child.on(\"error\", () => {\n settle(127);\n });\n\n child.on(\"exit\", () => {\n exited = true;\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 * Start a long-running command and return at once. Spawned like\n * {@link exec} (same argv/shell planning, own process group on POSIX), with\n * stdin closed. Output is decoded per stream with a `StringDecoder`, so a\n * chunk never ends inside a character. `kill()` kills the whole tree; a\n * still-running tree is also killed when the Node process exits.\n */\n public start(command: string, opts: WorkspaceBackendStartOptions): WorkspaceBackendProcess {\n const plan = planSpawn(command, opts.interpreter ?? \"none\", opts.interpreterPath);\n\n if (\"refused\" in plan) {\n opts.onOutput(\"stderr\", plan.refused);\n\n return { done: Promise.resolve({ exitCode: 127 }), kill: () => {} };\n }\n\n const child = spawn(plan.file, plan.args, {\n cwd: opts.cwd,\n env: opts.env,\n shell: false,\n windowsHide: true,\n windowsVerbatimArguments: plan.windowsVerbatimArguments,\n detached: platform !== \"win32\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const stdoutDecoder = new StringDecoder(\"utf8\");\n const stderrDecoder = new StringDecoder(\"utf8\");\n let finished = false;\n\n const kill = (): void => {\n if (!finished) {\n killTree(child.pid, child);\n }\n };\n\n const untrack = trackBackgroundProcess(kill);\n\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n const text = stdoutDecoder.write(chunk);\n\n if (text !== \"\") {\n opts.onOutput(\"stdout\", text);\n }\n });\n\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n const text = stderrDecoder.write(chunk);\n\n if (text !== \"\") {\n opts.onOutput(\"stderr\", text);\n }\n });\n\n const done = new Promise<{ exitCode: number }>((resolve) => {\n const finish = (exitCode: number): void => {\n if (finished) {\n return;\n }\n\n finished = true;\n untrack();\n\n const stdoutTail = stdoutDecoder.end();\n const stderrTail = stderrDecoder.end();\n\n if (stdoutTail !== \"\") {\n opts.onOutput(\"stdout\", stdoutTail);\n }\n\n if (stderrTail !== \"\") {\n opts.onOutput(\"stderr\", stderrTail);\n }\n\n resolve({ exitCode });\n };\n\n child.on(\"error\", (error) => {\n opts.onOutput(\"stderr\", `Command could not be started: ${error.message}`);\n finish(127);\n });\n\n child.on(\"close\", (code, signal) => {\n finish(code === null ? 128 + (signal === \"SIGKILL\" ? 9 : 1) : code);\n });\n });\n\n return { ...(child.pid !== undefined ? { pid: child.pid } : {}), done, kill };\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,6 +1,10 @@
|
|
|
1
|
+
import { WorkspaceEditMatchTier, WorkspaceEditMatching } from "../edit/match.mjs";
|
|
2
|
+
import { WorkspaceOutputKeep, WorkspaceOutputPolicy, WorkspaceTruncation } from "./workspace-output.type.mjs";
|
|
1
3
|
import { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./tool-io.type.mjs";
|
|
4
|
+
import { WorkspaceCommandDecision, WorkspaceCommandRule, WorkspaceShellAsk, WorkspaceShellAskDecision, WorkspaceShellAskReason, WorkspaceShellAskRequest, WorkspaceShellClassification, WorkspaceShellOnAsk } from "./shell-approval.type.mjs";
|
|
5
|
+
import { WorkspaceAfterEditEvent, WorkspaceEditLineEndings, WorkspaceEditPolicy, WorkspaceEditTool, WorkspaceHooks } from "./workspace-edit.type.mjs";
|
|
2
6
|
import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy } from "./workspace-policy.type.mjs";
|
|
3
7
|
import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./workspace-backend.contract.mjs";
|
|
4
8
|
import { WorkspaceOps } from "./workspace-ops.contract.mjs";
|
|
5
9
|
import { Workspace, WorkspaceTools } from "./workspace.contract.mjs";
|
|
6
|
-
export type { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, Workspace, WorkspaceAllowCommand, WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspaceOps, WorkspaceOutputStream, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WorkspaceTools, WriteFileInput, WriteFileResult };
|
|
10
|
+
export type { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, Workspace, WorkspaceAfterEditEvent, WorkspaceAllowCommand, WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandDecision, WorkspaceCommandPattern, WorkspaceCommandRule, WorkspaceEditLineEndings, WorkspaceEditMatchTier, WorkspaceEditMatching, WorkspaceEditPolicy, WorkspaceEditTool, WorkspaceHooks, WorkspaceListPolicy, WorkspaceOps, WorkspaceOutputKeep, WorkspaceOutputPolicy, WorkspaceOutputStream, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellAsk, WorkspaceShellAskDecision, WorkspaceShellAskReason, WorkspaceShellAskRequest, WorkspaceShellClassification, WorkspaceShellInterpreter, WorkspaceShellOnAsk, WorkspaceShellPolicy, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WorkspaceTools, WorkspaceTruncation, WriteFileInput, WriteFileResult };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { WorkspaceCommandPattern } from "./workspace-policy.type.mjs";
|
|
2
|
+
//#region ../../packages/ai-workspace/src/contracts/shell-approval.type.d.ts
|
|
3
|
+
/** What a command rule does to a command that matches it. */
|
|
4
|
+
export type WorkspaceCommandDecision = "allow" | "ask" | "deny";
|
|
5
|
+
/**
|
|
6
|
+
* One entry of `shell.rules`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* { match: "git push", effect: "ask", reason: "Pushing publishes work." }
|
|
10
|
+
*/
|
|
11
|
+
export interface WorkspaceCommandRule {
|
|
12
|
+
/**
|
|
13
|
+
* A string matches a command whose argv starts with those tokens (the
|
|
14
|
+
* executable compared by basename); a RegExp is tested against the argv
|
|
15
|
+
* joined with single spaces. Same semantics as `denyPatterns`.
|
|
16
|
+
*/
|
|
17
|
+
match: WorkspaceCommandPattern;
|
|
18
|
+
/** What happens to a matching command. */
|
|
19
|
+
effect: WorkspaceCommandDecision;
|
|
20
|
+
/** Shown to the model when the command is denied, and to the owner in an ask. */
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Why a command is waiting for a decision. */
|
|
24
|
+
export type WorkspaceShellAskReason =
|
|
25
|
+
/** It matched an `ask` rule, or matched no rule while `onAsk` is set. */
|
|
26
|
+
"rule" |
|
|
27
|
+
/** The line uses syntax the policy cannot read, so no command in it can be checked. */
|
|
28
|
+
"unparsed" |
|
|
29
|
+
/** It runs another command the policy cannot see into (`bash -c`, `xargs`, `iex`, …). */
|
|
30
|
+
"nested";
|
|
31
|
+
/** One command (or unreadable line) `shell.onAsk` is asked about. */
|
|
32
|
+
export interface WorkspaceShellAsk {
|
|
33
|
+
/** The command's argv; for an `"unparsed"` ask, the whole line as one element. */
|
|
34
|
+
argv: readonly string[];
|
|
35
|
+
/** The `ask` rule that matched, when one did. */
|
|
36
|
+
rule?: WorkspaceCommandRule;
|
|
37
|
+
/** Why it is being asked about. */
|
|
38
|
+
reason: WorkspaceShellAskReason;
|
|
39
|
+
}
|
|
40
|
+
/** What `shell.onAsk` is told: one call per line, covering every ask in it. */
|
|
41
|
+
export interface WorkspaceShellAskRequest {
|
|
42
|
+
/** The full command line. */
|
|
43
|
+
command: string;
|
|
44
|
+
/** Every simple command that needs a decision (never empty). */
|
|
45
|
+
asks: readonly WorkspaceShellAsk[];
|
|
46
|
+
/** The invocation's cancellation signal, when it has one. */
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The host's decision. `allow: false` refuses the line (reason
|
|
51
|
+
* `"approval-denied"`). `remember` adds a session allow rule for that
|
|
52
|
+
* pattern — shared with the workspace's `readonly()` and `scope()` views,
|
|
53
|
+
* lower than any configured deny, and above an `ask` rule.
|
|
54
|
+
*/
|
|
55
|
+
export interface WorkspaceShellAskDecision {
|
|
56
|
+
allow: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* One pattern or several (a line such as `git status -s && npm test` asks
|
|
59
|
+
* about two commands). Each is added once; list them with
|
|
60
|
+
* `workspace.shell.remembered()` and remove one with `forget(pattern)`.
|
|
61
|
+
*/
|
|
62
|
+
remember?: WorkspaceCommandPattern | readonly WorkspaceCommandPattern[];
|
|
63
|
+
/**
|
|
64
|
+
* Why the line was refused (used when `allow` is `false`). Appended to the
|
|
65
|
+
* refusal the model reads, cut at 500 characters with a `[truncated]` marker.
|
|
66
|
+
*/
|
|
67
|
+
reason?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Decides on commands that need approval. A throw or a rejection refuses the
|
|
71
|
+
* line (fail closed).
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* onAsk: async ({ asks }) => ({ allow: await confirm(asks[0].argv.join(" ")), remember: "git push" })
|
|
75
|
+
*/
|
|
76
|
+
export type WorkspaceShellOnAsk = (request: WorkspaceShellAskRequest) => Promise<WorkspaceShellAskDecision>;
|
|
77
|
+
/**
|
|
78
|
+
* What `workspace.shell.classify(line)` returns: the simple commands the
|
|
79
|
+
* line would run, in order, without running or checking anything.
|
|
80
|
+
*/
|
|
81
|
+
export interface WorkspaceShellClassification {
|
|
82
|
+
/** The argv of every simple command (substitutions and pipeline members included). */
|
|
83
|
+
commands: string[][];
|
|
84
|
+
/** Set when the line could not be read with confidence: why. `commands` is then empty. */
|
|
85
|
+
opaque?: string;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//# sourceMappingURL=shell-approval.type.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-approval.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/shell-approval.type.ts"],"mappings":";;;YAGY;;;;;;;iBAQK;;;;;;EAMf,OAAO;;EAEP,QAAQ;;EAER;;;YAIU;;;;;;;;iBASK;;EAEf;;EAEA,OAAO;;EAEP,QAAQ;;;iBAIO;;EAEf;;EAEA,eAAe;;EAEf,SAAS;;;;;;;;iBASM;EACf;;;;;;EAMA,WAAW,mCAAmC;;;;;EAK9C;;;;;;;;;YAUU,uBACV,SAAS,6BACN,QAAQ;;;;;iBAMI;;EAEf;;EAEA"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { WorkspaceEditMatchTier } from "../edit/match.mjs";
|
|
2
|
+
import { WorkspaceTruncation } from "./workspace-output.type.mjs";
|
|
1
3
|
//#region ../../packages/ai-workspace/src/contracts/tool-io.type.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Input/output shapes for the workspace tools and the direct
|
|
@@ -52,6 +54,10 @@ export interface ReadFileResult {
|
|
|
52
54
|
truncated: boolean;
|
|
53
55
|
/** True when `read.maxBytes` cut the window short. */
|
|
54
56
|
bytesCapped: boolean;
|
|
57
|
+
/** Set when `output.spillDir` is on and `bytesCapped`: where the full window was saved. */
|
|
58
|
+
truncation?: WorkspaceTruncation;
|
|
59
|
+
/** Set with `truncation.path`: the file holding the full window (absolute paths). */
|
|
60
|
+
spilledTo?: string[];
|
|
55
61
|
/** UTF-8 byte length of the full file. */
|
|
56
62
|
totalBytes: number;
|
|
57
63
|
/**
|
|
@@ -85,6 +91,10 @@ export interface EditFileResult {
|
|
|
85
91
|
replacements: number;
|
|
86
92
|
/** SHA-256 of the file content after the edit. */
|
|
87
93
|
hash: string;
|
|
94
|
+
/** Set only when the text was found by a loose tier (`"unicode"`, `"whitespace"`). */
|
|
95
|
+
matchTier?: Exclude<WorkspaceEditMatchTier, "exact">;
|
|
96
|
+
/** Output of the policy's `hooks.afterEdit`, between `[diagnostics]` lines. */
|
|
97
|
+
diagnostics?: string;
|
|
88
98
|
}
|
|
89
99
|
/** One replacement inside a {@link MultiEditInput}. */
|
|
90
100
|
export interface MultiEditOperation {
|
|
@@ -119,6 +129,10 @@ export interface MultiEditResult {
|
|
|
119
129
|
replacements: number;
|
|
120
130
|
/** SHA-256 of the file content after all edits. */
|
|
121
131
|
hash: string;
|
|
132
|
+
/** The loosest tier any edit needed; set only when it was not exact. */
|
|
133
|
+
matchTier?: Exclude<WorkspaceEditMatchTier, "exact">;
|
|
134
|
+
/** Output of the policy's `hooks.afterEdit`, between `[diagnostics]` lines. */
|
|
135
|
+
diagnostics?: string;
|
|
122
136
|
}
|
|
123
137
|
/**
|
|
124
138
|
* Input to the `apply_patch` tool / `applyPatch()` direct method — a unified
|
|
@@ -150,6 +164,8 @@ export interface ApplyPatchResult {
|
|
|
150
164
|
files: ApplyPatchFileResult[];
|
|
151
165
|
/** True when nothing was written because `dryRun` was set. */
|
|
152
166
|
dryRun: boolean;
|
|
167
|
+
/** Output of the policy's `hooks.afterEdit` for the written files. */
|
|
168
|
+
diagnostics?: string;
|
|
153
169
|
}
|
|
154
170
|
/** Input to the `write_file` tool / `writeFile()` direct method. */
|
|
155
171
|
export interface WriteFileInput {
|
|
@@ -166,6 +182,8 @@ export interface WriteFileResult {
|
|
|
166
182
|
bytesWritten: number;
|
|
167
183
|
/** SHA-256 of the written content. */
|
|
168
184
|
hash: string;
|
|
185
|
+
/** Output of the policy's `hooks.afterEdit`, between `[diagnostics]` lines. */
|
|
186
|
+
diagnostics?: string;
|
|
169
187
|
}
|
|
170
188
|
/** Options for the `exec()` direct method. */
|
|
171
189
|
export interface ExecOptions {
|
|
@@ -271,8 +289,15 @@ export interface ShellOutputResult extends ShellJobInfo {
|
|
|
271
289
|
* buffer holds the newest `shell.maxOutputBytes`).
|
|
272
290
|
*/
|
|
273
291
|
droppedBytes: number;
|
|
274
|
-
/** True when `droppedBytes > 0
|
|
292
|
+
/** True when `droppedBytes > 0` or `output` limits clipped a stream. */
|
|
275
293
|
truncated: boolean;
|
|
294
|
+
/** What `output` limits left out of each stream; set only when it clipped one. */
|
|
295
|
+
truncation?: {
|
|
296
|
+
stdout?: WorkspaceTruncation;
|
|
297
|
+
stderr?: WorkspaceTruncation;
|
|
298
|
+
};
|
|
299
|
+
/** Absolute paths of the files holding the complete output of each stream that spilled to disk. */
|
|
300
|
+
spilledTo?: string[];
|
|
276
301
|
}
|
|
277
302
|
/** Input to the `shell_kill` tool. */
|
|
278
303
|
export interface ShellKillInput {
|
|
@@ -291,6 +316,13 @@ export interface RunShellResult {
|
|
|
291
316
|
truncated: boolean;
|
|
292
317
|
/** True when the command was SIGKILLed for exceeding its timeout. */
|
|
293
318
|
timedOut: boolean;
|
|
319
|
+
/** What each clipped stream left out; set only for a stream that was clipped. */
|
|
320
|
+
truncation?: {
|
|
321
|
+
stdout?: WorkspaceTruncation;
|
|
322
|
+
stderr?: WorkspaceTruncation;
|
|
323
|
+
};
|
|
324
|
+
/** Absolute paths of the files holding the complete output of each stream that spilled to disk. */
|
|
325
|
+
spilledTo?: string[];
|
|
294
326
|
/**
|
|
295
327
|
* True when the invocation's `signal` aborted: before start (nothing ran)
|
|
296
328
|
* or mid-run (the process tree was killed). Never true together with
|
|
@@ -331,6 +363,12 @@ export interface GrepResult {
|
|
|
331
363
|
matches: GrepMatch[];
|
|
332
364
|
/** Total number of matches (== `matches.length` unless capped). */
|
|
333
365
|
total: number;
|
|
366
|
+
/** True when `output` limits cut `matches` short (the full list was saved). */
|
|
367
|
+
truncated?: boolean;
|
|
368
|
+
/** What was left out; set with `truncated`. */
|
|
369
|
+
truncation?: WorkspaceTruncation;
|
|
370
|
+
/** Absolute path of the file holding the complete output, when it spilled to disk. */
|
|
371
|
+
spilledTo?: string[];
|
|
334
372
|
}
|
|
335
373
|
/** Input to the `list_dir` tool. */
|
|
336
374
|
export interface ListDirInput {
|
|
@@ -379,6 +417,12 @@ export interface GlobInput {
|
|
|
379
417
|
export interface GlobResult {
|
|
380
418
|
/** Workspace-relative paths matching the pattern, sorted. */
|
|
381
419
|
paths: string[];
|
|
420
|
+
/** True when `output` limits cut `paths` short (the full list was saved). */
|
|
421
|
+
truncated?: boolean;
|
|
422
|
+
/** What was left out; set with `truncated`. */
|
|
423
|
+
truncation?: WorkspaceTruncation;
|
|
424
|
+
/** Absolute path of the file holding the complete output, when it spilled to disk. */
|
|
425
|
+
spilledTo?: string[];
|
|
382
426
|
}
|
|
383
427
|
/** State of one todo item. */
|
|
384
428
|
export type WorkspaceTodoStatus = "pending" | "in_progress" | "completed";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-io.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/tool-io.type.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool-io.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/tool-io.type.ts"],"mappings":";;;;;;;;;;;;;;YAeY;;iBAiBK;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;;iBAee;;EAEf;;EAEA;;;;;EAKA;;EAEA;;;;;;EAMA;;EAEA;;EAEA,aAAa;;EAEb;;EAEA;;;;;;EAMA;;;;;;iBAOe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA,YAAY,QAAQ;;EAEpB;;;iBAIe;;EAEf;;EAEA;;EAEA;;;;;;;;iBASe;;EAEf;;EAEA,OAAO;;EAEP;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA,YAAY,QAAQ;;EAEpB;;;;;;;iBAQe;;EAEf;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;;EAEf,OAAO;;EAEP;;EAEA;;;iBAIe;;EAEf;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;;iBAIe;;EAEf;;;;;;EAMA,SAAS;;;iBAIM;;EAEf;;EAEA;;;;;EAKA;;;;;;;;;;;YAYU;;iBAGK;;EAEf;;EAEA;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;;EAEA;;;iBAIe,iCAAiC;;EAEhD;;;iBAIe;;;;;;EAMf;;;iBAIe;;EAEf;;;;;EAKA;;;;;EAKA;;;iBAIe;;EAEf;;EAEA;;EAEA,SAAS;;;iBAIM,0BAA0B;;EAEzC;;EAEA;;;;;EAKA;;;;;EAKA;;EAEA;;EAEA;IAAe,SAAS;IAAqB,SAAS;;;EAEtD;;;iBAIe;;EAEf;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;IAAe,SAAS;IAAqB,SAAS;;;EAEtD;;;;;;EAMA;;;;;;;iBAQe;;EAEf;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf,SAAS;;EAET;;EAEA;;EAEA,aAAa;;EAEb;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;;YAIU;;iBAGK;;EAEf;;EAEA,MAAM;;EAEN;;EAEA;;;iBAIe;;EAEf;;EAEA,SAAS;;EAET;;;iBAIe;;EAEf;;;iBAIe;;EAEf;;EAEA;;EAEA,aAAa;;EAEb;;;YAIU;;iBAGK;;EAEf;;EAEA;;EAEA,QAAQ;;;iBAIO;;EAEf;;EAEA;;EAEA,QAAQ;;;iBAIO;;EAEf,OAAO;;;iBAIQ;;EAEf,OAAO;;EAEP,QAAQ,OAAO"}
|
|
@@ -47,12 +47,20 @@ export interface WorkspaceBackendExecOptions {
|
|
|
47
47
|
/**
|
|
48
48
|
* How to run `command`: `"none"` (default) tokenizes it into an argv and
|
|
49
49
|
* spawns it without a shell; `"sh"` hands the whole line to `/bin/sh -c`
|
|
50
|
-
* (`cmd.exe /d /v:off /s /c` on Windows)
|
|
50
|
+
* (`cmd.exe /d /v:off /s /c` on Windows); `"bash"`, `"powershell"` and
|
|
51
|
+
* `"cmd"` hand it to that shell (`-c`, `-NoLogo -NoProfile -NonInteractive
|
|
52
|
+
* -Command`, `/d /v:off /s /c`). The ops layer has already
|
|
51
53
|
* checked every command in the line against the policy; a backend that
|
|
52
54
|
* does not support `"sh"` must refuse the command rather than run it
|
|
53
55
|
* another way.
|
|
54
56
|
*/
|
|
55
57
|
interpreter?: WorkspaceShellInterpreter;
|
|
58
|
+
/**
|
|
59
|
+
* The shell executable to run `"bash"`, `"powershell"` and `"sh"` lines
|
|
60
|
+
* with (resolved by the ops layer from `shell.interpreterPath` or the
|
|
61
|
+
* host's `PATH`); absent ⇒ the backend uses the shell's usual name.
|
|
62
|
+
*/
|
|
63
|
+
interpreterPath?: string;
|
|
56
64
|
}
|
|
57
65
|
/** Which stream a background command's output chunk came from. */
|
|
58
66
|
export type WorkspaceOutputStream = "stdout" | "stderr";
|
|
@@ -64,6 +72,8 @@ export interface WorkspaceBackendStartOptions {
|
|
|
64
72
|
env?: Record<string, string>;
|
|
65
73
|
/** See {@link WorkspaceBackendExecOptions.interpreter}. */
|
|
66
74
|
interpreter?: WorkspaceShellInterpreter;
|
|
75
|
+
/** See {@link WorkspaceBackendExecOptions.interpreterPath}. */
|
|
76
|
+
interpreterPath?: string;
|
|
67
77
|
/**
|
|
68
78
|
* Receives output as it arrives, decoded as UTF-8 and never split inside a
|
|
69
79
|
* character. Called synchronously or later — never after `done` settles.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-backend.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-backend.contract.ts"],"mappings":";;;;;;;;iBAQiB;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;EAOA;;;;;;;iBAQe;;EAEf;;EAEA;;EAEA,MAAM;;;;;;;;;EASN,SAAS
|
|
1
|
+
{"version":3,"file":"workspace-backend.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-backend.contract.ts"],"mappings":";;;;;;;;iBAQiB;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;EAOA;;;;;;;iBAQe;;EAEf;;EAEA;;EAEA,MAAM;;;;;;;;;EASN,SAAS;;;;;;;;;;;EAWT,cAAc;;;;;;EAMd;;;YAIU;;iBAGK;;EAEf;;EAEA,MAAM;;EAEN,cAAc;;EAEd;;;;;EAKA,SAAS,QAAQ,uBAAuB;;;iBAIzB;;WAEN;;;;;WAKA,MAAM;IAAU;;;EAEzB;;;iBAIe;;EAEf;;EAEA;;;;;;;;;;;;;;;;;;;;iBAqBe;;EAEf,SAAS,kBAAkB;;EAE3B,UAAU,iBAAiB,kBAAkB;;EAE7C,OAAO,kBAAkB;;EAEzB,MAAM,kBAAkB;;EAExB,OAAO,kBAAkB;;EAEzB,KAAK,iBAAiB;;EAEtB,SAAS,kBAAkB;;EAE3B,KAAK,iBAAiB,OAAO,8BAA8B,QAAQ;;;;;;EAMnE,MAAM,kBAAkB,QAAQ;;;;;;EAMhC,OAAO,iBAAiB,MAAM,+BAA+B"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { WorkspaceEditMatchTier, WorkspaceEditMatching } from "../edit/match.mjs";
|
|
2
|
+
//#region ../../packages/ai-workspace/src/contracts/workspace-edit.type.d.ts
|
|
3
|
+
/** Line endings of the text an edit writes. */
|
|
4
|
+
export type WorkspaceEditLineEndings = "preserve" | "lf" | "crlf";
|
|
5
|
+
/**
|
|
6
|
+
* Edit sub-policy for `edit_file` / `multi_edit` and their direct methods.
|
|
7
|
+
*/
|
|
8
|
+
export interface WorkspaceEditPolicy {
|
|
9
|
+
/**
|
|
10
|
+
* How `oldString` is found. `"tiered"` (default) tries the exact text first,
|
|
11
|
+
* then the text with line endings normalised, with look-alike Unicode
|
|
12
|
+
* punctuation folded, and finally line by line ignoring trailing
|
|
13
|
+
* whitespace; a loose match must be unique even with `replaceAll`.
|
|
14
|
+
* `"exact"` accepts only the text byte for byte.
|
|
15
|
+
*/
|
|
16
|
+
matching?: WorkspaceEditMatching;
|
|
17
|
+
/**
|
|
18
|
+
* Most lines of nearby file text a "not found" error shows to help the next
|
|
19
|
+
* attempt. Default `12`; `0` turns hints off. A hint never exceeds
|
|
20
|
+
* `read.maxBytes`, and a file the jail refuses never gets one.
|
|
21
|
+
*/
|
|
22
|
+
missHintLines?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Line endings of replacement text. `"preserve"` (default) uses the file's
|
|
25
|
+
* dominant ending; `"lf"` / `"crlf"` force one.
|
|
26
|
+
*/
|
|
27
|
+
lineEndings?: WorkspaceEditLineEndings;
|
|
28
|
+
}
|
|
29
|
+
/** Which tool changed files, as reported to {@link WorkspaceHooks.afterEdit}. */
|
|
30
|
+
export type WorkspaceEditTool = "write_file" | "edit_file" | "multi_edit" | "apply_patch";
|
|
31
|
+
/** What {@link WorkspaceHooks.afterEdit} is told about a successful write. */
|
|
32
|
+
export interface WorkspaceAfterEditEvent {
|
|
33
|
+
/** Workspace-relative paths written by the call (deleted files are not listed). */
|
|
34
|
+
paths: readonly string[];
|
|
35
|
+
/** The tool that wrote them. */
|
|
36
|
+
tool: WorkspaceEditTool;
|
|
37
|
+
/** Cancellation of the agent run, when the caller has one. */
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Host callbacks around workspace changes.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* hooks: {
|
|
45
|
+
* afterEdit: async ({ paths }) => await typecheck(paths),
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
export interface WorkspaceHooks {
|
|
49
|
+
/**
|
|
50
|
+
* Runs once per call after `write_file`, `edit_file`, `multi_edit` or
|
|
51
|
+
* `apply_patch` has written files (not on a patch `dryRun`). Return text
|
|
52
|
+
* such as compiler or linter output and it comes back on the result as
|
|
53
|
+
* `diagnostics`, between `[diagnostics]` and `[/diagnostics]` lines, so the
|
|
54
|
+
* model sees the effect of its change at once. A throw, or no answer within
|
|
55
|
+
* `afterEditTimeoutMs`, reports "diagnostics unavailable" and never fails
|
|
56
|
+
* the edit.
|
|
57
|
+
*/
|
|
58
|
+
afterEdit?: (event: WorkspaceAfterEditEvent) => Promise<string | void> | string | void;
|
|
59
|
+
/** Longest diagnostics text kept; more is cut with a marker. Default `4000`. */
|
|
60
|
+
afterEditMaxChars?: number;
|
|
61
|
+
/** How long to wait for `afterEdit`, in ms. Default `10000`. */
|
|
62
|
+
afterEditTimeoutMs?: number;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export type { WorkspaceEditMatchTier, WorkspaceEditMatching };
|
|
66
|
+
//# sourceMappingURL=workspace-edit.type.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-edit.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-edit.type.ts"],"mappings":";;;YAKY;;;;iBAKK;;;;;;;;EAQf,WAAW;;;;;;EAMX;;;;;EAKA,cAAc;;;YAIJ;;iBAGK;;EAEf;;EAEA,MAAM;;EAEN,SAAS;;;;;;;;;;iBAWM;;;;;;;;;;EAUf,aAAa,OAAO,4BAA4B;;EAEhD;;EAEA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GrepResult, ListDirOptions, ListDirResult, MultiEditInput, MultiEditResult, ReadFileResult, RunShellResult, ShellJobInfo, ShellOutputOptions, ShellOutputResult, StartShellOptions } from "./tool-io.type.mjs";
|
|
1
|
+
import { ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobResult, GrepResult, ListDirOptions, ListDirResult, MultiEditInput, MultiEditResult, ReadFileResult, RunShellResult, ShellJobInfo, ShellOutputOptions, ShellOutputResult, StartShellOptions } from "./tool-io.type.mjs";
|
|
2
2
|
//#region ../../packages/ai-workspace/src/contracts/workspace-ops.contract.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The **policy-enforced** operation layer that sits between the dumb
|
|
@@ -34,6 +34,7 @@ export interface WorkspaceOps {
|
|
|
34
34
|
writeFile(path: string, content: string): Promise<{
|
|
35
35
|
hash: string;
|
|
36
36
|
bytesWritten: number;
|
|
37
|
+
diagnostics?: string;
|
|
37
38
|
}>;
|
|
38
39
|
/**
|
|
39
40
|
* Apply an exact-string edit under the read-before-edit guard:
|
|
@@ -87,6 +88,11 @@ export interface WorkspaceOps {
|
|
|
87
88
|
}): Promise<GrepResult>;
|
|
88
89
|
/** Resolve a glob to matching workspace-relative paths within the jail. */
|
|
89
90
|
glob(pattern: string): Promise<string[]>;
|
|
91
|
+
/**
|
|
92
|
+
* Like {@link WorkspaceOps.glob}, but returns the paths with the `output`
|
|
93
|
+
* truncation metadata. Optional: the `glob` tool falls back to `glob()`.
|
|
94
|
+
*/
|
|
95
|
+
globResult?(pattern: string): Promise<GlobResult>;
|
|
90
96
|
/** Whether a jailed path exists (file or directory). */
|
|
91
97
|
exists(path: string): Promise<boolean>;
|
|
92
98
|
/** Create a directory (and parents) at a jailed path; idempotent. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-ops.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-ops.contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"workspace-ops.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-ops.contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAuCiB;;;;;;;EAOf,SAAS,cAAc;IAAS;IAAiB;MAAmB,QAAQ;;EAG5E,UACE,cACA,kBACC;IAAU;IAAc;IAAsB;;;;;;;EAOjD,SAAS,OAAO,gBAAgB,QAAQ;;;;;;;EAQxC,UAAU,OAAO,iBAAiB,QAAQ;;;;;;;;;EAU1C,YAAY,OAAO,kBAAkB,QAAQ;;;;;;;EAQ7C,KAAK,iBAAiB,OAAO,cAAc,QAAQ;;;;;EAMnD,WAAW,iBAAiB,OAAO,oBAAoB,QAAQ;;EAG/D,YAAY,YAAY,OAAO,qBAAqB,QAAQ;;EAG5D,UAAU,aAAa,QAAQ;;EAG/B,cAAc;;;;;EAMd,QAAQ,eAAe,OAAO,iBAAiB,QAAQ;;EAGvD,KAAK,iBAAiB;IAAS;IAAe;MAAyB,QAAQ;;EAG/E,KAAK,kBAAkB;;;;;EAMvB,YAAY,kBAAkB,QAAQ;;EAGtC,OAAO,eAAe;;EAGtB,MAAM,eAAe;;EAGrB,OAAO,eAAe;;;;;;EAOtB,WAAW"}
|