@rivet-dev/agentos-core 0.0.0-main.2f99c77 → 0.0.0-main.5ac5a14
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/README.md +10 -196
- package/dist/agent-os.d.ts +508 -271
- package/dist/agent-os.js +2277 -1243
- package/dist/agentos-package.d.ts +6 -4
- package/dist/agentos-package.js +6 -3
- package/dist/base-filesystem.js +2 -2
- package/dist/{host-tools-zod.d.ts → bindings-zod.d.ts} +1 -1
- package/dist/{host-tools-zod.js → bindings-zod.js} +27 -20
- package/dist/bindings.d.ts +43 -0
- package/dist/bindings.js +34 -0
- package/dist/code-execution.d.ts +70 -0
- package/dist/code-execution.js +115 -0
- package/dist/cron/cron-manager.js +43 -10
- package/dist/cron/index.d.ts +1 -1
- package/dist/cron/types.d.ts +25 -9
- package/dist/filesystem-snapshot.d.ts +1 -2
- package/dist/filesystem-snapshot.js +0 -43
- package/dist/generated/AcpLimitsConfig.d.ts +16 -0
- package/dist/generated/BindingLimitsConfig.d.ts +10 -0
- package/dist/generated/CreateVmConfig.d.ts +2 -0
- package/dist/generated/ProcessLimitsConfig.d.ts +7 -0
- package/dist/generated/ProcessLimitsConfig.js +2 -0
- package/dist/generated/VmGroupConfig.d.ts +5 -0
- package/dist/generated/VmGroupConfig.js +2 -0
- package/dist/generated/VmLimitsConfig.d.ts +4 -2
- package/dist/generated/VmUserAccountConfig.d.ts +9 -0
- package/dist/generated/VmUserAccountConfig.js +2 -0
- package/dist/generated/VmUserConfig.d.ts +19 -0
- package/dist/generated/WasmLimitsConfig.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +5 -5
- package/dist/language-execution.d.ts +170 -0
- package/dist/language-execution.js +1 -0
- package/dist/layers.d.ts +5 -2
- package/dist/layers.js +0 -185
- package/dist/options-schema.d.ts +150 -31
- package/dist/options-schema.js +159 -26
- package/dist/packages.d.ts +1 -1
- package/dist/runtime-compat.d.ts +6 -35
- package/dist/runtime-compat.js +85 -373
- package/dist/runtime.d.ts +5 -1
- package/dist/sandbox.d.ts +4 -4
- package/dist/sandbox.js +34 -21
- package/dist/session-api.d.ts +180 -0
- package/dist/session-api.js +1 -0
- package/dist/sidecar/agentos-protocol.d.ts +434 -32
- package/dist/sidecar/agentos-protocol.js +693 -93
- package/dist/sidecar/rpc-client.d.ts +18 -6
- package/dist/sidecar/rpc-client.js +88 -39
- package/dist/test/runtime.d.ts +2 -1
- package/dist/test/runtime.js +2 -1
- package/dist/test/terminal-harness.js +0 -3
- package/dist/types.d.ts +7 -6
- package/package.json +38 -28
- package/dist/agent-session-types.d.ts +0 -67
- package/dist/generated/ToolLimitsConfig.d.ts +0 -10
- package/dist/host-tools.d.ts +0 -45
- package/dist/host-tools.js +0 -35
- package/dist/overlay-filesystem.d.ts +0 -19
- package/dist/overlay-filesystem.js +0 -645
- /package/dist/generated/{ToolLimitsConfig.js → BindingLimitsConfig.js} +0 -0
- /package/dist/{agent-session-types.js → generated/VmUserConfig.js} +0 -0
package/dist/agent-os.js
CHANGED
|
@@ -3,22 +3,143 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import * as executionProtocol from "@rivet-dev/agentos-runtime-core/protocol";
|
|
7
|
+
import { SidecarRejectedError } from "@rivet-dev/agentos-runtime-core/sidecar-errors";
|
|
8
|
+
import { validateBindings } from "./bindings.js";
|
|
9
|
+
import { zodToJsonSchema } from "./bindings-zod.js";
|
|
8
10
|
import { parseAgentOsOptions } from "./options-schema.js";
|
|
11
|
+
import { getSandboxDisposeHooks, resolveSandboxOptions, } from "./sandbox.js";
|
|
9
12
|
import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
|
|
10
13
|
import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
|
|
11
|
-
export { isAcpTimeoutErrorData } from "./json-rpc.js";
|
|
12
14
|
const ACP_PROTOCOL_VERSION = 1;
|
|
13
15
|
const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
|
|
14
16
|
const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
|
|
15
|
-
|
|
17
|
+
const PROCESS_OUTPUT_EVENT_LIMIT = 1_024;
|
|
18
|
+
function safeWireU64(value) {
|
|
19
|
+
const result = Number(value);
|
|
20
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
21
|
+
throw new RangeError(`wire integer ${value} exceeds JavaScript's safe range`);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function decodeDurableSessionInfo(value) {
|
|
16
26
|
return {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
sessionId: value.sessionId,
|
|
28
|
+
agent: value.agent,
|
|
29
|
+
cwd: value.cwd,
|
|
30
|
+
additionalDirectories: JSON.parse(value.additionalDirectories),
|
|
31
|
+
state: JSON.parse(value.state),
|
|
32
|
+
latestSequence: safeWireU64(value.latestSequence),
|
|
33
|
+
title: value.title ?? undefined,
|
|
34
|
+
metadata: value.metadata === null ? undefined : JSON.parse(value.metadata),
|
|
35
|
+
createdAt: value.createdAt,
|
|
36
|
+
updatedAt: value.updatedAt,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const PERMISSION_TERMINAL_REASONS = new Set([
|
|
40
|
+
"already_resolved",
|
|
41
|
+
"prompt_cancelled",
|
|
42
|
+
"adapter_exited",
|
|
43
|
+
"session_deleted",
|
|
44
|
+
"vm_shutdown",
|
|
45
|
+
"request_not_found",
|
|
46
|
+
]);
|
|
47
|
+
function permissionTerminalReason(value) {
|
|
48
|
+
if (value !== null &&
|
|
49
|
+
PERMISSION_TERMINAL_REASONS.has(value)) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`invalid permission terminal reason: ${value ?? "missing"}`);
|
|
53
|
+
}
|
|
54
|
+
function decodeDurableSessionEvent(value) {
|
|
55
|
+
const envelope = {
|
|
56
|
+
durability: "durable",
|
|
57
|
+
sessionId: value.sessionId,
|
|
58
|
+
sequence: safeWireU64(value.sequence),
|
|
59
|
+
timestamp: value.timestamp,
|
|
60
|
+
};
|
|
61
|
+
switch (value.event.tag) {
|
|
62
|
+
case "AcpDurableSessionUpdate": {
|
|
63
|
+
const update = JSON.parse(value.event.val.update);
|
|
64
|
+
const { sessionUpdate: type, ...payload } = update;
|
|
65
|
+
return {
|
|
66
|
+
...envelope,
|
|
67
|
+
type,
|
|
68
|
+
...payload,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
case "AcpDurablePermissionRequest": {
|
|
72
|
+
const request = JSON.parse(value.event.val.request);
|
|
73
|
+
const { sessionId: _sessionId, ...payload } = request;
|
|
74
|
+
return {
|
|
75
|
+
...envelope,
|
|
76
|
+
type: "permission_request",
|
|
77
|
+
requestId: value.event.val.requestId,
|
|
78
|
+
...payload,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
case "AcpDurablePermissionResponse": {
|
|
82
|
+
const status = value.event.val.status;
|
|
83
|
+
if (status !== "accepted" && status !== "not_pending") {
|
|
84
|
+
throw new Error(`invalid permission response event status: ${status}`);
|
|
85
|
+
}
|
|
86
|
+
const response = JSON.parse(value.event.val.response);
|
|
87
|
+
return {
|
|
88
|
+
...envelope,
|
|
89
|
+
type: "permission_response",
|
|
90
|
+
requestId: value.event.val.requestId,
|
|
91
|
+
...response,
|
|
92
|
+
status,
|
|
93
|
+
...(value.event.val.reason === null
|
|
94
|
+
? {}
|
|
95
|
+
: { reason: permissionTerminalReason(value.event.val.reason) }),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function normalizeSessionCapabilities(value) {
|
|
101
|
+
const capabilities = toRecord(value);
|
|
102
|
+
const prompt = toRecord(capabilities.promptCapabilities);
|
|
103
|
+
const mcp = toRecord(capabilities.mcpCapabilities);
|
|
104
|
+
const session = toRecord(capabilities.sessionCapabilities);
|
|
105
|
+
const extensions = Object.fromEntries(Object.entries(capabilities).filter(([key]) => ![
|
|
106
|
+
"loadSession",
|
|
107
|
+
"promptCapabilities",
|
|
108
|
+
"mcpCapabilities",
|
|
109
|
+
"sessionCapabilities",
|
|
110
|
+
].includes(key)));
|
|
111
|
+
return {
|
|
112
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
113
|
+
loadSession: capabilities.loadSession === true,
|
|
114
|
+
...(Object.keys(prompt).length > 0
|
|
115
|
+
? {
|
|
116
|
+
prompt: {
|
|
117
|
+
audio: prompt.audio === true || undefined,
|
|
118
|
+
embeddedContext: prompt.embeddedContext === true || undefined,
|
|
119
|
+
image: prompt.image === true || undefined,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
: {}),
|
|
123
|
+
...(Object.keys(mcp).length > 0
|
|
124
|
+
? {
|
|
125
|
+
mcp: {
|
|
126
|
+
http: mcp.http === true || undefined,
|
|
127
|
+
sse: mcp.sse === true || undefined,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
131
|
+
...(Object.keys(session).length > 0
|
|
132
|
+
? {
|
|
133
|
+
session: {
|
|
134
|
+
list: typeof session.list === "object" || undefined,
|
|
135
|
+
resume: typeof session.resume === "object" || undefined,
|
|
136
|
+
close: typeof session.close === "object" || undefined,
|
|
137
|
+
delete: typeof session.delete === "object" || undefined,
|
|
138
|
+
additionalDirectories: typeof session.additionalDirectories === "object" || undefined,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
...(Object.keys(extensions).length > 0 ? { extensions } : {}),
|
|
22
143
|
};
|
|
23
144
|
}
|
|
24
145
|
async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
@@ -32,26 +153,24 @@ async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
|
32
153
|
}),
|
|
33
154
|
]);
|
|
34
155
|
}
|
|
156
|
+
import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
|
|
35
157
|
import { getBaseEnvironment } from "./base-filesystem.js";
|
|
36
158
|
import { CronManager } from "./cron/cron-manager.js";
|
|
37
159
|
import { TimerScheduleDriver } from "./cron/timer-driver.js";
|
|
160
|
+
import { resolveDefaultSoftware } from "./default-software.js";
|
|
38
161
|
import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
|
|
39
162
|
import { serializeMountConfigForSidecar, } from "./js-bridge.js";
|
|
40
163
|
import { createSnapshotExport, } from "./layers.js";
|
|
41
|
-
import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
|
|
42
|
-
import { resolveDefaultSoftware } from "./default-software.js";
|
|
43
164
|
import { allowAll } from "./runtime-compat.js";
|
|
44
|
-
import {
|
|
165
|
+
import { decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
|
|
45
166
|
import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
|
|
46
167
|
import { createAgentOsSidecarClient, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
|
|
47
168
|
function defaultAgentStderrHandler(event) {
|
|
48
169
|
process.stderr.write(event.chunk);
|
|
49
170
|
}
|
|
50
171
|
function defaultAgentExitHandler(event) {
|
|
51
|
-
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"}
|
|
172
|
+
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"}; restore explicitly before retrying\n`);
|
|
52
173
|
}
|
|
53
|
-
const LEGACY_PERMISSION_METHOD = "request/permission";
|
|
54
|
-
const ACP_PERMISSION_METHOD = "session/request_permission";
|
|
55
174
|
class AcpDispatchError extends Error {
|
|
56
175
|
code;
|
|
57
176
|
data;
|
|
@@ -124,51 +243,6 @@ function normalizePackageRef(value) {
|
|
|
124
243
|
}
|
|
125
244
|
return undefined;
|
|
126
245
|
}
|
|
127
|
-
function parseAcpJson(value, context) {
|
|
128
|
-
if (value === null) {
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(value);
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
throw new Error(`invalid ACP ${context} JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function parseAcpJsonList(values, context) {
|
|
139
|
-
return values.map((value, index) => parseAcpJson(value, `${context}[${index}]`));
|
|
140
|
-
}
|
|
141
|
-
function sidecarSessionCreatedFromAcp(response) {
|
|
142
|
-
return {
|
|
143
|
-
sessionId: response.sessionId,
|
|
144
|
-
...(response.pid !== null ? { pid: response.pid } : {}),
|
|
145
|
-
modes: parseAcpJson(response.modes, "modes"),
|
|
146
|
-
configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
|
|
147
|
-
agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
|
|
148
|
-
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function sidecarSessionStateFromAcp(response) {
|
|
152
|
-
return {
|
|
153
|
-
sessionId: response.sessionId,
|
|
154
|
-
agentType: response.agentType,
|
|
155
|
-
processId: response.processId,
|
|
156
|
-
...(response.pid !== null ? { pid: response.pid } : {}),
|
|
157
|
-
closed: response.closed,
|
|
158
|
-
modes: parseAcpJson(response.modes, "modes"),
|
|
159
|
-
configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
|
|
160
|
-
agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
|
|
161
|
-
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function isLocalCancelledPromptResponse(method, response) {
|
|
165
|
-
const result = toRecord(response.result);
|
|
166
|
-
return (method === "session/prompt" &&
|
|
167
|
-
response.id === null &&
|
|
168
|
-
response.error === undefined &&
|
|
169
|
-
result.stopReason === "cancelled");
|
|
170
|
-
}
|
|
171
|
-
const CLOSED_SESSION_ID_RETENTION_LIMIT = 2048;
|
|
172
246
|
const CLOSED_SHELL_ID_RETENTION_LIMIT = 2048;
|
|
173
247
|
class BoundedSet {
|
|
174
248
|
limit;
|
|
@@ -205,51 +279,6 @@ class BoundedSet {
|
|
|
205
279
|
return this.#entries.size;
|
|
206
280
|
}
|
|
207
281
|
}
|
|
208
|
-
function shouldDispatchToSessionEventHandlers(notification) {
|
|
209
|
-
return notification.method === "session/update";
|
|
210
|
-
}
|
|
211
|
-
function toSessionModes(value) {
|
|
212
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
return value;
|
|
216
|
-
}
|
|
217
|
-
function toSessionConfigOptions(value) {
|
|
218
|
-
return Array.isArray(value) ? value : [];
|
|
219
|
-
}
|
|
220
|
-
function toAgentCapabilities(value) {
|
|
221
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
222
|
-
return {};
|
|
223
|
-
}
|
|
224
|
-
return value;
|
|
225
|
-
}
|
|
226
|
-
function toAgentInfo(value) {
|
|
227
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
if (typeof value.name !== "string") {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
return value;
|
|
234
|
-
}
|
|
235
|
-
function sessionEntryFromInit(sessionId, agentType, initData) {
|
|
236
|
-
return {
|
|
237
|
-
sessionId,
|
|
238
|
-
agentType,
|
|
239
|
-
processId: "",
|
|
240
|
-
pid: null,
|
|
241
|
-
closed: false,
|
|
242
|
-
modes: initData.modes ?? null,
|
|
243
|
-
configOptions: initData.configOptions ?? [],
|
|
244
|
-
capabilities: initData.capabilities ?? {},
|
|
245
|
-
agentInfo: initData.agentInfo ?? null,
|
|
246
|
-
eventHandlers: new Set(),
|
|
247
|
-
permissionHandlers: new Set(),
|
|
248
|
-
warnedNoPermissionHandler: false,
|
|
249
|
-
configOverrides: new Map(),
|
|
250
|
-
pendingPermissionReplies: new Map(),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
282
|
function isOverlayMountConfig(config) {
|
|
254
283
|
return "filesystem" in config;
|
|
255
284
|
}
|
|
@@ -445,14 +474,28 @@ const SIDECAR_BUILD_INPUTS = [
|
|
|
445
474
|
join(REPO_ROOT, "Cargo.toml"),
|
|
446
475
|
join(REPO_ROOT, "Cargo.lock"),
|
|
447
476
|
join(REPO_ROOT, "crates/bridge"),
|
|
477
|
+
join(REPO_ROOT, "crates/build-support"),
|
|
448
478
|
join(REPO_ROOT, "crates/execution"),
|
|
449
479
|
join(REPO_ROOT, "crates/kernel"),
|
|
450
|
-
join(REPO_ROOT, "crates/
|
|
480
|
+
join(REPO_ROOT, "crates/agentos-protocol"),
|
|
481
|
+
join(REPO_ROOT, "crates/agentos-sidecar"),
|
|
482
|
+
join(REPO_ROOT, "crates/native-sidecar"),
|
|
483
|
+
join(REPO_ROOT, "crates/native-sidecar-core"),
|
|
484
|
+
join(REPO_ROOT, "crates/sidecar-protocol"),
|
|
485
|
+
join(REPO_ROOT, "crates/v8-runtime"),
|
|
486
|
+
join(REPO_ROOT, "crates/vfs"),
|
|
487
|
+
join(REPO_ROOT, "crates/vm-config"),
|
|
488
|
+
join(REPO_ROOT, "packages/build-tools/bridge-src"),
|
|
489
|
+
join(REPO_ROOT, "packages/build-tools/package.json"),
|
|
490
|
+
join(REPO_ROOT, "packages/build-tools/scripts/build-v8-bridge.mjs"),
|
|
491
|
+
join(REPO_ROOT, "packages/core/fixtures/base-filesystem.json"),
|
|
492
|
+
join(REPO_ROOT, "packages/runtime-core/fixtures/base-filesystem.json"),
|
|
493
|
+
join(REPO_ROOT, "pnpm-lock.yaml"),
|
|
451
494
|
];
|
|
452
495
|
let ensuredSidecarBinary = null;
|
|
453
496
|
function collectConfiguredLowerPaths(config) {
|
|
454
497
|
const paths = new Set();
|
|
455
|
-
for (const lower of config?.lowers ?? []) {
|
|
498
|
+
for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
|
|
456
499
|
if (lower.kind !== "snapshot-export") {
|
|
457
500
|
continue;
|
|
458
501
|
}
|
|
@@ -463,7 +506,7 @@ function collectConfiguredLowerPaths(config) {
|
|
|
463
506
|
return paths;
|
|
464
507
|
}
|
|
465
508
|
function findBootstrapSeedEntry(config, path) {
|
|
466
|
-
for (const lower of config?.lowers ?? []) {
|
|
509
|
+
for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
|
|
467
510
|
if (lower.kind !== "snapshot-export") {
|
|
468
511
|
continue;
|
|
469
512
|
}
|
|
@@ -479,6 +522,9 @@ function findBootstrapSeedEntry(config, path) {
|
|
|
479
522
|
return meta ? { path, type: "directory", ...meta } : undefined;
|
|
480
523
|
}
|
|
481
524
|
function createKernelBootstrapLower(config, extraEntries = []) {
|
|
525
|
+
if (config?.type === "native") {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
482
528
|
const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
|
|
483
529
|
const existingPaths = collectConfiguredLowerPaths(config);
|
|
484
530
|
const entries = [
|
|
@@ -547,7 +593,7 @@ function buildLiveBootstrapDirectoryEntries(existingPaths, config) {
|
|
|
547
593
|
return entries;
|
|
548
594
|
}
|
|
549
595
|
async function bootstrapLiveBootstrapDirectories(client, session, vm, config) {
|
|
550
|
-
const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm)).map((entry) => entry.path));
|
|
596
|
+
const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm, Number.MAX_SAFE_INTEGER)).map((entry) => entry.path));
|
|
551
597
|
const entries = buildLiveBootstrapDirectoryEntries(existingPaths, config);
|
|
552
598
|
if (entries.length === 0) {
|
|
553
599
|
return;
|
|
@@ -720,11 +766,14 @@ function collectSidecarMountPlan(options) {
|
|
|
720
766
|
hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
721
767
|
return { sidecarMounts, hostMounts, hostPathMappings };
|
|
722
768
|
}
|
|
723
|
-
function
|
|
724
|
-
if (
|
|
769
|
+
function collectBindingBootstrapCommands(bindings) {
|
|
770
|
+
if (bindings.length === 0) {
|
|
725
771
|
return [];
|
|
726
772
|
}
|
|
727
|
-
return [
|
|
773
|
+
return [
|
|
774
|
+
"agentos",
|
|
775
|
+
...bindings.map((bindingCollection) => `agentos-${bindingCollection.name}`),
|
|
776
|
+
];
|
|
728
777
|
}
|
|
729
778
|
function validationMessage(error) {
|
|
730
779
|
if (typeof error === "object" &&
|
|
@@ -742,14 +791,16 @@ function validationMessage(error) {
|
|
|
742
791
|
}
|
|
743
792
|
return error instanceof Error ? error.message : String(error);
|
|
744
793
|
}
|
|
745
|
-
function
|
|
794
|
+
function bindingToSidecarDefinition(definition) {
|
|
746
795
|
return {
|
|
747
|
-
description:
|
|
748
|
-
inputSchema: zodToJsonSchema(
|
|
749
|
-
...(
|
|
750
|
-
|
|
796
|
+
description: definition.description,
|
|
797
|
+
inputSchema: zodToJsonSchema(definition.inputSchema),
|
|
798
|
+
...(definition.timeout !== undefined
|
|
799
|
+
? { timeoutMs: definition.timeout }
|
|
800
|
+
: {}),
|
|
801
|
+
...(definition.examples && definition.examples.length > 0
|
|
751
802
|
? {
|
|
752
|
-
examples:
|
|
803
|
+
examples: definition.examples.map((example) => ({
|
|
753
804
|
description: example.description,
|
|
754
805
|
input: example.input,
|
|
755
806
|
})),
|
|
@@ -757,8 +808,8 @@ function toolToSidecarDefinition(tool) {
|
|
|
757
808
|
: {}),
|
|
758
809
|
};
|
|
759
810
|
}
|
|
760
|
-
function combineInstructions(additionalInstructions,
|
|
761
|
-
const parts = [additionalInstructions,
|
|
811
|
+
function combineInstructions(additionalInstructions, bindingReference) {
|
|
812
|
+
const parts = [additionalInstructions, bindingReference]
|
|
762
813
|
.map((part) => part?.trim())
|
|
763
814
|
.filter((part) => Boolean(part));
|
|
764
815
|
if (parts.length === 0) {
|
|
@@ -766,48 +817,48 @@ function combineInstructions(additionalInstructions, toolReference) {
|
|
|
766
817
|
}
|
|
767
818
|
return parts.join("\n\n");
|
|
768
819
|
}
|
|
769
|
-
function
|
|
770
|
-
if (
|
|
820
|
+
function buildBindingReference(bindings) {
|
|
821
|
+
if (bindings.length === 0) {
|
|
771
822
|
return "";
|
|
772
823
|
}
|
|
773
824
|
const lines = [
|
|
774
|
-
"## Available Host
|
|
825
|
+
"## Available Host Bindings",
|
|
775
826
|
"",
|
|
776
|
-
"Run `agentos list-
|
|
827
|
+
"Run `agentos list-bindings` to see all available bindings.",
|
|
777
828
|
"",
|
|
778
829
|
];
|
|
779
|
-
for (const
|
|
780
|
-
lines.push(`### ${
|
|
830
|
+
for (const bindingCollection of bindings) {
|
|
831
|
+
lines.push(`### ${bindingCollection.name}`);
|
|
781
832
|
lines.push("");
|
|
782
|
-
lines.push(
|
|
833
|
+
lines.push(bindingCollection.description);
|
|
783
834
|
lines.push("");
|
|
784
|
-
for (const [
|
|
785
|
-
const
|
|
786
|
-
const signature =
|
|
835
|
+
for (const [bindingName, definition] of Object.entries(bindingCollection.bindings)) {
|
|
836
|
+
const sidecarBinding = bindingToSidecarDefinition(definition);
|
|
837
|
+
const signature = buildBindingFlagSignature(sidecarBinding.inputSchema);
|
|
787
838
|
const suffix = signature.length > 0 ? ` ${signature}` : "";
|
|
788
|
-
lines.push(`- \`agentos-${
|
|
839
|
+
lines.push(`- \`agentos-${bindingCollection.name} ${bindingName}${suffix}\` — ${definition.description}`);
|
|
789
840
|
}
|
|
790
841
|
lines.push("");
|
|
791
|
-
const
|
|
792
|
-
if (
|
|
842
|
+
const bindingsWithExamples = Object.entries(bindingCollection.bindings).filter(([, definition]) => definition.examples && definition.examples.length > 0);
|
|
843
|
+
if (bindingsWithExamples.length > 0) {
|
|
793
844
|
lines.push("**Examples:**");
|
|
794
845
|
lines.push("");
|
|
795
|
-
for (const [
|
|
796
|
-
for (const example of
|
|
797
|
-
const args =
|
|
846
|
+
for (const [bindingName, definition] of bindingsWithExamples) {
|
|
847
|
+
for (const example of definition.examples ?? []) {
|
|
848
|
+
const args = inputToBindingFlags(example.input);
|
|
798
849
|
const suffix = args.length > 0 ? ` ${args}` : "";
|
|
799
|
-
lines.push(`- ${example.description}: \`agentos-${
|
|
850
|
+
lines.push(`- ${example.description}: \`agentos-${bindingCollection.name} ${bindingName}${suffix}\``);
|
|
800
851
|
}
|
|
801
852
|
}
|
|
802
853
|
lines.push("");
|
|
803
854
|
}
|
|
804
|
-
lines.push(`Run \`agentos-${
|
|
855
|
+
lines.push(`Run \`agentos-${bindingCollection.name} <binding> --help\` for details.`);
|
|
805
856
|
lines.push("");
|
|
806
857
|
}
|
|
807
858
|
return lines.join("\n");
|
|
808
859
|
}
|
|
809
|
-
function
|
|
810
|
-
return
|
|
860
|
+
function buildBindingFlagSignature(schema) {
|
|
861
|
+
return describeBindingFlags(schema)
|
|
811
862
|
.map((flag) => {
|
|
812
863
|
if (flag.required) {
|
|
813
864
|
return `${flag.name} <${flag.type}>`;
|
|
@@ -816,7 +867,7 @@ function buildToolFlagSignature(schema) {
|
|
|
816
867
|
})
|
|
817
868
|
.join(" ");
|
|
818
869
|
}
|
|
819
|
-
function
|
|
870
|
+
function describeBindingFlags(schema) {
|
|
820
871
|
const schemaObject = asRecord(schema);
|
|
821
872
|
const properties = asRecord(schemaObject.properties);
|
|
822
873
|
const required = Array.isArray(schemaObject.required)
|
|
@@ -824,11 +875,11 @@ function describeToolFlags(schema) {
|
|
|
824
875
|
: new Set();
|
|
825
876
|
return Object.entries(properties).map(([fieldName, fieldSchema]) => ({
|
|
826
877
|
name: `--${camelToKebab(fieldName)}`,
|
|
827
|
-
type:
|
|
878
|
+
type: describeBindingFlagType(fieldSchema),
|
|
828
879
|
required: required.has(fieldName),
|
|
829
880
|
}));
|
|
830
881
|
}
|
|
831
|
-
function
|
|
882
|
+
function describeBindingFlagType(schema) {
|
|
832
883
|
const schemaObject = asRecord(schema);
|
|
833
884
|
const type = typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
834
885
|
if (type === "array") {
|
|
@@ -847,7 +898,7 @@ function describeJsonSchemaScalarType(schema) {
|
|
|
847
898
|
const schemaObject = asRecord(schema);
|
|
848
899
|
return typeof schemaObject.type === "string" ? schemaObject.type : "string";
|
|
849
900
|
}
|
|
850
|
-
function
|
|
901
|
+
function inputToBindingFlags(input) {
|
|
851
902
|
const inputObject = asRecord(input);
|
|
852
903
|
return Object.entries(inputObject)
|
|
853
904
|
.flatMap(([key, value]) => {
|
|
@@ -859,13 +910,13 @@ function inputToToolFlags(input) {
|
|
|
859
910
|
return [`--no-${camelToKebab(key)}`];
|
|
860
911
|
}
|
|
861
912
|
if (Array.isArray(value)) {
|
|
862
|
-
return value.map((item) => `${flag} ${
|
|
913
|
+
return value.map((item) => `${flag} ${bindingCliString(item)}`);
|
|
863
914
|
}
|
|
864
|
-
return [`${flag} ${
|
|
915
|
+
return [`${flag} ${bindingCliString(value)}`];
|
|
865
916
|
})
|
|
866
917
|
.join(" ");
|
|
867
918
|
}
|
|
868
|
-
function
|
|
919
|
+
function bindingCliString(value) {
|
|
869
920
|
return typeof value === "string" ? value : (JSON.stringify(value) ?? "null");
|
|
870
921
|
}
|
|
871
922
|
function camelToKebab(value) {
|
|
@@ -903,15 +954,15 @@ async function handleHostCallback(request, context) {
|
|
|
903
954
|
};
|
|
904
955
|
}
|
|
905
956
|
}
|
|
906
|
-
const
|
|
907
|
-
if (!
|
|
957
|
+
const definition = context.bindingMap.get(payload.callback_key);
|
|
958
|
+
if (!definition) {
|
|
908
959
|
return {
|
|
909
960
|
type: "host_callback_result",
|
|
910
961
|
invocation_id: payload.invocation_id,
|
|
911
|
-
error: `Unknown
|
|
962
|
+
error: `Unknown binding "${payload.callback_key}"`,
|
|
912
963
|
};
|
|
913
964
|
}
|
|
914
|
-
const permissionMode =
|
|
965
|
+
const permissionMode = bindingPermissionMode(context.permissions, payload.callback_key);
|
|
915
966
|
if (permissionMode !== "allow") {
|
|
916
967
|
return {
|
|
917
968
|
type: "host_callback_result",
|
|
@@ -919,7 +970,7 @@ async function handleHostCallback(request, context) {
|
|
|
919
970
|
error: `EACCES: blocked by binding.invoke policy for ${payload.callback_key}`,
|
|
920
971
|
};
|
|
921
972
|
}
|
|
922
|
-
const parsed =
|
|
973
|
+
const parsed = definition.inputSchema.safeParse(payload.input);
|
|
923
974
|
if (!parsed.success) {
|
|
924
975
|
return {
|
|
925
976
|
type: "host_callback_result",
|
|
@@ -931,7 +982,7 @@ async function handleHostCallback(request, context) {
|
|
|
931
982
|
return {
|
|
932
983
|
type: "host_callback_result",
|
|
933
984
|
invocation_id: payload.invocation_id,
|
|
934
|
-
result: await
|
|
985
|
+
result: await executeBinding(definition, payload.callback_key, parsed.data),
|
|
935
986
|
};
|
|
936
987
|
}
|
|
937
988
|
catch (error) {
|
|
@@ -942,14 +993,14 @@ async function handleHostCallback(request, context) {
|
|
|
942
993
|
};
|
|
943
994
|
}
|
|
944
995
|
}
|
|
945
|
-
function
|
|
946
|
-
const
|
|
947
|
-
for (const
|
|
948
|
-
for (const [
|
|
949
|
-
|
|
996
|
+
function buildBindingMap(bindings) {
|
|
997
|
+
const bindingMap = new Map();
|
|
998
|
+
for (const bindingCollection of bindings) {
|
|
999
|
+
for (const [bindingName, definition] of Object.entries(bindingCollection.bindings)) {
|
|
1000
|
+
bindingMap.set(`${bindingCollection.name}:${bindingName}`, definition);
|
|
950
1001
|
}
|
|
951
1002
|
}
|
|
952
|
-
return
|
|
1003
|
+
return bindingMap;
|
|
953
1004
|
}
|
|
954
1005
|
function bridgeErrorMessage(error) {
|
|
955
1006
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1048,7 +1099,7 @@ async function handleJsBridgeCall(request, context) {
|
|
|
1048
1099
|
await fs.chmod(path(), requireBridgeNumber(args.mode, "mode"));
|
|
1049
1100
|
break;
|
|
1050
1101
|
case "chown":
|
|
1051
|
-
await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"));
|
|
1102
|
+
await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"), { followSymlinks: args.followSymlinks !== false });
|
|
1052
1103
|
break;
|
|
1053
1104
|
case "utimes":
|
|
1054
1105
|
await fs.utimes(path(), requireBridgeNumber(args.atimeMs, "atimeMs"), requireBridgeNumber(args.mtimeMs, "mtimeMs"));
|
|
@@ -1096,90 +1147,90 @@ function parseHostCommandCallbackInput(input) {
|
|
|
1096
1147
|
};
|
|
1097
1148
|
}
|
|
1098
1149
|
async function handleHostCommandCallback(command, context) {
|
|
1099
|
-
const
|
|
1150
|
+
const directBindings = context.bindings.find((bindingCollection) => `agentos-${bindingCollection.name}` === command.command);
|
|
1100
1151
|
if (command.command === "agentos") {
|
|
1101
1152
|
return handleAgentOsRegistryCommand(command, context);
|
|
1102
1153
|
}
|
|
1103
|
-
if (
|
|
1104
|
-
return
|
|
1154
|
+
if (directBindings) {
|
|
1155
|
+
return handleAgentOsBindingCommand(command, context, directBindings);
|
|
1105
1156
|
}
|
|
1106
1157
|
throw new Error(`Unknown host callback command "${command.command}"`);
|
|
1107
1158
|
}
|
|
1108
1159
|
async function handleAgentOsRegistryCommand(command, context) {
|
|
1109
|
-
const [subcommand,
|
|
1160
|
+
const [subcommand, collectionName, bindingName, ...bindingArgs] = command.args;
|
|
1110
1161
|
if (!subcommand || isHelpFlag(subcommand)) {
|
|
1111
1162
|
return {
|
|
1112
|
-
usage: "agentos <command>: list-
|
|
1163
|
+
usage: "agentos <command>: list-bindings [collection], <collection> --help, or <collection> <binding> ...",
|
|
1113
1164
|
};
|
|
1114
1165
|
}
|
|
1115
|
-
if (subcommand === "list-
|
|
1116
|
-
return
|
|
1117
|
-
?
|
|
1118
|
-
:
|
|
1166
|
+
if (subcommand === "list-bindings") {
|
|
1167
|
+
return collectionName
|
|
1168
|
+
? describeBindingsPayload(context.bindings, collectionName)
|
|
1169
|
+
: listBindingsPayload(context.bindings);
|
|
1119
1170
|
}
|
|
1120
|
-
const
|
|
1121
|
-
if (!
|
|
1122
|
-
throw new Error(`No
|
|
1171
|
+
const bindingCollection = context.bindings.find((collection) => collection.name === subcommand);
|
|
1172
|
+
if (!bindingCollection) {
|
|
1173
|
+
throw new Error(`No binding collection "${subcommand}". Available: ${bindingsNames(context.bindings)}`);
|
|
1123
1174
|
}
|
|
1124
|
-
if (!
|
|
1125
|
-
return
|
|
1175
|
+
if (!collectionName || isHelpFlag(collectionName)) {
|
|
1176
|
+
return describeBindingsPayload(context.bindings, subcommand);
|
|
1126
1177
|
}
|
|
1127
|
-
if (
|
|
1128
|
-
return
|
|
1178
|
+
if (bindingName && isHelpFlag(bindingName)) {
|
|
1179
|
+
return describeBindingPayload(bindingCollection, collectionName);
|
|
1129
1180
|
}
|
|
1130
|
-
return
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
args: [
|
|
1181
|
+
return invokeBinding({
|
|
1182
|
+
bindingCollection,
|
|
1183
|
+
bindingName: collectionName,
|
|
1184
|
+
args: [bindingName, ...bindingArgs].filter((value) => typeof value === "string"),
|
|
1134
1185
|
cwd: command.cwd,
|
|
1135
1186
|
context,
|
|
1136
1187
|
});
|
|
1137
1188
|
}
|
|
1138
|
-
async function
|
|
1139
|
-
const [
|
|
1140
|
-
if (!
|
|
1141
|
-
return
|
|
1189
|
+
async function handleAgentOsBindingCommand(command, context, bindingCollection) {
|
|
1190
|
+
const [bindingName, helpOrFirstArg, ...rest] = command.args;
|
|
1191
|
+
if (!bindingName || isHelpFlag(bindingName)) {
|
|
1192
|
+
return describeBindingsPayload(context.bindings, bindingCollection.name);
|
|
1142
1193
|
}
|
|
1143
1194
|
if (helpOrFirstArg && isHelpFlag(helpOrFirstArg)) {
|
|
1144
|
-
return
|
|
1195
|
+
return describeBindingPayload(bindingCollection, bindingName);
|
|
1145
1196
|
}
|
|
1146
|
-
return
|
|
1147
|
-
|
|
1148
|
-
|
|
1197
|
+
return invokeBinding({
|
|
1198
|
+
bindingCollection,
|
|
1199
|
+
bindingName,
|
|
1149
1200
|
args: [helpOrFirstArg, ...rest].filter((value) => typeof value === "string"),
|
|
1150
1201
|
cwd: command.cwd,
|
|
1151
1202
|
context,
|
|
1152
1203
|
});
|
|
1153
1204
|
}
|
|
1154
|
-
async function
|
|
1155
|
-
const
|
|
1156
|
-
if (!
|
|
1157
|
-
throw new Error(`No
|
|
1205
|
+
async function invokeBinding({ bindingCollection, bindingName, args, cwd, context, }) {
|
|
1206
|
+
const definition = bindingCollection.bindings[bindingName];
|
|
1207
|
+
if (!definition) {
|
|
1208
|
+
throw new Error(`No binding "${bindingName}" in collection "${bindingCollection.name}". Available: ${bindingNames(bindingCollection)}`);
|
|
1158
1209
|
}
|
|
1159
|
-
const callbackKey = `${
|
|
1160
|
-
const permissionMode =
|
|
1210
|
+
const callbackKey = `${bindingCollection.name}:${bindingName}`;
|
|
1211
|
+
const permissionMode = bindingPermissionMode(context.permissions, callbackKey);
|
|
1161
1212
|
if (permissionMode !== "allow") {
|
|
1162
1213
|
throw new Error(`EACCES: blocked by binding.invoke policy for ${callbackKey}`);
|
|
1163
1214
|
}
|
|
1164
|
-
const input = await
|
|
1165
|
-
return
|
|
1215
|
+
const input = await parseBindingInput(definition, args, cwd, context.readFile);
|
|
1216
|
+
return executeBinding(definition, callbackKey, input);
|
|
1166
1217
|
}
|
|
1167
|
-
async function
|
|
1168
|
-
const parsed =
|
|
1218
|
+
async function executeBinding(definition, callbackKey, input) {
|
|
1219
|
+
const parsed = definition.inputSchema.safeParse(input);
|
|
1169
1220
|
if (!parsed.success) {
|
|
1170
1221
|
throw new Error(validationMessage(parsed.error));
|
|
1171
1222
|
}
|
|
1172
1223
|
return Promise.race([
|
|
1173
|
-
Promise.resolve(
|
|
1224
|
+
Promise.resolve(definition.execute(parsed.data)),
|
|
1174
1225
|
new Promise((_, reject) => {
|
|
1175
|
-
if (
|
|
1226
|
+
if (definition.timeout === undefined) {
|
|
1176
1227
|
return;
|
|
1177
1228
|
}
|
|
1178
|
-
setTimeout(() => reject(new Error(`
|
|
1229
|
+
setTimeout(() => reject(new Error(`Binding "${callbackKey}" timed out after ${definition.timeout}ms`)), definition.timeout);
|
|
1179
1230
|
}),
|
|
1180
1231
|
]);
|
|
1181
1232
|
}
|
|
1182
|
-
async function
|
|
1233
|
+
async function parseBindingInput(definition, args, cwd, readFile) {
|
|
1183
1234
|
if (args[0] === "--json") {
|
|
1184
1235
|
const value = args[1];
|
|
1185
1236
|
if (value === undefined) {
|
|
@@ -1198,9 +1249,9 @@ async function parseHostToolInput(tool, args, cwd, readFile) {
|
|
|
1198
1249
|
const text = new TextDecoder().decode(await readFile(guestPath));
|
|
1199
1250
|
return JSON.parse(text);
|
|
1200
1251
|
}
|
|
1201
|
-
return
|
|
1252
|
+
return parseBindingArgv(bindingToSidecarDefinition(definition).inputSchema, args);
|
|
1202
1253
|
}
|
|
1203
|
-
function
|
|
1254
|
+
function parseBindingArgv(schema, argv) {
|
|
1204
1255
|
const schemaObject = asRecord(schema);
|
|
1205
1256
|
const properties = asRecord(schemaObject.properties);
|
|
1206
1257
|
const required = Array.isArray(schemaObject.required)
|
|
@@ -1272,49 +1323,49 @@ function parseToolArgv(schema, argv) {
|
|
|
1272
1323
|
}
|
|
1273
1324
|
return input;
|
|
1274
1325
|
}
|
|
1275
|
-
function
|
|
1326
|
+
function listBindingsPayload(bindings) {
|
|
1276
1327
|
return {
|
|
1277
|
-
|
|
1278
|
-
name:
|
|
1279
|
-
description:
|
|
1280
|
-
|
|
1328
|
+
bindings: bindings.map((bindingCollection) => ({
|
|
1329
|
+
name: bindingCollection.name,
|
|
1330
|
+
description: bindingCollection.description,
|
|
1331
|
+
bindings: Object.keys(bindingCollection.bindings),
|
|
1281
1332
|
})),
|
|
1282
1333
|
};
|
|
1283
1334
|
}
|
|
1284
|
-
function
|
|
1285
|
-
const
|
|
1286
|
-
if (!
|
|
1287
|
-
throw new Error(`No
|
|
1335
|
+
function describeBindingsPayload(bindings, collectionName) {
|
|
1336
|
+
const bindingCollection = bindings.find((collection) => collection.name === collectionName);
|
|
1337
|
+
if (!bindingCollection) {
|
|
1338
|
+
throw new Error(`No binding collection "${collectionName}". Available: ${bindingsNames(bindings)}`);
|
|
1288
1339
|
}
|
|
1289
1340
|
return {
|
|
1290
|
-
name:
|
|
1291
|
-
description:
|
|
1292
|
-
|
|
1293
|
-
|
|
1341
|
+
name: bindingCollection.name,
|
|
1342
|
+
description: bindingCollection.description,
|
|
1343
|
+
bindings: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
|
|
1344
|
+
bindingName,
|
|
1294
1345
|
{
|
|
1295
|
-
description:
|
|
1296
|
-
flags:
|
|
1346
|
+
description: definition.description,
|
|
1347
|
+
flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
|
|
1297
1348
|
},
|
|
1298
1349
|
])),
|
|
1299
1350
|
};
|
|
1300
1351
|
}
|
|
1301
|
-
function
|
|
1302
|
-
const
|
|
1303
|
-
if (!
|
|
1304
|
-
throw new Error(`No
|
|
1352
|
+
function describeBindingPayload(bindingCollection, bindingName) {
|
|
1353
|
+
const definition = bindingCollection.bindings[bindingName];
|
|
1354
|
+
if (!definition) {
|
|
1355
|
+
throw new Error(`No binding "${bindingName}" in collection "${bindingCollection.name}". Available: ${bindingNames(bindingCollection)}`);
|
|
1305
1356
|
}
|
|
1306
1357
|
return {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
description:
|
|
1310
|
-
flags:
|
|
1311
|
-
examples:
|
|
1358
|
+
collection: bindingCollection.name,
|
|
1359
|
+
binding: bindingName,
|
|
1360
|
+
description: definition.description,
|
|
1361
|
+
flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
|
|
1362
|
+
examples: definition.examples?.map((example) => ({
|
|
1312
1363
|
description: example.description,
|
|
1313
1364
|
input: example.input,
|
|
1314
1365
|
})) ?? [],
|
|
1315
1366
|
};
|
|
1316
1367
|
}
|
|
1317
|
-
function
|
|
1368
|
+
function bindingPermissionMode(permissions, callbackKey) {
|
|
1318
1369
|
const scope = permissions.binding;
|
|
1319
1370
|
if (!scope) {
|
|
1320
1371
|
return "deny";
|
|
@@ -1349,11 +1400,11 @@ function permissionPatternMatches(pattern, value) {
|
|
|
1349
1400
|
.join("");
|
|
1350
1401
|
return new RegExp(`^${source}$`).test(value);
|
|
1351
1402
|
}
|
|
1352
|
-
function
|
|
1353
|
-
return
|
|
1403
|
+
function bindingsNames(bindings) {
|
|
1404
|
+
return bindings.map((bindingCollection) => bindingCollection.name).join(", ");
|
|
1354
1405
|
}
|
|
1355
|
-
function
|
|
1356
|
-
return Object.keys(
|
|
1406
|
+
function bindingNames(bindingCollection) {
|
|
1407
|
+
return Object.keys(bindingCollection.bindings).join(", ");
|
|
1357
1408
|
}
|
|
1358
1409
|
function isHelpFlag(value) {
|
|
1359
1410
|
return value === "--help" || value === "-h";
|
|
@@ -1362,32 +1413,230 @@ function jsonSchemaType(schema) {
|
|
|
1362
1413
|
const schemaObject = asRecord(schema);
|
|
1363
1414
|
return typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
1364
1415
|
}
|
|
1365
|
-
async function
|
|
1366
|
-
if (
|
|
1416
|
+
async function registerBindingsOnSidecar(client, session, vm, bindings) {
|
|
1417
|
+
if (bindings.length === 0) {
|
|
1367
1418
|
return "";
|
|
1368
1419
|
}
|
|
1369
|
-
for (const
|
|
1420
|
+
for (const bindingCollection of bindings) {
|
|
1370
1421
|
await client.registerHostCallbacks(session, vm, {
|
|
1371
|
-
name:
|
|
1372
|
-
description:
|
|
1373
|
-
commandAliases: [`agentos-${
|
|
1422
|
+
name: bindingCollection.name,
|
|
1423
|
+
description: bindingCollection.description,
|
|
1424
|
+
commandAliases: [`agentos-${bindingCollection.name}`],
|
|
1374
1425
|
registryCommandAliases: ["agentos"],
|
|
1375
|
-
callbacks: Object.fromEntries(Object.entries(
|
|
1376
|
-
|
|
1377
|
-
|
|
1426
|
+
callbacks: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
|
|
1427
|
+
bindingName,
|
|
1428
|
+
bindingToSidecarDefinition(definition),
|
|
1378
1429
|
])),
|
|
1379
1430
|
});
|
|
1380
1431
|
}
|
|
1381
|
-
return
|
|
1432
|
+
return buildBindingReference(bindings);
|
|
1433
|
+
}
|
|
1434
|
+
function executionIdentity(options) {
|
|
1435
|
+
return {
|
|
1436
|
+
contextId: options.contextId ?? null,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function executionOutput(options, background = false) {
|
|
1440
|
+
if (options.output?.retainEvents && !options.contextId && !background) {
|
|
1441
|
+
throw new Error("retainEvents is available only for spawned processes; use output.capture for attached runs");
|
|
1442
|
+
}
|
|
1443
|
+
const capture = options.output?.capture;
|
|
1444
|
+
return {
|
|
1445
|
+
capture: capture === "all"
|
|
1446
|
+
? executionProtocol.ExecutionOutputCapture.All
|
|
1447
|
+
: capture === "stderr"
|
|
1448
|
+
? executionProtocol.ExecutionOutputCapture.Stderr
|
|
1449
|
+
: capture === "none"
|
|
1450
|
+
? executionProtocol.ExecutionOutputCapture.None
|
|
1451
|
+
: null,
|
|
1452
|
+
retainEvents: options.output?.retainEvents ?? null,
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function executionBytes(data) {
|
|
1456
|
+
if (data === undefined)
|
|
1457
|
+
return null;
|
|
1458
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
1459
|
+
return bytes.slice().buffer;
|
|
1460
|
+
}
|
|
1461
|
+
function processExecutionOptions(options = {}, internal) {
|
|
1462
|
+
const background = internal?.background ?? false;
|
|
1463
|
+
return {
|
|
1464
|
+
identity: executionIdentity(options),
|
|
1465
|
+
output: executionOutput(options, background),
|
|
1466
|
+
operationId: internal?.executionId ?? null,
|
|
1467
|
+
background,
|
|
1468
|
+
cwd: options.cwd ?? null,
|
|
1469
|
+
env: options.env ? new Map(Object.entries(options.env)) : null,
|
|
1470
|
+
args: options.args ?? [],
|
|
1471
|
+
stdin: executionBytes(options.stdin),
|
|
1472
|
+
timeoutMs: protocolTimeout(options.timeoutMs),
|
|
1473
|
+
pty: options.pty
|
|
1474
|
+
? {
|
|
1475
|
+
cols: options.pty.cols === undefined
|
|
1476
|
+
? null
|
|
1477
|
+
: protocolUnsigned(options.pty.cols, 0xffff, "pty.cols"),
|
|
1478
|
+
rows: options.pty.rows === undefined
|
|
1479
|
+
? null
|
|
1480
|
+
: protocolUnsigned(options.pty.rows, 0xffff, "pty.rows"),
|
|
1481
|
+
}
|
|
1482
|
+
: null,
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
function protocolUnsigned(value, maximum, name) {
|
|
1486
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
|
1487
|
+
throw new RangeError(`${name} must be an integer between 0 and ${maximum}`);
|
|
1488
|
+
}
|
|
1489
|
+
return value;
|
|
1490
|
+
}
|
|
1491
|
+
function protocolTimeout(value) {
|
|
1492
|
+
if (value === undefined)
|
|
1493
|
+
return null;
|
|
1494
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1495
|
+
throw new RangeError("timeoutMs must be a non-negative safe integer");
|
|
1496
|
+
}
|
|
1497
|
+
return BigInt(value);
|
|
1498
|
+
}
|
|
1499
|
+
function safeProtocolNumber(value, name) {
|
|
1500
|
+
const result = Number(value);
|
|
1501
|
+
if (!Number.isSafeInteger(result)) {
|
|
1502
|
+
throw new RangeError(`${name} exceeds JavaScript's safe integer range`);
|
|
1503
|
+
}
|
|
1504
|
+
return result;
|
|
1505
|
+
}
|
|
1506
|
+
function mapExecutionState(state) {
|
|
1507
|
+
const mapped = state.toLowerCase();
|
|
1508
|
+
return (mapped === "creating" ? "idle" : mapped);
|
|
1509
|
+
}
|
|
1510
|
+
function mapExecutionOutcome(outcome) {
|
|
1511
|
+
return outcome === executionProtocol.ExecutionOutcome.TimedOut
|
|
1512
|
+
? "timed_out"
|
|
1513
|
+
: outcome.toLowerCase();
|
|
1514
|
+
}
|
|
1515
|
+
function mapExecutionDescriptor(descriptor) {
|
|
1516
|
+
return {
|
|
1517
|
+
contextId: descriptor.executionId,
|
|
1518
|
+
state: mapExecutionState(descriptor.state),
|
|
1519
|
+
...(descriptor.retainedLanguage
|
|
1520
|
+
? {
|
|
1521
|
+
language: descriptor.retainedLanguage ===
|
|
1522
|
+
executionProtocol.RetainedExecutionLanguage.JavaScript
|
|
1523
|
+
? "javascript"
|
|
1524
|
+
: "python",
|
|
1525
|
+
}
|
|
1526
|
+
: {}),
|
|
1527
|
+
createdAtMs: safeProtocolNumber(descriptor.createdAtMs, "createdAtMs"),
|
|
1528
|
+
...(descriptor.lastStartedAtMs !== null
|
|
1529
|
+
? {
|
|
1530
|
+
lastStartedAtMs: safeProtocolNumber(descriptor.lastStartedAtMs, "lastStartedAtMs"),
|
|
1531
|
+
}
|
|
1532
|
+
: {}),
|
|
1533
|
+
...(descriptor.lastCompletedAtMs !== null
|
|
1534
|
+
? {
|
|
1535
|
+
lastCompletedAtMs: safeProtocolNumber(descriptor.lastCompletedAtMs, "lastCompletedAtMs"),
|
|
1536
|
+
}
|
|
1537
|
+
: {}),
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function mapExecutionError(error) {
|
|
1541
|
+
if (!error)
|
|
1542
|
+
return undefined;
|
|
1543
|
+
return {
|
|
1544
|
+
code: error.code,
|
|
1545
|
+
name: error.name,
|
|
1546
|
+
message: error.message,
|
|
1547
|
+
...(error.stack ? { stack: error.stack } : {}),
|
|
1548
|
+
...(error.details
|
|
1549
|
+
? { details: JSON.parse(error.details) }
|
|
1550
|
+
: {}),
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function mapExecutionResult(result) {
|
|
1554
|
+
const base = {
|
|
1555
|
+
...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
|
|
1556
|
+
...(result.stdout !== null
|
|
1557
|
+
? { stdout: new TextDecoder().decode(result.stdout) }
|
|
1558
|
+
: {}),
|
|
1559
|
+
...(result.stderr !== null
|
|
1560
|
+
? { stderr: new TextDecoder().decode(result.stderr) }
|
|
1561
|
+
: {}),
|
|
1562
|
+
...(result.stdoutTruncated !== null
|
|
1563
|
+
? { stdoutTruncated: result.stdoutTruncated }
|
|
1564
|
+
: {}),
|
|
1565
|
+
...(result.stderrTruncated !== null
|
|
1566
|
+
? { stderrTruncated: result.stderrTruncated }
|
|
1567
|
+
: {}),
|
|
1568
|
+
...(result.evaluationValue
|
|
1569
|
+
? { evaluationValue: JSON.parse(result.evaluationValue) }
|
|
1570
|
+
: {}),
|
|
1571
|
+
...(result.typeScriptCheckResult
|
|
1572
|
+
? {
|
|
1573
|
+
typeScriptCheckResult: JSON.parse(result.typeScriptCheckResult),
|
|
1574
|
+
}
|
|
1575
|
+
: {}),
|
|
1576
|
+
};
|
|
1577
|
+
const outcome = mapExecutionOutcome(result.outcome);
|
|
1578
|
+
if (outcome === "succeeded") {
|
|
1579
|
+
return { ...base, outcome };
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
...base,
|
|
1583
|
+
outcome,
|
|
1584
|
+
error: mapExecutionError(result.error) ?? {
|
|
1585
|
+
code: "execution_failed",
|
|
1586
|
+
name: "ExecutionError",
|
|
1587
|
+
message: `execution completed with ${outcome}`,
|
|
1588
|
+
},
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
function mapTypeScriptCheckResult(result) {
|
|
1592
|
+
if (result.outcome !== "succeeded") {
|
|
1593
|
+
return { ...result, diagnostics: [] };
|
|
1594
|
+
}
|
|
1595
|
+
const data = result.typeScriptCheckResult;
|
|
1596
|
+
if (!data || Array.isArray(data) || typeof data !== "object") {
|
|
1597
|
+
throw new Error("TypeScript checker returned no diagnostic result");
|
|
1598
|
+
}
|
|
1599
|
+
const hasErrors = data.hasErrors;
|
|
1600
|
+
const diagnostics = data.diagnostics;
|
|
1601
|
+
if (typeof hasErrors !== "boolean" || !Array.isArray(diagnostics)) {
|
|
1602
|
+
throw new Error("TypeScript checker returned an invalid diagnostic result");
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
...result,
|
|
1606
|
+
hasErrors,
|
|
1607
|
+
diagnostics: diagnostics,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function mapExecutionOutputEvent(event) {
|
|
1611
|
+
return {
|
|
1612
|
+
executionId: event.executionId,
|
|
1613
|
+
generation: safeProtocolNumber(event.generation, "execution generation"),
|
|
1614
|
+
...(event.processId ? { processId: event.processId } : {}),
|
|
1615
|
+
sequence: safeProtocolNumber(event.sequence, "execution output sequence"),
|
|
1616
|
+
channel: event.channel.toLowerCase(),
|
|
1617
|
+
chunk: new Uint8Array(event.chunk),
|
|
1618
|
+
timestampMs: safeProtocolNumber(event.timestampMs, "execution timestamp"),
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
function mapExecutionCompletedEvent(event) {
|
|
1622
|
+
return {
|
|
1623
|
+
executionId: event.executionId,
|
|
1624
|
+
generation: safeProtocolNumber(event.generation, "execution generation"),
|
|
1625
|
+
outcome: mapExecutionOutcome(event.outcome),
|
|
1626
|
+
...(event.exitCode !== null ? { exitCode: event.exitCode } : {}),
|
|
1627
|
+
...(event.error ? { error: mapExecutionError(event.error) } : {}),
|
|
1628
|
+
};
|
|
1382
1629
|
}
|
|
1383
1630
|
export class AgentOs {
|
|
1384
1631
|
#kernel;
|
|
1385
1632
|
sidecar;
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
_sessionClosePromises = new Map();
|
|
1389
|
-
_pendingSessionRequestResolvers = new Map();
|
|
1633
|
+
_durableSessionEventHandlers = new Map();
|
|
1634
|
+
_agentExitHandlers = new Map();
|
|
1390
1635
|
_processes = new Map();
|
|
1636
|
+
_languageProcesses = new Map();
|
|
1637
|
+
_languageProcessIds = new Map();
|
|
1638
|
+
_executionOutputHandlers = new Map();
|
|
1639
|
+
_executionCompletedHandlers = new Map();
|
|
1391
1640
|
_shells = new Map();
|
|
1392
1641
|
// Value is the recorded exit code (undefined until/unless the exit
|
|
1393
1642
|
// resolves) so waitShell can still report it after the entry is dropped.
|
|
@@ -1398,13 +1647,12 @@ export class AgentOs {
|
|
|
1398
1647
|
_acpTerminalCounter = 0;
|
|
1399
1648
|
_softwareRoots;
|
|
1400
1649
|
_cronManager;
|
|
1401
|
-
|
|
1402
|
-
|
|
1650
|
+
_bindings = [];
|
|
1651
|
+
_bindingReference = "";
|
|
1403
1652
|
_permissions = allowAll;
|
|
1404
1653
|
_hostMounts;
|
|
1405
1654
|
_env;
|
|
1406
1655
|
_rootFilesystem;
|
|
1407
|
-
_additionalInstructions;
|
|
1408
1656
|
_sidecarLease = null;
|
|
1409
1657
|
_sidecarClient;
|
|
1410
1658
|
_sidecarSession;
|
|
@@ -1413,7 +1661,121 @@ export class AgentOs {
|
|
|
1413
1661
|
_agentStderrHandler;
|
|
1414
1662
|
_agentExitHandler;
|
|
1415
1663
|
_limitWarningHandler;
|
|
1416
|
-
|
|
1664
|
+
_disposeHooks = [];
|
|
1665
|
+
/**
|
|
1666
|
+
* Execute commands and manage child processes in the VM.
|
|
1667
|
+
*
|
|
1668
|
+
* `exec` evaluates a command with the configured shell, while `execFile`
|
|
1669
|
+
* invokes an executable directly with an argv array.
|
|
1670
|
+
*/
|
|
1671
|
+
process = {
|
|
1672
|
+
exec: this._exec.bind(this),
|
|
1673
|
+
execFile: this._execFile.bind(this),
|
|
1674
|
+
spawn: this._spawnProcess.bind(this),
|
|
1675
|
+
get: this._getProcess.bind(this),
|
|
1676
|
+
list: this._listProcesses.bind(this),
|
|
1677
|
+
tree: this._processTree.bind(this),
|
|
1678
|
+
wait: this._waitProcess.bind(this),
|
|
1679
|
+
signal: this._signalProcess.bind(this),
|
|
1680
|
+
kill: this._killProcess.bind(this),
|
|
1681
|
+
writeStdin: this._writeProcessStdin.bind(this),
|
|
1682
|
+
closeStdin: this._closeProcessStdin.bind(this),
|
|
1683
|
+
resizePty: this._resizeProcessPty.bind(this),
|
|
1684
|
+
readOutput: this._readProcessOutput.bind(this),
|
|
1685
|
+
};
|
|
1686
|
+
javascript = {
|
|
1687
|
+
execute: this._executeJavaScript.bind(this),
|
|
1688
|
+
evaluate: this._evaluateJavaScript.bind(this),
|
|
1689
|
+
executeFile: this._executeJavaScriptFile.bind(this),
|
|
1690
|
+
spawn: this._spawnJavaScript.bind(this),
|
|
1691
|
+
spawnFile: this._spawnJavaScriptFile.bind(this),
|
|
1692
|
+
npm: {
|
|
1693
|
+
install: this._installNpmPackages.bind(this),
|
|
1694
|
+
runScript: this._executeNpmScript.bind(this),
|
|
1695
|
+
runPackage: this._executeNpmPackage.bind(this),
|
|
1696
|
+
},
|
|
1697
|
+
};
|
|
1698
|
+
typescript = {
|
|
1699
|
+
execute: this._executeTypeScript.bind(this),
|
|
1700
|
+
evaluate: this._evaluateTypeScript.bind(this),
|
|
1701
|
+
executeFile: this._executeTypeScriptFile.bind(this),
|
|
1702
|
+
spawn: this._spawnTypeScript.bind(this),
|
|
1703
|
+
spawnFile: this._spawnTypeScriptFile.bind(this),
|
|
1704
|
+
check: this._checkTypeScript.bind(this),
|
|
1705
|
+
checkProject: this._checkTypeScriptProject.bind(this),
|
|
1706
|
+
};
|
|
1707
|
+
python = {
|
|
1708
|
+
execute: this._executePython.bind(this),
|
|
1709
|
+
evaluate: this._evaluatePython.bind(this),
|
|
1710
|
+
executeFile: this._executePythonFile.bind(this),
|
|
1711
|
+
executeModule: this._executePythonModule.bind(this),
|
|
1712
|
+
spawn: this._spawnPython.bind(this),
|
|
1713
|
+
spawnFile: this._spawnPythonFile.bind(this),
|
|
1714
|
+
spawnModule: this._spawnPythonModule.bind(this),
|
|
1715
|
+
install: this._installPythonPackages.bind(this),
|
|
1716
|
+
};
|
|
1717
|
+
contexts = {
|
|
1718
|
+
get: this._getContext.bind(this),
|
|
1719
|
+
list: this._listContexts.bind(this),
|
|
1720
|
+
reset: this._resetContext.bind(this),
|
|
1721
|
+
delete: this._deleteContext.bind(this),
|
|
1722
|
+
};
|
|
1723
|
+
terminal = {
|
|
1724
|
+
open: this._openTerminal.bind(this),
|
|
1725
|
+
write: this._writeTerminal.bind(this),
|
|
1726
|
+
resize: this._resizeTerminal.bind(this),
|
|
1727
|
+
wait: this._waitTerminal.bind(this),
|
|
1728
|
+
close: this._closeTerminal.bind(this),
|
|
1729
|
+
};
|
|
1730
|
+
filesystem = {
|
|
1731
|
+
readFile: this._readFile.bind(this),
|
|
1732
|
+
writeFile: this._writeFile.bind(this),
|
|
1733
|
+
readFiles: this._readFiles.bind(this),
|
|
1734
|
+
writeFiles: this._writeFiles.bind(this),
|
|
1735
|
+
stat: this._stat.bind(this),
|
|
1736
|
+
mkdir: this._mkdir.bind(this),
|
|
1737
|
+
readdir: this._readdir.bind(this),
|
|
1738
|
+
readdirEntries: this._readdirEntries.bind(this),
|
|
1739
|
+
readdirRecursive: this._readdirRecursive.bind(this),
|
|
1740
|
+
exists: this._exists.bind(this),
|
|
1741
|
+
move: this._move.bind(this),
|
|
1742
|
+
remove: this._remove.bind(this),
|
|
1743
|
+
export: this._exportRootFilesystem.bind(this),
|
|
1744
|
+
mount: this._mountFs.bind(this),
|
|
1745
|
+
unmount: this._unmountFs.bind(this),
|
|
1746
|
+
listMounts: this._listMounts.bind(this),
|
|
1747
|
+
};
|
|
1748
|
+
network = {
|
|
1749
|
+
httpRequest: this._httpRequest.bind(this),
|
|
1750
|
+
};
|
|
1751
|
+
software = {
|
|
1752
|
+
list: this._listSoftware.bind(this),
|
|
1753
|
+
link: this._linkSoftware.bind(this),
|
|
1754
|
+
};
|
|
1755
|
+
agents = {
|
|
1756
|
+
list: this._listAgents.bind(this),
|
|
1757
|
+
};
|
|
1758
|
+
sessions = {
|
|
1759
|
+
open: this._openSession.bind(this),
|
|
1760
|
+
get: this._getSession.bind(this),
|
|
1761
|
+
list: this._listSessions.bind(this),
|
|
1762
|
+
delete: this._deleteSession.bind(this),
|
|
1763
|
+
unload: this._unloadSession.bind(this),
|
|
1764
|
+
prompt: this._prompt.bind(this),
|
|
1765
|
+
cancelPrompt: this._cancelPrompt.bind(this),
|
|
1766
|
+
respondPermission: this._respondPermission.bind(this),
|
|
1767
|
+
readHistory: this._readHistory.bind(this),
|
|
1768
|
+
getConfig: this._getSessionConfig.bind(this),
|
|
1769
|
+
setConfigOption: this._setSessionConfigOption.bind(this),
|
|
1770
|
+
getCapabilities: this._getSessionCapabilities.bind(this),
|
|
1771
|
+
getAgentInfo: this._getSessionAgentInfo.bind(this),
|
|
1772
|
+
};
|
|
1773
|
+
cron = {
|
|
1774
|
+
schedule: this._scheduleCron.bind(this),
|
|
1775
|
+
list: this._listCronJobs.bind(this),
|
|
1776
|
+
cancel: this._cancelCronJob.bind(this),
|
|
1777
|
+
};
|
|
1778
|
+
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, agentStderrHandler, agentExitHandler, limitWarningHandler) {
|
|
1417
1779
|
this.#kernel = kernel;
|
|
1418
1780
|
this.sidecar = sidecar;
|
|
1419
1781
|
this._softwareRoots = softwareRoots;
|
|
@@ -1423,7 +1785,6 @@ export class AgentOs {
|
|
|
1423
1785
|
this._sidecarClient = sidecarClient;
|
|
1424
1786
|
this._sidecarSession = sidecarSession;
|
|
1425
1787
|
this._sidecarVm = sidecarVm;
|
|
1426
|
-
this._additionalInstructions = additionalInstructions;
|
|
1427
1788
|
this._agentStderrHandler = agentStderrHandler;
|
|
1428
1789
|
this._agentExitHandler = agentExitHandler;
|
|
1429
1790
|
this._limitWarningHandler = limitWarningHandler;
|
|
@@ -1448,7 +1809,7 @@ export class AgentOs {
|
|
|
1448
1809
|
// Default software is FULLY DYNAMIC: this package's own NON-agent
|
|
1449
1810
|
// @agentos-software/* dependencies (e.g. common), each default-exporting
|
|
1450
1811
|
// its registry-built descriptor. Agent packages are NOT projected here —
|
|
1451
|
-
//
|
|
1812
|
+
// openSession({ agent: id }) links the matching agent dependency into the running
|
|
1452
1813
|
// VM on first use, so agent closures (and pi's V8 snapshot bundle) only
|
|
1453
1814
|
// enter VMs that run them. Unbuilt packages throw with build
|
|
1454
1815
|
// instructions; opt out via defaultSoftware: false.
|
|
@@ -1479,25 +1840,27 @@ export class AgentOs {
|
|
|
1479
1840
|
// sidecar owns agent resolution, agent enumeration, and agent snapshot
|
|
1480
1841
|
// bundle loading from the projected package dirs.
|
|
1481
1842
|
const localMounts = await resolveCompatLocalMounts(options?.mounts);
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
validateToolkits(toolKits);
|
|
1843
|
+
if (options?.bindings && options.bindings.length > 0) {
|
|
1844
|
+
validateBindings(options.bindings);
|
|
1485
1845
|
}
|
|
1486
|
-
// Resolve the sidecar handle
|
|
1487
|
-
//
|
|
1846
|
+
// Resolve the sidecar handle before starting an external sandbox so option
|
|
1847
|
+
// validation failures cannot leak provider resources.
|
|
1488
1848
|
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1849
|
+
options = await resolveSandboxOptions(options);
|
|
1850
|
+
const sandboxDisposeHooks = getSandboxDisposeHooks(options);
|
|
1851
|
+
const bindings = options.bindings;
|
|
1489
1852
|
const createVmAdmin = async () => {
|
|
1490
1853
|
// The `/opt/agentos` projection is built by the sidecar from the
|
|
1491
1854
|
// forwarded `packages` (it owns the staging dir + read-only mount, and
|
|
1492
1855
|
// runtime `linkSoftware` appends to that live dir). The client no longer
|
|
1493
1856
|
// stages packages host-side.
|
|
1494
|
-
const
|
|
1857
|
+
const bindingBootstrapCommands = collectBindingBootstrapCommands(bindings ?? []);
|
|
1495
1858
|
const bootstrapCommands = [
|
|
1496
1859
|
...RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1497
|
-
...
|
|
1860
|
+
...bindingBootstrapCommands,
|
|
1498
1861
|
];
|
|
1499
1862
|
const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem);
|
|
1500
|
-
let
|
|
1863
|
+
let bindingReference = "";
|
|
1501
1864
|
let rootBridge = null;
|
|
1502
1865
|
let kernel = null;
|
|
1503
1866
|
let client = null;
|
|
@@ -1514,7 +1877,7 @@ export class AgentOs {
|
|
|
1514
1877
|
const env = getBaseEnvironment();
|
|
1515
1878
|
// Guest command paths. The sidecar owns the `/opt/agentos` projection and
|
|
1516
1879
|
// reports the exact projected package commands after `configureVm`.
|
|
1517
|
-
//
|
|
1880
|
+
// Binding-shim commands are added below.
|
|
1518
1881
|
const commandGuestPaths = new Map();
|
|
1519
1882
|
const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
|
|
1520
1883
|
mounts: options?.mounts,
|
|
@@ -1532,11 +1895,24 @@ export class AgentOs {
|
|
|
1532
1895
|
const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
|
|
1533
1896
|
const createVmConfig = {
|
|
1534
1897
|
env,
|
|
1898
|
+
database: options?.database,
|
|
1899
|
+
...(options?.user ? { user: options.user } : {}),
|
|
1535
1900
|
rootFilesystem: serializeRootFilesystemForSidecar(options?.rootFilesystem, bootstrapLower),
|
|
1536
1901
|
permissions: sidecarPermissions,
|
|
1537
1902
|
limits: options?.limits,
|
|
1538
1903
|
loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
|
|
1539
1904
|
bootstrapCommands,
|
|
1905
|
+
...(options?.rootFilesystem?.type === "native"
|
|
1906
|
+
? {
|
|
1907
|
+
nativeRoot: {
|
|
1908
|
+
plugin: {
|
|
1909
|
+
id: options.rootFilesystem.plugin.id,
|
|
1910
|
+
config: options.rootFilesystem.plugin.config ?? {},
|
|
1911
|
+
},
|
|
1912
|
+
readOnly: options.rootFilesystem.readOnly ?? false,
|
|
1913
|
+
},
|
|
1914
|
+
}
|
|
1915
|
+
: {}),
|
|
1540
1916
|
// 0.3: the Node builtin allow-list moved from configureVm to
|
|
1541
1917
|
// VM creation. `undefined` => engine default allow-list;
|
|
1542
1918
|
// `[]` => deny all; `[..]` => exactly those. Platform and
|
|
@@ -1577,16 +1953,16 @@ export class AgentOs {
|
|
|
1577
1953
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1578
1954
|
packages: sidecarPackages,
|
|
1579
1955
|
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1580
|
-
|
|
1956
|
+
bindingShimCommands: bindingBootstrapCommands,
|
|
1581
1957
|
});
|
|
1582
1958
|
for (const command of configuredVm.projectedCommands) {
|
|
1583
1959
|
commandGuestPaths.set(command.name, command.guestPath);
|
|
1584
1960
|
}
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1961
|
+
if (bindings && bindings.length > 0) {
|
|
1962
|
+
bindingReference = await registerBindingsOnSidecar(client, session, nativeVm, bindings);
|
|
1587
1963
|
commandGuestPaths.set("agentos", "/bin/agentos");
|
|
1588
|
-
for (const
|
|
1589
|
-
commandGuestPaths.set(`agentos-${
|
|
1964
|
+
for (const bindingCollection of bindings) {
|
|
1965
|
+
commandGuestPaths.set(`agentos-${bindingCollection.name}`, `/bin/agentos-${bindingCollection.name}`);
|
|
1590
1966
|
}
|
|
1591
1967
|
}
|
|
1592
1968
|
rootBridge = new NativeSidecarKernelProxy({
|
|
@@ -1602,17 +1978,19 @@ export class AgentOs {
|
|
|
1602
1978
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1603
1979
|
// Retained for runtime mount reconfigures: `configure_vm` is
|
|
1604
1980
|
// replace-on-write for the whole payload, so post-boot mountFs
|
|
1605
|
-
// must resend the boot packages and
|
|
1981
|
+
// must resend the boot packages and binding shims.
|
|
1606
1982
|
packages: sidecarPackages,
|
|
1607
1983
|
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1608
|
-
|
|
1984
|
+
bindingShimCommands: bindingBootstrapCommands,
|
|
1609
1985
|
commandGuestPaths,
|
|
1610
1986
|
onDispose: cleanup,
|
|
1611
1987
|
// The native process is owned by the AgentOsSidecar handle and
|
|
1612
1988
|
// shared across VMs; disposing this VM must not kill the process.
|
|
1613
1989
|
ownsClient: false,
|
|
1614
1990
|
});
|
|
1615
|
-
|
|
1991
|
+
if (options?.rootFilesystem?.type !== "native") {
|
|
1992
|
+
await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
|
|
1993
|
+
}
|
|
1616
1994
|
kernel = rootBridge;
|
|
1617
1995
|
const snapshotClient = client;
|
|
1618
1996
|
return {
|
|
@@ -1628,9 +2006,9 @@ export class AgentOs {
|
|
|
1628
2006
|
sidecarSession: session,
|
|
1629
2007
|
sidecarVm: nativeVm,
|
|
1630
2008
|
permissions: hostPermissions,
|
|
1631
|
-
snapshotRootFilesystem: async () => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm))),
|
|
1632
|
-
|
|
1633
|
-
|
|
2009
|
+
snapshotRootFilesystem: async (maxBytes) => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm, maxBytes))),
|
|
2010
|
+
bindings: bindings ?? [],
|
|
2011
|
+
bindingReference,
|
|
1634
2012
|
async dispose() {
|
|
1635
2013
|
if (kernel) {
|
|
1636
2014
|
const currentKernel = kernel;
|
|
@@ -1673,112 +2051,804 @@ export class AgentOs {
|
|
|
1673
2051
|
createVm: async () => createVmAdmin(),
|
|
1674
2052
|
});
|
|
1675
2053
|
const vmAdmin = sidecarLease.admin;
|
|
1676
|
-
const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.
|
|
2054
|
+
const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.onAgentStderr ?? defaultAgentStderrHandler, options?.onAgentExit ?? defaultAgentExitHandler, options?.onLimitWarning);
|
|
1677
2055
|
vm._sidecarLease = sidecarLease;
|
|
1678
|
-
vm.
|
|
1679
|
-
vm.
|
|
2056
|
+
vm._bindings = vmAdmin.bindings;
|
|
2057
|
+
vm._bindingReference = vmAdmin.bindingReference;
|
|
1680
2058
|
vm._permissions = vmAdmin.permissions;
|
|
2059
|
+
vm._disposeHooks.push(...sandboxDisposeHooks);
|
|
1681
2060
|
vm._installSidecarRequestHandler();
|
|
1682
2061
|
vm._cronManager = new CronManager(vm, options?.scheduleDriver ?? new TimerScheduleDriver());
|
|
1683
2062
|
return vm;
|
|
1684
2063
|
}
|
|
1685
2064
|
catch (error) {
|
|
1686
|
-
|
|
2065
|
+
const cleanupErrors = [];
|
|
2066
|
+
try {
|
|
2067
|
+
await sidecarLease?.dispose();
|
|
2068
|
+
}
|
|
2069
|
+
catch (disposeError) {
|
|
2070
|
+
cleanupErrors.push(disposeError);
|
|
2071
|
+
}
|
|
2072
|
+
const sandboxCleanupResults = await Promise.allSettled(sandboxDisposeHooks.map((hook) => hook()));
|
|
2073
|
+
cleanupErrors.push(...sandboxCleanupResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
|
|
2074
|
+
if (cleanupErrors.length > 0) {
|
|
2075
|
+
throw new AggregateError([error, ...cleanupErrors], "AgentOS VM creation and cleanup failed");
|
|
2076
|
+
}
|
|
2077
|
+
throw error;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
async createContext(contextId) {
|
|
2081
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "create_context", request: { contextId } });
|
|
2082
|
+
if (response.type !== "execution_descriptor") {
|
|
2083
|
+
throw new Error(`unexpected createContext response: ${response.type}`);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
async _executionOperation(payload, options, background = false) {
|
|
2087
|
+
if (options.signal?.aborted) {
|
|
2088
|
+
throw options.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
2089
|
+
}
|
|
2090
|
+
let executionId;
|
|
2091
|
+
const earlyOutput = [];
|
|
2092
|
+
const completedBeforeAdmission = new Set();
|
|
2093
|
+
let resolveCompletion;
|
|
2094
|
+
const completion = new Promise((resolve) => {
|
|
2095
|
+
resolveCompletion = resolve;
|
|
2096
|
+
});
|
|
2097
|
+
let warnedAboutEarlyEvents = false;
|
|
2098
|
+
const warnAboutEarlyEvents = () => {
|
|
2099
|
+
if (warnedAboutEarlyEvents)
|
|
2100
|
+
return;
|
|
2101
|
+
warnedAboutEarlyEvents = true;
|
|
2102
|
+
console.warn("[agentos] execution admission received more than 1024 early events; use process.readOutput() to replay any omitted output");
|
|
2103
|
+
};
|
|
2104
|
+
const deliverOutput = (event) => {
|
|
2105
|
+
if (event.channel === "stderr")
|
|
2106
|
+
options.onStderr?.(event.chunk);
|
|
2107
|
+
else
|
|
2108
|
+
options.onStdout?.(event.chunk);
|
|
2109
|
+
};
|
|
2110
|
+
const unsubscribeOutput = this._onExecutionOutput("*", (event) => {
|
|
2111
|
+
if (executionId === undefined) {
|
|
2112
|
+
if (earlyOutput.length < 1_024)
|
|
2113
|
+
earlyOutput.push(event);
|
|
2114
|
+
else
|
|
2115
|
+
warnAboutEarlyEvents();
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (event.executionId === executionId)
|
|
2119
|
+
deliverOutput(event);
|
|
2120
|
+
});
|
|
2121
|
+
let completed = false;
|
|
2122
|
+
const unsubscribeCompleted = this._onExecutionCompleted("*", (event) => {
|
|
2123
|
+
if (executionId === undefined) {
|
|
2124
|
+
if (completedBeforeAdmission.size < 1_024) {
|
|
2125
|
+
completedBeforeAdmission.add(event.executionId);
|
|
2126
|
+
}
|
|
2127
|
+
else
|
|
2128
|
+
warnAboutEarlyEvents();
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
if (event.executionId === executionId) {
|
|
2132
|
+
completed = true;
|
|
2133
|
+
resolveCompletion?.();
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
let response;
|
|
2137
|
+
try {
|
|
2138
|
+
response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, payload);
|
|
2139
|
+
}
|
|
2140
|
+
catch (error) {
|
|
2141
|
+
unsubscribeOutput();
|
|
2142
|
+
unsubscribeCompleted();
|
|
1687
2143
|
throw error;
|
|
1688
2144
|
}
|
|
2145
|
+
if (response.type !== "execution_accepted") {
|
|
2146
|
+
unsubscribeOutput();
|
|
2147
|
+
unsubscribeCompleted();
|
|
2148
|
+
throw new Error(`unexpected execution response: ${response.type}`);
|
|
2149
|
+
}
|
|
2150
|
+
const descriptor = response.response.execution;
|
|
2151
|
+
executionId = response.response.operationId;
|
|
2152
|
+
for (const event of earlyOutput) {
|
|
2153
|
+
if (event.executionId === executionId)
|
|
2154
|
+
deliverOutput(event);
|
|
2155
|
+
}
|
|
2156
|
+
completed = completedBeforeAdmission.has(executionId);
|
|
2157
|
+
if (completed)
|
|
2158
|
+
resolveCompletion?.();
|
|
2159
|
+
const abort = () => {
|
|
2160
|
+
// Admitted executions report cancellation through their structured result.
|
|
2161
|
+
void this._cancelExecution(response.response.operationId).catch((error) => {
|
|
2162
|
+
console.error("[agentos] failed to cancel aborted execution", error);
|
|
2163
|
+
});
|
|
2164
|
+
};
|
|
2165
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
2166
|
+
if (options.signal?.aborted)
|
|
2167
|
+
abort();
|
|
2168
|
+
const cleanup = () => {
|
|
2169
|
+
unsubscribeOutput();
|
|
2170
|
+
unsubscribeCompleted();
|
|
2171
|
+
options.signal?.removeEventListener("abort", abort);
|
|
2172
|
+
};
|
|
2173
|
+
if (background) {
|
|
2174
|
+
if (!descriptor || descriptor.pid === null) {
|
|
2175
|
+
cleanup();
|
|
2176
|
+
throw new Error("spawned execution admission returned no process id");
|
|
2177
|
+
}
|
|
2178
|
+
if (completed)
|
|
2179
|
+
cleanup();
|
|
2180
|
+
else {
|
|
2181
|
+
const unsubscribeBackgroundCompletion = this._onExecutionCompleted(descriptor.executionId, () => {
|
|
2182
|
+
unsubscribeBackgroundCompletion();
|
|
2183
|
+
cleanup();
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
return {
|
|
2187
|
+
executionId: descriptor.executionId,
|
|
2188
|
+
pid: descriptor.pid,
|
|
2189
|
+
completed,
|
|
2190
|
+
completion,
|
|
2191
|
+
createdAtMs: safeProtocolNumber(descriptor.createdAtMs, "process startedAtMs"),
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
try {
|
|
2195
|
+
await completion;
|
|
2196
|
+
return await this._waitExecutionResult(response.response.operationId);
|
|
2197
|
+
}
|
|
2198
|
+
finally {
|
|
2199
|
+
cleanup();
|
|
2200
|
+
}
|
|
1689
2201
|
}
|
|
2202
|
+
async _spawnLanguageOperation(buildPayload, options, language) {
|
|
2203
|
+
if ("contextId" in options) {
|
|
2204
|
+
throw new Error("contextId is not supported for spawned processes; contexts are for attached operations");
|
|
2205
|
+
}
|
|
2206
|
+
const executionId = `process-${randomUUID()}`;
|
|
2207
|
+
const internalOptions = {
|
|
2208
|
+
...options,
|
|
2209
|
+
output: options.output,
|
|
2210
|
+
};
|
|
2211
|
+
const admitted = (await this._executionOperation(buildPayload(internalOptions, executionId), internalOptions, true));
|
|
2212
|
+
const descriptor = {
|
|
2213
|
+
pid: admitted.pid,
|
|
2214
|
+
state: "running",
|
|
2215
|
+
language,
|
|
2216
|
+
startedAtMs: admitted.createdAtMs,
|
|
2217
|
+
};
|
|
2218
|
+
this._languageProcesses.set(admitted.pid, {
|
|
2219
|
+
executionId: admitted.executionId,
|
|
2220
|
+
descriptor,
|
|
2221
|
+
outputHandlers: new Set(),
|
|
2222
|
+
exitHandlers: new Set(),
|
|
2223
|
+
});
|
|
2224
|
+
this._languageProcessIds.set(admitted.executionId, admitted.pid);
|
|
2225
|
+
const reconcileCompletion = async () => {
|
|
2226
|
+
if (!this._languageProcesses.get(admitted.pid)?.exit) {
|
|
2227
|
+
await this._waitProcess(admitted.pid);
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
if (admitted.completed)
|
|
2231
|
+
await reconcileCompletion();
|
|
2232
|
+
else {
|
|
2233
|
+
void admitted.completion
|
|
2234
|
+
.then(reconcileCompletion)
|
|
2235
|
+
.catch((error) => console.error("[agentos] failed to reconcile spawned process completion", error));
|
|
2236
|
+
}
|
|
2237
|
+
if (options.signal) {
|
|
2238
|
+
const abort = () => {
|
|
2239
|
+
void this._killProcess(admitted.pid);
|
|
2240
|
+
};
|
|
2241
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
2242
|
+
}
|
|
2243
|
+
return this._languageProcesses.get(admitted.pid)?.descriptor ?? descriptor;
|
|
2244
|
+
}
|
|
2245
|
+
async _exec(command, options = {}) {
|
|
2246
|
+
return (await this._executionOperation({
|
|
2247
|
+
type: "shell_execution",
|
|
2248
|
+
request: { process: processExecutionOptions(options), command },
|
|
2249
|
+
}, options));
|
|
2250
|
+
}
|
|
2251
|
+
async _execFile(command, args = [], options = {}) {
|
|
2252
|
+
const operationOptions = { ...options, args: [...args] };
|
|
2253
|
+
return (await this._executionOperation({
|
|
2254
|
+
type: "argv_execution",
|
|
2255
|
+
request: {
|
|
2256
|
+
process: processExecutionOptions(operationOptions),
|
|
2257
|
+
command,
|
|
2258
|
+
},
|
|
2259
|
+
}, operationOptions));
|
|
2260
|
+
}
|
|
2261
|
+
/** @deprecated Use `process.exec()` for lifecycle-aware execution. */
|
|
1690
2262
|
async exec(command, options) {
|
|
1691
2263
|
return this.#kernel.exec(command, options);
|
|
1692
2264
|
}
|
|
2265
|
+
/** @deprecated Use `process.execFile()` for lifecycle-aware execution. */
|
|
1693
2266
|
async execArgv(command, args = [], options) {
|
|
1694
2267
|
const kernel = this.#kernel;
|
|
1695
2268
|
return kernel.execArgv(command, args, options);
|
|
1696
2269
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
// `_processes` is a process table for this VM's lifetime; it is freed wholesale
|
|
1711
|
-
// in dispose(). (H5: the leak was that dispose() never cleared it.)
|
|
1712
|
-
void proc.wait().then((code) => {
|
|
1713
|
-
for (const h of exitHandlers)
|
|
1714
|
-
h(code);
|
|
1715
|
-
});
|
|
1716
|
-
return { pid: proc.pid };
|
|
2270
|
+
async _executeJavaScript(source, options = {}) {
|
|
2271
|
+
return (await this._executionOperation({
|
|
2272
|
+
type: "javascript_execution",
|
|
2273
|
+
request: {
|
|
2274
|
+
process: processExecutionOptions(options),
|
|
2275
|
+
source,
|
|
2276
|
+
format: options.format === "commonjs"
|
|
2277
|
+
? executionProtocol.JavaScriptModuleFormat.CommonJs
|
|
2278
|
+
: executionProtocol.JavaScriptModuleFormat.Module,
|
|
2279
|
+
filePath: options.filePath ?? null,
|
|
2280
|
+
inputs: options.inputs ? JSON.stringify(options.inputs) : null,
|
|
2281
|
+
},
|
|
2282
|
+
}, options));
|
|
1717
2283
|
}
|
|
1718
|
-
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
if (
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2284
|
+
async _evaluationOperation(payload, options) {
|
|
2285
|
+
const result = (await this._executionOperation(payload, options));
|
|
2286
|
+
if (result.outcome !== "succeeded")
|
|
2287
|
+
return result;
|
|
2288
|
+
const value = result.evaluationValue;
|
|
2289
|
+
if (value === undefined) {
|
|
2290
|
+
throw new Error("successful evaluation did not return a JSON-serializable value");
|
|
2291
|
+
}
|
|
2292
|
+
return { ...result, value: value };
|
|
2293
|
+
}
|
|
2294
|
+
async _evaluateJavaScript(expression, options = {}) {
|
|
2295
|
+
return this._evaluationOperation({
|
|
2296
|
+
type: "javascript_evaluation",
|
|
2297
|
+
request: {
|
|
2298
|
+
process: processExecutionOptions(options),
|
|
2299
|
+
expression,
|
|
2300
|
+
format: options.format === "commonjs"
|
|
2301
|
+
? executionProtocol.JavaScriptModuleFormat.CommonJs
|
|
2302
|
+
: executionProtocol.JavaScriptModuleFormat.Module,
|
|
2303
|
+
filePath: options.filePath ?? null,
|
|
2304
|
+
inputs: options.inputs ? JSON.stringify(options.inputs) : null,
|
|
1732
2305
|
},
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2306
|
+
}, options);
|
|
2307
|
+
}
|
|
2308
|
+
async _executeJavaScriptFile(path, options = {}) {
|
|
2309
|
+
return (await this._executionOperation({
|
|
2310
|
+
type: "javascript_file_execution",
|
|
2311
|
+
request: { process: processExecutionOptions(options), path },
|
|
2312
|
+
}, options));
|
|
2313
|
+
}
|
|
2314
|
+
_spawnJavaScript(source, options = {}) {
|
|
2315
|
+
return this._spawnLanguageOperation((operationOptions, executionId) => ({
|
|
2316
|
+
type: "javascript_execution",
|
|
2317
|
+
request: {
|
|
2318
|
+
process: processExecutionOptions(operationOptions, {
|
|
2319
|
+
background: true,
|
|
2320
|
+
executionId,
|
|
2321
|
+
}),
|
|
2322
|
+
source,
|
|
2323
|
+
format: executionProtocol.JavaScriptModuleFormat.Module,
|
|
2324
|
+
filePath: null,
|
|
2325
|
+
inputs: null,
|
|
1736
2326
|
},
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
return
|
|
2327
|
+
}), options, "javascript");
|
|
2328
|
+
}
|
|
2329
|
+
_spawnJavaScriptFile(path, options = {}) {
|
|
2330
|
+
return this._spawnLanguageOperation((operationOptions, executionId) => ({
|
|
2331
|
+
type: "javascript_file_execution",
|
|
2332
|
+
request: {
|
|
2333
|
+
process: processExecutionOptions(operationOptions, {
|
|
2334
|
+
background: true,
|
|
2335
|
+
executionId,
|
|
2336
|
+
}),
|
|
2337
|
+
path,
|
|
2338
|
+
},
|
|
2339
|
+
}), options, "javascript");
|
|
2340
|
+
}
|
|
2341
|
+
async _executeTypeScript(source, options = {}) {
|
|
2342
|
+
return (await this._executionOperation({
|
|
2343
|
+
type: "typescript_execution",
|
|
2344
|
+
request: {
|
|
2345
|
+
process: processExecutionOptions(options),
|
|
2346
|
+
source,
|
|
2347
|
+
filePath: options.filePath ?? null,
|
|
2348
|
+
tsconfigPath: options.tsconfigPath ?? null,
|
|
2349
|
+
compilerOptions: options.compilerOptions
|
|
2350
|
+
? JSON.stringify(options.compilerOptions)
|
|
2351
|
+
: null,
|
|
2352
|
+
inputs: options.inputs ? JSON.stringify(options.inputs) : null,
|
|
2353
|
+
},
|
|
2354
|
+
}, options));
|
|
2355
|
+
}
|
|
2356
|
+
async _evaluateTypeScript(expression, options = {}) {
|
|
2357
|
+
return this._evaluationOperation({
|
|
2358
|
+
type: "typescript_evaluation",
|
|
2359
|
+
request: {
|
|
2360
|
+
process: processExecutionOptions(options),
|
|
2361
|
+
expression,
|
|
2362
|
+
filePath: options.filePath ?? null,
|
|
2363
|
+
tsconfigPath: options.tsconfigPath ?? null,
|
|
2364
|
+
compilerOptions: options.compilerOptions
|
|
2365
|
+
? JSON.stringify(options.compilerOptions)
|
|
2366
|
+
: null,
|
|
2367
|
+
inputs: options.inputs ? JSON.stringify(options.inputs) : null,
|
|
2368
|
+
},
|
|
2369
|
+
}, options);
|
|
2370
|
+
}
|
|
2371
|
+
async _executeTypeScriptFile(path, options = {}) {
|
|
2372
|
+
return (await this._executionOperation({
|
|
2373
|
+
type: "typescript_file_execution",
|
|
2374
|
+
request: {
|
|
2375
|
+
process: processExecutionOptions(options),
|
|
2376
|
+
path,
|
|
2377
|
+
tsconfigPath: options.tsconfigPath ?? null,
|
|
2378
|
+
compilerOptions: options.compilerOptions
|
|
2379
|
+
? JSON.stringify(options.compilerOptions)
|
|
2380
|
+
: null,
|
|
2381
|
+
},
|
|
2382
|
+
}, options));
|
|
2383
|
+
}
|
|
2384
|
+
_spawnTypeScript(source, options = {}) {
|
|
2385
|
+
return this._spawnLanguageOperation((operationOptions, executionId) => ({
|
|
2386
|
+
type: "typescript_execution",
|
|
2387
|
+
request: {
|
|
2388
|
+
process: processExecutionOptions(operationOptions, {
|
|
2389
|
+
background: true,
|
|
2390
|
+
executionId,
|
|
2391
|
+
}),
|
|
2392
|
+
source,
|
|
2393
|
+
filePath: null,
|
|
2394
|
+
tsconfigPath: null,
|
|
2395
|
+
compilerOptions: null,
|
|
2396
|
+
inputs: null,
|
|
2397
|
+
},
|
|
2398
|
+
}), options, "javascript");
|
|
2399
|
+
}
|
|
2400
|
+
_spawnTypeScriptFile(path, options = {}) {
|
|
2401
|
+
return this._spawnLanguageOperation((operationOptions, executionId) => ({
|
|
2402
|
+
type: "typescript_file_execution",
|
|
2403
|
+
request: {
|
|
2404
|
+
process: processExecutionOptions(operationOptions, {
|
|
2405
|
+
background: true,
|
|
2406
|
+
executionId,
|
|
2407
|
+
}),
|
|
2408
|
+
path,
|
|
2409
|
+
tsconfigPath: null,
|
|
2410
|
+
compilerOptions: null,
|
|
2411
|
+
},
|
|
2412
|
+
}), options, "javascript");
|
|
2413
|
+
}
|
|
2414
|
+
async _checkTypeScript(source, options = {}) {
|
|
2415
|
+
const result = (await this._executionOperation({
|
|
2416
|
+
type: "typescript_check",
|
|
2417
|
+
request: {
|
|
2418
|
+
identity: executionIdentity(options),
|
|
2419
|
+
output: executionOutput(options),
|
|
2420
|
+
source,
|
|
2421
|
+
cwd: options.cwd ?? null,
|
|
2422
|
+
filePath: options.filePath ?? null,
|
|
2423
|
+
tsconfigPath: options.tsconfigPath ?? null,
|
|
2424
|
+
compilerOptions: options.compilerOptions
|
|
2425
|
+
? JSON.stringify(options.compilerOptions)
|
|
2426
|
+
: null,
|
|
2427
|
+
timeoutMs: protocolTimeout(options.timeoutMs),
|
|
2428
|
+
},
|
|
2429
|
+
}, options));
|
|
2430
|
+
return mapTypeScriptCheckResult(result);
|
|
2431
|
+
}
|
|
2432
|
+
async _checkTypeScriptProject(options = {}) {
|
|
2433
|
+
const result = (await this._executionOperation({
|
|
2434
|
+
type: "typescript_project_check",
|
|
2435
|
+
request: {
|
|
2436
|
+
identity: executionIdentity(options),
|
|
2437
|
+
output: executionOutput(options),
|
|
2438
|
+
cwd: options.cwd ?? null,
|
|
2439
|
+
tsconfigPath: options.tsconfigPath ?? null,
|
|
2440
|
+
timeoutMs: protocolTimeout(options.timeoutMs),
|
|
2441
|
+
},
|
|
2442
|
+
}, options));
|
|
2443
|
+
return mapTypeScriptCheckResult(result);
|
|
2444
|
+
}
|
|
2445
|
+
async _installNpmPackages(packagesOrOptions = {}, maybeOptions = {}) {
|
|
2446
|
+
if (typeof packagesOrOptions === "string" ||
|
|
2447
|
+
Array.isArray(packagesOrOptions)) {
|
|
2448
|
+
const packages = Array.isArray(packagesOrOptions)
|
|
2449
|
+
? packagesOrOptions
|
|
2450
|
+
: [packagesOrOptions];
|
|
2451
|
+
return (await this._executionOperation({
|
|
2452
|
+
type: "npm_package_install",
|
|
2453
|
+
request: {
|
|
2454
|
+
identity: executionIdentity(maybeOptions),
|
|
2455
|
+
output: executionOutput(maybeOptions),
|
|
2456
|
+
cwd: maybeOptions.cwd ?? null,
|
|
2457
|
+
env: maybeOptions.env
|
|
2458
|
+
? new Map(Object.entries(maybeOptions.env))
|
|
2459
|
+
: null,
|
|
2460
|
+
timeoutMs: protocolTimeout(maybeOptions.timeoutMs),
|
|
2461
|
+
packages,
|
|
2462
|
+
dev: maybeOptions.dev ?? null,
|
|
2463
|
+
global: maybeOptions.global ?? null,
|
|
2464
|
+
},
|
|
2465
|
+
}, maybeOptions));
|
|
2466
|
+
}
|
|
2467
|
+
const options = packagesOrOptions;
|
|
2468
|
+
return (await this._executionOperation({
|
|
2469
|
+
type: "npm_project_install",
|
|
2470
|
+
request: {
|
|
2471
|
+
identity: executionIdentity(options),
|
|
2472
|
+
output: executionOutput(options),
|
|
2473
|
+
cwd: options.cwd ?? null,
|
|
2474
|
+
env: options.env ? new Map(Object.entries(options.env)) : null,
|
|
2475
|
+
timeoutMs: protocolTimeout(options.timeoutMs),
|
|
2476
|
+
frozen: options.frozen ?? null,
|
|
2477
|
+
},
|
|
2478
|
+
}, options));
|
|
2479
|
+
}
|
|
2480
|
+
async _executeNpmScript(script, options = {}) {
|
|
2481
|
+
return (await this._executionOperation({
|
|
2482
|
+
type: "npm_script_execution",
|
|
2483
|
+
request: { process: processExecutionOptions(options), script },
|
|
2484
|
+
}, options));
|
|
2485
|
+
}
|
|
2486
|
+
async _executeNpmPackage(packageSpec, options = {}) {
|
|
2487
|
+
return (await this._executionOperation({
|
|
2488
|
+
type: "npm_package_execution",
|
|
2489
|
+
request: {
|
|
2490
|
+
process: processExecutionOptions(options),
|
|
2491
|
+
packageSpec,
|
|
2492
|
+
binary: options.binary ?? null,
|
|
2493
|
+
},
|
|
2494
|
+
}, options));
|
|
2495
|
+
}
|
|
2496
|
+
async _executePython(source, options = {}) {
|
|
2497
|
+
return (await this._executionOperation({
|
|
2498
|
+
type: "python_execution",
|
|
2499
|
+
request: {
|
|
2500
|
+
process: processExecutionOptions(options),
|
|
2501
|
+
source,
|
|
2502
|
+
inputs: options.inputs ? JSON.stringify(options.inputs) : null,
|
|
2503
|
+
},
|
|
2504
|
+
}, options));
|
|
2505
|
+
}
|
|
2506
|
+
async _evaluatePython(expression, options = {}) {
|
|
2507
|
+
return this._evaluationOperation({
|
|
2508
|
+
type: "python_evaluation",
|
|
2509
|
+
request: {
|
|
2510
|
+
process: processExecutionOptions(options),
|
|
2511
|
+
expression,
|
|
2512
|
+
inputs: options.inputs ? JSON.stringify(options.inputs) : null,
|
|
2513
|
+
},
|
|
2514
|
+
}, options);
|
|
2515
|
+
}
|
|
2516
|
+
async _executePythonFile(path, options = {}) {
|
|
2517
|
+
return (await this._executionOperation({
|
|
2518
|
+
type: "python_file_execution",
|
|
2519
|
+
request: { process: processExecutionOptions(options), path },
|
|
2520
|
+
}, options));
|
|
2521
|
+
}
|
|
2522
|
+
async _executePythonModule(module, options = {}) {
|
|
2523
|
+
return (await this._executionOperation({
|
|
2524
|
+
type: "python_module_execution",
|
|
2525
|
+
request: { process: processExecutionOptions(options), module },
|
|
2526
|
+
}, options));
|
|
2527
|
+
}
|
|
2528
|
+
_spawnPython(source, options = {}) {
|
|
2529
|
+
return this._spawnLanguageOperation((operationOptions, executionId) => ({
|
|
2530
|
+
type: "python_execution",
|
|
2531
|
+
request: {
|
|
2532
|
+
process: processExecutionOptions(operationOptions, {
|
|
2533
|
+
background: true,
|
|
2534
|
+
executionId,
|
|
2535
|
+
}),
|
|
2536
|
+
source,
|
|
2537
|
+
inputs: null,
|
|
2538
|
+
},
|
|
2539
|
+
}), options, "python");
|
|
2540
|
+
}
|
|
2541
|
+
_spawnPythonFile(path, options = {}) {
|
|
2542
|
+
return this._spawnLanguageOperation((operationOptions, executionId) => ({
|
|
2543
|
+
type: "python_file_execution",
|
|
2544
|
+
request: {
|
|
2545
|
+
process: processExecutionOptions(operationOptions, {
|
|
2546
|
+
background: true,
|
|
2547
|
+
executionId,
|
|
2548
|
+
}),
|
|
2549
|
+
path,
|
|
2550
|
+
},
|
|
2551
|
+
}), options, "python");
|
|
2552
|
+
}
|
|
2553
|
+
_spawnPythonModule(module, options = {}) {
|
|
2554
|
+
return this._spawnLanguageOperation((operationOptions, executionId) => ({
|
|
2555
|
+
type: "python_module_execution",
|
|
2556
|
+
request: {
|
|
2557
|
+
process: processExecutionOptions(operationOptions, {
|
|
2558
|
+
background: true,
|
|
2559
|
+
executionId,
|
|
2560
|
+
}),
|
|
2561
|
+
module,
|
|
2562
|
+
},
|
|
2563
|
+
}), options, "python");
|
|
2564
|
+
}
|
|
2565
|
+
async _installPythonPackages(packagesOrOptions = {}, maybeOptions = {}) {
|
|
2566
|
+
const packages = typeof packagesOrOptions === "string"
|
|
2567
|
+
? [packagesOrOptions]
|
|
2568
|
+
: Array.isArray(packagesOrOptions)
|
|
2569
|
+
? packagesOrOptions
|
|
2570
|
+
: [];
|
|
2571
|
+
const options = typeof packagesOrOptions === "string" || Array.isArray(packagesOrOptions)
|
|
2572
|
+
? maybeOptions
|
|
2573
|
+
: packagesOrOptions;
|
|
2574
|
+
return (await this._executionOperation({
|
|
2575
|
+
type: "python_install",
|
|
2576
|
+
request: {
|
|
2577
|
+
identity: executionIdentity(options),
|
|
2578
|
+
output: executionOutput(options),
|
|
2579
|
+
cwd: options.cwd ?? null,
|
|
2580
|
+
env: options.env ? new Map(Object.entries(options.env)) : null,
|
|
2581
|
+
timeoutMs: protocolTimeout(options.timeoutMs),
|
|
2582
|
+
packages,
|
|
2583
|
+
upgrade: options.upgrade ?? null,
|
|
2584
|
+
requirementsFile: options.requirementsFile ?? null,
|
|
2585
|
+
indexUrl: options.indexUrl ?? null,
|
|
2586
|
+
extraIndexUrls: options.extraIndexUrls ?? [],
|
|
2587
|
+
},
|
|
2588
|
+
}, options));
|
|
2589
|
+
}
|
|
2590
|
+
async _getContext(contextId) {
|
|
2591
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "get_execution", request: { executionId: contextId } });
|
|
2592
|
+
if (response.type !== "execution_descriptor") {
|
|
2593
|
+
throw new Error(`unexpected getContext response: ${response.type}`);
|
|
2594
|
+
}
|
|
2595
|
+
return mapExecutionDescriptor(response.response.execution);
|
|
2596
|
+
}
|
|
2597
|
+
async _listContexts() {
|
|
2598
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "list_executions" });
|
|
2599
|
+
if (response.type !== "execution_list") {
|
|
2600
|
+
throw new Error(`unexpected listContexts response: ${response.type}`);
|
|
2601
|
+
}
|
|
2602
|
+
return response.response.executions.map(mapExecutionDescriptor);
|
|
2603
|
+
}
|
|
2604
|
+
async _waitExecutionResult(executionId) {
|
|
2605
|
+
let resolveCompletion;
|
|
2606
|
+
const completion = new Promise((resolve) => {
|
|
2607
|
+
resolveCompletion = resolve;
|
|
2608
|
+
});
|
|
2609
|
+
const unsubscribe = this._onExecutionCompleted(executionId, () => {
|
|
2610
|
+
resolveCompletion?.();
|
|
2611
|
+
});
|
|
2612
|
+
let response;
|
|
2613
|
+
try {
|
|
2614
|
+
try {
|
|
2615
|
+
response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "wait_execution", request: { executionId } });
|
|
2616
|
+
}
|
|
2617
|
+
catch (error) {
|
|
2618
|
+
if (!(error instanceof SidecarRejectedError) ||
|
|
2619
|
+
error.detail.code !== "execution_busy") {
|
|
2620
|
+
throw error;
|
|
2621
|
+
}
|
|
2622
|
+
await completion;
|
|
2623
|
+
response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "wait_execution", request: { executionId } });
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
finally {
|
|
2627
|
+
unsubscribe();
|
|
2628
|
+
}
|
|
2629
|
+
if (response.type !== "execution_completed") {
|
|
2630
|
+
throw new Error(`unexpected waitExecution response: ${response.type}`);
|
|
2631
|
+
}
|
|
2632
|
+
return mapExecutionResult(response.response);
|
|
2633
|
+
}
|
|
2634
|
+
async _cancelExecution(executionId) {
|
|
2635
|
+
await this._descriptorLifecycleRequest("cancel_execution", executionId);
|
|
2636
|
+
}
|
|
2637
|
+
async _signalExecution(executionId, signal) {
|
|
2638
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "signal_execution", request: { executionId, signal } });
|
|
2639
|
+
if (response.type !== "execution_descriptor") {
|
|
2640
|
+
throw new Error(`unexpected signalExecution response: ${response.type}`);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
async _resetContext(contextId) {
|
|
2644
|
+
await this._descriptorLifecycleRequest("reset_execution", contextId);
|
|
2645
|
+
}
|
|
2646
|
+
async _descriptorLifecycleRequest(type, executionId) {
|
|
2647
|
+
const payload = type === "cancel_execution"
|
|
2648
|
+
? { type, request: { executionId } }
|
|
2649
|
+
: { type, request: { executionId } };
|
|
2650
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, payload);
|
|
2651
|
+
if (response.type !== "execution_descriptor") {
|
|
2652
|
+
throw new Error(`unexpected ${type} response: ${response.type}`);
|
|
2653
|
+
}
|
|
2654
|
+
return mapExecutionDescriptor(response.response.execution);
|
|
2655
|
+
}
|
|
2656
|
+
async _deleteContext(contextId) {
|
|
2657
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "delete_execution", request: { executionId: contextId } });
|
|
2658
|
+
if (response.type !== "execution_deleted") {
|
|
2659
|
+
throw new Error(`unexpected deleteContext response: ${response.type}`);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
async _writeExecutionStdin(executionId, data) {
|
|
2663
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, {
|
|
2664
|
+
type: "write_execution_stdin",
|
|
2665
|
+
request: { executionId, chunk: executionBytes(data) },
|
|
2666
|
+
});
|
|
2667
|
+
if (response.type !== "execution_io") {
|
|
2668
|
+
throw new Error(`unexpected writeExecutionStdin response: ${response.type}`);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
async _closeExecutionStdin(executionId) {
|
|
2672
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, { type: "close_execution_stdin", request: { executionId } });
|
|
2673
|
+
if (response.type !== "execution_io") {
|
|
2674
|
+
throw new Error(`unexpected closeExecutionStdin response: ${response.type}`);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
async _resizeExecutionPty(executionId, size) {
|
|
2678
|
+
const cols = protocolUnsigned(size.cols, 0xffff, "size.cols");
|
|
2679
|
+
const rows = protocolUnsigned(size.rows, 0xffff, "size.rows");
|
|
2680
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, {
|
|
2681
|
+
type: "resize_execution_pty",
|
|
2682
|
+
request: { executionId, cols, rows },
|
|
2683
|
+
});
|
|
2684
|
+
if (response.type !== "execution_io") {
|
|
2685
|
+
throw new Error(`unexpected resizeExecutionPty response: ${response.type}`);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
async _readExecutionOutput(executionId, options = {}) {
|
|
2689
|
+
const limit = options.limit === undefined
|
|
2690
|
+
? null
|
|
2691
|
+
: protocolUnsigned(options.limit, 0xffff_ffff, "limit");
|
|
2692
|
+
const response = await this._sidecarClient.sendVmRequest(this._sidecarSession, this._sidecarVm, {
|
|
2693
|
+
type: "read_execution_output",
|
|
2694
|
+
request: {
|
|
2695
|
+
executionId,
|
|
2696
|
+
cursor: options.cursor ?? null,
|
|
2697
|
+
limit,
|
|
2698
|
+
},
|
|
2699
|
+
});
|
|
2700
|
+
if (response.type !== "execution_output_page") {
|
|
2701
|
+
throw new Error(`unexpected readExecutionOutput response: ${response.type}`);
|
|
2702
|
+
}
|
|
2703
|
+
return {
|
|
2704
|
+
events: response.response.events.map(mapExecutionOutputEvent),
|
|
2705
|
+
nextCursor: response.response.nextCursor,
|
|
2706
|
+
hasMore: response.response.hasMore,
|
|
2707
|
+
truncated: response.response.truncated,
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
_onExecutionOutput(executionId, handler) {
|
|
2711
|
+
const handlers = this._executionOutputHandlers.get(executionId) ?? new Set();
|
|
2712
|
+
handlers.add(handler);
|
|
2713
|
+
this._executionOutputHandlers.set(executionId, handlers);
|
|
2714
|
+
return () => {
|
|
2715
|
+
handlers.delete(handler);
|
|
2716
|
+
if (handlers.size === 0)
|
|
2717
|
+
this._executionOutputHandlers.delete(executionId);
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
_onExecutionCompleted(executionId, handler) {
|
|
2721
|
+
const handlers = this._executionCompletedHandlers.get(executionId) ?? new Set();
|
|
2722
|
+
handlers.add(handler);
|
|
2723
|
+
this._executionCompletedHandlers.set(executionId, handlers);
|
|
2724
|
+
return () => {
|
|
2725
|
+
handlers.delete(handler);
|
|
2726
|
+
if (handlers.size === 0)
|
|
2727
|
+
this._executionCompletedHandlers.delete(executionId);
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
_trackProcess(proc, command, args, retainEvents, outputHandlers, exitHandlers) {
|
|
2731
|
+
const entry = {
|
|
2732
|
+
proc,
|
|
2733
|
+
command,
|
|
2734
|
+
args,
|
|
2735
|
+
startedAtMs: Date.now(),
|
|
2736
|
+
retainEvents,
|
|
2737
|
+
events: [],
|
|
2738
|
+
nextSequence: 0,
|
|
2739
|
+
signal: undefined,
|
|
2740
|
+
exit: undefined,
|
|
2741
|
+
outputHandlers,
|
|
2742
|
+
exitHandlers,
|
|
2743
|
+
};
|
|
2744
|
+
this._processes.set(proc.pid, entry);
|
|
2745
|
+
// NOTE: do NOT delete from `_processes` on exit — the public API contract
|
|
2746
|
+
// (getProcess/listProcesses/stopProcess, see process-management.test.ts)
|
|
2747
|
+
// requires exited processes to stay queryable (running:false, exitCode set).
|
|
2748
|
+
// `_processes` is a process table for this VM's lifetime; it is freed wholesale
|
|
2749
|
+
// in dispose(). (H5: the leak was that dispose() never cleared it.)
|
|
2750
|
+
void proc.wait().then((code) => {
|
|
2751
|
+
const exit = {
|
|
2752
|
+
pid: proc.pid,
|
|
2753
|
+
outcome: entry.signal ? "signalled" : "exited",
|
|
2754
|
+
exitCode: code,
|
|
2755
|
+
...(entry.signal ? { signal: entry.signal } : {}),
|
|
2756
|
+
};
|
|
2757
|
+
entry.exit = exit;
|
|
2758
|
+
for (const h of exitHandlers)
|
|
2759
|
+
h(exit);
|
|
2760
|
+
});
|
|
2761
|
+
return {
|
|
2762
|
+
pid: proc.pid,
|
|
2763
|
+
state: "running",
|
|
2764
|
+
command,
|
|
2765
|
+
startedAtMs: entry.startedAtMs,
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
async _spawnProcess(command, args = [], options = {}) {
|
|
2769
|
+
const outputHandlers = new Set();
|
|
2770
|
+
const exitHandlers = new Set();
|
|
2771
|
+
const recordOutput = (channel, data) => {
|
|
2772
|
+
const entry = this._processes.get(proc.pid);
|
|
2773
|
+
if (!entry?.retainEvents)
|
|
2774
|
+
return;
|
|
2775
|
+
if (entry.events.length >= PROCESS_OUTPUT_EVENT_LIMIT) {
|
|
2776
|
+
entry.events.shift();
|
|
2777
|
+
}
|
|
2778
|
+
entry.events.push({
|
|
2779
|
+
pid: proc.pid,
|
|
2780
|
+
sequence: entry.nextSequence++,
|
|
2781
|
+
channel,
|
|
2782
|
+
chunk: data,
|
|
2783
|
+
timestampMs: Date.now(),
|
|
2784
|
+
});
|
|
2785
|
+
};
|
|
2786
|
+
const proc = this.#kernel.spawn(command, args, {
|
|
2787
|
+
cwd: options.cwd,
|
|
2788
|
+
env: options.env,
|
|
2789
|
+
stdin: options.stdin,
|
|
2790
|
+
timeout: options.timeoutMs,
|
|
2791
|
+
streamStdin: true,
|
|
2792
|
+
onStdout: (data) => {
|
|
2793
|
+
recordOutput("stdout", data);
|
|
2794
|
+
options?.onStdout?.(data);
|
|
2795
|
+
for (const h of outputHandlers) {
|
|
2796
|
+
h({ pid: proc.pid, stream: "stdout", data });
|
|
2797
|
+
}
|
|
2798
|
+
},
|
|
2799
|
+
onStderr: (data) => {
|
|
2800
|
+
recordOutput("stderr", data);
|
|
2801
|
+
options?.onStderr?.(data);
|
|
2802
|
+
for (const h of outputHandlers) {
|
|
2803
|
+
h({ pid: proc.pid, stream: "stderr", data });
|
|
2804
|
+
}
|
|
2805
|
+
},
|
|
2806
|
+
});
|
|
2807
|
+
return this._trackProcess(proc, command, args, options.output?.retainEvents ?? false, outputHandlers, exitHandlers);
|
|
2808
|
+
}
|
|
2809
|
+
spawn(command, args = [], options) {
|
|
2810
|
+
return this.process.spawn(command, args, options);
|
|
1753
2811
|
}
|
|
1754
|
-
/**
|
|
1755
|
-
|
|
2812
|
+
/** Write data to a process's stdin. */
|
|
2813
|
+
_writeProcessStdin(pid, data) {
|
|
2814
|
+
const language = this._languageProcesses.get(pid);
|
|
2815
|
+
if (language) {
|
|
2816
|
+
return this._writeExecutionStdin(language.executionId, data);
|
|
2817
|
+
}
|
|
1756
2818
|
const entry = this._processes.get(pid);
|
|
1757
2819
|
if (!entry)
|
|
1758
2820
|
throw new Error(`Process not found: ${pid}`);
|
|
1759
|
-
entry.
|
|
1760
|
-
return () => {
|
|
1761
|
-
entry.stdoutHandlers.delete(handler);
|
|
1762
|
-
};
|
|
2821
|
+
return entry.proc.writeStdin(data);
|
|
1763
2822
|
}
|
|
1764
|
-
/**
|
|
1765
|
-
|
|
2823
|
+
/** Close a process's stdin stream. */
|
|
2824
|
+
_closeProcessStdin(pid) {
|
|
2825
|
+
const language = this._languageProcesses.get(pid);
|
|
2826
|
+
if (language) {
|
|
2827
|
+
return this._closeExecutionStdin(language.executionId);
|
|
2828
|
+
}
|
|
1766
2829
|
const entry = this._processes.get(pid);
|
|
1767
2830
|
if (!entry)
|
|
1768
2831
|
throw new Error(`Process not found: ${pid}`);
|
|
1769
|
-
entry.
|
|
2832
|
+
return entry.proc.closeStdin();
|
|
2833
|
+
}
|
|
2834
|
+
/** Subscribe to stdout and stderr from a process. */
|
|
2835
|
+
onProcessOutput(pid, handler) {
|
|
2836
|
+
const entry = this._processes.get(pid) ?? this._languageProcesses.get(pid);
|
|
2837
|
+
if (!entry)
|
|
2838
|
+
throw new Error(`Process not found: ${pid}`);
|
|
2839
|
+
entry.outputHandlers.add(handler);
|
|
1770
2840
|
return () => {
|
|
1771
|
-
entry.
|
|
2841
|
+
entry.outputHandlers.delete(handler);
|
|
1772
2842
|
};
|
|
1773
2843
|
}
|
|
1774
2844
|
/** Subscribe to process exit. Returns an unsubscribe function. */
|
|
1775
2845
|
onProcessExit(pid, handler) {
|
|
1776
|
-
const entry = this._processes.get(pid);
|
|
2846
|
+
const entry = this._processes.get(pid) ?? this._languageProcesses.get(pid);
|
|
1777
2847
|
if (!entry)
|
|
1778
2848
|
throw new Error(`Process not found: ${pid}`);
|
|
1779
2849
|
// If already exited, call immediately.
|
|
1780
|
-
if (entry
|
|
1781
|
-
handler(entry.
|
|
2850
|
+
if ("exit" in entry && entry.exit) {
|
|
2851
|
+
handler(entry.exit);
|
|
1782
2852
|
return () => { };
|
|
1783
2853
|
}
|
|
1784
2854
|
entry.exitHandlers.add(handler);
|
|
@@ -1787,11 +2857,39 @@ export class AgentOs {
|
|
|
1787
2857
|
};
|
|
1788
2858
|
}
|
|
1789
2859
|
/** Wait for a process to exit. Returns the exit code. */
|
|
1790
|
-
|
|
2860
|
+
async _waitProcess(pid) {
|
|
2861
|
+
const language = this._languageProcesses.get(pid);
|
|
2862
|
+
if (language) {
|
|
2863
|
+
if (language.exit)
|
|
2864
|
+
return language.exit;
|
|
2865
|
+
const result = await this._waitExecutionResult(language.executionId);
|
|
2866
|
+
const exit = {
|
|
2867
|
+
pid,
|
|
2868
|
+
outcome: result.outcome === "timed_out"
|
|
2869
|
+
? "timed_out"
|
|
2870
|
+
: result.outcome === "cancelled"
|
|
2871
|
+
? "signalled"
|
|
2872
|
+
: "exited",
|
|
2873
|
+
...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
|
|
2874
|
+
...(language.signal ? { signal: language.signal } : {}),
|
|
2875
|
+
};
|
|
2876
|
+
language.exit = exit;
|
|
2877
|
+
language.descriptor = { ...language.descriptor, state: "exited" };
|
|
2878
|
+
for (const handler of language.exitHandlers)
|
|
2879
|
+
handler(exit);
|
|
2880
|
+
return exit;
|
|
2881
|
+
}
|
|
1791
2882
|
const entry = this._processes.get(pid);
|
|
1792
2883
|
if (!entry)
|
|
1793
2884
|
throw new Error(`Process not found: ${pid}`);
|
|
1794
|
-
|
|
2885
|
+
if (entry.exit)
|
|
2886
|
+
return entry.exit;
|
|
2887
|
+
const exitCode = await entry.proc.wait();
|
|
2888
|
+
return (entry.exit ?? {
|
|
2889
|
+
pid,
|
|
2890
|
+
outcome: "exited",
|
|
2891
|
+
exitCode,
|
|
2892
|
+
});
|
|
1795
2893
|
}
|
|
1796
2894
|
_assertSafeAbsolutePath(path) {
|
|
1797
2895
|
if (!path.startsWith("/")) {
|
|
@@ -1810,15 +2908,15 @@ export class AgentOs {
|
|
|
1810
2908
|
_vfs() {
|
|
1811
2909
|
return this.#kernel.vfs;
|
|
1812
2910
|
}
|
|
1813
|
-
async
|
|
2911
|
+
async _readFile(path) {
|
|
1814
2912
|
this._assertSafeAbsolutePath(path);
|
|
1815
2913
|
return this.#kernel.readFile(path);
|
|
1816
2914
|
}
|
|
1817
|
-
async
|
|
2915
|
+
async _writeFile(path, content) {
|
|
1818
2916
|
this._assertWritableAbsolutePath(path);
|
|
1819
2917
|
return this.#kernel.writeFile(path, content);
|
|
1820
2918
|
}
|
|
1821
|
-
async
|
|
2919
|
+
async _writeFiles(entries) {
|
|
1822
2920
|
const results = [];
|
|
1823
2921
|
for (const entry of entries) {
|
|
1824
2922
|
try {
|
|
@@ -1841,7 +2939,7 @@ export class AgentOs {
|
|
|
1841
2939
|
}
|
|
1842
2940
|
return results;
|
|
1843
2941
|
}
|
|
1844
|
-
async
|
|
2942
|
+
async _readFiles(paths) {
|
|
1845
2943
|
const results = [];
|
|
1846
2944
|
for (const path of paths) {
|
|
1847
2945
|
try {
|
|
@@ -1870,18 +2968,27 @@ export class AgentOs {
|
|
|
1870
2968
|
// exists() loop makes `mkdir -p` cost O(components * tree).
|
|
1871
2969
|
await this.#kernel.mkdir(path);
|
|
1872
2970
|
}
|
|
1873
|
-
async
|
|
2971
|
+
async _mkdir(path, options) {
|
|
1874
2972
|
if (options?.recursive) {
|
|
1875
2973
|
return this._mkdirp(path);
|
|
1876
2974
|
}
|
|
1877
2975
|
this._assertSafeAbsolutePath(path);
|
|
1878
2976
|
return this.#kernel.mkdir(path);
|
|
1879
2977
|
}
|
|
1880
|
-
async
|
|
2978
|
+
async _readdir(path) {
|
|
1881
2979
|
this._assertSafeAbsolutePath(path);
|
|
1882
2980
|
return this.#kernel.readdir(path);
|
|
1883
2981
|
}
|
|
1884
|
-
async
|
|
2982
|
+
async _readdirEntries(path) {
|
|
2983
|
+
this._assertSafeAbsolutePath(path);
|
|
2984
|
+
const entries = await this.#kernel.readdirRecursive(path, { maxDepth: 0 });
|
|
2985
|
+
return entries.map((entry) => ({
|
|
2986
|
+
name: entry.name,
|
|
2987
|
+
isDirectory: entry.isDirectory,
|
|
2988
|
+
isSymbolicLink: entry.isSymbolicLink,
|
|
2989
|
+
}));
|
|
2990
|
+
}
|
|
2991
|
+
async _readdirRecursive(path, options) {
|
|
1885
2992
|
this._assertSafeAbsolutePath(path);
|
|
1886
2993
|
const exclude = options?.exclude ? new Set(options.exclude) : undefined;
|
|
1887
2994
|
const entries = await this.#kernel.readdirRecursive(path, {
|
|
@@ -1911,20 +3018,27 @@ export class AgentOs {
|
|
|
1911
3018
|
}
|
|
1912
3019
|
return results;
|
|
1913
3020
|
}
|
|
1914
|
-
async
|
|
3021
|
+
async _stat(path) {
|
|
1915
3022
|
this._assertSafeAbsolutePath(path);
|
|
1916
3023
|
return this.#kernel.stat(path);
|
|
1917
3024
|
}
|
|
1918
|
-
async
|
|
3025
|
+
async _exists(path) {
|
|
1919
3026
|
this._assertSafeAbsolutePath(path);
|
|
1920
3027
|
return this.#kernel.exists(path);
|
|
1921
3028
|
}
|
|
1922
|
-
async
|
|
3029
|
+
async _exportRootFilesystem(options) {
|
|
3030
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0) {
|
|
3031
|
+
throw new RangeError("maxBytes must be a positive safe integer");
|
|
3032
|
+
}
|
|
1923
3033
|
const nativeSnapshot = this._sidecarLease?.admin.snapshotRootFilesystem;
|
|
1924
|
-
|
|
1925
|
-
|
|
3034
|
+
const snapshot = nativeSnapshot
|
|
3035
|
+
? await nativeSnapshot(options.maxBytes)
|
|
3036
|
+
: createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
|
|
3037
|
+
const size = Buffer.byteLength(JSON.stringify(snapshot));
|
|
3038
|
+
if (size > options.maxBytes) {
|
|
3039
|
+
throw new RangeError(`root filesystem export is ${size} bytes, limit is ${options.maxBytes}; raise maxBytes to export this filesystem`);
|
|
1926
3040
|
}
|
|
1927
|
-
return
|
|
3041
|
+
return snapshot;
|
|
1928
3042
|
}
|
|
1929
3043
|
/**
|
|
1930
3044
|
* Mount a filesystem into the running VM. Resolves once the mount has been
|
|
@@ -1932,25 +3046,144 @@ export class AgentOs {
|
|
|
1932
3046
|
* after the returned promise settles; a delivery failure rejects instead of
|
|
1933
3047
|
* leaving the mount silently host-only.
|
|
1934
3048
|
*/
|
|
1935
|
-
async
|
|
1936
|
-
this._assertSafeAbsolutePath(path);
|
|
1937
|
-
|
|
3049
|
+
async _mountFs(descriptor) {
|
|
3050
|
+
this._assertSafeAbsolutePath(descriptor.path);
|
|
3051
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
3052
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
3053
|
+
}
|
|
3054
|
+
await this.#kernel.mountDescriptor({
|
|
3055
|
+
guestPath: descriptor.path,
|
|
3056
|
+
readOnly: descriptor.readOnly ?? false,
|
|
3057
|
+
plugin: {
|
|
3058
|
+
id: descriptor.plugin.id,
|
|
3059
|
+
config: descriptor.plugin.config ?? {},
|
|
3060
|
+
},
|
|
3061
|
+
});
|
|
1938
3062
|
}
|
|
1939
|
-
async
|
|
3063
|
+
async _unmountFs(path) {
|
|
1940
3064
|
this._assertSafeAbsolutePath(path);
|
|
1941
|
-
|
|
3065
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
3066
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
3067
|
+
}
|
|
3068
|
+
await this.#kernel.unmountDescriptor(path);
|
|
1942
3069
|
}
|
|
1943
|
-
async
|
|
3070
|
+
async _listMounts() {
|
|
3071
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy))
|
|
3072
|
+
return [];
|
|
3073
|
+
return this.#kernel.listMounts();
|
|
3074
|
+
}
|
|
3075
|
+
async _move(from, to) {
|
|
1944
3076
|
this._assertWritableAbsolutePath(from);
|
|
1945
3077
|
this._assertWritableAbsolutePath(to);
|
|
1946
3078
|
await this.#kernel.movePath(from, to);
|
|
1947
3079
|
}
|
|
1948
|
-
async
|
|
3080
|
+
async _remove(path, options) {
|
|
1949
3081
|
this._assertWritableAbsolutePath(path);
|
|
1950
3082
|
await this.#kernel.removePath(path, {
|
|
1951
3083
|
recursive: options?.recursive ?? false,
|
|
1952
3084
|
});
|
|
1953
3085
|
}
|
|
3086
|
+
/** @deprecated Use `filesystem.readFile()`. */
|
|
3087
|
+
readFile(path) {
|
|
3088
|
+
return this.filesystem.readFile(path);
|
|
3089
|
+
}
|
|
3090
|
+
/** @deprecated Use `filesystem.writeFile()`. */
|
|
3091
|
+
writeFile(path, content) {
|
|
3092
|
+
return this.filesystem.writeFile(path, content);
|
|
3093
|
+
}
|
|
3094
|
+
/** @deprecated Use `filesystem.writeFiles()`. */
|
|
3095
|
+
writeFiles(entries) {
|
|
3096
|
+
return this.filesystem.writeFiles(entries);
|
|
3097
|
+
}
|
|
3098
|
+
/** @deprecated Use `filesystem.readFiles()`. */
|
|
3099
|
+
readFiles(paths) {
|
|
3100
|
+
return this.filesystem.readFiles(paths);
|
|
3101
|
+
}
|
|
3102
|
+
/** @deprecated Use `filesystem.mkdir()`. */
|
|
3103
|
+
mkdir(path, options) {
|
|
3104
|
+
return this.filesystem.mkdir(path, options);
|
|
3105
|
+
}
|
|
3106
|
+
/** @deprecated Use `filesystem.readdir()`. */
|
|
3107
|
+
readdir(path) {
|
|
3108
|
+
return this.filesystem.readdir(path);
|
|
3109
|
+
}
|
|
3110
|
+
/** @deprecated Use `filesystem.readdirEntries()`. */
|
|
3111
|
+
readdirEntries(path) {
|
|
3112
|
+
return this.filesystem.readdirEntries(path);
|
|
3113
|
+
}
|
|
3114
|
+
/** @deprecated Use `filesystem.readdirRecursive()`. */
|
|
3115
|
+
readdirRecursive(path, options) {
|
|
3116
|
+
return this.filesystem.readdirRecursive(path, options);
|
|
3117
|
+
}
|
|
3118
|
+
/** @deprecated Use `filesystem.stat()`. */
|
|
3119
|
+
stat(path) {
|
|
3120
|
+
return this.filesystem.stat(path);
|
|
3121
|
+
}
|
|
3122
|
+
/** @deprecated Use `filesystem.exists()`. */
|
|
3123
|
+
exists(path) {
|
|
3124
|
+
return this.filesystem.exists(path);
|
|
3125
|
+
}
|
|
3126
|
+
/** @deprecated Use `filesystem.export()`. */
|
|
3127
|
+
exportRootFilesystem(options) {
|
|
3128
|
+
return this.filesystem.export(options);
|
|
3129
|
+
}
|
|
3130
|
+
/** @deprecated Use `filesystem.mount()`. */
|
|
3131
|
+
mountFs(descriptor) {
|
|
3132
|
+
return this.filesystem.mount(descriptor);
|
|
3133
|
+
}
|
|
3134
|
+
/** @deprecated Use `filesystem.unmount()`. */
|
|
3135
|
+
unmountFs(path) {
|
|
3136
|
+
return this.filesystem.unmount(path);
|
|
3137
|
+
}
|
|
3138
|
+
/** @deprecated Use `filesystem.listMounts()`. */
|
|
3139
|
+
listMounts() {
|
|
3140
|
+
return this.filesystem.listMounts();
|
|
3141
|
+
}
|
|
3142
|
+
/** @deprecated Use `filesystem.move()`. */
|
|
3143
|
+
move(from, to) {
|
|
3144
|
+
return this.filesystem.move(from, to);
|
|
3145
|
+
}
|
|
3146
|
+
/** @deprecated Use `filesystem.remove()`. */
|
|
3147
|
+
remove(path, options) {
|
|
3148
|
+
return this.filesystem.remove(path, options);
|
|
3149
|
+
}
|
|
3150
|
+
async _httpRequest(request) {
|
|
3151
|
+
this._assertSafeAbsolutePath(request.path.split("?")[0] || "/");
|
|
3152
|
+
const method = request.method?.toUpperCase() ?? "GET";
|
|
3153
|
+
const body = typeof request.body === "string"
|
|
3154
|
+
? request.body
|
|
3155
|
+
: request.body === undefined
|
|
3156
|
+
? undefined
|
|
3157
|
+
: new TextDecoder().decode(request.body);
|
|
3158
|
+
const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
3159
|
+
port: request.port,
|
|
3160
|
+
method,
|
|
3161
|
+
path: request.path,
|
|
3162
|
+
headersJson: JSON.stringify(request.headers ?? {}),
|
|
3163
|
+
...(method !== "GET" && method !== "HEAD" && body !== undefined
|
|
3164
|
+
? { body }
|
|
3165
|
+
: {}),
|
|
3166
|
+
}));
|
|
3167
|
+
const headers = {};
|
|
3168
|
+
for (const [key, value] of responsePayload.headers ?? []) {
|
|
3169
|
+
headers[key] = value;
|
|
3170
|
+
}
|
|
3171
|
+
return {
|
|
3172
|
+
status: responsePayload.status,
|
|
3173
|
+
statusText: responsePayload.statusText ?? "",
|
|
3174
|
+
headers,
|
|
3175
|
+
body: new Uint8Array(Buffer.from(responsePayload.body ?? "", "base64")),
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
/** @deprecated Use `network.httpRequest()`. */
|
|
3179
|
+
httpRequest(request) {
|
|
3180
|
+
return this.network.httpRequest(request);
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Fetch an HTTP endpoint inside the VM while preserving duplicate response
|
|
3184
|
+
* headers and arbitrary binary bodies. agentOS Apps uses this compatibility
|
|
3185
|
+
* surface for its direct actor API.
|
|
3186
|
+
*/
|
|
1954
3187
|
async fetch(port, request) {
|
|
1955
3188
|
const url = new URL(request.url);
|
|
1956
3189
|
const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
@@ -1959,7 +3192,9 @@ export class AgentOs {
|
|
|
1959
3192
|
path: `${url.pathname}${url.search}`,
|
|
1960
3193
|
headersJson: JSON.stringify(Object.fromEntries(request.headers.entries())),
|
|
1961
3194
|
...(request.method !== "GET" && request.method !== "HEAD"
|
|
1962
|
-
? {
|
|
3195
|
+
? {
|
|
3196
|
+
bodyBase64: Buffer.from(await request.arrayBuffer()).toString("base64"),
|
|
3197
|
+
}
|
|
1963
3198
|
: {}),
|
|
1964
3199
|
}));
|
|
1965
3200
|
const headers = new Headers();
|
|
@@ -1968,22 +3203,72 @@ export class AgentOs {
|
|
|
1968
3203
|
}
|
|
1969
3204
|
return new Response(Buffer.from(responsePayload.body ?? "", "base64"), {
|
|
1970
3205
|
status: responsePayload.status,
|
|
1971
|
-
statusText: responsePayload.statusText,
|
|
3206
|
+
statusText: responsePayload.statusText ?? "",
|
|
1972
3207
|
headers,
|
|
1973
3208
|
});
|
|
1974
3209
|
}
|
|
1975
|
-
|
|
3210
|
+
async fetchStreamStart(port, request) {
|
|
3211
|
+
const url = new URL(request.url);
|
|
3212
|
+
return JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
3213
|
+
port,
|
|
3214
|
+
method: request.method,
|
|
3215
|
+
path: `${url.pathname}${url.search}`,
|
|
3216
|
+
headersJson: JSON.stringify(Object.fromEntries(request.headers.entries())),
|
|
3217
|
+
...(request.method !== "GET" && request.method !== "HEAD"
|
|
3218
|
+
? {
|
|
3219
|
+
bodyBase64: Buffer.from(await request.arrayBuffer()).toString("base64"),
|
|
3220
|
+
}
|
|
3221
|
+
: {}),
|
|
3222
|
+
streamOperation: "start",
|
|
3223
|
+
}));
|
|
3224
|
+
}
|
|
3225
|
+
async fetchStreamRead(streamId, maxBytes = 64 * 1024) {
|
|
3226
|
+
const response = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
3227
|
+
port: 0,
|
|
3228
|
+
method: "GET",
|
|
3229
|
+
path: "/",
|
|
3230
|
+
headersJson: "{}",
|
|
3231
|
+
streamOperation: "read",
|
|
3232
|
+
streamId,
|
|
3233
|
+
maxBytes,
|
|
3234
|
+
}));
|
|
3235
|
+
return {
|
|
3236
|
+
body: new Uint8Array(Buffer.from(response.body, "base64")),
|
|
3237
|
+
done: response.done,
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
async fetchStreamCancel(streamId) {
|
|
3241
|
+
await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
3242
|
+
port: 0,
|
|
3243
|
+
method: "GET",
|
|
3244
|
+
path: "/",
|
|
3245
|
+
headersJson: "{}",
|
|
3246
|
+
streamOperation: "cancel",
|
|
3247
|
+
streamId,
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
_openTerminal(options) {
|
|
1976
3251
|
const shellId = `shell-${++this._shellCounter}`;
|
|
1977
3252
|
this._closedShellIds.delete(shellId);
|
|
1978
3253
|
const dataHandlers = new Set();
|
|
1979
|
-
const
|
|
3254
|
+
const stderrHandlers = new Set();
|
|
3255
|
+
const exitHandlers = new Set();
|
|
3256
|
+
const handle = this.#kernel.openShell({
|
|
3257
|
+
...options,
|
|
3258
|
+
onStderr: (data) => {
|
|
3259
|
+
for (const handler of stderrHandlers)
|
|
3260
|
+
handler({ shellId, data });
|
|
3261
|
+
},
|
|
3262
|
+
});
|
|
1980
3263
|
handle.onData = (data) => {
|
|
1981
|
-
for (const
|
|
1982
|
-
|
|
3264
|
+
for (const handler of dataHandlers)
|
|
3265
|
+
handler({ shellId, data });
|
|
1983
3266
|
};
|
|
1984
3267
|
const entry = {
|
|
1985
3268
|
handle,
|
|
1986
3269
|
dataHandlers,
|
|
3270
|
+
stderrHandlers,
|
|
3271
|
+
exitHandlers,
|
|
1987
3272
|
exitPromise: Promise.resolve(0),
|
|
1988
3273
|
};
|
|
1989
3274
|
const exitPromise = handle.wait();
|
|
@@ -1998,6 +3283,8 @@ export class AgentOs {
|
|
|
1998
3283
|
};
|
|
1999
3284
|
entry.exitPromise = exitPromise.then((exitCode) => {
|
|
2000
3285
|
finalize(exitCode);
|
|
3286
|
+
for (const handler of exitHandlers)
|
|
3287
|
+
handler({ shellId, exitCode });
|
|
2001
3288
|
return exitCode;
|
|
2002
3289
|
}, (error) => {
|
|
2003
3290
|
finalize();
|
|
@@ -2011,13 +3298,17 @@ export class AgentOs {
|
|
|
2011
3298
|
return this.#kernel.connectTerminal(options);
|
|
2012
3299
|
}
|
|
2013
3300
|
/** Write data to a shell's PTY input. */
|
|
2014
|
-
|
|
3301
|
+
_writeTerminal(shellId, data) {
|
|
2015
3302
|
const entry = this._shells.get(shellId);
|
|
2016
3303
|
if (!entry)
|
|
2017
3304
|
throw new Error(`Shell not found: ${shellId}`);
|
|
2018
3305
|
return entry.handle.write(data);
|
|
2019
3306
|
}
|
|
2020
|
-
/**
|
|
3307
|
+
/**
|
|
3308
|
+
* Subscribe to ordered PTY output (stdout and stderr). Returns an unsubscribe
|
|
3309
|
+
* function. `OpenShellOptions.onStderr` is a diagnostic tap for callers that
|
|
3310
|
+
* need channel identity; do not render both surfaces.
|
|
3311
|
+
*/
|
|
2021
3312
|
onShellData(shellId, handler) {
|
|
2022
3313
|
const entry = this._shells.get(shellId);
|
|
2023
3314
|
if (!entry)
|
|
@@ -2027,8 +3318,30 @@ export class AgentOs {
|
|
|
2027
3318
|
entry.dataHandlers.delete(handler);
|
|
2028
3319
|
};
|
|
2029
3320
|
}
|
|
3321
|
+
/** Subscribe to the stderr-only diagnostic stream for a shell. */
|
|
3322
|
+
onShellStderr(shellId, handler) {
|
|
3323
|
+
const entry = this._shells.get(shellId);
|
|
3324
|
+
if (!entry)
|
|
3325
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
3326
|
+
entry.stderrHandlers.add(handler);
|
|
3327
|
+
return () => entry.stderrHandlers.delete(handler);
|
|
3328
|
+
}
|
|
3329
|
+
/** Subscribe to shell exit. */
|
|
3330
|
+
onShellExit(shellId, handler) {
|
|
3331
|
+
const entry = this._shells.get(shellId);
|
|
3332
|
+
if (!entry) {
|
|
3333
|
+
const exitCode = this._closedShellIds.get(shellId);
|
|
3334
|
+
if (exitCode !== undefined) {
|
|
3335
|
+
handler({ shellId, exitCode });
|
|
3336
|
+
return () => { };
|
|
3337
|
+
}
|
|
3338
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
3339
|
+
}
|
|
3340
|
+
entry.exitHandlers.add(handler);
|
|
3341
|
+
return () => entry.exitHandlers.delete(handler);
|
|
3342
|
+
}
|
|
2030
3343
|
/** Notify a shell of terminal resize. */
|
|
2031
|
-
|
|
3344
|
+
_resizeTerminal(shellId, cols, rows) {
|
|
2032
3345
|
const entry = this._shells.get(shellId);
|
|
2033
3346
|
if (!entry)
|
|
2034
3347
|
throw new Error(`Shell not found: ${shellId}`);
|
|
@@ -2039,7 +3352,7 @@ export class AgentOs {
|
|
|
2039
3352
|
* immediately for a shell that has already exited (within the closed-shell
|
|
2040
3353
|
* retention window).
|
|
2041
3354
|
*/
|
|
2042
|
-
|
|
3355
|
+
_waitTerminal(shellId) {
|
|
2043
3356
|
const entry = this._shells.get(shellId);
|
|
2044
3357
|
if (!entry) {
|
|
2045
3358
|
const exitCode = this._closedShellIds.get(shellId);
|
|
@@ -2050,7 +3363,7 @@ export class AgentOs {
|
|
|
2050
3363
|
return entry.exitPromise;
|
|
2051
3364
|
}
|
|
2052
3365
|
/** Kill a shell process and remove it from tracking. */
|
|
2053
|
-
|
|
3366
|
+
_closeTerminal(shellId) {
|
|
2054
3367
|
const entry = this._shells.get(shellId);
|
|
2055
3368
|
if (!entry) {
|
|
2056
3369
|
if (this._closedShellIds.has(shellId)) {
|
|
@@ -2062,6 +3375,26 @@ export class AgentOs {
|
|
|
2062
3375
|
this._shells.delete(shellId);
|
|
2063
3376
|
this._closedShellIds.add(shellId);
|
|
2064
3377
|
}
|
|
3378
|
+
/** @deprecated Use `terminal.open()`. */
|
|
3379
|
+
openShell(options) {
|
|
3380
|
+
return this.terminal.open(options);
|
|
3381
|
+
}
|
|
3382
|
+
/** @deprecated Use `terminal.write()`. */
|
|
3383
|
+
writeShell(shellId, data) {
|
|
3384
|
+
return this.terminal.write(shellId, data);
|
|
3385
|
+
}
|
|
3386
|
+
/** @deprecated Use `terminal.resize()`. */
|
|
3387
|
+
resizeShell(shellId, cols, rows) {
|
|
3388
|
+
this.terminal.resize(shellId, cols, rows);
|
|
3389
|
+
}
|
|
3390
|
+
/** @deprecated Use `terminal.wait()`. */
|
|
3391
|
+
waitShell(shellId) {
|
|
3392
|
+
return this.terminal.wait(shellId);
|
|
3393
|
+
}
|
|
3394
|
+
/** @deprecated Use `terminal.close()`. */
|
|
3395
|
+
closeShell(shellId) {
|
|
3396
|
+
this.terminal.close(shellId);
|
|
3397
|
+
}
|
|
2065
3398
|
_resolveVmPathToHostPath(vmPath) {
|
|
2066
3399
|
const normalizedVmPath = posixPath.normalize(vmPath);
|
|
2067
3400
|
for (const mount of this._hostMounts) {
|
|
@@ -2077,34 +3410,51 @@ export class AgentOs {
|
|
|
2077
3410
|
return null;
|
|
2078
3411
|
}
|
|
2079
3412
|
/** Returns info about all processes spawned via spawn(). */
|
|
2080
|
-
|
|
2081
|
-
return [
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
3413
|
+
async _listProcesses() {
|
|
3414
|
+
return [
|
|
3415
|
+
...[...this._processes.values()].map(({ proc, command, startedAtMs }) => ({
|
|
3416
|
+
pid: proc.pid,
|
|
3417
|
+
command,
|
|
3418
|
+
state: proc.exitCode === null ? "running" : "exited",
|
|
3419
|
+
startedAtMs,
|
|
3420
|
+
})),
|
|
3421
|
+
...[...this._languageProcesses.values()].map((entry) => entry.descriptor),
|
|
3422
|
+
];
|
|
2088
3423
|
}
|
|
2089
3424
|
/** Returns all kernel processes across all active runtimes (WASM and Node). */
|
|
2090
|
-
|
|
3425
|
+
_listAllProcesses() {
|
|
2091
3426
|
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
2092
3427
|
return this.#kernel.snapshotProcesses();
|
|
2093
3428
|
}
|
|
2094
3429
|
return [...this.#kernel.processes.values()];
|
|
2095
3430
|
}
|
|
2096
3431
|
/** Returns processes organized as a tree using ppid relationships. */
|
|
2097
|
-
|
|
2098
|
-
const all = this.
|
|
3432
|
+
async _processTree() {
|
|
3433
|
+
const all = this._listAllProcesses();
|
|
2099
3434
|
const nodeMap = new Map();
|
|
2100
3435
|
// Index: create a tree node for each process
|
|
2101
3436
|
for (const proc of all) {
|
|
2102
|
-
nodeMap.set(proc.pid, {
|
|
3437
|
+
nodeMap.set(proc.pid, {
|
|
3438
|
+
pid: proc.pid,
|
|
3439
|
+
ppid: proc.ppid,
|
|
3440
|
+
command: proc.command,
|
|
3441
|
+
state: proc.status,
|
|
3442
|
+
startedAtMs: proc.startTime,
|
|
3443
|
+
children: [],
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
for (const process of this._languageProcesses.values()) {
|
|
3447
|
+
if (!nodeMap.has(process.descriptor.pid)) {
|
|
3448
|
+
nodeMap.set(process.descriptor.pid, {
|
|
3449
|
+
...process.descriptor,
|
|
3450
|
+
children: [],
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
2103
3453
|
}
|
|
2104
3454
|
// Wire: attach each node to its parent
|
|
2105
3455
|
const roots = [];
|
|
2106
3456
|
for (const node of nodeMap.values()) {
|
|
2107
|
-
const parent = nodeMap.get(node.ppid);
|
|
3457
|
+
const parent = node.ppid === undefined ? undefined : nodeMap.get(node.ppid);
|
|
2108
3458
|
if (parent) {
|
|
2109
3459
|
parent.children.push(node);
|
|
2110
3460
|
}
|
|
@@ -2115,7 +3465,10 @@ export class AgentOs {
|
|
|
2115
3465
|
return roots;
|
|
2116
3466
|
}
|
|
2117
3467
|
/** Returns info about a specific process by PID. Throws if not found. */
|
|
2118
|
-
|
|
3468
|
+
async _getProcess(pid) {
|
|
3469
|
+
const language = this._languageProcesses.get(pid);
|
|
3470
|
+
if (language)
|
|
3471
|
+
return language.descriptor;
|
|
2119
3472
|
const entry = this._processes.get(pid);
|
|
2120
3473
|
if (!entry) {
|
|
2121
3474
|
throw new Error(`Process not found: ${pid}`);
|
|
@@ -2123,64 +3476,337 @@ export class AgentOs {
|
|
|
2123
3476
|
return {
|
|
2124
3477
|
pid: entry.proc.pid,
|
|
2125
3478
|
command: entry.command,
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
exitCode: entry.proc.exitCode,
|
|
3479
|
+
state: entry.proc.exitCode === null ? "running" : "exited",
|
|
3480
|
+
startedAtMs: entry.startedAtMs,
|
|
2129
3481
|
};
|
|
2130
3482
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
3483
|
+
async _signalProcess(pid, signal) {
|
|
3484
|
+
const language = this._languageProcesses.get(pid);
|
|
3485
|
+
if (language) {
|
|
3486
|
+
language.signal = signal;
|
|
3487
|
+
await this._signalExecution(language.executionId, signal);
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
2133
3490
|
const entry = this._processes.get(pid);
|
|
2134
3491
|
if (!entry) {
|
|
2135
3492
|
throw new Error(`Process not found: ${pid}`);
|
|
2136
3493
|
}
|
|
2137
3494
|
if (entry.proc.exitCode !== null)
|
|
2138
3495
|
return;
|
|
2139
|
-
entry.
|
|
3496
|
+
entry.signal = signal;
|
|
3497
|
+
const number = signal === "SIGKILL" ? 9 : signal === "SIGINT" ? 2 : 15;
|
|
3498
|
+
entry.proc.kill(number);
|
|
2140
3499
|
}
|
|
2141
3500
|
/** Send SIGKILL to force-kill a process. No-op if already exited. */
|
|
2142
|
-
|
|
3501
|
+
async _killProcess(pid) {
|
|
3502
|
+
await this._signalProcess(pid, "SIGKILL");
|
|
3503
|
+
}
|
|
3504
|
+
async _resizeProcessPty(pid, size) {
|
|
3505
|
+
const language = this._languageProcesses.get(pid);
|
|
3506
|
+
if (!language) {
|
|
3507
|
+
throw new Error(`Process ${pid} does not have a managed PTY`);
|
|
3508
|
+
}
|
|
3509
|
+
await this._resizeExecutionPty(language.executionId, size);
|
|
3510
|
+
}
|
|
3511
|
+
async _readProcessOutput(pid, options = {}) {
|
|
3512
|
+
const language = this._languageProcesses.get(pid);
|
|
3513
|
+
if (language) {
|
|
3514
|
+
const page = await this._readExecutionOutput(language.executionId, {
|
|
3515
|
+
cursor: options.after === undefined ? undefined : `1:${options.after + 1}`,
|
|
3516
|
+
});
|
|
3517
|
+
return {
|
|
3518
|
+
pid,
|
|
3519
|
+
events: page.events.map((event) => ({
|
|
3520
|
+
pid,
|
|
3521
|
+
sequence: event.sequence,
|
|
3522
|
+
channel: event.channel,
|
|
3523
|
+
chunk: event.chunk,
|
|
3524
|
+
timestampMs: event.timestampMs,
|
|
3525
|
+
})),
|
|
3526
|
+
nextCursor: page.nextCursor,
|
|
3527
|
+
hasMore: page.hasMore,
|
|
3528
|
+
truncated: page.truncated,
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
2143
3531
|
const entry = this._processes.get(pid);
|
|
2144
|
-
if (!entry)
|
|
3532
|
+
if (!entry)
|
|
2145
3533
|
throw new Error(`Process not found: ${pid}`);
|
|
3534
|
+
if (!entry.retainEvents) {
|
|
3535
|
+
throw new Error(`Process ${pid} was not spawned with output.retainEvents enabled`);
|
|
2146
3536
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
3537
|
+
const after = options.after ?? -1;
|
|
3538
|
+
const events = entry.events.filter((event) => event.sequence > after);
|
|
3539
|
+
return {
|
|
3540
|
+
pid,
|
|
3541
|
+
events,
|
|
3542
|
+
nextCursor: String(events.at(-1)?.sequence ?? after),
|
|
3543
|
+
hasMore: false,
|
|
3544
|
+
truncated: entry.events.length === PROCESS_OUTPUT_EVENT_LIMIT &&
|
|
3545
|
+
after < (entry.events[0]?.sequence ?? 0) - 1,
|
|
3546
|
+
};
|
|
2150
3547
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
3548
|
+
async _openSession(input) {
|
|
3549
|
+
const response = await this._sendAcpRequest({
|
|
3550
|
+
tag: "AcpOpenSessionRequest",
|
|
3551
|
+
val: {
|
|
3552
|
+
sessionId: input.sessionId ?? null,
|
|
3553
|
+
agent: input.agent,
|
|
3554
|
+
cwd: input.cwd ?? null,
|
|
3555
|
+
additionalDirectories: input.additionalDirectories === undefined
|
|
3556
|
+
? null
|
|
3557
|
+
: JSON.stringify(input.additionalDirectories),
|
|
3558
|
+
env: input.env === undefined ? null : JSON.stringify(input.env),
|
|
3559
|
+
mcpServers: input.mcpServers === undefined
|
|
3560
|
+
? null
|
|
3561
|
+
: JSON.stringify(input.mcpServers),
|
|
3562
|
+
permissionPolicy: input.permissionPolicy ?? null,
|
|
3563
|
+
skipOsInstructions: input.skipOsInstructions ?? null,
|
|
3564
|
+
additionalInstructions: combineInstructions(input.additionalInstructions, this._bindingReference) ?? null,
|
|
3565
|
+
},
|
|
3566
|
+
});
|
|
3567
|
+
if (response.tag !== "AcpOpenSessionResponse") {
|
|
3568
|
+
throw new Error(`unexpected openSession response: ${response.tag}`);
|
|
3569
|
+
}
|
|
2157
3570
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
3571
|
+
async _getSession(input) {
|
|
3572
|
+
const response = await this._sendAcpRequest({
|
|
3573
|
+
tag: "AcpGetDurableSessionRequest",
|
|
3574
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
3575
|
+
});
|
|
3576
|
+
if (response.tag !== "AcpGetDurableSessionResponse") {
|
|
3577
|
+
throw new Error(`unexpected getSession response: ${response.tag}`);
|
|
3578
|
+
}
|
|
3579
|
+
return decodeDurableSessionInfo(response.val.session);
|
|
3580
|
+
}
|
|
3581
|
+
async _listSessions(input) {
|
|
3582
|
+
const response = await this._sendAcpRequest({
|
|
3583
|
+
tag: "AcpListDurableSessionsRequest",
|
|
3584
|
+
val: { cursor: input?.cursor ?? null, limit: input?.limit ?? null },
|
|
3585
|
+
});
|
|
3586
|
+
if (response.tag !== "AcpListDurableSessionsResponse") {
|
|
3587
|
+
throw new Error(`unexpected listSessions response: ${response.tag}`);
|
|
3588
|
+
}
|
|
3589
|
+
return {
|
|
3590
|
+
sessions: response.val.sessions.map(decodeDurableSessionInfo),
|
|
3591
|
+
nextCursor: response.val.nextCursor,
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
async _deleteSession(input = {}) {
|
|
3595
|
+
const response = await this._sendAcpRequest({
|
|
3596
|
+
tag: "AcpDeleteSessionRequest",
|
|
3597
|
+
val: { sessionId: input.sessionId ?? null },
|
|
3598
|
+
});
|
|
3599
|
+
if (response.tag !== "AcpDeleteSessionResponse") {
|
|
3600
|
+
throw new Error(`unexpected deleteSession response: ${response.tag}`);
|
|
2163
3601
|
}
|
|
2164
|
-
|
|
3602
|
+
}
|
|
3603
|
+
async _unloadSession(input) {
|
|
3604
|
+
const response = await this._sendAcpRequest({
|
|
3605
|
+
tag: "AcpUnloadSessionRequest",
|
|
3606
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
3607
|
+
});
|
|
3608
|
+
if (response.tag !== "AcpUnloadSessionResponse") {
|
|
3609
|
+
throw new Error(`unexpected unloadSession response: ${response.tag}`);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
async _prompt(input) {
|
|
3613
|
+
const response = await this._sendAcpRequest({
|
|
3614
|
+
tag: "AcpPromptRequest",
|
|
3615
|
+
val: {
|
|
3616
|
+
sessionId: input.sessionId ?? null,
|
|
3617
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
3618
|
+
content: JSON.stringify(input.content),
|
|
3619
|
+
},
|
|
3620
|
+
});
|
|
3621
|
+
if (response.tag !== "AcpPromptResponse") {
|
|
3622
|
+
throw new Error(`unexpected prompt response: ${response.tag}`);
|
|
3623
|
+
}
|
|
3624
|
+
return {
|
|
3625
|
+
sessionId: response.val.sessionId,
|
|
3626
|
+
message: response.val.message === null ? null : JSON.parse(response.val.message),
|
|
3627
|
+
stopReason: response.val.stopReason,
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
async _cancelPrompt(input) {
|
|
3631
|
+
const response = await this._sendAcpRequest({
|
|
3632
|
+
tag: "AcpCancelPromptRequest",
|
|
3633
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
3634
|
+
});
|
|
3635
|
+
if (response.tag !== "AcpCancelPromptResponse") {
|
|
3636
|
+
throw new Error(`unexpected cancelPrompt response: ${response.tag}`);
|
|
3637
|
+
}
|
|
3638
|
+
if (response.val.status !== "cancelled" &&
|
|
3639
|
+
response.val.status !== "no_active_prompt") {
|
|
3640
|
+
throw new Error(`invalid cancelPrompt status: ${response.val.status}`);
|
|
3641
|
+
}
|
|
3642
|
+
return { status: response.val.status };
|
|
3643
|
+
}
|
|
3644
|
+
async _respondPermission(input) {
|
|
3645
|
+
const response = await this._sendAcpRequest({
|
|
3646
|
+
tag: "AcpRespondPermissionRequest",
|
|
3647
|
+
val: {
|
|
3648
|
+
sessionId: input.sessionId,
|
|
3649
|
+
requestId: input.requestId,
|
|
3650
|
+
optionId: input.optionId,
|
|
3651
|
+
},
|
|
3652
|
+
});
|
|
3653
|
+
if (response.tag !== "AcpRespondPermissionResponse") {
|
|
3654
|
+
throw new Error(`unexpected respondPermission response: ${response.tag}`);
|
|
3655
|
+
}
|
|
3656
|
+
if (response.val.status !== "accepted" &&
|
|
3657
|
+
response.val.status !== "not_pending") {
|
|
3658
|
+
throw new Error(`invalid respondPermission status: ${response.val.status}`);
|
|
3659
|
+
}
|
|
3660
|
+
if (response.val.status === "accepted") {
|
|
3661
|
+
if (response.val.reason !== null) {
|
|
3662
|
+
throw new Error("accepted permission response must not include a reason");
|
|
3663
|
+
}
|
|
3664
|
+
return { status: "accepted" };
|
|
3665
|
+
}
|
|
3666
|
+
return {
|
|
3667
|
+
status: "not_pending",
|
|
3668
|
+
reason: permissionTerminalReason(response.val.reason),
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
async _readHistory(input) {
|
|
3672
|
+
const response = await this._sendAcpRequest({
|
|
3673
|
+
tag: "AcpReadHistoryRequest",
|
|
3674
|
+
val: {
|
|
3675
|
+
sessionId: input?.sessionId ?? null,
|
|
3676
|
+
before: input?.before === undefined ? null : BigInt(input.before),
|
|
3677
|
+
after: input?.after === undefined ? null : BigInt(input.after),
|
|
3678
|
+
limit: input?.limit ?? null,
|
|
3679
|
+
},
|
|
3680
|
+
});
|
|
3681
|
+
if (response.tag !== "AcpHistoryPageResponse") {
|
|
3682
|
+
throw new Error(`unexpected readHistory response: ${response.tag}`);
|
|
3683
|
+
}
|
|
3684
|
+
return {
|
|
3685
|
+
events: response.val.events.map(decodeDurableSessionEvent),
|
|
3686
|
+
hasMoreBefore: response.val.hasMoreBefore,
|
|
3687
|
+
hasMoreAfter: response.val.hasMoreAfter,
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
async _getSessionConfig(input) {
|
|
3691
|
+
const response = await this._sendAcpRequest({
|
|
3692
|
+
tag: "AcpGetSessionConfigRequest",
|
|
3693
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
3694
|
+
});
|
|
3695
|
+
if (response.tag !== "AcpSessionConfigResponse") {
|
|
3696
|
+
throw new Error(`unexpected getSessionConfig response: ${response.tag}`);
|
|
3697
|
+
}
|
|
3698
|
+
return {
|
|
3699
|
+
revision: safeWireU64(response.val.revision),
|
|
3700
|
+
options: JSON.parse(response.val.options),
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
async _setSessionConfigOption(input) {
|
|
3704
|
+
const response = await this._sendAcpRequest({
|
|
3705
|
+
tag: "AcpSetSessionConfigOptionRequest",
|
|
3706
|
+
val: {
|
|
3707
|
+
sessionId: input.sessionId ?? null,
|
|
3708
|
+
configId: input.configId,
|
|
3709
|
+
value: JSON.stringify(input.value),
|
|
3710
|
+
},
|
|
3711
|
+
});
|
|
3712
|
+
if (response.tag !== "AcpSessionConfigResponse") {
|
|
3713
|
+
throw new Error(`unexpected setSessionConfigOption response: ${response.tag}`);
|
|
3714
|
+
}
|
|
3715
|
+
return {
|
|
3716
|
+
revision: safeWireU64(response.val.revision),
|
|
3717
|
+
options: JSON.parse(response.val.options),
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
async _getSessionCapabilities(input) {
|
|
3721
|
+
const response = await this._sendAcpRequest({
|
|
3722
|
+
tag: "AcpGetSessionCapabilitiesRequest",
|
|
3723
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
3724
|
+
});
|
|
3725
|
+
if (response.tag !== "AcpSessionCapabilitiesResponse") {
|
|
3726
|
+
throw new Error(`unexpected getSessionCapabilities response: ${response.tag}`);
|
|
3727
|
+
}
|
|
3728
|
+
return response.val.capabilities === null
|
|
3729
|
+
? null
|
|
3730
|
+
: normalizeSessionCapabilities(JSON.parse(response.val.capabilities));
|
|
3731
|
+
}
|
|
3732
|
+
async _getSessionAgentInfo(input) {
|
|
3733
|
+
const response = await this._sendAcpRequest({
|
|
3734
|
+
tag: "AcpGetSessionAgentInfoRequest",
|
|
3735
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
3736
|
+
});
|
|
3737
|
+
if (response.tag !== "AcpSessionAgentInfoResponse") {
|
|
3738
|
+
throw new Error(`unexpected getSessionAgentInfo response: ${response.tag}`);
|
|
3739
|
+
}
|
|
3740
|
+
return response.val.agentInfo === null
|
|
3741
|
+
? null
|
|
3742
|
+
: JSON.parse(response.val.agentInfo);
|
|
3743
|
+
}
|
|
3744
|
+
/** @deprecated Use `sessions.open()`. */
|
|
3745
|
+
openSession(input) {
|
|
3746
|
+
return this.sessions.open(input);
|
|
3747
|
+
}
|
|
3748
|
+
/** @deprecated Use `sessions.get()`. */
|
|
3749
|
+
getSession(input) {
|
|
3750
|
+
return this.sessions.get(input);
|
|
3751
|
+
}
|
|
3752
|
+
/** @deprecated Use `sessions.list()`. */
|
|
3753
|
+
listSessions(input) {
|
|
3754
|
+
return this.sessions.list(input);
|
|
3755
|
+
}
|
|
3756
|
+
/** @deprecated Use `sessions.delete()`. */
|
|
3757
|
+
deleteSession(input = {}) {
|
|
3758
|
+
return this.sessions.delete(input);
|
|
3759
|
+
}
|
|
3760
|
+
/** @deprecated Use `sessions.unload()`. */
|
|
3761
|
+
unloadSession(input) {
|
|
3762
|
+
return this.sessions.unload(input);
|
|
3763
|
+
}
|
|
3764
|
+
/** @deprecated Use `sessions.prompt()`. */
|
|
3765
|
+
prompt(input) {
|
|
3766
|
+
return this.sessions.prompt(input);
|
|
3767
|
+
}
|
|
3768
|
+
/** @deprecated Use `sessions.cancelPrompt()`. */
|
|
3769
|
+
cancelPrompt(input) {
|
|
3770
|
+
return this.sessions.cancelPrompt(input);
|
|
3771
|
+
}
|
|
3772
|
+
/** @deprecated Use `sessions.respondPermission()`. */
|
|
3773
|
+
respondPermission(input) {
|
|
3774
|
+
return this.sessions.respondPermission(input);
|
|
3775
|
+
}
|
|
3776
|
+
/** @deprecated Use `sessions.readHistory()`. */
|
|
3777
|
+
readHistory(input) {
|
|
3778
|
+
return this.sessions.readHistory(input);
|
|
3779
|
+
}
|
|
3780
|
+
/** @deprecated Use `sessions.getConfig()`. */
|
|
3781
|
+
getSessionConfig(input) {
|
|
3782
|
+
return this.sessions.getConfig(input);
|
|
3783
|
+
}
|
|
3784
|
+
/** @deprecated Use `sessions.setConfigOption()`. */
|
|
3785
|
+
setSessionConfigOption(input) {
|
|
3786
|
+
return this.sessions.setConfigOption(input);
|
|
3787
|
+
}
|
|
3788
|
+
/** @deprecated Use `sessions.getCapabilities()`. */
|
|
3789
|
+
getSessionCapabilities(input) {
|
|
3790
|
+
return this.sessions.getCapabilities(input);
|
|
3791
|
+
}
|
|
3792
|
+
/** @deprecated Use `sessions.getAgentInfo()`. */
|
|
3793
|
+
getSessionAgentInfo(input) {
|
|
3794
|
+
return this.sessions.getAgentInfo(input);
|
|
2165
3795
|
}
|
|
2166
3796
|
/**
|
|
2167
3797
|
* Dynamically link a software package into the RUNNING VM. The package's
|
|
2168
3798
|
* `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
|
|
2169
3799
|
* pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
|
|
2170
3800
|
* writing into its staging dir is reflected live with no reboot. An `agent`
|
|
2171
|
-
* block registers the package for `
|
|
3801
|
+
* block registers the package for `openSession({ agent: name })`. Persists for the VM's
|
|
2172
3802
|
* lifetime (and across a snapshot iff the volume persists).
|
|
2173
3803
|
*/
|
|
2174
|
-
async
|
|
2175
|
-
const ref = normalizePackageRef(descriptor);
|
|
2176
|
-
if (!ref) {
|
|
2177
|
-
throw new Error("Invalid agentOS package reference");
|
|
2178
|
-
}
|
|
3804
|
+
async _linkSoftware(descriptor) {
|
|
2179
3805
|
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
2180
3806
|
// appends the package to its live host-backed staging dir; the commands
|
|
2181
3807
|
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
2182
3808
|
// duplicate command, surfaced here as a thrown error.
|
|
2183
|
-
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm,
|
|
3809
|
+
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, descriptor);
|
|
2184
3810
|
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
2185
3811
|
this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
|
|
2186
3812
|
command.name,
|
|
@@ -2189,13 +3815,13 @@ export class AgentOs {
|
|
|
2189
3815
|
// Retain the linked package for runtime mount reconfigures:
|
|
2190
3816
|
// `configure_vm` is replace-on-write, so a later `mountFs` that
|
|
2191
3817
|
// resent only the boot packages would unproject this one.
|
|
2192
|
-
this.#kernel.registerLinkedPackage(
|
|
3818
|
+
this.#kernel.registerLinkedPackage(descriptor);
|
|
2193
3819
|
}
|
|
2194
3820
|
// The client parses no manifests: an `agent` block in the linked package is
|
|
2195
3821
|
// picked up by the sidecar (it owns the projected `/opt/agentos` and answers
|
|
2196
|
-
//
|
|
3822
|
+
// openSession/listAgents from it). Nothing to record client-side.
|
|
2197
3823
|
}
|
|
2198
|
-
async
|
|
3824
|
+
async _listSoftware() {
|
|
2199
3825
|
return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
|
|
2200
3826
|
}
|
|
2201
3827
|
/**
|
|
@@ -2204,7 +3830,7 @@ export class AgentOs {
|
|
|
2204
3830
|
* projected `/opt/agentos` packages (the client parses no manifests). Every such
|
|
2205
3831
|
* agent is a package materialized into the VM, so `installed` is always `true`.
|
|
2206
3832
|
*/
|
|
2207
|
-
async
|
|
3833
|
+
async _listAgents() {
|
|
2208
3834
|
const response = await this._sendAcpRequest({
|
|
2209
3835
|
tag: "AcpListAgentsRequest",
|
|
2210
3836
|
val: { reserved: false },
|
|
@@ -2215,201 +3841,148 @@ export class AgentOs {
|
|
|
2215
3841
|
return response.val.agents.map((agent) => ({
|
|
2216
3842
|
id: agent.id,
|
|
2217
3843
|
installed: agent.installed,
|
|
2218
|
-
adapterEntrypoint: agent.adapterEntrypoint,
|
|
2219
3844
|
}));
|
|
2220
3845
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
session.closed = state.closed;
|
|
2225
|
-
session.modes = toSessionModes(state.modes);
|
|
2226
|
-
session.configOptions = toSessionConfigOptions(state.configOptions);
|
|
2227
|
-
this._applySyntheticConfigOverrides(session);
|
|
2228
|
-
session.capabilities = toAgentCapabilities(state.agentCapabilities);
|
|
2229
|
-
session.agentInfo = toAgentInfo(state.agentInfo);
|
|
2230
|
-
}
|
|
2231
|
-
_applySessionUpdate(session, notification) {
|
|
2232
|
-
if (notification.method !== "session/update") {
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
const params = toRecord(notification.params);
|
|
2236
|
-
const update = toRecord(params.update ?? params);
|
|
2237
|
-
const sessionUpdate = update.sessionUpdate;
|
|
2238
|
-
if (sessionUpdate === "current_mode_update" &&
|
|
2239
|
-
typeof update.currentModeId === "string" &&
|
|
2240
|
-
session.modes) {
|
|
2241
|
-
session.modes = {
|
|
2242
|
-
...session.modes,
|
|
2243
|
-
currentModeId: update.currentModeId,
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
if ((sessionUpdate === "config_option_update" ||
|
|
2247
|
-
sessionUpdate === "config_options_update") &&
|
|
2248
|
-
Array.isArray(update.configOptions)) {
|
|
2249
|
-
session.configOptions = update.configOptions;
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
_recordSessionNotification(session, notification) {
|
|
2253
|
-
this._applySessionUpdate(session, notification);
|
|
2254
|
-
if (shouldDispatchToSessionEventHandlers(notification)) {
|
|
2255
|
-
this._dispatchSessionEvent(session, notification);
|
|
2256
|
-
}
|
|
2257
|
-
if (notification.method === LEGACY_PERMISSION_METHOD ||
|
|
2258
|
-
notification.method === ACP_PERMISSION_METHOD) {
|
|
2259
|
-
const params = toRecord(notification.params);
|
|
2260
|
-
const permissionId = params.permissionId;
|
|
2261
|
-
if (typeof permissionId === "string" ||
|
|
2262
|
-
typeof permissionId === "number") {
|
|
2263
|
-
const request = {
|
|
2264
|
-
permissionId: String(permissionId),
|
|
2265
|
-
description: typeof params.description === "string"
|
|
2266
|
-
? params.description
|
|
2267
|
-
: undefined,
|
|
2268
|
-
params,
|
|
2269
|
-
};
|
|
2270
|
-
for (const handler of session.permissionHandlers) {
|
|
2271
|
-
handler(request);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
_dispatchSessionEvent(session, notification) {
|
|
2277
|
-
if (session.eventHandlers.size === 0) {
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
for (const subscriber of [...session.eventHandlers]) {
|
|
2281
|
-
try {
|
|
2282
|
-
subscriber.handler(notification);
|
|
2283
|
-
}
|
|
2284
|
-
catch {
|
|
2285
|
-
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
3846
|
+
/** @deprecated Use `software.link()`. */
|
|
3847
|
+
linkSoftware(descriptor) {
|
|
3848
|
+
return this.software.link(descriptor);
|
|
2288
3849
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
};
|
|
2293
|
-
session.eventHandlers.add(subscriber);
|
|
2294
|
-
return () => {
|
|
2295
|
-
session.eventHandlers.delete(subscriber);
|
|
2296
|
-
};
|
|
3850
|
+
/** @deprecated Use `software.list()`. */
|
|
3851
|
+
listSoftware() {
|
|
3852
|
+
return this.software.list();
|
|
2297
3853
|
}
|
|
2298
|
-
/**
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
* by both the bare-callback and JSON-RPC permission paths so the message and
|
|
2302
|
-
* the once-per-session guard cannot drift between them.
|
|
2303
|
-
*/
|
|
2304
|
-
_warnNoPermissionHandlerOnce(session, params) {
|
|
2305
|
-
if (session.warnedNoPermissionHandler) {
|
|
2306
|
-
return;
|
|
2307
|
-
}
|
|
2308
|
-
session.warnedNoPermissionHandler = true;
|
|
2309
|
-
this._emitSessionWarning(session, `agentos: a tool-permission request (${this._permissionToolLabel(params)}) was ` +
|
|
2310
|
-
`auto-denied because no onPermissionRequest handler is registered for session ` +
|
|
2311
|
-
`${session.sessionId}. Register one with vm.onPermissionRequest(sessionId, ...) and ` +
|
|
2312
|
-
`reply via vm.respondPermission(...) to let the agent use tools.`);
|
|
2313
|
-
}
|
|
2314
|
-
/** Best-effort human label for the tool named in a permission request. */
|
|
2315
|
-
_permissionToolLabel(params) {
|
|
2316
|
-
if (typeof params.toolName === "string") {
|
|
2317
|
-
return params.toolName;
|
|
2318
|
-
}
|
|
2319
|
-
const toolCall = params.toolCall;
|
|
2320
|
-
if (toolCall &&
|
|
2321
|
-
typeof toolCall === "object" &&
|
|
2322
|
-
typeof toolCall.title === "string") {
|
|
2323
|
-
return toolCall.title;
|
|
2324
|
-
}
|
|
2325
|
-
return "a tool";
|
|
2326
|
-
}
|
|
2327
|
-
/**
|
|
2328
|
-
* Emit a host-visible warning for a session through the same agent-process log
|
|
2329
|
-
* channel that surfaces adapter stderr (`onAgentStderr`, default: process
|
|
2330
|
-
* stderr). Used for agent-os-owned diagnostics — e.g. a permission request
|
|
2331
|
-
* that was auto-denied because no host hook is registered — so they never fire
|
|
2332
|
-
* silently inside the sidecar.
|
|
2333
|
-
*/
|
|
2334
|
-
_emitSessionWarning(session, message) {
|
|
2335
|
-
const handler = this._agentStderrHandler;
|
|
2336
|
-
if (!handler) {
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2339
|
-
try {
|
|
2340
|
-
handler({
|
|
2341
|
-
sessionId: session.sessionId,
|
|
2342
|
-
agentType: session.agentType,
|
|
2343
|
-
processId: session.processId,
|
|
2344
|
-
pid: session.pid,
|
|
2345
|
-
chunk: new TextEncoder().encode(`${message}\n`),
|
|
2346
|
-
});
|
|
2347
|
-
}
|
|
2348
|
-
catch {
|
|
2349
|
-
// A warning sink failure must never affect permission handling.
|
|
2350
|
-
}
|
|
3854
|
+
/** @deprecated Use `agents.list()`. */
|
|
3855
|
+
listAgents() {
|
|
3856
|
+
return this.agents.list();
|
|
2351
3857
|
}
|
|
2352
3858
|
_recordAgentStderr(event) {
|
|
2353
|
-
|
|
2354
|
-
[...this._sessions.values()].find((candidate) => candidate.processId === event.processId);
|
|
2355
|
-
const sessionId = event.sessionId || session?.sessionId;
|
|
2356
|
-
if (!sessionId) {
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
const handler = this._agentStderrHandler;
|
|
2360
|
-
if (!handler) {
|
|
3859
|
+
if (!event.sessionId) {
|
|
2361
3860
|
return;
|
|
2362
3861
|
}
|
|
2363
|
-
|
|
2364
|
-
handler({
|
|
2365
|
-
sessionId,
|
|
2366
|
-
agentType: event.agentType || session?.agentType || "",
|
|
2367
|
-
processId: event.processId,
|
|
2368
|
-
pid: session?.pid ?? null,
|
|
2369
|
-
chunk: new Uint8Array(event.chunk),
|
|
2370
|
-
});
|
|
2371
|
-
}
|
|
2372
|
-
catch {
|
|
2373
|
-
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
_recordAgentExit(event) {
|
|
2377
|
-
const session = this._sessions.get(event.sessionId);
|
|
2378
|
-
const handler = this._agentExitHandler;
|
|
3862
|
+
const handler = this._agentStderrHandler;
|
|
2379
3863
|
if (!handler) {
|
|
2380
3864
|
return;
|
|
2381
3865
|
}
|
|
2382
3866
|
try {
|
|
2383
3867
|
handler({
|
|
2384
3868
|
sessionId: event.sessionId,
|
|
2385
|
-
agentType: event.agentType
|
|
3869
|
+
agentType: event.agentType,
|
|
2386
3870
|
processId: event.processId,
|
|
2387
|
-
pid:
|
|
2388
|
-
|
|
2389
|
-
restart: event.restart,
|
|
2390
|
-
restartCount: event.restartCount,
|
|
2391
|
-
maxRestarts: event.maxRestarts,
|
|
3871
|
+
pid: null,
|
|
3872
|
+
chunk: new Uint8Array(event.chunk),
|
|
2392
3873
|
});
|
|
2393
3874
|
}
|
|
2394
|
-
catch {
|
|
2395
|
-
|
|
3875
|
+
catch (error) {
|
|
3876
|
+
console.error("AgentOS stderr handler failed", error);
|
|
2396
3877
|
}
|
|
2397
3878
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
3879
|
+
_recordAgentExit(event) {
|
|
3880
|
+
const publicEvent = {
|
|
3881
|
+
sessionId: event.sessionId,
|
|
3882
|
+
agentType: event.agentType,
|
|
3883
|
+
processId: event.processId,
|
|
3884
|
+
pid: event.pid,
|
|
3885
|
+
exitCode: event.exitCode,
|
|
3886
|
+
restart: event.restart,
|
|
3887
|
+
restartCount: event.restartCount,
|
|
3888
|
+
maxRestarts: event.maxRestarts,
|
|
3889
|
+
};
|
|
3890
|
+
const handler = this._agentExitHandler;
|
|
3891
|
+
if (handler) {
|
|
3892
|
+
try {
|
|
3893
|
+
handler(publicEvent);
|
|
3894
|
+
}
|
|
3895
|
+
catch (error) {
|
|
3896
|
+
console.error("AgentOS agent-exit handler failed", error);
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
for (const key of ["*", event.sessionId]) {
|
|
3900
|
+
for (const subscription of this._agentExitHandlers.get(key) ?? []) {
|
|
3901
|
+
try {
|
|
3902
|
+
subscription(publicEvent);
|
|
3903
|
+
}
|
|
3904
|
+
catch (error) {
|
|
3905
|
+
console.error("AgentOS agent-exit subscription failed", error);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
2401
3908
|
}
|
|
2402
|
-
session.configOptions = session.configOptions.map((option) => {
|
|
2403
|
-
const override = session.configOverrides.get(option.id) ??
|
|
2404
|
-
(typeof option.category === "string"
|
|
2405
|
-
? session.configOverrides.get(option.category)
|
|
2406
|
-
: undefined);
|
|
2407
|
-
return override === undefined
|
|
2408
|
-
? option
|
|
2409
|
-
: { ...option, currentValue: override };
|
|
2410
|
-
});
|
|
2411
3909
|
}
|
|
2412
3910
|
_handleSidecarEvent(event) {
|
|
3911
|
+
if (event.payload.type === "execution_output") {
|
|
3912
|
+
const output = mapExecutionOutputEvent(event.payload.event);
|
|
3913
|
+
const pid = this._languageProcessIds.get(output.executionId);
|
|
3914
|
+
if (pid !== undefined) {
|
|
3915
|
+
const process = this._languageProcesses.get(pid);
|
|
3916
|
+
if (process) {
|
|
3917
|
+
const publicOutput = {
|
|
3918
|
+
pid,
|
|
3919
|
+
stream: output.channel === "stderr" ? "stderr" : "stdout",
|
|
3920
|
+
data: output.chunk,
|
|
3921
|
+
};
|
|
3922
|
+
for (const handler of process.outputHandlers) {
|
|
3923
|
+
try {
|
|
3924
|
+
handler(publicOutput);
|
|
3925
|
+
}
|
|
3926
|
+
catch (error) {
|
|
3927
|
+
console.error("AgentOS process output handler failed", error);
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
for (const key of ["*", output.executionId]) {
|
|
3933
|
+
for (const handler of this._executionOutputHandlers.get(key) ?? []) {
|
|
3934
|
+
try {
|
|
3935
|
+
handler(output);
|
|
3936
|
+
}
|
|
3937
|
+
catch (error) {
|
|
3938
|
+
console.error("AgentOS execution output handler failed", error);
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
return;
|
|
3943
|
+
}
|
|
3944
|
+
if (event.payload.type === "execution_completed") {
|
|
3945
|
+
const completed = mapExecutionCompletedEvent(event.payload.event);
|
|
3946
|
+
const pid = this._languageProcessIds.get(completed.executionId);
|
|
3947
|
+
if (pid !== undefined) {
|
|
3948
|
+
const process = this._languageProcesses.get(pid);
|
|
3949
|
+
if (process && !process.exit) {
|
|
3950
|
+
const exit = {
|
|
3951
|
+
pid,
|
|
3952
|
+
outcome: completed.outcome === "timed_out"
|
|
3953
|
+
? "timed_out"
|
|
3954
|
+
: completed.outcome === "cancelled"
|
|
3955
|
+
? "signalled"
|
|
3956
|
+
: "exited",
|
|
3957
|
+
...(completed.exitCode !== undefined
|
|
3958
|
+
? { exitCode: completed.exitCode }
|
|
3959
|
+
: {}),
|
|
3960
|
+
...(process.signal ? { signal: process.signal } : {}),
|
|
3961
|
+
};
|
|
3962
|
+
process.exit = exit;
|
|
3963
|
+
process.descriptor = { ...process.descriptor, state: "exited" };
|
|
3964
|
+
for (const handler of process.exitHandlers) {
|
|
3965
|
+
try {
|
|
3966
|
+
handler(exit);
|
|
3967
|
+
}
|
|
3968
|
+
catch (error) {
|
|
3969
|
+
console.error("AgentOS process exit handler failed", error);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
for (const key of ["*", completed.executionId]) {
|
|
3975
|
+
for (const handler of this._executionCompletedHandlers.get(key) ?? []) {
|
|
3976
|
+
try {
|
|
3977
|
+
handler(completed);
|
|
3978
|
+
}
|
|
3979
|
+
catch (error) {
|
|
3980
|
+
console.error("AgentOS execution completion handler failed", error);
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
2413
3986
|
if (event.payload.type === "ext") {
|
|
2414
3987
|
this._handleAcpExtEvent(event.payload.envelope);
|
|
2415
3988
|
return;
|
|
@@ -2419,25 +3992,6 @@ export class AgentOs {
|
|
|
2419
3992
|
}
|
|
2420
3993
|
if (event.payload.name === "limit_warning") {
|
|
2421
3994
|
this._handleLimitWarning(event.payload.detail);
|
|
2422
|
-
return;
|
|
2423
|
-
}
|
|
2424
|
-
if (event.payload.name !== "acp.session_event") {
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
const sessionId = event.payload.detail.session_id;
|
|
2428
|
-
const session = sessionId ? this._sessions.get(sessionId) : undefined;
|
|
2429
|
-
if (!session) {
|
|
2430
|
-
return;
|
|
2431
|
-
}
|
|
2432
|
-
const notificationText = event.payload.detail.notification;
|
|
2433
|
-
if (typeof notificationText !== "string") {
|
|
2434
|
-
return;
|
|
2435
|
-
}
|
|
2436
|
-
try {
|
|
2437
|
-
this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(notificationText)));
|
|
2438
|
-
}
|
|
2439
|
-
catch {
|
|
2440
|
-
// Ignore malformed event payloads from the sidecar.
|
|
2441
3995
|
}
|
|
2442
3996
|
}
|
|
2443
3997
|
_handleLimitWarning(detail) {
|
|
@@ -2457,8 +4011,8 @@ export class AgentOs {
|
|
|
2457
4011
|
fillPercent: toNumber(detail.fillPercent),
|
|
2458
4012
|
});
|
|
2459
4013
|
}
|
|
2460
|
-
catch {
|
|
2461
|
-
|
|
4014
|
+
catch (error) {
|
|
4015
|
+
console.error("AgentOS limit-warning handler failed", error);
|
|
2462
4016
|
}
|
|
2463
4017
|
}
|
|
2464
4018
|
_handleAcpExtEvent(envelope) {
|
|
@@ -2468,14 +4022,24 @@ export class AgentOs {
|
|
|
2468
4022
|
try {
|
|
2469
4023
|
const event = decodeAcpEvent(envelope.payload);
|
|
2470
4024
|
switch (event.tag) {
|
|
2471
|
-
case "
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
4025
|
+
case "AcpDurableSessionEvent": {
|
|
4026
|
+
this._emitDurableSessionEvent(decodeDurableSessionEvent(event.val));
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
case "AcpEphemeralSessionUpdateEvent": {
|
|
4030
|
+
const update = JSON.parse(event.val.update);
|
|
4031
|
+
const { sessionUpdate: type, ...payload } = update;
|
|
4032
|
+
this._emitDurableSessionEvent({
|
|
4033
|
+
durability: "ephemeral",
|
|
4034
|
+
type,
|
|
4035
|
+
sessionId: event.val.sessionId,
|
|
4036
|
+
afterSequence: safeWireU64(event.val.afterSequence),
|
|
4037
|
+
...payload,
|
|
4038
|
+
});
|
|
2477
4039
|
return;
|
|
2478
4040
|
}
|
|
4041
|
+
case "AcpSessionEvent":
|
|
4042
|
+
return;
|
|
2479
4043
|
case "AcpAgentStderrEvent": {
|
|
2480
4044
|
this._recordAgentStderr(event.val);
|
|
2481
4045
|
return;
|
|
@@ -2486,22 +4050,19 @@ export class AgentOs {
|
|
|
2486
4050
|
}
|
|
2487
4051
|
}
|
|
2488
4052
|
}
|
|
2489
|
-
catch {
|
|
2490
|
-
|
|
4053
|
+
catch (error) {
|
|
4054
|
+
console.error("AgentOS failed to decode an ACP sidecar event", error);
|
|
2491
4055
|
}
|
|
2492
4056
|
}
|
|
2493
|
-
|
|
2494
|
-
const
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
message,
|
|
2503
|
-
},
|
|
2504
|
-
};
|
|
4057
|
+
_emitDurableSessionEvent(entry) {
|
|
4058
|
+
for (const handler of this._durableSessionEventHandlers.get(entry.sessionId) ?? []) {
|
|
4059
|
+
try {
|
|
4060
|
+
handler(entry);
|
|
4061
|
+
}
|
|
4062
|
+
catch (error) {
|
|
4063
|
+
console.error("AgentOS session event handler failed", error);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
2505
4066
|
}
|
|
2506
4067
|
async _sendAcpRequest(request) {
|
|
2507
4068
|
const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
|
|
@@ -2519,300 +4080,10 @@ export class AgentOs {
|
|
|
2519
4080
|
}
|
|
2520
4081
|
return response;
|
|
2521
4082
|
}
|
|
2522
|
-
async _sendSessionRequest(sessionId, method, params) {
|
|
2523
|
-
const session = this._requireSession(sessionId);
|
|
2524
|
-
const response = await new Promise((resolve, reject) => {
|
|
2525
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId) ?? new Set();
|
|
2526
|
-
const resolver = {
|
|
2527
|
-
method,
|
|
2528
|
-
resolve: (response) => {
|
|
2529
|
-
resolve(response);
|
|
2530
|
-
},
|
|
2531
|
-
};
|
|
2532
|
-
resolvers.add(resolver);
|
|
2533
|
-
this._pendingSessionRequestResolvers.set(sessionId, resolvers);
|
|
2534
|
-
void this._sendAcpRequest({
|
|
2535
|
-
tag: "AcpSessionRequest",
|
|
2536
|
-
val: {
|
|
2537
|
-
sessionId,
|
|
2538
|
-
method,
|
|
2539
|
-
params: params === undefined ? null : JSON.stringify(params),
|
|
2540
|
-
},
|
|
2541
|
-
})
|
|
2542
|
-
.then((response) => {
|
|
2543
|
-
if (response.tag !== "AcpSessionRpcResponse") {
|
|
2544
|
-
throw new Error(`unexpected response to AcpSessionRequest: ${response.tag}`);
|
|
2545
|
-
}
|
|
2546
|
-
return toJsonRpcResponse(JSON.parse(response.val.response));
|
|
2547
|
-
})
|
|
2548
|
-
.then(resolve, reject)
|
|
2549
|
-
.finally(() => {
|
|
2550
|
-
const nextResolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2551
|
-
if (!nextResolvers) {
|
|
2552
|
-
return;
|
|
2553
|
-
}
|
|
2554
|
-
nextResolvers.delete(resolver);
|
|
2555
|
-
if (nextResolvers.size === 0) {
|
|
2556
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2557
|
-
}
|
|
2558
|
-
});
|
|
2559
|
-
});
|
|
2560
|
-
const liveSession = this._sessions.get(sessionId);
|
|
2561
|
-
if (liveSession && !isLocalCancelledPromptResponse(method, response)) {
|
|
2562
|
-
await this._hydrateSessionState(liveSession).catch(() => { });
|
|
2563
|
-
}
|
|
2564
|
-
if (!response.error) {
|
|
2565
|
-
if (method === "session/set_mode" &&
|
|
2566
|
-
typeof params?.modeId === "string" &&
|
|
2567
|
-
session.modes) {
|
|
2568
|
-
session.modes = {
|
|
2569
|
-
...session.modes,
|
|
2570
|
-
currentModeId: params.modeId,
|
|
2571
|
-
};
|
|
2572
|
-
}
|
|
2573
|
-
if (method === "session/set_config_option" &&
|
|
2574
|
-
typeof params?.configId === "string" &&
|
|
2575
|
-
typeof params?.value === "string") {
|
|
2576
|
-
const nextValue = params.value;
|
|
2577
|
-
const updatedOption = session.configOptions.find((option) => option.id === params.configId);
|
|
2578
|
-
session.configOverrides.set(params.configId, nextValue);
|
|
2579
|
-
if (typeof updatedOption?.category === "string") {
|
|
2580
|
-
session.configOverrides.set(updatedOption.category, nextValue);
|
|
2581
|
-
}
|
|
2582
|
-
session.configOptions = session.configOptions.map((option) => option.id === params.configId
|
|
2583
|
-
? { ...option, currentValue: nextValue }
|
|
2584
|
-
: option);
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
return response;
|
|
2588
|
-
}
|
|
2589
|
-
async _setSessionConfigByCategory(sessionId, category, value) {
|
|
2590
|
-
const session = this._requireSession(sessionId);
|
|
2591
|
-
const option = session.configOptions.find((entry) => entry.category === category);
|
|
2592
|
-
if (option?.readOnly) {
|
|
2593
|
-
return this._unsupportedConfigResponse(session.agentType, category);
|
|
2594
|
-
}
|
|
2595
|
-
const response = await this._sendSessionRequest(sessionId, "session/set_config_option", {
|
|
2596
|
-
configId: option?.id ?? category,
|
|
2597
|
-
value,
|
|
2598
|
-
});
|
|
2599
|
-
return response;
|
|
2600
|
-
}
|
|
2601
|
-
_removeSession(sessionId) {
|
|
2602
|
-
this._sessions.delete(sessionId);
|
|
2603
|
-
}
|
|
2604
|
-
_abortPendingSessionRequests(sessionId) {
|
|
2605
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2606
|
-
if (!resolvers) {
|
|
2607
|
-
return;
|
|
2608
|
-
}
|
|
2609
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2610
|
-
const response = {
|
|
2611
|
-
jsonrpc: "2.0",
|
|
2612
|
-
id: null,
|
|
2613
|
-
error: {
|
|
2614
|
-
code: -32_000,
|
|
2615
|
-
message: `Session closed: ${sessionId}`,
|
|
2616
|
-
},
|
|
2617
|
-
};
|
|
2618
|
-
for (const resolver of resolvers) {
|
|
2619
|
-
resolver.resolve(response);
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
_cancelPendingPromptRequests(sessionId) {
|
|
2623
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2624
|
-
if (!resolvers) {
|
|
2625
|
-
return false;
|
|
2626
|
-
}
|
|
2627
|
-
const response = {
|
|
2628
|
-
jsonrpc: "2.0",
|
|
2629
|
-
id: null,
|
|
2630
|
-
result: {
|
|
2631
|
-
stopReason: "cancelled",
|
|
2632
|
-
},
|
|
2633
|
-
};
|
|
2634
|
-
let cancelledPrompt = false;
|
|
2635
|
-
for (const resolver of [...resolvers]) {
|
|
2636
|
-
if (resolver.method !== "session/prompt") {
|
|
2637
|
-
continue;
|
|
2638
|
-
}
|
|
2639
|
-
resolvers.delete(resolver);
|
|
2640
|
-
resolver.resolve(response);
|
|
2641
|
-
cancelledPrompt = true;
|
|
2642
|
-
}
|
|
2643
|
-
if (resolvers.size === 0) {
|
|
2644
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2645
|
-
}
|
|
2646
|
-
return cancelledPrompt;
|
|
2647
|
-
}
|
|
2648
|
-
_rejectPendingPermissionReplies(sessionId) {
|
|
2649
|
-
const session = this._sessions.get(sessionId);
|
|
2650
|
-
if (!session) {
|
|
2651
|
-
return;
|
|
2652
|
-
}
|
|
2653
|
-
this._rejectPendingPermissionRepliesFromSession(session);
|
|
2654
|
-
}
|
|
2655
|
-
_rejectPendingPermissionRepliesFromSession(session) {
|
|
2656
|
-
for (const [permissionId, pendingReply,] of session.pendingPermissionReplies) {
|
|
2657
|
-
clearTimeout(pendingReply.timer);
|
|
2658
|
-
pendingReply.reject(new Error(`Session closed before permission reply: ${permissionId}`));
|
|
2659
|
-
}
|
|
2660
|
-
session.pendingPermissionReplies.clear();
|
|
2661
|
-
}
|
|
2662
|
-
async _closeSessionInternal(sessionId) {
|
|
2663
|
-
const closing = this._sessionClosePromises.get(sessionId);
|
|
2664
|
-
if (closing) {
|
|
2665
|
-
return closing;
|
|
2666
|
-
}
|
|
2667
|
-
if (this._closedSessionIds.has(sessionId)) {
|
|
2668
|
-
return;
|
|
2669
|
-
}
|
|
2670
|
-
this._abortPendingSessionRequests(sessionId);
|
|
2671
|
-
this._rejectPendingPermissionReplies(sessionId);
|
|
2672
|
-
this._requireSession(sessionId);
|
|
2673
|
-
this._removeSession(sessionId);
|
|
2674
|
-
this._closedSessionIds.add(sessionId);
|
|
2675
|
-
const closePromise = this._sendAcpRequest({
|
|
2676
|
-
tag: "AcpCloseSessionRequest",
|
|
2677
|
-
val: { sessionId },
|
|
2678
|
-
})
|
|
2679
|
-
.then((response) => {
|
|
2680
|
-
if (response.tag !== "AcpSessionClosedResponse") {
|
|
2681
|
-
throw new Error(`unexpected response to AcpCloseSessionRequest: ${response.tag}`);
|
|
2682
|
-
}
|
|
2683
|
-
})
|
|
2684
|
-
.finally(() => {
|
|
2685
|
-
this._sessionClosePromises.delete(sessionId);
|
|
2686
|
-
});
|
|
2687
|
-
this._sessionClosePromises.set(sessionId, closePromise);
|
|
2688
|
-
await closePromise;
|
|
2689
|
-
}
|
|
2690
|
-
async _hydrateSessionState(session) {
|
|
2691
|
-
const response = await this._sendAcpRequest({
|
|
2692
|
-
tag: "AcpGetSessionStateRequest",
|
|
2693
|
-
val: { sessionId: session.sessionId },
|
|
2694
|
-
});
|
|
2695
|
-
if (response.tag !== "AcpSessionStateResponse") {
|
|
2696
|
-
throw new Error(`unexpected response to AcpGetSessionStateRequest: ${response.tag}`);
|
|
2697
|
-
}
|
|
2698
|
-
const state = sidecarSessionStateFromAcp(response.val);
|
|
2699
|
-
this._syncSessionState(session, state);
|
|
2700
|
-
}
|
|
2701
|
-
async createSession(agentType, options) {
|
|
2702
|
-
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
2703
|
-
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
2704
|
-
// projected `/opt/agentos/<name>/current/agentos-package.json` and spawns
|
|
2705
|
-
// (including the agent's static launch args and manifest env defaults).
|
|
2706
|
-
// System-prompt assembly/injection (launch args / OPENCODE_CONTEXTPATHS) is
|
|
2707
|
-
// owned by the sidecar; the host only forwards additionalInstructions /
|
|
2708
|
-
// skipOsInstructions plus the caller's env.
|
|
2709
|
-
const launchEnv = { ...options?.env };
|
|
2710
|
-
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2711
|
-
const response = await this._sendAcpRequest({
|
|
2712
|
-
tag: "AcpCreateSessionRequest",
|
|
2713
|
-
val: {
|
|
2714
|
-
agentType: String(agentType),
|
|
2715
|
-
runtime: AcpRuntimeKind.JavaScript,
|
|
2716
|
-
args: [],
|
|
2717
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2718
|
-
cwd: sessionCwd,
|
|
2719
|
-
mcpServers: JSON.stringify(options?.mcpServers ?? []),
|
|
2720
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2721
|
-
clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
|
|
2722
|
-
additionalInstructions: combineInstructions([this._additionalInstructions, options?.additionalInstructions]
|
|
2723
|
-
.map((part) => part?.trim())
|
|
2724
|
-
.filter((part) => Boolean(part))
|
|
2725
|
-
.join("\n\n") || undefined, this._toolReference),
|
|
2726
|
-
skipOsInstructions: options?.skipOsInstructions ?? false,
|
|
2727
|
-
},
|
|
2728
|
-
});
|
|
2729
|
-
if (response.tag !== "AcpSessionCreatedResponse") {
|
|
2730
|
-
throw new Error(`unexpected create_session response: ${response.tag}`);
|
|
2731
|
-
}
|
|
2732
|
-
const created = sidecarSessionCreatedFromAcp(response.val);
|
|
2733
|
-
// The sessionId is chosen by the (untrusted/buggy) ACP adapter or sidecar. If it collides
|
|
2734
|
-
// with a live session already in `_sessions`, blindly overwriting the entry would orphan the
|
|
2735
|
-
// first session's event/permission handlers and pending permission replies. Fail closed:
|
|
2736
|
-
// reject the colliding create and leave the original session intact. (A previously-closed,
|
|
2737
|
-
// evicted id may still be re-used; only a live, non-closed entry is a collision.)
|
|
2738
|
-
const existing = this._sessions.get(created.sessionId);
|
|
2739
|
-
if (existing !== undefined && !existing.closed) {
|
|
2740
|
-
throw new Error(`session id collision: ${created.sessionId}`);
|
|
2741
|
-
}
|
|
2742
|
-
const initData = {
|
|
2743
|
-
modes: toSessionModes(created.modes) ?? undefined,
|
|
2744
|
-
configOptions: toSessionConfigOptions(created.configOptions),
|
|
2745
|
-
capabilities: toAgentCapabilities(created.agentCapabilities),
|
|
2746
|
-
agentInfo: toAgentInfo(created.agentInfo) ?? undefined,
|
|
2747
|
-
};
|
|
2748
|
-
const session = sessionEntryFromInit(created.sessionId, String(agentType), initData);
|
|
2749
|
-
this._closedSessionIds.delete(created.sessionId);
|
|
2750
|
-
this._sessions.set(created.sessionId, session);
|
|
2751
|
-
try {
|
|
2752
|
-
await this._hydrateSessionState(session);
|
|
2753
|
-
}
|
|
2754
|
-
catch (error) {
|
|
2755
|
-
this._removeSession(created.sessionId);
|
|
2756
|
-
throw error;
|
|
2757
|
-
}
|
|
2758
|
-
return { sessionId: created.sessionId };
|
|
2759
|
-
}
|
|
2760
|
-
/**
|
|
2761
|
-
* Resume a session that exists in durable storage but is not live in this VM
|
|
2762
|
-
* (e.g. after a Rivet actor slept and woke with a fresh VM). Thin forwarder:
|
|
2763
|
-
* resolves the agent config + adapter entrypoint exactly as {@link createSession}
|
|
2764
|
-
* does, then forwards a single `AcpResumeSessionRequest` to the sidecar, which
|
|
2765
|
-
* owns the resume state machine (native `session/load` when the agent supports
|
|
2766
|
-
* it, else `session/new` + a transcript-continuation preamble). The returned
|
|
2767
|
-
* `sessionId` is the live id in this VM (equal to the requested id for native
|
|
2768
|
-
* loads, freshly assigned for the fallback); the caller remaps `external -> live`.
|
|
2769
|
-
* The new live session is registered + hydrated locally so subsequent prompts
|
|
2770
|
-
* route to it.
|
|
2771
|
-
*
|
|
2772
|
-
* Resume depends on a durable root; on a non-durable (default in-memory) root
|
|
2773
|
-
* there is no surviving store and the fallback tier always runs.
|
|
2774
|
-
*/
|
|
2775
|
-
async resumeSession(sessionId, agentType, options) {
|
|
2776
|
-
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
2777
|
-
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
2778
|
-
// projected manifest, exactly as createSession does.
|
|
2779
|
-
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2780
|
-
const launchEnv = { ...options?.env };
|
|
2781
|
-
const response = await this._sendAcpRequest({
|
|
2782
|
-
tag: "AcpResumeSessionRequest",
|
|
2783
|
-
val: {
|
|
2784
|
-
sessionId,
|
|
2785
|
-
agentType: String(agentType),
|
|
2786
|
-
transcriptPath: options?.transcriptPath ?? null,
|
|
2787
|
-
cwd: sessionCwd,
|
|
2788
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2789
|
-
},
|
|
2790
|
-
});
|
|
2791
|
-
if (response.tag !== "AcpSessionResumedResponse") {
|
|
2792
|
-
throw new Error(`unexpected resume_session response: ${response.tag}`);
|
|
2793
|
-
}
|
|
2794
|
-
const { sessionId: liveSessionId, mode } = response.val;
|
|
2795
|
-
// Register + hydrate the live session so subsequent prompts route to it.
|
|
2796
|
-
const existing = this._sessions.get(liveSessionId);
|
|
2797
|
-
if (existing !== undefined && !existing.closed) {
|
|
2798
|
-
throw new Error(`session id collision: ${liveSessionId}`);
|
|
2799
|
-
}
|
|
2800
|
-
const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
|
|
2801
|
-
this._closedSessionIds.delete(liveSessionId);
|
|
2802
|
-
this._sessions.set(liveSessionId, session);
|
|
2803
|
-
try {
|
|
2804
|
-
await this._hydrateSessionState(session);
|
|
2805
|
-
}
|
|
2806
|
-
catch (error) {
|
|
2807
|
-
this._removeSession(liveSessionId);
|
|
2808
|
-
throw error;
|
|
2809
|
-
}
|
|
2810
|
-
return { sessionId: liveSessionId, mode };
|
|
2811
|
-
}
|
|
2812
4083
|
_installSidecarRequestHandler() {
|
|
2813
4084
|
const context = {
|
|
2814
|
-
|
|
2815
|
-
|
|
4085
|
+
bindings: this._bindings,
|
|
4086
|
+
bindingMap: buildBindingMap(this._bindings),
|
|
2816
4087
|
permissions: this._permissions,
|
|
2817
4088
|
readFile: (path) => this.readFile(path),
|
|
2818
4089
|
};
|
|
@@ -2841,25 +4112,6 @@ export class AgentOs {
|
|
|
2841
4112
|
}
|
|
2842
4113
|
const callback = decodeAcpCallback(envelope.payload);
|
|
2843
4114
|
switch (callback.tag) {
|
|
2844
|
-
case "AcpPermissionCallback": {
|
|
2845
|
-
const reply = await this._handleAcpPermissionCallback(callback.val.sessionId, callback.val.permissionId, {
|
|
2846
|
-
...toRecord(JSON.parse(callback.val.params)),
|
|
2847
|
-
_acpMethod: ACP_PERMISSION_METHOD,
|
|
2848
|
-
});
|
|
2849
|
-
return {
|
|
2850
|
-
type: "ext_result",
|
|
2851
|
-
envelope: {
|
|
2852
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
2853
|
-
payload: encodeAcpCallbackResponse({
|
|
2854
|
-
tag: "AcpPermissionCallbackResponse",
|
|
2855
|
-
val: {
|
|
2856
|
-
permissionId: callback.val.permissionId,
|
|
2857
|
-
reply,
|
|
2858
|
-
},
|
|
2859
|
-
}),
|
|
2860
|
-
},
|
|
2861
|
-
};
|
|
2862
|
-
}
|
|
2863
4115
|
case "AcpHostRequestCallback": {
|
|
2864
4116
|
const response = await this._dispatchAcpSidecarRequest(toJsonRpcRequest(JSON.parse(callback.val.request)));
|
|
2865
4117
|
return {
|
|
@@ -2911,8 +4163,6 @@ export class AgentOs {
|
|
|
2911
4163
|
async _handleSupportedAcpSidecarRequest(request) {
|
|
2912
4164
|
const params = this._acpParams(request);
|
|
2913
4165
|
switch (request.method) {
|
|
2914
|
-
case ACP_PERMISSION_METHOD:
|
|
2915
|
-
return this._handleAcpPermissionRequest(request, params);
|
|
2916
4166
|
case "fs/read":
|
|
2917
4167
|
case "fs/read_text_file":
|
|
2918
4168
|
return this._handleAcpReadFile(params);
|
|
@@ -2945,99 +4195,6 @@ export class AgentOs {
|
|
|
2945
4195
|
});
|
|
2946
4196
|
}
|
|
2947
4197
|
}
|
|
2948
|
-
_normalizeAcpPermissionOptionId(options, reply) {
|
|
2949
|
-
const optionTargets = reply === "always"
|
|
2950
|
-
? {
|
|
2951
|
-
optionIds: new Set(["always", "allow_always"]),
|
|
2952
|
-
kinds: new Set(["allow_always"]),
|
|
2953
|
-
}
|
|
2954
|
-
: reply === "once"
|
|
2955
|
-
? {
|
|
2956
|
-
optionIds: new Set(["once", "allow_once"]),
|
|
2957
|
-
kinds: new Set(["allow_once"]),
|
|
2958
|
-
}
|
|
2959
|
-
: {
|
|
2960
|
-
optionIds: new Set(["reject", "reject_once"]),
|
|
2961
|
-
kinds: new Set(["reject_once"]),
|
|
2962
|
-
};
|
|
2963
|
-
const matched = options?.find((option) => {
|
|
2964
|
-
const optionId = typeof option.optionId === "string" ? option.optionId : undefined;
|
|
2965
|
-
const kind = typeof option.kind === "string" ? option.kind : undefined;
|
|
2966
|
-
return ((optionId !== undefined && optionTargets.optionIds.has(optionId)) ||
|
|
2967
|
-
(kind !== undefined && optionTargets.kinds.has(kind)));
|
|
2968
|
-
});
|
|
2969
|
-
if (matched && typeof matched.optionId === "string") {
|
|
2970
|
-
return matched.optionId;
|
|
2971
|
-
}
|
|
2972
|
-
if (reply === "always") {
|
|
2973
|
-
return "allow_always";
|
|
2974
|
-
}
|
|
2975
|
-
if (reply === "once") {
|
|
2976
|
-
return "allow_once";
|
|
2977
|
-
}
|
|
2978
|
-
return "reject_once";
|
|
2979
|
-
}
|
|
2980
|
-
_buildAcpPermissionResult(reply, params) {
|
|
2981
|
-
const options = Array.isArray(params.options)
|
|
2982
|
-
? params.options.filter((option) => typeof option === "object" && option !== null)
|
|
2983
|
-
: undefined;
|
|
2984
|
-
const optionId = this._normalizeAcpPermissionOptionId(options, reply);
|
|
2985
|
-
return {
|
|
2986
|
-
outcome: optionId
|
|
2987
|
-
? {
|
|
2988
|
-
outcome: "selected",
|
|
2989
|
-
optionId,
|
|
2990
|
-
}
|
|
2991
|
-
: {
|
|
2992
|
-
outcome: "cancelled",
|
|
2993
|
-
},
|
|
2994
|
-
};
|
|
2995
|
-
}
|
|
2996
|
-
async _handleAcpPermissionRequest(request, params) {
|
|
2997
|
-
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
2998
|
-
if (!sessionId) {
|
|
2999
|
-
throw new AcpDispatchError(-32602, `${ACP_PERMISSION_METHOD} requires a sessionId`);
|
|
3000
|
-
}
|
|
3001
|
-
const session = this._sessions.get(sessionId);
|
|
3002
|
-
if (!session) {
|
|
3003
|
-
throw new AcpDispatchError(-32602, `Session not found: ${sessionId}`);
|
|
3004
|
-
}
|
|
3005
|
-
const permissionId = String(request.id);
|
|
3006
|
-
const permissionParams = {
|
|
3007
|
-
...params,
|
|
3008
|
-
permissionId,
|
|
3009
|
-
_acpMethod: request.method,
|
|
3010
|
-
};
|
|
3011
|
-
if (session.permissionHandlers.size === 0) {
|
|
3012
|
-
// Default-closed deny; warn once (host-visible) so a forgotten
|
|
3013
|
-
// onPermissionRequest handler is an observable cause rather than a
|
|
3014
|
-
// silent denial. See _warnNoPermissionHandlerOnce.
|
|
3015
|
-
this._warnNoPermissionHandlerOnce(session, permissionParams);
|
|
3016
|
-
return this._buildAcpPermissionResult("reject", permissionParams);
|
|
3017
|
-
}
|
|
3018
|
-
const reply = await new Promise((resolve, reject) => {
|
|
3019
|
-
const timer = setTimeout(() => {
|
|
3020
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
3021
|
-
reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
|
|
3022
|
-
}, 120_000);
|
|
3023
|
-
session.pendingPermissionReplies.set(permissionId, {
|
|
3024
|
-
resolve,
|
|
3025
|
-
reject,
|
|
3026
|
-
timer,
|
|
3027
|
-
});
|
|
3028
|
-
const permissionRequest = {
|
|
3029
|
-
permissionId,
|
|
3030
|
-
description: typeof permissionParams["description"] === "string"
|
|
3031
|
-
? permissionParams["description"]
|
|
3032
|
-
: undefined,
|
|
3033
|
-
params: permissionParams,
|
|
3034
|
-
};
|
|
3035
|
-
for (const handler of session.permissionHandlers) {
|
|
3036
|
-
handler(permissionRequest);
|
|
3037
|
-
}
|
|
3038
|
-
});
|
|
3039
|
-
return this._buildAcpPermissionResult(reply, permissionParams);
|
|
3040
|
-
}
|
|
3041
4198
|
_acpParams(request) {
|
|
3042
4199
|
if (!request.params) {
|
|
3043
4200
|
return {};
|
|
@@ -3210,9 +4367,6 @@ export class AgentOs {
|
|
|
3210
4367
|
...(cwd ? { cwd } : {}),
|
|
3211
4368
|
...(cols !== undefined ? { cols: Math.trunc(cols) } : {}),
|
|
3212
4369
|
...(rows !== undefined ? { rows: Math.trunc(rows) } : {}),
|
|
3213
|
-
onStderr: (data) => {
|
|
3214
|
-
this._appendAcpTerminalOutput(terminal, data);
|
|
3215
|
-
},
|
|
3216
4370
|
}),
|
|
3217
4371
|
output: "",
|
|
3218
4372
|
truncated: false,
|
|
@@ -3230,12 +4384,12 @@ export class AgentOs {
|
|
|
3230
4384
|
this._acpTerminals.set(terminalId, terminal);
|
|
3231
4385
|
return { terminalId };
|
|
3232
4386
|
}
|
|
3233
|
-
_handleAcpWriteTerminal(params) {
|
|
4387
|
+
async _handleAcpWriteTerminal(params) {
|
|
3234
4388
|
const method = "terminal/write";
|
|
3235
4389
|
const terminal = this._requireAcpTerminal(params, method);
|
|
3236
4390
|
const data = this._requireAcpStringParam(params, "data", method);
|
|
3237
4391
|
const encoding = this._optionalAcpStringParam(params, "encoding", method);
|
|
3238
|
-
terminal.handle.write(encoding === "base64" ? Buffer.from(data, "base64") : data);
|
|
4392
|
+
await terminal.handle.write(encoding === "base64" ? Buffer.from(data, "base64") : data);
|
|
3239
4393
|
return null;
|
|
3240
4394
|
}
|
|
3241
4395
|
_handleAcpReadTerminal(params) {
|
|
@@ -3289,217 +4443,71 @@ export class AgentOs {
|
|
|
3289
4443
|
terminal.handle.resize(Math.trunc(cols), Math.trunc(rows));
|
|
3290
4444
|
return null;
|
|
3291
4445
|
}
|
|
3292
|
-
|
|
3293
|
-
const
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
if (
|
|
3298
|
-
|
|
3299
|
-
// (host-visible) so a forgotten onPermissionRequest handler is not an
|
|
3300
|
-
// invisible cause of an agent that cannot use any tool.
|
|
3301
|
-
this._warnNoPermissionHandlerOnce(session, params);
|
|
3302
|
-
return "reject";
|
|
3303
|
-
}
|
|
3304
|
-
try {
|
|
3305
|
-
return await new Promise((resolve, reject) => {
|
|
3306
|
-
const timer = setTimeout(() => {
|
|
3307
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
3308
|
-
reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
|
|
3309
|
-
}, 120_000);
|
|
3310
|
-
session.pendingPermissionReplies.set(permissionId, {
|
|
3311
|
-
resolve,
|
|
3312
|
-
reject,
|
|
3313
|
-
timer,
|
|
3314
|
-
});
|
|
3315
|
-
const permissionRequest = {
|
|
3316
|
-
permissionId,
|
|
3317
|
-
description: typeof params.description === "string"
|
|
3318
|
-
? params.description
|
|
3319
|
-
: undefined,
|
|
3320
|
-
params,
|
|
3321
|
-
};
|
|
3322
|
-
for (const handler of session.permissionHandlers) {
|
|
3323
|
-
handler(permissionRequest);
|
|
3324
|
-
}
|
|
3325
|
-
});
|
|
3326
|
-
}
|
|
3327
|
-
catch {
|
|
3328
|
-
return "reject";
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
/**
|
|
3332
|
-
* Gracefully destroy a session: cancel any pending work, close the client,
|
|
3333
|
-
* and remove from tracking. Unlike close() which is abrupt, this attempts
|
|
3334
|
-
* a graceful shutdown sequence.
|
|
3335
|
-
*/
|
|
3336
|
-
async destroySession(sessionId) {
|
|
3337
|
-
this._requireSession(sessionId);
|
|
3338
|
-
try {
|
|
3339
|
-
await this.cancelSession(sessionId);
|
|
4446
|
+
onSessionEvent(sessionIdOrHandler, maybeHandler) {
|
|
4447
|
+
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "main";
|
|
4448
|
+
const handler = typeof sessionIdOrHandler === "function"
|
|
4449
|
+
? sessionIdOrHandler
|
|
4450
|
+
: maybeHandler;
|
|
4451
|
+
if (!handler) {
|
|
4452
|
+
throw new TypeError("onSessionEvent requires a handler");
|
|
3340
4453
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
const session = this._requireSession(sessionId);
|
|
3349
|
-
let agentText = "";
|
|
3350
|
-
const handler = (event) => {
|
|
3351
|
-
const params = toRecord(event.params);
|
|
3352
|
-
const update = toRecord(params.update);
|
|
3353
|
-
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
3354
|
-
const content = toRecord(update.content);
|
|
3355
|
-
if (typeof content.text === "string") {
|
|
3356
|
-
agentText += content.text;
|
|
3357
|
-
}
|
|
4454
|
+
const handlers = this._durableSessionEventHandlers.get(sessionId) ?? new Set();
|
|
4455
|
+
handlers.add(handler);
|
|
4456
|
+
this._durableSessionEventHandlers.set(sessionId, handlers);
|
|
4457
|
+
return () => {
|
|
4458
|
+
handlers.delete(handler);
|
|
4459
|
+
if (handlers.size === 0) {
|
|
4460
|
+
this._durableSessionEventHandlers.delete(sessionId);
|
|
3358
4461
|
}
|
|
3359
4462
|
};
|
|
3360
|
-
const unsubscribe = this._subscribeSessionEvents(session, handler);
|
|
3361
|
-
try {
|
|
3362
|
-
const response = await this._sendSessionRequest(sessionId, "session/prompt", {
|
|
3363
|
-
prompt: [{ type: "text", text }],
|
|
3364
|
-
});
|
|
3365
|
-
return { response, text: agentText };
|
|
3366
|
-
}
|
|
3367
|
-
finally {
|
|
3368
|
-
unsubscribe();
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
/** Cancel ongoing agent work for a session. */
|
|
3372
|
-
async cancelSession(sessionId) {
|
|
3373
|
-
this._requireSession(sessionId);
|
|
3374
|
-
const cancelledPendingPrompt = this._cancelPendingPromptRequests(sessionId);
|
|
3375
|
-
if (cancelledPendingPrompt) {
|
|
3376
|
-
// Session control requests share the same framed sidecar transport as an
|
|
3377
|
-
// in-flight prompt request. If the adapter is blocked in prompt I/O, a
|
|
3378
|
-
// synchronous cancel RPC can wedge behind that prompt until the transport
|
|
3379
|
-
// timeout fires. Resolve the local prompt immediately, then let the sidecar
|
|
3380
|
-
// cancellation continue in the background as best effort.
|
|
3381
|
-
void this._sendSessionRequest(sessionId, "session/cancel").catch(() => { });
|
|
3382
|
-
return {
|
|
3383
|
-
jsonrpc: "2.0",
|
|
3384
|
-
id: null,
|
|
3385
|
-
result: {
|
|
3386
|
-
cancelled: true,
|
|
3387
|
-
requested: true,
|
|
3388
|
-
via: "prompt-fallback",
|
|
3389
|
-
},
|
|
3390
|
-
};
|
|
3391
|
-
}
|
|
3392
|
-
const response = await this._sendSessionRequest(sessionId, "session/cancel");
|
|
3393
|
-
if (response.error?.code === -32601) {
|
|
3394
|
-
return {
|
|
3395
|
-
jsonrpc: "2.0",
|
|
3396
|
-
id: null,
|
|
3397
|
-
result: {
|
|
3398
|
-
cancelled: false,
|
|
3399
|
-
requested: true,
|
|
3400
|
-
via: "unsupported",
|
|
3401
|
-
},
|
|
3402
|
-
};
|
|
3403
|
-
}
|
|
3404
|
-
return response;
|
|
3405
|
-
}
|
|
3406
|
-
closeSession(sessionId) {
|
|
3407
|
-
if (!this._sessions.has(sessionId) &&
|
|
3408
|
-
!this._closedSessionIds.has(sessionId) &&
|
|
3409
|
-
!this._sessionClosePromises.has(sessionId)) {
|
|
3410
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
3411
|
-
}
|
|
3412
|
-
const closePromise = this._closeSessionInternal(sessionId);
|
|
3413
|
-
// `closeSession()` is intentionally fire-and-forget; suppress unhandled
|
|
3414
|
-
// rejections here and let tracked close promises surface errors to any
|
|
3415
|
-
// internal/test callers awaiting `_sessionClosePromises`.
|
|
3416
|
-
void closePromise.catch(() => { });
|
|
3417
|
-
}
|
|
3418
|
-
async respondPermission(sessionId, permissionId, reply) {
|
|
3419
|
-
const session = this._requireSession(sessionId);
|
|
3420
|
-
const pendingReply = session.pendingPermissionReplies.get(permissionId);
|
|
3421
|
-
if (pendingReply) {
|
|
3422
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
3423
|
-
clearTimeout(pendingReply.timer);
|
|
3424
|
-
pendingReply.resolve(reply);
|
|
3425
|
-
return {
|
|
3426
|
-
jsonrpc: "2.0",
|
|
3427
|
-
id: null,
|
|
3428
|
-
result: {
|
|
3429
|
-
permissionId,
|
|
3430
|
-
reply,
|
|
3431
|
-
via: "sidecar-request",
|
|
3432
|
-
},
|
|
3433
|
-
};
|
|
3434
|
-
}
|
|
3435
|
-
return this._sendSessionRequest(sessionId, LEGACY_PERMISSION_METHOD, {
|
|
3436
|
-
permissionId,
|
|
3437
|
-
reply,
|
|
3438
|
-
});
|
|
3439
|
-
}
|
|
3440
|
-
async setSessionMode(sessionId, modeId) {
|
|
3441
|
-
return this._sendSessionRequest(sessionId, "session/set_mode", {
|
|
3442
|
-
modeId,
|
|
3443
|
-
});
|
|
3444
|
-
}
|
|
3445
|
-
getSessionModes(sessionId) {
|
|
3446
|
-
return this._requireSession(sessionId).modes;
|
|
3447
|
-
}
|
|
3448
|
-
async setSessionModel(sessionId, model) {
|
|
3449
|
-
return this._setSessionConfigByCategory(sessionId, "model", model);
|
|
3450
|
-
}
|
|
3451
|
-
async setSessionThoughtLevel(sessionId, level) {
|
|
3452
|
-
return this._setSessionConfigByCategory(sessionId, "thought_level", level);
|
|
3453
|
-
}
|
|
3454
|
-
getSessionConfigOptions(sessionId) {
|
|
3455
|
-
return [...this._requireSession(sessionId).configOptions];
|
|
3456
|
-
}
|
|
3457
|
-
getSessionCapabilities(sessionId) {
|
|
3458
|
-
const caps = this._requireSession(sessionId).capabilities;
|
|
3459
|
-
return Object.keys(caps).length > 0 ? caps : null;
|
|
3460
|
-
}
|
|
3461
|
-
getSessionAgentInfo(sessionId) {
|
|
3462
|
-
return this._requireSession(sessionId).agentInfo;
|
|
3463
|
-
}
|
|
3464
|
-
async rawSessionSend(sessionId, method, params) {
|
|
3465
|
-
return this._sendSessionRequest(sessionId, method, params);
|
|
3466
|
-
}
|
|
3467
|
-
async rawSend(sessionId, method, params) {
|
|
3468
|
-
return this.rawSessionSend(sessionId, method, params);
|
|
3469
|
-
}
|
|
3470
|
-
onSessionEvent(sessionId, handler) {
|
|
3471
|
-
const session = this._requireSession(sessionId);
|
|
3472
|
-
return this._subscribeSessionEvents(session, handler);
|
|
3473
4463
|
}
|
|
3474
|
-
|
|
3475
|
-
const
|
|
3476
|
-
|
|
4464
|
+
onAgentExit(sessionIdOrHandler, maybeHandler) {
|
|
4465
|
+
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "*";
|
|
4466
|
+
const handler = typeof sessionIdOrHandler === "function"
|
|
4467
|
+
? sessionIdOrHandler
|
|
4468
|
+
: maybeHandler;
|
|
4469
|
+
if (!handler)
|
|
4470
|
+
throw new TypeError("onAgentExit requires a handler");
|
|
4471
|
+
const handlers = this._agentExitHandlers.get(sessionId) ?? new Set();
|
|
4472
|
+
handlers.add(handler);
|
|
4473
|
+
this._agentExitHandlers.set(sessionId, handlers);
|
|
3477
4474
|
return () => {
|
|
3478
|
-
|
|
4475
|
+
handlers.delete(handler);
|
|
4476
|
+
if (handlers.size === 0)
|
|
4477
|
+
this._agentExitHandlers.delete(sessionId);
|
|
3479
4478
|
};
|
|
3480
4479
|
}
|
|
3481
4480
|
// ── Cron ────────────────────────────────────────────────────
|
|
3482
4481
|
/** Schedule a cron job. Returns a handle with the job ID and a cancel method. */
|
|
3483
|
-
|
|
4482
|
+
_scheduleCron(options) {
|
|
3484
4483
|
return this._cronManager.schedule(options);
|
|
3485
4484
|
}
|
|
3486
4485
|
/** List all registered cron jobs. */
|
|
3487
|
-
|
|
4486
|
+
_listCronJobs() {
|
|
3488
4487
|
return this._cronManager.list();
|
|
3489
4488
|
}
|
|
3490
4489
|
/** Cancel a cron job by ID. */
|
|
3491
|
-
|
|
4490
|
+
_cancelCronJob(id) {
|
|
3492
4491
|
this._cronManager.cancel(id);
|
|
3493
4492
|
}
|
|
4493
|
+
/** @deprecated Use `cron.schedule()`. */
|
|
4494
|
+
scheduleCron(options) {
|
|
4495
|
+
return this.cron.schedule(options);
|
|
4496
|
+
}
|
|
4497
|
+
/** @deprecated Use `cron.list()`. */
|
|
4498
|
+
listCronJobs() {
|
|
4499
|
+
return this.cron.list();
|
|
4500
|
+
}
|
|
4501
|
+
/** @deprecated Use `cron.cancel()`. */
|
|
4502
|
+
cancelCronJob(id) {
|
|
4503
|
+
this.cron.cancel(id);
|
|
4504
|
+
}
|
|
3494
4505
|
/** Subscribe to cron lifecycle events (fire, complete, error). */
|
|
3495
4506
|
onCronEvent(handler) {
|
|
3496
4507
|
this._cronManager.onEvent(handler);
|
|
3497
4508
|
}
|
|
3498
4509
|
async dispose() {
|
|
3499
4510
|
this._cronManager.dispose();
|
|
3500
|
-
for (const sessionId of [...this._sessions.keys()]) {
|
|
3501
|
-
await this._closeSessionInternal(sessionId).catch(() => { });
|
|
3502
|
-
}
|
|
3503
4511
|
for (const [id, entry] of this._shells) {
|
|
3504
4512
|
entry.handle.kill();
|
|
3505
4513
|
}
|
|
@@ -3512,14 +4520,30 @@ export class AgentOs {
|
|
|
3512
4520
|
}
|
|
3513
4521
|
this._acpTerminals.clear();
|
|
3514
4522
|
this._processes.clear();
|
|
4523
|
+
this._executionOutputHandlers.clear();
|
|
4524
|
+
this._executionCompletedHandlers.clear();
|
|
3515
4525
|
await waitForTrackedExitPromises([...shellExitPromises, ...terminalExitPromises], SHELL_DISPOSE_TIMEOUT_MS);
|
|
3516
4526
|
this._disposeSidecarEventListener();
|
|
3517
4527
|
const sidecarLease = this._sidecarLease;
|
|
3518
4528
|
this._sidecarLease = null;
|
|
3519
|
-
|
|
3520
|
-
|
|
4529
|
+
const disposeVm = sidecarLease
|
|
4530
|
+
? sidecarLease.dispose()
|
|
4531
|
+
: this.#kernel.dispose();
|
|
4532
|
+
const hooks = this._disposeHooks.splice(0);
|
|
4533
|
+
const errors = [];
|
|
4534
|
+
try {
|
|
4535
|
+
await disposeVm;
|
|
4536
|
+
}
|
|
4537
|
+
catch (error) {
|
|
4538
|
+
errors.push(error);
|
|
4539
|
+
}
|
|
4540
|
+
const hookResults = await Promise.allSettled(hooks.map((hook) => hook()));
|
|
4541
|
+
errors.push(...hookResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []));
|
|
4542
|
+
if (errors.length === 1)
|
|
4543
|
+
throw errors[0];
|
|
4544
|
+
if (errors.length > 1) {
|
|
4545
|
+
throw new AggregateError(errors, "AgentOS VM disposal failed");
|
|
3521
4546
|
}
|
|
3522
|
-
return this.#kernel.dispose();
|
|
3523
4547
|
}
|
|
3524
4548
|
}
|
|
3525
4549
|
const agentOsRuntimeAdmins = new WeakMap();
|
|
@@ -3583,7 +4607,17 @@ function applySharedSidecarHold(state) {
|
|
|
3583
4607
|
if (!child)
|
|
3584
4608
|
return;
|
|
3585
4609
|
const hold = (state.eventLoopHolds ?? 0) > 0;
|
|
3586
|
-
|
|
4610
|
+
// Include the complete stdio array, not just the named fd0-fd2 aliases.
|
|
4611
|
+
// Protocol v8 carries responses over fd3; leaving that control socket
|
|
4612
|
+
// referenced pins Node's event loop after the final VM lease is disposed.
|
|
4613
|
+
const handles = new Set([
|
|
4614
|
+
child,
|
|
4615
|
+
child.stdin,
|
|
4616
|
+
child.stdout,
|
|
4617
|
+
child.stderr,
|
|
4618
|
+
...(child.stdio ?? []),
|
|
4619
|
+
]);
|
|
4620
|
+
for (const handle of handles) {
|
|
3587
4621
|
if (!handle)
|
|
3588
4622
|
continue;
|
|
3589
4623
|
try {
|
|
@@ -3790,7 +4824,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3790
4824
|
}
|
|
3791
4825
|
let transport;
|
|
3792
4826
|
const client = createAgentOsSidecarClient({
|
|
3793
|
-
async
|
|
4827
|
+
async createOwnershipTransport(sessionBootstrap) {
|
|
3794
4828
|
transport = await createInProcessSidecarTransport(sessionBootstrap, options);
|
|
3795
4829
|
return transport;
|
|
3796
4830
|
},
|
|
@@ -3810,7 +4844,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3810
4844
|
let disposed = false;
|
|
3811
4845
|
let leaseRecord;
|
|
3812
4846
|
try {
|
|
3813
|
-
const session = await client.
|
|
4847
|
+
const session = await client.createOwnershipSession({
|
|
3814
4848
|
placement: cloneSidecarPlacement(state.description.placement),
|
|
3815
4849
|
});
|
|
3816
4850
|
const vm = await session.createVm();
|