@agent-api/app-engine 0.0.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.
Files changed (62) hide show
  1. package/README.md +46 -0
  2. package/dist/agent/runner.d.ts +117 -0
  3. package/dist/agent/runner.js +486 -0
  4. package/dist/agent.d.ts +2 -0
  5. package/dist/agent.js +2 -0
  6. package/dist/chat-options.d.ts +37 -0
  7. package/dist/chat-options.js +42 -0
  8. package/dist/config.d.ts +66 -0
  9. package/dist/config.js +201 -0
  10. package/dist/conversation/index.d.ts +17 -0
  11. package/dist/conversation/index.js +54 -0
  12. package/dist/engine/agent-engine.d.ts +38 -0
  13. package/dist/engine/agent-engine.js +146 -0
  14. package/dist/engine/index.d.ts +50 -0
  15. package/dist/engine/index.js +26 -0
  16. package/dist/engine/services.d.ts +20 -0
  17. package/dist/engine/services.js +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/profile.d.ts +57 -0
  21. package/dist/profile.js +211 -0
  22. package/dist/runtime/index.d.ts +23 -0
  23. package/dist/runtime/index.js +177 -0
  24. package/dist/update.d.ts +16 -0
  25. package/dist/update.js +74 -0
  26. package/dist/workbench/auth-controller.d.ts +43 -0
  27. package/dist/workbench/auth-controller.js +84 -0
  28. package/dist/workbench/auth-gate-controller.d.ts +62 -0
  29. package/dist/workbench/auth-gate-controller.js +231 -0
  30. package/dist/workbench/command-controller.d.ts +29 -0
  31. package/dist/workbench/command-controller.js +426 -0
  32. package/dist/workbench/conversation-controller.d.ts +32 -0
  33. package/dist/workbench/conversation-controller.js +53 -0
  34. package/dist/workbench/engine.d.ts +66 -0
  35. package/dist/workbench/engine.js +291 -0
  36. package/dist/workbench/input-controller.d.ts +44 -0
  37. package/dist/workbench/input-controller.js +71 -0
  38. package/dist/workbench/isolator-installer.d.ts +29 -0
  39. package/dist/workbench/isolator-installer.js +208 -0
  40. package/dist/workbench/lifecycle-controller.d.ts +30 -0
  41. package/dist/workbench/lifecycle-controller.js +75 -0
  42. package/dist/workbench/local-controller.d.ts +21 -0
  43. package/dist/workbench/local-controller.js +94 -0
  44. package/dist/workbench/render-model.d.ts +46 -0
  45. package/dist/workbench/render-model.js +61 -0
  46. package/dist/workbench/runtime-controller.d.ts +12 -0
  47. package/dist/workbench/runtime-controller.js +57 -0
  48. package/dist/workbench/session.d.ts +29 -0
  49. package/dist/workbench/session.js +42 -0
  50. package/dist/workbench/settings-controller.d.ts +80 -0
  51. package/dist/workbench/settings-controller.js +309 -0
  52. package/dist/workbench/shell-isolation.d.ts +20 -0
  53. package/dist/workbench/shell-isolation.js +13 -0
  54. package/dist/workbench/state.d.ts +187 -0
  55. package/dist/workbench/state.js +392 -0
  56. package/dist/workbench/turn-controller.d.ts +25 -0
  57. package/dist/workbench/turn-controller.js +164 -0
  58. package/dist/workbench/view-model.d.ts +34 -0
  59. package/dist/workbench/view-model.js +121 -0
  60. package/dist/workdir/index.d.ts +22 -0
  61. package/dist/workdir/index.js +46 -0
  62. package/package.json +50 -0
@@ -0,0 +1,291 @@
1
+ import { createInitialWorkbenchState, formatTranscript, formatTranscriptPreview, helpText, parsePendingApprovalCommand, parseWorkbenchCommand, workbenchReducer, workdirText, } from "./state.js";
2
+ export function createWorkbenchEngine(options) {
3
+ let state = createInitialWorkbenchState(options);
4
+ let pendingApprovalInvalidInputs = 0;
5
+ const listeners = new Set();
6
+ const notify = () => {
7
+ for (const listener of listeners)
8
+ listener();
9
+ };
10
+ const dispatch = (action) => {
11
+ if (action.type === "local_tool.pending.set" || action.type === "local_tool.pending.clear") {
12
+ pendingApprovalInvalidInputs = 0;
13
+ }
14
+ const next = workbenchReducer(state, action);
15
+ if (Object.is(next, state))
16
+ return;
17
+ state = next;
18
+ notify();
19
+ };
20
+ return {
21
+ snapshot() {
22
+ return state;
23
+ },
24
+ subscribe(listener) {
25
+ listeners.add(listener);
26
+ return () => {
27
+ listeners.delete(listener);
28
+ };
29
+ },
30
+ dispatch,
31
+ handleCommand(command) {
32
+ switch (command.kind) {
33
+ case "quit":
34
+ return handled({ type: "exit" });
35
+ case "login":
36
+ return handled({ type: "login" });
37
+ case "logout":
38
+ return handled({ type: "logout" });
39
+ case "delete_profile":
40
+ return handled({ type: "delete_profile" });
41
+ case "switch_profile":
42
+ return handled({ type: "switch_profile", name: command.name });
43
+ case "auth_status":
44
+ return handled({ type: "show_auth_status" });
45
+ case "export":
46
+ return handled({
47
+ type: "export_transcript",
48
+ path: command.path,
49
+ transcript: formatTranscript(state.messages),
50
+ conversation: state.currentConversation,
51
+ });
52
+ case "refresh_catalog":
53
+ dispatch({ type: "activity.add", level: "success", text: "Preset and tool catalogs refreshed" });
54
+ dispatch({ type: "message.add", role: "system", text: "Cleared cached preset and server tool catalogs. The next agent turn will fetch fresh platform configuration." });
55
+ return handled({ type: "clear_preset_tool_catalog_cache" });
56
+ case "invalid":
57
+ dispatch({
58
+ type: "message.add",
59
+ role: "system",
60
+ text: `Unknown command: /${command.command}\nType /help for supported commands.`,
61
+ });
62
+ dispatch({ type: "activity.add", level: "warning", text: `Unknown command: /${command.command}` });
63
+ return handled();
64
+ case "help":
65
+ dispatch({ type: "message.add", role: "system", text: helpText() });
66
+ return handled();
67
+ case "clear":
68
+ dispatch({ type: "messages.clear" });
69
+ return handled();
70
+ case "render":
71
+ if (!command.mode) {
72
+ dispatch({ type: "message.add", role: "system", text: `Render mode: ${state.renderMode}. Use /render markdown or /render raw.` });
73
+ return handled();
74
+ }
75
+ dispatch({ type: "settings.set", settings: { renderMode: command.mode } });
76
+ dispatch({ type: "activity.add", level: "success", text: `Render mode: ${command.mode}` });
77
+ dispatch({ type: "message.add", role: "system", text: `Render mode set to ${command.mode}.` });
78
+ return handled();
79
+ case "transcript":
80
+ dispatch({ type: "message.add", role: "system", text: formatTranscriptPreview(state.messages) });
81
+ dispatch({ type: "activity.add", level: "success", text: "Transcript preview ready" });
82
+ return handled();
83
+ case "context":
84
+ dispatch({ type: "context.set", enabled: command.enabled ?? !state.contextEnabled });
85
+ return handled();
86
+ case "access":
87
+ if (!command.mode) {
88
+ dispatch({ type: "message.add", role: "system", text: `Local access: ${state.accessMode}. Use /access off, /access approval, or /access full.` });
89
+ return handled();
90
+ }
91
+ dispatch({ type: "access.set", mode: command.mode });
92
+ return handled();
93
+ case "model":
94
+ if (!command.value) {
95
+ dispatch({ type: "message.add", role: "system", text: `Model: ${state.runModel || "auto"}. Use /model <name> or /model auto.` });
96
+ return handled();
97
+ }
98
+ dispatch({ type: "settings.set", settings: { runModel: normalizeOptionalSetting(command.value, ["auto", "none", "off", "clear"]) } });
99
+ dispatch({ type: "activity.add", text: `Model: ${normalizeOptionalSetting(command.value, ["auto", "none", "off", "clear"]) || "auto"}` });
100
+ return handled();
101
+ case "workdir":
102
+ if (command.enabled === undefined) {
103
+ dispatch({
104
+ type: "message.add",
105
+ role: "system",
106
+ text: [
107
+ workdirText(state.workdir),
108
+ "",
109
+ `local_workdir tool: ${state.contextEnabled ? "on" : "off"}`,
110
+ `local_shell tool: ${state.contextEnabled ? "on" : "off"}`,
111
+ "Use /access approval or /access full to expose local tools, or /access off to hide them.",
112
+ ].join("\n"),
113
+ });
114
+ return handled();
115
+ }
116
+ dispatch({ type: "context.set", enabled: command.enabled });
117
+ dispatch({
118
+ type: "activity.add",
119
+ level: command.enabled ? "success" : "warning",
120
+ text: `local tools ${command.enabled ? "enabled" : "disabled"}`,
121
+ });
122
+ dispatch({
123
+ type: "message.add",
124
+ role: "system",
125
+ text: command.enabled
126
+ ? "local_workdir and local_shell are now available to the model in approval mode. Use /access full to allow execution without prompts."
127
+ : "local tools are now hidden from the model.",
128
+ });
129
+ return handled();
130
+ default:
131
+ return unhandled();
132
+ }
133
+ },
134
+ handleAgentEvent(event) {
135
+ switch (event.type) {
136
+ case "text.delta":
137
+ return event.delta ? eventResult({ type: "append_text_delta", delta: event.delta }) : eventResult();
138
+ case "response.started":
139
+ if (event.responseID) {
140
+ dispatch({ type: "activity.add", text: `Response started: ${event.responseID}` });
141
+ return eventResult({ type: "set_active_response_id", responseID: event.responseID });
142
+ }
143
+ dispatch({ type: "activity.add", text: "Response started" });
144
+ return eventResult();
145
+ case "response.completed":
146
+ dispatch({ type: "activity.add", level: "success", text: event.responseID ? `Response completed: ${event.responseID}` : "Response completed" });
147
+ return eventResult({ type: "flush_text_delta_buffer" });
148
+ case "response.failed":
149
+ dispatch({ type: "activity.add", level: "error", text: event.message });
150
+ return eventResult({ type: "flush_text_delta_buffer" });
151
+ case "reasoning.started":
152
+ dispatch({ type: "activity.add", text: "Reasoning started" });
153
+ return eventResult();
154
+ case "reasoning.stopped":
155
+ dispatch({ type: "activity.add", text: event.thought ? `Reasoning stopped: ${event.thought}` : "Reasoning stopped" });
156
+ return eventResult();
157
+ case "reasoning.search_queries":
158
+ dispatch({ type: "activity.add", text: `Search queries: ${event.queries.join(", ") || "none"}` });
159
+ return eventResult();
160
+ case "reasoning.search_results":
161
+ dispatch({ type: "activity.add", text: `Search results: ${event.count}` });
162
+ return eventResult();
163
+ case "reasoning.fetch_url_queries":
164
+ dispatch({ type: "activity.add", text: `Fetch URLs: ${event.urls.join(", ") || "none"}` });
165
+ return eventResult();
166
+ case "reasoning.fetch_url_results":
167
+ dispatch({ type: "activity.add", text: `Fetched URL results: ${event.count}` });
168
+ return eventResult();
169
+ case "tool.completed":
170
+ dispatch({ type: "activity.add", level: event.status === "failed" ? "error" : "success", text: `Tool completed: ${event.name}${event.status ? ` (${event.status})` : ""}` });
171
+ return eventResult();
172
+ case "local_tool.completed":
173
+ dispatch({
174
+ type: "activity.add",
175
+ level: event.requiresApproval ? "warning" : "success",
176
+ text: `Local tool: ${event.name}${event.action ? `.${event.action}` : ""}${event.requiresApproval ? " (approval required)" : ""}`,
177
+ });
178
+ return eventResult();
179
+ case "local_tool.approval_requested":
180
+ dispatch({
181
+ type: "local_tool.pending.set",
182
+ approval: {
183
+ name: event.name,
184
+ action: event.action,
185
+ arguments: event.arguments,
186
+ preview: event.preview,
187
+ callID: event.callID,
188
+ responseID: event.responseID,
189
+ },
190
+ });
191
+ dispatch({ type: "message.add", role: "system", text: formatLocalToolApproval(event) });
192
+ return eventResult();
193
+ case "model.requested":
194
+ dispatch({ type: "activity.add", text: `Model requested: ${modelLabel(event.model, event.provider)}` });
195
+ return eventResult();
196
+ case "model.completed":
197
+ dispatch({ type: "activity.add", level: "success", text: `Model completed: ${modelLabel(event.model, event.provider)}` });
198
+ return eventResult();
199
+ case "model.failed":
200
+ dispatch({ type: "activity.add", level: "error", text: `Model failed: ${modelLabel(event.model, event.provider)}` });
201
+ return eventResult();
202
+ case "step.completed":
203
+ dispatch({ type: "activity.add", level: "success", text: `Step completed: ${event.stepType || "step"}` });
204
+ return eventResult();
205
+ case "step.failed":
206
+ dispatch({ type: "activity.add", level: "error", text: `Step failed: ${event.stepType || "step"}` });
207
+ return eventResult();
208
+ case "raw":
209
+ return eventResult();
210
+ }
211
+ },
212
+ submit(input) {
213
+ const trimmed = input.trim();
214
+ if (!trimmed)
215
+ return { kind: "handled" };
216
+ if (state.pendingLocalTool) {
217
+ const command = parsePendingApprovalCommand(trimmed);
218
+ if (command) {
219
+ pendingApprovalInvalidInputs = 0;
220
+ return { kind: "command", command };
221
+ }
222
+ handleInvalidPendingApprovalInput();
223
+ return { kind: "handled" };
224
+ }
225
+ const command = parseWorkbenchCommand(trimmed);
226
+ if (command)
227
+ return { kind: "command", command };
228
+ return { kind: "prompt", prompt: trimmed };
229
+ },
230
+ };
231
+ function handleInvalidPendingApprovalInput() {
232
+ pendingApprovalInvalidInputs += 1;
233
+ const attempts = pendingApprovalInvalidInputs;
234
+ const maxAttempts = 3;
235
+ if (attempts >= maxAttempts) {
236
+ dispatch({
237
+ type: "message.add",
238
+ role: "system",
239
+ text: "Local approval aborted after too many invalid inputs. The pending action was not executed.",
240
+ });
241
+ dispatch({ type: "activity.add", level: "warning", text: "Local approval aborted" });
242
+ dispatch({ type: "local_tool.pending.clear" });
243
+ pendingApprovalInvalidInputs = 0;
244
+ return;
245
+ }
246
+ dispatch({
247
+ type: "message.add",
248
+ role: "system",
249
+ text: `Local approval is pending. Enter /apply or /yes to execute once, /apply-all or /yes-all to allow future local actions, or /reject or /no to discard. Invalid input ${attempts}/${maxAttempts}.`,
250
+ });
251
+ dispatch({ type: "activity.add", level: "warning", text: "Waiting for local approval command" });
252
+ }
253
+ }
254
+ function handled(...effects) {
255
+ return { handled: true, effects };
256
+ }
257
+ function unhandled() {
258
+ return { handled: false, effects: [] };
259
+ }
260
+ function eventResult(...effects) {
261
+ return { effects };
262
+ }
263
+ function modelLabel(model, provider) {
264
+ if (model && provider)
265
+ return `${provider}/${model}`;
266
+ return model || provider || "unknown";
267
+ }
268
+ function formatLocalToolApproval(event) {
269
+ const label = `${event.name}${event.action ? `.${event.action}` : ""}`;
270
+ return [
271
+ `Local action requires approval: ${label}.`,
272
+ event.preview ? `Preview:\n${formatPreview(event.preview)}` : undefined,
273
+ "Review it in the workbench, then use /apply to execute once, /apply-all to allow future local actions, or /reject to discard it.",
274
+ ].filter(Boolean).join("\n\n");
275
+ }
276
+ function formatPreview(preview) {
277
+ if (typeof preview === "string")
278
+ return preview;
279
+ try {
280
+ return JSON.stringify(preview, null, 2);
281
+ }
282
+ catch {
283
+ return String(preview);
284
+ }
285
+ }
286
+ function normalizeOptionalSetting(value, clearValues) {
287
+ const trimmed = value.trim();
288
+ if (!trimmed)
289
+ return undefined;
290
+ return clearValues.includes(trimmed.toLowerCase()) ? undefined : trimmed;
291
+ }
@@ -0,0 +1,44 @@
1
+ export interface WorkbenchInputKey {
2
+ backspace?: boolean;
3
+ ctrl?: boolean;
4
+ delete?: boolean;
5
+ downArrow?: boolean;
6
+ end?: boolean;
7
+ escape?: boolean;
8
+ home?: boolean;
9
+ meta?: boolean;
10
+ pageDown?: boolean;
11
+ pageUp?: boolean;
12
+ return?: boolean;
13
+ upArrow?: boolean;
14
+ }
15
+ export type WorkbenchInputEffect = {
16
+ type: "exit";
17
+ } | {
18
+ type: "scroll";
19
+ delta: number;
20
+ } | {
21
+ type: "scroll_top";
22
+ } | {
23
+ type: "scroll_bottom";
24
+ } | {
25
+ type: "abort";
26
+ } | {
27
+ type: "submit";
28
+ input: string;
29
+ } | {
30
+ type: "ignored_busy";
31
+ };
32
+ export interface WorkbenchInputResult {
33
+ draft: string;
34
+ effects: WorkbenchInputEffect[];
35
+ }
36
+ export interface WorkbenchInputContext {
37
+ busy: boolean;
38
+ draft: string;
39
+ viewportHeight: number;
40
+ }
41
+ export interface WorkbenchInputController {
42
+ handle(input: string, key: WorkbenchInputKey, context: WorkbenchInputContext): WorkbenchInputResult;
43
+ }
44
+ export declare function createWorkbenchInputController(): WorkbenchInputController;
@@ -0,0 +1,71 @@
1
+ import { createInputHistory } from "./state.js";
2
+ export function createWorkbenchInputController() {
3
+ const history = createInputHistory();
4
+ return {
5
+ handle(input, key, context) {
6
+ if (key.ctrl && input === "c")
7
+ return result(context.draft, { type: "exit" });
8
+ if (key.pageUp || (key.ctrl && input === "u")) {
9
+ return result(context.draft, { type: "scroll", delta: Math.max(1, Math.floor(context.viewportHeight / 2)) });
10
+ }
11
+ if (key.pageDown || (key.ctrl && input === "d")) {
12
+ return result(context.draft, { type: "scroll", delta: -Math.max(1, Math.floor(context.viewportHeight / 2)) });
13
+ }
14
+ if (key.home)
15
+ return result(context.draft, { type: "scroll_top" });
16
+ if (key.end)
17
+ return result(context.draft, { type: "scroll_bottom" });
18
+ if (key.upArrow)
19
+ return result(history.previous(context.draft));
20
+ if (key.downArrow)
21
+ return result(history.next(context.draft));
22
+ if (context.busy) {
23
+ return handleBusyInput(input, key, context.draft, history);
24
+ }
25
+ return handleReadyInput(input, key, context.draft, history);
26
+ },
27
+ };
28
+ }
29
+ function handleBusyInput(input, key, draft, history) {
30
+ if (key.escape)
31
+ return result(draft, { type: "abort" });
32
+ if (key.return) {
33
+ const command = draft.trim();
34
+ history.record(command);
35
+ if (command === "/abort" || command === "/cancel")
36
+ return result("", { type: "abort" });
37
+ if (command)
38
+ return result("", { type: "ignored_busy" });
39
+ return result("");
40
+ }
41
+ if (key.backspace || key.delete) {
42
+ history.reset();
43
+ return result(draft.slice(0, -1));
44
+ }
45
+ if (input && !key.ctrl && !key.meta) {
46
+ history.reset();
47
+ return result(draft + input);
48
+ }
49
+ return result(draft);
50
+ }
51
+ function handleReadyInput(input, key, draft, history) {
52
+ if (key.return) {
53
+ const prompt = draft.trim();
54
+ if (!prompt)
55
+ return result(draft);
56
+ history.record(prompt);
57
+ return result("", { type: "submit", input: prompt });
58
+ }
59
+ if (key.backspace || key.delete) {
60
+ history.reset();
61
+ return result(draft.slice(0, -1));
62
+ }
63
+ if (input && !key.ctrl && !key.meta) {
64
+ history.reset();
65
+ return result(draft + input);
66
+ }
67
+ return result(draft);
68
+ }
69
+ function result(draft, ...effects) {
70
+ return { draft, effects };
71
+ }
@@ -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
+ }
@@ -0,0 +1,30 @@
1
+ import type { WorkbenchAuthController } from "./auth-controller.js";
2
+ import type { WorkbenchAction, WorkbenchWorkdirStatus } from "./state.js";
3
+ import { checkForUpdate, formatUpdateNotice, type UpdateCheckResult } from "../update.js";
4
+ export type WorkbenchLifecycleEffect = {
5
+ type: "dispatch";
6
+ action: WorkbenchAction;
7
+ } | {
8
+ type: "close";
9
+ delayMs: number;
10
+ };
11
+ export interface WorkbenchLifecycleController {
12
+ maybeCheckForUpdate(): Promise<WorkbenchLifecycleEffect[]>;
13
+ refreshAuth(profile?: string): Promise<WorkbenchLifecycleEffect[]>;
14
+ initialPrompt(input: {
15
+ busy: boolean;
16
+ promptParts: string[];
17
+ requiresWorkdir?: boolean;
18
+ workdir: WorkbenchWorkdirStatus | null;
19
+ }): string | undefined;
20
+ }
21
+ export interface WorkbenchLifecycleControllerOptions {
22
+ authController: WorkbenchAuthController;
23
+ checkForUpdateImpl?: typeof checkForUpdate;
24
+ formatUpdateNoticeImpl?: typeof formatUpdateNotice;
25
+ formatError?: (error: unknown) => string;
26
+ refreshWindowMs?: number;
27
+ updateCheckEnabled?: boolean;
28
+ }
29
+ export declare function createWorkbenchLifecycleController(options: WorkbenchLifecycleControllerOptions): WorkbenchLifecycleController;
30
+ export declare function updateNoticeEffects(result: UpdateCheckResult | null | undefined, formatNotice?: typeof formatUpdateNotice): WorkbenchLifecycleEffect[];