@agent-api/sdk 1.2.3 → 1.3.1
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 +21 -0
- package/README.md +8 -0
- package/dist/local/core.d.ts +1 -0
- package/dist/local/core.js +22 -1
- package/dist/local/shell.d.ts +76 -0
- package/dist/local/shell.js +347 -13
- package/dist/local/tools.d.ts +1 -0
- package/dist/local/tools.js +38 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist-cjs/local/core.js +22 -1
- package/dist-cjs/local/shell.js +348 -13
- package/dist-cjs/local/tools.js +38 -2
- package/dist-cjs/node.js +2 -1
- package/dist-cjs/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog — @agent-api/sdk
|
|
2
2
|
|
|
3
|
+
## 1.3.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Made `local_workdir` grep behave like familiar grep: `path` may be omitted, a file path, or a directory subtree.
|
|
8
|
+
- Model-facing `local_workdir` execution errors now return structured `{ ok: false, error }` tool results instead of aborting agent runs.
|
|
9
|
+
|
|
10
|
+
## 1.3.0
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added SDK support for `local_shell` isolation modes: `none`, `auto`, and `required`.
|
|
15
|
+
- Added `IsolatorLocalShellRunner`, a Node-local runner that delegates shell execution to the shared `agent-isolator` binary.
|
|
16
|
+
- Added model-facing `shell_isolation` metadata for approval previews and execution results.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `local_shell` auto isolation now tries an explicitly configured `agent-isolator` path first and falls back to direct host execution when isolation is unavailable.
|
|
21
|
+
- SDKs no longer assume `agent-isolator` is discoverable on `PATH`; pass `isolator.executablePath` or set `AGENT_ISOLATOR_PATH`.
|
|
22
|
+
- Direct host execution remains the default/fallback path and reports its actual non-isolated guarantees.
|
|
23
|
+
|
|
3
24
|
## 1.2.3
|
|
4
25
|
|
|
5
26
|
### Fixed
|
package/README.md
CHANGED
|
@@ -199,6 +199,14 @@ Keep UI and OS interaction policy in your host framework. Electron, Tauri, Qt, o
|
|
|
199
199
|
|
|
200
200
|
`local_shell` executes commands through a pluggable command runner. The default `HostLocalShellRunner` runs on the user's local machine, bounds captured output, enforces a timeout, and confines relative `workdir` overrides under the configured cwd. It is not a security sandbox; use approval mode unless your application intentionally grants full local command access.
|
|
201
201
|
|
|
202
|
+
For OS-level isolation, install the standalone `agent-isolator` binary from the
|
|
203
|
+
GitHub Release artifacts and pass its explicit path through
|
|
204
|
+
`isolator.executablePath` or `AGENT_ISOLATOR_PATH`. `isolation: "auto"` tries
|
|
205
|
+
that configured isolator first and falls back to direct execution if it is
|
|
206
|
+
missing or unavailable. `isolation: "required"` fails closed when an isolating
|
|
207
|
+
runner cannot be selected. The SDK does not search `PATH`, run postinstall
|
|
208
|
+
scripts, or build native binaries during package installation.
|
|
209
|
+
|
|
202
210
|
```typescript
|
|
203
211
|
const workdir = local.data.child("workdirs/demo");
|
|
204
212
|
|
package/dist/local/core.d.ts
CHANGED
|
@@ -309,6 +309,7 @@ export declare class LocalFileStore {
|
|
|
309
309
|
readLines(relativePath: string, params: LocalReadLinesParams): Promise<LocalFileLines>;
|
|
310
310
|
patchLines(relativePath: string, params: LocalPatchLinesParams): Promise<LocalFileLinesPatch>;
|
|
311
311
|
grep(params: LocalGrepParams): Promise<LocalGrepResponse>;
|
|
312
|
+
private grepCandidates;
|
|
312
313
|
summarize(params?: LocalSummarizeParams): Promise<LocalSummary>;
|
|
313
314
|
private walk;
|
|
314
315
|
}
|
package/dist/local/core.js
CHANGED
|
@@ -310,7 +310,7 @@ export class LocalFileStore {
|
|
|
310
310
|
const maxFiles = positiveInt(params.maxFiles, 500);
|
|
311
311
|
const maxBytesPerFile = positiveInt(params.maxBytesPerFile, 512 * 1024);
|
|
312
312
|
const maxLineLength = positiveInt(params.maxLineLength, 500);
|
|
313
|
-
const stats = await this.
|
|
313
|
+
const stats = await this.grepCandidates(params.path ?? ".", params.ignore);
|
|
314
314
|
const matches = [];
|
|
315
315
|
let filesScanned = 0;
|
|
316
316
|
let scanTruncated = false;
|
|
@@ -348,6 +348,27 @@ export class LocalFileStore {
|
|
|
348
348
|
}
|
|
349
349
|
return { object: "list", matches, files_scanned: filesScanned, scan_truncated: scanTruncated };
|
|
350
350
|
}
|
|
351
|
+
async grepCandidates(relativePath, ignore) {
|
|
352
|
+
const fullPath = this.resolvePath(relativePath);
|
|
353
|
+
const info = await stat(fullPath);
|
|
354
|
+
const portablePath = toPortablePath(path.relative(this.root, fullPath)) || ".";
|
|
355
|
+
if (ignored(portablePath, ignore)) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
if (info.isFile()) {
|
|
359
|
+
return [{
|
|
360
|
+
path: portablePath,
|
|
361
|
+
fullPath,
|
|
362
|
+
type: "file",
|
|
363
|
+
size: info.size,
|
|
364
|
+
modifiedAt: info.mtime,
|
|
365
|
+
}];
|
|
366
|
+
}
|
|
367
|
+
if (info.isDirectory()) {
|
|
368
|
+
return await this.list(relativePath, { recursive: true, ignore });
|
|
369
|
+
}
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
351
372
|
async summarize(params = {}) {
|
|
352
373
|
const maxFiles = positiveInt(params.maxFiles, 2000);
|
|
353
374
|
const maxPreviews = positiveInt(params.maxPreviews, 20);
|
package/dist/local/shell.d.ts
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
import type { FunctionTool } from "../types/tools.js";
|
|
2
2
|
import type { LocalWorkdir } from "./core.js";
|
|
3
3
|
export type LocalShellAccessMode = "approval" | "full";
|
|
4
|
+
/**
|
|
5
|
+
* Requested isolation policy for local shell execution.
|
|
6
|
+
* - none: direct host execution.
|
|
7
|
+
* - auto: use an isolating runner when the host app provides one; otherwise fall back to direct execution.
|
|
8
|
+
* - required: fail closed unless the configured runner reports real isolation.
|
|
9
|
+
*/
|
|
10
|
+
export type LocalShellIsolationMode = "none" | "auto" | "required";
|
|
11
|
+
export interface LocalShellIsolationGuarantees {
|
|
12
|
+
filesystem: "none" | "workdir-mounted" | "policy-enforced";
|
|
13
|
+
network: "allowed" | "blocked" | "configurable";
|
|
14
|
+
user: "host-user" | "unprivileged-user" | "namespace-user";
|
|
15
|
+
process: "host-process-tree" | "child-contained" | "pid-namespace";
|
|
16
|
+
resources: "none" | "timeout-only" | "cpu-memory-limits";
|
|
17
|
+
}
|
|
18
|
+
export interface LocalShellIsolationResourceOptions {
|
|
19
|
+
memoryMb?: number;
|
|
20
|
+
cpuCount?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface LocalShellIsolationOptions {
|
|
23
|
+
filesystem?: "host" | "workdir-readonly" | "workdir-readwrite";
|
|
24
|
+
network?: "allowed" | "blocked";
|
|
25
|
+
env?: "inherit" | "minimal";
|
|
26
|
+
resources?: LocalShellIsolationResourceOptions;
|
|
27
|
+
}
|
|
28
|
+
export interface LocalShellIsolationStatus {
|
|
29
|
+
executor: "direct" | "isolator";
|
|
30
|
+
driver: string;
|
|
31
|
+
isolated: boolean;
|
|
32
|
+
fallback: boolean;
|
|
33
|
+
requested: Required<LocalShellIsolationOptions> & {
|
|
34
|
+
resources: LocalShellIsolationResourceOptions;
|
|
35
|
+
};
|
|
36
|
+
guarantees: LocalShellIsolationGuarantees;
|
|
37
|
+
warnings: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface LocalShellIsolatorStatusResult {
|
|
40
|
+
version?: string;
|
|
41
|
+
driver: string;
|
|
42
|
+
status: LocalShellIsolationStatus;
|
|
43
|
+
drivers?: Array<{
|
|
44
|
+
name: string;
|
|
45
|
+
platform: string;
|
|
46
|
+
available: boolean;
|
|
47
|
+
warnings?: string[];
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
4
50
|
export interface LocalShellRequest {
|
|
5
51
|
command: string;
|
|
6
52
|
description?: string;
|
|
@@ -24,8 +70,10 @@ export interface LocalShellResult {
|
|
|
24
70
|
duration_ms: number;
|
|
25
71
|
timed_out: boolean;
|
|
26
72
|
truncated: boolean;
|
|
73
|
+
shell_isolation: LocalShellIsolationStatus;
|
|
27
74
|
}
|
|
28
75
|
export interface LocalCommandRunner {
|
|
76
|
+
readonly isolationStatus?: LocalShellIsolationStatus;
|
|
29
77
|
run(request: LocalShellRequest, context?: LocalShellContext): Promise<LocalShellResult>;
|
|
30
78
|
}
|
|
31
79
|
export interface HostLocalShellRunnerOptions {
|
|
@@ -34,6 +82,15 @@ export interface HostLocalShellRunnerOptions {
|
|
|
34
82
|
timeoutMs?: number;
|
|
35
83
|
maxOutputBytes?: number;
|
|
36
84
|
env?: NodeJS.ProcessEnv;
|
|
85
|
+
isolationOptions?: LocalShellIsolationOptions;
|
|
86
|
+
}
|
|
87
|
+
export interface IsolatorLocalShellRunnerOptions {
|
|
88
|
+
executablePath?: string;
|
|
89
|
+
driver?: "auto" | "direct" | "bwrap" | "sandbox-exec" | "windows-job" | string;
|
|
90
|
+
cwd?: string;
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
maxOutputBytes?: number;
|
|
93
|
+
isolationOptions?: LocalShellIsolationOptions;
|
|
37
94
|
}
|
|
38
95
|
export declare class HostLocalShellRunner implements LocalCommandRunner {
|
|
39
96
|
readonly cwd: string;
|
|
@@ -41,11 +98,27 @@ export declare class HostLocalShellRunner implements LocalCommandRunner {
|
|
|
41
98
|
readonly timeoutMs: number;
|
|
42
99
|
readonly maxOutputBytes: number;
|
|
43
100
|
readonly env: NodeJS.ProcessEnv;
|
|
101
|
+
readonly isolationStatus: LocalShellIsolationStatus;
|
|
44
102
|
constructor(options?: HostLocalShellRunnerOptions);
|
|
45
103
|
run(request: LocalShellRequest, context?: LocalShellContext): Promise<LocalShellResult>;
|
|
46
104
|
}
|
|
105
|
+
export declare class IsolatorLocalShellRunner implements LocalCommandRunner {
|
|
106
|
+
readonly executablePath: string;
|
|
107
|
+
readonly driver: string;
|
|
108
|
+
readonly cwd: string;
|
|
109
|
+
readonly timeoutMs: number;
|
|
110
|
+
readonly maxOutputBytes: number;
|
|
111
|
+
readonly isolationOptions: LocalShellIsolationOptions;
|
|
112
|
+
readonly isolationStatus: LocalShellIsolationStatus;
|
|
113
|
+
readonly statusResult: LocalShellIsolatorStatusResult;
|
|
114
|
+
constructor(options?: IsolatorLocalShellRunnerOptions);
|
|
115
|
+
run(request: LocalShellRequest, context?: LocalShellContext): Promise<LocalShellResult>;
|
|
116
|
+
}
|
|
47
117
|
export interface LocalShellToolRegistryOptions {
|
|
48
118
|
accessMode?: LocalShellAccessMode;
|
|
119
|
+
isolation?: LocalShellIsolationMode;
|
|
120
|
+
isolationOptions?: LocalShellIsolationOptions;
|
|
121
|
+
isolator?: IsolatorLocalShellRunnerOptions | boolean;
|
|
49
122
|
toolName?: string;
|
|
50
123
|
runner?: LocalCommandRunner;
|
|
51
124
|
cwd?: string;
|
|
@@ -56,6 +129,8 @@ export interface LocalShellToolRegistryOptions {
|
|
|
56
129
|
}
|
|
57
130
|
export interface LocalShellToolPresentationOptions {
|
|
58
131
|
accessMode?: LocalShellAccessMode;
|
|
132
|
+
isolationStatus?: LocalShellIsolationStatus;
|
|
133
|
+
isolationOptions?: LocalShellIsolationOptions;
|
|
59
134
|
cwd?: string;
|
|
60
135
|
shell?: string | boolean;
|
|
61
136
|
platform?: NodeJS.Platform;
|
|
@@ -73,6 +148,7 @@ export interface LocalShellToolRegistry {
|
|
|
73
148
|
export declare class LocalShellDriver {
|
|
74
149
|
readonly accessMode: LocalShellAccessMode;
|
|
75
150
|
readonly runner: LocalCommandRunner;
|
|
151
|
+
readonly isolationStatus: LocalShellIsolationStatus;
|
|
76
152
|
constructor(options?: LocalShellToolRegistryOptions);
|
|
77
153
|
dispatch(args: Record<string, unknown>, context?: LocalShellContext): Promise<Record<string, unknown>>;
|
|
78
154
|
requiresApproval(): boolean;
|
package/dist/local/shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export class HostLocalShellRunner {
|
|
4
4
|
cwd;
|
|
@@ -6,12 +6,14 @@ export class HostLocalShellRunner {
|
|
|
6
6
|
timeoutMs;
|
|
7
7
|
maxOutputBytes;
|
|
8
8
|
env;
|
|
9
|
+
isolationStatus;
|
|
9
10
|
constructor(options = {}) {
|
|
10
11
|
this.cwd = path.resolve(options.cwd ?? process.cwd());
|
|
11
12
|
this.shell = options.shell ?? true;
|
|
12
13
|
this.timeoutMs = positiveInteger(options.timeoutMs, 2 * 60 * 1000, "timeoutMs");
|
|
13
14
|
this.maxOutputBytes = positiveInteger(options.maxOutputBytes, 128 * 1024, "maxOutputBytes");
|
|
14
15
|
this.env = { ...process.env, ...(options.env ?? {}) };
|
|
16
|
+
this.isolationStatus = directIsolationStatus(false, options.isolationOptions);
|
|
15
17
|
}
|
|
16
18
|
async run(request, context = {}) {
|
|
17
19
|
const command = requiredString(request.command, "command");
|
|
@@ -54,6 +56,7 @@ export class HostLocalShellRunner {
|
|
|
54
56
|
duration_ms: Date.now() - started,
|
|
55
57
|
timed_out: timedOut,
|
|
56
58
|
truncated: stdout.truncated || stderr.truncated,
|
|
59
|
+
shell_isolation: this.isolationStatus,
|
|
57
60
|
});
|
|
58
61
|
};
|
|
59
62
|
const kill = () => {
|
|
@@ -93,23 +96,61 @@ export class HostLocalShellRunner {
|
|
|
93
96
|
});
|
|
94
97
|
}
|
|
95
98
|
}
|
|
99
|
+
export class IsolatorLocalShellRunner {
|
|
100
|
+
executablePath;
|
|
101
|
+
driver;
|
|
102
|
+
cwd;
|
|
103
|
+
timeoutMs;
|
|
104
|
+
maxOutputBytes;
|
|
105
|
+
isolationOptions;
|
|
106
|
+
isolationStatus;
|
|
107
|
+
statusResult;
|
|
108
|
+
constructor(options = {}) {
|
|
109
|
+
this.executablePath = resolveIsolatorExecutablePath(options.executablePath);
|
|
110
|
+
this.driver = options.driver ?? "auto";
|
|
111
|
+
this.cwd = path.resolve(options.cwd ?? process.cwd());
|
|
112
|
+
this.timeoutMs = positiveInteger(options.timeoutMs, 2 * 60 * 1000, "timeoutMs");
|
|
113
|
+
this.maxOutputBytes = positiveInteger(options.maxOutputBytes, 128 * 1024, "maxOutputBytes");
|
|
114
|
+
this.isolationOptions = options.isolationOptions ?? {};
|
|
115
|
+
this.statusResult = isolatorStatusSync(this.executablePath, this.driver);
|
|
116
|
+
this.isolationStatus = this.statusResult.status;
|
|
117
|
+
}
|
|
118
|
+
async run(request, context = {}) {
|
|
119
|
+
const command = requiredString(request.command, "command");
|
|
120
|
+
const cwd = resolveContainedPath(this.cwd, request.workdir);
|
|
121
|
+
const timeoutMs = request.timeoutMs == null
|
|
122
|
+
? this.timeoutMs
|
|
123
|
+
: positiveInteger(request.timeoutMs, this.timeoutMs, "timeoutMs");
|
|
124
|
+
const response = await isolatorRequest(this.executablePath, this.driver, {
|
|
125
|
+
id: `run_${Date.now().toString(36)}`,
|
|
126
|
+
method: "run",
|
|
127
|
+
params: {
|
|
128
|
+
command,
|
|
129
|
+
description: request.description,
|
|
130
|
+
cwd,
|
|
131
|
+
timeout_ms: timeoutMs,
|
|
132
|
+
max_output_bytes: this.maxOutputBytes,
|
|
133
|
+
env: request.env,
|
|
134
|
+
isolation: this.isolationOptions,
|
|
135
|
+
},
|
|
136
|
+
}, context);
|
|
137
|
+
return isolatorRunResult(response.result);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
96
140
|
export class LocalShellDriver {
|
|
97
141
|
accessMode;
|
|
98
142
|
runner;
|
|
143
|
+
isolationStatus;
|
|
99
144
|
constructor(options = {}) {
|
|
100
145
|
const cwd = options.workdir?.root ?? options.cwd;
|
|
101
146
|
this.accessMode = options.accessMode ?? "approval";
|
|
102
|
-
this.runner = options
|
|
103
|
-
|
|
104
|
-
shell: options.shell,
|
|
105
|
-
timeoutMs: options.timeoutMs,
|
|
106
|
-
maxOutputBytes: options.maxOutputBytes,
|
|
107
|
-
});
|
|
147
|
+
this.runner = resolveShellRunner(options, cwd);
|
|
148
|
+
this.isolationStatus = this.runner.isolationStatus ?? directIsolationStatus(options.isolation === "auto", options.isolationOptions);
|
|
108
149
|
}
|
|
109
150
|
async dispatch(args, context = {}) {
|
|
110
151
|
const request = shellRequest(args);
|
|
111
152
|
if (this.accessMode !== "full") {
|
|
112
|
-
return shellApprovalRequired(request);
|
|
153
|
+
return shellApprovalRequired(request, this.isolationStatus);
|
|
113
154
|
}
|
|
114
155
|
return shellToolResult(await this.runner.run(request, context));
|
|
115
156
|
}
|
|
@@ -117,16 +158,80 @@ export class LocalShellDriver {
|
|
|
117
158
|
return this.accessMode !== "full";
|
|
118
159
|
}
|
|
119
160
|
}
|
|
161
|
+
function resolveShellRunner(options, cwd) {
|
|
162
|
+
if (options.runner) {
|
|
163
|
+
if (options.isolation === "required" && options.runner.isolationStatus?.isolated !== true) {
|
|
164
|
+
throw new Error("local_shell isolation is required, but the configured runner does not report isolation");
|
|
165
|
+
}
|
|
166
|
+
return options.runner;
|
|
167
|
+
}
|
|
168
|
+
let isolatorFallbackWarning;
|
|
169
|
+
if (options.isolation === "auto" || options.isolation === "required" || options.isolator) {
|
|
170
|
+
const isolatorOptions = typeof options.isolator === "object" ? options.isolator : {};
|
|
171
|
+
try {
|
|
172
|
+
const runner = new IsolatorLocalShellRunner({
|
|
173
|
+
cwd,
|
|
174
|
+
timeoutMs: options.timeoutMs,
|
|
175
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
176
|
+
isolationOptions: options.isolationOptions,
|
|
177
|
+
...isolatorOptions,
|
|
178
|
+
});
|
|
179
|
+
if (options.isolation === "required" && runner.isolationStatus.isolated !== true) {
|
|
180
|
+
throw new Error(`local_shell isolation is required, but agent-isolator selected non-isolated driver ${runner.isolationStatus.driver}`);
|
|
181
|
+
}
|
|
182
|
+
if (options.isolation !== "none" || options.isolator) {
|
|
183
|
+
return runner;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (options.isolation === "required") {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
isolatorFallbackWarning = errorMessage(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const runner = new HostLocalShellRunner({
|
|
194
|
+
cwd,
|
|
195
|
+
shell: options.shell,
|
|
196
|
+
timeoutMs: options.timeoutMs,
|
|
197
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
198
|
+
isolationOptions: options.isolationOptions,
|
|
199
|
+
});
|
|
200
|
+
if (options.isolation === "auto") {
|
|
201
|
+
let status = directIsolationStatus(true, options.isolationOptions);
|
|
202
|
+
if (isolatorFallbackWarning) {
|
|
203
|
+
status = {
|
|
204
|
+
...status,
|
|
205
|
+
warnings: [...status.warnings, `Isolator unavailable: ${isolatorFallbackWarning}`],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return withIsolationStatus(runner, status);
|
|
209
|
+
}
|
|
210
|
+
return runner;
|
|
211
|
+
}
|
|
212
|
+
function resolveIsolatorExecutablePath(explicitPath) {
|
|
213
|
+
const configured = (explicitPath?.trim() || process.env.AGENT_ISOLATOR_PATH?.trim() || "");
|
|
214
|
+
if (!configured) {
|
|
215
|
+
throw new Error("agent-isolator executable path is not configured; pass isolator.executablePath or set AGENT_ISOLATOR_PATH");
|
|
216
|
+
}
|
|
217
|
+
return configured;
|
|
218
|
+
}
|
|
219
|
+
function errorMessage(error) {
|
|
220
|
+
return error instanceof Error ? error.message : String(error);
|
|
221
|
+
}
|
|
120
222
|
export function createLocalShellToolRegistry(options = {}) {
|
|
121
223
|
const toolName = options.toolName ?? "local_shell";
|
|
122
224
|
const driver = new LocalShellDriver(options);
|
|
123
225
|
const hostRunner = driver.runner instanceof HostLocalShellRunner ? driver.runner : undefined;
|
|
226
|
+
const isolatorRunner = driver.runner instanceof IsolatorLocalShellRunner ? driver.runner : undefined;
|
|
124
227
|
const definition = localShellToolDefinition(toolName, {
|
|
125
228
|
accessMode: driver.accessMode,
|
|
126
|
-
cwd: options.workdir?.root ?? hostRunner?.cwd ?? options.cwd,
|
|
229
|
+
cwd: options.workdir?.root ?? hostRunner?.cwd ?? isolatorRunner?.cwd ?? options.cwd,
|
|
127
230
|
shell: hostRunner?.shell ?? options.shell,
|
|
128
|
-
timeoutMs: hostRunner?.timeoutMs ?? options.timeoutMs,
|
|
129
|
-
maxOutputBytes: hostRunner?.maxOutputBytes ?? options.maxOutputBytes,
|
|
231
|
+
timeoutMs: hostRunner?.timeoutMs ?? isolatorRunner?.timeoutMs ?? options.timeoutMs,
|
|
232
|
+
maxOutputBytes: hostRunner?.maxOutputBytes ?? isolatorRunner?.maxOutputBytes ?? options.maxOutputBytes,
|
|
233
|
+
isolationStatus: driver.isolationStatus,
|
|
234
|
+
isolationOptions: options.isolationOptions,
|
|
130
235
|
});
|
|
131
236
|
return {
|
|
132
237
|
accessMode: driver.accessMode,
|
|
@@ -158,13 +263,16 @@ function localShellToolDescription(options = {}) {
|
|
|
158
263
|
const platform = options.platform ?? process.platform;
|
|
159
264
|
const shell = shellDisplayName(options.shell);
|
|
160
265
|
const accessMode = options.accessMode ?? "approval";
|
|
266
|
+
const isolation = options.isolationStatus ?? directIsolationStatus(false, options.isolationOptions);
|
|
161
267
|
const cwd = options.cwd ? path.resolve(options.cwd) : undefined;
|
|
162
268
|
const timeoutMs = options.timeoutMs ?? 2 * 60 * 1000;
|
|
163
269
|
const maxOutputBytes = options.maxOutputBytes ?? 128 * 1024;
|
|
164
270
|
return [
|
|
165
271
|
"Run a local shell command through one model-facing primitive.",
|
|
166
272
|
"Prefer local_workdir for file discovery, reading, and editing. Use local_shell for package managers, tests, build commands, git, and other process-level tasks.",
|
|
167
|
-
`Execution environment: platform=${platform}; shell=${shell}; access_mode=${accessMode}; default_timeout_ms=${timeoutMs}; max_output_bytes=${maxOutputBytes}.`,
|
|
273
|
+
`Execution environment: platform=${platform}; shell=${shell}; access_mode=${accessMode}; isolation_driver=${isolation.driver}; isolated=${isolation.isolated}; fallback=${isolation.fallback}; default_timeout_ms=${timeoutMs}; max_output_bytes=${maxOutputBytes}.`,
|
|
274
|
+
isolationRequestDescription(isolation.requested),
|
|
275
|
+
isolation.warnings.length > 0 ? `Isolation warning: ${isolation.warnings.join(" ")}` : "",
|
|
168
276
|
cwd ? `Default cwd: ${cwd}. Relative command paths resolve from this cwd unless workdir is set.` : "Relative command paths resolve from the configured cwd unless workdir is set.",
|
|
169
277
|
"The workdir parameter must be a relative child path of the configured cwd. Use workdir instead of cd when possible.",
|
|
170
278
|
"Absolute paths inside the command are permitted by the host OS if the user/process has permission; this tool is not a filesystem sandbox or security sandbox.",
|
|
@@ -206,7 +314,7 @@ function shellRequest(args) {
|
|
|
206
314
|
timeoutMs: optionalNumber(args.timeoutMs ?? args.timeout_ms, "timeout_ms"),
|
|
207
315
|
};
|
|
208
316
|
}
|
|
209
|
-
function shellApprovalRequired(request) {
|
|
317
|
+
function shellApprovalRequired(request, isolationStatus) {
|
|
210
318
|
return {
|
|
211
319
|
ok: false,
|
|
212
320
|
requires_approval: true,
|
|
@@ -215,9 +323,235 @@ function shellApprovalRequired(request) {
|
|
|
215
323
|
description: request.description,
|
|
216
324
|
workdir: request.workdir,
|
|
217
325
|
timeout_ms: request.timeoutMs,
|
|
326
|
+
shell_isolation: isolationStatus,
|
|
218
327
|
message: "local_shell command execution requires approval",
|
|
219
328
|
};
|
|
220
329
|
}
|
|
330
|
+
function directIsolationStatus(fallback, options = {}) {
|
|
331
|
+
const requested = normalizeIsolationOptions(options);
|
|
332
|
+
const warnings = directIsolationWarnings(fallback, requested);
|
|
333
|
+
return {
|
|
334
|
+
executor: "direct",
|
|
335
|
+
driver: "direct",
|
|
336
|
+
isolated: false,
|
|
337
|
+
fallback,
|
|
338
|
+
requested,
|
|
339
|
+
guarantees: {
|
|
340
|
+
filesystem: "none",
|
|
341
|
+
network: "allowed",
|
|
342
|
+
user: "host-user",
|
|
343
|
+
process: "host-process-tree",
|
|
344
|
+
resources: "timeout-only",
|
|
345
|
+
},
|
|
346
|
+
warnings,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function normalizeIsolationOptions(options = {}) {
|
|
350
|
+
return {
|
|
351
|
+
filesystem: options.filesystem ?? "host",
|
|
352
|
+
network: options.network ?? "allowed",
|
|
353
|
+
env: options.env ?? "inherit",
|
|
354
|
+
resources: {
|
|
355
|
+
memoryMb: options.resources?.memoryMb,
|
|
356
|
+
cpuCount: options.resources?.cpuCount,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function directIsolationWarnings(fallback, requested) {
|
|
361
|
+
const warnings = [
|
|
362
|
+
fallback
|
|
363
|
+
? "No local shell isolator is configured; falling back to direct host execution."
|
|
364
|
+
: "Direct host execution has no OS-level isolation.",
|
|
365
|
+
];
|
|
366
|
+
if (requested.filesystem !== "host") {
|
|
367
|
+
warnings.push(`Requested filesystem isolation (${requested.filesystem}) is not enforced by direct execution.`);
|
|
368
|
+
}
|
|
369
|
+
if (requested.network === "blocked") {
|
|
370
|
+
warnings.push("Requested network blocking is not enforced by direct execution.");
|
|
371
|
+
}
|
|
372
|
+
if (requested.env === "minimal") {
|
|
373
|
+
warnings.push("Requested minimal environment is not enforced by direct execution.");
|
|
374
|
+
}
|
|
375
|
+
if (requested.resources.memoryMb != null || requested.resources.cpuCount != null) {
|
|
376
|
+
warnings.push("Requested CPU or memory limits are not enforced by direct execution.");
|
|
377
|
+
}
|
|
378
|
+
return warnings;
|
|
379
|
+
}
|
|
380
|
+
function isolationRequestDescription(options) {
|
|
381
|
+
const resources = [
|
|
382
|
+
options.resources.memoryMb == null ? "" : `memory_mb=${options.resources.memoryMb}`,
|
|
383
|
+
options.resources.cpuCount == null ? "" : `cpu_count=${options.resources.cpuCount}`,
|
|
384
|
+
].filter(Boolean).join("; ");
|
|
385
|
+
return `Requested isolation: filesystem=${options.filesystem}; network=${options.network}; env=${options.env}${resources ? `; ${resources}` : ""}.`;
|
|
386
|
+
}
|
|
387
|
+
function withIsolationStatus(runner, status) {
|
|
388
|
+
Object.defineProperty(runner, "isolationStatus", {
|
|
389
|
+
configurable: true,
|
|
390
|
+
enumerable: true,
|
|
391
|
+
value: status,
|
|
392
|
+
});
|
|
393
|
+
return runner;
|
|
394
|
+
}
|
|
395
|
+
function isolatorStatusSync(executablePath, driver) {
|
|
396
|
+
const request = JSON.stringify({ id: "status", method: "status", params: {} });
|
|
397
|
+
const result = spawnSync(executablePath, ["--once", `--driver=${driver}`], {
|
|
398
|
+
input: request,
|
|
399
|
+
encoding: "utf8",
|
|
400
|
+
maxBuffer: 1024 * 1024,
|
|
401
|
+
windowsHide: true,
|
|
402
|
+
});
|
|
403
|
+
if (result.error) {
|
|
404
|
+
throw result.error;
|
|
405
|
+
}
|
|
406
|
+
if (result.status !== 0) {
|
|
407
|
+
throw new Error((result.stderr || result.stdout || `agent-isolator exited with status ${result.status}`).trim());
|
|
408
|
+
}
|
|
409
|
+
const envelope = parseIsolatorEnvelope(result.stdout);
|
|
410
|
+
if (envelope.error) {
|
|
411
|
+
throw new Error(envelope.error.message || envelope.error.code || "agent-isolator status failed");
|
|
412
|
+
}
|
|
413
|
+
return isolatorStatusResult(envelope.result);
|
|
414
|
+
}
|
|
415
|
+
async function isolatorRequest(executablePath, driver, envelope, context) {
|
|
416
|
+
return await new Promise((resolve, reject) => {
|
|
417
|
+
const child = spawn(executablePath, ["--once", `--driver=${driver}`], {
|
|
418
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
419
|
+
windowsHide: true,
|
|
420
|
+
});
|
|
421
|
+
const stdout = new BoundedText(1024 * 1024);
|
|
422
|
+
const stderr = new BoundedText(1024 * 1024);
|
|
423
|
+
let settled = false;
|
|
424
|
+
const finish = (error) => {
|
|
425
|
+
if (settled)
|
|
426
|
+
return;
|
|
427
|
+
settled = true;
|
|
428
|
+
context.signal?.removeEventListener("abort", abort);
|
|
429
|
+
if (error) {
|
|
430
|
+
reject(error);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const parsed = parseIsolatorEnvelope(stdout.text());
|
|
435
|
+
if (parsed.error) {
|
|
436
|
+
reject(new Error(parsed.error.message || parsed.error.code || "agent-isolator request failed"));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
resolve({ result: parsed.result });
|
|
440
|
+
}
|
|
441
|
+
catch (parseError) {
|
|
442
|
+
const details = stderr.text();
|
|
443
|
+
reject(new Error(`${parseError instanceof Error ? parseError.message : String(parseError)}${details ? `: ${details}` : ""}`));
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const abort = () => {
|
|
447
|
+
if (!child.killed)
|
|
448
|
+
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
449
|
+
};
|
|
450
|
+
if (context.signal?.aborted) {
|
|
451
|
+
abort();
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
context.signal?.addEventListener("abort", abort, { once: true });
|
|
455
|
+
}
|
|
456
|
+
child.stdout?.on("data", (chunk) => stdout.append(chunk));
|
|
457
|
+
child.stderr?.on("data", (chunk) => stderr.append(chunk));
|
|
458
|
+
child.on("error", finish);
|
|
459
|
+
child.on("close", (code) => {
|
|
460
|
+
if (code !== 0) {
|
|
461
|
+
finish(new Error((stderr.text() || stdout.text() || `agent-isolator exited with status ${code}`).trim()));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
finish();
|
|
465
|
+
});
|
|
466
|
+
child.stdin?.end(JSON.stringify(envelope));
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function parseIsolatorEnvelope(text) {
|
|
470
|
+
const trimmed = text.trim();
|
|
471
|
+
if (!trimmed) {
|
|
472
|
+
throw new Error("agent-isolator returned an empty response");
|
|
473
|
+
}
|
|
474
|
+
return JSON.parse(trimmed);
|
|
475
|
+
}
|
|
476
|
+
function isolatorStatusResult(value) {
|
|
477
|
+
if (!isRecord(value)) {
|
|
478
|
+
throw new Error("agent-isolator status result must be an object");
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
version: typeof value.version === "string" ? value.version : undefined,
|
|
482
|
+
driver: requiredString(value.driver, "driver"),
|
|
483
|
+
status: isolationStatusFromUnknown(value.status),
|
|
484
|
+
drivers: Array.isArray(value.drivers)
|
|
485
|
+
? value.drivers.filter(isRecord).map((item) => ({
|
|
486
|
+
name: String(item.name ?? ""),
|
|
487
|
+
platform: String(item.platform ?? ""),
|
|
488
|
+
available: item.available === true,
|
|
489
|
+
warnings: Array.isArray(item.warnings) ? item.warnings.map(String) : undefined,
|
|
490
|
+
}))
|
|
491
|
+
: undefined,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function isolatorRunResult(value) {
|
|
495
|
+
if (!isRecord(value)) {
|
|
496
|
+
throw new Error("agent-isolator run result must be an object");
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
ok: value.ok === true,
|
|
500
|
+
command: requiredString(value.command, "command"),
|
|
501
|
+
description: optionalString(value.description, "description"),
|
|
502
|
+
cwd: requiredString(value.cwd, "cwd"),
|
|
503
|
+
exit_code: typeof value.exit_code === "number" ? value.exit_code : null,
|
|
504
|
+
signal: typeof value.signal === "string" ? value.signal : null,
|
|
505
|
+
stdout: typeof value.stdout === "string" ? value.stdout : "",
|
|
506
|
+
stderr: typeof value.stderr === "string" ? value.stderr : "",
|
|
507
|
+
output: typeof value.output === "string" ? value.output : "(no output)",
|
|
508
|
+
duration_ms: typeof value.duration_ms === "number" ? value.duration_ms : 0,
|
|
509
|
+
timed_out: value.timed_out === true,
|
|
510
|
+
truncated: value.truncated === true,
|
|
511
|
+
shell_isolation: isolationStatusFromUnknown(value.shell_isolation),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function isolationStatusFromUnknown(value) {
|
|
515
|
+
if (!isRecord(value)) {
|
|
516
|
+
throw new Error("shell isolation status must be an object");
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
executor: value.executor === "isolator" ? "isolator" : "direct",
|
|
520
|
+
driver: requiredString(value.driver, "shell_isolation.driver"),
|
|
521
|
+
isolated: value.isolated === true,
|
|
522
|
+
fallback: value.fallback === true,
|
|
523
|
+
requested: normalizeIsolationOptions(isRecord(value.requested) ? isolationOptionsFromUnknown(value.requested) : {}),
|
|
524
|
+
guarantees: isolationGuaranteesFromUnknown(value.guarantees),
|
|
525
|
+
warnings: Array.isArray(value.warnings) ? value.warnings.map(String) : [],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function isolationOptionsFromUnknown(value) {
|
|
529
|
+
const resources = isRecord(value.resources) ? value.resources : {};
|
|
530
|
+
return {
|
|
531
|
+
filesystem: value.filesystem === "workdir-readonly" || value.filesystem === "workdir-readwrite" || value.filesystem === "host"
|
|
532
|
+
? value.filesystem
|
|
533
|
+
: undefined,
|
|
534
|
+
network: value.network === "blocked" || value.network === "allowed" ? value.network : undefined,
|
|
535
|
+
env: value.env === "minimal" || value.env === "inherit" ? value.env : undefined,
|
|
536
|
+
resources: {
|
|
537
|
+
memoryMb: typeof resources.memoryMb === "number" ? resources.memoryMb : undefined,
|
|
538
|
+
cpuCount: typeof resources.cpuCount === "number" ? resources.cpuCount : undefined,
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function isolationGuaranteesFromUnknown(value) {
|
|
543
|
+
const record = isRecord(value) ? value : {};
|
|
544
|
+
return {
|
|
545
|
+
filesystem: record.filesystem === "workdir-mounted" || record.filesystem === "policy-enforced" ? record.filesystem : "none",
|
|
546
|
+
network: record.network === "blocked" || record.network === "configurable" ? record.network : "allowed",
|
|
547
|
+
user: record.user === "unprivileged-user" || record.user === "namespace-user" ? record.user : "host-user",
|
|
548
|
+
process: record.process === "child-contained" || record.process === "pid-namespace" ? record.process : "host-process-tree",
|
|
549
|
+
resources: record.resources === "cpu-memory-limits" ? "cpu-memory-limits" : record.resources === "timeout-only" ? "timeout-only" : "none",
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function isRecord(value) {
|
|
553
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
554
|
+
}
|
|
221
555
|
function shellToolResult(result) {
|
|
222
556
|
return {
|
|
223
557
|
...result,
|
package/dist/local/tools.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare class LocalWorkdirDriver {
|
|
|
23
23
|
readonly accessMode: LocalWorkdirAccessMode;
|
|
24
24
|
constructor(workdir: LocalWorkdir, options?: LocalWorkdirDriverOptions);
|
|
25
25
|
dispatch(args: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
26
|
+
private dispatchUnsafe;
|
|
26
27
|
requiresApproval(args: Record<string, unknown>): boolean;
|
|
27
28
|
private dispatchApplyEdits;
|
|
28
29
|
private dispatchWrite;
|