@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.
- package/CHANGELOG.md +52 -2
- package/README.md +34 -21
- package/cjs/index.cjs +3134 -315
- package/cjs/index.cjs.map +1 -1
- package/esm/backends/local.d.mts +1 -3
- package/esm/backends/local.d.mts.map +1 -1
- package/esm/backends/local.mjs +166 -28
- package/esm/backends/local.mjs.map +1 -1
- package/esm/backends/mock.d.mts +14 -9
- package/esm/backends/mock.d.mts.map +1 -1
- package/esm/backends/mock.mjs +34 -0
- package/esm/backends/mock.mjs.map +1 -1
- package/esm/contracts/index.d.mts +5 -4
- package/esm/contracts/tool-io.type.d.mts +216 -19
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +62 -4
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +33 -6
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-policy.type.d.mts +136 -16
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +65 -13
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +103 -17
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +22 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/index.d.mts +14 -5
- package/esm/index.mjs +10 -2
- package/esm/ops.d.mts +21 -3
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +230 -64
- package/esm/ops.mjs.map +1 -1
- package/esm/policy/jail.mjs +73 -0
- package/esm/policy/jail.mjs.map +1 -0
- package/esm/policy/policy.d.mts +30 -20
- package/esm/policy/policy.d.mts.map +1 -1
- package/esm/policy/policy.mjs +64 -87
- package/esm/policy/policy.mjs.map +1 -1
- package/esm/policy/shell-check.d.mts +63 -0
- package/esm/policy/shell-check.d.mts.map +1 -0
- package/esm/policy/shell-check.mjs +638 -0
- package/esm/policy/shell-check.mjs.map +1 -0
- package/esm/policy/shell-parser.mjs +802 -0
- package/esm/policy/shell-parser.mjs.map +1 -0
- package/esm/policy/tokenize-command.d.mts +62 -0
- package/esm/policy/tokenize-command.d.mts.map +1 -0
- package/esm/policy/tokenize-command.mjs +76 -24
- package/esm/policy/tokenize-command.mjs.map +1 -1
- package/esm/shell-jobs.d.mts +45 -0
- package/esm/shell-jobs.d.mts.map +1 -0
- package/esm/shell-jobs.mjs +247 -0
- package/esm/shell-jobs.mjs.map +1 -0
- package/esm/todos.d.mts +22 -0
- package/esm/todos.d.mts.map +1 -0
- package/esm/todos.mjs +73 -0
- package/esm/todos.mjs.map +1 -0
- package/esm/tools/edit-file.d.mts +2 -3
- package/esm/tools/edit-file.d.mts.map +1 -1
- package/esm/tools/edit-file.mjs +2 -1
- package/esm/tools/edit-file.mjs.map +1 -1
- package/esm/tools/glob.d.mts +3 -4
- package/esm/tools/glob.d.mts.map +1 -1
- package/esm/tools/glob.mjs +2 -1
- package/esm/tools/glob.mjs.map +1 -1
- package/esm/tools/grep.d.mts +3 -4
- package/esm/tools/grep.d.mts.map +1 -1
- package/esm/tools/grep.mjs +2 -1
- package/esm/tools/grep.mjs.map +1 -1
- package/esm/tools/json-schemas.mjs +175 -0
- package/esm/tools/json-schemas.mjs.map +1 -0
- package/esm/tools/list-dir.d.mts +31 -0
- package/esm/tools/list-dir.d.mts.map +1 -0
- package/esm/tools/list-dir.mjs +50 -0
- package/esm/tools/list-dir.mjs.map +1 -0
- package/esm/tools/multi-edit.d.mts +37 -0
- package/esm/tools/multi-edit.d.mts.map +1 -0
- package/esm/tools/multi-edit.mjs +121 -0
- package/esm/tools/multi-edit.mjs.map +1 -0
- package/esm/tools/read-file.d.mts +2 -3
- package/esm/tools/read-file.d.mts.map +1 -1
- package/esm/tools/read-file.mjs +2 -1
- package/esm/tools/read-file.mjs.map +1 -1
- package/esm/tools/run-shell.d.mts +36 -15
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-shell.mjs +59 -53
- package/esm/tools/run-shell.mjs.map +1 -1
- package/esm/tools/run-tests.d.mts +11 -4
- package/esm/tools/run-tests.d.mts.map +1 -1
- package/esm/tools/run-tests.mjs +10 -3
- package/esm/tools/run-tests.mjs.map +1 -1
- package/esm/tools/schema.mjs +23 -18
- package/esm/tools/schema.mjs.map +1 -1
- package/esm/tools/shell-kill.d.mts +24 -0
- package/esm/tools/shell-kill.d.mts.map +1 -0
- package/esm/tools/shell-kill.mjs +36 -0
- package/esm/tools/shell-kill.mjs.map +1 -0
- package/esm/tools/shell-output.d.mts +29 -0
- package/esm/tools/shell-output.d.mts.map +1 -0
- package/esm/tools/shell-output.mjs +49 -0
- package/esm/tools/shell-output.mjs.map +1 -0
- package/esm/tools/todo.d.mts +42 -0
- package/esm/tools/todo.d.mts.map +1 -0
- package/esm/tools/todo.mjs +126 -0
- package/esm/tools/todo.mjs.map +1 -0
- package/esm/tools/write-file.d.mts +2 -3
- package/esm/tools/write-file.d.mts.map +1 -1
- package/esm/tools/write-file.mjs +2 -1
- package/esm/tools/write-file.mjs.map +1 -1
- package/esm/workspace.d.mts +14 -11
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +108 -27
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +155 -34
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/README.md +2 -2
- package/skills/build-loop-agent/SKILL.md +7 -7
- package/skills/index.md +6 -1
- package/skills/use-a-workspace/SKILL.md +148 -27
package/esm/index.d.mts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import { EditFileInput, EditFileResult, ExecOptions, GlobInput, GlobResult, GrepInput, GrepMatch, GrepResult, ReadFileInput, ReadFileResult, RunShellInput, RunShellResult, RunTestsInput, WorkspaceToolName, WriteFileInput, WriteFileResult } from "./contracts/tool-io.type.mjs";
|
|
2
|
-
import {
|
|
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";
|
|
2
|
+
import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy } from "./contracts/workspace-policy.type.mjs";
|
|
3
|
+
import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./contracts/workspace-backend.contract.mjs";
|
|
3
4
|
import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
|
|
4
|
-
import { WorkspaceBackendType, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellPolicy } from "./contracts/workspace-policy.type.mjs";
|
|
5
5
|
import { Workspace, WorkspaceTools } from "./contracts/workspace.contract.mjs";
|
|
6
|
-
import
|
|
6
|
+
import "./contracts/index.mjs";
|
|
7
|
+
import { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, WorkspaceJobError, WorkspaceJobErrorOptions, WorkspaceJobFailure, WorkspacePolicyError, WorkspacePolicyErrorOptions, WorkspacePolicyViolation, WorkspaceShellRefusalReason } from "./errors.mjs";
|
|
7
8
|
import { ResolvedPath, buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
|
|
9
|
+
import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand } from "./policy/shell-check.mjs";
|
|
10
|
+
import { ArgvAnalysis, analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
|
|
8
11
|
import { createOps } from "./ops.mjs";
|
|
9
12
|
import { createLocalBackend } from "./backends/local.mjs";
|
|
10
13
|
import { MockBackendSeed, MockExecResult, createMockBackend } from "./backends/mock.mjs";
|
|
11
14
|
import { makeEditFileTool } from "./tools/edit-file.mjs";
|
|
12
15
|
import { MakeGlobToolOptions, makeGlobTool } from "./tools/glob.mjs";
|
|
13
16
|
import { MakeGrepToolOptions, makeGrepTool } from "./tools/grep.mjs";
|
|
17
|
+
import { makeListDirTool } from "./tools/list-dir.mjs";
|
|
18
|
+
import { makeMultiEditTool } from "./tools/multi-edit.mjs";
|
|
14
19
|
import { makeReadFileTool } from "./tools/read-file.mjs";
|
|
15
20
|
import { MakeRunShellToolOptions, makeRunShellTool } from "./tools/run-shell.mjs";
|
|
16
21
|
import { MakeRunTestsToolOptions, makeRunTestsTool } from "./tools/run-tests.mjs";
|
|
22
|
+
import { makeShellKillTool } from "./tools/shell-kill.mjs";
|
|
23
|
+
import { makeShellOutputTool } from "./tools/shell-output.mjs";
|
|
24
|
+
import { TodoStore } from "./todos.mjs";
|
|
25
|
+
import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
|
|
17
26
|
import { makeWriteFileTool } from "./tools/write-file.mjs";
|
|
18
27
|
import { workspace } from "./workspace.mjs";
|
|
19
|
-
export { type EditFileInput, type EditFileResult, type ExecOptions, type GlobInput, type GlobResult, type GrepInput, type GrepMatch, type GrepResult, type MakeGlobToolOptions, type MakeGrepToolOptions, type MakeRunShellToolOptions, type MakeRunTestsToolOptions, type MockBackendSeed, type MockExecResult, type ReadFileInput, type ReadFileResult, type ResolvedPath, type RunShellInput, type RunShellResult, type RunTestsInput, type Workspace, type WorkspaceBackend, type WorkspaceBackendExecOptions, type WorkspaceBackendExecResult, type WorkspaceBackendType, WorkspaceEditError, type WorkspaceEditErrorOptions, type WorkspaceEditFailure, type WorkspaceOps, type WorkspacePolicy, WorkspacePolicyError, type WorkspacePolicyErrorOptions, type WorkspacePolicyViolation, type WorkspaceReadPolicy, type WorkspaceShellPolicy, type WorkspaceToolName, type WorkspaceTools, type WriteFileInput, type WriteFileResult, buildEnv, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeWriteFileTool, resolveInJail, workspace };
|
|
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 };
|
|
20
29
|
import "./workspace.mjs";
|
package/esm/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
|
|
1
|
+
import { WorkspaceEditError, WorkspaceJobError, WorkspacePolicyError } from "./errors.mjs";
|
|
2
|
+
import { analyzeArgvCommand, tokenizeCommand } from "./policy/tokenize-command.mjs";
|
|
3
|
+
import { checkShellCommand } from "./policy/shell-check.mjs";
|
|
2
4
|
import { buildEnv, isCommandAllowed, resolveInJail } from "./policy/policy.mjs";
|
|
3
5
|
import { createOps } from "./ops.mjs";
|
|
4
6
|
import { createLocalBackend } from "./backends/local.mjs";
|
|
@@ -6,10 +8,16 @@ import { createMockBackend } from "./backends/mock.mjs";
|
|
|
6
8
|
import { makeEditFileTool } from "./tools/edit-file.mjs";
|
|
7
9
|
import { makeGlobTool } from "./tools/glob.mjs";
|
|
8
10
|
import { makeGrepTool } from "./tools/grep.mjs";
|
|
11
|
+
import { makeListDirTool } from "./tools/list-dir.mjs";
|
|
12
|
+
import { makeMultiEditTool } from "./tools/multi-edit.mjs";
|
|
9
13
|
import { makeReadFileTool } from "./tools/read-file.mjs";
|
|
10
14
|
import { makeRunShellTool } from "./tools/run-shell.mjs";
|
|
11
15
|
import { makeRunTestsTool } from "./tools/run-tests.mjs";
|
|
16
|
+
import { makeShellKillTool } from "./tools/shell-kill.mjs";
|
|
17
|
+
import { makeShellOutputTool } from "./tools/shell-output.mjs";
|
|
18
|
+
import { TodoStore } from "./todos.mjs";
|
|
19
|
+
import { makeTodoReadTool, makeTodoWriteTool } from "./tools/todo.mjs";
|
|
12
20
|
import { makeWriteFileTool } from "./tools/write-file.mjs";
|
|
13
21
|
import { workspace } from "./workspace.mjs";
|
|
14
22
|
|
|
15
|
-
export { WorkspaceEditError, WorkspacePolicyError, buildEnv, createLocalBackend, createMockBackend, createOps, isCommandAllowed, makeEditFileTool, makeGlobTool, makeGrepTool, makeReadFileTool, makeRunShellTool, makeRunTestsTool, makeWriteFileTool, resolveInJail, workspace };
|
|
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 };
|
package/esm/ops.d.mts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
|
|
1
2
|
import { WorkspaceBackend } from "./contracts/workspace-backend.contract.mjs";
|
|
2
3
|
import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
|
|
3
|
-
import
|
|
4
|
+
import "./contracts/index.mjs";
|
|
5
|
+
import { ShellJobs } from "./shell-jobs.mjs";
|
|
4
6
|
//#region ../../packages/ai-workspace/src/ops.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* The internal, single-instance implementation of {@link WorkspaceOps}.
|
|
9
|
+
* Holds the backend + policy and is the one place the jail, command
|
|
10
|
+
* gating, read caps, and the read-before-edit guard are enforced — both
|
|
11
|
+
* the agent-facing tools and the human-facing direct methods funnel
|
|
12
|
+
* through this object, so there is exactly one set of rules.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* State an ops layer shares with the other projections of the same
|
|
16
|
+
* workspace (`readonly()`, `scope()`): the background commands it owns.
|
|
17
|
+
*/
|
|
18
|
+
export interface OpsRuntime {
|
|
19
|
+
/** The workspace's background commands. */
|
|
20
|
+
jobs: ShellJobs;
|
|
21
|
+
}
|
|
5
22
|
/**
|
|
6
23
|
* Create the policy-enforced operation layer over a backend.
|
|
7
24
|
*
|
|
@@ -14,12 +31,13 @@ import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
|
|
|
14
31
|
*
|
|
15
32
|
* @param backend - The dumb IO executor (local disk or in-memory mock).
|
|
16
33
|
* @param policy - The policy that bounds every operation.
|
|
34
|
+
* @param runtime - State shared with other views of the same workspace
|
|
35
|
+
* (its background commands); a fresh one when omitted.
|
|
17
36
|
*
|
|
18
37
|
* @example
|
|
19
38
|
* const ops = createOps(localBackend, { cwd: "/srv/api", shell: { allow: ["npm"] } });
|
|
20
39
|
* const { content, hash } = await ops.readFile("src/index.ts");
|
|
21
40
|
*/
|
|
22
|
-
declare function createOps(backend: WorkspaceBackend, policy: WorkspacePolicy): WorkspaceOps;
|
|
41
|
+
export declare function createOps(backend: WorkspaceBackend, policy: WorkspacePolicy, runtime?: OpsRuntime): WorkspaceOps;
|
|
23
42
|
//#endregion
|
|
24
|
-
export { createOps };
|
|
25
43
|
//# sourceMappingURL=ops.d.mts.map
|
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":";;;;;;;;;;;;;;;;;iBA8LiB;;EAEf,MAAM;;;;;;;;;;;;;;;;;;;;;wBAitBQ,UACd,SAAS,kBACT,QAAQ,iBACR,UAAU,aACT"}
|
package/esm/ops.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { WorkspaceEditError, WorkspacePolicyError } from "./errors.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { globToRegExp, isInside, matchesAnyGlob } from "./policy/jail.mjs";
|
|
3
|
+
import { checkShellCommand } from "./policy/shell-check.mjs";
|
|
4
|
+
import { buildCommandEnv, resolveInJail } from "./policy/policy.mjs";
|
|
5
|
+
import { ShellJobs } from "./shell-jobs.mjs";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { fs } from "@warlock.js/fs";
|
|
5
8
|
|
|
@@ -10,6 +13,14 @@ const DEFAULT_READ_LINES = 2e3;
|
|
|
10
13
|
const DEFAULT_MAX_GREP_MATCHES = 1e3;
|
|
11
14
|
/** Default per-command output byte cap when the policy sets none. */
|
|
12
15
|
const DEFAULT_MAX_OUTPUT_BYTES = 1e6;
|
|
16
|
+
/** Default `list.ignore` when the policy sets none. */
|
|
17
|
+
const DEFAULT_LIST_IGNORE = ["node_modules", ".git"];
|
|
18
|
+
/** Default deepest `depth` a listing may ask for. */
|
|
19
|
+
const DEFAULT_LIST_MAX_DEPTH = 5;
|
|
20
|
+
/** Default most entries one listing returns. */
|
|
21
|
+
const DEFAULT_LIST_MAX_ENTRIES = 500;
|
|
22
|
+
/** Most edits one `multiEdit` call may carry. */
|
|
23
|
+
const MAX_MULTI_EDITS = 100;
|
|
13
24
|
/**
|
|
14
25
|
* Exit code reported for a command refused because its signal was already
|
|
15
26
|
* aborted (the shell convention for an interrupted command). Callers should
|
|
@@ -100,32 +111,6 @@ function capOutput(value, maxBytes) {
|
|
|
100
111
|
return clipUtf8(value, maxBytes);
|
|
101
112
|
}
|
|
102
113
|
/**
|
|
103
|
-
* Minimal glob match over a `/`-separated relative path. Supports `**`
|
|
104
|
-
* (spans separators), `*` (within a segment), and `?` (one non-separator
|
|
105
|
-
* char) — enough for the workspace's `glob`/grep narrowing without
|
|
106
|
-
* pulling in a runtime dependency.
|
|
107
|
-
*/
|
|
108
|
-
function globToRegExp(glob) {
|
|
109
|
-
let source = "";
|
|
110
|
-
for (let index = 0; index < glob.length; index++) {
|
|
111
|
-
const char = glob[index];
|
|
112
|
-
if (char === "*") {
|
|
113
|
-
if (glob[index + 1] === "*") {
|
|
114
|
-
source += ".*";
|
|
115
|
-
index++;
|
|
116
|
-
if (glob[index + 1] === "/") index++;
|
|
117
|
-
} else source += "[^/]*";
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (char === "?") {
|
|
121
|
-
source += "[^/]";
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
125
|
-
}
|
|
126
|
-
return new RegExp(`^${source}$`);
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
114
|
* A quantifier: `+`, `*`, `?`, or a `{m,n}` bound.
|
|
130
115
|
*/
|
|
131
116
|
const QUANTIFIER_SOURCE = String.raw`[+*?]|\{\d*,?\d*\}`;
|
|
@@ -147,17 +132,11 @@ function isSafeGrepPattern(pattern) {
|
|
|
147
132
|
if (pattern.length > MAX_GREP_PATTERN_LENGTH) return false;
|
|
148
133
|
return !NESTED_QUANTIFIER_PATTERN.test(pattern);
|
|
149
134
|
}
|
|
150
|
-
/**
|
|
151
|
-
* The internal, single-instance implementation of {@link WorkspaceOps}.
|
|
152
|
-
* Holds the backend + policy and is the one place the jail, command
|
|
153
|
-
* gating, read caps, and the read-before-edit guard are enforced — both
|
|
154
|
-
* the agent-facing tools and the human-facing direct methods funnel
|
|
155
|
-
* through this object, so there is exactly one set of rules.
|
|
156
|
-
*/
|
|
157
135
|
var Ops = class {
|
|
158
|
-
constructor(backend, policy) {
|
|
136
|
+
constructor(backend, policy, runtime) {
|
|
159
137
|
this.backend = backend;
|
|
160
138
|
this.policy = policy;
|
|
139
|
+
this.runtime = runtime;
|
|
161
140
|
this.pathLocks = /* @__PURE__ */ new Map();
|
|
162
141
|
}
|
|
163
142
|
/** Run `task` after every earlier mutation of `absolutePath` has settled. */
|
|
@@ -251,30 +230,76 @@ var Ops = class {
|
|
|
251
230
|
expectedHash: input.expectHash,
|
|
252
231
|
actualHash: currentHash
|
|
253
232
|
});
|
|
254
|
-
const
|
|
255
|
-
if (occurrences === 0) throw new WorkspaceEditError(`The text to replace was not found in "${input.path}".`, {
|
|
256
|
-
type: "not-found",
|
|
257
|
-
path: relativePath || input.path,
|
|
258
|
-
matches: 0
|
|
259
|
-
});
|
|
260
|
-
if (occurrences > 1 && !input.replaceAll) throw new WorkspaceEditError(`The text to replace is not unique in "${input.path}" (${occurrences} matches); pass replaceAll or include more surrounding context.`, {
|
|
261
|
-
type: "not-unique",
|
|
262
|
-
path: relativePath || input.path,
|
|
263
|
-
matches: occurrences
|
|
264
|
-
});
|
|
265
|
-
const updated = input.replaceAll ? current.split(input.oldString).join(input.newString) : replaceFirst(current, input.oldString, input.newString);
|
|
233
|
+
const { content: updated, replacements } = applyReplacement(current, input, input.path, relativePath || input.path);
|
|
266
234
|
await this.backend.writeFile(absolutePath, updated);
|
|
267
235
|
return {
|
|
268
236
|
path: relativePath || input.path,
|
|
269
|
-
replacements
|
|
237
|
+
replacements,
|
|
270
238
|
hash: fs.hash.string(updated)
|
|
271
239
|
};
|
|
272
240
|
}
|
|
273
|
-
async
|
|
274
|
-
|
|
241
|
+
async multiEdit(input) {
|
|
242
|
+
const { absolutePath, relativePath } = await resolveInJail(this.policy, input.path);
|
|
243
|
+
const shownPath = relativePath || input.path;
|
|
244
|
+
if (input.edits.length === 0 || input.edits.length > 100) throw new RangeError(`A multi-edit needs between 1 and ${100} edits (got ${input.edits.length}).`);
|
|
245
|
+
return this.withPathLock(absolutePath, async () => {
|
|
246
|
+
const current = await this.backend.readFile(absolutePath);
|
|
247
|
+
const currentHash = fs.hash.string(current);
|
|
248
|
+
if (input.expectHash !== void 0 && input.expectHash !== currentHash) throw new WorkspaceEditError(`File "${input.path}" changed since it was read; none of the edits were applied.`, {
|
|
249
|
+
type: "stale-hash",
|
|
250
|
+
path: shownPath,
|
|
251
|
+
expectedHash: input.expectHash,
|
|
252
|
+
actualHash: currentHash
|
|
253
|
+
});
|
|
254
|
+
let content = current;
|
|
255
|
+
let replacements = 0;
|
|
256
|
+
input.edits.forEach((edit, editIndex) => {
|
|
257
|
+
const applied = applyReplacement(content, edit, input.path, shownPath, editIndex);
|
|
258
|
+
content = applied.content;
|
|
259
|
+
replacements += applied.replacements;
|
|
260
|
+
});
|
|
261
|
+
await this.backend.writeFile(absolutePath, content);
|
|
262
|
+
return {
|
|
263
|
+
path: shownPath,
|
|
264
|
+
edits: input.edits.length,
|
|
265
|
+
replacements,
|
|
266
|
+
hash: fs.hash.string(content)
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check a command line against the shell policy: every command in it
|
|
272
|
+
* (argv or shell mode), then — shell mode — every redirection target and
|
|
273
|
+
* `cd` directory against the jail with symlinks resolved.
|
|
274
|
+
*/
|
|
275
|
+
async assertCommandAllowed(command) {
|
|
276
|
+
const verdict = checkShellCommand(this.policy, command);
|
|
277
|
+
if (!verdict.allowed) throw new WorkspacePolicyError(verdict.message ?? `Command is not permitted by the workspace shell policy: ${command}`, {
|
|
278
|
+
type: "denied-command",
|
|
279
|
+
command,
|
|
280
|
+
...verdict.reason !== void 0 ? { reason: verdict.reason } : {},
|
|
281
|
+
...verdict.executable !== void 0 ? { executable: verdict.executable } : {},
|
|
282
|
+
...verdict.path !== void 0 ? { path: verdict.path } : {}
|
|
283
|
+
});
|
|
284
|
+
for (const target of verdict.paths) try {
|
|
285
|
+
await resolveInJail(this.policy, target.path);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (!(error instanceof WorkspacePolicyError)) throw error;
|
|
288
|
+
throw new WorkspacePolicyError(target.access === "cwd" ? `\`cd ${target.target}\` resolves (through a symlink) outside the workspace jail or onto a denied path.` : `The redirection target \`${target.target}\` resolves (through a symlink) outside the workspace jail or onto a denied path.`, {
|
|
289
|
+
type: "denied-command",
|
|
290
|
+
reason: "path-escape",
|
|
291
|
+
command,
|
|
292
|
+
path: target.target
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (this.runtime.jobs.isDisposed) throw new WorkspacePolicyError("The workspace has been disposed; it cannot run new commands.", {
|
|
275
296
|
type: "denied-command",
|
|
297
|
+
reason: "not-allowed",
|
|
276
298
|
command
|
|
277
299
|
});
|
|
300
|
+
}
|
|
301
|
+
async exec(command, opts) {
|
|
302
|
+
await this.assertCommandAllowed(command);
|
|
278
303
|
const signal = opts?.signal;
|
|
279
304
|
if (signal?.aborted) return {
|
|
280
305
|
exitCode: CANCELLED_BEFORE_START_EXIT_CODE,
|
|
@@ -287,11 +312,13 @@ var Ops = class {
|
|
|
287
312
|
const shell = this.policy.shell;
|
|
288
313
|
const timeoutMs = opts?.timeoutMs ?? shell?.timeoutMs;
|
|
289
314
|
const maxOutputBytes = shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
315
|
+
const interpreter = shell?.interpreter ?? "none";
|
|
290
316
|
const result = await this.backend.exec(command, {
|
|
291
317
|
cwd: this.policy.cwd,
|
|
292
318
|
timeoutMs,
|
|
293
|
-
env:
|
|
294
|
-
...signal ? { signal } : {}
|
|
319
|
+
env: buildCommandEnv(this.policy),
|
|
320
|
+
...signal ? { signal } : {},
|
|
321
|
+
...interpreter !== "none" ? { interpreter } : {}
|
|
295
322
|
});
|
|
296
323
|
const stdout = capOutput(result.stdout, maxOutputBytes);
|
|
297
324
|
const stderr = capOutput(result.stderr, maxOutputBytes);
|
|
@@ -305,6 +332,120 @@ var Ops = class {
|
|
|
305
332
|
cancelled
|
|
306
333
|
};
|
|
307
334
|
}
|
|
335
|
+
async startShell(command, opts) {
|
|
336
|
+
const background = this.policy.shell?.background;
|
|
337
|
+
if (background === void 0) throw new WorkspacePolicyError("Background commands are not enabled for this workspace. Run the command without background, or ask for shell.background to be enabled.", {
|
|
338
|
+
type: "denied-command",
|
|
339
|
+
reason: "not-allowed",
|
|
340
|
+
command
|
|
341
|
+
});
|
|
342
|
+
await this.assertCommandAllowed(command);
|
|
343
|
+
const start = this.backend.start?.bind(this.backend);
|
|
344
|
+
if (start === void 0) throw new WorkspacePolicyError("This workspace backend cannot run background commands.", {
|
|
345
|
+
type: "denied-command",
|
|
346
|
+
reason: "not-allowed",
|
|
347
|
+
command
|
|
348
|
+
});
|
|
349
|
+
const shell = this.policy.shell;
|
|
350
|
+
const interpreter = shell?.interpreter ?? "none";
|
|
351
|
+
const timeoutMs = lowerOf(background.timeoutMs, opts?.timeoutMs);
|
|
352
|
+
const env = buildCommandEnv(this.policy);
|
|
353
|
+
return this.runtime.jobs.start({
|
|
354
|
+
command,
|
|
355
|
+
retainBytes: shell?.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
|
|
356
|
+
maxJobs: background.maxJobs ?? 4,
|
|
357
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
358
|
+
launch: (onOutput) => start(command, {
|
|
359
|
+
cwd: this.policy.cwd,
|
|
360
|
+
env,
|
|
361
|
+
...interpreter !== "none" ? { interpreter } : {},
|
|
362
|
+
onOutput
|
|
363
|
+
})
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
shellOutput(id, opts) {
|
|
367
|
+
return this.runtime.jobs.output(id, opts);
|
|
368
|
+
}
|
|
369
|
+
shellKill(id) {
|
|
370
|
+
return this.runtime.jobs.kill(id);
|
|
371
|
+
}
|
|
372
|
+
listShells() {
|
|
373
|
+
return this.runtime.jobs.list();
|
|
374
|
+
}
|
|
375
|
+
dispose() {
|
|
376
|
+
return this.runtime.jobs.dispose();
|
|
377
|
+
}
|
|
378
|
+
async listDir(inputPath = ".", opts) {
|
|
379
|
+
const { absolutePath: root, relativePath: rootRelative } = await resolveInJail(this.policy, inputPath);
|
|
380
|
+
const { absolutePath: jailRoot } = await resolveInJail(this.policy, ".");
|
|
381
|
+
const listPolicy = this.policy.list;
|
|
382
|
+
const maxDepth = Math.max(1, Math.floor(listPolicy?.maxDepth ?? DEFAULT_LIST_MAX_DEPTH));
|
|
383
|
+
const depth = Math.min(Math.max(1, Math.floor(opts?.depth ?? 1)), maxDepth);
|
|
384
|
+
const maxEntries = Math.max(1, Math.floor(listPolicy?.maxEntries ?? DEFAULT_LIST_MAX_ENTRIES));
|
|
385
|
+
const ignore = [...listPolicy?.ignore ?? DEFAULT_LIST_IGNORE, ...opts?.ignore ?? []];
|
|
386
|
+
const insideCwd = isInside(root, jailRoot);
|
|
387
|
+
if (!await this.isDirectory(root)) throw new Error(`"${inputPath}" is not a directory in the workspace.`);
|
|
388
|
+
const entries = [];
|
|
389
|
+
let level = [root];
|
|
390
|
+
let truncated = false;
|
|
391
|
+
for (let currentDepth = 1; currentDepth <= depth && level.length > 0; currentDepth++) {
|
|
392
|
+
const nextLevel = [];
|
|
393
|
+
for (const directory of level) {
|
|
394
|
+
let children;
|
|
395
|
+
try {
|
|
396
|
+
children = await this.backend.list(directory);
|
|
397
|
+
} catch {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
children.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
401
|
+
for (const child of children) {
|
|
402
|
+
const relativeToJail = insideCwd ? path.relative(jailRoot, child).split(path.sep).join("/") : child;
|
|
403
|
+
if (insideCwd && (this.isDenied(relativeToJail) || matchesAnyGlob(relativeToJail, ignore))) continue;
|
|
404
|
+
const relativeToRoot = path.relative(root, child).split(path.sep).join("/");
|
|
405
|
+
if (!insideCwd && matchesAnyGlob(relativeToRoot, ignore)) continue;
|
|
406
|
+
if (entries.length >= maxEntries) {
|
|
407
|
+
truncated = true;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
const described = await this.describe(child);
|
|
411
|
+
entries.push({
|
|
412
|
+
path: relativeToJail,
|
|
413
|
+
type: described.type,
|
|
414
|
+
...described.size !== void 0 ? { size: described.size } : {},
|
|
415
|
+
depth: currentDepth
|
|
416
|
+
});
|
|
417
|
+
if (described.type === "directory") nextLevel.push(child);
|
|
418
|
+
}
|
|
419
|
+
if (truncated) break;
|
|
420
|
+
}
|
|
421
|
+
if (truncated) break;
|
|
422
|
+
level = nextLevel;
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
path: insideCwd ? rootRelative || "." : root,
|
|
426
|
+
entries,
|
|
427
|
+
truncated
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/** Kind and size of a listed entry; symlinks are reported, never followed. */
|
|
431
|
+
async describe(absPath) {
|
|
432
|
+
if (this.backend.stat) try {
|
|
433
|
+
const stats = await this.backend.stat(absPath);
|
|
434
|
+
return stats.type === "file" ? {
|
|
435
|
+
type: "file",
|
|
436
|
+
size: stats.size
|
|
437
|
+
} : { type: stats.type };
|
|
438
|
+
} catch {
|
|
439
|
+
return { type: "other" };
|
|
440
|
+
}
|
|
441
|
+
if (await this.isDirectory(absPath)) try {
|
|
442
|
+
await resolveInJail(this.policy, absPath);
|
|
443
|
+
return { type: "directory" };
|
|
444
|
+
} catch {
|
|
445
|
+
return { type: "other" };
|
|
446
|
+
}
|
|
447
|
+
return { type: "file" };
|
|
448
|
+
}
|
|
308
449
|
async grep(pattern, opts) {
|
|
309
450
|
if (!isSafeGrepPattern(pattern)) throw new WorkspacePolicyError(`Grep pattern is too long or too likely to cause catastrophic regex backtracking: ${pattern}`, {
|
|
310
451
|
type: "unsafe-pattern",
|
|
@@ -328,7 +469,7 @@ var Ops = class {
|
|
|
328
469
|
}
|
|
329
470
|
const lines = content.split("\n");
|
|
330
471
|
for (let index = 0; index < lines.length; index++) {
|
|
331
|
-
const line = lines[index];
|
|
472
|
+
const line = lines[index] ?? "";
|
|
332
473
|
if (line.length > MAX_GREP_LINE_SCAN_LENGTH) continue;
|
|
333
474
|
if (regex.test(line)) {
|
|
334
475
|
matches.push({
|
|
@@ -375,17 +516,40 @@ var Ops = class {
|
|
|
375
516
|
/** Whether a `/`-separated relative path hits the policy deny list. */
|
|
376
517
|
isDenied(relativePath) {
|
|
377
518
|
const denyPaths = this.policy.denyPaths;
|
|
378
|
-
|
|
379
|
-
return denyPaths.some((glob) => {
|
|
380
|
-
if (globToRegExp(glob).test(relativePath)) return true;
|
|
381
|
-
if (!glob.includes("*") && !glob.includes("?")) {
|
|
382
|
-
const prefix = glob.endsWith("/") ? glob : `${glob}/`;
|
|
383
|
-
return relativePath.startsWith(prefix);
|
|
384
|
-
}
|
|
385
|
-
return false;
|
|
386
|
-
});
|
|
519
|
+
return denyPaths !== void 0 && denyPaths.length > 0 && matchesAnyGlob(relativePath, denyPaths);
|
|
387
520
|
}
|
|
388
521
|
};
|
|
522
|
+
/** The lower of two optional limits (either may be absent). */
|
|
523
|
+
function lowerOf(policyLimit, requested) {
|
|
524
|
+
if (requested === void 0 || requested <= 0) return policyLimit;
|
|
525
|
+
return policyLimit === void 0 ? requested : Math.min(policyLimit, requested);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Apply one exact-string replacement to `content` under the edit rules:
|
|
529
|
+
* `oldString` must occur, and exactly once unless `replaceAll`.
|
|
530
|
+
*/
|
|
531
|
+
function applyReplacement(content, edit, inputPath, shownPath, editIndex) {
|
|
532
|
+
const occurrences = countOccurrences(content, edit.oldString);
|
|
533
|
+
const which = editIndex === void 0 ? "" : `Edit ${editIndex + 1}: `;
|
|
534
|
+
const nothingApplied = editIndex === void 0 ? "" : " None of the edits were applied.";
|
|
535
|
+
const indexOption = editIndex === void 0 ? {} : { editIndex };
|
|
536
|
+
if (occurrences === 0) throw new WorkspaceEditError(`${which}The text to replace was not found in "${inputPath}".${nothingApplied}`, {
|
|
537
|
+
type: "not-found",
|
|
538
|
+
path: shownPath,
|
|
539
|
+
matches: 0,
|
|
540
|
+
...indexOption
|
|
541
|
+
});
|
|
542
|
+
if (occurrences > 1 && !edit.replaceAll) throw new WorkspaceEditError(`${which}The text to replace is not unique in "${inputPath}" (${occurrences} matches); pass replaceAll or include more surrounding context.${nothingApplied}`, {
|
|
543
|
+
type: "not-unique",
|
|
544
|
+
path: shownPath,
|
|
545
|
+
matches: occurrences,
|
|
546
|
+
...indexOption
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
content: edit.replaceAll ? content.split(edit.oldString).join(edit.newString) : replaceFirst(content, edit.oldString, edit.newString),
|
|
550
|
+
replacements: edit.replaceAll ? occurrences : 1
|
|
551
|
+
};
|
|
552
|
+
}
|
|
389
553
|
/** Count non-overlapping occurrences of `needle` in `haystack`. */
|
|
390
554
|
function countOccurrences(haystack, needle) {
|
|
391
555
|
if (needle === "") return 0;
|
|
@@ -417,13 +581,15 @@ function replaceFirst(haystack, needle, replacement) {
|
|
|
417
581
|
*
|
|
418
582
|
* @param backend - The dumb IO executor (local disk or in-memory mock).
|
|
419
583
|
* @param policy - The policy that bounds every operation.
|
|
584
|
+
* @param runtime - State shared with other views of the same workspace
|
|
585
|
+
* (its background commands); a fresh one when omitted.
|
|
420
586
|
*
|
|
421
587
|
* @example
|
|
422
588
|
* const ops = createOps(localBackend, { cwd: "/srv/api", shell: { allow: ["npm"] } });
|
|
423
589
|
* const { content, hash } = await ops.readFile("src/index.ts");
|
|
424
590
|
*/
|
|
425
|
-
function createOps(backend, policy) {
|
|
426
|
-
return new Ops(backend, policy);
|
|
591
|
+
function createOps(backend, policy, runtime) {
|
|
592
|
+
return new Ops(backend, policy, runtime ?? { jobs: new ShellJobs() });
|
|
427
593
|
}
|
|
428
594
|
|
|
429
595
|
//#endregion
|