@warlock.js/ai-workspace 5.39.1 → 5.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/cjs/index.cjs +691 -114
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/tool-io.type.d.mts +32 -1
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +10 -1
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +7 -1
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +17 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +1 -0
- package/esm/errors.mjs.map +1 -1
- package/esm/index.d.mts +6 -2
- package/esm/index.mjs +5 -1
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +16 -0
- package/esm/ops.mjs.map +1 -1
- package/esm/patch/apply-hunks.d.mts +14 -0
- package/esm/patch/apply-hunks.d.mts.map +1 -0
- package/esm/patch/apply-hunks.mjs +89 -0
- package/esm/patch/apply-hunks.mjs.map +1 -0
- package/esm/patch/apply-patch-generic.d.mts +14 -0
- package/esm/patch/apply-patch-generic.d.mts.map +1 -0
- package/esm/patch/apply-patch-generic.mjs +68 -0
- package/esm/patch/apply-patch-generic.mjs.map +1 -0
- package/esm/patch/execute-patch.mjs +151 -0
- package/esm/patch/execute-patch.mjs.map +1 -0
- package/esm/patch/parse-patch.d.mts +45 -0
- package/esm/patch/parse-patch.d.mts.map +1 -0
- package/esm/patch/parse-patch.mjs +201 -0
- package/esm/patch/parse-patch.mjs.map +1 -0
- package/esm/tools/apply-patch.d.mts +27 -0
- package/esm/tools/apply-patch.d.mts.map +1 -0
- package/esm/tools/apply-patch.mjs +61 -0
- package/esm/tools/apply-patch.mjs.map +1 -0
- package/esm/tools/edit-file.mjs +1 -1
- package/esm/tools/glob.mjs +1 -1
- package/esm/tools/grep.mjs +1 -1
- package/esm/tools/json-schemas.mjs +10 -1
- package/esm/tools/json-schemas.mjs.map +1 -1
- package/esm/tools/list-dir.mjs +1 -1
- package/esm/tools/multi-edit.mjs +1 -1
- package/esm/tools/read-file.mjs +1 -1
- package/esm/tools/run-shell.mjs +1 -1
- package/esm/tools/run-tests.mjs +1 -1
- package/esm/tools/shell-kill.mjs +1 -1
- package/esm/tools/shell-output.mjs +1 -1
- package/esm/tools/todo.mjs +1 -1
- package/esm/tools/write-file.mjs +1 -1
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +9 -0
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +7 -6
- package/llms.txt +2 -2
- package/package.json +3 -3
- package/skills/build-loop-agent/SKILL.md +1 -1
- package/skills/index.md +1 -1
- package/skills/use-a-workspace/SKILL.md +5 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./tool-io.type.mjs";
|
|
1
|
+
import { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./tool-io.type.mjs";
|
|
2
2
|
import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy } from "./workspace-policy.type.mjs";
|
|
3
3
|
import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./workspace-backend.contract.mjs";
|
|
4
4
|
import { WorkspaceOps } from "./workspace-ops.contract.mjs";
|
|
5
5
|
import { Workspace, WorkspaceTools } from "./workspace.contract.mjs";
|
|
6
|
-
export type { EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, Workspace, WorkspaceAllowCommand, WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspaceOps, WorkspaceOutputStream, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WorkspaceTools, WriteFileInput, WriteFileResult };
|
|
6
|
+
export type { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, Workspace, WorkspaceAllowCommand, WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspaceOps, WorkspaceOutputStream, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WorkspaceTools, WriteFileInput, WriteFileResult };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* The discriminating set of tool names a workspace can vend, used by
|
|
11
11
|
* `tools.pick(...)` to select a least-privilege subset.
|
|
12
12
|
*/
|
|
13
|
-
export type WorkspaceToolName = "readFile" | "editFile" | "multiEdit" | "writeFile" | "listDir" | "runShell" | "shellOutput" | "shellKill" | "runTests" | "grep" | "glob" | "todoWrite" | "todoRead";
|
|
13
|
+
export type WorkspaceToolName = "readFile" | "editFile" | "multiEdit" | "applyPatch" | "writeFile" | "listDir" | "runShell" | "shellOutput" | "shellKill" | "runTests" | "grep" | "glob" | "todoWrite" | "todoRead";
|
|
14
14
|
/** Input to the `read_file` tool / `readFile()` direct method. */
|
|
15
15
|
export interface ReadFileInput {
|
|
16
16
|
/** Workspace-relative path to read. */
|
|
@@ -120,6 +120,37 @@ export interface MultiEditResult {
|
|
|
120
120
|
/** SHA-256 of the file content after all edits. */
|
|
121
121
|
hash: string;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Input to the `apply_patch` tool / `applyPatch()` direct method — a unified
|
|
125
|
+
* diff (plain or `git diff` style) that may modify, create, delete and rename
|
|
126
|
+
* several files. It applies completely or not at all.
|
|
127
|
+
*/
|
|
128
|
+
export interface ApplyPatchInput {
|
|
129
|
+
/** The unified diff text. */
|
|
130
|
+
patch: string;
|
|
131
|
+
/** Check that every hunk applies, and report what would change, without writing anything. */
|
|
132
|
+
dryRun?: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** What a patch did (or, on a dry run, would do) to one file. */
|
|
135
|
+
export interface ApplyPatchFileResult {
|
|
136
|
+
/** Workspace-relative path after the change (the removed path for a delete). */
|
|
137
|
+
path: string;
|
|
138
|
+
/** What happened to the file. */
|
|
139
|
+
action: "modified" | "created" | "deleted" | "renamed";
|
|
140
|
+
/** The previous path, for a rename. */
|
|
141
|
+
oldPath?: string;
|
|
142
|
+
/** Number of hunks applied. */
|
|
143
|
+
hunks: number;
|
|
144
|
+
/** SHA-256 of the file after the change; absent for a delete. */
|
|
145
|
+
hash?: string;
|
|
146
|
+
}
|
|
147
|
+
/** Result of a successful patch. */
|
|
148
|
+
export interface ApplyPatchResult {
|
|
149
|
+
/** Per-file outcome, in patch order. */
|
|
150
|
+
files: ApplyPatchFileResult[];
|
|
151
|
+
/** True when nothing was written because `dryRun` was set. */
|
|
152
|
+
dryRun: boolean;
|
|
153
|
+
}
|
|
123
154
|
/** Input to the `write_file` tool / `writeFile()` direct method. */
|
|
124
155
|
export interface WriteFileInput {
|
|
125
156
|
/** Workspace-relative path to write (created if absent). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-io.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/tool-io.type.ts"],"mappings":";;;;;;;;;;;;YAYY;;
|
|
1
|
+
{"version":3,"file":"tool-io.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/tool-io.type.ts"],"mappings":";;;;;;;;;;;;YAYY;;iBAiBK;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;;iBAee;;EAEf;;EAEA;;;;;EAKA;;EAEA;;;;;;EAMA;;EAEA;;EAEA;;;;;;EAMA;;;;;;iBAOe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;;;;;;;iBASe;;EAEf;;EAEA,OAAO;;EAEP;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;iBAQe;;EAEf;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIe;;EAEf,OAAO;;EAEP;;;iBAIe;;EAEf;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf;;;;;;EAMA,SAAS;;;iBAIM;;EAEf;;EAEA;;;;;EAKA;;;;;;;;;;;YAYU;;iBAGK;;EAEf;;EAEA;;EAEA,QAAQ;;EAER;;EAEA;;EAEA;;EAEA;;;iBAIe,iCAAiC;;EAEhD;;;iBAIe;;;;;;EAMf;;;iBAIe;;EAEf;;;;;EAKA;;;;;EAKA;;;iBAIe;;EAEf;;EAEA;;EAEA,SAAS;;;iBAIM,0BAA0B;;EAEzC;;EAEA;;;;;EAKA;;;;;EAKA;;EAEA;;;iBAIe;;EAEf;;;iBAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;EAMA;;;;;;;iBAQe;;EAEf;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf,SAAS;;EAET;;;iBAIe;;EAEf;;EAEA;;EAEA;;;iBAIe;;EAEf;;EAEA;;;YAIU;;iBAGK;;EAEf;;EAEA,MAAM;;EAEN;;EAEA;;;iBAIe;;EAEf;;EAEA,SAAS;;EAET;;;iBAIe;;EAEf;;;iBAIe;;EAEf;;;YAIU;;iBAGK;;EAEf;;EAEA;;EAEA,QAAQ;;;iBAIO;;EAEf;;EAEA;;EAEA,QAAQ;;;iBAIO;;EAEf,OAAO;;;iBAIQ;;EAEf,OAAO;;EAEP,QAAQ,OAAO"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EditFileInput, EditFileResult, ExecOptions, GrepResult, ListDirOptions, ListDirResult, MultiEditInput, MultiEditResult, ReadFileResult, RunShellResult, ShellJobInfo, ShellOutputOptions, ShellOutputResult, StartShellOptions } from "./tool-io.type.mjs";
|
|
1
|
+
import { ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GrepResult, ListDirOptions, ListDirResult, MultiEditInput, MultiEditResult, ReadFileResult, RunShellResult, ShellJobInfo, ShellOutputOptions, ShellOutputResult, StartShellOptions } from "./tool-io.type.mjs";
|
|
2
2
|
//#region ../../packages/ai-workspace/src/contracts/workspace-ops.contract.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The **policy-enforced** operation layer that sits between the dumb
|
|
@@ -48,6 +48,15 @@ export interface WorkspaceOps {
|
|
|
48
48
|
* not at all (`WorkspaceEditError` with `editIndex`).
|
|
49
49
|
*/
|
|
50
50
|
multiEdit(input: MultiEditInput): Promise<MultiEditResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Apply a unified diff that may modify, create, delete and rename several
|
|
53
|
+
* files. Every path goes through the jail, binary files are refused, and
|
|
54
|
+
* the patch is atomic: all hunks apply (and are written, with a rollback if
|
|
55
|
+
* a write fails) or nothing changes. `dryRun` checks without writing.
|
|
56
|
+
* Optional: a custom implementation that omits it gets a generic one built
|
|
57
|
+
* on `exists`, `readFile`, `writeFile` and `remove` (see `applyPatchWithOps`).
|
|
58
|
+
*/
|
|
59
|
+
applyPatch?(input: ApplyPatchInput): Promise<ApplyPatchResult>;
|
|
51
60
|
/**
|
|
52
61
|
* Run a command line after checking every command in it against the
|
|
53
62
|
* shell policy (see `WorkspaceShellPolicy`), with the policy environment,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-ops.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-ops.contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"workspace-ops.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-ops.contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;iBAsCiB;;;;;;;EAOf,SAAS,cAAc;IAAS;IAAiB;MAAmB,QAAQ;;EAG5E,UAAU,cAAc,kBAAkB;IAAU;IAAc;;;;;;;EAOlE,SAAS,OAAO,gBAAgB,QAAQ;;;;;;;EAQxC,UAAU,OAAO,iBAAiB,QAAQ;;;;;;;;;EAU1C,YAAY,OAAO,kBAAkB,QAAQ;;;;;;;EAQ7C,KAAK,iBAAiB,OAAO,cAAc,QAAQ;;;;;EAMnD,WAAW,iBAAiB,OAAO,oBAAoB,QAAQ;;EAG/D,YAAY,YAAY,OAAO,qBAAqB,QAAQ;;EAG5D,UAAU,aAAa,QAAQ;;EAG/B,cAAc;;;;;EAMd,QAAQ,eAAe,OAAO,iBAAiB,QAAQ;;EAGvD,KAAK,iBAAiB;IAAS;IAAe;MAAyB,QAAQ;;EAG/E,KAAK,kBAAkB;;EAGvB,OAAO,eAAe;;EAGtB,MAAM,eAAe;;EAGrB,OAAO,eAAe;;;;;;EAOtB,WAAW"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EditFileInput, EditFileResult, ExecOptions, GrepResult, ListDirOptions, ListDirResult, MultiEditInput, MultiEditResult, ReadFileResult, RunShellResult, ShellJobInfo, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, WorkspaceTodoInput, WorkspaceToolName } from "./tool-io.type.mjs";
|
|
1
|
+
import { ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GrepResult, ListDirOptions, ListDirResult, MultiEditInput, MultiEditResult, ReadFileResult, RunShellResult, ShellJobInfo, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, WorkspaceTodoInput, WorkspaceToolName } from "./tool-io.type.mjs";
|
|
2
2
|
import { WorkspacePolicy } from "./workspace-policy.type.mjs";
|
|
3
3
|
import { ToolContract } from "@warlock.js/ai";
|
|
4
4
|
//#region ../../packages/ai-workspace/src/contracts/workspace.contract.d.ts
|
|
@@ -33,6 +33,10 @@ export interface WorkspaceTools {
|
|
|
33
33
|
multiEdit(opts?: {
|
|
34
34
|
name?: string;
|
|
35
35
|
}): ToolContract;
|
|
36
|
+
/** The `apply_patch` tool (a unified diff across files: modify, create, delete, rename; all or none). */
|
|
37
|
+
applyPatch(opts?: {
|
|
38
|
+
name?: string;
|
|
39
|
+
}): ToolContract;
|
|
36
40
|
/** The `write_file` tool (atomic full-content write). */
|
|
37
41
|
writeFile(opts?: {
|
|
38
42
|
name?: string;
|
|
@@ -121,6 +125,8 @@ export interface Workspace {
|
|
|
121
125
|
editFile(input: EditFileInput): Promise<EditFileResult>;
|
|
122
126
|
/** Apply several exact-string edits to one file — all of them or none. */
|
|
123
127
|
multiEdit(input: MultiEditInput): Promise<MultiEditResult>;
|
|
128
|
+
/** Apply a unified diff (modify, create, delete, rename) to the workspace — all of it or none. */
|
|
129
|
+
applyPatch(input: ApplyPatchInput): Promise<ApplyPatchResult>;
|
|
124
130
|
/** List a jailed directory (bounded, ignores applied, symlinks not followed). */
|
|
125
131
|
listDir(path?: string, opts?: ListDirOptions): Promise<ListDirResult>;
|
|
126
132
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace.contract.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"workspace.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace.contract.ts"],"mappings":";;;;;;;;;;;;;;iBAkCiB;;;;;;EAMf,OAAO;;EAEP,QAAQ,OAAO,sBAAsB;;EAErC,SAAS;IAAS;MAAkB;;EAEpC,SAAS;IAAS;MAAkB;;EAEpC,UAAU;IAAS;MAAkB;;EAErC,WAAW;IAAS;MAAkB;;EAEtC,UAAU;IAAS;MAAkB;;EAErC,QAAQ;IAAS;MAAkB;;EAEnC,SAAS;IAAS;MAAkB;;EAEpC,YAAY;IAAS;MAAkB;;EAEvC,UAAU;IAAS;MAAkB;;EAErC,SAAS;IAAS;IAAe;MAAqB;;EAEtD,KAAK;IAAS;MAAkB;;EAEhC,KAAK;IAAS;MAAkB;;EAEhC,UAAU;IAAS;MAAkB;;EAErC,SAAS;IAAS;MAAkB;;;;;;;;;;;;;;;;;;;;;;;;iBAyBrB;;WAEN,QAAQ;;WAGR,OAAO;;;;;;;EAQhB,SAAS,cAAc;IAAS;IAAiB;MAAmB,QAAQ;;EAG5E,UAAU,cAAc,kBAAkB;IAAU;IAAc;;;EAGlE,SAAS,OAAO,gBAAgB,QAAQ;;EAGxC,UAAU,OAAO,iBAAiB,QAAQ;;EAG1C,WAAW,OAAO,kBAAkB,QAAQ;;EAG5C,QAAQ,eAAe,OAAO,iBAAiB,QAAQ;;;;;;EAOvD,KAAK,iBAAiB,OAAO,cAAc,QAAQ;;;;;;EAOnD,WAAW,iBAAiB,OAAO,oBAAoB,QAAQ;;EAG/D,YAAY,YAAY,OAAO,qBAAqB,QAAQ;;EAG5D,UAAU,aAAa,QAAQ;;EAG/B,cAAc;;EAGd,aAAa;;EAGb,WAAW,OAAO,uBAAuB;;;;;;;EAQzC,WAAW;;EAGX,KAAK,iBAAiB;IAAS;IAAe;MAAyB,QAAQ;;EAG/E,KAAK,kBAAkB;;EAGvB,OAAO,eAAe;;EAGtB,MAAM,eAAe;;EAGrB,OAAO,eAAe;;;;;;EAOtB,YAAY;;;;;;EAOZ,MAAM,iBAAiB"}
|
package/esm/errors.d.mts
CHANGED
|
@@ -103,8 +103,17 @@ export declare class WorkspacePolicyError extends AIError {
|
|
|
103
103
|
* was not set, so the edit is ambiguous.
|
|
104
104
|
* - `"stale-hash"` — the file's current hash did not match the supplied
|
|
105
105
|
* `expectHash`; the file changed since it was read.
|
|
106
|
+
* - `"patch-invalid"` — `applyPatch` was given text that is not a usable
|
|
107
|
+
* unified diff (or one that is too large).
|
|
108
|
+
* - `"hunk-failed"` — a patch hunk's context is not in the file; `hunkIndex`
|
|
109
|
+
* names it.
|
|
110
|
+
* - `"binary-file"` — the patch, or a file it touches, is binary.
|
|
111
|
+
* - `"file-exists"` — the patch creates (or renames onto) a path that
|
|
112
|
+
* already exists.
|
|
113
|
+
* - `"file-missing"` — the patch modifies, renames or deletes a path that
|
|
114
|
+
* does not exist.
|
|
106
115
|
*/
|
|
107
|
-
export type WorkspaceEditFailure = "not-found" | "not-unique" | "stale-hash";
|
|
116
|
+
export type WorkspaceEditFailure = "not-found" | "not-unique" | "stale-hash" | "patch-invalid" | "hunk-failed" | "binary-file" | "file-exists" | "file-missing";
|
|
108
117
|
/**
|
|
109
118
|
* Options for {@link WorkspaceEditError} — the structured `type`
|
|
110
119
|
* discriminator plus optional match-count / hash context for the
|
|
@@ -123,6 +132,8 @@ export type WorkspaceEditErrorOptions = AIErrorOptions & {
|
|
|
123
132
|
actualHash?: string;
|
|
124
133
|
/** 0-based index of the failing edit, for a `multiEdit` rejection. */
|
|
125
134
|
editIndex?: number;
|
|
135
|
+
/** 0-based index of the hunk that failed, for an `applyPatch` rejection. */
|
|
136
|
+
hunkIndex?: number;
|
|
126
137
|
};
|
|
127
138
|
/**
|
|
128
139
|
* An `editFile` operation was rejected by the read-before-edit guard:
|
|
@@ -154,6 +165,11 @@ export declare class WorkspaceEditError extends AIError {
|
|
|
154
165
|
* Nothing was written: a multi-edit applies all of its edits or none.
|
|
155
166
|
*/
|
|
156
167
|
readonly editIndex?: number;
|
|
168
|
+
/**
|
|
169
|
+
* For an `applyPatch` rejection, the 0-based index of the hunk that failed
|
|
170
|
+
* within its file. Nothing was written.
|
|
171
|
+
*/
|
|
172
|
+
readonly hunkIndex?: number;
|
|
157
173
|
constructor(message: string, options: WorkspaceEditErrorOptions);
|
|
158
174
|
}
|
|
159
175
|
/**
|
package/esm/errors.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/errors.ts"],"mappings":";;;;;;;;;;;;;;;YAeY;;;;;;;;;;;;;;;;;;;;;;;;;;YA2BA;;;;;;YAeA,8BAA8B;;EAExC,MAAM;;EAEN;;EAEA;;EAEA;;EAEA,SAAS;;EAET;;;;;;;;;;;;;;;;;;;qBAoBW,6BAA6B;;WAExB,MAAM;;WAEN;;WAEA;;WAEA;;;;;;WAMA,SAAS;;WAET;EAEhB,YAAmB,iBAAiB,SAAS
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/errors.ts"],"mappings":";;;;;;;;;;;;;;;YAeY;;;;;;;;;;;;;;;;;;;;;;;;;;YA2BA;;;;;;YAeA,8BAA8B;;EAExC,MAAM;;EAEN;;EAEA;;EAEA;;EAEA,SAAS;;EAET;;;;;;;;;;;;;;;;;;;qBAoBW,6BAA6B;;WAExB,MAAM;;WAEN;;WAEA;;WAEA;;;;;;WAMA,SAAS;;WAET;EAEhB,YAAmB,iBAAiB,SAAS;;;;;;;;;;;;;;;;;;;;YA+BnC;;;;;;YAeA,4BAA4B;;EAEtC,MAAM;;EAEN;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;;;qBAiBW,2BAA2B;;WAEtB,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;;;;WAKA;;;;;WAKA;EAEhB,YAAmB,iBAAiB,SAAS;;;;;;;;;YAqBnC;;YAGA,2BAA2B;;EAErC,MAAM;;EAEN;;;;;;;;;;;;qBAaW,0BAA0B;;WAErB,MAAM;;WAEN;EAEhB,YAAmB,iBAAiB,SAAS"}
|
package/esm/errors.mjs
CHANGED
package/esm/errors.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/errors.ts"],"sourcesContent":["import { AIError, type AIErrorOptions } from \"@warlock.js/ai\";\n\n/**\n * Why a workspace policy check rejected an operation.\n *\n * - `\"path-escape\"` — a resolved path fell outside the `cwd` jail (or an\n * `allowPaths` root), or matched a `denyPaths` glob.\n * - `\"denied-command\"` — the shell policy refused a command line (`reason`\n * says why: an executable outside `shell.allow` or inside `shell.deny`, a\n * metacharacter in argv mode, a redirection outside the jail in shell\n * mode, …), or a mutating method was called on a read-only projection.\n * - `\"unsafe-pattern\"` — a `grep` pattern was too long or matched a\n * catastrophic-backtracking shape (nested quantifiers) that could hang\n * the process (ReDoS).\n */\nexport type WorkspacePolicyViolation = \"path-escape\" | \"denied-command\" | \"unsafe-pattern\";\n\n/**\n * Why the shell policy refused a command. Set on every\n * {@link WorkspacePolicyError} of type `\"denied-command\"` raised for a\n * command line, so a host can phrase the refusal itself instead of parsing\n * the message.\n *\n * - `\"metacharacter\"` — argv mode (the default `interpreter: \"none\"`) met an\n * unquoted shell operator (`&&`, `;`, `|`, `>`, `$`, backticks, …). The\n * message suggests how to split the line into separate commands.\n * - `\"unbalanced-quotes\"` — a quote is never closed.\n * - `\"denied-executable\"` — a command's executable is listed in `shell.deny`.\n * - `\"not-allowed\"` — a command's executable is not in `shell.allow`, the\n * `shell.allowCommand` hook refused it, the workspace has no shell policy,\n * the line is empty, or background commands are not enabled.\n * - `\"denied-pattern\"` — a command matched one of `shell.denyPatterns`.\n * - `\"nested-command\"` — a command runs another command the policy cannot\n * inspect (`bash -c …`, `eval`, `xargs`, `sudo`, a wrapper with options the\n * policy cannot read), while the policy has deny rules it would bypass.\n * - `\"unsupported-syntax\"` — shell mode (`interpreter: \"sh\"`) met syntax the\n * policy does not parse with confidence (compound commands, arithmetic,\n * brace expansion, background `&`, a command name decided at run time, …),\n * so the line is refused rather than guessed at.\n * - `\"path-escape\"` — shell mode: a redirection target or `cd` directory\n * resolves outside the workspace jail or hits `denyPaths`.\n */\nexport type WorkspaceShellRefusalReason =\n | \"metacharacter\"\n | \"unbalanced-quotes\"\n | \"denied-executable\"\n | \"not-allowed\"\n | \"denied-pattern\"\n | \"nested-command\"\n | \"unsupported-syntax\"\n | \"path-escape\";\n\n/**\n * Options for {@link WorkspacePolicyError} — the structured `type`\n * discriminator plus, where relevant, the offending path or command for\n * branchable diagnostics without parsing the message.\n */\nexport type WorkspacePolicyErrorOptions = AIErrorOptions & {\n /** Which policy rule was violated. */\n type: WorkspacePolicyViolation;\n /** The offending workspace-relative path (for `\"path-escape\"`). */\n path?: string;\n /** The offending command line (for `\"denied-command\"`). */\n command?: string;\n /** The offending regex pattern (for `\"unsafe-pattern\"`). */\n pattern?: string;\n /** Why the shell policy refused the command (for `\"denied-command\"`). */\n reason?: WorkspaceShellRefusalReason;\n /** The executable the refusal is about, when there is one. */\n executable?: string;\n};\n\n/**\n * The workspace policy engine refused an operation — a path escaped the\n * jail (or hit a deny glob), or a shell command's executable was not\n * allowed.\n *\n * **Surface.** This is returned to the agent as tool-error *data*, never\n * a thrown run-killer — the agent reads the failure and self-corrects.\n * Extends the framework `AIError` (category `\"tool\"`, code\n * `TOOL_EXEC_FAILED`) so it flows through the same typed error contract\n * as every other AI error; branch on `error.type` for the specific\n * violation.\n *\n * @example\n * if (error instanceof WorkspacePolicyError && error.type === \"denied-command\") {\n * console.warn(`Blocked command: ${error.command}`);\n * }\n */\nexport class WorkspacePolicyError extends AIError {\n /** Which policy rule was violated. */\n public readonly type: WorkspacePolicyViolation;\n /** The offending path, when the violation was a path escape. */\n public readonly path?: string;\n /** The offending command, when the violation was a denied command. */\n public readonly command?: string;\n /** The offending regex pattern, when the violation was `\"unsafe-pattern\"`. */\n public readonly pattern?: string;\n /**\n * Why the shell policy refused a command line — set on every\n * `\"denied-command\"` refusal of a command (see\n * {@link WorkspaceShellRefusalReason}).\n */\n public readonly reason?: WorkspaceShellRefusalReason;\n /** The executable the shell refusal is about, when there is one. */\n public readonly executable?: string;\n\n public constructor(message: string, options: WorkspacePolicyErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspacePolicyError\";\n this.type = options.type;\n this.path = options.path;\n this.command = options.command;\n this.pattern = options.pattern;\n this.reason = options.reason;\n this.executable = options.executable;\n }\n}\n\n/**\n * Why an edit was rejected.\n *\n * - `\"not-found\"` — the `oldString` did not appear in the file.\n * - `\"not-unique\"` — `oldString` matched more than once and `replaceAll`\n * was not set, so the edit is ambiguous.\n * - `\"stale-hash\"` — the file's current hash did not match the supplied\n * `expectHash`; the file changed since it was read.\n */\nexport type WorkspaceEditFailure = \"not-found\" | \"not-unique\" | \"stale-hash\";\n\n/**\n * Options for {@link WorkspaceEditError} — the structured `type`\n * discriminator plus optional match-count / hash context for the\n * `\"not-unique\"` and `\"stale-hash\"` cases.\n */\nexport type WorkspaceEditErrorOptions = AIErrorOptions & {\n /** Why the edit was rejected. */\n type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n path: string;\n /** How many times `oldString` matched (for `\"not-unique\"`). */\n matches?: number;\n /** The hash the caller expected (for `\"stale-hash\"`). */\n expectedHash?: string;\n /** The file's actual current hash (for `\"stale-hash\"`). */\n actualHash?: string;\n /** 0-based index of the failing edit, for a `multiEdit` rejection. */\n editIndex?: number;\n};\n\n/**\n * An `editFile` operation was rejected by the read-before-edit guard:\n * the `oldString` was absent, matched non-uniquely without `replaceAll`,\n * or the file's hash no longer matched the supplied `expectHash`.\n *\n * **Surface.** Like {@link WorkspacePolicyError}, returned to the agent\n * as tool-error *data* so it can re-read and retry. Extends `AIError`\n * (category `\"tool\"`, code `TOOL_EXEC_FAILED`); branch on `error.type`.\n *\n * @example\n * if (error instanceof WorkspaceEditError && error.type === \"stale-hash\") {\n * // re-read the file and retry the edit with the fresh hash\n * }\n */\nexport class WorkspaceEditError extends AIError {\n /** Why the edit was rejected. */\n public readonly type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n public readonly path: string;\n /** How many times `oldString` matched, for the `\"not-unique\"` case. */\n public readonly matches?: number;\n /** The hash the caller expected, for the `\"stale-hash\"` case. */\n public readonly expectedHash?: string;\n /** The file's actual current hash, for the `\"stale-hash\"` case. */\n public readonly actualHash?: string;\n /**\n * For a `multiEdit` rejection, the 0-based index of the edit that failed.\n * Nothing was written: a multi-edit applies all of its edits or none.\n */\n public readonly editIndex?: number;\n\n public constructor(message: string, options: WorkspaceEditErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspaceEditError\";\n this.type = options.type;\n this.path = options.path;\n this.matches = options.matches;\n this.expectedHash = options.expectedHash;\n this.actualHash = options.actualHash;\n this.editIndex = options.editIndex;\n }\n}\n\n/**\n * Why a background-command operation failed.\n *\n * - `\"not-found\"` — no background command with that id exists in this\n * workspace (it never existed, or it finished long enough ago to be\n * pruned).\n */\nexport type WorkspaceJobFailure = \"not-found\";\n\n/** Options for {@link WorkspaceJobError}. */\nexport type WorkspaceJobErrorOptions = AIErrorOptions & {\n /** Why the operation failed. */\n type: WorkspaceJobFailure;\n /** The background command id the caller asked for. */\n jobId: string;\n};\n\n/**\n * A `shell_output` / `shell_kill` call (or `shellOutput()` / `shellKill()`)\n * named a background command this workspace does not know. Returned to the\n * agent as tool-error data like the other workspace errors.\n *\n * @example\n * if (error instanceof WorkspaceJobError && error.type === \"not-found\") {\n * console.warn(`No background command ${error.jobId}`);\n * }\n */\nexport class WorkspaceJobError extends AIError {\n /** Why the operation failed. */\n public readonly type: WorkspaceJobFailure;\n /** The background command id the caller asked for. */\n public readonly jobId: string;\n\n public constructor(message: string, options: WorkspaceJobErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspaceJobError\";\n this.type = options.type;\n this.jobId = options.jobId;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyFA,IAAa,uBAAb,cAA0C,QAAQ;CAkBhD,AAAO,YAAY,SAAiB,SAAsC;EACxE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,OAAO,QAAQ;EACpB,KAAK,UAAU,QAAQ;EACvB,KAAK,UAAU,QAAQ;EACvB,KAAK,SAAS,QAAQ;EACtB,KAAK,aAAa,QAAQ;CAC5B;AACF;;;;;;;;;;;;;;;AA+CA,IAAa,qBAAb,cAAwC,QAAQ;CAiB9C,AAAO,YAAY,SAAiB,SAAoC;EACtE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,OAAO,QAAQ;EACpB,KAAK,UAAU,QAAQ;EACvB,KAAK,eAAe,QAAQ;EAC5B,KAAK,aAAa,QAAQ;EAC1B,KAAK,YAAY,QAAQ;CAC3B;AACF;;;;;;;;;;;AA6BA,IAAa,oBAAb,cAAuC,QAAQ;CAM7C,AAAO,YAAY,SAAiB,SAAmC;EACrE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,QAAQ,QAAQ;CACvB;AACF"}
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/errors.ts"],"sourcesContent":["import { AIError, type AIErrorOptions } from \"@warlock.js/ai\";\n\n/**\n * Why a workspace policy check rejected an operation.\n *\n * - `\"path-escape\"` — a resolved path fell outside the `cwd` jail (or an\n * `allowPaths` root), or matched a `denyPaths` glob.\n * - `\"denied-command\"` — the shell policy refused a command line (`reason`\n * says why: an executable outside `shell.allow` or inside `shell.deny`, a\n * metacharacter in argv mode, a redirection outside the jail in shell\n * mode, …), or a mutating method was called on a read-only projection.\n * - `\"unsafe-pattern\"` — a `grep` pattern was too long or matched a\n * catastrophic-backtracking shape (nested quantifiers) that could hang\n * the process (ReDoS).\n */\nexport type WorkspacePolicyViolation = \"path-escape\" | \"denied-command\" | \"unsafe-pattern\";\n\n/**\n * Why the shell policy refused a command. Set on every\n * {@link WorkspacePolicyError} of type `\"denied-command\"` raised for a\n * command line, so a host can phrase the refusal itself instead of parsing\n * the message.\n *\n * - `\"metacharacter\"` — argv mode (the default `interpreter: \"none\"`) met an\n * unquoted shell operator (`&&`, `;`, `|`, `>`, `$`, backticks, …). The\n * message suggests how to split the line into separate commands.\n * - `\"unbalanced-quotes\"` — a quote is never closed.\n * - `\"denied-executable\"` — a command's executable is listed in `shell.deny`.\n * - `\"not-allowed\"` — a command's executable is not in `shell.allow`, the\n * `shell.allowCommand` hook refused it, the workspace has no shell policy,\n * the line is empty, or background commands are not enabled.\n * - `\"denied-pattern\"` — a command matched one of `shell.denyPatterns`.\n * - `\"nested-command\"` — a command runs another command the policy cannot\n * inspect (`bash -c …`, `eval`, `xargs`, `sudo`, a wrapper with options the\n * policy cannot read), while the policy has deny rules it would bypass.\n * - `\"unsupported-syntax\"` — shell mode (`interpreter: \"sh\"`) met syntax the\n * policy does not parse with confidence (compound commands, arithmetic,\n * brace expansion, background `&`, a command name decided at run time, …),\n * so the line is refused rather than guessed at.\n * - `\"path-escape\"` — shell mode: a redirection target or `cd` directory\n * resolves outside the workspace jail or hits `denyPaths`.\n */\nexport type WorkspaceShellRefusalReason =\n | \"metacharacter\"\n | \"unbalanced-quotes\"\n | \"denied-executable\"\n | \"not-allowed\"\n | \"denied-pattern\"\n | \"nested-command\"\n | \"unsupported-syntax\"\n | \"path-escape\";\n\n/**\n * Options for {@link WorkspacePolicyError} — the structured `type`\n * discriminator plus, where relevant, the offending path or command for\n * branchable diagnostics without parsing the message.\n */\nexport type WorkspacePolicyErrorOptions = AIErrorOptions & {\n /** Which policy rule was violated. */\n type: WorkspacePolicyViolation;\n /** The offending workspace-relative path (for `\"path-escape\"`). */\n path?: string;\n /** The offending command line (for `\"denied-command\"`). */\n command?: string;\n /** The offending regex pattern (for `\"unsafe-pattern\"`). */\n pattern?: string;\n /** Why the shell policy refused the command (for `\"denied-command\"`). */\n reason?: WorkspaceShellRefusalReason;\n /** The executable the refusal is about, when there is one. */\n executable?: string;\n};\n\n/**\n * The workspace policy engine refused an operation — a path escaped the\n * jail (or hit a deny glob), or a shell command's executable was not\n * allowed.\n *\n * **Surface.** This is returned to the agent as tool-error *data*, never\n * a thrown run-killer — the agent reads the failure and self-corrects.\n * Extends the framework `AIError` (category `\"tool\"`, code\n * `TOOL_EXEC_FAILED`) so it flows through the same typed error contract\n * as every other AI error; branch on `error.type` for the specific\n * violation.\n *\n * @example\n * if (error instanceof WorkspacePolicyError && error.type === \"denied-command\") {\n * console.warn(`Blocked command: ${error.command}`);\n * }\n */\nexport class WorkspacePolicyError extends AIError {\n /** Which policy rule was violated. */\n public readonly type: WorkspacePolicyViolation;\n /** The offending path, when the violation was a path escape. */\n public readonly path?: string;\n /** The offending command, when the violation was a denied command. */\n public readonly command?: string;\n /** The offending regex pattern, when the violation was `\"unsafe-pattern\"`. */\n public readonly pattern?: string;\n /**\n * Why the shell policy refused a command line — set on every\n * `\"denied-command\"` refusal of a command (see\n * {@link WorkspaceShellRefusalReason}).\n */\n public readonly reason?: WorkspaceShellRefusalReason;\n /** The executable the shell refusal is about, when there is one. */\n public readonly executable?: string;\n\n public constructor(message: string, options: WorkspacePolicyErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspacePolicyError\";\n this.type = options.type;\n this.path = options.path;\n this.command = options.command;\n this.pattern = options.pattern;\n this.reason = options.reason;\n this.executable = options.executable;\n }\n}\n\n/**\n * Why an edit was rejected.\n *\n * - `\"not-found\"` — the `oldString` did not appear in the file.\n * - `\"not-unique\"` — `oldString` matched more than once and `replaceAll`\n * was not set, so the edit is ambiguous.\n * - `\"stale-hash\"` — the file's current hash did not match the supplied\n * `expectHash`; the file changed since it was read.\n * - `\"patch-invalid\"` — `applyPatch` was given text that is not a usable\n * unified diff (or one that is too large).\n * - `\"hunk-failed\"` — a patch hunk's context is not in the file; `hunkIndex`\n * names it.\n * - `\"binary-file\"` — the patch, or a file it touches, is binary.\n * - `\"file-exists\"` — the patch creates (or renames onto) a path that\n * already exists.\n * - `\"file-missing\"` — the patch modifies, renames or deletes a path that\n * does not exist.\n */\nexport type WorkspaceEditFailure =\n | \"not-found\"\n | \"not-unique\"\n | \"stale-hash\"\n | \"patch-invalid\"\n | \"hunk-failed\"\n | \"binary-file\"\n | \"file-exists\"\n | \"file-missing\";\n\n/**\n * Options for {@link WorkspaceEditError} — the structured `type`\n * discriminator plus optional match-count / hash context for the\n * `\"not-unique\"` and `\"stale-hash\"` cases.\n */\nexport type WorkspaceEditErrorOptions = AIErrorOptions & {\n /** Why the edit was rejected. */\n type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n path: string;\n /** How many times `oldString` matched (for `\"not-unique\"`). */\n matches?: number;\n /** The hash the caller expected (for `\"stale-hash\"`). */\n expectedHash?: string;\n /** The file's actual current hash (for `\"stale-hash\"`). */\n actualHash?: string;\n /** 0-based index of the failing edit, for a `multiEdit` rejection. */\n editIndex?: number;\n /** 0-based index of the hunk that failed, for an `applyPatch` rejection. */\n hunkIndex?: number;\n};\n\n/**\n * An `editFile` operation was rejected by the read-before-edit guard:\n * the `oldString` was absent, matched non-uniquely without `replaceAll`,\n * or the file's hash no longer matched the supplied `expectHash`.\n *\n * **Surface.** Like {@link WorkspacePolicyError}, returned to the agent\n * as tool-error *data* so it can re-read and retry. Extends `AIError`\n * (category `\"tool\"`, code `TOOL_EXEC_FAILED`); branch on `error.type`.\n *\n * @example\n * if (error instanceof WorkspaceEditError && error.type === \"stale-hash\") {\n * // re-read the file and retry the edit with the fresh hash\n * }\n */\nexport class WorkspaceEditError extends AIError {\n /** Why the edit was rejected. */\n public readonly type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n public readonly path: string;\n /** How many times `oldString` matched, for the `\"not-unique\"` case. */\n public readonly matches?: number;\n /** The hash the caller expected, for the `\"stale-hash\"` case. */\n public readonly expectedHash?: string;\n /** The file's actual current hash, for the `\"stale-hash\"` case. */\n public readonly actualHash?: string;\n /**\n * For a `multiEdit` rejection, the 0-based index of the edit that failed.\n * Nothing was written: a multi-edit applies all of its edits or none.\n */\n public readonly editIndex?: number;\n /**\n * For an `applyPatch` rejection, the 0-based index of the hunk that failed\n * within its file. Nothing was written.\n */\n public readonly hunkIndex?: number;\n\n public constructor(message: string, options: WorkspaceEditErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspaceEditError\";\n this.type = options.type;\n this.path = options.path;\n this.matches = options.matches;\n this.expectedHash = options.expectedHash;\n this.actualHash = options.actualHash;\n this.editIndex = options.editIndex;\n this.hunkIndex = options.hunkIndex;\n }\n}\n\n/**\n * Why a background-command operation failed.\n *\n * - `\"not-found\"` — no background command with that id exists in this\n * workspace (it never existed, or it finished long enough ago to be\n * pruned).\n */\nexport type WorkspaceJobFailure = \"not-found\";\n\n/** Options for {@link WorkspaceJobError}. */\nexport type WorkspaceJobErrorOptions = AIErrorOptions & {\n /** Why the operation failed. */\n type: WorkspaceJobFailure;\n /** The background command id the caller asked for. */\n jobId: string;\n};\n\n/**\n * A `shell_output` / `shell_kill` call (or `shellOutput()` / `shellKill()`)\n * named a background command this workspace does not know. Returned to the\n * agent as tool-error data like the other workspace errors.\n *\n * @example\n * if (error instanceof WorkspaceJobError && error.type === \"not-found\") {\n * console.warn(`No background command ${error.jobId}`);\n * }\n */\nexport class WorkspaceJobError extends AIError {\n /** Why the operation failed. */\n public readonly type: WorkspaceJobFailure;\n /** The background command id the caller asked for. */\n public readonly jobId: string;\n\n public constructor(message: string, options: WorkspaceJobErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspaceJobError\";\n this.type = options.type;\n this.jobId = options.jobId;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyFA,IAAa,uBAAb,cAA0C,QAAQ;CAkBhD,AAAO,YAAY,SAAiB,SAAsC;EACxE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,OAAO,QAAQ;EACpB,KAAK,UAAU,QAAQ;EACvB,KAAK,UAAU,QAAQ;EACvB,KAAK,SAAS,QAAQ;EACtB,KAAK,aAAa,QAAQ;CAC5B;AACF;;;;;;;;;;;;;;;AAkEA,IAAa,qBAAb,cAAwC,QAAQ;CAsB9C,AAAO,YAAY,SAAiB,SAAoC;EACtE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,OAAO,QAAQ;EACpB,KAAK,UAAU,QAAQ;EACvB,KAAK,eAAe,QAAQ;EAC5B,KAAK,aAAa,QAAQ;EAC1B,KAAK,YAAY,QAAQ;EACzB,KAAK,YAAY,QAAQ;CAC3B;AACF;;;;;;;;;;;AA6BA,IAAa,oBAAb,cAAuC,QAAQ;CAM7C,AAAO,YAAY,SAAiB,SAAmC;EACrE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,QAAQ,QAAQ;CACvB;AACF"}
|
package/esm/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./contracts/tool-io.type.mjs";
|
|
1
|
+
import { ApplyPatchFileResult, ApplyPatchInput, ApplyPatchResult, EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ListDirEntry, ListDirEntryType, ListDirInput, ListDirOptions, ListDirResult, MultiEditInput, MultiEditOperation, MultiEditResult, ReadFileInput, ReadFileResult, RunShellBackgroundResult, RunShellInput, RunShellResult, RunTestsInput, ShellJobInfo, ShellJobStatus, ShellKillInput, ShellOutputInput, ShellOutputOptions, ShellOutputResult, StartShellOptions, TodoListResult, TodoWriteInput, WorkspaceTodo, WorkspaceTodoInput, WorkspaceTodoStatus, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./contracts/tool-io.type.mjs";
|
|
2
2
|
import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy } from "./contracts/workspace-policy.type.mjs";
|
|
3
3
|
import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./contracts/workspace-backend.contract.mjs";
|
|
4
4
|
import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
|
|
@@ -8,9 +8,13 @@ import { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, Wo
|
|
|
8
8
|
import { ResolvedPath, buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
|
|
9
9
|
import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand } from "./policy/shell-check.mjs";
|
|
10
10
|
import { ArgvAnalysis, analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
|
|
11
|
+
import { applyPatchWithOps } from "./patch/apply-patch-generic.mjs";
|
|
12
|
+
import { FilePatch, FilePatchKind, PatchHunk, PatchLine, parsePatch } from "./patch/parse-patch.mjs";
|
|
13
|
+
import { applyHunks } from "./patch/apply-hunks.mjs";
|
|
11
14
|
import { createOps } from "./ops.mjs";
|
|
12
15
|
import { createLocalBackend } from "./backends/local.mjs";
|
|
13
16
|
import { MockBackendSeed, MockExecResult, createMockBackend } from "./backends/mock.mjs";
|
|
17
|
+
import { makeApplyPatchTool } from "./tools/apply-patch.mjs";
|
|
14
18
|
import { makeEditFileTool } from "./tools/edit-file.mjs";
|
|
15
19
|
import { MakeGlobToolOptions, makeGlobTool } from "./tools/glob.mjs";
|
|
16
20
|
import { MakeGrepToolOptions, makeGrepTool } from "./tools/grep.mjs";
|
|
@@ -25,5 +29,5 @@ import { TodoStore } from "./todos.mjs";
|
|
|
25
29
|
import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
|
|
26
30
|
import { makeWriteFileTool } from "./tools/write-file.mjs";
|
|
27
31
|
import { workspace } from "./workspace.mjs";
|
|
28
|
-
export { type ArgvAnalysis, type EditFileInput, type EditFileResult, type ExecOptions, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type ListDirEntry, type ListDirEntryType, type ListDirInput, type ListDirOptions, type ListDirResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type MultiEditInput, type MultiEditOperation, type MultiEditResult, type ReadFileInput, type ReadFileResult, type ResolvedPath, type RunShellBackgroundResult, type RunShellInput, type RunShellResult, type RunTestsInput, type ShellJobInfo, type ShellJobStatus, type ShellKillInput, type ShellOutputInput, type ShellOutputOptions, type ShellOutputResult, type StartShellOptions, type TodoListResult, TodoStore, type TodoWriteInput, type Workspace, type WorkspaceAllowCommand, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendProcess, type WorkspaceBackendStartOptions, type WorkspaceBackendStat, type WorkspaceBackendType, type WorkspaceBackgroundPolicy, type WorkspaceCommandCheckContext, type WorkspaceCommandPattern, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, WorkspaceJobError, type WorkspaceJobErrorOptions, type WorkspaceJobFailure, type WorkspaceListPolicy, type WorkspaceOps, type WorkspaceOutputStream, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellCheck, type WorkspaceShellInterpreter, type WorkspaceShellPath, type WorkspaceShellPathAccess, type WorkspaceShellPolicy, type WorkspaceShellRefusalReason, type WorkspaceTodo, type WorkspaceTodoInput, type WorkspaceTodoStatus, type WorkspaceToolName, type WorkspaceTools, type WriteFileInput, type WriteFileResult, analyzeArgvCommand, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, resolveInJail, tokenizeCommand, workspace };
|
|
32
|
+
export { type ApplyPatchFileResult, type ApplyPatchInput, type ApplyPatchResult, type ArgvAnalysis, type EditFileInput, type EditFileResult, type ExecOptions, type FilePatch, type FilePatchKind, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type ListDirEntry, type ListDirEntryType, type ListDirInput, type ListDirOptions, type ListDirResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type MultiEditInput, type MultiEditOperation, type MultiEditResult, type PatchHunk, type PatchLine, type ReadFileInput, type ReadFileResult, type ResolvedPath, type RunShellBackgroundResult, type RunShellInput, type RunShellResult, type RunTestsInput, type ShellJobInfo, type ShellJobStatus, type ShellKillInput, type ShellOutputInput, type ShellOutputOptions, type ShellOutputResult, type StartShellOptions, type TodoListResult, TodoStore, type TodoWriteInput, type Workspace, type WorkspaceAllowCommand, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendProcess, type WorkspaceBackendStartOptions, type WorkspaceBackendStat, type WorkspaceBackendType, type WorkspaceBackgroundPolicy, type WorkspaceCommandCheckContext, type WorkspaceCommandPattern, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, WorkspaceJobError, type WorkspaceJobErrorOptions, type WorkspaceJobFailure, type WorkspaceListPolicy, type WorkspaceOps, type WorkspaceOutputStream, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellCheck, type WorkspaceShellInterpreter, type WorkspaceShellPath, type WorkspaceShellPathAccess, type WorkspaceShellPolicy, type WorkspaceShellRefusalReason, type WorkspaceTodo, type WorkspaceTodoInput, type WorkspaceTodoStatus, type WorkspaceToolName, type WorkspaceTools, type WriteFileInput, type WriteFileResult, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, tokenizeCommand, workspace };
|
|
29
33
|
import "./workspace.mjs";
|
package/esm/index.mjs
CHANGED
|
@@ -2,9 +2,13 @@ import { WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError } from "./e
|
|
|
2
2
|
import { analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
|
|
3
3
|
import { checkShellCommand } from "./policy/shell-check.mjs";
|
|
4
4
|
import { buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
|
|
5
|
+
import { applyHunks } from "./patch/apply-hunks.mjs";
|
|
6
|
+
import { parsePatch } from "./patch/parse-patch.mjs";
|
|
7
|
+
import { applyPatchWithOps } from "./patch/apply-patch-generic.mjs";
|
|
5
8
|
import { createOps } from "./ops.mjs";
|
|
6
9
|
import { createLocalBackend } from "./backends/local.mjs";
|
|
7
10
|
import { createMockBackend } from "./backends/mock.mjs";
|
|
11
|
+
import { makeApplyPatchTool } from "./tools/apply-patch.mjs";
|
|
8
12
|
import { makeEditFileTool } from "./tools/edit-file.mjs";
|
|
9
13
|
import { makeGlobTool } from "./tools/glob.mjs";
|
|
10
14
|
import { makeGrepTool } from "./tools/grep.mjs";
|
|
@@ -20,4 +24,4 @@ import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
|
|
|
20
24
|
import { makeWriteFileTool } from "./tools/write-file.mjs";
|
|
21
25
|
import { workspace } from "./workspace.mjs";
|
|
22
26
|
|
|
23
|
-
export { TodoStore, WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError, analyzeArgvCommand, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, resolveInJail, tokenizeCommand, workspace };
|
|
27
|
+
export { TodoStore, WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError, analyzeArgvCommand, applyHunks, applyPatchWithOps, buildEnv, checkShellCommand, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeApplyPatchTool, makeEditFileTool, makeGlobTool, makeGrepTool, makeListDirTool, makeMultiEditTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeShellKillTool, makeShellOutputTool, makeTodoReadTool, makeTodoWriteTool, makeWriteFileTool, parsePatch, resolveInJail, tokenizeCommand, workspace };
|
package/esm/ops.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ops.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/ops.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAiMiB;;EAEf,MAAM;;;;;;;;;;;;;;;;;;;;;wBAouBQ,UACd,SAAS,kBACT,QAAQ,iBACR,UAAU,aACT"}
|
package/esm/ops.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
|
|
|
2
2
|
import { globToRegExp, isInside, matchesAnyGlob } from "./policy/jail.mjs";
|
|
3
3
|
import { checkShellCommand } from "./policy/shell-check.mjs";
|
|
4
4
|
import { buildCommandEnv, resolveInJail } from "./policy/policy.mjs";
|
|
5
|
+
import { executePatch } from "./patch/execute-patch.mjs";
|
|
5
6
|
import { ShellJobs } from "./shell-jobs.mjs";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { fs } from "@warlock.js/fs";
|
|
@@ -267,6 +268,21 @@ var Ops = class {
|
|
|
267
268
|
};
|
|
268
269
|
});
|
|
269
270
|
}
|
|
271
|
+
async applyPatch(input) {
|
|
272
|
+
return executePatch(input, {
|
|
273
|
+
resolve: (inputPath) => resolveInJail(this.policy, inputPath),
|
|
274
|
+
exists: (absolutePath) => this.backend.exists(absolutePath),
|
|
275
|
+
stat: this.backend.stat ? (absolutePath) => this.backend.stat(absolutePath) : void 0,
|
|
276
|
+
read: (absolutePath) => this.backend.readFile(absolutePath),
|
|
277
|
+
write: async (absolutePath, content) => {
|
|
278
|
+
await this.backend.mkdir(path.dirname(absolutePath));
|
|
279
|
+
await this.backend.writeFile(absolutePath, content);
|
|
280
|
+
},
|
|
281
|
+
remove: (absolutePath) => this.backend.remove(absolutePath),
|
|
282
|
+
withLock: (key, task) => this.withPathLock(key, task),
|
|
283
|
+
display: (absolutePath) => path.relative(this.policy.cwd, absolutePath)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
270
286
|
/**
|
|
271
287
|
* Check a command line against the shell policy: every command in it
|
|
272
288
|
* (argv or shell mode), then — shell mode — every redirection target and
|
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 type {\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 /**\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 { 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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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PatchHunk } from "./parse-patch.mjs";
|
|
2
|
+
//#region ../../packages/ai-workspace/src/patch/apply-hunks.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Apply a file's hunks to its content, in memory. Hunks are located by their
|
|
5
|
+
* context (not only by line number, so a few lines of drift are fine), must
|
|
6
|
+
* not overlap, and keep the file's line endings: untouched lines are
|
|
7
|
+
* byte-for-byte as they were and added lines use the file's CRLF/LF style.
|
|
8
|
+
*
|
|
9
|
+
* Throws a `WorkspaceEditError` (`hunk-failed`) naming the first hunk whose
|
|
10
|
+
* context is not in the file; nothing is returned in that case.
|
|
11
|
+
*/
|
|
12
|
+
export declare function applyHunks(content: string, hunks: PatchHunk[], displayPath: string): string;
|
|
13
|
+
//#endregion
|
|
14
|
+
//# sourceMappingURL=apply-hunks.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-hunks.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/patch/apply-hunks.ts"],"mappings":";;;;;;;;;;;wBAyEgB,WAAW,iBAAiB,OAAO,aAAa"}
|