@warlock.js/ai-workspace 5.31.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 +6 -0
- package/cjs/index.cjs +34 -22
- 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 +1 -1
- package/esm/backends/local.mjs.map +1 -1
- package/esm/backends/mock.d.mts +5 -6
- package/esm/backends/mock.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/tool-io.type.d.mts +37 -38
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-backend.contract.d.mts +7 -9
- package/esm/contracts/workspace-backend.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +1 -3
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace-policy.type.d.mts +10 -11
- package/esm/contracts/workspace-policy.type.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +2 -4
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +35 -23
- package/esm/errors.d.mts.map +1 -1
- package/esm/index.d.mts +1 -0
- package/esm/ops.d.mts +3 -4
- package/esm/ops.d.mts.map +1 -1
- package/esm/policy/policy.d.mts +5 -5
- package/esm/policy/policy.d.mts.map +1 -1
- package/esm/policy/policy.mjs +4 -3
- package/esm/policy/policy.mjs.map +1 -1
- package/esm/policy/shell-check.d.mts +5 -6
- package/esm/policy/shell-check.d.mts.map +1 -1
- package/esm/policy/shell-check.mjs +24 -13
- package/esm/policy/shell-check.mjs.map +1 -1
- package/esm/policy/shell-parser.mjs +2 -2
- package/esm/policy/shell-parser.mjs.map +1 -1
- package/esm/policy/tokenize-command.d.mts +3 -4
- package/esm/policy/tokenize-command.d.mts.map +1 -1
- package/esm/policy/tokenize-command.mjs +2 -2
- package/esm/policy/tokenize-command.mjs.map +1 -1
- package/esm/shell-jobs.d.mts +3 -3
- package/esm/shell-jobs.d.mts.map +1 -1
- package/esm/shell-jobs.mjs +1 -1
- package/esm/todos.d.mts +2 -2
- package/esm/todos.d.mts.map +1 -1
- package/esm/todos.mjs +1 -1
- package/esm/tools/edit-file.d.mts +2 -3
- package/esm/tools/edit-file.d.mts.map +1 -1
- package/esm/tools/glob.d.mts +3 -4
- package/esm/tools/glob.d.mts.map +1 -1
- package/esm/tools/grep.d.mts +3 -4
- package/esm/tools/grep.d.mts.map +1 -1
- package/esm/tools/list-dir.d.mts +2 -3
- package/esm/tools/list-dir.d.mts.map +1 -1
- package/esm/tools/multi-edit.d.mts +2 -3
- package/esm/tools/multi-edit.d.mts.map +1 -1
- package/esm/tools/read-file.d.mts +2 -3
- package/esm/tools/read-file.d.mts.map +1 -1
- package/esm/tools/run-shell.d.mts +5 -6
- package/esm/tools/run-shell.d.mts.map +1 -1
- package/esm/tools/run-tests.d.mts +3 -4
- package/esm/tools/run-tests.d.mts.map +1 -1
- package/esm/tools/shell-kill.d.mts +2 -3
- package/esm/tools/shell-kill.d.mts.map +1 -1
- package/esm/tools/shell-output.d.mts +2 -3
- package/esm/tools/shell-output.d.mts.map +1 -1
- package/esm/tools/todo.d.mts +3 -4
- package/esm/tools/todo.d.mts.map +1 -1
- package/esm/tools/write-file.d.mts +2 -3
- package/esm/tools/write-file.d.mts.map +1 -1
- package/esm/workspace.d.mts +3 -3
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +1 -1
- package/esm/workspace.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -7,7 +7,7 @@
|
|
|
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
11
|
/**
|
|
12
12
|
* How `run_shell` / `exec()` interpret a command line.
|
|
13
13
|
*
|
|
@@ -23,7 +23,7 @@ type WorkspaceBackendType = "local" | "mock";
|
|
|
23
23
|
* redirection target and `cd` directory against the path jail. Syntax the
|
|
24
24
|
* parser cannot read with confidence is refused, never guessed at.
|
|
25
25
|
*/
|
|
26
|
-
type WorkspaceShellInterpreter = "none" | "sh";
|
|
26
|
+
export type WorkspaceShellInterpreter = "none" | "sh";
|
|
27
27
|
/**
|
|
28
28
|
* A command pattern in `shell.denyPatterns`.
|
|
29
29
|
*
|
|
@@ -36,9 +36,9 @@ type WorkspaceShellInterpreter = "none" | "sh";
|
|
|
36
36
|
* is tested as `"git push"`). Use it for option orders and spellings:
|
|
37
37
|
* `/^git push\b.*(--force|-f\b)/`. A `g`/`y` flag is ignored.
|
|
38
38
|
*/
|
|
39
|
-
type WorkspaceCommandPattern = string | RegExp;
|
|
39
|
+
export type WorkspaceCommandPattern = string | RegExp;
|
|
40
40
|
/** What {@link WorkspaceAllowCommand} is told about the command it checks. */
|
|
41
|
-
interface WorkspaceCommandCheckContext {
|
|
41
|
+
export interface WorkspaceCommandCheckContext {
|
|
42
42
|
/** The full command line the checked command is part of. */
|
|
43
43
|
command: string;
|
|
44
44
|
/** The interpreter the line runs under. */
|
|
@@ -61,7 +61,7 @@ interface WorkspaceCommandCheckContext {
|
|
|
61
61
|
* allowCommand: (argv) =>
|
|
62
62
|
* argv[0] === "git" && argv[1] === "push" ? "Pushing is done by the release job." : true
|
|
63
63
|
*/
|
|
64
|
-
type WorkspaceAllowCommand = (argv: readonly string[], context: WorkspaceCommandCheckContext) => boolean | string;
|
|
64
|
+
export type WorkspaceAllowCommand = (argv: readonly string[], context: WorkspaceCommandCheckContext) => boolean | string;
|
|
65
65
|
/**
|
|
66
66
|
* Background-command sub-policy. Present ⇒ `run_shell({ background: true })`
|
|
67
67
|
* and `startShell()` may start long-running commands (dev servers,
|
|
@@ -69,7 +69,7 @@ type WorkspaceAllowCommand = (argv: readonly string[], context: WorkspaceCommand
|
|
|
69
69
|
* refused. Every background command is killed — its whole process tree —
|
|
70
70
|
* by `workspace.dispose()`, and when the Node process exits.
|
|
71
71
|
*/
|
|
72
|
-
interface WorkspaceBackgroundPolicy {
|
|
72
|
+
export interface WorkspaceBackgroundPolicy {
|
|
73
73
|
/** Most background commands running at once. Default `4`. */
|
|
74
74
|
maxJobs?: number;
|
|
75
75
|
/**
|
|
@@ -103,7 +103,7 @@ interface WorkspaceBackgroundPolicy {
|
|
|
103
103
|
* drops the variables that change how a shell parses or starts (`ENV`,
|
|
104
104
|
* `BASH_ENV`, `SHELLOPTS`, `BASHOPTS`, `CDPATH`, `IFS`, `PS4`, `BASH_FUNC_*`).
|
|
105
105
|
*/
|
|
106
|
-
interface WorkspaceShellPolicy {
|
|
106
|
+
export interface WorkspaceShellPolicy {
|
|
107
107
|
/** Allowed executable basenames (e.g. `["npm", "node"]`). */
|
|
108
108
|
allow?: string[];
|
|
109
109
|
/** Denied executable basenames — wins over `allow`. */
|
|
@@ -150,7 +150,7 @@ interface WorkspaceShellPolicy {
|
|
|
150
150
|
* Read sub-policy controlling how much of a file a single read returns.
|
|
151
151
|
* Applies equally to the `read_file` tool and the `readFile()` direct method.
|
|
152
152
|
*/
|
|
153
|
-
interface WorkspaceReadPolicy {
|
|
153
|
+
export interface WorkspaceReadPolicy {
|
|
154
154
|
/**
|
|
155
155
|
* Hard cap on the UTF-8 bytes of the `content` a single read returns,
|
|
156
156
|
* line-number prefixes and newlines included. A window that would exceed
|
|
@@ -167,7 +167,7 @@ interface WorkspaceReadPolicy {
|
|
|
167
167
|
* `denyPaths` are always hidden from listings; `ignore` hides more without
|
|
168
168
|
* blocking access (an ignored file can still be read).
|
|
169
169
|
*/
|
|
170
|
-
interface WorkspaceListPolicy {
|
|
170
|
+
export interface WorkspaceListPolicy {
|
|
171
171
|
/**
|
|
172
172
|
* Globs (`*`, `**`, `?`; a bare name also hides its contents) left out of
|
|
173
173
|
* listings. Default `["node_modules", ".git"]`; setting it replaces the
|
|
@@ -198,7 +198,7 @@ interface WorkspaceListPolicy {
|
|
|
198
198
|
* backend: "local",
|
|
199
199
|
* };
|
|
200
200
|
*/
|
|
201
|
-
interface WorkspacePolicy {
|
|
201
|
+
export interface WorkspacePolicy {
|
|
202
202
|
/** Absolute jail root — the workspace cannot read or write outside it. */
|
|
203
203
|
cwd: string;
|
|
204
204
|
/** Extra readable roots outside `cwd` (resolved absolute paths). */
|
|
@@ -215,5 +215,4 @@ interface WorkspacePolicy {
|
|
|
215
215
|
backend?: WorkspaceBackendType;
|
|
216
216
|
}
|
|
217
217
|
//#endregion
|
|
218
|
-
export { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy, WorkspaceCommandCheckContext, WorkspaceCommandPattern, WorkspaceListPolicy, WorkspacePolicy, WorkspaceReadPolicy, WorkspaceShellInterpreter, WorkspaceShellPolicy };
|
|
219
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":"
|
|
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
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,7 +12,7 @@ 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 {
|
|
15
|
+
export interface WorkspaceTools {
|
|
17
16
|
/**
|
|
18
17
|
* Every tool this workspace vends, as `ToolContract[]`, ready for
|
|
19
18
|
* `ai.agent({ tools })`. `shell_output` / `shell_kill` are included only
|
|
@@ -98,7 +97,7 @@ interface WorkspaceTools {
|
|
|
98
97
|
* await ws.writeFile("src/routes.ts", BOILERPLATE);
|
|
99
98
|
* const files = await ws.glob("src/models/**\/*.ts");
|
|
100
99
|
*/
|
|
101
|
-
interface Workspace {
|
|
100
|
+
export interface Workspace {
|
|
102
101
|
/** The immutable policy bounding this workspace. */
|
|
103
102
|
readonly policy: WorkspacePolicy;
|
|
104
103
|
/** Agent-facing tool factories. */
|
|
@@ -180,5 +179,4 @@ interface Workspace {
|
|
|
180
179
|
scope(subdir: string): Workspace;
|
|
181
180
|
}
|
|
182
181
|
//#endregion
|
|
183
|
-
export { Workspace, WorkspaceTools };
|
|
184
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":"
|
|
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,5 +1,4 @@
|
|
|
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.
|
|
@@ -14,7 +13,7 @@ import { AIError, AIErrorOptions } from "@warlock.js/ai";
|
|
|
14
13
|
* catastrophic-backtracking shape (nested quantifiers) that could hang
|
|
15
14
|
* the process (ReDoS).
|
|
16
15
|
*/
|
|
17
|
-
type WorkspacePolicyViolation = "path-escape" | "denied-command" | "unsafe-pattern";
|
|
16
|
+
export type WorkspacePolicyViolation = "path-escape" | "denied-command" | "unsafe-pattern";
|
|
18
17
|
/**
|
|
19
18
|
* Why the shell policy refused a command. Set on every
|
|
20
19
|
* {@link WorkspacePolicyError} of type `"denied-command"` raised for a
|
|
@@ -40,18 +39,24 @@ type WorkspacePolicyViolation = "path-escape" | "denied-command" | "unsafe-patte
|
|
|
40
39
|
* - `"path-escape"` — shell mode: a redirection target or `cd` directory
|
|
41
40
|
* resolves outside the workspace jail or hits `denyPaths`.
|
|
42
41
|
*/
|
|
43
|
-
type WorkspaceShellRefusalReason = "metacharacter" | "unbalanced-quotes" | "denied-executable" | "not-allowed" | "denied-pattern" | "nested-command" | "unsupported-syntax" | "path-escape";
|
|
42
|
+
export type WorkspaceShellRefusalReason = "metacharacter" | "unbalanced-quotes" | "denied-executable" | "not-allowed" | "denied-pattern" | "nested-command" | "unsupported-syntax" | "path-escape";
|
|
44
43
|
/**
|
|
45
44
|
* Options for {@link WorkspacePolicyError} — the structured `type`
|
|
46
45
|
* discriminator plus, where relevant, the offending path or command for
|
|
47
46
|
* branchable diagnostics without parsing the message.
|
|
48
47
|
*/
|
|
49
|
-
type WorkspacePolicyErrorOptions = AIErrorOptions & {
|
|
50
|
-
/** Which policy rule was violated. */
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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"`). */
|
|
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. */
|
|
55
60
|
executable?: string;
|
|
56
61
|
};
|
|
57
62
|
/**
|
|
@@ -71,7 +76,7 @@ type WorkspacePolicyErrorOptions = AIErrorOptions & {
|
|
|
71
76
|
* console.warn(`Blocked command: ${error.command}`);
|
|
72
77
|
* }
|
|
73
78
|
*/
|
|
74
|
-
declare class WorkspacePolicyError extends AIError {
|
|
79
|
+
export declare class WorkspacePolicyError extends AIError {
|
|
75
80
|
/** Which policy rule was violated. */
|
|
76
81
|
readonly type: WorkspacePolicyViolation;
|
|
77
82
|
/** The offending path, when the violation was a path escape. */
|
|
@@ -99,18 +104,24 @@ declare class WorkspacePolicyError extends AIError {
|
|
|
99
104
|
* - `"stale-hash"` — the file's current hash did not match the supplied
|
|
100
105
|
* `expectHash`; the file changed since it was read.
|
|
101
106
|
*/
|
|
102
|
-
type WorkspaceEditFailure = "not-found" | "not-unique" | "stale-hash";
|
|
107
|
+
export type WorkspaceEditFailure = "not-found" | "not-unique" | "stale-hash";
|
|
103
108
|
/**
|
|
104
109
|
* Options for {@link WorkspaceEditError} — the structured `type`
|
|
105
110
|
* discriminator plus optional match-count / hash context for the
|
|
106
111
|
* `"not-unique"` and `"stale-hash"` cases.
|
|
107
112
|
*/
|
|
108
|
-
type WorkspaceEditErrorOptions = AIErrorOptions & {
|
|
109
|
-
/** Why the edit was rejected. */
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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"`). */
|
|
123
|
+
actualHash?: string;
|
|
124
|
+
/** 0-based index of the failing edit, for a `multiEdit` rejection. */
|
|
114
125
|
editIndex?: number;
|
|
115
126
|
};
|
|
116
127
|
/**
|
|
@@ -127,7 +138,7 @@ type WorkspaceEditErrorOptions = AIErrorOptions & {
|
|
|
127
138
|
* // re-read the file and retry the edit with the fresh hash
|
|
128
139
|
* }
|
|
129
140
|
*/
|
|
130
|
-
declare class WorkspaceEditError extends AIError {
|
|
141
|
+
export declare class WorkspaceEditError extends AIError {
|
|
131
142
|
/** Why the edit was rejected. */
|
|
132
143
|
readonly type: WorkspaceEditFailure;
|
|
133
144
|
/** The edited file's workspace-relative path. */
|
|
@@ -152,10 +163,12 @@ declare class WorkspaceEditError extends AIError {
|
|
|
152
163
|
* workspace (it never existed, or it finished long enough ago to be
|
|
153
164
|
* pruned).
|
|
154
165
|
*/
|
|
155
|
-
type WorkspaceJobFailure = "not-found";
|
|
166
|
+
export type WorkspaceJobFailure = "not-found";
|
|
156
167
|
/** Options for {@link WorkspaceJobError}. */
|
|
157
|
-
type WorkspaceJobErrorOptions = AIErrorOptions & {
|
|
158
|
-
/** Why the operation failed. */
|
|
168
|
+
export type WorkspaceJobErrorOptions = AIErrorOptions & {
|
|
169
|
+
/** Why the operation failed. */
|
|
170
|
+
type: WorkspaceJobFailure;
|
|
171
|
+
/** The background command id the caller asked for. */
|
|
159
172
|
jobId: string;
|
|
160
173
|
};
|
|
161
174
|
/**
|
|
@@ -168,7 +181,7 @@ type WorkspaceJobErrorOptions = AIErrorOptions & {
|
|
|
168
181
|
* console.warn(`No background command ${error.jobId}`);
|
|
169
182
|
* }
|
|
170
183
|
*/
|
|
171
|
-
declare class WorkspaceJobError extends AIError {
|
|
184
|
+
export declare class WorkspaceJobError extends AIError {
|
|
172
185
|
/** Why the operation failed. */
|
|
173
186
|
readonly type: WorkspaceJobFailure;
|
|
174
187
|
/** The background command id the caller asked for. */
|
|
@@ -176,5 +189,4 @@ declare class WorkspaceJobError extends AIError {
|
|
|
176
189
|
constructor(message: string, options: WorkspaceJobErrorOptions);
|
|
177
190
|
}
|
|
178
191
|
//#endregion
|
|
179
|
-
export { WorkspaceEditError, WorkspaceEditErrorOptions, WorkspaceEditFailure, WorkspaceJobError, WorkspaceJobErrorOptions, WorkspaceJobFailure, WorkspacePolicyError, WorkspacePolicyErrorOptions, WorkspacePolicyViolation, WorkspaceShellRefusalReason };
|
|
180
192
|
//# sourceMappingURL=errors.d.mts.map
|
package/esm/errors.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/errors.ts"],"mappings":"
|
|
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/index.d.mts
CHANGED
|
@@ -3,6 +3,7 @@ import { WorkspaceAllowCommand, WorkspaceBackendType, WorkspaceBackgroundPolicy,
|
|
|
3
3
|
import { WorkspaceBackend, WorkspaceBackendExecOptions, WorkspaceBackendExecResult, WorkspaceBackendProcess, WorkspaceBackendStartOptions, WorkspaceBackendStat, WorkspaceOutputStream } from "./contracts/workspace-backend.contract.mjs";
|
|
4
4
|
import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
|
|
5
5
|
import { Workspace, WorkspaceTools } from "./contracts/workspace.contract.mjs";
|
|
6
|
+
import "./contracts/index.mjs";
|
|
6
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";
|
|
8
9
|
import { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand } from "./policy/shell-check.mjs";
|
package/esm/ops.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { WorkspacePolicy } from "./contracts/workspace-policy.type.mjs";
|
|
2
2
|
import { WorkspaceBackend } from "./contracts/workspace-backend.contract.mjs";
|
|
3
3
|
import { WorkspaceOps } from "./contracts/workspace-ops.contract.mjs";
|
|
4
|
+
import "./contracts/index.mjs";
|
|
4
5
|
import { ShellJobs } from "./shell-jobs.mjs";
|
|
5
|
-
|
|
6
6
|
//#region ../../packages/ai-workspace/src/ops.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* The internal, single-instance implementation of {@link WorkspaceOps}.
|
|
@@ -15,7 +15,7 @@ import { ShellJobs } from "./shell-jobs.mjs";
|
|
|
15
15
|
* State an ops layer shares with the other projections of the same
|
|
16
16
|
* workspace (`readonly()`, `scope()`): the background commands it owns.
|
|
17
17
|
*/
|
|
18
|
-
interface OpsRuntime {
|
|
18
|
+
export interface OpsRuntime {
|
|
19
19
|
/** The workspace's background commands. */
|
|
20
20
|
jobs: ShellJobs;
|
|
21
21
|
}
|
|
@@ -38,7 +38,6 @@ interface OpsRuntime {
|
|
|
38
38
|
* const ops = createOps(localBackend, { cwd: "/srv/api", shell: { allow: ["npm"] } });
|
|
39
39
|
* const { content, hash } = await ops.readFile("src/index.ts");
|
|
40
40
|
*/
|
|
41
|
-
declare function createOps(backend: WorkspaceBackend, policy: WorkspacePolicy, runtime?: OpsRuntime): WorkspaceOps;
|
|
41
|
+
export declare function createOps(backend: WorkspaceBackend, policy: WorkspacePolicy, runtime?: OpsRuntime): WorkspaceOps;
|
|
42
42
|
//#endregion
|
|
43
|
-
export { createOps };
|
|
44
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/policy/policy.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WorkspacePolicy } from "../contracts/workspace-policy.type.mjs";
|
|
2
|
+
import "../contracts/index.mjs";
|
|
2
3
|
//#region ../../packages/ai-workspace/src/policy/policy.d.ts
|
|
3
4
|
/**
|
|
4
5
|
* The outcome of resolving a workspace-relative (or absolute) input path
|
|
@@ -6,7 +7,7 @@ import { WorkspacePolicy } from "../contracts/workspace-policy.type.mjs";
|
|
|
6
7
|
* touch, plus the workspace-relative form (POSIX-style, `/`-separated)
|
|
7
8
|
* the agent and tool results echo back.
|
|
8
9
|
*/
|
|
9
|
-
interface ResolvedPath {
|
|
10
|
+
export interface ResolvedPath {
|
|
10
11
|
/** Canonical absolute path (symlinks in existing ancestors collapsed). */
|
|
11
12
|
absolutePath: string;
|
|
12
13
|
/**
|
|
@@ -45,7 +46,7 @@ interface ResolvedPath {
|
|
|
45
46
|
* @example
|
|
46
47
|
* const { absolutePath } = await resolveInJail(policy, "src/index.ts");
|
|
47
48
|
*/
|
|
48
|
-
declare function resolveInJail(policy: WorkspacePolicy, inputPath: string): Promise<ResolvedPath>;
|
|
49
|
+
export declare function resolveInJail(policy: WorkspacePolicy, inputPath: string): Promise<ResolvedPath>;
|
|
49
50
|
/**
|
|
50
51
|
* Whether a command line is permitted by the policy's `shell` sub-policy —
|
|
51
52
|
* `checkShellCommand(policy, command).allowed`.
|
|
@@ -69,7 +70,7 @@ declare function resolveInJail(policy: WorkspacePolicy, inputPath: string): Prom
|
|
|
69
70
|
* isCommandAllowed({ cwd, shell: { allow: ["npm"] } }, "rm -rf /"); // false
|
|
70
71
|
* isCommandAllowed({ cwd, shell: { allow: ["npm"] } }, "npm test; rm -rf /"); // false
|
|
71
72
|
*/
|
|
72
|
-
declare function isCommandAllowed(policy: WorkspacePolicy, command: string): boolean;
|
|
73
|
+
export declare function isCommandAllowed(policy: WorkspacePolicy, command: string): boolean;
|
|
73
74
|
/**
|
|
74
75
|
* Build the exact environment a spawned process receives — `process.env`
|
|
75
76
|
* is **never** inherited wholesale. The result is
|
|
@@ -82,7 +83,6 @@ declare function isCommandAllowed(policy: WorkspacePolicy, command: string): boo
|
|
|
82
83
|
* buildEnv({ cwd, shell: { inheritEnv: ["PATH"], env: { CI: "1" } } });
|
|
83
84
|
* // → { PATH: <process PATH>, CI: "1" }
|
|
84
85
|
*/
|
|
85
|
-
declare function buildEnv(policy: WorkspacePolicy): Record<string, string>;
|
|
86
|
+
export declare function buildEnv(policy: WorkspacePolicy): Record<string, string>;
|
|
86
87
|
//#endregion
|
|
87
|
-
export { ResolvedPath, buildEnv, isCommandAllowed, resolveInJail };
|
|
88
88
|
//# sourceMappingURL=policy.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"mappings":";;;;;;;;;iBAaiB;;EAEf;;;;;;EAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAyEoB,cACpB,QAAQ,iBACR,oBACC,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;wBAqEK,iBAAiB,QAAQ,iBAAiB;;;;;;;;;;;;;wBAgB1C,SAAS,QAAQ,kBAAkB"}
|
package/esm/policy/policy.mjs
CHANGED
|
@@ -61,8 +61,9 @@ async function resolveInJail(policy, inputPath) {
|
|
|
61
61
|
const insideCwd = isInside(absolutePath, jailRoot);
|
|
62
62
|
const allowRoots = policy.allowPaths ?? [];
|
|
63
63
|
let insideAllow = false;
|
|
64
|
-
if (!insideCwd) {
|
|
65
|
-
|
|
64
|
+
if (!insideCwd) for (const root of allowRoots) {
|
|
65
|
+
const canonicalRoot = await canonicalize(root);
|
|
66
|
+
if (isInside(absolutePath, canonicalRoot)) {
|
|
66
67
|
insideAllow = true;
|
|
67
68
|
break;
|
|
68
69
|
}
|
|
@@ -139,7 +140,7 @@ function buildEnv(policy) {
|
|
|
139
140
|
* `PS4` can run command substitutions, `BASH_FUNC_*` imports functions that
|
|
140
141
|
* shadow commands, `CDPATH` moves `cd`, `IFS` re-splits words).
|
|
141
142
|
*/
|
|
142
|
-
const SHELL_STARTUP_VARIABLES = new Set([
|
|
143
|
+
const SHELL_STARTUP_VARIABLES = /* @__PURE__ */ new Set([
|
|
143
144
|
"ENV",
|
|
144
145
|
"BASH_ENV",
|
|
145
146
|
"SHELLOPTS",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"sourcesContent":["import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type { WorkspacePolicy } from \"../contracts\";\nimport { isInside, matchesAnyGlob } from \"./jail\";\nimport { checkShellCommand } from \"./shell-check\";\n\n/**\n * The outcome of resolving a workspace-relative (or absolute) input path\n * against the jail — the canonical absolute location the backend should\n * touch, plus the workspace-relative form (POSIX-style, `/`-separated)\n * the agent and tool results echo back.\n */\nexport interface ResolvedPath {\n /** Canonical absolute path (symlinks in existing ancestors collapsed). */\n absolutePath: string;\n /**\n * The path relative to `policy.cwd`, `/`-separated regardless of OS,\n * used in tool results so the agent always sees stable workspace paths.\n * Empty string when the resolved path IS the jail root.\n */\n relativePath: string;\n}\n\n/**\n * Resolve the canonical absolute form of `target`, collapsing any\n * symlinks. `target` may not yet exist (a fresh `writeFile`/`mkdir`),\n * so we realpath the deepest **existing** ancestor and re-attach the\n * non-existent tail — a symlinked ancestor still cannot smuggle the\n * path out of the jail, while genuinely new leaves stay creatable.\n */\nasync function canonicalize(target: string): Promise<string> {\n let resolvedTarget = path.resolve(target);\n const tail: string[] = [];\n\n // Walk up until an existing ancestor is found (or we hit the root).\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n const real = await realpath(resolvedTarget);\n\n return tail.length > 0 ? path.join(real, ...tail) : real;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n\n // Only ENOENT means \"this segment doesn't exist yet\" — keep\n // walking up. Any other error (EACCES, ELOOP, …) is a real fault.\n if (code !== \"ENOENT\") {\n throw error;\n }\n\n const parent = path.dirname(resolvedTarget);\n\n // Reached the filesystem root without finding an existing\n // ancestor — give back the lexically-resolved path unchanged.\n if (parent === resolvedTarget) {\n return path.join(resolvedTarget, ...tail);\n }\n\n tail.unshift(path.basename(resolvedTarget));\n resolvedTarget = parent;\n }\n }\n}\n\n/**\n * Resolve and jail a single input path against a {@link WorkspacePolicy}.\n *\n * The input is resolved against `policy.cwd`, canonicalized (symlinks in\n * existing ancestors collapsed so a symlinked directory cannot escape\n * the jail), then accepted **only** when it sits under `cwd` or one of\n * the `allowPaths` roots. A path that escapes, or that matches any\n * `denyPaths` glob even while inside `cwd`, is rejected with a\n * {@link WorkspacePolicyError} of type `\"path-escape\"`.\n *\n * Why this does not use `@warlock.js/fs`'s confined reader (`fs.confined`):\n * the two answer different questions. The confined reader refuses absolute\n * paths, refuses every symbolic link (or follows only links inside its one\n * root), requires the path to exist, and reads the file itself through a\n * no-follow, identity-checked open. This jail must instead accept absolute\n * paths inside `cwd`, several `allowPaths` roots, `denyPaths` globs, a\n * symlinked `cwd`, links whose target stays inside, and paths that do not\n * exist yet (`writeFile`, `mkdir`), and it returns a path that writes,\n * `exec`'s working directory and redirections use afterwards. Switching would\n * change what agents may touch, so the workspace keeps this resolver.\n *\n * @param policy - The bounding policy (its `cwd` is the jail root).\n * @param inputPath - A workspace-relative or absolute path to resolve.\n * @returns The canonical absolute path plus its `/`-separated relative form.\n * @throws {WorkspacePolicyError} When the path escapes the jail or hits a deny glob.\n *\n * @example\n * const { absolutePath } = await resolveInJail(policy, \"src/index.ts\");\n */\nexport async function resolveInJail(\n policy: WorkspacePolicy,\n inputPath: string,\n): Promise<ResolvedPath> {\n const jailRoot = await canonicalize(policy.cwd);\n const requested = path.isAbsolute(inputPath) ? inputPath : path.join(policy.cwd, inputPath);\n const absolutePath = await canonicalize(requested);\n\n const insideCwd = isInside(absolutePath, jailRoot);\n const allowRoots = policy.allowPaths ?? [];\n let insideAllow = false;\n\n if (!insideCwd) {\n for (const root of allowRoots) {\n const canonicalRoot = await canonicalize(root);\n\n if (isInside(absolutePath, canonicalRoot)) {\n insideAllow = true;\n\n break;\n }\n }\n }\n\n if (!insideCwd && !insideAllow) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" resolves outside the workspace jail.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n\n // `denyPaths` is evaluated relative to the jail root and wins even\n // when the path is comfortably inside `cwd`.\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, absolutePath).split(path.sep).join(\"/\")\n : \"\";\n\n if (insideCwd && policy.denyPaths && policy.denyPaths.length > 0) {\n if (matchesAnyGlob(relativeToJail, policy.denyPaths)) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" is blocked by the workspace deny list.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n }\n\n return { absolutePath, relativePath: relativeToJail };\n}\n\n/**\n * Whether a command line is permitted by the policy's `shell` sub-policy —\n * `checkShellCommand(policy, command).allowed`.\n *\n * In argv mode (the default) the command must tokenize into a single argv\n * (no unquoted `;`, `&&`, `|`, backticks, `$`, redirection, …): the backend\n * spawns it with no shell, and unquoted metacharacters were exactly how an\n * injected command chain used to ride past the allowlist. In shell mode\n * (`interpreter: \"sh\"`) every simple command in the line is checked. Each\n * argv's executable basename is matched against `shell.deny` (wins, case\n * insensitive) then `shell.allow` (exhaustive when non-empty), then\n * `shell.denyPatterns` and `shell.allowCommand`. An absent `shell` block\n * means no command may run at all.\n *\n * Returns a plain `boolean`; use {@link checkShellCommand} for the reason\n * and the model-facing message. Redirection targets are checked lexically\n * here; `exec` re-checks them with symlinks resolved.\n *\n * @example\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test\"); // true\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"rm -rf /\"); // false\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test; rm -rf /\"); // false\n */\nexport function isCommandAllowed(policy: WorkspacePolicy, command: string): boolean {\n return checkShellCommand(policy, command).allowed;\n}\n\n/**\n * Build the exact environment a spawned process receives — `process.env`\n * is **never** inherited wholesale. The result is\n * `{ ...pick(process.env, inheritEnv), ...shell.env }`, so a command\n * cannot see `PATH` (and thus often cannot find `node`/`npm`) unless the\n * policy opts in via `shell.inheritEnv: [\"PATH\"]`. Explicit `shell.env`\n * values override inherited ones on key collision.\n *\n * @example\n * buildEnv({ cwd, shell: { inheritEnv: [\"PATH\"], env: { CI: \"1\" } } });\n * // → { PATH: <process PATH>, CI: \"1\" }\n */\nexport function buildEnv(policy: WorkspacePolicy): Record<string, string> {\n const shell = policy.shell;\n const env: Record<string, string> = {};\n\n if (!shell) {\n return env;\n }\n\n for (const key of shell.inheritEnv ?? []) {\n const value = process.env[key];\n\n if (value !== undefined) {\n env[key] = value;\n }\n }\n\n if (shell.env) {\n for (const [key, value] of Object.entries(shell.env)) {\n env[key] = value;\n }\n }\n\n return env;\n}\n\n/**\n * Variables removed from the environment of a shell-mode (`interpreter:\n * \"sh\"`) command: each changes how the shell itself starts, parses or\n * resolves the line (`ENV`/`BASH_ENV` run a startup file, `SHELLOPTS` +\n * `PS4` can run command substitutions, `BASH_FUNC_*` imports functions that\n * shadow commands, `CDPATH` moves `cd`, `IFS` re-splits words).\n */\nconst SHELL_STARTUP_VARIABLES = new Set([\n \"ENV\",\n \"BASH_ENV\",\n \"SHELLOPTS\",\n \"BASHOPTS\",\n \"CDPATH\",\n \"IFS\",\n \"PS4\",\n \"GLOBIGNORE\",\n]);\n\n/**\n * The environment a command actually receives: {@link buildEnv}, minus the\n * shell start-up variables when the policy runs lines through a shell.\n * Compared case-insensitively, because Windows environment names are.\n */\nexport function buildCommandEnv(policy: WorkspacePolicy): Record<string, string> {\n const env = buildEnv(policy);\n\n if ((policy.shell?.interpreter ?? \"none\") !== \"sh\") {\n return env;\n }\n\n for (const key of Object.keys(env)) {\n const upper = key.toUpperCase();\n\n if (SHELL_STARTUP_VARIABLES.has(upper) || upper.startsWith(\"BASH_FUNC_\")) {\n delete env[key];\n }\n }\n\n return env;\n}\n"],"mappings":";;;;;;;;;;;;;;AA+BA,eAAe,aAAa,QAAiC;CAC3D,IAAI,iBAAiB,KAAK,QAAQ,MAAM;CACxC,MAAM,OAAiB,CAAC;CAIxB,OAAO,MACL,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,cAAc;EAE1C,OAAO,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,GAAG,IAAI,IAAI;CACtD,SAAS,OAAO;EAKd,IAJc,MAAgC,SAIjC,UACX,MAAM;EAGR,MAAM,SAAS,KAAK,QAAQ,cAAc;EAI1C,IAAI,WAAW,gBACb,OAAO,KAAK,KAAK,gBAAgB,GAAG,IAAI;EAG1C,KAAK,QAAQ,KAAK,SAAS,cAAc,CAAC;EAC1C,iBAAiB;CACnB;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,eAAsB,cACpB,QACA,WACuB;CACvB,MAAM,WAAW,MAAM,aAAa,OAAO,GAAG;CAE9C,MAAM,eAAe,MAAM,aADT,KAAK,WAAW,SAAS,IAAI,YAAY,KAAK,KAAK,OAAO,KAAK,SAAS,CACzC;CAEjD,MAAM,YAAY,SAAS,cAAc,QAAQ;CACjD,MAAM,aAAa,OAAO,cAAc,CAAC;CACzC,IAAI,cAAc;CAElB,IAAI,CAAC,WACH;OAAK,MAAM,QAAQ,YAGjB,IAAI,SAAS,cAAc,MAFC,aAAa,IAAI,CAEL,GAAG;GACzC,cAAc;GAEd;EACF;CACF;CAGF,IAAI,CAAC,aAAa,CAAC,aACjB,MAAM,IAAI,qBAAqB,SAAS,UAAU,yCAAyC;EACzF,MAAM;EACN,MAAM;CACR,CAAC;CAKH,MAAM,iBAAiB,YACnB,KAAK,SAAS,UAAU,YAAY,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,IAC9D;CAEJ,IAAI,aAAa,OAAO,aAAa,OAAO,UAAU,SAAS,GAC7D;MAAI,eAAe,gBAAgB,OAAO,SAAS,GACjD,MAAM,IAAI,qBAAqB,SAAS,UAAU,2CAA2C;GAC3F,MAAM;GACN,MAAM;EACR,CAAC;CACH;CAGF,OAAO;EAAE;EAAc,cAAc;CAAe;AACtD;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,QAAyB,SAA0B;CAClF,OAAO,kBAAkB,QAAQ,OAAO,CAAC,CAAC;AAC5C;;;;;;;;;;;;;AAcA,SAAgB,SAAS,QAAiD;CACxE,MAAM,QAAQ,OAAO;CACrB,MAAM,MAA8B,CAAC;CAErC,IAAI,CAAC,OACH,OAAO;CAGT,KAAK,MAAM,OAAO,MAAM,cAAc,CAAC,GAAG;EACxC,MAAM,QAAQ,QAAQ,IAAI;EAE1B,IAAI,UAAU,QACZ,IAAI,OAAO;CAEf;CAEA,IAAI,MAAM,KACR,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,GACjD,IAAI,OAAO;CAIf,OAAO;AACT;;;;;;;;AASA,MAAM,0BAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;AAOD,SAAgB,gBAAgB,QAAiD;CAC/E,MAAM,MAAM,SAAS,MAAM;CAE3B,KAAK,OAAO,OAAO,eAAe,YAAY,MAC5C,OAAO;CAGT,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG,GAAG;EAClC,MAAM,QAAQ,IAAI,YAAY;EAE9B,IAAI,wBAAwB,IAAI,KAAK,KAAK,MAAM,WAAW,YAAY,GACrE,OAAO,IAAI;CAEf;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"policy.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/policy.ts"],"sourcesContent":["import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\nimport { WorkspacePolicyError } from \"../errors\";\nimport type { WorkspacePolicy } from \"../contracts\";\nimport { isInside, matchesAnyGlob } from \"./jail\";\nimport { checkShellCommand } from \"./shell-check\";\n\n/**\n * The outcome of resolving a workspace-relative (or absolute) input path\n * against the jail — the canonical absolute location the backend should\n * touch, plus the workspace-relative form (POSIX-style, `/`-separated)\n * the agent and tool results echo back.\n */\nexport interface ResolvedPath {\n /** Canonical absolute path (symlinks in existing ancestors collapsed). */\n absolutePath: string;\n /**\n * The path relative to `policy.cwd`, `/`-separated regardless of OS,\n * used in tool results so the agent always sees stable workspace paths.\n * Empty string when the resolved path IS the jail root.\n */\n relativePath: string;\n}\n\n/**\n * Resolve the canonical absolute form of `target`, collapsing any\n * symlinks. `target` may not yet exist (a fresh `writeFile`/`mkdir`),\n * so we realpath the deepest **existing** ancestor and re-attach the\n * non-existent tail — a symlinked ancestor still cannot smuggle the\n * path out of the jail, while genuinely new leaves stay creatable.\n */\nasync function canonicalize(target: string): Promise<string> {\n let resolvedTarget = path.resolve(target);\n const tail: string[] = [];\n\n // Walk up until an existing ancestor is found (or we hit the root).\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n const real = await realpath(resolvedTarget);\n\n return tail.length > 0 ? path.join(real, ...tail) : real;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n\n // Only ENOENT means \"this segment doesn't exist yet\" — keep\n // walking up. Any other error (EACCES, ELOOP, …) is a real fault.\n if (code !== \"ENOENT\") {\n throw error;\n }\n\n const parent = path.dirname(resolvedTarget);\n\n // Reached the filesystem root without finding an existing\n // ancestor — give back the lexically-resolved path unchanged.\n if (parent === resolvedTarget) {\n return path.join(resolvedTarget, ...tail);\n }\n\n tail.unshift(path.basename(resolvedTarget));\n resolvedTarget = parent;\n }\n }\n}\n\n/**\n * Resolve and jail a single input path against a {@link WorkspacePolicy}.\n *\n * The input is resolved against `policy.cwd`, canonicalized (symlinks in\n * existing ancestors collapsed so a symlinked directory cannot escape\n * the jail), then accepted **only** when it sits under `cwd` or one of\n * the `allowPaths` roots. A path that escapes, or that matches any\n * `denyPaths` glob even while inside `cwd`, is rejected with a\n * {@link WorkspacePolicyError} of type `\"path-escape\"`.\n *\n * Why this does not use `@warlock.js/fs`'s confined reader (`fs.confined`):\n * the two answer different questions. The confined reader refuses absolute\n * paths, refuses every symbolic link (or follows only links inside its one\n * root), requires the path to exist, and reads the file itself through a\n * no-follow, identity-checked open. This jail must instead accept absolute\n * paths inside `cwd`, several `allowPaths` roots, `denyPaths` globs, a\n * symlinked `cwd`, links whose target stays inside, and paths that do not\n * exist yet (`writeFile`, `mkdir`), and it returns a path that writes,\n * `exec`'s working directory and redirections use afterwards. Switching would\n * change what agents may touch, so the workspace keeps this resolver.\n *\n * @param policy - The bounding policy (its `cwd` is the jail root).\n * @param inputPath - A workspace-relative or absolute path to resolve.\n * @returns The canonical absolute path plus its `/`-separated relative form.\n * @throws {WorkspacePolicyError} When the path escapes the jail or hits a deny glob.\n *\n * @example\n * const { absolutePath } = await resolveInJail(policy, \"src/index.ts\");\n */\nexport async function resolveInJail(\n policy: WorkspacePolicy,\n inputPath: string,\n): Promise<ResolvedPath> {\n const jailRoot = await canonicalize(policy.cwd);\n const requested = path.isAbsolute(inputPath) ? inputPath : path.join(policy.cwd, inputPath);\n const absolutePath = await canonicalize(requested);\n\n const insideCwd = isInside(absolutePath, jailRoot);\n const allowRoots = policy.allowPaths ?? [];\n let insideAllow = false;\n\n if (!insideCwd) {\n for (const root of allowRoots) {\n const canonicalRoot = await canonicalize(root);\n\n if (isInside(absolutePath, canonicalRoot)) {\n insideAllow = true;\n\n break;\n }\n }\n }\n\n if (!insideCwd && !insideAllow) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" resolves outside the workspace jail.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n\n // `denyPaths` is evaluated relative to the jail root and wins even\n // when the path is comfortably inside `cwd`.\n const relativeToJail = insideCwd\n ? path.relative(jailRoot, absolutePath).split(path.sep).join(\"/\")\n : \"\";\n\n if (insideCwd && policy.denyPaths && policy.denyPaths.length > 0) {\n if (matchesAnyGlob(relativeToJail, policy.denyPaths)) {\n throw new WorkspacePolicyError(`Path \"${inputPath}\" is blocked by the workspace deny list.`, {\n type: \"path-escape\",\n path: inputPath,\n });\n }\n }\n\n return { absolutePath, relativePath: relativeToJail };\n}\n\n/**\n * Whether a command line is permitted by the policy's `shell` sub-policy —\n * `checkShellCommand(policy, command).allowed`.\n *\n * In argv mode (the default) the command must tokenize into a single argv\n * (no unquoted `;`, `&&`, `|`, backticks, `$`, redirection, …): the backend\n * spawns it with no shell, and unquoted metacharacters were exactly how an\n * injected command chain used to ride past the allowlist. In shell mode\n * (`interpreter: \"sh\"`) every simple command in the line is checked. Each\n * argv's executable basename is matched against `shell.deny` (wins, case\n * insensitive) then `shell.allow` (exhaustive when non-empty), then\n * `shell.denyPatterns` and `shell.allowCommand`. An absent `shell` block\n * means no command may run at all.\n *\n * Returns a plain `boolean`; use {@link checkShellCommand} for the reason\n * and the model-facing message. Redirection targets are checked lexically\n * here; `exec` re-checks them with symlinks resolved.\n *\n * @example\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test\"); // true\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"rm -rf /\"); // false\n * isCommandAllowed({ cwd, shell: { allow: [\"npm\"] } }, \"npm test; rm -rf /\"); // false\n */\nexport function isCommandAllowed(policy: WorkspacePolicy, command: string): boolean {\n return checkShellCommand(policy, command).allowed;\n}\n\n/**\n * Build the exact environment a spawned process receives — `process.env`\n * is **never** inherited wholesale. The result is\n * `{ ...pick(process.env, inheritEnv), ...shell.env }`, so a command\n * cannot see `PATH` (and thus often cannot find `node`/`npm`) unless the\n * policy opts in via `shell.inheritEnv: [\"PATH\"]`. Explicit `shell.env`\n * values override inherited ones on key collision.\n *\n * @example\n * buildEnv({ cwd, shell: { inheritEnv: [\"PATH\"], env: { CI: \"1\" } } });\n * // → { PATH: <process PATH>, CI: \"1\" }\n */\nexport function buildEnv(policy: WorkspacePolicy): Record<string, string> {\n const shell = policy.shell;\n const env: Record<string, string> = {};\n\n if (!shell) {\n return env;\n }\n\n for (const key of shell.inheritEnv ?? []) {\n const value = process.env[key];\n\n if (value !== undefined) {\n env[key] = value;\n }\n }\n\n if (shell.env) {\n for (const [key, value] of Object.entries(shell.env)) {\n env[key] = value;\n }\n }\n\n return env;\n}\n\n/**\n * Variables removed from the environment of a shell-mode (`interpreter:\n * \"sh\"`) command: each changes how the shell itself starts, parses or\n * resolves the line (`ENV`/`BASH_ENV` run a startup file, `SHELLOPTS` +\n * `PS4` can run command substitutions, `BASH_FUNC_*` imports functions that\n * shadow commands, `CDPATH` moves `cd`, `IFS` re-splits words).\n */\nconst SHELL_STARTUP_VARIABLES = new Set([\n \"ENV\",\n \"BASH_ENV\",\n \"SHELLOPTS\",\n \"BASHOPTS\",\n \"CDPATH\",\n \"IFS\",\n \"PS4\",\n \"GLOBIGNORE\",\n]);\n\n/**\n * The environment a command actually receives: {@link buildEnv}, minus the\n * shell start-up variables when the policy runs lines through a shell.\n * Compared case-insensitively, because Windows environment names are.\n */\nexport function buildCommandEnv(policy: WorkspacePolicy): Record<string, string> {\n const env = buildEnv(policy);\n\n if ((policy.shell?.interpreter ?? \"none\") !== \"sh\") {\n return env;\n }\n\n for (const key of Object.keys(env)) {\n const upper = key.toUpperCase();\n\n if (SHELL_STARTUP_VARIABLES.has(upper) || upper.startsWith(\"BASH_FUNC_\")) {\n delete env[key];\n }\n }\n\n return env;\n}\n"],"mappings":";;;;;;;;;;;;;;AA+BA,eAAe,aAAa,QAAiC;CAC3D,IAAI,iBAAiB,KAAK,QAAQ,MAAM;CACxC,MAAM,OAAiB,CAAC;CAIxB,OAAO,MACL,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,cAAc;EAE1C,OAAO,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,GAAG,IAAI,IAAI;CACtD,SAAS,OAAO;EAKd,IAJc,MAAgC,SAIjC,UACX,MAAM;EAGR,MAAM,SAAS,KAAK,QAAQ,cAAc;EAI1C,IAAI,WAAW,gBACb,OAAO,KAAK,KAAK,gBAAgB,GAAG,IAAI;EAG1C,KAAK,QAAQ,KAAK,SAAS,cAAc,CAAC;EAC1C,iBAAiB;CACnB;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,eAAsB,cACpB,QACA,WACuB;CACvB,MAAM,WAAW,MAAM,aAAa,OAAO,GAAG;CAE9C,MAAM,eAAe,MAAM,aADT,KAAK,WAAW,SAAS,IAAI,YAAY,KAAK,KAAK,OAAO,KAAK,SAAS,CACzC;CAEjD,MAAM,YAAY,SAAS,cAAc,QAAQ;CACjD,MAAM,aAAa,OAAO,cAAc,CAAC;CACzC,IAAI,cAAc;CAElB,IAAI,CAAC,WACH,KAAK,MAAM,QAAQ,YAAY;EAC7B,MAAM,gBAAgB,MAAM,aAAa,IAAI;EAE7C,IAAI,SAAS,cAAc,aAAa,GAAG;GACzC,cAAc;GAEd;EACF;CACF;CAGF,IAAI,CAAC,aAAa,CAAC,aACjB,MAAM,IAAI,qBAAqB,SAAS,UAAU,yCAAyC;EACzF,MAAM;EACN,MAAM;CACR,CAAC;CAKH,MAAM,iBAAiB,YACnB,KAAK,SAAS,UAAU,YAAY,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,IAC9D;CAEJ,IAAI,aAAa,OAAO,aAAa,OAAO,UAAU,SAAS,GAC7D;MAAI,eAAe,gBAAgB,OAAO,SAAS,GACjD,MAAM,IAAI,qBAAqB,SAAS,UAAU,2CAA2C;GAC3F,MAAM;GACN,MAAM;EACR,CAAC;CACH;CAGF,OAAO;EAAE;EAAc,cAAc;CAAe;AACtD;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,QAAyB,SAA0B;CAClF,OAAO,kBAAkB,QAAQ,OAAO,CAAC,CAAC;AAC5C;;;;;;;;;;;;;AAcA,SAAgB,SAAS,QAAiD;CACxE,MAAM,QAAQ,OAAO;CACrB,MAAM,MAA8B,CAAC;CAErC,IAAI,CAAC,OACH,OAAO;CAGT,KAAK,MAAM,OAAO,MAAM,cAAc,CAAC,GAAG;EACxC,MAAM,QAAQ,QAAQ,IAAI;EAE1B,IAAI,UAAU,QACZ,IAAI,OAAO;CAEf;CAEA,IAAI,MAAM,KACR,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,GACjD,IAAI,OAAO;CAIf,OAAO;AACT;;;;;;;;AASA,MAAM,0CAA0B,IAAI,IAAI;CACtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;AAOD,SAAgB,gBAAgB,QAAiD;CAC/E,MAAM,MAAM,SAAS,MAAM;CAE3B,KAAK,OAAO,OAAO,eAAe,YAAY,MAC5C,OAAO;CAGT,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG,GAAG;EAClC,MAAM,QAAQ,IAAI,YAAY;EAE9B,IAAI,wBAAwB,IAAI,KAAK,KAAK,MAAM,WAAW,YAAY,GACrE,OAAO,IAAI;CAEf;CAEA,OAAO;AACT"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { WorkspacePolicy } from "../contracts/workspace-policy.type.mjs";
|
|
2
|
+
import "../contracts/index.mjs";
|
|
2
3
|
import { WorkspaceShellRefusalReason } from "../errors.mjs";
|
|
3
|
-
|
|
4
4
|
//#region ../../packages/ai-workspace/src/policy/shell-check.d.ts
|
|
5
5
|
/** How a shell-mode line touches a path the jail has to vet. */
|
|
6
|
-
type WorkspaceShellPathAccess = "read" | "write" | "cwd";
|
|
6
|
+
export type WorkspaceShellPathAccess = "read" | "write" | "cwd";
|
|
7
7
|
/** A path a shell-mode line redirects to/from or `cd`s into. */
|
|
8
|
-
interface WorkspaceShellPath {
|
|
8
|
+
export interface WorkspaceShellPath {
|
|
9
9
|
/** Absolute path, resolved lexically against the workspace root (or an earlier `cd`). */
|
|
10
10
|
path: string;
|
|
11
11
|
/** What the line does with it. */
|
|
@@ -17,7 +17,7 @@ interface WorkspaceShellPath {
|
|
|
17
17
|
* The shell policy's verdict on one command line — see
|
|
18
18
|
* {@link checkShellCommand}.
|
|
19
19
|
*/
|
|
20
|
-
interface WorkspaceShellCheck {
|
|
20
|
+
export interface WorkspaceShellCheck {
|
|
21
21
|
/** Whether the line may run (path targets still get a symlink-aware recheck in `exec`). */
|
|
22
22
|
allowed: boolean;
|
|
23
23
|
/** Why it was refused. */
|
|
@@ -58,7 +58,6 @@ interface WorkspaceShellCheck {
|
|
|
58
58
|
* checkShellCommand({ cwd, shell: { allow: ["git"] } }, "git add -A && git commit -m wip");
|
|
59
59
|
* // { allowed: false, reason: "metacharacter", message: "`&&` is not supported in argv mode: run `git add -A`, then `git commit -m wip`, …" }
|
|
60
60
|
*/
|
|
61
|
-
declare function checkShellCommand(policy: WorkspacePolicy, command: string): WorkspaceShellCheck;
|
|
61
|
+
export declare function checkShellCommand(policy: WorkspacePolicy, command: string): WorkspaceShellCheck;
|
|
62
62
|
//#endregion
|
|
63
|
-
export { WorkspaceShellCheck, WorkspaceShellPath, WorkspaceShellPathAccess, checkShellCommand };
|
|
64
63
|
//# sourceMappingURL=shell-check.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-check.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/shell-check.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"shell-check.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/policy/shell-check.ts"],"mappings":";;;;;YAmBY;;iBAGK;;EAEf;;EAEA,QAAQ;;EAER;;;;;;iBAOe;;EAEf;;EAEA,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA6hCO,kBAAkB,QAAQ,iBAAiB,kBAAkB"}
|
|
@@ -10,7 +10,7 @@ import path from "node:path";
|
|
|
10
10
|
* With deny rules in force they would hide a denied command, so they are
|
|
11
11
|
* refused unless the policy names them in `allow`.
|
|
12
12
|
*/
|
|
13
|
-
const OPAQUE_RUNNERS = new Set([
|
|
13
|
+
const OPAQUE_RUNNERS = /* @__PURE__ */ new Set([
|
|
14
14
|
"sh",
|
|
15
15
|
"bash",
|
|
16
16
|
"dash",
|
|
@@ -54,7 +54,7 @@ const OPAQUE_RUNNERS = new Set([
|
|
|
54
54
|
"flock"
|
|
55
55
|
]);
|
|
56
56
|
/** POSIX shell builtins that evaluate code or change how later names resolve. */
|
|
57
|
-
const SHELL_OPAQUE_BUILTINS = new Set([
|
|
57
|
+
const SHELL_OPAQUE_BUILTINS = /* @__PURE__ */ new Set([
|
|
58
58
|
"eval",
|
|
59
59
|
".",
|
|
60
60
|
"source",
|
|
@@ -78,7 +78,7 @@ const SHELL_OPAQUE_BUILTINS = new Set([
|
|
|
78
78
|
"readarray"
|
|
79
79
|
]);
|
|
80
80
|
/** cmd.exe builtins that run commands or change the environment. */
|
|
81
|
-
const CMD_OPAQUE_BUILTINS = new Set([
|
|
81
|
+
const CMD_OPAQUE_BUILTINS = /* @__PURE__ */ new Set([
|
|
82
82
|
"call",
|
|
83
83
|
"start",
|
|
84
84
|
"set",
|
|
@@ -90,7 +90,7 @@ const CMD_OPAQUE_BUILTINS = new Set([
|
|
|
90
90
|
* subscripts such as `a[$(cmd)]`): their arguments must be literal and free
|
|
91
91
|
* of `[`.
|
|
92
92
|
*/
|
|
93
|
-
const ARITHMETIC_SENSITIVE_BUILTINS = new Set([
|
|
93
|
+
const ARITHMETIC_SENSITIVE_BUILTINS = /* @__PURE__ */ new Set([
|
|
94
94
|
"test",
|
|
95
95
|
"[",
|
|
96
96
|
"printf",
|
|
@@ -114,7 +114,7 @@ const ARITHMETIC_SENSITIVE_BUILTINS = new Set([
|
|
|
114
114
|
* the jail's sight, `PS4` is expanded — command substitutions included —
|
|
115
115
|
* on every traced line, `IFS` re-splits expansions).
|
|
116
116
|
*/
|
|
117
|
-
const SENSITIVE_VARIABLES = new Set([
|
|
117
|
+
const SENSITIVE_VARIABLES = /* @__PURE__ */ new Set([
|
|
118
118
|
"CDPATH",
|
|
119
119
|
"ENV",
|
|
120
120
|
"BASH_ENV",
|
|
@@ -132,13 +132,13 @@ const SENSITIVE_VARIABLES = new Set([
|
|
|
132
132
|
"BASH_LOADABLES_PATH"
|
|
133
133
|
]);
|
|
134
134
|
/** Redirection targets that are always fine even though they sit outside the jail. */
|
|
135
|
-
const DEVICE_TARGETS = new Set([
|
|
135
|
+
const DEVICE_TARGETS = /* @__PURE__ */ new Set([
|
|
136
136
|
"/dev/null",
|
|
137
137
|
"/dev/stdout",
|
|
138
138
|
"/dev/stderr"
|
|
139
139
|
]);
|
|
140
140
|
/** cmd.exe's null device. */
|
|
141
|
-
const CMD_DEVICE_TARGETS = new Set(["nul"]);
|
|
141
|
+
const CMD_DEVICE_TARGETS = /* @__PURE__ */ new Set(["nul"]);
|
|
142
142
|
/** Deepest chain of wrappers (`env time nice …`) the policy unwraps. */
|
|
143
143
|
const MAX_WRAPPER_DEPTH = 8;
|
|
144
144
|
/** Most distinct directories a line may `cd` between. */
|
|
@@ -237,7 +237,8 @@ var LineChecker = class {
|
|
|
237
237
|
verdict = false;
|
|
238
238
|
}
|
|
239
239
|
if (verdict === true) return;
|
|
240
|
-
|
|
240
|
+
const explanation = typeof verdict === "string" && verdict.trim() !== "" ? ` ${verdict.trim()}` : "";
|
|
241
|
+
throw new Refusal("not-allowed", `${shown} was refused by the workspace shell policy.${explanation}`, executableBasename(argv[0] ?? "", this.dialect));
|
|
241
242
|
}
|
|
242
243
|
/** Whether the policy names `executable` in a non-empty `allow` list. */
|
|
243
244
|
explicitlyAllowed(executable) {
|
|
@@ -260,7 +261,10 @@ var LineChecker = class {
|
|
|
260
261
|
if (executable === "") throw new Refusal("not-allowed", "The command has no executable name.");
|
|
261
262
|
if (dynamic[0] === true) throw new Refusal("unsupported-syntax", `The command name \`${first}\` is decided when the line runs, so the workspace shell policy cannot check it. Write the command name literally.`);
|
|
262
263
|
if (this.deny.has(key)) throw new Refusal("denied-executable", `\`${executable}\` is denied by the workspace shell policy (in ${show(argv.join(" "))}).`, executable);
|
|
263
|
-
if (this.allow.length > 0 && !this.allow.includes(executable))
|
|
264
|
+
if (this.allow.length > 0 && !this.allow.includes(executable)) {
|
|
265
|
+
const listed = this.allow.slice(0, 20).join(", ");
|
|
266
|
+
throw new Refusal("not-allowed", `\`${executable}\` is not in the workspace shell allowlist. Allowed commands: ${listed}${this.allow.length > 20 ? ", …" : ""}.`, executable);
|
|
267
|
+
}
|
|
264
268
|
this.checkHooks(argv, depth > 0);
|
|
265
269
|
this.commands.push([...argv]);
|
|
266
270
|
if (OPAQUE_RUNNERS.has(key) || this.shellMode && this.dialect === "posix" && SHELL_OPAQUE_BUILTINS.has(key) || this.shellMode && this.dialect === "cmd" && CMD_OPAQUE_BUILTINS.has(key)) {
|
|
@@ -504,16 +508,23 @@ function argvModeRefusal(command) {
|
|
|
504
508
|
const segments = parsed.ok ? parsed.segments : [];
|
|
505
509
|
if ((token === "&&" || token === "||" || token === ";" || token === "\n") && segments.length >= 2) {
|
|
506
510
|
const shownOperator = token === "\n" ? "A line break" : `\`${token}\``;
|
|
507
|
-
if ((segments[0]?.source ?? "").trim().split(/\s+/)[0] === "cd")
|
|
511
|
+
if ((segments[0]?.source ?? "").trim().split(/\s+/)[0] === "cd") {
|
|
512
|
+
const rest = segments.slice(1).map((segment) => show(segment.source));
|
|
513
|
+
return new Refusal("metacharacter", `${shownOperator} is not supported in argv mode, and \`cd\` does not carry over: every command starts in the workspace root. Run ${rest.join(", then ")} on its own, with paths relative to the root.${suffix}`);
|
|
514
|
+
}
|
|
508
515
|
const steps = segments.map((segment) => show(segment.source));
|
|
509
516
|
const joiner = token === "||" ? ", and if it fails, " : ", then ";
|
|
510
517
|
return new Refusal("metacharacter", `${shownOperator} is not supported in argv mode: run ${steps.join(joiner)}, as separate commands.${suffix}`);
|
|
511
518
|
}
|
|
512
519
|
if (token === "&" && segments.length >= 1) {
|
|
513
520
|
const steps = segments.map((segment) => show(segment.source));
|
|
514
|
-
|
|
521
|
+
const run = steps.length === 1 ? `run ${steps[0] ?? ""} without it` : `run ${steps.join(", then ")} as separate commands`;
|
|
522
|
+
return new Refusal("metacharacter", `\`&\` is not supported in argv mode: ${run}; start a long-running command with background: true.${suffix}`);
|
|
523
|
+
}
|
|
524
|
+
if (token === "|") {
|
|
525
|
+
const pipeline = command.split("|").map((part) => part.trim()).filter(Boolean);
|
|
526
|
+
return new Refusal("metacharacter", `\`|\` (a pipe) is not supported in argv mode: run ${show(pipeline[0] ?? command)} on its own and read its output yourself.${suffix}`);
|
|
515
527
|
}
|
|
516
|
-
if (token === "|") return new Refusal("metacharacter", `\`|\` (a pipe) is not supported in argv mode: run ${show(command.split("|").map((part) => part.trim()).filter(Boolean)[0] ?? command)} on its own and read its output yourself.${suffix}`);
|
|
517
528
|
if (token === ">" || token === ">>" || token === ">&") return new Refusal("metacharacter", `\`${token}\` (redirection) is not supported in argv mode: run the command without it and save the output with write_file if you need it.${suffix}`);
|
|
518
529
|
if (token === "<" || token === "<<" || token === "<&") return new Refusal("metacharacter", `\`${token}\` (input redirection) is not supported in argv mode: pass the file path to the command as an argument.${suffix}`);
|
|
519
530
|
if (token === "$(" || token === "`") return new Refusal("metacharacter", `Command substitution (${token === "$(" ? "`$(…)`" : "backticks"}) is not supported in argv mode: run the inner command first, then pass its output literally.${suffix}`);
|
|
@@ -623,5 +634,5 @@ function checkShellCommandInDialect(policy, command, dialect) {
|
|
|
623
634
|
}
|
|
624
635
|
|
|
625
636
|
//#endregion
|
|
626
|
-
export { checkShellCommand };
|
|
637
|
+
export { checkShellCommand, checkShellCommandInDialect, executableBasename, shellDialect };
|
|
627
638
|
//# sourceMappingURL=shell-check.mjs.map
|