@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +14 -1
  3. package/cjs/index.cjs +1847 -141
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts.map +1 -1
  6. package/esm/backends/local.mjs +36 -22
  7. package/esm/backends/local.mjs.map +1 -1
  8. package/esm/contracts/index.d.mts +5 -1
  9. package/esm/contracts/shell-approval.type.d.mts +88 -0
  10. package/esm/contracts/shell-approval.type.d.mts.map +1 -0
  11. package/esm/contracts/tool-io.type.d.mts +45 -1
  12. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  13. package/esm/contracts/workspace-backend.contract.d.mts +11 -1
  14. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  15. package/esm/contracts/workspace-edit.type.d.mts +66 -0
  16. package/esm/contracts/workspace-edit.type.d.mts.map +1 -0
  17. package/esm/contracts/workspace-ops.contract.d.mts +7 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  19. package/esm/contracts/workspace-output.type.d.mts +57 -0
  20. package/esm/contracts/workspace-output.type.d.mts.map +1 -0
  21. package/esm/contracts/workspace-policy.type.d.mts +38 -1
  22. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  23. package/esm/contracts/workspace.contract.d.mts +23 -1
  24. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  25. package/esm/edit/apply-replacement.mjs +58 -0
  26. package/esm/edit/apply-replacement.mjs.map +1 -0
  27. package/esm/edit/match.d.mts +16 -0
  28. package/esm/edit/match.d.mts.map +1 -0
  29. package/esm/edit/match.mjs +214 -0
  30. package/esm/edit/match.mjs.map +1 -0
  31. package/esm/errors.d.mts +12 -1
  32. package/esm/errors.d.mts.map +1 -1
  33. package/esm/errors.mjs +1 -0
  34. package/esm/errors.mjs.map +1 -1
  35. package/esm/hooks/after-edit.mjs +36 -0
  36. package/esm/hooks/after-edit.mjs.map +1 -0
  37. package/esm/index.d.mts +7 -2
  38. package/esm/index.mjs +3 -2
  39. package/esm/ops.d.mts +3 -1
  40. package/esm/ops.d.mts.map +1 -1
  41. package/esm/ops.mjs +213 -75
  42. package/esm/ops.mjs.map +1 -1
  43. package/esm/output/spill-dir.mjs +15 -0
  44. package/esm/output/spill-dir.mjs.map +1 -0
  45. package/esm/output/spill.mjs +223 -0
  46. package/esm/output/spill.mjs.map +1 -0
  47. package/esm/policy/approval-helpers.mjs +22 -0
  48. package/esm/policy/approval-helpers.mjs.map +1 -0
  49. package/esm/policy/policy.d.mts +3 -1
  50. package/esm/policy/policy.d.mts.map +1 -1
  51. package/esm/policy/policy.mjs +12 -4
  52. package/esm/policy/policy.mjs.map +1 -1
  53. package/esm/policy/powershell-aliases.mjs +275 -0
  54. package/esm/policy/powershell-aliases.mjs.map +1 -0
  55. package/esm/policy/powershell-parser.mjs +420 -0
  56. package/esm/policy/powershell-parser.mjs.map +1 -0
  57. package/esm/policy/shell-check.d.mts +32 -2
  58. package/esm/policy/shell-check.d.mts.map +1 -1
  59. package/esm/policy/shell-check.mjs +183 -31
  60. package/esm/policy/shell-check.mjs.map +1 -1
  61. package/esm/policy/shell-interpreter.d.mts +63 -0
  62. package/esm/policy/shell-interpreter.d.mts.map +1 -0
  63. package/esm/policy/shell-interpreter.mjs +123 -0
  64. package/esm/policy/shell-interpreter.mjs.map +1 -0
  65. package/esm/policy/shell-parser.d.mts +33 -0
  66. package/esm/policy/shell-parser.d.mts.map +1 -0
  67. package/esm/policy/shell-parser.mjs +3 -0
  68. package/esm/policy/shell-parser.mjs.map +1 -1
  69. package/esm/tools/glob.mjs +1 -0
  70. package/esm/tools/glob.mjs.map +1 -1
  71. package/esm/tools/run-shell.d.mts +10 -0
  72. package/esm/tools/run-shell.d.mts.map +1 -1
  73. package/esm/tools/run-shell.mjs +19 -1
  74. package/esm/tools/run-shell.mjs.map +1 -1
  75. package/esm/tools/run-tests.d.mts +7 -0
  76. package/esm/tools/run-tests.d.mts.map +1 -1
  77. package/esm/tools/run-tests.mjs +5 -4
  78. package/esm/tools/run-tests.mjs.map +1 -1
  79. package/esm/tools/write-file.mjs +3 -2
  80. package/esm/tools/write-file.mjs.map +1 -1
  81. package/esm/workspace.d.mts.map +1 -1
  82. package/esm/workspace.mjs +27 -5
  83. package/esm/workspace.mjs.map +1 -1
  84. package/llms-full.txt +70 -17
  85. package/package.json +3 -3
  86. package/skills/use-a-workspace/SKILL.md +70 -17
package/esm/ops.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ops.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fs } from \"@warlock.js/fs\";\nimport { WorkspaceEditError, WorkspacePolicyError } from \"./errors\";\nimport { globToRegExp, isInside, matchesAnyGlob } from \"./policy/jail\";\nimport { buildCommandEnv, resolveInJail } from \"./policy/policy\";\nimport { checkShellCommand } from \"./policy/shell-check\";\nimport { DEFAULT_MAX_BACKGROUND_JOBS, ShellJobs } from \"./shell-jobs\";\nimport { executePatch } from \"./patch/execute-patch\";\nimport type {\n ApplyPatchInput,\n ApplyPatchResult,\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepMatch,\n GrepResult,\n ListDirEntry,\n ListDirEntryType,\n ListDirOptions,\n ListDirResult,\n MultiEditInput,\n MultiEditOperation,\n MultiEditResult,\n ReadFileResult,\n RunShellResult,\n ShellJobInfo,\n ShellOutputOptions,\n ShellOutputResult,\n StartShellOptions,\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/** Default `list.ignore` when the policy sets none. */\nconst DEFAULT_LIST_IGNORE: readonly string[] = [\"node_modules\", \".git\"];\n/** Default deepest `depth` a listing may ask for. */\nconst DEFAULT_LIST_MAX_DEPTH = 5;\n/** Default most entries one listing returns. */\nconst DEFAULT_LIST_MAX_ENTRIES = 500;\n/** Most edits one `multiEdit` call may carry. */\nexport const MAX_MULTI_EDITS = 100;\n/**\n * Exit code reported for a command refused because its signal was already\n * aborted (the shell convention for an interrupted command). Callers should\n * branch on `cancelled`, not on this number.\n */\nconst CANCELLED_BEFORE_START_EXIT_CODE = 130;\n/**\n * Hard ceiling on `grep` pattern length. A model-controlled regex has no\n * legitimate reason to be this long; longer patterns are rejected outright\n * rather than compiled.\n */\nconst MAX_GREP_PATTERN_LENGTH = 200;\n/**\n * Hard ceiling on the number of characters of a single line handed to\n * `RegExp#test` during a `grep`. Catastrophic backtracking is exponential\n * in input length, so bounding the input scanned per call bounds the\n * worst-case time a single pathological line can cost — lines longer than\n * this are skipped rather than scanned.\n */\nconst MAX_GREP_LINE_SCAN_LENGTH = 2000;\n\n/** The `cat -n` prefix for a line: right-aligned number (min width 6) and a tab. */\nfunction linePrefix(lineNumber: number): string {\n return `${String(lineNumber).padStart(6, \" \")}\\t`;\n}\n\n/**\n * Clip `value` to at most `maxBytes` UTF-8 bytes without splitting a\n * character: the cut backs off over continuation bytes (`10xxxxxx`) to the\n * start of the character it would have split, so the result never carries\n * a replacement character.\n */\nfunction clipUtf8(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 let cut = Math.max(0, maxBytes);\n\n while (cut > 0 && ((bytes[cut] ?? 0) & 0xc0) === 0x80) {\n cut--;\n }\n\n return { value: bytes.subarray(0, cut).toString(\"utf8\"), truncated: true };\n}\n\n/**\n * Number a window of lines `cat -n` style, keeping the joined output within\n * `maxBytes` UTF-8 bytes (prefixes and newlines included) when a cap is set.\n * A line that does not fit whole is clipped on a character boundary; it is\n * still counted as returned when its prefix and at least one character fit.\n */\nfunction numberWindow(\n lines: string[],\n startLine: number,\n maxBytes: number | undefined,\n): { content: string; returnedLines: number; bytesCapped: boolean } {\n const parts: string[] = [];\n let used = 0;\n\n for (let index = 0; index < lines.length; index++) {\n const prefix = linePrefix(startLine + index);\n const numbered = prefix + lines[index];\n const separator = parts.length > 0 ? 1 : 0;\n\n if (maxBytes === undefined) {\n parts.push(numbered);\n\n continue;\n }\n\n const cost = separator + Buffer.byteLength(numbered, \"utf8\");\n\n if (used + cost <= maxBytes) {\n parts.push(numbered);\n used += cost;\n\n continue;\n }\n\n const room = maxBytes - used - separator - Buffer.byteLength(prefix, \"utf8\");\n const clipped = room > 0 ? clipUtf8(lines[index] ?? \"\", room).value : \"\";\n\n if (clipped.length > 0) {\n parts.push(prefix + clipped);\n }\n\n return { content: parts.join(\"\\n\"), returnedLines: parts.length, bytesCapped: true };\n }\n\n return { content: parts.join(\"\\n\"), returnedLines: parts.length, bytesCapped: false };\n}\n\n/**\n * Clip a captured stream at `maxBytes` (measured in UTF-8 bytes), on a\n * character boundary. Returns the possibly-clipped string plus whether\n * clipping occurred.\n */\nfunction capOutput(value: string, maxBytes: number): { value: string; truncated: boolean } {\n return clipUtf8(value, maxBytes);\n}\n\n/**\n * A quantifier: `+`, `*`, `?`, or a `{m,n}` bound.\n */\nconst QUANTIFIER_SOURCE = String.raw`[+*?]|\\{\\d*,?\\d*\\}`;\n\n/**\n * Heuristic catastrophic-backtracking detector: flags a quantified group\n * that itself contains a quantifier (e.g. `(a+)+`, `(a*)*`, `(.+)*`) —\n * the classic exponential-blowup shape. Not a full regex-safety analyzer\n * (it won't catch every ReDoS shape, e.g. quantified alternation like\n * `(a|a)+`), but it rejects the shape an agent is most likely to emit,\n * intentionally or via prompt injection.\n */\nconst NESTED_QUANTIFIER_PATTERN = new RegExp(\n String.raw`\\([^()]*(?:${QUANTIFIER_SOURCE})[^()]*\\)(?:${QUANTIFIER_SOURCE})`,\n);\n\n/**\n * Whether `pattern` is safe enough to compile and run against workspace\n * content: within the length cap and free of the nested-quantifier shape\n * that causes catastrophic regex backtracking (ReDoS).\n */\nfunction isSafeGrepPattern(pattern: string): boolean {\n if (pattern.length > MAX_GREP_PATTERN_LENGTH) {\n return false;\n }\n\n return !NESTED_QUANTIFIER_PATTERN.test(pattern);\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 */\n/**\n * State an ops layer shares with the other projections of the same\n * workspace (`readonly()`, `scope()`): the background commands it owns.\n */\nexport interface OpsRuntime {\n /** The workspace's background commands. */\n jobs: ShellJobs;\n}\n\nclass Ops implements WorkspaceOps {\n /**\n * Tail of the mutation chain per resolved absolute path. Writes, edits and\n * removes on the same path run one after another, so two tool calls the\n * agent dispatches concurrently cannot both read the same content, both\n * pass the stale-hash guard and silently drop one edit: the second one\n * re-reads after the first has written.\n */\n private readonly pathLocks = new Map<string, Promise<void>>();\n\n public constructor(\n private readonly backend: WorkspaceBackend,\n private readonly policy: WorkspacePolicy,\n private readonly runtime: OpsRuntime,\n ) {}\n\n /** Run `task` after every earlier mutation of `absolutePath` has settled. */\n private async withPathLock<T>(absolutePath: string, task: () => Promise<T>): Promise<T> {\n const previous = this.pathLocks.get(absolutePath) ?? Promise.resolve();\n let release: () => void = () => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const tail = previous.then(() => current);\n\n this.pathLocks.set(absolutePath, tail);\n\n try {\n await previous;\n\n return await task();\n } finally {\n release();\n\n if (this.pathLocks.get(absolutePath) === tail) {\n this.pathLocks.delete(absolutePath);\n }\n }\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<ReadFileResult> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath);\n const raw = await this.backend.readFile(absolutePath);\n\n // Hash the full content the backend returned (SHA-256 via\n // @warlock.js/fs), not the returned window and not the on-disk file —\n // the mock backend keeps content in memory, and the stale-edit guard\n // later hashes the backend's content too, so it must be the SAME\n // source of truth whatever window or byte cap applied.\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 const maxBytes = this.policy.read?.maxBytes;\n\n const startIndex = offset - 1;\n const window = lines.slice(startIndex, startIndex + Math.max(0, limit));\n const { content, returnedLines, bytesCapped } = numberWindow(\n window,\n offset,\n maxBytes === undefined ? undefined : Math.max(0, Math.floor(maxBytes)),\n );\n const endLine = offset + returnedLines - 1;\n\n return {\n content,\n startLine: offset,\n endLine,\n totalLines,\n truncated: bytesCapped || endLine < totalLines,\n bytesCapped,\n totalBytes: Buffer.byteLength(raw, \"utf8\"),\n hash,\n };\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 return this.withPathLock(absolutePath, async () => {\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\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n\n return this.withPathLock(absolutePath, () => this.applyEdit(input, absolutePath, relativePath));\n }\n\n /** The read-check-replace-write of {@link editFile}, run under its path lock. */\n private async applyEdit(\n input: EditFileInput,\n absolutePath: string,\n relativePath: string,\n ): Promise<EditFileResult> {\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 { content: updated, replacements } = applyReplacement(\n current,\n input,\n input.path,\n relativePath || input.path,\n );\n\n await this.backend.writeFile(absolutePath, updated);\n\n return {\n path: relativePath || input.path,\n replacements,\n hash: fs.hash.string(updated),\n };\n }\n\n public async multiEdit(input: MultiEditInput): Promise<MultiEditResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n const shownPath = relativePath || input.path;\n\n if (input.edits.length === 0 || input.edits.length > MAX_MULTI_EDITS) {\n throw new RangeError(\n `A multi-edit needs between 1 and ${MAX_MULTI_EDITS} edits (got ${input.edits.length}).`,\n );\n }\n\n return this.withPathLock(absolutePath, async () => {\n const current = await this.backend.readFile(absolutePath);\n const currentHash = fs.hash.string(current);\n\n if (input.expectHash !== undefined && input.expectHash !== currentHash) {\n throw new WorkspaceEditError(\n `File \"${input.path}\" changed since it was read; none of the edits were applied.`,\n {\n type: \"stale-hash\",\n path: shownPath,\n expectedHash: input.expectHash,\n actualHash: currentHash,\n },\n );\n }\n\n // Apply every edit in memory first; one failure leaves the file untouched.\n let content = current;\n let replacements = 0;\n\n input.edits.forEach((edit, editIndex) => {\n const applied = applyReplacement(content, edit, input.path, shownPath, editIndex);\n\n content = applied.content;\n replacements += applied.replacements;\n });\n\n await this.backend.writeFile(absolutePath, content);\n\n return {\n path: shownPath,\n edits: input.edits.length,\n replacements,\n hash: fs.hash.string(content),\n };\n });\n }\n\n public async applyPatch(input: ApplyPatchInput): Promise<ApplyPatchResult> {\n return executePatch(input, {\n resolve: (inputPath) => resolveInJail(this.policy, inputPath),\n exists: (absolutePath) => this.backend.exists(absolutePath),\n stat: this.backend.stat\n ? (absolutePath) =>\n (this.backend.stat as NonNullable<WorkspaceBackend[\"stat\"]>)(absolutePath)\n : undefined,\n read: (absolutePath) => this.backend.readFile(absolutePath),\n write: async (absolutePath, content) => {\n await this.backend.mkdir(path.dirname(absolutePath));\n await this.backend.writeFile(absolutePath, content);\n },\n remove: (absolutePath) => this.backend.remove(absolutePath),\n withLock: (key, task) => this.withPathLock(key, task),\n display: (absolutePath) => path.relative(this.policy.cwd, absolutePath),\n });\n }\n\n /**\n * Check a command line against the shell policy: every command in it\n * (argv or shell mode), then — shell mode — every redirection target and\n * `cd` directory against the jail with symlinks resolved.\n */\n private async assertCommandAllowed(command: string): Promise<void> {\n const verdict = checkShellCommand(this.policy, command);\n\n if (!verdict.allowed) {\n throw new WorkspacePolicyError(\n verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`,\n {\n type: \"denied-command\",\n command,\n ...(verdict.reason !== undefined ? { reason: verdict.reason } : {}),\n ...(verdict.executable !== undefined ? { executable: verdict.executable } : {}),\n ...(verdict.path !== undefined ? { path: verdict.path } : {}),\n },\n );\n }\n\n for (const target of verdict.paths) {\n try {\n await resolveInJail(this.policy, target.path);\n } catch (error) {\n if (!(error instanceof WorkspacePolicyError)) {\n throw error;\n }\n\n throw new WorkspacePolicyError(\n target.access === \"cwd\"\n ? `\\`cd ${target.target}\\` resolves (through a symlink) outside the workspace jail or onto a denied path.`\n : `The redirection target \\`${target.target}\\` resolves (through a symlink) outside the workspace jail or onto a denied path.`,\n { type: \"denied-command\", reason: \"path-escape\", command, path: target.target },\n );\n }\n }\n\n if (this.runtime.jobs.isDisposed) {\n throw new WorkspacePolicyError(\n \"The workspace has been disposed; it cannot run new commands.\",\n { type: \"denied-command\", reason: \"not-allowed\", command },\n );\n }\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n await this.assertCommandAllowed(command);\n\n const signal = opts?.signal;\n\n // Refuse an already-cancelled invocation here, so no backend — built-in\n // or custom — ever starts a command whose caller has given up on it.\n if (signal?.aborted) {\n return {\n exitCode: CANCELLED_BEFORE_START_EXIT_CODE,\n stdout: \"\",\n stderr: \"Command was not executed: the invocation was cancelled before it started.\",\n truncated: false,\n timedOut: false,\n cancelled: true,\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 const interpreter = shell?.interpreter ?? \"none\";\n\n const result = await this.backend.exec(command, {\n cwd: this.policy.cwd,\n timeoutMs,\n env: buildCommandEnv(this.policy),\n ...(signal ? { signal } : {}),\n ...(interpreter !== \"none\" ? { interpreter } : {}),\n });\n\n const stdout = capOutput(result.stdout, maxOutputBytes);\n const stderr = capOutput(result.stderr, maxOutputBytes);\n const cancelled = result.cancelled === true;\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 && !cancelled,\n cancelled,\n };\n }\n\n public async startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo> {\n const background = this.policy.shell?.background;\n\n if (background === undefined) {\n throw new WorkspacePolicyError(\n \"Background commands are not enabled for this workspace. Run the command without background, or ask for shell.background to be enabled.\",\n { type: \"denied-command\", reason: \"not-allowed\", command },\n );\n }\n\n await this.assertCommandAllowed(command);\n\n const start = this.backend.start?.bind(this.backend);\n\n if (start === undefined) {\n throw new WorkspacePolicyError(\"This workspace backend cannot run background commands.\", {\n type: \"denied-command\",\n reason: \"not-allowed\",\n command,\n });\n }\n\n const shell = this.policy.shell;\n const interpreter = shell?.interpreter ?? \"none\";\n const timeoutMs = lowerOf(background.timeoutMs, opts?.timeoutMs);\n const env = buildCommandEnv(this.policy);\n\n return this.runtime.jobs.start({\n command,\n retainBytes: shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,\n maxJobs: background.maxJobs ?? DEFAULT_MAX_BACKGROUND_JOBS,\n ...(timeoutMs !== undefined ? { timeoutMs } : {}),\n launch: (onOutput) =>\n start(command, {\n cwd: this.policy.cwd,\n env,\n ...(interpreter !== \"none\" ? { interpreter } : {}),\n onOutput,\n }),\n });\n }\n\n public shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n return this.runtime.jobs.output(id, opts);\n }\n\n public shellKill(id: string): Promise<ShellJobInfo> {\n return this.runtime.jobs.kill(id);\n }\n\n public listShells(): ShellJobInfo[] {\n return this.runtime.jobs.list();\n }\n\n public dispose(): Promise<void> {\n return this.runtime.jobs.dispose();\n }\n\n public async listDir(inputPath = \".\", opts?: ListDirOptions): Promise<ListDirResult> {\n const { absolutePath: root, relativePath: rootRelative } = await resolveInJail(\n this.policy,\n inputPath,\n );\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const listPolicy = this.policy.list;\n const maxDepth = Math.max(1, Math.floor(listPolicy?.maxDepth ?? DEFAULT_LIST_MAX_DEPTH));\n const depth = Math.min(Math.max(1, Math.floor(opts?.depth ?? 1)), maxDepth);\n const maxEntries = Math.max(1, Math.floor(listPolicy?.maxEntries ?? DEFAULT_LIST_MAX_ENTRIES));\n const ignore = [...(listPolicy?.ignore ?? DEFAULT_LIST_IGNORE), ...(opts?.ignore ?? [])];\n const insideCwd = isInside(root, jailRoot);\n\n if (!(await this.isDirectory(root))) {\n throw new Error(`\"${inputPath}\" is not a directory in the workspace.`);\n }\n\n const entries: ListDirEntry[] = [];\n let level: string[] = [root];\n let truncated = false;\n\n for (let currentDepth = 1; currentDepth <= depth && level.length > 0; currentDepth++) {\n const nextLevel: string[] = [];\n\n for (const directory of level) {\n let children: string[];\n\n try {\n children = await this.backend.list(directory);\n } catch {\n continue;\n }\n\n children.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));\n\n for (const child of children) {\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, child).split(path.sep).join(\"/\")\n : child;\n\n if (\n insideCwd &&\n (this.isDenied(relativeToJail) || matchesAnyGlob(relativeToJail, ignore))\n ) {\n continue;\n }\n\n const relativeToRoot = path.relative(root, child).split(path.sep).join(\"/\");\n\n if (!insideCwd && matchesAnyGlob(relativeToRoot, ignore)) {\n continue;\n }\n\n if (entries.length >= maxEntries) {\n truncated = true;\n\n break;\n }\n\n const described = await this.describe(child);\n\n entries.push({\n path: relativeToJail,\n type: described.type,\n ...(described.size !== undefined ? { size: described.size } : {}),\n depth: currentDepth,\n });\n\n if (described.type === \"directory\") {\n nextLevel.push(child);\n }\n }\n\n if (truncated) {\n break;\n }\n }\n\n if (truncated) {\n break;\n }\n\n level = nextLevel;\n }\n\n return { path: insideCwd ? rootRelative || \".\" : root, entries, truncated };\n }\n\n /** Kind and size of a listed entry; symlinks are reported, never followed. */\n private async describe(absPath: string): Promise<{ type: ListDirEntryType; size?: number }> {\n if (this.backend.stat) {\n try {\n const stats = await this.backend.stat(absPath);\n\n return stats.type === \"file\" ? { type: \"file\", size: stats.size } : { type: stats.type };\n } catch {\n return { type: \"other\" };\n }\n }\n\n // No `stat`: a child is a directory iff it can be listed, and it must\n // still resolve inside the jail before it is descended into.\n if (await this.isDirectory(absPath)) {\n try {\n await resolveInJail(this.policy, absPath);\n\n return { type: \"directory\" };\n } catch {\n return { type: \"other\" };\n }\n }\n\n return { type: \"file\" };\n }\n\n public async grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n if (!isSafeGrepPattern(pattern)) {\n throw new WorkspacePolicyError(\n `Grep pattern is too long or too likely to cause catastrophic regex backtracking: ${pattern}`,\n { type: \"unsafe-pattern\", pattern },\n );\n }\n\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 const line = lines[index] ?? \"\";\n\n // Cap the input size a single `RegExp#test` call scans: backtracking\n // cost is exponential in input length, so this bounds the worst-case\n // time even a pathological (but length/shape-allowed) pattern can\n // burn on any one line.\n if (line.length > MAX_GREP_LINE_SCAN_LENGTH) {\n continue;\n }\n\n if (regex.test(line)) {\n matches.push({ path: relativePath, line: index + 1, text: line });\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.withPathLock(absolutePath, () => 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 return (\n denyPaths !== undefined && denyPaths.length > 0 && matchesAnyGlob(relativePath, denyPaths)\n );\n }\n}\n\n/** The lower of two optional limits (either may be absent). */\nfunction lowerOf(\n policyLimit: number | undefined,\n requested: number | undefined,\n): number | undefined {\n if (requested === undefined || requested <= 0) {\n return policyLimit;\n }\n\n return policyLimit === undefined ? requested : Math.min(policyLimit, requested);\n}\n\n/**\n * Apply one exact-string replacement to `content` under the edit rules:\n * `oldString` must occur, and exactly once unless `replaceAll`.\n */\nfunction applyReplacement(\n content: string,\n edit: MultiEditOperation,\n inputPath: string,\n shownPath: string,\n editIndex?: number,\n): { content: string; replacements: number } {\n const occurrences = countOccurrences(content, edit.oldString);\n const which = editIndex === undefined ? \"\" : `Edit ${editIndex + 1}: `;\n const nothingApplied = editIndex === undefined ? \"\" : \" None of the edits were applied.\";\n const indexOption = editIndex === undefined ? {} : { editIndex };\n\n if (occurrences === 0) {\n throw new WorkspaceEditError(\n `${which}The text to replace was not found in \"${inputPath}\".${nothingApplied}`,\n { type: \"not-found\", path: shownPath, matches: 0, ...indexOption },\n );\n }\n\n if (occurrences > 1 && !edit.replaceAll) {\n throw new WorkspaceEditError(\n `${which}The text to replace is not unique in \"${inputPath}\" (${occurrences} matches); ` +\n `pass replaceAll or include more surrounding context.${nothingApplied}`,\n { type: \"not-unique\", path: shownPath, matches: occurrences, ...indexOption },\n );\n }\n\n return {\n content: edit.replaceAll\n ? content.split(edit.oldString).join(edit.newString)\n : replaceFirst(content, edit.oldString, edit.newString),\n replacements: edit.replaceAll ? occurrences : 1,\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 * @param runtime - State shared with other views of the same workspace\n * (its background commands); a fresh one when omitted.\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 runtime?: OpsRuntime,\n): WorkspaceOps {\n return new Ops(backend, policy, runtime ?? { jobs: new ShellJobs() });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ops.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fs } from \"@warlock.js/fs\";\nimport { applyReplacement, type AppliedReplacementOptions } from \"./edit/apply-replacement\";\nimport { WorkspaceEditError, WorkspacePolicyError } from \"./errors\";\nimport { runAfterEdit } from \"./hooks/after-edit\";\nimport { OutputSpiller, splitLines } from \"./output/spill\";\nimport { globToRegExp, isInside, matchesAnyGlob } from \"./policy/jail\";\nimport { buildCommandEnv, resolveInJail } from \"./policy/policy\";\nimport { refusalReason, samePattern } from \"./policy/approval-helpers\";\nimport { checkShellCommand } from \"./policy/shell-check\";\nimport { resolveShell, type ResolvedShell } from \"./policy/shell-interpreter\";\nimport { DEFAULT_MAX_BACKGROUND_JOBS, ShellJobs } from \"./shell-jobs\";\nimport { executePatch } from \"./patch/execute-patch\";\nimport type {\n ApplyPatchInput,\n ApplyPatchResult,\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepMatch,\n GlobResult,\n GrepResult,\n ListDirEntry,\n ListDirEntryType,\n ListDirOptions,\n ListDirResult,\n MultiEditInput,\n MultiEditResult,\n ReadFileResult,\n RunShellResult,\n ShellJobInfo,\n ShellOutputOptions,\n ShellOutputResult,\n StartShellOptions,\n WorkspaceBackend,\n WorkspaceCommandPattern,\n WorkspaceOps,\n WorkspacePolicy,\n WorkspaceShellAsk,\n WorkspaceShellOnAsk,\n WorkspaceEditTool,\n WorkspaceTruncation,\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/** Default `list.ignore` when the policy sets none. */\nconst DEFAULT_LIST_IGNORE: readonly string[] = [\"node_modules\", \".git\"];\n/** Default deepest `depth` a listing may ask for. */\nconst DEFAULT_LIST_MAX_DEPTH = 5;\n/** Default most entries one listing returns. */\nconst DEFAULT_LIST_MAX_ENTRIES = 500;\n/** Most edits one `multiEdit` call may carry. */\nexport const MAX_MULTI_EDITS = 100;\n/**\n * Exit code reported for a command refused because its signal was already\n * aborted (the shell convention for an interrupted command). Callers should\n * branch on `cancelled`, not on this number.\n */\nconst CANCELLED_BEFORE_START_EXIT_CODE = 130;\n/**\n * Hard ceiling on `grep` pattern length. A model-controlled regex has no\n * legitimate reason to be this long; longer patterns are rejected outright\n * rather than compiled.\n */\nconst MAX_GREP_PATTERN_LENGTH = 200;\n/**\n * Hard ceiling on the number of characters of a single line handed to\n * `RegExp#test` during a `grep`. Catastrophic backtracking is exponential\n * in input length, so bounding the input scanned per call bounds the\n * worst-case time a single pathological line can cost — lines longer than\n * this are skipped rather than scanned.\n */\nconst MAX_GREP_LINE_SCAN_LENGTH = 2000;\n\n/** The `cat -n` prefix for a line: right-aligned number (min width 6) and a tab. */\nfunction linePrefix(lineNumber: number): string {\n return `${String(lineNumber).padStart(6, \" \")}\\t`;\n}\n\n/**\n * Clip `value` to at most `maxBytes` UTF-8 bytes without splitting a\n * character: the cut backs off over continuation bytes (`10xxxxxx`) to the\n * start of the character it would have split, so the result never carries\n * a replacement character.\n */\nfunction clipUtf8(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 let cut = Math.max(0, maxBytes);\n\n while (cut > 0 && ((bytes[cut] ?? 0) & 0xc0) === 0x80) {\n cut--;\n }\n\n return { value: bytes.subarray(0, cut).toString(\"utf8\"), truncated: true };\n}\n\n/**\n * Number a window of lines `cat -n` style, keeping the joined output within\n * `maxBytes` UTF-8 bytes (prefixes and newlines included) when a cap is set.\n * A line that does not fit whole is clipped on a character boundary; it is\n * still counted as returned when its prefix and at least one character fit.\n */\nfunction numberWindow(\n lines: string[],\n startLine: number,\n maxBytes: number | undefined,\n): { content: string; returnedLines: number; bytesCapped: boolean } {\n const parts: string[] = [];\n let used = 0;\n\n for (let index = 0; index < lines.length; index++) {\n const prefix = linePrefix(startLine + index);\n const numbered = prefix + lines[index];\n const separator = parts.length > 0 ? 1 : 0;\n\n if (maxBytes === undefined) {\n parts.push(numbered);\n\n continue;\n }\n\n const cost = separator + Buffer.byteLength(numbered, \"utf8\");\n\n if (used + cost <= maxBytes) {\n parts.push(numbered);\n used += cost;\n\n continue;\n }\n\n const room = maxBytes - used - separator - Buffer.byteLength(prefix, \"utf8\");\n const clipped = room > 0 ? clipUtf8(lines[index] ?? \"\", room).value : \"\";\n\n if (clipped.length > 0) {\n parts.push(prefix + clipped);\n }\n\n return { content: parts.join(\"\\n\"), returnedLines: parts.length, bytesCapped: true };\n }\n\n return { content: parts.join(\"\\n\"), returnedLines: parts.length, bytesCapped: false };\n}\n\n/**\n * Clip a captured stream at `maxBytes` (measured in UTF-8 bytes), on a\n * character boundary. Returns the possibly-clipped string plus whether\n * clipping occurred.\n */\nfunction capOutput(value: string, maxBytes: number): { value: string; truncated: boolean } {\n return clipUtf8(value, maxBytes);\n}\n\n/**\n * A quantifier: `+`, `*`, `?`, or a `{m,n}` bound.\n */\nconst QUANTIFIER_SOURCE = String.raw`[+*?]|\\{\\d*,?\\d*\\}`;\n\n/**\n * Heuristic catastrophic-backtracking detector: flags a quantified group\n * that itself contains a quantifier (e.g. `(a+)+`, `(a*)*`, `(.+)*`) —\n * the classic exponential-blowup shape. Not a full regex-safety analyzer\n * (it won't catch every ReDoS shape, e.g. quantified alternation like\n * `(a|a)+`), but it rejects the shape an agent is most likely to emit,\n * intentionally or via prompt injection.\n */\nconst NESTED_QUANTIFIER_PATTERN = new RegExp(\n String.raw`\\([^()]*(?:${QUANTIFIER_SOURCE})[^()]*\\)(?:${QUANTIFIER_SOURCE})`,\n);\n\n/**\n * Whether `pattern` is safe enough to compile and run against workspace\n * content: within the length cap and free of the nested-quantifier shape\n * that causes catastrophic regex backtracking (ReDoS).\n */\nfunction isSafeGrepPattern(pattern: string): boolean {\n if (pattern.length > MAX_GREP_PATTERN_LENGTH) {\n return false;\n }\n\n return !NESTED_QUANTIFIER_PATTERN.test(pattern);\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 */\n/**\n * State an ops layer shares with the other projections of the same\n * workspace (`readonly()`, `scope()`): the background commands it owns.\n */\nexport interface OpsRuntime {\n /** The workspace's background commands. */\n jobs: ShellJobs;\n /** Patterns the host approved with `remember`, shared by every view of the workspace. */\n approvals?: WorkspaceCommandPattern[];\n}\n\nclass Ops implements WorkspaceOps {\n /**\n * Tail of the mutation chain per resolved absolute path. Writes, edits and\n * removes on the same path run one after another, so two tool calls the\n * agent dispatches concurrently cannot both read the same content, both\n * pass the stale-hash guard and silently drop one edit: the second one\n * re-reads after the first has written.\n */\n private readonly pathLocks = new Map<string, Promise<void>>();\n\n /** The shell `policy.shell.interpreter` resolves to on this host, resolved once. */\n private readonly resolvedShell: ResolvedShell;\n\n /** Patterns approved with `remember` (shared through the runtime). */\n private readonly approvals: WorkspaceCommandPattern[];\n\n /** Saves oversized output to files when `output.spillDir` is set. */\n private readonly spiller: OutputSpiller;\n\n public constructor(\n private readonly backend: WorkspaceBackend,\n private readonly policy: WorkspacePolicy,\n private readonly runtime: OpsRuntime,\n ) {\n this.resolvedShell = resolveShell(policy.shell);\n this.approvals = runtime.approvals ?? [];\n this.spiller = new OutputSpiller(backend, policy);\n }\n\n /** The edit-policy settings applied to every replacement. */\n private editOptions(): AppliedReplacementOptions {\n const edit = this.policy.edit;\n\n return {\n matching: edit?.matching ?? \"tiered\",\n missHintLines: edit?.missHintLines ?? 12,\n lineEndings: edit?.lineEndings ?? \"preserve\",\n ...(this.policy.read?.maxBytes !== undefined\n ? { hintMaxBytes: this.policy.read.maxBytes }\n : {}),\n };\n }\n\n /** Run `afterEdit` for a call that wrote `paths`; `undefined` when there is no output. */\n private diagnosticsFor(\n tool: WorkspaceEditTool,\n paths: readonly string[],\n ): Promise<string | undefined> {\n return runAfterEdit(this.policy, { paths, tool });\n }\n\n /** Run `task` after every earlier mutation of `absolutePath` has settled. */\n private async withPathLock<T>(absolutePath: string, task: () => Promise<T>): Promise<T> {\n const previous = this.pathLocks.get(absolutePath) ?? Promise.resolve();\n let release: () => void = () => {};\n const current = new Promise<void>((resolve) => {\n release = resolve;\n });\n const tail = previous.then(() => current);\n\n this.pathLocks.set(absolutePath, tail);\n\n try {\n await previous;\n\n return await task();\n } finally {\n release();\n\n if (this.pathLocks.get(absolutePath) === tail) {\n this.pathLocks.delete(absolutePath);\n }\n }\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<ReadFileResult> {\n const { absolutePath } = await resolveInJail(this.policy, inputPath, \"read\");\n const raw = await this.backend.readFile(absolutePath);\n\n // Hash the full content the backend returned (SHA-256 via\n // @warlock.js/fs), not the returned window and not the on-disk file —\n // the mock backend keeps content in memory, and the stale-edit guard\n // later hashes the backend's content too, so it must be the SAME\n // source of truth whatever window or byte cap applied.\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 const maxBytes = this.policy.read?.maxBytes;\n\n const startIndex = offset - 1;\n const window = lines.slice(startIndex, startIndex + Math.max(0, limit));\n const { content, returnedLines, bytesCapped } = numberWindow(\n window,\n offset,\n maxBytes === undefined ? undefined : Math.max(0, Math.floor(maxBytes)),\n );\n const endLine = offset + returnedLines - 1;\n const spillDir = this.spiller.spillDir;\n let spilled: { content: string; truncation: WorkspaceTruncation } | undefined;\n\n // A read of a spill file is never spilled again; it is already paged.\n if (bytesCapped && spillDir !== undefined && !isInside(absolutePath, spillDir)) {\n const full = numberWindow(window, offset, undefined).content;\n const file = await this.spiller.save(\"read_file\", full);\n\n if (file !== undefined) {\n spilled = {\n content: `${content}\\n[showing lines 1-${returnedLines} of ${window.length}; full output saved to ${file}]`,\n truncation: {\n shown: { lines: returnedLines },\n total: { lines: window.length, bytes: Buffer.byteLength(full, \"utf8\") },\n path: file,\n },\n };\n }\n }\n\n return {\n content: spilled?.content ?? content,\n ...(spilled !== undefined ? { truncation: spilled.truncation } : {}),\n ...(spilled?.truncation.path !== undefined ? { spilledTo: [spilled.truncation.path] } : {}),\n startLine: offset,\n endLine,\n totalLines,\n truncated: bytesCapped || endLine < totalLines,\n bytesCapped,\n totalBytes: Buffer.byteLength(raw, \"utf8\"),\n hash,\n };\n }\n\n public async writeFile(\n inputPath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number; diagnostics?: string }> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, inputPath);\n\n const written = await this.withPathLock(absolutePath, async () => {\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 const diagnostics = await this.diagnosticsFor(\"write_file\", [relativePath || inputPath]);\n\n return { ...written, ...(diagnostics !== undefined ? { diagnostics } : {}) };\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n\n const result = await this.withPathLock(absolutePath, () =>\n this.applyEdit(input, absolutePath, relativePath),\n );\n const diagnostics = await this.diagnosticsFor(\"edit_file\", [result.path]);\n\n return diagnostics === undefined ? result : { ...result, diagnostics };\n }\n\n /** The read-check-replace-write of {@link editFile}, run under its path lock. */\n private async applyEdit(\n input: EditFileInput,\n absolutePath: string,\n relativePath: string,\n ): Promise<EditFileResult> {\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 {\n content: updated,\n replacements,\n matchTier,\n } = applyReplacement(\n current,\n input,\n input.path,\n relativePath || input.path,\n this.editOptions(),\n );\n\n await this.backend.writeFile(absolutePath, updated);\n\n return {\n path: relativePath || input.path,\n replacements,\n hash: fs.hash.string(updated),\n ...(matchTier !== undefined ? { matchTier } : {}),\n };\n }\n\n public async multiEdit(input: MultiEditInput): Promise<MultiEditResult> {\n const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);\n const shownPath = relativePath || input.path;\n\n if (input.edits.length === 0 || input.edits.length > MAX_MULTI_EDITS) {\n throw new RangeError(\n `A multi-edit needs between 1 and ${MAX_MULTI_EDITS} edits (got ${input.edits.length}).`,\n );\n }\n\n const result = await this.withPathLock(absolutePath, async () => {\n const current = await this.backend.readFile(absolutePath);\n const currentHash = fs.hash.string(current);\n\n if (input.expectHash !== undefined && input.expectHash !== currentHash) {\n throw new WorkspaceEditError(\n `File \"${input.path}\" changed since it was read; none of the edits were applied.`,\n {\n type: \"stale-hash\",\n path: shownPath,\n expectedHash: input.expectHash,\n actualHash: currentHash,\n },\n );\n }\n\n // Apply every edit in memory first; one failure leaves the file untouched.\n let content = current;\n let replacements = 0;\n let matchTier: MultiEditResult[\"matchTier\"];\n const options = this.editOptions();\n\n input.edits.forEach((edit, editIndex) => {\n const applied = applyReplacement(content, edit, input.path, shownPath, options, editIndex);\n\n content = applied.content;\n replacements += applied.replacements;\n\n if (applied.matchTier === \"whitespace\" || matchTier === undefined) {\n matchTier = applied.matchTier ?? matchTier;\n }\n });\n\n await this.backend.writeFile(absolutePath, content);\n\n return {\n path: shownPath,\n edits: input.edits.length,\n replacements,\n hash: fs.hash.string(content),\n ...(matchTier !== undefined ? { matchTier } : {}),\n };\n });\n const diagnostics = await this.diagnosticsFor(\"multi_edit\", [shownPath]);\n\n return diagnostics === undefined ? result : { ...result, diagnostics };\n }\n\n public async applyPatch(input: ApplyPatchInput): Promise<ApplyPatchResult> {\n const result = await this.runPatch(input);\n\n if (result.dryRun) {\n return result;\n }\n\n const paths = result.files.filter((file) => file.action !== \"deleted\").map((file) => file.path);\n const diagnostics = await this.diagnosticsFor(\"apply_patch\", paths);\n\n return diagnostics === undefined ? result : { ...result, diagnostics };\n }\n\n private runPatch(input: ApplyPatchInput): Promise<ApplyPatchResult> {\n return executePatch(input, {\n resolve: (inputPath) => resolveInJail(this.policy, inputPath),\n exists: (absolutePath) => this.backend.exists(absolutePath),\n stat: this.backend.stat\n ? (absolutePath) =>\n (this.backend.stat as NonNullable<WorkspaceBackend[\"stat\"]>)(absolutePath)\n : undefined,\n read: (absolutePath) => this.backend.readFile(absolutePath),\n write: async (absolutePath, content) => {\n await this.backend.mkdir(path.dirname(absolutePath));\n await this.backend.writeFile(absolutePath, content);\n },\n remove: (absolutePath) => this.backend.remove(absolutePath),\n withLock: (key, task) => this.withPathLock(key, task),\n display: (absolutePath) => path.relative(this.policy.cwd, absolutePath),\n });\n }\n\n /**\n * Check a command line against the shell policy: every command in it\n * (argv or shell mode), then — shell mode — every redirection target and\n * `cd` directory against the jail with symlinks resolved.\n */\n private async assertCommandAllowed(command: string, signal?: AbortSignal): Promise<void> {\n const verdict = checkShellCommand(this.policy, command, {\n remembered: this.approvals,\n resolved: this.resolvedShell,\n });\n const onAsk = this.policy.shell?.onAsk;\n const needsApproval =\n !verdict.allowed && verdict.reason === \"approval-required\" && verdict.asks.length > 0;\n\n if (!verdict.allowed && !(needsApproval && onAsk !== undefined)) {\n throw new WorkspacePolicyError(\n verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`,\n {\n type: \"denied-command\",\n command,\n ...(verdict.reason !== undefined ? { reason: verdict.reason } : {}),\n ...(verdict.executable !== undefined ? { executable: verdict.executable } : {}),\n ...(verdict.path !== undefined ? { path: verdict.path } : {}),\n },\n );\n }\n\n for (const target of verdict.paths) {\n try {\n await resolveInJail(this.policy, target.path);\n } catch (error) {\n if (!(error instanceof WorkspacePolicyError)) {\n throw error;\n }\n\n throw new WorkspacePolicyError(\n target.access === \"cwd\"\n ? `\\`cd ${target.target}\\` resolves (through a symlink) outside the workspace jail or onto a denied path.`\n : `The redirection target \\`${target.target}\\` resolves (through a symlink) outside the workspace jail or onto a denied path.`,\n { type: \"denied-command\", reason: \"path-escape\", command, path: target.target },\n );\n }\n }\n\n if (this.runtime.jobs.isDisposed) {\n throw new WorkspacePolicyError(\n \"The workspace has been disposed; it cannot run new commands.\",\n { type: \"denied-command\", reason: \"not-allowed\", command },\n );\n }\n\n if (needsApproval && onAsk !== undefined) {\n await this.askForApproval(command, verdict.asks, onAsk, signal);\n }\n }\n\n /** Put the commands that need approval to `shell.onAsk`; refuse the line unless it says yes. */\n private async askForApproval(\n command: string,\n asks: WorkspaceShellAsk[],\n onAsk: WorkspaceShellOnAsk,\n signal: AbortSignal | undefined,\n ): Promise<void> {\n let decision: Awaited<ReturnType<WorkspaceShellOnAsk>> | undefined;\n\n try {\n decision = await onAsk({ command, asks, ...(signal ? { signal } : {}) });\n } catch {\n decision = undefined;\n }\n\n if (decision?.allow !== true) {\n const reason = refusalReason(decision?.reason);\n\n throw new WorkspacePolicyError(\n `Running ${asks.map((ask) => `\\`${ask.argv.join(\" \")}\\``).join(\", \")} was not approved${reason}`,\n { type: \"denied-command\", reason: \"approval-denied\", command },\n );\n }\n\n if (decision.remember !== undefined) {\n const patterns = Array.isArray(decision.remember) ? decision.remember : [decision.remember];\n\n for (const pattern of patterns as WorkspaceCommandPattern[]) {\n if (!this.approvals.some((entry) => samePattern(entry, pattern))) {\n this.approvals.push(pattern);\n }\n }\n }\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n await this.assertCommandAllowed(command, opts?.signal);\n\n const signal = opts?.signal;\n\n // Refuse an already-cancelled invocation here, so no backend — built-in\n // or custom — ever starts a command whose caller has given up on it.\n if (signal?.aborted) {\n return {\n exitCode: CANCELLED_BEFORE_START_EXIT_CODE,\n stdout: \"\",\n stderr: \"Command was not executed: the invocation was cancelled before it started.\",\n truncated: false,\n timedOut: false,\n cancelled: true,\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 const interpreter = this.resolvedShell.interpreter;\n\n const result = await this.backend.exec(command, {\n cwd: this.policy.cwd,\n timeoutMs,\n env: buildCommandEnv(this.policy),\n ...(signal ? { signal } : {}),\n ...(interpreter !== \"none\" ? { interpreter } : {}),\n ...(interpreter !== \"none\" && this.resolvedShell.file !== undefined\n ? { interpreterPath: this.resolvedShell.file }\n : {}),\n });\n\n const stdout = await this.fitStream(\"run_shell-stdout\", result.stdout, maxOutputBytes);\n const stderr = await this.fitStream(\"run_shell-stderr\", result.stderr, maxOutputBytes);\n const cancelled = result.cancelled === true;\n const truncation = streamTruncation(stdout.truncation, stderr.truncation);\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 && !cancelled,\n cancelled,\n ...(truncation !== undefined ? { truncation } : {}),\n ...spilledField(truncation),\n };\n }\n\n public async startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo> {\n const background = this.policy.shell?.background;\n\n if (background === undefined) {\n throw new WorkspacePolicyError(\n \"Background commands are not enabled for this workspace. Run the command without background, or ask for shell.background to be enabled.\",\n { type: \"denied-command\", reason: \"not-allowed\", command },\n );\n }\n\n await this.assertCommandAllowed(command);\n\n const start = this.backend.start?.bind(this.backend);\n\n if (start === undefined) {\n throw new WorkspacePolicyError(\"This workspace backend cannot run background commands.\", {\n type: \"denied-command\",\n reason: \"not-allowed\",\n command,\n });\n }\n\n const shell = this.policy.shell;\n const interpreter = this.resolvedShell.interpreter;\n const interpreterPath = interpreter !== \"none\" ? this.resolvedShell.file : undefined;\n const timeoutMs = lowerOf(background.timeoutMs, opts?.timeoutMs);\n const env = buildCommandEnv(this.policy);\n\n return this.runtime.jobs.start({\n command,\n retainBytes: shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,\n maxJobs: background.maxJobs ?? DEFAULT_MAX_BACKGROUND_JOBS,\n ...(timeoutMs !== undefined ? { timeoutMs } : {}),\n launch: (onOutput) =>\n start(command, {\n cwd: this.policy.cwd,\n env,\n ...(interpreter !== \"none\" ? { interpreter } : {}),\n ...(interpreterPath !== undefined ? { interpreterPath } : {}),\n onOutput,\n }),\n });\n }\n\n public async shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n const result = await this.runtime.jobs.output(id, opts);\n\n if (!this.spiller.enabled) {\n return result;\n }\n\n const stdout = await this.spiller.text(`shell_output-${id}-stdout`, result.stdout, \"head+tail\");\n const stderr = await this.spiller.text(`shell_output-${id}-stderr`, result.stderr, \"head+tail\");\n const truncation = streamTruncation(stdout.truncation, stderr.truncation);\n\n return truncation === undefined\n ? result\n : {\n ...result,\n stdout: stdout.value,\n stderr: stderr.value,\n truncated: true,\n truncation,\n ...spilledField(truncation),\n };\n }\n\n /**\n * Fit one captured stream to the limits: spilled to a file when\n * `output.spillDir` is set, otherwise clipped at `maxBytes` as always.\n */\n private async fitStream(\n label: string,\n value: string,\n maxBytes: number,\n ): Promise<{ value: string; truncated: boolean; truncation?: WorkspaceTruncation }> {\n if (this.spiller.enabled) {\n const cap = this.policy.shell?.maxOutputBytes;\n const spilled = await this.spiller.text(label, value, \"head+tail\", cap);\n\n if (spilled.truncation !== undefined) {\n return { value: spilled.value, truncated: true, truncation: spilled.truncation };\n }\n }\n\n const capped = capOutput(value, maxBytes);\n\n if (!capped.truncated) {\n return capped;\n }\n\n return {\n ...capped,\n truncation: {\n shown: { lines: splitLines(capped.value).length },\n total: { lines: splitLines(value).length, bytes: Buffer.byteLength(value, \"utf8\") },\n },\n };\n }\n\n public shellKill(id: string): Promise<ShellJobInfo> {\n return this.runtime.jobs.kill(id);\n }\n\n public listShells(): ShellJobInfo[] {\n return this.runtime.jobs.list();\n }\n\n public async dispose(): Promise<void> {\n await this.runtime.jobs.dispose();\n await this.spiller.dispose();\n }\n\n public async listDir(inputPath = \".\", opts?: ListDirOptions): Promise<ListDirResult> {\n const { absolutePath: root, relativePath: rootRelative } = await resolveInJail(\n this.policy,\n inputPath,\n );\n const { absolutePath: jailRoot } = await resolveInJail(this.policy, \".\");\n const listPolicy = this.policy.list;\n const maxDepth = Math.max(1, Math.floor(listPolicy?.maxDepth ?? DEFAULT_LIST_MAX_DEPTH));\n const depth = Math.min(Math.max(1, Math.floor(opts?.depth ?? 1)), maxDepth);\n const maxEntries = Math.max(1, Math.floor(listPolicy?.maxEntries ?? DEFAULT_LIST_MAX_ENTRIES));\n const ignore = [...(listPolicy?.ignore ?? DEFAULT_LIST_IGNORE), ...(opts?.ignore ?? [])];\n const insideCwd = isInside(root, jailRoot);\n\n if (!(await this.isDirectory(root))) {\n throw new Error(`\"${inputPath}\" is not a directory in the workspace.`);\n }\n\n const entries: ListDirEntry[] = [];\n let level: string[] = [root];\n let truncated = false;\n\n for (let currentDepth = 1; currentDepth <= depth && level.length > 0; currentDepth++) {\n const nextLevel: string[] = [];\n\n for (const directory of level) {\n let children: string[];\n\n try {\n children = await this.backend.list(directory);\n } catch {\n continue;\n }\n\n children.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));\n\n for (const child of children) {\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, child).split(path.sep).join(\"/\")\n : child;\n\n if (\n insideCwd &&\n (this.isDenied(relativeToJail) || matchesAnyGlob(relativeToJail, ignore))\n ) {\n continue;\n }\n\n const relativeToRoot = path.relative(root, child).split(path.sep).join(\"/\");\n\n if (!insideCwd && matchesAnyGlob(relativeToRoot, ignore)) {\n continue;\n }\n\n if (entries.length >= maxEntries) {\n truncated = true;\n\n break;\n }\n\n const described = await this.describe(child);\n\n entries.push({\n path: relativeToJail,\n type: described.type,\n ...(described.size !== undefined ? { size: described.size } : {}),\n depth: currentDepth,\n });\n\n if (described.type === \"directory\") {\n nextLevel.push(child);\n }\n }\n\n if (truncated) {\n break;\n }\n }\n\n if (truncated) {\n break;\n }\n\n level = nextLevel;\n }\n\n return { path: insideCwd ? rootRelative || \".\" : root, entries, truncated };\n }\n\n /** Kind and size of a listed entry; symlinks are reported, never followed. */\n private async describe(absPath: string): Promise<{ type: ListDirEntryType; size?: number }> {\n if (this.backend.stat) {\n try {\n const stats = await this.backend.stat(absPath);\n\n return stats.type === \"file\" ? { type: \"file\", size: stats.size } : { type: stats.type };\n } catch {\n return { type: \"other\" };\n }\n }\n\n // No `stat`: a child is a directory iff it can be listed, and it must\n // still resolve inside the jail before it is descended into.\n if (await this.isDirectory(absPath)) {\n try {\n await resolveInJail(this.policy, absPath);\n\n return { type: \"directory\" };\n } catch {\n return { type: \"other\" };\n }\n }\n\n return { type: \"file\" };\n }\n\n public async grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n if (!isSafeGrepPattern(pattern)) {\n throw new WorkspacePolicyError(\n `Grep pattern is too long or too likely to cause catastrophic regex backtracking: ${pattern}`,\n { type: \"unsafe-pattern\", pattern },\n );\n }\n\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 const line = lines[index] ?? \"\";\n\n // Cap the input size a single `RegExp#test` call scans: backtracking\n // cost is exponential in input length, so this bounds the worst-case\n // time even a pathological (but length/shape-allowed) pattern can\n // burn on any one line.\n if (line.length > MAX_GREP_LINE_SCAN_LENGTH) {\n continue;\n }\n\n if (regex.test(line)) {\n matches.push({ path: relativePath, line: index + 1, text: line });\n\n if (matches.length >= DEFAULT_MAX_GREP_MATCHES) {\n return this.fitMatches(matches);\n }\n }\n }\n }\n\n return this.fitMatches(matches);\n }\n\n /** Apply the `output` limits to a finished match list. */\n private async fitMatches(matches: GrepMatch[]): Promise<GrepResult> {\n const fitted = await this.spiller.list(\n \"grep\",\n matches,\n (match) => `${match.path}:${match.line}:${match.text}`,\n \"head\",\n );\n\n return fitted.truncation === undefined\n ? { matches, total: matches.length }\n : {\n matches: fitted.items,\n total: matches.length,\n truncated: true,\n truncation: fitted.truncation,\n ...spilledField({ stdout: fitted.truncation }),\n };\n }\n\n public async globResult(pattern: string): Promise<GlobResult> {\n const all = await this.glob(pattern);\n const fitted = await this.spiller.list(\"glob\", all, (entry) => entry, \"head\");\n\n return fitted.truncation === undefined\n ? { paths: all }\n : {\n paths: fitted.items,\n truncated: true,\n truncation: fitted.truncation,\n ...spilledField({ stdout: fitted.truncation }),\n };\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.withPathLock(absolutePath, () => 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 return (\n denyPaths !== undefined && denyPaths.length > 0 && matchesAnyGlob(relativePath, denyPaths)\n );\n }\n}\n\n/** `{ spilledTo }` for every spill file named in the truncation metadata, or nothing. */\nfunction spilledField(\n truncation: { stdout?: WorkspaceTruncation; stderr?: WorkspaceTruncation } | undefined,\n): { spilledTo?: string[] } {\n const paths = [truncation?.stdout?.path, truncation?.stderr?.path].filter(\n (file): file is string => file !== undefined,\n );\n\n return paths.length > 0 ? { spilledTo: paths } : {};\n}\n\n/** Combine per-stream truncation metadata; `undefined` when neither stream was clipped. */\nfunction streamTruncation(\n stdout: WorkspaceTruncation | undefined,\n stderr: WorkspaceTruncation | undefined,\n): { stdout?: WorkspaceTruncation; stderr?: WorkspaceTruncation } | undefined {\n if (stdout === undefined && stderr === undefined) {\n return undefined;\n }\n\n return {\n ...(stdout !== undefined ? { stdout } : {}),\n ...(stderr !== undefined ? { stderr } : {}),\n };\n}\n\n/** The lower of two optional limits (either may be absent). */\nfunction lowerOf(\n policyLimit: number | undefined,\n requested: number | undefined,\n): number | undefined {\n if (requested === undefined || requested <= 0) {\n return policyLimit;\n }\n\n return policyLimit === undefined ? requested : Math.min(policyLimit, requested);\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 * @param runtime - State shared with other views of the same workspace\n * (its background commands); a fresh one when omitted.\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 runtime?: OpsRuntime,\n): WorkspaceOps {\n return new Ops(backend, policy, runtime ?? { jobs: new ShellJobs() });\n}\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+
3
+ //#region ../../packages/ai-workspace/src/output/spill-dir.ts
4
+ /**
5
+ * The absolute directory spill files go to, or `undefined` when the policy
6
+ * has no `output.spillDir` (spilling is off).
7
+ */
8
+ function resolveSpillDir(policy) {
9
+ const dir = policy.output?.spillDir;
10
+ return dir === void 0 ? void 0 : path.resolve(policy.cwd, dir);
11
+ }
12
+
13
+ //#endregion
14
+ export { resolveSpillDir };
15
+ //# sourceMappingURL=spill-dir.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spill-dir.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/output/spill-dir.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { WorkspacePolicy } from \"../contracts\";\n\n/**\n * The absolute directory spill files go to, or `undefined` when the policy\n * has no `output.spillDir` (spilling is off).\n */\nexport function resolveSpillDir(policy: WorkspacePolicy): string | undefined {\n const dir = policy.output?.spillDir;\n\n return dir === undefined ? undefined : path.resolve(policy.cwd, dir);\n}\n"],"mappings":";;;;;;;AAOA,SAAgB,gBAAgB,QAA6C;CAC3E,MAAM,MAAM,OAAO,QAAQ;CAE3B,OAAO,QAAQ,SAAY,SAAY,KAAK,QAAQ,OAAO,KAAK,GAAG;AACrE"}
@@ -0,0 +1,223 @@
1
+ import { resolveSpillDir } from "./spill-dir.mjs";
2
+ import path from "node:path";
3
+
4
+ //#region ../../packages/ai-workspace/src/output/spill.ts
5
+ const DEFAULT_MAX_LINES = 2e3;
6
+ const DEFAULT_MAX_BYTES = 51200;
7
+ const DEFAULT_RETENTION_MS = 6048e5;
8
+ const SPILL_NAME = /^spill-(\d+)-[a-z0-9]+-[a-z0-9_-]+\.txt$/;
9
+ function byteLength(value) {
10
+ return Buffer.byteLength(value, "utf8");
11
+ }
12
+ /** The first `maxBytes` bytes of `value`, cut on a character boundary. */
13
+ function headOf(value, maxBytes) {
14
+ const bytes = Buffer.from(value, "utf8");
15
+ if (bytes.byteLength <= maxBytes) return value;
16
+ let cut = Math.max(0, maxBytes);
17
+ while (cut > 0 && ((bytes[cut] ?? 0) & 192) === 128) cut--;
18
+ return bytes.subarray(0, cut).toString("utf8");
19
+ }
20
+ /** The last `maxBytes` bytes of `value`, cut on a character boundary. */
21
+ function tailOf(value, maxBytes) {
22
+ const bytes = Buffer.from(value, "utf8");
23
+ if (bytes.byteLength <= maxBytes) return value;
24
+ let from = bytes.byteLength - Math.max(0, maxBytes);
25
+ while (from < bytes.byteLength && ((bytes[from] ?? 0) & 192) === 128) from++;
26
+ return bytes.subarray(from).toString("utf8");
27
+ }
28
+ /** Lines of `value`; a trailing line break does not add an empty line. */
29
+ function splitLines(value) {
30
+ if (value === "") return [];
31
+ const lines = value.split("\n");
32
+ if (lines[lines.length - 1] === "") lines.pop();
33
+ return lines;
34
+ }
35
+ /** Pick lines from the front within `lines` / `bytes`; one oversized line is clipped. */
36
+ function takeHead(lines, maxLines, maxBytes) {
37
+ const out = [];
38
+ let used = 0;
39
+ for (const line of lines) {
40
+ if (out.length >= maxLines) break;
41
+ const cost = byteLength(line) + 1;
42
+ if (used + cost > maxBytes) {
43
+ if (out.length === 0 && maxBytes > 0) out.push(headOf(line, maxBytes));
44
+ break;
45
+ }
46
+ out.push(line);
47
+ used += cost;
48
+ }
49
+ return out;
50
+ }
51
+ /** Pick lines from the back, same limits; the result is in file order. */
52
+ function takeTail(lines, maxLines, maxBytes) {
53
+ const out = [];
54
+ let used = 0;
55
+ for (let index = lines.length - 1; index >= 0; index--) {
56
+ const line = lines[index] ?? "";
57
+ if (out.length >= maxLines) break;
58
+ const cost = byteLength(line) + 1;
59
+ if (used + cost > maxBytes) {
60
+ if (out.length === 0 && maxBytes > 0) out.push(tailOf(line, maxBytes));
61
+ break;
62
+ }
63
+ out.unshift(line);
64
+ used += cost;
65
+ }
66
+ return out;
67
+ }
68
+ /** Choose the lines to keep for `keep`; `headCount` lines come from the front. */
69
+ function select(lines, keep, maxLines, maxBytes) {
70
+ if (keep === "head") return {
71
+ head: takeHead(lines, maxLines, maxBytes),
72
+ tail: []
73
+ };
74
+ if (keep === "tail") return {
75
+ head: [],
76
+ tail: takeTail(lines, maxLines, maxBytes)
77
+ };
78
+ const headLines = Math.ceil(maxLines / 2);
79
+ const headBytes = Math.ceil(maxBytes / 2);
80
+ const head = takeHead(lines, headLines, headBytes);
81
+ return {
82
+ head,
83
+ tail: takeTail(lines.slice(head.length), maxLines - head.length, maxBytes - headBytes)
84
+ };
85
+ }
86
+ /**
87
+ * Keeps oversized command, search and read output from flooding the model:
88
+ * with `output.spillDir` set it saves the full output to a file and returns
89
+ * the part the policy keeps plus a pointer to it. One instance per ops layer;
90
+ * it remembers the files it wrote so `dispose()` can remove them.
91
+ */
92
+ var OutputSpiller = class {
93
+ constructor(backend, policy) {
94
+ this.backend = backend;
95
+ this.policy = policy;
96
+ this.created = [];
97
+ this.dir = resolveSpillDir(policy);
98
+ if (this.dir !== void 0 && this.retentionMs > 0) this.sweep();
99
+ }
100
+ /** True when `output.spillDir` is set. */
101
+ get enabled() {
102
+ return this.dir !== void 0;
103
+ }
104
+ /** The spill directory, when spilling is on. */
105
+ get spillDir() {
106
+ return this.dir;
107
+ }
108
+ get retentionMs() {
109
+ return this.policy.output?.retentionMs ?? DEFAULT_RETENTION_MS;
110
+ }
111
+ limits(cap) {
112
+ const configured = this.policy.output?.maxBytes ?? DEFAULT_MAX_BYTES;
113
+ return {
114
+ maxLines: Math.max(1, Math.floor(this.policy.output?.maxLines ?? DEFAULT_MAX_LINES)),
115
+ maxBytes: Math.max(1, Math.floor(cap === void 0 ? configured : Math.min(configured, cap)))
116
+ };
117
+ }
118
+ /** Remove spill files older than the retention (only files named like ours). */
119
+ async sweep() {
120
+ if (this.dir === void 0) return;
121
+ try {
122
+ const now = Date.now();
123
+ for (const file of await this.backend.list(this.dir)) {
124
+ const match = SPILL_NAME.exec(path.basename(file));
125
+ if (match !== null && now - Number(match[1]) > this.retentionMs) await this.backend.remove(file).catch(() => void 0);
126
+ }
127
+ } catch {}
128
+ }
129
+ /** Write `content` to a new spill file and return its absolute path. */
130
+ async write(label, content) {
131
+ if (this.dir === void 0) return;
132
+ try {
133
+ if (this.retentionMs > 0) await this.sweep();
134
+ const safe = label.toLowerCase().replace(/[^a-z0-9_-]+/g, "-") || "output";
135
+ const rand = Math.random().toString(36).slice(2, 8) || "x";
136
+ const file = path.join(this.dir, `spill-${Date.now()}-${rand}-${safe}.txt`);
137
+ await this.backend.mkdir(this.dir);
138
+ await this.backend.writeFile(file, content);
139
+ this.created.push(file);
140
+ return file;
141
+ } catch {
142
+ return;
143
+ }
144
+ }
145
+ /**
146
+ * Fit `value` to the policy. Returns it untouched when it fits. Over the
147
+ * limits with spilling on, the whole text is saved and the kept lines come
148
+ * back with a `[showing lines … of …; full output saved to …]` marker.
149
+ * `cap` is a tighter byte limit from elsewhere (`shell.maxOutputBytes`).
150
+ */
151
+ async text(label, value, defaultKeep, cap) {
152
+ if (this.dir === void 0) return { value };
153
+ const { maxLines, maxBytes } = this.limits(cap);
154
+ const lines = splitLines(value);
155
+ const totalBytes = byteLength(value);
156
+ if (lines.length <= maxLines && totalBytes <= maxBytes) return { value };
157
+ const { head, tail } = select(lines, this.policy.output?.keep ?? defaultKeep, maxLines, maxBytes);
158
+ const file = await this.write(label, value);
159
+ const shown = head.length + tail.length;
160
+ const total = lines.length;
161
+ const where = file === void 0 ? "the rest was dropped" : `full output saved to ${file}`;
162
+ const marker = `[showing ${tail.length === 0 ? `lines 1-${head.length}` : head.length === 0 ? `lines ${total - tail.length + 1}-${total}` : `lines 1-${head.length} and ${total - tail.length + 1}-${total}`} of ${total}; ${where}]`;
163
+ const parts = [...head.length > 0 ? [head.join("\n")] : [], marker];
164
+ if (tail.length > 0) {
165
+ parts.push(tail.join("\n"));
166
+ if (file !== void 0) parts.push(`[full output saved to ${file}]`);
167
+ }
168
+ return {
169
+ value: parts.join("\n"),
170
+ truncation: {
171
+ shown: { lines: shown },
172
+ total: {
173
+ lines: total,
174
+ bytes: totalBytes
175
+ },
176
+ ...file !== void 0 ? { path: file } : {}
177
+ }
178
+ };
179
+ }
180
+ /**
181
+ * Fit a list of entries (one line each when rendered) to the policy; the
182
+ * rendered full list is what gets saved.
183
+ */
184
+ async list(label, items, render, defaultKeep) {
185
+ if (this.dir === void 0) return { items };
186
+ const { maxLines, maxBytes } = this.limits(void 0);
187
+ const lines = items.map(render);
188
+ const totalBytes = lines.reduce((sum, line) => sum + byteLength(line) + 1, 0);
189
+ if (items.length <= maxLines && totalBytes <= maxBytes) return { items };
190
+ const { head, tail } = select(lines, this.policy.output?.keep ?? defaultKeep, maxLines, maxBytes);
191
+ const headCount = head.length;
192
+ const tailCount = tail.length;
193
+ const file = await this.write(label, lines.join("\n") + "\n");
194
+ return {
195
+ items: [...items.slice(0, headCount), ...items.slice(items.length - tailCount)],
196
+ truncation: {
197
+ shown: { lines: headCount + tailCount },
198
+ total: {
199
+ lines: items.length,
200
+ bytes: totalBytes
201
+ },
202
+ ...file !== void 0 ? { path: file } : {}
203
+ }
204
+ };
205
+ }
206
+ /** Save the complete text of an output that was already cut elsewhere. */
207
+ async save(label, content) {
208
+ return this.write(label, content);
209
+ }
210
+ /** Remove expired files, and this run's own when `retentionMs` is `0`. */
211
+ async dispose() {
212
+ if (this.dir === void 0) return;
213
+ if (this.retentionMs === 0) {
214
+ for (const file of this.created.splice(0)) await this.backend.remove(file).catch(() => void 0);
215
+ return;
216
+ }
217
+ await this.sweep();
218
+ }
219
+ };
220
+
221
+ //#endregion
222
+ export { OutputSpiller, splitLines };
223
+ //# sourceMappingURL=spill.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spill.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/output/spill.ts"],"sourcesContent":["import path from \"node:path\";\nimport type {\n WorkspaceBackend,\n WorkspaceOutputKeep,\n WorkspacePolicy,\n WorkspaceTruncation,\n} from \"../contracts\";\nimport { resolveSpillDir } from \"./spill-dir\";\n\nconst DEFAULT_MAX_LINES = 2000;\nconst DEFAULT_MAX_BYTES = 50 * 1024;\nconst DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;\nconst SPILL_NAME = /^spill-(\\d+)-[a-z0-9]+-[a-z0-9_-]+\\.txt$/;\n\n/** What spilling an output produced. */\nexport interface SpilledText {\n /** The text to return (marker included). */\n value: string;\n /** Set when anything was left out. */\n truncation?: WorkspaceTruncation;\n}\n\n/** What spilling a list of entries produced. */\nexport interface SpilledList<T> {\n items: T[];\n truncation?: WorkspaceTruncation;\n}\n\nfunction byteLength(value: string): number {\n return Buffer.byteLength(value, \"utf8\");\n}\n\n/** The first `maxBytes` bytes of `value`, cut on a character boundary. */\nfunction headOf(value: string, maxBytes: number): string {\n const bytes = Buffer.from(value, \"utf8\");\n\n if (bytes.byteLength <= maxBytes) {\n return value;\n }\n\n let cut = Math.max(0, maxBytes);\n\n while (cut > 0 && ((bytes[cut] ?? 0) & 0xc0) === 0x80) {\n cut--;\n }\n\n return bytes.subarray(0, cut).toString(\"utf8\");\n}\n\n/** The last `maxBytes` bytes of `value`, cut on a character boundary. */\nfunction tailOf(value: string, maxBytes: number): string {\n const bytes = Buffer.from(value, \"utf8\");\n\n if (bytes.byteLength <= maxBytes) {\n return value;\n }\n\n let from = bytes.byteLength - Math.max(0, maxBytes);\n\n while (from < bytes.byteLength && ((bytes[from] ?? 0) & 0xc0) === 0x80) {\n from++;\n }\n\n return bytes.subarray(from).toString(\"utf8\");\n}\n\n/** Lines of `value`; a trailing line break does not add an empty line. */\nexport function splitLines(value: string): string[] {\n if (value === \"\") {\n return [];\n }\n\n const lines = value.split(\"\\n\");\n\n if (lines[lines.length - 1] === \"\") {\n lines.pop();\n }\n\n return lines;\n}\n\n/** Pick lines from the front within `lines` / `bytes`; one oversized line is clipped. */\nfunction takeHead(lines: string[], maxLines: number, maxBytes: number): string[] {\n const out: string[] = [];\n let used = 0;\n\n for (const line of lines) {\n if (out.length >= maxLines) {\n break;\n }\n\n const cost = byteLength(line) + 1;\n\n if (used + cost > maxBytes) {\n if (out.length === 0 && maxBytes > 0) {\n out.push(headOf(line, maxBytes));\n }\n\n break;\n }\n\n out.push(line);\n used += cost;\n }\n\n return out;\n}\n\n/** Pick lines from the back, same limits; the result is in file order. */\nfunction takeTail(lines: string[], maxLines: number, maxBytes: number): string[] {\n const out: string[] = [];\n let used = 0;\n\n for (let index = lines.length - 1; index >= 0; index--) {\n const line = lines[index] ?? \"\";\n\n if (out.length >= maxLines) {\n break;\n }\n\n const cost = byteLength(line) + 1;\n\n if (used + cost > maxBytes) {\n if (out.length === 0 && maxBytes > 0) {\n out.push(tailOf(line, maxBytes));\n }\n\n break;\n }\n\n out.unshift(line);\n used += cost;\n }\n\n return out;\n}\n\n/** Choose the lines to keep for `keep`; `headCount` lines come from the front. */\nfunction select(\n lines: string[],\n keep: WorkspaceOutputKeep,\n maxLines: number,\n maxBytes: number,\n): { head: string[]; tail: string[] } {\n if (keep === \"head\") {\n return { head: takeHead(lines, maxLines, maxBytes), tail: [] };\n }\n\n if (keep === \"tail\") {\n return { head: [], tail: takeTail(lines, maxLines, maxBytes) };\n }\n\n const headLines = Math.ceil(maxLines / 2);\n const headBytes = Math.ceil(maxBytes / 2);\n const head = takeHead(lines, headLines, headBytes);\n const rest = lines.slice(head.length);\n const tail = takeTail(rest, maxLines - head.length, maxBytes - headBytes);\n\n return { head, tail };\n}\n\n/**\n * Keeps oversized command, search and read output from flooding the model:\n * with `output.spillDir` set it saves the full output to a file and returns\n * the part the policy keeps plus a pointer to it. One instance per ops layer;\n * it remembers the files it wrote so `dispose()` can remove them.\n */\nexport class OutputSpiller {\n private readonly created: string[] = [];\n private readonly dir: string | undefined;\n\n public constructor(\n private readonly backend: WorkspaceBackend,\n private readonly policy: WorkspacePolicy,\n ) {\n this.dir = resolveSpillDir(policy);\n\n if (this.dir !== undefined && this.retentionMs > 0) {\n void this.sweep();\n }\n }\n\n /** True when `output.spillDir` is set. */\n public get enabled(): boolean {\n return this.dir !== undefined;\n }\n\n /** The spill directory, when spilling is on. */\n public get spillDir(): string | undefined {\n return this.dir;\n }\n\n private get retentionMs(): number {\n return this.policy.output?.retentionMs ?? DEFAULT_RETENTION_MS;\n }\n\n private limits(cap: number | undefined): { maxLines: number; maxBytes: number } {\n const configured = this.policy.output?.maxBytes ?? DEFAULT_MAX_BYTES;\n\n return {\n maxLines: Math.max(1, Math.floor(this.policy.output?.maxLines ?? DEFAULT_MAX_LINES)),\n maxBytes: Math.max(1, Math.floor(cap === undefined ? configured : Math.min(configured, cap))),\n };\n }\n\n /** Remove spill files older than the retention (only files named like ours). */\n public async sweep(): Promise<void> {\n if (this.dir === undefined) {\n return;\n }\n\n try {\n const now = Date.now();\n\n for (const file of await this.backend.list(this.dir)) {\n const match = SPILL_NAME.exec(path.basename(file));\n\n if (match !== null && now - Number(match[1]) > this.retentionMs) {\n await this.backend.remove(file).catch(() => undefined);\n }\n }\n } catch {\n // No spill directory yet, or it is not listable: nothing to clean.\n }\n }\n\n /** Write `content` to a new spill file and return its absolute path. */\n private async write(label: string, content: string): Promise<string | undefined> {\n if (this.dir === undefined) {\n return undefined;\n }\n\n try {\n if (this.retentionMs > 0) {\n await this.sweep();\n }\n\n const safe = label.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\") || \"output\";\n const rand = Math.random().toString(36).slice(2, 8) || \"x\";\n const file = path.join(this.dir, `spill-${Date.now()}-${rand}-${safe}.txt`);\n\n await this.backend.mkdir(this.dir);\n await this.backend.writeFile(file, content);\n this.created.push(file);\n\n return file;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Fit `value` to the policy. Returns it untouched when it fits. Over the\n * limits with spilling on, the whole text is saved and the kept lines come\n * back with a `[showing lines … of …; full output saved to …]` marker.\n * `cap` is a tighter byte limit from elsewhere (`shell.maxOutputBytes`).\n */\n public async text(\n label: string,\n value: string,\n defaultKeep: WorkspaceOutputKeep,\n cap?: number,\n ): Promise<SpilledText> {\n if (this.dir === undefined) {\n return { value };\n }\n\n const { maxLines, maxBytes } = this.limits(cap);\n const lines = splitLines(value);\n const totalBytes = byteLength(value);\n\n if (lines.length <= maxLines && totalBytes <= maxBytes) {\n return { value };\n }\n\n const keep = this.policy.output?.keep ?? defaultKeep;\n const { head, tail } = select(lines, keep, maxLines, maxBytes);\n const file = await this.write(label, value);\n const shown = head.length + tail.length;\n const total = lines.length;\n const where = file === undefined ? \"the rest was dropped\" : `full output saved to ${file}`;\n const range =\n tail.length === 0\n ? `lines 1-${head.length}`\n : head.length === 0\n ? `lines ${total - tail.length + 1}-${total}`\n : `lines 1-${head.length} and ${total - tail.length + 1}-${total}`;\n const marker = `[showing ${range} of ${total}; ${where}]`;\n const parts = [...(head.length > 0 ? [head.join(\"\\n\")] : []), marker];\n\n if (tail.length > 0) {\n parts.push(tail.join(\"\\n\"));\n\n // The marker sits between head and tail; end the text with the file too.\n if (file !== undefined) {\n parts.push(`[full output saved to ${file}]`);\n }\n }\n\n return {\n value: parts.join(\"\\n\"),\n truncation: {\n shown: { lines: shown },\n total: { lines: total, bytes: totalBytes },\n ...(file !== undefined ? { path: file } : {}),\n },\n };\n }\n\n /**\n * Fit a list of entries (one line each when rendered) to the policy; the\n * rendered full list is what gets saved.\n */\n public async list<T>(\n label: string,\n items: T[],\n render: (item: T) => string,\n defaultKeep: WorkspaceOutputKeep,\n ): Promise<SpilledList<T>> {\n if (this.dir === undefined) {\n return { items };\n }\n\n const { maxLines, maxBytes } = this.limits(undefined);\n const lines = items.map(render);\n const totalBytes = lines.reduce((sum, line) => sum + byteLength(line) + 1, 0);\n\n if (items.length <= maxLines && totalBytes <= maxBytes) {\n return { items };\n }\n\n const keep = this.policy.output?.keep ?? defaultKeep;\n // Count how many whole entries fit from each end, then slice the items.\n const { head, tail } = select(lines, keep, maxLines, maxBytes);\n const headCount = head.length;\n const tailCount = tail.length;\n const file = await this.write(label, lines.join(\"\\n\") + \"\\n\");\n\n return {\n items: [...items.slice(0, headCount), ...items.slice(items.length - tailCount)],\n truncation: {\n shown: { lines: headCount + tailCount },\n total: { lines: items.length, bytes: totalBytes },\n ...(file !== undefined ? { path: file } : {}),\n },\n };\n }\n\n /** Save the complete text of an output that was already cut elsewhere. */\n public async save(label: string, content: string): Promise<string | undefined> {\n return this.write(label, content);\n }\n\n /** Remove expired files, and this run's own when `retentionMs` is `0`. */\n public async dispose(): Promise<void> {\n if (this.dir === undefined) {\n return;\n }\n\n if (this.retentionMs === 0) {\n for (const file of this.created.splice(0)) {\n await this.backend.remove(file).catch(() => undefined);\n }\n\n return;\n }\n\n await this.sweep();\n }\n}\n"],"mappings":";;;;AASA,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,uBAAuB;AAC7B,MAAM,aAAa;AAgBnB,SAAS,WAAW,OAAuB;CACzC,OAAO,OAAO,WAAW,OAAO,MAAM;AACxC;;AAGA,SAAS,OAAO,OAAe,UAA0B;CACvD,MAAM,QAAQ,OAAO,KAAK,OAAO,MAAM;CAEvC,IAAI,MAAM,cAAc,UACtB,OAAO;CAGT,IAAI,MAAM,KAAK,IAAI,GAAG,QAAQ;CAE9B,OAAO,MAAM,OAAO,MAAM,QAAQ,KAAK,SAAU,KAC/C;CAGF,OAAO,MAAM,SAAS,GAAG,GAAG,CAAC,CAAC,SAAS,MAAM;AAC/C;;AAGA,SAAS,OAAO,OAAe,UAA0B;CACvD,MAAM,QAAQ,OAAO,KAAK,OAAO,MAAM;CAEvC,IAAI,MAAM,cAAc,UACtB,OAAO;CAGT,IAAI,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,QAAQ;CAElD,OAAO,OAAO,MAAM,gBAAgB,MAAM,SAAS,KAAK,SAAU,KAChE;CAGF,OAAO,MAAM,SAAS,IAAI,CAAC,CAAC,SAAS,MAAM;AAC7C;;AAGA,SAAgB,WAAW,OAAyB;CAClD,IAAI,UAAU,IACZ,OAAO,CAAC;CAGV,MAAM,QAAQ,MAAM,MAAM,IAAI;CAE9B,IAAI,MAAM,MAAM,SAAS,OAAO,IAC9B,MAAM,IAAI;CAGZ,OAAO;AACT;;AAGA,SAAS,SAAS,OAAiB,UAAkB,UAA4B;CAC/E,MAAM,MAAgB,CAAC;CACvB,IAAI,OAAO;CAEX,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,IAAI,UAAU,UAChB;EAGF,MAAM,OAAO,WAAW,IAAI,IAAI;EAEhC,IAAI,OAAO,OAAO,UAAU;GAC1B,IAAI,IAAI,WAAW,KAAK,WAAW,GACjC,IAAI,KAAK,OAAO,MAAM,QAAQ,CAAC;GAGjC;EACF;EAEA,IAAI,KAAK,IAAI;EACb,QAAQ;CACV;CAEA,OAAO;AACT;;AAGA,SAAS,SAAS,OAAiB,UAAkB,UAA4B;CAC/E,MAAM,MAAgB,CAAC;CACvB,IAAI,OAAO;CAEX,KAAK,IAAI,QAAQ,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS;EACtD,MAAM,OAAO,MAAM,UAAU;EAE7B,IAAI,IAAI,UAAU,UAChB;EAGF,MAAM,OAAO,WAAW,IAAI,IAAI;EAEhC,IAAI,OAAO,OAAO,UAAU;GAC1B,IAAI,IAAI,WAAW,KAAK,WAAW,GACjC,IAAI,KAAK,OAAO,MAAM,QAAQ,CAAC;GAGjC;EACF;EAEA,IAAI,QAAQ,IAAI;EAChB,QAAQ;CACV;CAEA,OAAO;AACT;;AAGA,SAAS,OACP,OACA,MACA,UACA,UACoC;CACpC,IAAI,SAAS,QACX,OAAO;EAAE,MAAM,SAAS,OAAO,UAAU,QAAQ;EAAG,MAAM,CAAC;CAAE;CAG/D,IAAI,SAAS,QACX,OAAO;EAAE,MAAM,CAAC;EAAG,MAAM,SAAS,OAAO,UAAU,QAAQ;CAAE;CAG/D,MAAM,YAAY,KAAK,KAAK,WAAW,CAAC;CACxC,MAAM,YAAY,KAAK,KAAK,WAAW,CAAC;CACxC,MAAM,OAAO,SAAS,OAAO,WAAW,SAAS;CAIjD,OAAO;EAAE;EAAM,MAFF,SADA,MAAM,MAAM,KAAK,MACL,GAAG,WAAW,KAAK,QAAQ,WAAW,SAE7C;CAAE;AACtB;;;;;;;AAQA,IAAa,gBAAb,MAA2B;CAIzB,AAAO,YACL,AAAiB,SACjB,AAAiB,QACjB;EAFiB;EACA;iBALkB,CAAC;EAOpC,KAAK,MAAM,gBAAgB,MAAM;EAEjC,IAAI,KAAK,QAAQ,UAAa,KAAK,cAAc,GAC/C,AAAK,KAAK,MAAM;CAEpB;;CAGA,IAAW,UAAmB;EAC5B,OAAO,KAAK,QAAQ;CACtB;;CAGA,IAAW,WAA+B;EACxC,OAAO,KAAK;CACd;CAEA,IAAY,cAAsB;EAChC,OAAO,KAAK,OAAO,QAAQ,eAAe;CAC5C;CAEA,AAAQ,OAAO,KAAiE;EAC9E,MAAM,aAAa,KAAK,OAAO,QAAQ,YAAY;EAEnD,OAAO;GACL,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,OAAO,QAAQ,YAAY,iBAAiB,CAAC;GACnF,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,SAAY,aAAa,KAAK,IAAI,YAAY,GAAG,CAAC,CAAC;EAC9F;CACF;;CAGA,MAAa,QAAuB;EAClC,IAAI,KAAK,QAAQ,QACf;EAGF,IAAI;GACF,MAAM,MAAM,KAAK,IAAI;GAErB,KAAK,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK,KAAK,GAAG,GAAG;IACpD,MAAM,QAAQ,WAAW,KAAK,KAAK,SAAS,IAAI,CAAC;IAEjD,IAAI,UAAU,QAAQ,MAAM,OAAO,MAAM,EAAE,IAAI,KAAK,aAClD,MAAM,KAAK,QAAQ,OAAO,IAAI,CAAC,CAAC,YAAY,MAAS;GAEzD;EACF,QAAQ,CAER;CACF;;CAGA,MAAc,MAAM,OAAe,SAA8C;EAC/E,IAAI,KAAK,QAAQ,QACf;EAGF,IAAI;GACF,IAAI,KAAK,cAAc,GACrB,MAAM,KAAK,MAAM;GAGnB,MAAM,OAAO,MAAM,YAAY,CAAC,CAAC,QAAQ,iBAAiB,GAAG,KAAK;GAClE,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK;GACvD,MAAM,OAAO,KAAK,KAAK,KAAK,KAAK,SAAS,KAAK,IAAI,EAAE,GAAG,KAAK,GAAG,KAAK,KAAK;GAE1E,MAAM,KAAK,QAAQ,MAAM,KAAK,GAAG;GACjC,MAAM,KAAK,QAAQ,UAAU,MAAM,OAAO;GAC1C,KAAK,QAAQ,KAAK,IAAI;GAEtB,OAAO;EACT,QAAQ;GACN;EACF;CACF;;;;;;;CAQA,MAAa,KACX,OACA,OACA,aACA,KACsB;EACtB,IAAI,KAAK,QAAQ,QACf,OAAO,EAAE,MAAM;EAGjB,MAAM,EAAE,UAAU,aAAa,KAAK,OAAO,GAAG;EAC9C,MAAM,QAAQ,WAAW,KAAK;EAC9B,MAAM,aAAa,WAAW,KAAK;EAEnC,IAAI,MAAM,UAAU,YAAY,cAAc,UAC5C,OAAO,EAAE,MAAM;EAIjB,MAAM,EAAE,MAAM,SAAS,OAAO,OADjB,KAAK,OAAO,QAAQ,QAAQ,aACE,UAAU,QAAQ;EAC7D,MAAM,OAAO,MAAM,KAAK,MAAM,OAAO,KAAK;EAC1C,MAAM,QAAQ,KAAK,SAAS,KAAK;EACjC,MAAM,QAAQ,MAAM;EACpB,MAAM,QAAQ,SAAS,SAAY,yBAAyB,wBAAwB;EAOpF,MAAM,SAAS,YALb,KAAK,WAAW,IACZ,WAAW,KAAK,WAChB,KAAK,WAAW,IACd,SAAS,QAAQ,KAAK,SAAS,EAAE,GAAG,UACpC,WAAW,KAAK,OAAO,OAAO,QAAQ,KAAK,SAAS,EAAE,GAAG,QAChC,MAAM,MAAM,IAAI,MAAM;EACvD,MAAM,QAAQ,CAAC,GAAI,KAAK,SAAS,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,GAAI,MAAM;EAEpE,IAAI,KAAK,SAAS,GAAG;GACnB,MAAM,KAAK,KAAK,KAAK,IAAI,CAAC;GAG1B,IAAI,SAAS,QACX,MAAM,KAAK,yBAAyB,KAAK,EAAE;EAE/C;EAEA,OAAO;GACL,OAAO,MAAM,KAAK,IAAI;GACtB,YAAY;IACV,OAAO,EAAE,OAAO,MAAM;IACtB,OAAO;KAAE,OAAO;KAAO,OAAO;IAAW;IACzC,GAAI,SAAS,SAAY,EAAE,MAAM,KAAK,IAAI,CAAC;GAC7C;EACF;CACF;;;;;CAMA,MAAa,KACX,OACA,OACA,QACA,aACyB;EACzB,IAAI,KAAK,QAAQ,QACf,OAAO,EAAE,MAAM;EAGjB,MAAM,EAAE,UAAU,aAAa,KAAK,OAAO,MAAS;EACpD,MAAM,QAAQ,MAAM,IAAI,MAAM;EAC9B,MAAM,aAAa,MAAM,QAAQ,KAAK,SAAS,MAAM,WAAW,IAAI,IAAI,GAAG,CAAC;EAE5E,IAAI,MAAM,UAAU,YAAY,cAAc,UAC5C,OAAO,EAAE,MAAM;EAKjB,MAAM,EAAE,MAAM,SAAS,OAAO,OAFjB,KAAK,OAAO,QAAQ,QAAQ,aAEE,UAAU,QAAQ;EAC7D,MAAM,YAAY,KAAK;EACvB,MAAM,YAAY,KAAK;EACvB,MAAM,OAAO,MAAM,KAAK,MAAM,OAAO,MAAM,KAAK,IAAI,IAAI,IAAI;EAE5D,OAAO;GACL,OAAO,CAAC,GAAG,MAAM,MAAM,GAAG,SAAS,GAAG,GAAG,MAAM,MAAM,MAAM,SAAS,SAAS,CAAC;GAC9E,YAAY;IACV,OAAO,EAAE,OAAO,YAAY,UAAU;IACtC,OAAO;KAAE,OAAO,MAAM;KAAQ,OAAO;IAAW;IAChD,GAAI,SAAS,SAAY,EAAE,MAAM,KAAK,IAAI,CAAC;GAC7C;EACF;CACF;;CAGA,MAAa,KAAK,OAAe,SAA8C;EAC7E,OAAO,KAAK,MAAM,OAAO,OAAO;CAClC;;CAGA,MAAa,UAAyB;EACpC,IAAI,KAAK,QAAQ,QACf;EAGF,IAAI,KAAK,gBAAgB,GAAG;GAC1B,KAAK,MAAM,QAAQ,KAAK,QAAQ,OAAO,CAAC,GACtC,MAAM,KAAK,QAAQ,OAAO,IAAI,CAAC,CAAC,YAAY,MAAS;GAGvD;EACF;EAEA,MAAM,KAAK,MAAM;CACnB;AACF"}
@@ -0,0 +1,22 @@
1
+ //#region ../../packages/ai-workspace/src/policy/approval-helpers.ts
2
+ /** The longest refusal reason passed on to the model, in characters. */
3
+ const MAX_REFUSAL_REASON = 500;
4
+ /** Whether two command patterns are the same (strings by value, RegExps by source and flags). */
5
+ function samePattern(a, b) {
6
+ if (typeof a === "string" || typeof b === "string") return a === b;
7
+ return a.source === b.source && a.flags === b.flags;
8
+ }
9
+ /**
10
+ * The tail of a refusal message for an owner's reason: `"."` without one,
11
+ * otherwise `" — <reason>"` cut at {@link MAX_REFUSAL_REASON} characters with
12
+ * a visible marker.
13
+ */
14
+ function refusalReason(reason) {
15
+ const text = typeof reason === "string" ? reason.trim() : "";
16
+ if (text.length === 0) return ".";
17
+ return ` — ${text.length > 500 ? `${text.slice(0, 500)}… [truncated]` : text}`;
18
+ }
19
+
20
+ //#endregion
21
+ export { refusalReason, samePattern };
22
+ //# sourceMappingURL=approval-helpers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-helpers.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/approval-helpers.ts"],"sourcesContent":["import type { WorkspaceCommandPattern } from \"../contracts\";\n\n/** The longest refusal reason passed on to the model, in characters. */\nexport const MAX_REFUSAL_REASON = 500;\n\n/** Whether two command patterns are the same (strings by value, RegExps by source and flags). */\nexport function samePattern(a: WorkspaceCommandPattern, b: WorkspaceCommandPattern): boolean {\n if (typeof a === \"string\" || typeof b === \"string\") {\n return a === b;\n }\n\n return a.source === b.source && a.flags === b.flags;\n}\n\n/**\n * The tail of a refusal message for an owner's reason: `\".\"` without one,\n * otherwise `\" — <reason>\"` cut at {@link MAX_REFUSAL_REASON} characters with\n * a visible marker.\n */\nexport function refusalReason(reason: string | undefined): string {\n const text = typeof reason === \"string\" ? reason.trim() : \"\";\n\n if (text.length === 0) {\n return \".\";\n }\n\n const shown =\n text.length > MAX_REFUSAL_REASON ? `${text.slice(0, MAX_REFUSAL_REASON)}… [truncated]` : text;\n\n return ` — ${shown}`;\n}\n"],"mappings":";;AAGA,MAAa,qBAAqB;;AAGlC,SAAgB,YAAY,GAA4B,GAAqC;CAC3F,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UACxC,OAAO,MAAM;CAGf,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;AAChD;;;;;;AAOA,SAAgB,cAAc,QAAoC;CAChE,MAAM,OAAO,OAAO,WAAW,WAAW,OAAO,KAAK,IAAI;CAE1D,IAAI,KAAK,WAAW,GAClB,OAAO;CAMT,OAAO,MAFL,KAAK,eAA8B,GAAG,KAAK,MAAM,MAAqB,EAAE,iBAAiB;AAG7F"}
@@ -40,13 +40,15 @@ export interface ResolvedPath {
40
40
  *
41
41
  * @param policy - The bounding policy (its `cwd` is the jail root).
42
42
  * @param inputPath - A workspace-relative or absolute path to resolve.
43
+ * @param access - `"read"` also accepts the `output.spillDir` directory (only
44
+ * `readFile` asks for it); `"write"` (default) never does.
43
45
  * @returns The canonical absolute path plus its `/`-separated relative form.
44
46
  * @throws {WorkspacePolicyError} When the path escapes the jail or hits a deny glob.
45
47
  *
46
48
  * @example
47
49
  * const { absolutePath } = await resolveInJail(policy, "src/index.ts");
48
50
  */
49
- export declare function resolveInJail(policy: WorkspacePolicy, inputPath: string): Promise<ResolvedPath>;
51
+ export declare function resolveInJail(policy: WorkspacePolicy, inputPath: string, access?: "read" | "write"): Promise<ResolvedPath>;
50
52
  /**
51
53
  * Whether a command line is permitted by the policy's `shell` sub-policy —
52
54
  * `checkShellCommand(policy, command).allowed`.
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"mappings":";;;;;;;;;iBAaiB;;EAEf;;;;;;EAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAyEoB,cACpB,QAAQ,iBACR,oBACC,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;wBAqEK,iBAAiB,QAAQ,iBAAiB;;;;;;;;;;;;;wBAgB1C,SAAS,QAAQ,kBAAkB"}
1
+ {"version":3,"file":"policy.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"mappings":";;;;;;;;;iBAciB;;EAEf;;;;;;EAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA2EoB,cACpB,QAAQ,iBACR,mBACA,4BACC,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;wBA2EK,iBAAiB,QAAQ,iBAAiB;;;;;;;;;;;;;wBAgB1C,SAAS,QAAQ,kBAAkB"}