@agent-api/cli 0.1.1 → 0.2.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/dist/agent/runner.d.ts +2 -0
- package/dist/agent/runner.js +3 -0
- package/dist/config.d.ts +24 -2
- package/dist/config.js +101 -11
- package/dist/conversation/index.js +8 -8
- package/dist/index.js +3 -2
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +94 -2
- package/dist/tui/ink/app.js +25 -1
- package/dist/tui/ink/components.d.ts +2 -1
- package/dist/tui/ink/components.js +4 -4
- package/dist/tui/workbench.d.ts +5 -2
- package/dist/tui/workbench.js +4 -1
- package/dist/workbench/command-controller.js +48 -0
- package/dist/workbench/isolator-installer.d.ts +29 -0
- package/dist/workbench/isolator-installer.js +208 -0
- package/dist/workbench/local-controller.d.ts +2 -0
- package/dist/workbench/local-controller.js +6 -1
- package/dist/workbench/session.js +3 -1
- package/dist/workbench/settings-controller.d.ts +27 -0
- package/dist/workbench/settings-controller.js +170 -2
- package/dist/workbench/shell-isolation.d.ts +20 -0
- package/dist/workbench/shell-isolation.js +13 -0
- package/dist/workbench/turn-controller.js +2 -0
- package/package.json +2 -2
|
@@ -99,6 +99,7 @@ export function createWorkbenchCommandController(options) {
|
|
|
99
99
|
contextEnabled: state.contextEnabled,
|
|
100
100
|
defaultPreset: state.defaultPreset,
|
|
101
101
|
renderMode: state.renderMode,
|
|
102
|
+
shellIsolation: state.shellIsolation,
|
|
102
103
|
}),
|
|
103
104
|
});
|
|
104
105
|
return;
|
|
@@ -140,6 +141,53 @@ export function createWorkbenchCommandController(options) {
|
|
|
140
141
|
dispatch({ type: "activity.add", level: "error", text: "Default preset save failed" });
|
|
141
142
|
}
|
|
142
143
|
}
|
|
144
|
+
if (command.field === "isolation") {
|
|
145
|
+
if (!command.value) {
|
|
146
|
+
dispatch({
|
|
147
|
+
type: "message.add",
|
|
148
|
+
role: "system",
|
|
149
|
+
text: options.settingsController.shellIsolationHelp(state.shellIsolation),
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const settings = await options.settingsController.saveShellIsolationMode(command.value);
|
|
155
|
+
dispatch({ type: "settings.set", settings: { shellIsolation: settings.shellIsolation } });
|
|
156
|
+
dispatch({ type: "message.add", role: "system", text: settings.message });
|
|
157
|
+
dispatch({ type: "activity.add", level: "success", text: settings.activity });
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
dispatch({ type: "message.add", role: "system", text: `Could not save shell isolation mode: ${userFacingError(error)}` });
|
|
161
|
+
dispatch({ type: "activity.add", level: "error", text: "Shell isolation save failed" });
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (command.field === "isolator") {
|
|
166
|
+
if (!command.value) {
|
|
167
|
+
dispatch({
|
|
168
|
+
type: "message.add",
|
|
169
|
+
role: "system",
|
|
170
|
+
text: options.settingsController.isolatorPathHelp(state.shellIsolation),
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const [subcommand = "", ...rest] = command.value.split(/\s+/);
|
|
176
|
+
const value = rest.join(" ").trim();
|
|
177
|
+
const settings = subcommand === "source"
|
|
178
|
+
? await options.settingsController.saveIsolatorSource(value)
|
|
179
|
+
: subcommand === "path"
|
|
180
|
+
? await options.settingsController.saveIsolatorPath(value)
|
|
181
|
+
: await options.settingsController.saveIsolatorPath(command.value);
|
|
182
|
+
dispatch({ type: "settings.set", settings: { shellIsolation: settings.shellIsolation } });
|
|
183
|
+
dispatch({ type: "message.add", role: "system", text: settings.message });
|
|
184
|
+
dispatch({ type: "activity.add", level: "success", text: settings.activity });
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
dispatch({ type: "message.add", role: "system", text: `Could not save isolator path: ${userFacingError(error)}` });
|
|
188
|
+
dispatch({ type: "activity.add", level: "error", text: "Isolator path save failed" });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
143
191
|
}
|
|
144
192
|
async function runPresetCommand(value) {
|
|
145
193
|
const state = options.engine.snapshot();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface IsolatorInstallConfig {
|
|
2
|
+
sourceURL?: string | null;
|
|
3
|
+
executablePath?: string | null;
|
|
4
|
+
sha256?: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface IsolatorInstallResult {
|
|
7
|
+
executablePath: string;
|
|
8
|
+
sourceURL: string;
|
|
9
|
+
bytes: number;
|
|
10
|
+
sha256: string;
|
|
11
|
+
replaced: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface IsolatorEnsureResult {
|
|
14
|
+
executablePath: string;
|
|
15
|
+
sourceURL: string;
|
|
16
|
+
sha256?: string | null;
|
|
17
|
+
repaired: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface IsolatorInstallOptions {
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
probeTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function installConfiguredIsolator(config: IsolatorInstallConfig, options?: IsolatorInstallOptions): Promise<IsolatorInstallResult>;
|
|
24
|
+
export declare function validateInstalledIsolator(executablePath: string, options?: Pick<IsolatorInstallOptions, "probeTimeoutMs">): Promise<string>;
|
|
25
|
+
export declare function ensureConfiguredIsolator(config: IsolatorInstallConfig, options?: IsolatorInstallOptions): Promise<IsolatorEnsureResult>;
|
|
26
|
+
export declare function relocateInstalledIsolator(fromPath: string, toPath: string, options?: Pick<IsolatorInstallOptions, "probeTimeoutMs">): Promise<string>;
|
|
27
|
+
export declare function defaultIsolatorInstallPath(): string;
|
|
28
|
+
export declare function normalizeSourceURL(value: string | null | undefined): string;
|
|
29
|
+
export declare function normalizeInstallPath(value: string | null | undefined): string;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { access, chmod, copyFile, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { runtime } from "../runtime/index.js";
|
|
7
|
+
export async function installConfiguredIsolator(config, options = {}) {
|
|
8
|
+
const sourceURL = normalizeSourceURL(config.sourceURL);
|
|
9
|
+
const executablePath = normalizeInstallPath(config.executablePath);
|
|
10
|
+
const targetDir = path.dirname(executablePath);
|
|
11
|
+
const existing = await existingTargetState(executablePath);
|
|
12
|
+
await mkdir(targetDir, { recursive: true });
|
|
13
|
+
await ensureWritableDirectory(targetDir);
|
|
14
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
15
|
+
const response = await fetchImpl(sourceURL);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`isolator download failed: HTTP ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
20
|
+
if (body.length === 0) {
|
|
21
|
+
throw new Error("isolator download failed: empty response body");
|
|
22
|
+
}
|
|
23
|
+
const actualSha256 = createHash("sha256").update(body).digest("hex");
|
|
24
|
+
const expectedSha256 = config.sha256?.trim().toLowerCase();
|
|
25
|
+
if (expectedSha256 && actualSha256 !== expectedSha256) {
|
|
26
|
+
throw new Error(`isolator checksum mismatch: expected ${expectedSha256}, got ${actualSha256}`);
|
|
27
|
+
}
|
|
28
|
+
const tempPath = path.join(targetDir, `.${path.basename(executablePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
29
|
+
try {
|
|
30
|
+
await writeFile(tempPath, body, { mode: 0o700 });
|
|
31
|
+
if (process.platform !== "win32") {
|
|
32
|
+
await chmod(tempPath, 0o700);
|
|
33
|
+
}
|
|
34
|
+
await probeIsolator(tempPath, options.probeTimeoutMs ?? 10_000);
|
|
35
|
+
await rename(tempPath, executablePath);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
await rm(tempPath, { force: true });
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
executablePath,
|
|
43
|
+
sourceURL,
|
|
44
|
+
bytes: body.length,
|
|
45
|
+
sha256: actualSha256,
|
|
46
|
+
replaced: existing === "file",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function validateInstalledIsolator(executablePath, options = {}) {
|
|
50
|
+
const normalized = normalizeInstallPath(executablePath);
|
|
51
|
+
const state = await existingTargetState(normalized);
|
|
52
|
+
if (state !== "file") {
|
|
53
|
+
throw new Error(`isolator executable does not exist: ${normalized}`);
|
|
54
|
+
}
|
|
55
|
+
await probeIsolator(normalized, options.probeTimeoutMs ?? 10_000);
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
export async function ensureConfiguredIsolator(config, options = {}) {
|
|
59
|
+
const sourceURL = normalizeSourceURL(config.sourceURL);
|
|
60
|
+
const executablePath = normalizeInstallPath(config.executablePath);
|
|
61
|
+
try {
|
|
62
|
+
await validateInstalledIsolator(executablePath, options);
|
|
63
|
+
return {
|
|
64
|
+
executablePath,
|
|
65
|
+
sourceURL,
|
|
66
|
+
sha256: config.sha256,
|
|
67
|
+
repaired: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
const result = await installConfiguredIsolator({ sourceURL, executablePath, sha256: config.sha256 }, options);
|
|
72
|
+
return {
|
|
73
|
+
executablePath: result.executablePath,
|
|
74
|
+
sourceURL: result.sourceURL,
|
|
75
|
+
sha256: result.sha256,
|
|
76
|
+
repaired: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function relocateInstalledIsolator(fromPath, toPath, options = {}) {
|
|
81
|
+
const source = await validateInstalledIsolator(fromPath, options);
|
|
82
|
+
const target = normalizeInstallPath(toPath);
|
|
83
|
+
if (source === target)
|
|
84
|
+
return target;
|
|
85
|
+
const targetDir = path.dirname(target);
|
|
86
|
+
await existingTargetState(target);
|
|
87
|
+
await mkdir(targetDir, { recursive: true });
|
|
88
|
+
await ensureWritableDirectory(targetDir);
|
|
89
|
+
const tempPath = path.join(targetDir, `.${path.basename(target)}.${process.pid}.${Date.now()}.tmp`);
|
|
90
|
+
try {
|
|
91
|
+
await copyFile(source, tempPath);
|
|
92
|
+
if (process.platform !== "win32") {
|
|
93
|
+
await chmod(tempPath, 0o700);
|
|
94
|
+
}
|
|
95
|
+
await probeIsolator(tempPath, options.probeTimeoutMs ?? 10_000);
|
|
96
|
+
await rename(tempPath, target);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
await rm(tempPath, { force: true });
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
return target;
|
|
103
|
+
}
|
|
104
|
+
export function defaultIsolatorInstallPath() {
|
|
105
|
+
return path.join(runtime.dirs.data, "bin", process.platform === "win32" ? "agent-isolator.exe" : "agent-isolator");
|
|
106
|
+
}
|
|
107
|
+
export function normalizeSourceURL(value) {
|
|
108
|
+
const trimmed = value?.trim();
|
|
109
|
+
if (!trimmed)
|
|
110
|
+
throw new Error("isolator sourceURL is required");
|
|
111
|
+
let url;
|
|
112
|
+
try {
|
|
113
|
+
url = new URL(trimmed);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error("isolator sourceURL must be a valid URL");
|
|
117
|
+
}
|
|
118
|
+
if (url.protocol !== "https:") {
|
|
119
|
+
throw new Error("isolator sourceURL must use https");
|
|
120
|
+
}
|
|
121
|
+
return url.toString();
|
|
122
|
+
}
|
|
123
|
+
export function normalizeInstallPath(value) {
|
|
124
|
+
const trimmed = value?.trim();
|
|
125
|
+
if (!trimmed)
|
|
126
|
+
throw new Error("isolator executablePath is required");
|
|
127
|
+
if (!path.isAbsolute(trimmed)) {
|
|
128
|
+
throw new Error("isolator executablePath must be absolute");
|
|
129
|
+
}
|
|
130
|
+
const normalized = path.normalize(trimmed);
|
|
131
|
+
const root = path.parse(normalized).root;
|
|
132
|
+
if (normalized === root) {
|
|
133
|
+
throw new Error("isolator executablePath cannot be a filesystem root");
|
|
134
|
+
}
|
|
135
|
+
if (path.basename(normalized).startsWith(".")) {
|
|
136
|
+
throw new Error("isolator executablePath must not be a hidden temp-style filename");
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
async function existingTargetState(file) {
|
|
141
|
+
try {
|
|
142
|
+
const info = await stat(file);
|
|
143
|
+
if (!info.isFile()) {
|
|
144
|
+
throw new Error(`isolator executablePath exists but is not a file: ${file}`);
|
|
145
|
+
}
|
|
146
|
+
return "file";
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error?.code === "ENOENT")
|
|
150
|
+
return "missing";
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function ensureWritableDirectory(dir) {
|
|
155
|
+
try {
|
|
156
|
+
await access(dir, fsConstants.W_OK);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw new Error(`isolator target directory is not writable: ${dir}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function probeIsolator(executablePath, timeoutMs) {
|
|
163
|
+
const request = JSON.stringify({ id: "status", method: "status", params: {} });
|
|
164
|
+
const child = spawn(executablePath, ["--once", "--driver=auto"], {
|
|
165
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
166
|
+
windowsHide: true,
|
|
167
|
+
});
|
|
168
|
+
const chunks = [];
|
|
169
|
+
const errors = [];
|
|
170
|
+
child.stdout.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
171
|
+
child.stderr.on("data", (chunk) => errors.push(Buffer.from(chunk)));
|
|
172
|
+
child.stdin.end(`${request}\n`);
|
|
173
|
+
const code = await new Promise((resolve, reject) => {
|
|
174
|
+
const timer = setTimeout(() => {
|
|
175
|
+
child.kill();
|
|
176
|
+
reject(new Error("isolator probe timed out"));
|
|
177
|
+
}, timeoutMs);
|
|
178
|
+
child.on("error", (error) => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
reject(error);
|
|
181
|
+
});
|
|
182
|
+
child.on("close", (exitCode) => {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
resolve(exitCode);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
const stdout = Buffer.concat(chunks).toString("utf8").trim();
|
|
188
|
+
const stderr = Buffer.concat(errors).toString("utf8").trim();
|
|
189
|
+
if (code !== 0) {
|
|
190
|
+
throw new Error(stderr || stdout || `isolator probe failed with exit code ${code}`);
|
|
191
|
+
}
|
|
192
|
+
const line = stdout.split(/\r?\n/).find(Boolean);
|
|
193
|
+
if (!line)
|
|
194
|
+
throw new Error("isolator probe returned no response");
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(line);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
throw new Error("isolator probe returned invalid JSON");
|
|
201
|
+
}
|
|
202
|
+
if (parsed?.error) {
|
|
203
|
+
throw new Error(parsed.error.message || parsed.error.code || "isolator probe failed");
|
|
204
|
+
}
|
|
205
|
+
if (!parsed?.result?.status?.driver) {
|
|
206
|
+
throw new Error("isolator probe response did not include shell isolation status");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { openWorkdir } from "../workdir/index.js";
|
|
2
2
|
import type { WorkbenchState, WorkbenchWorkdirStatus } from "../tui/workbench.js";
|
|
3
|
+
import type { ShellIsolationPreferences } from "./shell-isolation.js";
|
|
3
4
|
export interface WorkbenchLocalController {
|
|
4
5
|
load(path?: string): Promise<WorkbenchWorkdirStatus>;
|
|
5
6
|
isLoaded(): boolean;
|
|
@@ -13,6 +14,7 @@ export interface WorkbenchLocalController {
|
|
|
13
14
|
}
|
|
14
15
|
export interface WorkbenchLocalControllerOptions {
|
|
15
16
|
openWorkdirImpl?: typeof openWorkdir;
|
|
17
|
+
getShellIsolation?: () => ShellIsolationPreferences | undefined;
|
|
16
18
|
}
|
|
17
19
|
type LocalApprovalLike = NonNullable<WorkbenchState["pendingLocalTool"]>;
|
|
18
20
|
export declare function createWorkbenchLocalController(options?: WorkbenchLocalControllerOptions): WorkbenchLocalController;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLocalShellToolRegistry, createLocalWorkdirToolRegistry } from "@agent-api/sdk/local";
|
|
2
2
|
import { openWorkdir } from "../workdir/index.js";
|
|
3
|
+
import { localShellIsolationOptions } from "./shell-isolation.js";
|
|
3
4
|
export function createWorkbenchLocalController(options = {}) {
|
|
4
5
|
const openWorkdirImpl = options.openWorkdirImpl ?? openWorkdir;
|
|
5
6
|
let workdir = null;
|
|
@@ -54,7 +55,11 @@ export function createWorkbenchLocalController(options = {}) {
|
|
|
54
55
|
async applyApproval(approval) {
|
|
55
56
|
const current = requireWorkdir();
|
|
56
57
|
const workdirRegistry = createLocalWorkdirToolRegistry(current.workdir, { accessMode: "full" });
|
|
57
|
-
const shellRegistry = createLocalShellToolRegistry({
|
|
58
|
+
const shellRegistry = createLocalShellToolRegistry({
|
|
59
|
+
workdir: current.workdir,
|
|
60
|
+
accessMode: "full",
|
|
61
|
+
...localShellIsolationOptions(options.getShellIsolation?.()),
|
|
62
|
+
});
|
|
58
63
|
if (approval.name === workdirRegistry.toolName) {
|
|
59
64
|
return await workdirRegistry.execute(approval.name, approval.arguments);
|
|
60
65
|
}
|
|
@@ -14,7 +14,9 @@ export function createWorkbenchSession(options) {
|
|
|
14
14
|
model: options.baseOptions.model,
|
|
15
15
|
preset: options.baseOptions.preset,
|
|
16
16
|
});
|
|
17
|
-
const local = createWorkbenchLocalController(
|
|
17
|
+
const local = createWorkbenchLocalController({
|
|
18
|
+
getShellIsolation: () => engine.snapshot().shellIsolation,
|
|
19
|
+
});
|
|
18
20
|
const runtime = createWorkbenchRuntimeController({ dispatch: engine.dispatch });
|
|
19
21
|
const turn = createWorkbenchTurnController({
|
|
20
22
|
baseOptions: options.baseOptions,
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { clearPresetToolCatalogCache, isAvailablePreset, listAvailablePresets, type AgentRunOptions } from "../agent.js";
|
|
2
2
|
import { loadWorkbenchPreferences, updateWorkbenchPreferences, type WorkbenchPreferences } from "../config.js";
|
|
3
3
|
import type { RenderMode } from "../tui/workbench.js";
|
|
4
|
+
import type { ShellIsolationMode, ShellIsolationPreferences } from "./shell-isolation.js";
|
|
5
|
+
import { type IsolatorInstallOptions } from "./isolator-installer.js";
|
|
4
6
|
export interface WorkbenchSettingsSnapshot {
|
|
5
7
|
defaultPreset?: string | null;
|
|
6
8
|
runPreset?: string;
|
|
9
|
+
shellIsolation?: ShellIsolationPreferences;
|
|
10
|
+
activity?: string;
|
|
11
|
+
warning?: string;
|
|
7
12
|
}
|
|
8
13
|
export interface WorkbenchSettingsController {
|
|
9
14
|
loadInitial(options: Pick<AgentRunOptions, "modelExplicit" | "preset" | "presetExplicit">): Promise<WorkbenchSettingsSnapshot>;
|
|
@@ -15,6 +20,18 @@ export interface WorkbenchSettingsController {
|
|
|
15
20
|
message: string;
|
|
16
21
|
activity: string;
|
|
17
22
|
}>;
|
|
23
|
+
saveShellIsolationMode(value: string): Promise<WorkbenchSettingsSnapshot & {
|
|
24
|
+
message: string;
|
|
25
|
+
activity: string;
|
|
26
|
+
}>;
|
|
27
|
+
saveIsolatorPath(value: string): Promise<WorkbenchSettingsSnapshot & {
|
|
28
|
+
message: string;
|
|
29
|
+
activity: string;
|
|
30
|
+
}>;
|
|
31
|
+
saveIsolatorSource(value: string): Promise<WorkbenchSettingsSnapshot & {
|
|
32
|
+
message: string;
|
|
33
|
+
activity: string;
|
|
34
|
+
}>;
|
|
18
35
|
validatePreset(profileName: string | undefined, preset: string): Promise<boolean>;
|
|
19
36
|
presetListText(input: {
|
|
20
37
|
profileName?: string;
|
|
@@ -29,8 +46,11 @@ export interface WorkbenchSettingsController {
|
|
|
29
46
|
runModel?: string;
|
|
30
47
|
runPreset?: string;
|
|
31
48
|
renderMode: RenderMode;
|
|
49
|
+
shellIsolation?: ShellIsolationPreferences;
|
|
32
50
|
}): string;
|
|
33
51
|
defaultPresetHelp(defaultPreset?: string | null): string;
|
|
52
|
+
shellIsolationHelp(shellIsolation?: ShellIsolationPreferences): string;
|
|
53
|
+
isolatorPathHelp(shellIsolation?: ShellIsolationPreferences): string;
|
|
34
54
|
clearPresetToolCatalogCache(baseURL?: string): void;
|
|
35
55
|
}
|
|
36
56
|
export interface WorkbenchSettingsControllerOptions {
|
|
@@ -39,6 +59,7 @@ export interface WorkbenchSettingsControllerOptions {
|
|
|
39
59
|
isAvailablePresetImpl?: typeof isAvailablePreset;
|
|
40
60
|
listAvailablePresetsImpl?: typeof listAvailablePresets;
|
|
41
61
|
clearPresetToolCatalogCacheImpl?: typeof clearPresetToolCatalogCache;
|
|
62
|
+
isolatorInstallOptions?: IsolatorInstallOptions;
|
|
42
63
|
formatError?: (error: unknown) => string;
|
|
43
64
|
}
|
|
44
65
|
export declare function createWorkbenchSettingsController(options?: WorkbenchSettingsControllerOptions): WorkbenchSettingsController;
|
|
@@ -48,5 +69,11 @@ export declare class UnknownPresetError extends Error {
|
|
|
48
69
|
}
|
|
49
70
|
export declare function normalizeDefaultPreset(value: string): string | null | undefined;
|
|
50
71
|
export declare function formatDefaultPreset(value: string | null | undefined): string;
|
|
72
|
+
export declare function normalizeShellIsolationMode(value: string): ShellIsolationMode;
|
|
73
|
+
export declare function normalizeIsolatorPath(value: string): string | null;
|
|
74
|
+
export declare function normalizeIsolatorSource(value: string): string | null;
|
|
75
|
+
export declare function formatShellIsolation(value: ShellIsolationPreferences | undefined): ShellIsolationMode;
|
|
76
|
+
export declare function formatIsolatorPath(value: ShellIsolationPreferences | undefined): string;
|
|
77
|
+
export declare function formatIsolatorSource(value: ShellIsolationPreferences | undefined): string;
|
|
51
78
|
export declare function effectiveDefaultPreset(preferences: WorkbenchPreferences, builtInPreset?: string): string | undefined;
|
|
52
79
|
export declare function formatPresetList(presets: Awaited<ReturnType<typeof listAvailablePresets>>, currentPreset?: string): string[];
|
|
@@ -1,22 +1,101 @@
|
|
|
1
1
|
import { clearPresetToolCatalogCache, isAvailablePreset, listAvailablePresets, } from "../agent.js";
|
|
2
2
|
import { loadWorkbenchPreferences, updateWorkbenchPreferences, } from "../config.js";
|
|
3
|
+
import { ensureConfiguredIsolator, installConfiguredIsolator, normalizeSourceURL, relocateInstalledIsolator, validateInstalledIsolator, } from "./isolator-installer.js";
|
|
3
4
|
export function createWorkbenchSettingsController(options = {}) {
|
|
4
5
|
const loadWorkbenchPreferencesImpl = options.loadWorkbenchPreferencesImpl ?? loadWorkbenchPreferences;
|
|
5
6
|
const updateWorkbenchPreferencesImpl = options.updateWorkbenchPreferencesImpl ?? updateWorkbenchPreferences;
|
|
6
7
|
const isAvailablePresetImpl = options.isAvailablePresetImpl ?? isAvailablePreset;
|
|
7
8
|
const listAvailablePresetsImpl = options.listAvailablePresetsImpl ?? listAvailablePresets;
|
|
8
9
|
const clearPresetToolCatalogCacheImpl = options.clearPresetToolCatalogCacheImpl ?? clearPresetToolCatalogCache;
|
|
10
|
+
const isolatorInstallOptions = options.isolatorInstallOptions ?? {};
|
|
9
11
|
const formatError = options.formatError ?? userFacingError;
|
|
10
12
|
return {
|
|
11
13
|
async loadInitial(agentOptions) {
|
|
12
|
-
const
|
|
14
|
+
const loadedPreferences = await loadWorkbenchPreferencesImpl();
|
|
15
|
+
const { preferences, activity, warning } = await reconcileConfiguredIsolator(loadedPreferences, updateWorkbenchPreferencesImpl, isolatorInstallOptions, formatError);
|
|
13
16
|
return {
|
|
14
17
|
defaultPreset: preferences.defaultPreset,
|
|
18
|
+
...(preferences.isolation ? { shellIsolation: preferences.isolation } : {}),
|
|
19
|
+
...(activity ? { activity } : {}),
|
|
20
|
+
...(warning ? { warning } : {}),
|
|
15
21
|
runPreset: shouldApplyDefaultPreset(agentOptions)
|
|
16
22
|
? effectiveDefaultPreset(preferences, agentOptions.preset)
|
|
17
23
|
: undefined,
|
|
18
24
|
};
|
|
19
25
|
},
|
|
26
|
+
async saveShellIsolationMode(value) {
|
|
27
|
+
const mode = normalizeShellIsolationMode(value);
|
|
28
|
+
const preferences = await updateWorkbenchPreferencesImpl({ isolation: { mode } });
|
|
29
|
+
return {
|
|
30
|
+
...settingsSnapshot(preferences),
|
|
31
|
+
message: `Saved shell isolation mode: ${formatShellIsolation(preferences.isolation)}.`,
|
|
32
|
+
activity: `Shell isolation mode saved: ${preferences.isolation?.mode ?? "auto"}`,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
async saveIsolatorPath(value) {
|
|
36
|
+
const executablePath = normalizeIsolatorPath(value);
|
|
37
|
+
if (!executablePath) {
|
|
38
|
+
const preferences = await updateWorkbenchPreferencesImpl({ isolation: { executablePath } });
|
|
39
|
+
return {
|
|
40
|
+
...settingsSnapshot(preferences),
|
|
41
|
+
message: `Saved isolator path: ${formatIsolatorPath(preferences.isolation)}.`,
|
|
42
|
+
activity: "Isolator path cleared",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const current = (await loadWorkbenchPreferencesImpl()).isolation;
|
|
46
|
+
if (current?.sourceURL) {
|
|
47
|
+
const result = await installConfiguredIsolator({
|
|
48
|
+
sourceURL: current.sourceURL,
|
|
49
|
+
executablePath,
|
|
50
|
+
sha256: current.sha256,
|
|
51
|
+
}, isolatorInstallOptions);
|
|
52
|
+
const preferences = await updateWorkbenchPreferencesImpl({
|
|
53
|
+
isolation: { executablePath: result.executablePath, sourceURL: result.sourceURL, sha256: result.sha256, installSkipped: false },
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
...settingsSnapshot(preferences),
|
|
57
|
+
message: `Installed isolator to ${result.executablePath}.`,
|
|
58
|
+
activity: result.replaced ? "Isolator refreshed" : "Isolator installed",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const validatedPath = current?.executablePath
|
|
62
|
+
? await relocateInstalledIsolator(current.executablePath, executablePath, isolatorInstallOptions)
|
|
63
|
+
: await validateInstalledIsolator(executablePath, isolatorInstallOptions);
|
|
64
|
+
const preferences = await updateWorkbenchPreferencesImpl({ isolation: { executablePath: validatedPath, installSkipped: false } });
|
|
65
|
+
return {
|
|
66
|
+
...settingsSnapshot(preferences),
|
|
67
|
+
message: `Saved verified isolator path: ${formatIsolatorPath(preferences.isolation)}.`,
|
|
68
|
+
activity: "Isolator path verified",
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
async saveIsolatorSource(value) {
|
|
72
|
+
const sourceURL = normalizeIsolatorSource(value);
|
|
73
|
+
const current = (await loadWorkbenchPreferencesImpl()).isolation;
|
|
74
|
+
if (!sourceURL) {
|
|
75
|
+
const preferences = await updateWorkbenchPreferencesImpl({ isolation: { sourceURL, sha256: null } });
|
|
76
|
+
return {
|
|
77
|
+
...settingsSnapshot(preferences),
|
|
78
|
+
message: "Cleared isolator source URL.",
|
|
79
|
+
activity: "Isolator source cleared",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (!current?.executablePath) {
|
|
83
|
+
throw new Error("Set a verified isolator path before saving a source URL.");
|
|
84
|
+
}
|
|
85
|
+
const result = await installConfiguredIsolator({
|
|
86
|
+
sourceURL,
|
|
87
|
+
executablePath: current.executablePath,
|
|
88
|
+
sha256: current.sha256,
|
|
89
|
+
}, isolatorInstallOptions);
|
|
90
|
+
const preferences = await updateWorkbenchPreferencesImpl({
|
|
91
|
+
isolation: { sourceURL: result.sourceURL, executablePath: result.executablePath, sha256: result.sha256, installSkipped: false },
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
...settingsSnapshot(preferences),
|
|
95
|
+
message: `Installed isolator from ${result.sourceURL}.`,
|
|
96
|
+
activity: result.replaced ? "Isolator refreshed from source" : "Isolator installed from source",
|
|
97
|
+
};
|
|
98
|
+
},
|
|
20
99
|
async saveDefaultPreset(input) {
|
|
21
100
|
const normalized = normalizeDefaultPreset(input.value);
|
|
22
101
|
if (typeof normalized === "string" && !(await isAvailablePresetImpl(input.profileName, normalized))) {
|
|
@@ -25,6 +104,7 @@ export function createWorkbenchSettingsController(options = {}) {
|
|
|
25
104
|
const preferences = await updateWorkbenchPreferencesImpl({ defaultPreset: normalized });
|
|
26
105
|
return {
|
|
27
106
|
defaultPreset: preferences.defaultPreset,
|
|
107
|
+
...(preferences.isolation ? { shellIsolation: preferences.isolation } : {}),
|
|
28
108
|
runPreset: shouldApplyDefaultPreset(input.options)
|
|
29
109
|
? effectiveDefaultPreset(preferences, input.options.preset)
|
|
30
110
|
: undefined,
|
|
@@ -57,6 +137,16 @@ export function createWorkbenchSettingsController(options = {}) {
|
|
|
57
137
|
defaultPresetHelp(defaultPreset) {
|
|
58
138
|
return `Default preset: ${formatDefaultPreset(defaultPreset)}. Use /config preset <name>, /config preset none, or /config preset reset.`;
|
|
59
139
|
},
|
|
140
|
+
shellIsolationHelp(shellIsolation) {
|
|
141
|
+
return `Shell isolation: ${formatShellIsolation(shellIsolation)}. Use /config isolation none, /config isolation auto, or /config isolation required.`;
|
|
142
|
+
},
|
|
143
|
+
isolatorPathHelp(shellIsolation) {
|
|
144
|
+
return [
|
|
145
|
+
`Isolator path: ${formatIsolatorPath(shellIsolation)}`,
|
|
146
|
+
`Isolator source: ${formatIsolatorSource(shellIsolation)}`,
|
|
147
|
+
"Use /config isolator path <absolute-path>, /config isolator source <https-url>, or /config isolator none to clear the path.",
|
|
148
|
+
].join("\n");
|
|
149
|
+
},
|
|
60
150
|
clearPresetToolCatalogCache(baseURL) {
|
|
61
151
|
clearPresetToolCatalogCacheImpl(baseURL);
|
|
62
152
|
},
|
|
@@ -84,6 +174,49 @@ export function formatDefaultPreset(value) {
|
|
|
84
174
|
return "built-in (pro-search)";
|
|
85
175
|
return value ?? "none";
|
|
86
176
|
}
|
|
177
|
+
function settingsSnapshot(preferences) {
|
|
178
|
+
return {
|
|
179
|
+
...("defaultPreset" in preferences ? { defaultPreset: preferences.defaultPreset } : {}),
|
|
180
|
+
...(preferences.isolation ? { shellIsolation: preferences.isolation } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function normalizeShellIsolationMode(value) {
|
|
184
|
+
const lowered = value.trim().toLowerCase();
|
|
185
|
+
if (lowered === "none" || lowered === "off" || lowered === "disable" || lowered === "disabled")
|
|
186
|
+
return "none";
|
|
187
|
+
if (lowered === "auto" || lowered === "default")
|
|
188
|
+
return "auto";
|
|
189
|
+
if (lowered === "required" || lowered === "require" || lowered === "strict")
|
|
190
|
+
return "required";
|
|
191
|
+
throw new Error("Unknown shell isolation mode. Use none, auto, or required.");
|
|
192
|
+
}
|
|
193
|
+
export function normalizeIsolatorPath(value) {
|
|
194
|
+
const trimmed = value.trim();
|
|
195
|
+
if (!trimmed)
|
|
196
|
+
throw new Error("Usage: /config isolator <path>, or /config isolator none.");
|
|
197
|
+
const lowered = trimmed.toLowerCase();
|
|
198
|
+
if (["none", "off", "disable", "disabled", "clear", "reset"].includes(lowered))
|
|
199
|
+
return null;
|
|
200
|
+
return trimmed;
|
|
201
|
+
}
|
|
202
|
+
export function normalizeIsolatorSource(value) {
|
|
203
|
+
const trimmed = value.trim();
|
|
204
|
+
if (!trimmed)
|
|
205
|
+
throw new Error("Usage: /config isolator source <https-url>, or /config isolator source none.");
|
|
206
|
+
const lowered = trimmed.toLowerCase();
|
|
207
|
+
if (["none", "off", "disable", "disabled", "clear", "reset"].includes(lowered))
|
|
208
|
+
return null;
|
|
209
|
+
return normalizeSourceURL(trimmed);
|
|
210
|
+
}
|
|
211
|
+
export function formatShellIsolation(value) {
|
|
212
|
+
return value?.mode ?? "auto";
|
|
213
|
+
}
|
|
214
|
+
export function formatIsolatorPath(value) {
|
|
215
|
+
return value?.executablePath || process.env.AGENT_ISOLATOR_PATH || "not configured";
|
|
216
|
+
}
|
|
217
|
+
export function formatIsolatorSource(value) {
|
|
218
|
+
return value?.sourceURL || "not configured";
|
|
219
|
+
}
|
|
87
220
|
export function effectiveDefaultPreset(preferences, builtInPreset) {
|
|
88
221
|
if ("defaultPreset" in preferences)
|
|
89
222
|
return preferences.defaultPreset ?? undefined;
|
|
@@ -101,13 +234,16 @@ export function formatPresetList(presets, currentPreset) {
|
|
|
101
234
|
function shouldApplyDefaultPreset(options) {
|
|
102
235
|
return !options.presetExplicit && !options.modelExplicit;
|
|
103
236
|
}
|
|
104
|
-
function runConfigText({ accessMode, contextEnabled, defaultPreset, profileName, runModel, runPreset, renderMode, }) {
|
|
237
|
+
function runConfigText({ accessMode, contextEnabled, defaultPreset, profileName, runModel, runPreset, renderMode, shellIsolation, }) {
|
|
105
238
|
return [
|
|
106
239
|
`Profile: ${profileName}`,
|
|
107
240
|
`Preset: ${runPreset || "none"}`,
|
|
108
241
|
`Default preset: ${formatDefaultPreset(defaultPreset)}`,
|
|
109
242
|
`Model: ${runModel || "auto"}`,
|
|
110
243
|
`Render mode: ${renderMode}`,
|
|
244
|
+
`Shell isolation: ${formatShellIsolation(shellIsolation)}`,
|
|
245
|
+
`Isolator path: ${formatIsolatorPath(shellIsolation)}`,
|
|
246
|
+
`Isolator source: ${formatIsolatorSource(shellIsolation)}`,
|
|
111
247
|
`local_workdir tool: ${contextEnabled ? "on" : "off"}`,
|
|
112
248
|
`local_shell tool: ${contextEnabled ? "on" : "off"}`,
|
|
113
249
|
`Local access: ${accessMode}`,
|
|
@@ -118,3 +254,35 @@ function userFacingError(error) {
|
|
|
118
254
|
return error.message;
|
|
119
255
|
return String(error);
|
|
120
256
|
}
|
|
257
|
+
async function reconcileConfiguredIsolator(preferences, updatePreferences, installOptions, formatError) {
|
|
258
|
+
const isolation = preferences.isolation;
|
|
259
|
+
if (!isolation?.sourceURL || !isolation.executablePath)
|
|
260
|
+
return { preferences };
|
|
261
|
+
try {
|
|
262
|
+
const result = await ensureConfiguredIsolator({
|
|
263
|
+
sourceURL: isolation.sourceURL,
|
|
264
|
+
executablePath: isolation.executablePath,
|
|
265
|
+
sha256: isolation.sha256,
|
|
266
|
+
}, installOptions);
|
|
267
|
+
if (!result.repaired)
|
|
268
|
+
return { preferences };
|
|
269
|
+
const updated = await updatePreferences({
|
|
270
|
+
isolation: {
|
|
271
|
+
sourceURL: result.sourceURL,
|
|
272
|
+
executablePath: result.executablePath,
|
|
273
|
+
sha256: result.sha256,
|
|
274
|
+
installSkipped: false,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
preferences: updated,
|
|
279
|
+
activity: `Reinstalled isolator: ${result.executablePath}`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
return {
|
|
284
|
+
preferences,
|
|
285
|
+
warning: `Configured isolator is unavailable: ${formatError(error)}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ShellIsolationMode = "none" | "auto" | "required";
|
|
2
|
+
export interface ShellIsolationPreferences {
|
|
3
|
+
mode?: ShellIsolationMode;
|
|
4
|
+
executablePath?: string | null;
|
|
5
|
+
version?: string | null;
|
|
6
|
+
sourceURL?: string | null;
|
|
7
|
+
sha256?: string | null;
|
|
8
|
+
installSkipped?: boolean | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function localShellIsolationOptions(preferences?: ShellIsolationPreferences): {
|
|
11
|
+
readonly isolator?: {
|
|
12
|
+
executablePath: string;
|
|
13
|
+
} | undefined;
|
|
14
|
+
readonly isolation: ShellIsolationMode;
|
|
15
|
+
readonly isolationOptions: {
|
|
16
|
+
readonly filesystem: "workdir-readwrite";
|
|
17
|
+
readonly network: "allowed";
|
|
18
|
+
readonly env: "inherit";
|
|
19
|
+
};
|
|
20
|
+
};
|