@warlock.js/ai-workspace 5.30.0 → 5.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +52 -2
  2. package/README.md +34 -21
  3. package/cjs/index.cjs +3134 -315
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/backends/local.d.mts +1 -3
  6. package/esm/backends/local.d.mts.map +1 -1
  7. package/esm/backends/local.mjs +166 -28
  8. package/esm/backends/local.mjs.map +1 -1
  9. package/esm/backends/mock.d.mts +14 -9
  10. package/esm/backends/mock.d.mts.map +1 -1
  11. package/esm/backends/mock.mjs +34 -0
  12. package/esm/backends/mock.mjs.map +1 -1
  13. package/esm/contracts/index.d.mts +5 -4
  14. package/esm/contracts/tool-io.type.d.mts +216 -19
  15. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  16. package/esm/contracts/workspace-backend.contract.d.mts +62 -4
  17. package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
  18. package/esm/contracts/workspace-ops.contract.d.mts +33 -6
  19. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  20. package/esm/contracts/workspace-policy.type.d.mts +136 -16
  21. package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
  22. package/esm/contracts/workspace.contract.d.mts +65 -13
  23. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  24. package/esm/errors.d.mts +103 -17
  25. package/esm/errors.d.mts.map +1 -1
  26. package/esm/errors.mjs +22 -1
  27. package/esm/errors.mjs.map +1 -1
  28. package/esm/index.d.mts +14 -5
  29. package/esm/index.mjs +10 -2
  30. package/esm/ops.d.mts +21 -3
  31. package/esm/ops.d.mts.map +1 -1
  32. package/esm/ops.mjs +230 -64
  33. package/esm/ops.mjs.map +1 -1
  34. package/esm/policy/jail.mjs +73 -0
  35. package/esm/policy/jail.mjs.map +1 -0
  36. package/esm/policy/policy.d.mts +30 -20
  37. package/esm/policy/policy.d.mts.map +1 -1
  38. package/esm/policy/policy.mjs +64 -87
  39. package/esm/policy/policy.mjs.map +1 -1
  40. package/esm/policy/shell-check.d.mts +63 -0
  41. package/esm/policy/shell-check.d.mts.map +1 -0
  42. package/esm/policy/shell-check.mjs +638 -0
  43. package/esm/policy/shell-check.mjs.map +1 -0
  44. package/esm/policy/shell-parser.mjs +802 -0
  45. package/esm/policy/shell-parser.mjs.map +1 -0
  46. package/esm/policy/tokenize-command.d.mts +62 -0
  47. package/esm/policy/tokenize-command.d.mts.map +1 -0
  48. package/esm/policy/tokenize-command.mjs +76 -24
  49. package/esm/policy/tokenize-command.mjs.map +1 -1
  50. package/esm/shell-jobs.d.mts +45 -0
  51. package/esm/shell-jobs.d.mts.map +1 -0
  52. package/esm/shell-jobs.mjs +247 -0
  53. package/esm/shell-jobs.mjs.map +1 -0
  54. package/esm/todos.d.mts +22 -0
  55. package/esm/todos.d.mts.map +1 -0
  56. package/esm/todos.mjs +73 -0
  57. package/esm/todos.mjs.map +1 -0
  58. package/esm/tools/edit-file.d.mts +2 -3
  59. package/esm/tools/edit-file.d.mts.map +1 -1
  60. package/esm/tools/edit-file.mjs +2 -1
  61. package/esm/tools/edit-file.mjs.map +1 -1
  62. package/esm/tools/glob.d.mts +3 -4
  63. package/esm/tools/glob.d.mts.map +1 -1
  64. package/esm/tools/glob.mjs +2 -1
  65. package/esm/tools/glob.mjs.map +1 -1
  66. package/esm/tools/grep.d.mts +3 -4
  67. package/esm/tools/grep.d.mts.map +1 -1
  68. package/esm/tools/grep.mjs +2 -1
  69. package/esm/tools/grep.mjs.map +1 -1
  70. package/esm/tools/json-schemas.mjs +175 -0
  71. package/esm/tools/json-schemas.mjs.map +1 -0
  72. package/esm/tools/list-dir.d.mts +31 -0
  73. package/esm/tools/list-dir.d.mts.map +1 -0
  74. package/esm/tools/list-dir.mjs +50 -0
  75. package/esm/tools/list-dir.mjs.map +1 -0
  76. package/esm/tools/multi-edit.d.mts +37 -0
  77. package/esm/tools/multi-edit.d.mts.map +1 -0
  78. package/esm/tools/multi-edit.mjs +121 -0
  79. package/esm/tools/multi-edit.mjs.map +1 -0
  80. package/esm/tools/read-file.d.mts +2 -3
  81. package/esm/tools/read-file.d.mts.map +1 -1
  82. package/esm/tools/read-file.mjs +2 -1
  83. package/esm/tools/read-file.mjs.map +1 -1
  84. package/esm/tools/run-shell.d.mts +36 -15
  85. package/esm/tools/run-shell.d.mts.map +1 -1
  86. package/esm/tools/run-shell.mjs +59 -53
  87. package/esm/tools/run-shell.mjs.map +1 -1
  88. package/esm/tools/run-tests.d.mts +11 -4
  89. package/esm/tools/run-tests.d.mts.map +1 -1
  90. package/esm/tools/run-tests.mjs +10 -3
  91. package/esm/tools/run-tests.mjs.map +1 -1
  92. package/esm/tools/schema.mjs +23 -18
  93. package/esm/tools/schema.mjs.map +1 -1
  94. package/esm/tools/shell-kill.d.mts +24 -0
  95. package/esm/tools/shell-kill.d.mts.map +1 -0
  96. package/esm/tools/shell-kill.mjs +36 -0
  97. package/esm/tools/shell-kill.mjs.map +1 -0
  98. package/esm/tools/shell-output.d.mts +29 -0
  99. package/esm/tools/shell-output.d.mts.map +1 -0
  100. package/esm/tools/shell-output.mjs +49 -0
  101. package/esm/tools/shell-output.mjs.map +1 -0
  102. package/esm/tools/todo.d.mts +42 -0
  103. package/esm/tools/todo.d.mts.map +1 -0
  104. package/esm/tools/todo.mjs +126 -0
  105. package/esm/tools/todo.mjs.map +1 -0
  106. package/esm/tools/write-file.d.mts +2 -3
  107. package/esm/tools/write-file.d.mts.map +1 -1
  108. package/esm/tools/write-file.mjs +2 -1
  109. package/esm/tools/write-file.mjs.map +1 -1
  110. package/esm/workspace.d.mts +14 -11
  111. package/esm/workspace.d.mts.map +1 -1
  112. package/esm/workspace.mjs +108 -27
  113. package/esm/workspace.mjs.map +1 -1
  114. package/llms-full.txt +155 -34
  115. package/llms.txt +1 -1
  116. package/package.json +3 -3
  117. package/skills/README.md +2 -2
  118. package/skills/build-loop-agent/SKILL.md +7 -7
  119. package/skills/index.md +6 -1
  120. package/skills/use-a-workspace/SKILL.md +148 -27
@@ -7,34 +7,131 @@
7
7
  * The worktree and container backends from the design doc are deferred
8
8
  * past the W1 surface and intentionally absent from this union.
9
9
  */
10
- type WorkspaceBackendType = "local" | "mock";
10
+ export type WorkspaceBackendType = "local" | "mock";
11
+ /**
12
+ * How `run_shell` / `exec()` interpret a command line.
13
+ *
14
+ * - `"none"` (default) — **argv mode**: the line is tokenized into one argv
15
+ * (quotes respected, no shell semantics) and spawned without a shell. Any
16
+ * unquoted shell operator is refused with a message that says how to split
17
+ * the line.
18
+ * - `"sh"` — **shell mode** (explicit opt-in): the line runs through
19
+ * `/bin/sh -c` (`cmd.exe /d /v:off /s /c` on Windows). Before it runs, the
20
+ * line is parsed and every simple command in it — inside `;`, `&&`, `||`,
21
+ * pipes, subshells, `$( )` and backticks — is checked against the same
22
+ * `deny` / `allow` / `denyPatterns` / `allowCommand` rules, and every
23
+ * redirection target and `cd` directory against the path jail. Syntax the
24
+ * parser cannot read with confidence is refused, never guessed at.
25
+ */
26
+ export type WorkspaceShellInterpreter = "none" | "sh";
27
+ /**
28
+ * A command pattern in `shell.denyPatterns`.
29
+ *
30
+ * - A **string** is tokenized like a command and matches a command whose
31
+ * argv **starts with** those tokens — the executable compared by basename,
32
+ * case-insensitively. `"git push --force"` matches
33
+ * `git push --force origin main` but not `git push origin --force`.
34
+ * - A **RegExp** is tested against the command's argv joined with single
35
+ * spaces, the executable reduced to its basename (`/usr/bin/git push`
36
+ * is tested as `"git push"`). Use it for option orders and spellings:
37
+ * `/^git push\b.*(--force|-f\b)/`. A `g`/`y` flag is ignored.
38
+ */
39
+ export type WorkspaceCommandPattern = string | RegExp;
40
+ /** What {@link WorkspaceAllowCommand} is told about the command it checks. */
41
+ export interface WorkspaceCommandCheckContext {
42
+ /** The full command line the checked command is part of. */
43
+ command: string;
44
+ /** The interpreter the line runs under. */
45
+ interpreter: WorkspaceShellInterpreter;
46
+ /**
47
+ * True when the argv is a command run through a wrapper the policy
48
+ * unwrapped (`time npm test` is checked as `["time", "npm", "test"]` and
49
+ * then as the nested `["npm", "test"]`) or by `find -exec`.
50
+ */
51
+ nested: boolean;
52
+ }
53
+ /**
54
+ * A host predicate over every simple command's argv. Return `true` to let
55
+ * the command through, `false` to refuse it, or a string to refuse it with
56
+ * that explanation (shown to the model). A throw refuses the command too —
57
+ * the hook fails closed. It can only narrow what `allow` / `deny` permit,
58
+ * never widen it, and it must be synchronous.
59
+ *
60
+ * @example
61
+ * allowCommand: (argv) =>
62
+ * argv[0] === "git" && argv[1] === "push" ? "Pushing is done by the release job." : true
63
+ */
64
+ export type WorkspaceAllowCommand = (argv: readonly string[], context: WorkspaceCommandCheckContext) => boolean | string;
65
+ /**
66
+ * Background-command sub-policy. Present ⇒ `run_shell({ background: true })`
67
+ * and `startShell()` may start long-running commands (dev servers,
68
+ * watchers) that belong to the workspace; absent ⇒ background commands are
69
+ * refused. Every background command is killed — its whole process tree —
70
+ * by `workspace.dispose()`, and when the Node process exits.
71
+ */
72
+ export interface WorkspaceBackgroundPolicy {
73
+ /** Most background commands running at once. Default `4`. */
74
+ maxJobs?: number;
75
+ /**
76
+ * Wall-clock cap for one background command, after which its process tree
77
+ * is killed (`status: "timed-out"`). Default: none — it runs until killed
78
+ * or disposed. A model-supplied `timeoutMs` can only lower it.
79
+ */
80
+ timeoutMs?: number;
81
+ }
11
82
  /**
12
83
  * Shell-execution sub-policy for `run_shell` / `run_tests` and the
13
84
  * `exec()` direct method.
14
85
  *
15
- * **Command gating.** Each command is tokenized into an argv (quotes
16
- * respected, NO shell semantics) and executed without a shell; a command
17
- * containing unquoted shell metacharacters (`;`, `&`, `|`, backticks,
18
- * `$`, redirection, parentheses) is denied outright, so metacharacters
19
- * cannot chain extra commands past the gate. The argv's leading
20
- * executable basename is matched against `allow` / `deny`; **deny wins**
21
- * when a name appears in both. An empty/absent `allow` means no command
22
- * is permitted unless the policy explicitly opts in — the workspace is
23
- * fail-closed.
86
+ * **Command gating.** By default (`interpreter: "none"`) each command is
87
+ * tokenized into an argv (quotes respected, NO shell semantics) and executed
88
+ * without a shell; a command containing unquoted shell metacharacters (`;`,
89
+ * `&`, `|`, backticks, `$`, redirection, parentheses) is refused with a
90
+ * message that names the operator and suggests the split. With
91
+ * `interpreter: "sh"` the line runs through a shell, and every simple
92
+ * command in it is checked. Each command's executable basename is matched
93
+ * against `allow` / `deny`; **deny wins** (and matches case-insensitively).
94
+ * A non-empty `allow` is exhaustive. `denyPatterns` and `allowCommand`
95
+ * then refine the decision per argv. A command that would run another
96
+ * command out of the policy's sight (`bash -c`, `eval`, `xargs`, `sudo`, …)
97
+ * is refused when the policy has deny rules, unless it is named in `allow`.
24
98
  *
25
99
  * **Environment.** The spawned process does NOT inherit `process.env`.
26
100
  * The effective environment is `{ ...pick(process.env, inheritEnv), ...env }`
27
101
  * — so `run_shell` cannot find `node`/`npm` unless `inheritEnv` includes
28
- * `"PATH"`. This is deliberate isolation, documented loudly.
102
+ * `"PATH"`. This is deliberate isolation, documented loudly. Shell mode also
103
+ * drops the variables that change how a shell parses or starts (`ENV`,
104
+ * `BASH_ENV`, `SHELLOPTS`, `BASHOPTS`, `CDPATH`, `IFS`, `PS4`, `BASH_FUNC_*`).
29
105
  */
30
- interface WorkspaceShellPolicy {
106
+ export interface WorkspaceShellPolicy {
31
107
  /** Allowed executable basenames (e.g. `["npm", "node"]`). */
32
108
  allow?: string[];
33
109
  /** Denied executable basenames — wins over `allow`. */
34
110
  deny?: string[];
111
+ /**
112
+ * Argument-level denials checked against every simple command (see
113
+ * {@link WorkspaceCommandPattern}), e.g. `["rm -rf /", "git push --force"]`.
114
+ */
115
+ denyPatterns?: WorkspaceCommandPattern[];
116
+ /**
117
+ * Host predicate over every simple command's argv (see
118
+ * {@link WorkspaceAllowCommand}).
119
+ */
120
+ allowCommand?: WorkspaceAllowCommand;
121
+ /**
122
+ * How the command line is interpreted. Default `"none"` (argv mode, no
123
+ * shell). `"sh"` is an explicit opt-in to shell syntax — pipes, `&&`,
124
+ * redirections — still checked per simple command (see
125
+ * {@link WorkspaceShellInterpreter}).
126
+ */
127
+ interpreter?: WorkspaceShellInterpreter;
35
128
  /** Per-command wall-clock cap; on expiry the process is SIGKILLed. */
36
129
  timeoutMs?: number;
37
- /** Max bytes captured from stdout/stderr before output is truncated. */
130
+ /**
131
+ * Max bytes captured from stdout/stderr before output is truncated. For a
132
+ * background command it is the most output kept per command; older output
133
+ * is dropped first.
134
+ */
38
135
  maxOutputBytes?: number;
39
136
  /** Explicit environment variables injected into every spawned process. */
40
137
  env?: Record<string, string>;
@@ -43,12 +140,17 @@ interface WorkspaceShellPolicy {
43
140
  * Nothing from `process.env` leaks in unless listed here.
44
141
  */
45
142
  inheritEnv?: string[];
143
+ /**
144
+ * Enables background commands (see {@link WorkspaceBackgroundPolicy}).
145
+ * Absent ⇒ `run_shell({ background: true })` is refused.
146
+ */
147
+ background?: WorkspaceBackgroundPolicy;
46
148
  }
47
149
  /**
48
150
  * Read sub-policy controlling how much of a file a single read returns.
49
151
  * Applies equally to the `read_file` tool and the `readFile()` direct method.
50
152
  */
51
- interface WorkspaceReadPolicy {
153
+ export interface WorkspaceReadPolicy {
52
154
  /**
53
155
  * Hard cap on the UTF-8 bytes of the `content` a single read returns,
54
156
  * line-number prefixes and newlines included. A window that would exceed
@@ -60,6 +162,23 @@ interface WorkspaceReadPolicy {
60
162
  /** Default number of lines a read returns when no explicit limit is given. */
61
163
  defaultLines?: number;
62
164
  }
165
+ /**
166
+ * Directory-listing sub-policy for the `list_dir` tool and `listDir()`.
167
+ * `denyPaths` are always hidden from listings; `ignore` hides more without
168
+ * blocking access (an ignored file can still be read).
169
+ */
170
+ export interface WorkspaceListPolicy {
171
+ /**
172
+ * Globs (`*`, `**`, `?`; a bare name also hides its contents) left out of
173
+ * listings. Default `["node_modules", ".git"]`; setting it replaces the
174
+ * default.
175
+ */
176
+ ignore?: string[];
177
+ /** Deepest `depth` a listing may ask for. Default `5`. */
178
+ maxDepth?: number;
179
+ /** Most entries one listing returns; the rest set `truncated`. Default `500`. */
180
+ maxEntries?: number;
181
+ }
63
182
  /**
64
183
  * The full policy that bounds a workspace — the single source of truth
65
184
  * the policy engine enforces for both `.tools.*` and the direct methods.
@@ -79,7 +198,7 @@ interface WorkspaceReadPolicy {
79
198
  * backend: "local",
80
199
  * };
81
200
  */
82
- interface WorkspacePolicy {
201
+ export interface WorkspacePolicy {
83
202
  /** Absolute jail root — the workspace cannot read or write outside it. */
84
203
  cwd: string;
85
204
  /** Extra readable roots outside `cwd` (resolved absolute paths). */
@@ -90,9 +209,10 @@ interface WorkspacePolicy {
90
209
  shell?: WorkspaceShellPolicy;
91
210
  /** Read sub-policy (byte cap + default line window). */
92
211
  read?: WorkspaceReadPolicy;
212
+ /** Directory-listing sub-policy (ignore globs + bounds). */
213
+ list?: WorkspaceListPolicy;
93
214
  /** Executor the workspace runs on. Defaults to `"local"`. */
94
215
  backend?: WorkspaceBackendType;
95
216
  }
96
217
  //#endregion
97
- export { WorkspaceBackendType, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellPolicy };
98
218
  //# sourceMappingURL=workspace-policy.type.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-policy.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-policy.type.ts"],"mappings":";;AAQA;;;;AAAgC;AAqBhC;;KArBY,oBAAA;;;;;;;;;;AAoCA;AAOZ;;;;AAUc;AAsBd;;;;UAtDiB,oBAAA;EAkEL;EAhEV,KAAA;EAgE8B;EA9D9B,IAAA;EAsDA;EApDA,SAAA;EAwDA;EAtDA,cAAA;EAwDA;EAtDA,GAAA,GAAM,MAAM;EAwDZ;;;AAA8B;EAnD9B,UAAA;AAAA;;;;;UAOe,mBAAA;;;;;;;;EAQf,QAAA;;EAEA,YAAY;AAAA;;;;;;;;;;;;;;;;;;;;UAsBG,eAAA;;EAEf,GAAA;;EAEA,UAAA;;EAEA,SAAA;;EAEA,KAAA,GAAQ,oBAAA;;EAER,IAAA,GAAO,mBAAA;;EAEP,OAAA,GAAU,oBAAA;AAAA"}
1
+ {"version":3,"file":"workspace-policy.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace-policy.type.ts"],"mappings":";;;;;;;;;YAQY;;;;;;;;;;;;;;;;YAiBA;;;;;;;;;;;;;YAcA,mCAAmC;;iBAG9B;;EAEf;;EAEA,aAAa;;;;;;EAMb;;;;;;;;;;;;;YAcU,yBACV,yBACA,SAAS;;;;;;;;iBAUM;;EAEf;;;;;;EAMA;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2Be;;EAEf;;EAEA;;;;;EAKA,eAAe;;;;;EAKf,eAAe;;;;;;;EAOf,cAAc;;EAEd;;;;;;EAMA;;EAEA,MAAM;;;;;EAKN;;;;;EAKA,aAAa;;;;;;iBAOE;;;;;;;;EAQf;;EAEA;;;;;;;iBAQe;;;;;;EAMf;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;;iBAsBe;;EAEf;;EAEA;;EAEA;;EAEA,QAAQ;;EAER,OAAO;;EAEP,OAAO;;EAEP,UAAU"}
@@ -1,7 +1,6 @@
1
- import { EditFileInput, EditFileResult, ExecOptions, GrepResult, ReadFileResult, RunShellResult, WorkspaceToolName } from "./tool-io.type.mjs";
1
+ import { 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
-
5
4
  //#region ../../packages/ai-workspace/src/contracts/workspace.contract.d.ts
6
5
  /**
7
6
  * The agent-facing tool surface of a workspace. Each factory returns a
@@ -13,8 +12,12 @@ import { ToolContract } from "@warlock.js/ai";
13
12
  * expose the same capability under a custom tool name when wiring
14
13
  * multiple agents.
15
14
  */
16
- interface WorkspaceTools {
17
- /** All seven tools as `ToolContract[]`, ready for `ai.agent({ tools })`. */
15
+ export interface WorkspaceTools {
16
+ /**
17
+ * Every tool this workspace vends, as `ToolContract[]`, ready for
18
+ * `ai.agent({ tools })`. `shell_output` / `shell_kill` are included only
19
+ * when the policy enables background commands (`shell.background`).
20
+ */
18
21
  all(): ToolContract[];
19
22
  /** A named subset, e.g. `pick("readFile", "grep")` for least privilege. */
20
23
  pick(...names: WorkspaceToolName[]): ToolContract[];
@@ -26,14 +29,30 @@ interface WorkspaceTools {
26
29
  editFile(opts?: {
27
30
  name?: string;
28
31
  }): ToolContract;
32
+ /** The `multi_edit` tool (several replacements in one file, all or none). */
33
+ multiEdit(opts?: {
34
+ name?: string;
35
+ }): ToolContract;
29
36
  /** The `write_file` tool (atomic full-content write). */
30
37
  writeFile(opts?: {
31
38
  name?: string;
32
39
  }): ToolContract;
33
- /** The `run_shell` tool (policy-gated command execution). */
40
+ /** The `list_dir` tool (bounded, jailed directory listing). */
41
+ listDir(opts?: {
42
+ name?: string;
43
+ }): ToolContract;
44
+ /** The `run_shell` tool (policy-gated command execution, optionally in the background). */
34
45
  runShell(opts?: {
35
46
  name?: string;
36
47
  }): ToolContract;
48
+ /** The `shell_output` tool (read a background command's new output). */
49
+ shellOutput(opts?: {
50
+ name?: string;
51
+ }): ToolContract;
52
+ /** The `shell_kill` tool (stop a background command). */
53
+ shellKill(opts?: {
54
+ name?: string;
55
+ }): ToolContract;
37
56
  /** The `run_tests` tool (runs the configured test command). */
38
57
  runTests(opts?: {
39
58
  name?: string;
@@ -47,6 +66,14 @@ interface WorkspaceTools {
47
66
  glob(opts?: {
48
67
  name?: string;
49
68
  }): ToolContract;
69
+ /** The `todo_write` tool (replace the workspace's todo list). */
70
+ todoWrite(opts?: {
71
+ name?: string;
72
+ }): ToolContract;
73
+ /** The `todo_read` tool (read the workspace's todo list). */
74
+ todoRead(opts?: {
75
+ name?: string;
76
+ }): ToolContract;
50
77
  }
51
78
  /**
52
79
  * The bounded place an agent reads, writes, and runs commands — a
@@ -54,12 +81,11 @@ interface WorkspaceTools {
54
81
  * ops layer, two ways to touch it: the agent uses `.tools.*`; you use the
55
82
  * direct methods.
56
83
  *
57
- * This is the **W1 surface only**: the policy jail, the seven tools, the
58
- * direct methods, `tools.all()` / `tools.pick()`, plus the two
59
- * composition projections `readonly()` and `scope()`. The transactional
60
- * lifecycle (`snapshot` / `restore` / `diff` / `changes`), hooks
61
- * (`on`), `dispose`, `usage`, and the worktree/container backends are
62
- * deferred to later phases and intentionally absent here.
84
+ * The surface: the policy jail, the tools, the direct methods,
85
+ * `tools.all()` / `tools.pick()`, the two composition projections
86
+ * `readonly()` and `scope()`, background commands, a todo list, and
87
+ * `dispose()`. Projections share their parent's background commands and
88
+ * todo list; disposing any of them disposes the whole workspace.
63
89
  *
64
90
  * @example
65
91
  * const ws = ai.workspace({ cwd: "/srv/acme-api", shell: { allow: ["npm"], inheritEnv: ["PATH"] } });
@@ -71,7 +97,7 @@ interface WorkspaceTools {
71
97
  * await ws.writeFile("src/routes.ts", BOILERPLATE);
72
98
  * const files = await ws.glob("src/models/**\/*.ts");
73
99
  */
74
- interface Workspace {
100
+ export interface Workspace {
75
101
  /** The immutable policy bounding this workspace. */
76
102
  readonly policy: WorkspacePolicy;
77
103
  /** Agent-facing tool factories. */
@@ -93,12 +119,39 @@ interface Workspace {
93
119
  }>;
94
120
  /** Apply an exact-string edit under the read-before-edit stale guard. */
95
121
  editFile(input: EditFileInput): Promise<EditFileResult>;
122
+ /** Apply several exact-string edits to one file — all of them or none. */
123
+ multiEdit(input: MultiEditInput): Promise<MultiEditResult>;
124
+ /** List a jailed directory (bounded, ignores applied, symlinks not followed). */
125
+ listDir(path?: string, opts?: ListDirOptions): Promise<ListDirResult>;
96
126
  /**
97
127
  * Run a policy-gated shell command and capture its outcome. `signal`
98
128
  * cancels this one call: not started when already aborted, process tree
99
129
  * killed when it aborts mid-run, `cancelled: true` either way.
100
130
  */
101
131
  exec(command: string, opts?: ExecOptions): Promise<RunShellResult>;
132
+ /**
133
+ * Start a policy-checked background command owned by this workspace
134
+ * (needs `shell.background`). It runs until it exits, `shellKill(id)`,
135
+ * its timeout, or `dispose()`.
136
+ */
137
+ startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo>;
138
+ /** New output of a background command since `since` (default: the last read). */
139
+ shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult>;
140
+ /** Kill a background command's whole process tree. */
141
+ shellKill(id: string): Promise<ShellJobInfo>;
142
+ /** Every background command this workspace knows, oldest first. */
143
+ listShells(): ShellJobInfo[];
144
+ /** The workspace's todo list (what `todo_read` returns). */
145
+ readTodos(): TodoListResult;
146
+ /** Replace the workspace's todo list (what `todo_write` does). */
147
+ writeTodos(todos: WorkspaceTodoInput[]): TodoListResult;
148
+ /**
149
+ * Close the workspace: kill every background command's process tree and
150
+ * refuse new commands (a foreground command already running finishes on
151
+ * its own, or by its timeout / signal). Idempotent; resolves once the
152
+ * background processes are gone.
153
+ */
154
+ dispose(): Promise<void>;
102
155
  /** Search jailed file contents for a regex pattern. */
103
156
  grep(pattern: string, opts?: {
104
157
  glob?: string;
@@ -126,5 +179,4 @@ interface Workspace {
126
179
  scope(subdir: string): Workspace;
127
180
  }
128
181
  //#endregion
129
- export { Workspace, WorkspaceTools };
130
182
  //# sourceMappingURL=workspace.contract.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace.contract.ts"],"mappings":";;;;;;;AAsBA;;;;;;;;UAAiB,cAAA;EAYqB;EAVpC,GAAA,IAAO,YAAA;EAcyB;EAZhC,IAAA,IAAQ,KAAA,EAAO,iBAAA,KAAsB,YAAA;EAcO;EAZ5C,QAAA,CAAS,IAAA;IAAS,IAAA;EAAA,IAAkB,YAAA;EAFpC;EAIA,QAAA,CAAS,IAAA;IAAS,IAAA;EAAA,IAAkB,YAAA;EAFpC;EAIA,SAAA,CAAU,IAAA;IAAS,IAAA;EAAA,IAAkB,YAAA;EAFrC;EAIA,QAAA,CAAS,IAAA;IAAS,IAAA;EAAA,IAAkB,YAAA;EAFpC;EAIA,QAAA,CAAS,IAAA;IAAS,IAAA;IAAe,OAAA;EAAA,IAAqB,YAAA;EAFpC;EAIlB,IAAA,CAAK,IAAA;IAAS,IAAA;EAAA,IAAkB,YAAA;EAFd;EAIlB,IAAA,CAAK,IAAA;IAAS,IAAA;EAAA,IAAkB,YAAA;AAAA;;;;;;;;;AAAY;AA0B9C;;;;;;;;;;;;;;UAAiB,SAAA;EAsCZ;EAAA,SApCM,MAAA,EAAQ,eAAA;EA0CK;EAAA,SAvCb,KAAA,EAAO,cAAA;EA6CM;;;;;;EArCtB,QAAA,CACE,IAAA,UACA,IAAA;IAAS,MAAA;IAAiB,KAAA;EAAA,IACzB,OAAA,CAAQ,cAAA;EAFT;EAKF,SAAA,CACE,IAAA,UACA,OAAA,WACC,OAAA;IAAU,IAAA;IAAc,YAAA;EAAA;EANhB;EASX,QAAA,CAAS,KAAA,EAAO,aAAA,GAAgB,OAAA,CAAQ,cAAA;EALtC;;;;;EAYF,IAAA,CAAK,OAAA,UAAiB,IAAA,GAAO,WAAA,GAAc,OAAA,CAAQ,cAAA;EAPnC;EAUhB,IAAA,CACE,OAAA,UACA,IAAA;IAAS,IAAA;IAAe,UAAA;EAAA,IACvB,OAAA,CAAQ,UAAA;EANN;EASL,IAAA,CAAK,OAAA,WAAkB,OAAA;EATD;EAYtB,MAAA,CAAO,IAAA,WAAe,OAAA;EAZ6B;EAenD,KAAA,CAAM,IAAA,WAAe,OAAA;EAXnB;EAcF,MAAA,CAAO,IAAA,WAAe,OAAA;EAbI;;;;;EAoB1B,QAAA,IAAY,SAAA;EAhBW;;;;;EAuBvB,KAAA,CAAM,MAAA,WAAiB,SAAA;AAAA"}
1
+ {"version":3,"file":"workspace.contract.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/contracts/workspace.contract.ts"],"mappings":";;;;;;;;;;;;;;iBAgCiB;;;;;;EAMf,OAAO;;EAEP,QAAQ,OAAO,sBAAsB;;EAErC,SAAS;IAAS;MAAkB;;EAEpC,SAAS;IAAS;MAAkB;;EAEpC,UAAU;IAAS;MAAkB;;EAErC,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,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
@@ -1,28 +1,63 @@
1
1
  import { AIError, AIErrorOptions } from "@warlock.js/ai";
2
-
3
2
  //#region ../../packages/ai-workspace/src/errors.d.ts
4
3
  /**
5
4
  * Why a workspace policy check rejected an operation.
6
5
  *
7
6
  * - `"path-escape"` — a resolved path fell outside the `cwd` jail (or an
8
7
  * `allowPaths` root), or matched a `denyPaths` glob.
9
- * - `"denied-command"` — a shell command's leading executable basename
10
- * was not in `shell.allow`, or was explicitly in `shell.deny`.
8
+ * - `"denied-command"` — the shell policy refused a command line (`reason`
9
+ * says why: an executable outside `shell.allow` or inside `shell.deny`, a
10
+ * metacharacter in argv mode, a redirection outside the jail in shell
11
+ * mode, …), or a mutating method was called on a read-only projection.
11
12
  * - `"unsafe-pattern"` — a `grep` pattern was too long or matched a
12
13
  * catastrophic-backtracking shape (nested quantifiers) that could hang
13
14
  * the process (ReDoS).
14
15
  */
15
- type WorkspacePolicyViolation = "path-escape" | "denied-command" | "unsafe-pattern";
16
+ export type WorkspacePolicyViolation = "path-escape" | "denied-command" | "unsafe-pattern";
17
+ /**
18
+ * Why the shell policy refused a command. Set on every
19
+ * {@link WorkspacePolicyError} of type `"denied-command"` raised for a
20
+ * command line, so a host can phrase the refusal itself instead of parsing
21
+ * the message.
22
+ *
23
+ * - `"metacharacter"` — argv mode (the default `interpreter: "none"`) met an
24
+ * unquoted shell operator (`&&`, `;`, `|`, `>`, `$`, backticks, …). The
25
+ * message suggests how to split the line into separate commands.
26
+ * - `"unbalanced-quotes"` — a quote is never closed.
27
+ * - `"denied-executable"` — a command's executable is listed in `shell.deny`.
28
+ * - `"not-allowed"` — a command's executable is not in `shell.allow`, the
29
+ * `shell.allowCommand` hook refused it, the workspace has no shell policy,
30
+ * the line is empty, or background commands are not enabled.
31
+ * - `"denied-pattern"` — a command matched one of `shell.denyPatterns`.
32
+ * - `"nested-command"` — a command runs another command the policy cannot
33
+ * inspect (`bash -c …`, `eval`, `xargs`, `sudo`, a wrapper with options the
34
+ * policy cannot read), while the policy has deny rules it would bypass.
35
+ * - `"unsupported-syntax"` — shell mode (`interpreter: "sh"`) met syntax the
36
+ * policy does not parse with confidence (compound commands, arithmetic,
37
+ * brace expansion, background `&`, a command name decided at run time, …),
38
+ * so the line is refused rather than guessed at.
39
+ * - `"path-escape"` — shell mode: a redirection target or `cd` directory
40
+ * resolves outside the workspace jail or hits `denyPaths`.
41
+ */
42
+ export type WorkspaceShellRefusalReason = "metacharacter" | "unbalanced-quotes" | "denied-executable" | "not-allowed" | "denied-pattern" | "nested-command" | "unsupported-syntax" | "path-escape";
16
43
  /**
17
44
  * Options for {@link WorkspacePolicyError} — the structured `type`
18
45
  * discriminator plus, where relevant, the offending path or command for
19
46
  * branchable diagnostics without parsing the message.
20
47
  */
21
- type WorkspacePolicyErrorOptions = AIErrorOptions & {
22
- /** Which policy rule was violated. */type: WorkspacePolicyViolation; /** The offending workspace-relative path (for `"path-escape"`). */
23
- path?: string; /** The offending command line (for `"denied-command"`). */
24
- command?: string; /** The offending regex pattern (for `"unsafe-pattern"`). */
48
+ export type WorkspacePolicyErrorOptions = AIErrorOptions & {
49
+ /** Which policy rule was violated. */
50
+ type: WorkspacePolicyViolation;
51
+ /** The offending workspace-relative path (for `"path-escape"`). */
52
+ path?: string;
53
+ /** The offending command line (for `"denied-command"`). */
54
+ command?: string;
55
+ /** The offending regex pattern (for `"unsafe-pattern"`). */
25
56
  pattern?: string;
57
+ /** Why the shell policy refused the command (for `"denied-command"`). */
58
+ reason?: WorkspaceShellRefusalReason;
59
+ /** The executable the refusal is about, when there is one. */
60
+ executable?: string;
26
61
  };
27
62
  /**
28
63
  * The workspace policy engine refused an operation — a path escaped the
@@ -41,7 +76,7 @@ type WorkspacePolicyErrorOptions = AIErrorOptions & {
41
76
  * console.warn(`Blocked command: ${error.command}`);
42
77
  * }
43
78
  */
44
- declare class WorkspacePolicyError extends AIError {
79
+ export declare class WorkspacePolicyError extends AIError {
45
80
  /** Which policy rule was violated. */
46
81
  readonly type: WorkspacePolicyViolation;
47
82
  /** The offending path, when the violation was a path escape. */
@@ -50,6 +85,14 @@ declare class WorkspacePolicyError extends AIError {
50
85
  readonly command?: string;
51
86
  /** The offending regex pattern, when the violation was `"unsafe-pattern"`. */
52
87
  readonly pattern?: string;
88
+ /**
89
+ * Why the shell policy refused a command line — set on every
90
+ * `"denied-command"` refusal of a command (see
91
+ * {@link WorkspaceShellRefusalReason}).
92
+ */
93
+ readonly reason?: WorkspaceShellRefusalReason;
94
+ /** The executable the shell refusal is about, when there is one. */
95
+ readonly executable?: string;
53
96
  constructor(message: string, options: WorkspacePolicyErrorOptions);
54
97
  }
55
98
  /**
@@ -61,18 +104,25 @@ declare class WorkspacePolicyError extends AIError {
61
104
  * - `"stale-hash"` — the file's current hash did not match the supplied
62
105
  * `expectHash`; the file changed since it was read.
63
106
  */
64
- type WorkspaceEditFailure = "not-found" | "not-unique" | "stale-hash";
107
+ export type WorkspaceEditFailure = "not-found" | "not-unique" | "stale-hash";
65
108
  /**
66
109
  * Options for {@link WorkspaceEditError} — the structured `type`
67
110
  * discriminator plus optional match-count / hash context for the
68
111
  * `"not-unique"` and `"stale-hash"` cases.
69
112
  */
70
- type WorkspaceEditErrorOptions = AIErrorOptions & {
71
- /** Why the edit was rejected. */type: WorkspaceEditFailure; /** The edited file's workspace-relative path. */
72
- path: string; /** How many times `oldString` matched (for `"not-unique"`). */
73
- matches?: number; /** The hash the caller expected (for `"stale-hash"`). */
74
- expectedHash?: string; /** The file's actual current hash (for `"stale-hash"`). */
113
+ export type WorkspaceEditErrorOptions = AIErrorOptions & {
114
+ /** Why the edit was rejected. */
115
+ type: WorkspaceEditFailure;
116
+ /** The edited file's workspace-relative path. */
117
+ path: string;
118
+ /** How many times `oldString` matched (for `"not-unique"`). */
119
+ matches?: number;
120
+ /** The hash the caller expected (for `"stale-hash"`). */
121
+ expectedHash?: string;
122
+ /** The file's actual current hash (for `"stale-hash"`). */
75
123
  actualHash?: string;
124
+ /** 0-based index of the failing edit, for a `multiEdit` rejection. */
125
+ editIndex?: number;
76
126
  };
77
127
  /**
78
128
  * An `editFile` operation was rejected by the read-before-edit guard:
@@ -88,7 +138,7 @@ type WorkspaceEditErrorOptions = AIErrorOptions & {
88
138
  * // re-read the file and retry the edit with the fresh hash
89
139
  * }
90
140
  */
91
- declare class WorkspaceEditError extends AIError {
141
+ export declare class WorkspaceEditError extends AIError {
92
142
  /** Why the edit was rejected. */
93
143
  readonly type: WorkspaceEditFailure;
94
144
  /** The edited file's workspace-relative path. */
@@ -99,8 +149,44 @@ declare class WorkspaceEditError extends AIError {
99
149
  readonly expectedHash?: string;
100
150
  /** The file's actual current hash, for the `"stale-hash"` case. */
101
151
  readonly actualHash?: string;
152
+ /**
153
+ * For a `multiEdit` rejection, the 0-based index of the edit that failed.
154
+ * Nothing was written: a multi-edit applies all of its edits or none.
155
+ */
156
+ readonly editIndex?: number;
102
157
  constructor(message: string, options: WorkspaceEditErrorOptions);
103
158
  }
159
+ /**
160
+ * Why a background-command operation failed.
161
+ *
162
+ * - `"not-found"` — no background command with that id exists in this
163
+ * workspace (it never existed, or it finished long enough ago to be
164
+ * pruned).
165
+ */
166
+ export type WorkspaceJobFailure = "not-found";
167
+ /** Options for {@link WorkspaceJobError}. */
168
+ export type WorkspaceJobErrorOptions = AIErrorOptions & {
169
+ /** Why the operation failed. */
170
+ type: WorkspaceJobFailure;
171
+ /** The background command id the caller asked for. */
172
+ jobId: string;
173
+ };
174
+ /**
175
+ * A `shell_output` / `shell_kill` call (or `shellOutput()` / `shellKill()`)
176
+ * named a background command this workspace does not know. Returned to the
177
+ * agent as tool-error data like the other workspace errors.
178
+ *
179
+ * @example
180
+ * if (error instanceof WorkspaceJobError && error.type === "not-found") {
181
+ * console.warn(`No background command ${error.jobId}`);
182
+ * }
183
+ */
184
+ export declare class WorkspaceJobError extends AIError {
185
+ /** Why the operation failed. */
186
+ readonly type: WorkspaceJobFailure;
187
+ /** The background command id the caller asked for. */
188
+ readonly jobId: string;
189
+ constructor(message: string, options: WorkspaceJobErrorOptions);
190
+ }
104
191
  //#endregion
105
- export { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, WorkspacePolicyError, WorkspacePolicyErrorOptions, WorkspacePolicyViolation };
106
192
  //# sourceMappingURL=errors.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/errors.ts"],"mappings":";;;;;AAaA;;;;AAAoC;AAOpC;;;;KAPY,wBAAA;;;;;;KAOA,2BAAA,GAA8B,cAAA;EAQjC,sCANP,IAAA,EAAM,wBAAwB,EA0BE;EAxBhC,IAAA,WA0BsB;EAxBtB,OAAA,WAsBwC;EApBxC,OAAA;AAAA;;;;;;;;;;;;AA8BwE;AAoB1E;;;;AAAgC;cA9BnB,oBAAA,SAA6B,OAAA;EAqCL;EAAA,SAnCnB,IAAA,EAAM,wBAAA;EAqCI;EAAA,SAnCV,IAAA;EAmChB;EAAA,SAjCgB,OAAA;EAmChB;EAAA,SAjCgB,OAAA;cAEG,OAAA,UAAiB,OAAA,EAAS,2BAAA;AAAA;;AAqCnC;AAiBZ;;;;;;;KAlCY,oBAAA;;;;;;KAOA,yBAAA,GAA4B,cAAA;EAqCtB,iCAnChB,IAAA,EAAM,oBAAoB,EAqCP;EAnCnB,IAAA,UAmCoC;EAjCpC,OAAA,WAiCsE;EA/BtE,YAAA;EAEA,UAAA;AAAA;;;;;;;;;;;;;;;cAiBW,kBAAA,SAA2B,OAAA;;WAEtB,IAAA,EAAM,oBAAA;;WAEN,IAAA;;WAEA,OAAA;;WAEA,YAAA;;WAEA,UAAA;cAEG,OAAA,UAAiB,OAAA,EAAS,yBAAA;AAAA"}
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;;;;;;;;;;;YAsBnC;;;;;;YAOA,4BAA4B;;EAEtC,MAAM;;EAEN;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;;;qBAiBW,2BAA2B;;WAEtB,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;;;;WAKA;EAEhB,YAAmB,iBAAiB,SAAS;;;;;;;;;YAoBnC;;YAGA,2BAA2B;;EAErC,MAAM;;EAEN;;;;;;;;;;;;qBAaW,0BAA0B;;WAErB,MAAM;;WAEN;EAEhB,YAAmB,iBAAiB,SAAS"}
package/esm/errors.mjs CHANGED
@@ -26,6 +26,8 @@ var WorkspacePolicyError = class extends AIError {
26
26
  this.path = options.path;
27
27
  this.command = options.command;
28
28
  this.pattern = options.pattern;
29
+ this.reason = options.reason;
30
+ this.executable = options.executable;
29
31
  }
30
32
  };
31
33
  /**
@@ -51,9 +53,28 @@ var WorkspaceEditError = class extends AIError {
51
53
  this.matches = options.matches;
52
54
  this.expectedHash = options.expectedHash;
53
55
  this.actualHash = options.actualHash;
56
+ this.editIndex = options.editIndex;
57
+ }
58
+ };
59
+ /**
60
+ * A `shell_output` / `shell_kill` call (or `shellOutput()` / `shellKill()`)
61
+ * named a background command this workspace does not know. Returned to the
62
+ * agent as tool-error data like the other workspace errors.
63
+ *
64
+ * @example
65
+ * if (error instanceof WorkspaceJobError && error.type === "not-found") {
66
+ * console.warn(`No background command ${error.jobId}`);
67
+ * }
68
+ */
69
+ var WorkspaceJobError = class extends AIError {
70
+ constructor(message, options) {
71
+ super("TOOL_EXEC_FAILED", message, options);
72
+ this.name = "WorkspaceJobError";
73
+ this.type = options.type;
74
+ this.jobId = options.jobId;
54
75
  }
55
76
  };
56
77
 
57
78
  //#endregion
58
- export { WorkspaceEditError, WorkspacePolicyError };
79
+ export { WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError };
59
80
  //# sourceMappingURL=errors.mjs.map
@@ -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\"` — a shell command's leading executable basename\n * was not in `shell.allow`, or was explicitly in `shell.deny`.\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 * 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};\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 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 }\n}\n\n/**\n * Why an edit was rejected.\n *\n * - `\"not-found\"` — the `oldString` did not appear in the file.\n * - `\"not-unique\"` — `oldString` matched more than once and `replaceAll`\n * was not set, so the edit is ambiguous.\n * - `\"stale-hash\"` — the file's current hash did not match the supplied\n * `expectHash`; the file changed since it was read.\n */\nexport type WorkspaceEditFailure = \"not-found\" | \"not-unique\" | \"stale-hash\";\n\n/**\n * Options for {@link WorkspaceEditError} — the structured `type`\n * discriminator plus optional match-count / hash context for the\n * `\"not-unique\"` and `\"stale-hash\"` cases.\n */\nexport type WorkspaceEditErrorOptions = AIErrorOptions & {\n /** Why the edit was rejected. */\n type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n path: string;\n /** How many times `oldString` matched (for `\"not-unique\"`). */\n matches?: number;\n /** The hash the caller expected (for `\"stale-hash\"`). */\n expectedHash?: string;\n /** The file's actual current hash (for `\"stale-hash\"`). */\n actualHash?: string;\n};\n\n/**\n * An `editFile` operation was rejected by the read-before-edit guard:\n * the `oldString` was absent, matched non-uniquely without `replaceAll`,\n * or the file's hash no longer matched the supplied `expectHash`.\n *\n * **Surface.** Like {@link WorkspacePolicyError}, returned to the agent\n * as tool-error *data* so it can re-read and retry. Extends `AIError`\n * (category `\"tool\"`, code `TOOL_EXEC_FAILED`); branch on `error.type`.\n *\n * @example\n * if (error instanceof WorkspaceEditError && error.type === \"stale-hash\") {\n * // re-read the file and retry the edit with the fresh hash\n * }\n */\nexport class WorkspaceEditError extends AIError {\n /** Why the edit was rejected. */\n public readonly type: WorkspaceEditFailure;\n /** The edited file's workspace-relative path. */\n public readonly path: string;\n /** How many times `oldString` matched, for the `\"not-unique\"` case. */\n public readonly matches?: number;\n /** The hash the caller expected, for the `\"stale-hash\"` case. */\n public readonly expectedHash?: string;\n /** The file's actual current hash, for the `\"stale-hash\"` case. */\n public readonly actualHash?: string;\n\n public constructor(message: string, options: WorkspaceEditErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WorkspaceEditError\";\n this.type = options.type;\n this.path = options.path;\n this.matches = options.matches;\n this.expectedHash = options.expectedHash;\n this.actualHash = options.actualHash;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,uBAAb,cAA0C,QAAQ;CAUhD,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;CACzB;AACF;;;;;;;;;;;;;;;AA6CA,IAAa,qBAAb,cAAwC,QAAQ;CAY9C,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;CAC5B;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 */\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"}