@warlock.js/ai-workspace 5.45.0 → 5.47.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 +20 -0
- package/README.md +2 -0
- package/cjs/index.cjs +514 -54
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +24 -1
- package/esm/backends/local.mjs.map +1 -1
- package/esm/contracts/index.d.mts +4 -4
- package/esm/contracts/tool-io.type.d.mts +6 -0
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +20 -0
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-edit.type.d.mts +30 -0
- package/esm/contracts/workspace-edit.type.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +7 -2
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-policy.type.d.mts +23 -0
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +4 -1
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/hooks/shell-output.mjs +113 -0
- package/esm/hooks/shell-output.mjs.map +1 -0
- package/esm/index.d.mts +4 -4
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +108 -40
- package/esm/ops.mjs.map +1 -1
- package/esm/policy/shell-check.d.mts +2 -0
- package/esm/policy/shell-check.d.mts.map +1 -1
- package/esm/policy/shell-check.mjs +59 -5
- package/esm/policy/shell-check.mjs.map +1 -1
- package/esm/search/walk.mjs +209 -0
- package/esm/search/walk.mjs.map +1 -0
- package/esm/shell-jobs.d.mts +1 -1
- package/esm/shell-jobs.d.mts.map +1 -1
- package/esm/shell-jobs.mjs +1 -1
- package/esm/shell-jobs.mjs.map +1 -1
- package/esm/tools/glob.mjs +5 -4
- package/esm/tools/glob.mjs.map +1 -1
- package/esm/tools/grep.mjs +3 -2
- package/esm/tools/grep.mjs.map +1 -1
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-shell.mjs +2 -1
- package/esm/tools/run-shell.mjs.map +1 -1
- package/esm/workspace.mjs +2 -2
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +36 -17
- package/package.json +3 -3
- package/skills/use-a-workspace/SKILL.md +36 -17
|
@@ -30,22 +30,23 @@ Paths are resolved against `cwd` with symlinks collapsed and accepted inside `cw
|
|
|
30
30
|
|
|
31
31
|
## The policy surface
|
|
32
32
|
|
|
33
|
-
| Field
|
|
34
|
-
|
|
|
35
|
-
| `cwd`
|
|
36
|
-
| `allowPaths`
|
|
37
|
-
| `denyPaths`
|
|
38
|
-
| `shell.allow` / `shell.deny`
|
|
39
|
-
| `shell.denyPatterns` / `shell.allowCommand`
|
|
40
|
-
| `shell.interpreter`
|
|
41
|
-
| `shell.rules` / `shell.onAsk`
|
|
42
|
-
| `shell.background`
|
|
43
|
-
| `list.ignore` / `list.maxDepth` / `list.maxEntries`
|
|
44
|
-
| `
|
|
45
|
-
| `shell.
|
|
46
|
-
| `shell.
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
33
|
+
| Field | What it does |
|
|
34
|
+
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `cwd` | **Absolute** jail root. Every path resolves against it and must stay inside. |
|
|
36
|
+
| `allowPaths` | Extra readable roots outside `cwd`. |
|
|
37
|
+
| `denyPaths` | Globs (`*`, `**`, `?`) blocked **even inside `cwd`** — e.g. `[".git/**", ".env*"]`. A bare dir name (`"node_modules"`) also blocks its contents. |
|
|
38
|
+
| `shell.allow` / `shell.deny` | Executable **basenames** matched against every command's argv (after tokenizing — see below). **Deny wins**, case-insensitively. An `allow` list is exhaustive (fail-closed); no `shell` block at all ⇒ nothing may run. |
|
|
39
|
+
| `shell.denyPatterns` / `shell.allowCommand` | Argument rules on every command: string prefix patterns (`"git push --force"`) or RegExps, and a predicate `(argv, ctx) => true \| false \| "why"`. |
|
|
40
|
+
| `shell.interpreter` | `"none"` (default, argv), `"sh"`, `"bash"`, `"powershell"`, `"cmd"` or `"auto"` (opt-in shell syntax, every command checked); `shell.interpreterPath` sets the shell executable. |
|
|
41
|
+
| `shell.rules` / `shell.onAsk` | Ordered `{ match, effect: "allow" \| "ask" \| "deny", reason? }` rules (last match wins, a deny anywhere refuses the line); `onAsk({ command, asks, signal })` returns `{ allow, remember? }`. Refusals: `"approval-required"`, `"approval-denied"`. `ws.shell.classify(line)` lists a line's commands. `remember` takes one pattern or an array and `reason` (≤ 500 chars) reaches the model in the refusal; `ws.shell.remembered()` / `forget(pattern)` list and remove remembered patterns. |
|
|
42
|
+
| `shell.background` | `{ maxJobs?, timeoutMs? }` — enables background commands. Absent ⇒ refused. |
|
|
43
|
+
| `list.ignore` / `list.maxDepth` / `list.maxEntries` | `list_dir` bounds: hidden globs (default `node_modules`, `.git`), depth cap (5), entry cap (500). |
|
|
44
|
+
| `search.skipDirs` / `search.gitignore` / `search.maxFiles` / `search.concurrency` | What `glob` and `grep` walk: folders never entered (default `[".git", "node_modules"]`, matched by name at any depth or by path glob), whether `.gitignore` files are honoured (default `true`), the most files one search looks at before it stops and sets `truncated` + `maxFilesReached` (default 50000), and how many folders are read at once (default 16). `denyPaths` and the jail apply regardless. A pattern whose leading folder is skipped (`node_modules/pkg/*.json`) still searches it. |
|
|
45
|
+
| `shell.inheritEnv` | Opt-in `process.env` keys to pass through (e.g. `["PATH"]`). **Nothing leaks in otherwise** — a command can't find `node`/`npm` without `PATH`. |
|
|
46
|
+
| `shell.env` | Explicit env vars injected into every spawned process (override inherited on collision). |
|
|
47
|
+
| `shell.timeoutMs` / `shell.maxOutputBytes` | Per-command wall-clock cap (process tree SIGKILLed on expiry) and stdout/stderr byte cap (clipped on a character boundary). |
|
|
48
|
+
| `read.defaultLines` / `read.maxBytes` | Default read window + a ceiling on the UTF-8 bytes of the returned `content` (line numbers included). See [Read windows and the byte ceiling](#read-windows-and-the-byte-ceiling). |
|
|
49
|
+
| `backend` | `"local"` (default — real disk) or `"mock"` (in-memory, for tests). |
|
|
49
50
|
|
|
50
51
|
> **The most common gotcha:** `run_shell` fails to find `node`/`npm` because the env is empty. Add `shell: { allow: ["npm", "node"], inheritEnv: ["PATH"] }`.
|
|
51
52
|
|
|
@@ -98,6 +99,7 @@ export async function commitWork(cwd: string) {
|
|
|
98
99
|
|
|
99
100
|
- Shell mode parses the line and checks every simple command — inside `;`, `&&`, `||`, pipes, `( )`, `$( )`, backticks, here-documents and `NAME=value cmd` prefixes. Redirection targets and `cd` directories must be literal paths inside the jail (`/dev/null` is fine), re-checked with symlinks resolved.
|
|
100
101
|
- It refuses what it can't parse with confidence (`reason: "unsupported-syntax"`): compound commands, arithmetic, brace expansion, unusual `${…}` forms, bash-only redirections, a command name decided at run time, `&` in a line (use `background: true`), assignments to `IFS`/`PS4`/`CDPATH`/`BASH_ENV`…; it drops shell start-up variables (`ENV`, `BASH_ENV`, `SHELLOPTS`, `BASH_FUNC_*`, …) from the environment.
|
|
102
|
+
- Bash on Windows reads an unquoted backslash as an escape: a line like `cd ab\x\y` is refused (`"unsupported-syntax"`) with the forward-slash form (`ab/x/y`) in the message, and `run_shell`'s description says to write paths with forward slashes. Quoted paths and `a\ b` pass; Linux and macOS are unchanged.
|
|
101
103
|
- `reason` values: `"metacharacter"`, `"unbalanced-quotes"`, `"denied-executable"`, `"not-allowed"`, `"denied-pattern"`, `"nested-command"`, `"unsupported-syntax"`, `"path-escape"`. `checkShellCommand(policy, line)` returns the verdict without running anything; `classifyShellCommand(line, policy?)` returns `{ commands, opaque? }` with the same parser a host approval gate can reuse; `resolveShell(policy.shell)` reports which interpreter and executable `"auto"` (or a named interpreter) resolves to on this host, plus the PowerShell `edition` (`"desktop"` = Windows PowerShell 5.1, `"core"` = 7+) and `version`; the 5.1 dialect (`"powershell5"`) refuses `&&` / `||`, and on Windows `"auto"` prefers `pwsh`, then Git Bash, then 5.1, then `cmd`. The `PowerShellEdition` type is exported.
|
|
102
104
|
- Pair shell mode with an exhaustive `allow`: `deny` alone is a best-effort filter.
|
|
103
105
|
|
|
@@ -176,7 +178,7 @@ export async function devLoop(cwd: string) {
|
|
|
176
178
|
- `run_shell` → `{ exitCode, stdout, stderr, truncated, timedOut, cancelled, truncation?: { stdout?, stderr? }, spilledTo? }`; one path per spilled stream.
|
|
177
179
|
- `shell_output` → the fields above under Background, plus `truncation?: { stdout?, stderr? }` and `spilledTo?`.
|
|
178
180
|
- `read_file` → `{ content, ..., truncated, bytesCapped, truncation?, spilledTo? }`; spills only when `read.maxBytes` cut the window.
|
|
179
|
-
- `grep` → `{ matches, total, truncated?, truncation?, spilledTo? }`; `glob` → `{ paths, truncated?, truncation?, spilledTo? }`.
|
|
181
|
+
- `grep` → `{ matches, total, truncated?, truncation?, spilledTo? }`; `glob` → `{ paths, truncated?, truncation?, spilledTo?, maxFilesReached? }`. Both start at the pattern's static folder prefix (`src/api/**/*.ts` reads only `src/api`), never enter `node_modules`, `.git`, `denyPaths` folders or gitignored paths (see `search.*`), and take an optional `signal` that stops the walk.
|
|
180
182
|
- `truncation` is `{ shown: { lines }, total: { lines, bytes }, path? }`; without `spillDir` there is no `path` and no `spilledTo`.
|
|
181
183
|
- `list_dir({ path?, depth?, ignore? })` → `{ path, entries: [{ path, type, size?, depth }], truncated }`; `denyPaths` always hidden, `list.ignore` (default `node_modules`, `.git`) hidden from listings, symlinks reported but never followed.
|
|
182
184
|
- `apply_patch({ patch, dryRun? })` → `{ files: [{ path, action, oldPath?, hunks, hash? }], dryRun }`; a unified diff that modifies, creates, deletes and renames files, all or none; paths jailed, hunks matched by context, line endings kept, binary files refused (`binary-file`); `dryRun` checks only. Not in `readonly()`.
|
|
@@ -259,6 +261,23 @@ ai.workspace({
|
|
|
259
261
|
|
|
260
262
|
After `write_file`, `edit_file`, `multi_edit` or `apply_patch` writes files, the hook runs once per call with the written paths (not on a patch `dryRun`, not for deleted files). Text it returns comes back on the result as `diagnostics`, between `[diagnostics]` and `[/diagnostics]` lines; output over `afterEditMaxChars` is cut with a marker. A hook that throws or does not answer within `afterEditTimeoutMs` yields `diagnostics unavailable` and never fails the edit.
|
|
261
263
|
|
|
264
|
+
## Live shell output — `hooks.onShellOutput`
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
workspace({
|
|
268
|
+
cwd,
|
|
269
|
+
shell: { allow: ["npm"], env: { API_KEY: process.env.API_KEY! } },
|
|
270
|
+
hooks: {
|
|
271
|
+
onShellOutput: ({ command, stream, chunk, jobId, callId }) => ui.append(chunk),
|
|
272
|
+
redactShellOutput: (text) => text.replace(/sk-[a-z0-9]+/g, "[redacted]"),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`run_shell` returns its output when the command ends; `onShellOutput` (event type `WorkspaceShellOutputEvent`) lets a host that shows the session to a person see it while the command runs. It fires for foreground commands and for background jobs (`jobId` is set; it keeps firing after `run_shell` has returned). Text is decoded, in order per stream, delivered at most about 20 times a second (faster output is merged), and never changes the result the agent gets (truncation and spilling are as before). A hook that throws is ignored. Pass `callId` to `ws.exec(command, { callId })` to have it echoed on a foreground command's events. A backend that cannot stream delivers the whole capture once, when the command ends.
|
|
278
|
+
|
|
279
|
+
The workspace redacts nothing in results, so live text is raw unless you mask it. Redaction needs a secret in one piece but a process writes in arbitrary pieces, so output is buffered to line boundaries first and then redacted: a secret split across two reads is whole by the time `redactShellOutput(text)` sees it. The unfinished last line is held until its newline arrives or the command ends (a line over 16 KB is cut, keeping its last 512 characters back). Values in `shell.env` of eight or more characters are masked as `[redacted]` before `redactShellOutput` runs. If the redactor throws, that text is dropped, never delivered unmasked. A secret that contains a newline cannot be matched this way.
|
|
280
|
+
|
|
262
281
|
## Oversized output — `output`
|
|
263
282
|
|
|
264
283
|
```ts
|