@rivet-dev/agentos-core 0.0.0-abc-fixes-1.d8c4d95 → 0.0.0-agent-acp-codex-session-benchmark.d578a31
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 +48 -48
- package/dist/agent-os.d.ts +215 -161
- package/dist/agent-os.js +1300 -2576
- package/dist/agentos-package.d.ts +27 -0
- package/dist/agentos-package.js +21 -0
- package/dist/base-filesystem.d.ts +1 -16
- package/dist/base-filesystem.js +1 -173
- package/dist/cron/cron-manager.d.ts +39 -14
- package/dist/cron/cron-manager.js +289 -128
- package/dist/cron/errors.d.ts +8 -0
- package/dist/cron/errors.js +16 -0
- package/dist/cron/index.d.ts +1 -3
- package/dist/cron/index.js +1 -2
- package/dist/cron/types.d.ts +2 -2
- package/dist/default-software.d.ts +8 -0
- package/dist/default-software.js +10 -0
- package/dist/filesystem-snapshot.d.ts +1 -1
- package/dist/filesystem-snapshot.js +1 -1
- package/dist/generated/AcpLimitsConfig.d.ts +4 -0
- package/dist/generated/AcpLimitsConfig.js +2 -0
- package/dist/generated/CreateVmConfig.d.ts +29 -0
- package/dist/generated/FsPermissionRule.d.ts +6 -0
- package/dist/generated/FsPermissionRule.js +1 -0
- package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
- package/dist/generated/FsPermissionRuleSet.js +1 -0
- package/dist/generated/FsPermissionScope.d.ts +3 -0
- package/dist/generated/FsPermissionScope.js +1 -0
- package/dist/generated/HttpLimitsConfig.d.ts +3 -0
- package/dist/generated/HttpLimitsConfig.js +2 -0
- package/dist/generated/JsModuleResolution.d.ts +1 -0
- package/dist/generated/JsModuleResolution.js +2 -0
- package/dist/generated/JsRuntimeConfig.d.ts +31 -0
- package/dist/generated/JsRuntimeConfig.js +1 -0
- package/dist/generated/JsRuntimeLimitsConfig.d.ts +8 -0
- package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
- package/dist/generated/JsRuntimePlatform.d.ts +1 -0
- package/dist/generated/JsRuntimePlatform.js +2 -0
- package/dist/generated/MountPluginDescriptor.d.ts +4 -0
- package/dist/generated/MountPluginDescriptor.js +2 -0
- package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
- package/dist/generated/NativeRootFilesystemConfig.js +1 -0
- package/dist/generated/PatternPermissionRule.d.ts +6 -0
- package/dist/generated/PatternPermissionRule.js +1 -0
- package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
- package/dist/generated/PatternPermissionRuleSet.js +1 -0
- package/dist/generated/PatternPermissionScope.d.ts +3 -0
- package/dist/generated/PatternPermissionScope.js +1 -0
- package/dist/generated/PermissionMode.d.ts +1 -0
- package/dist/generated/PermissionMode.js +2 -0
- package/dist/generated/PermissionsPolicy.d.ts +10 -0
- package/dist/generated/PermissionsPolicy.js +1 -0
- package/dist/generated/PluginLimitsConfig.d.ts +4 -0
- package/dist/generated/PluginLimitsConfig.js +2 -0
- package/dist/generated/PythonLimitsConfig.d.ts +6 -0
- package/dist/generated/PythonLimitsConfig.js +2 -0
- package/dist/generated/ResourceLimitsConfig.d.ts +25 -0
- package/dist/generated/ResourceLimitsConfig.js +2 -0
- package/dist/generated/RootFilesystemConfig.d.ts +9 -0
- package/dist/generated/RootFilesystemConfig.js +1 -0
- package/dist/generated/RootFilesystemEntry.d.ts +13 -0
- package/dist/generated/RootFilesystemEntry.js +1 -0
- package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
- package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
- package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
- package/dist/generated/RootFilesystemEntryKind.js +2 -0
- package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
- package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
- package/dist/generated/RootFilesystemMode.d.ts +1 -0
- package/dist/generated/RootFilesystemMode.js +2 -0
- package/dist/generated/ToolLimitsConfig.d.ts +10 -0
- package/dist/generated/ToolLimitsConfig.js +2 -0
- package/dist/generated/VmDnsConfig.d.ts +6 -0
- package/dist/generated/VmDnsConfig.js +2 -0
- package/dist/generated/VmLimitsConfig.d.ts +18 -0
- package/dist/generated/VmLimitsConfig.js +1 -0
- package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
- package/dist/generated/VmListenPolicyConfig.js +2 -0
- package/dist/generated/WasmLimitsConfig.d.ts +7 -0
- package/dist/generated/WasmLimitsConfig.js +2 -0
- package/dist/host-dir-mount.d.ts +5 -5
- package/dist/host-dir-mount.js +4 -4
- package/dist/host-tools-zod.js +27 -7
- package/dist/host-tools.d.ts +2 -9
- package/dist/host-tools.js +0 -18
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -4
- package/dist/layers.d.ts +3 -3
- package/dist/layers.js +19 -10
- package/dist/memory-filesystem.d.ts +39 -0
- package/dist/memory-filesystem.js +375 -0
- package/dist/options-schema.d.ts +452 -0
- package/dist/options-schema.js +269 -0
- package/dist/overlay-filesystem.d.ts +1 -1
- package/dist/overlay-filesystem.js +3 -3
- package/dist/packages.d.ts +10 -123
- package/dist/packages.js +3 -329
- package/dist/runtime.d.ts +41 -184
- package/dist/sidecar/agentos-protocol.d.ts +232 -29
- package/dist/sidecar/agentos-protocol.js +456 -120
- package/dist/sidecar/binary.d.ts +2 -2
- package/dist/sidecar/binary.js +2 -2
- package/dist/sidecar/native-process-client.d.ts +5 -3
- package/dist/sidecar/native-process-client.js +5 -3
- package/dist/sidecar/permissions.d.ts +4 -3
- package/dist/sidecar/permissions.js +6 -23
- package/dist/sidecar/rpc-client.d.ts +74 -86
- package/dist/sidecar/rpc-client.js +422 -1418
- package/dist/test/file-system.d.ts +1 -1
- package/dist/test/file-system.js +3 -1
- package/dist/test/runtime.d.ts +2 -3
- package/dist/test/runtime.js +1 -1
- package/dist/test/terminal-harness.d.ts +5 -4
- package/dist/test/terminal-harness.js +12 -7
- package/dist/types.d.ts +12 -5
- package/package.json +37 -41
- package/dist/agents.d.ts +0 -63
- package/dist/agents.js +0 -42
- package/dist/cron/parse-schedule.d.ts +0 -25
- package/dist/cron/parse-schedule.js +0 -66
- package/dist/cron/schedule-driver.d.ts +0 -19
- package/dist/cron/timer-driver.d.ts +0 -17
- package/dist/cron/timer-driver.js +0 -61
- package/dist/runtime-compat.d.ts +0 -475
- package/dist/runtime-compat.js +0 -2010
- package/dist/sidecar/cargo.d.ts +0 -2
- package/dist/sidecar/cargo.js +0 -142
- package/fixtures/AGENTOS_SYSTEM_PROMPT.md +0 -21
- /package/dist/{cron/schedule-driver.js → generated/CreateVmConfig.js} +0 -0
package/dist/agent-os.js
CHANGED
|
@@ -1,35 +1,11 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { dirname, join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import commonSoftware from "@agentos-software/common";
|
|
8
|
-
import { validateToolkits } from "./host-tools.js";
|
|
2
|
+
import { posix as posixPath } from "node:path";
|
|
9
3
|
import { zodToJsonSchema } from "./host-tools-zod.js";
|
|
4
|
+
import { parseAgentOsOptions } from "./options-schema.js";
|
|
10
5
|
import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
|
|
11
|
-
import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
|
|
12
6
|
export { isAcpTimeoutErrorData } from "./json-rpc.js";
|
|
13
|
-
const ACP_PROTOCOL_VERSION = 1;
|
|
14
7
|
const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
|
|
15
8
|
const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
|
|
16
|
-
/**
|
|
17
|
-
* Reserved `env` key on `AcpResumeSessionRequest` carrying the resolved adapter
|
|
18
|
-
* bin entrypoint. The resume wire request omits a dedicated `adapterEntrypoint`
|
|
19
|
-
* field; the sidecar reads the entrypoint from this key and strips it before
|
|
20
|
-
* launching the adapter. Must stay in sync with the sidecar constant of the same
|
|
21
|
-
* name in `crates/agentos-sidecar/src/acp_extension.rs`.
|
|
22
|
-
*/
|
|
23
|
-
const RESUME_ADAPTER_ENTRYPOINT_ENV = "AGENT_OS_RESUME_ADAPTER_ENTRYPOINT";
|
|
24
|
-
function defaultAcpClientCapabilities() {
|
|
25
|
-
return {
|
|
26
|
-
fs: {
|
|
27
|
-
readTextFile: true,
|
|
28
|
-
writeTextFile: true,
|
|
29
|
-
},
|
|
30
|
-
terminal: true,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
9
|
async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
34
10
|
if (promises.length === 0) {
|
|
35
11
|
return;
|
|
@@ -41,31 +17,21 @@ async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
|
41
17
|
}),
|
|
42
18
|
]);
|
|
43
19
|
}
|
|
44
|
-
import { AGENT_CONFIGS } from "./agents.js";
|
|
45
|
-
import { getBaseEnvironment, getBaseFilesystemEntries, } from "./base-filesystem.js";
|
|
46
20
|
import { CronManager } from "./cron/cron-manager.js";
|
|
47
|
-
import {
|
|
48
|
-
import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
|
|
49
|
-
import { createHostDirBackend } from "./host-dir-mount.js";
|
|
21
|
+
import { resolveDefaultSoftware } from "./default-software.js";
|
|
50
22
|
import { serializeMountConfigForSidecar, } from "./js-bridge.js";
|
|
51
23
|
import { createSnapshotExport, } from "./layers.js";
|
|
52
|
-
import {
|
|
53
|
-
import { allowAll } from "./runtime-compat.js";
|
|
54
|
-
import { AcpRuntimeKind, decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
|
|
24
|
+
import { decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
|
|
55
25
|
import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
|
|
56
|
-
import { createAgentOsSidecarClient,
|
|
26
|
+
import { createAgentOsSidecarClient, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
|
|
27
|
+
function defaultAgentStderrHandler(event) {
|
|
28
|
+
process.stderr.write(event.chunk);
|
|
29
|
+
}
|
|
30
|
+
function defaultAgentExitHandler(event) {
|
|
31
|
+
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"} restart=${event.restart} (${event.restartCount}/${event.maxRestarts})\n`);
|
|
32
|
+
}
|
|
57
33
|
const LEGACY_PERMISSION_METHOD = "request/permission";
|
|
58
34
|
const ACP_PERMISSION_METHOD = "session/request_permission";
|
|
59
|
-
class AcpDispatchError extends Error {
|
|
60
|
-
code;
|
|
61
|
-
data;
|
|
62
|
-
constructor(code, message, data) {
|
|
63
|
-
super(message);
|
|
64
|
-
this.name = "AcpDispatchError";
|
|
65
|
-
this.code = code;
|
|
66
|
-
this.data = data;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
35
|
function toJsonRpcNotification(value) {
|
|
70
36
|
if (!value ||
|
|
71
37
|
typeof value !== "object" ||
|
|
@@ -88,24 +54,24 @@ function toJsonRpcResponse(value) {
|
|
|
88
54
|
}
|
|
89
55
|
return value;
|
|
90
56
|
}
|
|
91
|
-
function toJsonRpcRequest(value) {
|
|
92
|
-
if (!value ||
|
|
93
|
-
typeof value !== "object" ||
|
|
94
|
-
Array.isArray(value) ||
|
|
95
|
-
value.jsonrpc !== "2.0" ||
|
|
96
|
-
!(typeof value.id === "number" ||
|
|
97
|
-
typeof value.id === "string" ||
|
|
98
|
-
value.id === null) ||
|
|
99
|
-
typeof value.method !== "string") {
|
|
100
|
-
throw new Error("Invalid JSON-RPC request from ACP callback");
|
|
101
|
-
}
|
|
102
|
-
return value;
|
|
103
|
-
}
|
|
104
57
|
function toRecord(value) {
|
|
105
58
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
106
59
|
? value
|
|
107
60
|
: {};
|
|
108
61
|
}
|
|
62
|
+
function normalizePackageRef(value) {
|
|
63
|
+
// The single package reference is `packagePath`: the packed `.aospkg` file
|
|
64
|
+
// (registry-built packages export `{ packagePath }`), or a package dir for
|
|
65
|
+
// local transition fixtures. A raw string is shorthand for the same path.
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
return { path: value };
|
|
68
|
+
}
|
|
69
|
+
const record = toRecord(value);
|
|
70
|
+
if (typeof record.packagePath === "string") {
|
|
71
|
+
return { path: record.packagePath };
|
|
72
|
+
}
|
|
73
|
+
throw new TypeError("Invalid software package reference: expected a path string or { packagePath: string }");
|
|
74
|
+
}
|
|
109
75
|
function parseAcpJson(value, context) {
|
|
110
76
|
if (value === null) {
|
|
111
77
|
return undefined;
|
|
@@ -120,16 +86,6 @@ function parseAcpJson(value, context) {
|
|
|
120
86
|
function parseAcpJsonList(values, context) {
|
|
121
87
|
return values.map((value, index) => parseAcpJson(value, `${context}[${index}]`));
|
|
122
88
|
}
|
|
123
|
-
function sidecarSessionCreatedFromAcp(response) {
|
|
124
|
-
return {
|
|
125
|
-
sessionId: response.sessionId,
|
|
126
|
-
...(response.pid !== null ? { pid: response.pid } : {}),
|
|
127
|
-
modes: parseAcpJson(response.modes, "modes"),
|
|
128
|
-
configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
|
|
129
|
-
agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
|
|
130
|
-
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
89
|
function sidecarSessionStateFromAcp(response) {
|
|
134
90
|
return {
|
|
135
91
|
sessionId: response.sessionId,
|
|
@@ -143,47 +99,6 @@ function sidecarSessionStateFromAcp(response) {
|
|
|
143
99
|
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
144
100
|
};
|
|
145
101
|
}
|
|
146
|
-
function isLocalCancelledPromptResponse(method, response) {
|
|
147
|
-
const result = toRecord(response.result);
|
|
148
|
-
return (method === "session/prompt" &&
|
|
149
|
-
response.id === null &&
|
|
150
|
-
response.error === undefined &&
|
|
151
|
-
result.stopReason === "cancelled");
|
|
152
|
-
}
|
|
153
|
-
const CLOSED_SESSION_ID_RETENTION_LIMIT = 2048;
|
|
154
|
-
const CLOSED_SHELL_ID_RETENTION_LIMIT = 2048;
|
|
155
|
-
class BoundedSet {
|
|
156
|
-
limit;
|
|
157
|
-
#entries = new Map();
|
|
158
|
-
constructor(limit) {
|
|
159
|
-
if (!Number.isInteger(limit) || limit <= 0) {
|
|
160
|
-
throw new Error(`BoundedSet limit must be a positive integer: ${limit}`);
|
|
161
|
-
}
|
|
162
|
-
this.limit = limit;
|
|
163
|
-
}
|
|
164
|
-
add(value) {
|
|
165
|
-
if (this.#entries.has(value)) {
|
|
166
|
-
this.#entries.delete(value);
|
|
167
|
-
}
|
|
168
|
-
this.#entries.set(value, undefined);
|
|
169
|
-
if (this.#entries.size <= this.limit) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const oldest = this.#entries.keys().next();
|
|
173
|
-
if (!oldest.done) {
|
|
174
|
-
this.#entries.delete(oldest.value);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
has(value) {
|
|
178
|
-
return this.#entries.has(value);
|
|
179
|
-
}
|
|
180
|
-
delete(value) {
|
|
181
|
-
return this.#entries.delete(value);
|
|
182
|
-
}
|
|
183
|
-
get size() {
|
|
184
|
-
return this.#entries.size;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
102
|
function shouldDispatchToSessionEventHandlers(notification) {
|
|
188
103
|
return notification.method === "session/update";
|
|
189
104
|
}
|
|
@@ -211,20 +126,15 @@ function toAgentInfo(value) {
|
|
|
211
126
|
}
|
|
212
127
|
return value;
|
|
213
128
|
}
|
|
214
|
-
function
|
|
129
|
+
function sessionEntryFromRoute(response) {
|
|
215
130
|
return {
|
|
216
|
-
sessionId,
|
|
217
|
-
agentType,
|
|
218
|
-
processId:
|
|
219
|
-
pid:
|
|
220
|
-
closed: false,
|
|
221
|
-
modes: initData.modes ?? null,
|
|
222
|
-
configOptions: initData.configOptions ?? [],
|
|
223
|
-
capabilities: initData.capabilities ?? {},
|
|
224
|
-
agentInfo: initData.agentInfo ?? null,
|
|
131
|
+
sessionId: response.sessionId,
|
|
132
|
+
agentType: response.agentType,
|
|
133
|
+
processId: response.processId,
|
|
134
|
+
pid: response.pid,
|
|
225
135
|
eventHandlers: new Set(),
|
|
226
136
|
permissionHandlers: new Set(),
|
|
227
|
-
|
|
137
|
+
warnedNoPermissionHandler: false,
|
|
228
138
|
pendingPermissionReplies: new Map(),
|
|
229
139
|
};
|
|
230
140
|
}
|
|
@@ -234,502 +144,41 @@ function isOverlayMountConfig(config) {
|
|
|
234
144
|
function isNativeMountConfig(config) {
|
|
235
145
|
return "plugin" in config;
|
|
236
146
|
}
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return {};
|
|
242
|
-
}
|
|
243
|
-
function getHostDirMountPluginConfig(config) {
|
|
244
|
-
const object = asMountConfigJsonObject(config);
|
|
245
|
-
if (typeof object.hostPath !== "string") {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
const hostPathConfig = {
|
|
249
|
-
hostPath: object.hostPath,
|
|
250
|
-
};
|
|
251
|
-
if (typeof object.readOnly === "boolean") {
|
|
252
|
-
hostPathConfig.readOnly = object.readOnly;
|
|
253
|
-
}
|
|
254
|
-
return hostPathConfig;
|
|
255
|
-
}
|
|
256
|
-
function getSandboxAgentMountPluginConfig(config) {
|
|
257
|
-
const object = asMountConfigJsonObject(config);
|
|
258
|
-
if (typeof object.baseUrl !== "string") {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
const sandboxConfig = {
|
|
262
|
-
baseUrl: object.baseUrl,
|
|
263
|
-
};
|
|
264
|
-
if (typeof object.token === "string") {
|
|
265
|
-
sandboxConfig.token = object.token;
|
|
266
|
-
}
|
|
267
|
-
if (typeof object.basePath === "string") {
|
|
268
|
-
sandboxConfig.basePath = object.basePath;
|
|
269
|
-
}
|
|
270
|
-
if (typeof object.timeoutMs === "number") {
|
|
271
|
-
sandboxConfig.timeoutMs = object.timeoutMs;
|
|
272
|
-
}
|
|
273
|
-
if (typeof object.maxFullReadBytes === "number") {
|
|
274
|
-
sandboxConfig.maxFullReadBytes = object.maxFullReadBytes;
|
|
275
|
-
}
|
|
276
|
-
if (object.headers &&
|
|
277
|
-
typeof object.headers === "object" &&
|
|
278
|
-
!Array.isArray(object.headers)) {
|
|
279
|
-
const headers = Object.entries(object.headers)
|
|
280
|
-
.filter(([, value]) => typeof value === "string")
|
|
281
|
-
.map(([name, value]) => [name, value]);
|
|
282
|
-
if (headers.length > 0) {
|
|
283
|
-
sandboxConfig.headers = Object.fromEntries(headers);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return sandboxConfig;
|
|
287
|
-
}
|
|
288
|
-
function getS3MountPluginConfig(config) {
|
|
289
|
-
const object = asMountConfigJsonObject(config);
|
|
290
|
-
if (typeof object.bucket !== "string") {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
const s3Config = {
|
|
294
|
-
bucket: object.bucket,
|
|
295
|
-
};
|
|
296
|
-
if (typeof object.prefix === "string") {
|
|
297
|
-
s3Config.prefix = object.prefix;
|
|
298
|
-
}
|
|
299
|
-
if (typeof object.region === "string") {
|
|
300
|
-
s3Config.region = object.region;
|
|
301
|
-
}
|
|
302
|
-
if (typeof object.endpoint === "string") {
|
|
303
|
-
s3Config.endpoint = object.endpoint;
|
|
304
|
-
}
|
|
305
|
-
if (typeof object.chunkSize === "number") {
|
|
306
|
-
s3Config.chunkSize = object.chunkSize;
|
|
307
|
-
}
|
|
308
|
-
if (typeof object.inlineThreshold === "number") {
|
|
309
|
-
s3Config.inlineThreshold = object.inlineThreshold;
|
|
310
|
-
}
|
|
311
|
-
if (object.credentials &&
|
|
312
|
-
typeof object.credentials === "object" &&
|
|
313
|
-
!Array.isArray(object.credentials) &&
|
|
314
|
-
typeof object.credentials.accessKeyId === "string" &&
|
|
315
|
-
typeof object.credentials.secretAccessKey === "string") {
|
|
316
|
-
s3Config.credentials = {
|
|
317
|
-
accessKeyId: object.credentials.accessKeyId,
|
|
318
|
-
secretAccessKey: object.credentials.secretAccessKey,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
return s3Config;
|
|
322
|
-
}
|
|
323
|
-
function getGoogleDriveMountPluginConfig(config) {
|
|
324
|
-
const object = asMountConfigJsonObject(config);
|
|
325
|
-
if (typeof object.folderId !== "string") {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
if (!object.credentials ||
|
|
329
|
-
typeof object.credentials !== "object" ||
|
|
330
|
-
Array.isArray(object.credentials) ||
|
|
331
|
-
typeof object.credentials.clientEmail !== "string" ||
|
|
332
|
-
typeof object.credentials.privateKey !== "string") {
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
const googleDriveConfig = {
|
|
336
|
-
credentials: {
|
|
337
|
-
clientEmail: object.credentials.clientEmail,
|
|
338
|
-
privateKey: object.credentials.privateKey,
|
|
339
|
-
},
|
|
340
|
-
folderId: object.folderId,
|
|
341
|
-
};
|
|
342
|
-
if (typeof object.keyPrefix === "string") {
|
|
343
|
-
googleDriveConfig.keyPrefix = object.keyPrefix;
|
|
344
|
-
}
|
|
345
|
-
if (typeof object.chunkSize === "number") {
|
|
346
|
-
googleDriveConfig.chunkSize = object.chunkSize;
|
|
347
|
-
}
|
|
348
|
-
if (typeof object.inlineThreshold === "number") {
|
|
349
|
-
googleDriveConfig.inlineThreshold = object.inlineThreshold;
|
|
350
|
-
}
|
|
351
|
-
return googleDriveConfig;
|
|
352
|
-
}
|
|
353
|
-
const KERNEL_POSIX_BOOTSTRAP_DIRS = [
|
|
354
|
-
"/dev",
|
|
355
|
-
"/proc",
|
|
356
|
-
"/tmp",
|
|
357
|
-
"/bin",
|
|
358
|
-
"/lib",
|
|
359
|
-
"/sbin",
|
|
360
|
-
"/boot",
|
|
361
|
-
"/etc",
|
|
362
|
-
"/root",
|
|
363
|
-
"/run",
|
|
364
|
-
"/srv",
|
|
365
|
-
"/sys",
|
|
366
|
-
"/opt",
|
|
367
|
-
"/mnt",
|
|
368
|
-
"/media",
|
|
369
|
-
"/home",
|
|
370
|
-
"/home/agentos",
|
|
371
|
-
"/workspace",
|
|
372
|
-
"/usr",
|
|
373
|
-
"/usr/bin",
|
|
374
|
-
"/usr/games",
|
|
375
|
-
"/usr/include",
|
|
376
|
-
"/usr/lib",
|
|
377
|
-
"/usr/libexec",
|
|
378
|
-
"/usr/man",
|
|
379
|
-
"/usr/local",
|
|
380
|
-
"/usr/local/bin",
|
|
381
|
-
"/usr/sbin",
|
|
382
|
-
"/usr/share",
|
|
383
|
-
"/usr/share/man",
|
|
384
|
-
"/var",
|
|
385
|
-
"/var/cache",
|
|
386
|
-
"/var/empty",
|
|
387
|
-
"/var/lib",
|
|
388
|
-
"/var/lock",
|
|
389
|
-
"/var/log",
|
|
390
|
-
"/var/run",
|
|
391
|
-
"/var/spool",
|
|
392
|
-
"/var/tmp",
|
|
393
|
-
"/etc/agentos",
|
|
394
|
-
];
|
|
395
|
-
const NODE_RUNTIME_BOOTSTRAP_COMMANDS = ["node", "npm", "npx"];
|
|
396
|
-
const KERNEL_COMMAND_STUB = "#!/bin/sh\n# kernel command stub\n";
|
|
397
|
-
const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
|
|
398
|
-
const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-sidecar");
|
|
399
|
-
const SIDECAR_BUILD_INPUTS = [
|
|
400
|
-
join(REPO_ROOT, "Cargo.toml"),
|
|
401
|
-
join(REPO_ROOT, "Cargo.lock"),
|
|
402
|
-
join(REPO_ROOT, "crates/bridge"),
|
|
403
|
-
join(REPO_ROOT, "crates/execution"),
|
|
404
|
-
join(REPO_ROOT, "crates/kernel"),
|
|
405
|
-
join(REPO_ROOT, "crates/sidecar"),
|
|
406
|
-
];
|
|
407
|
-
let ensuredSidecarBinary = null;
|
|
408
|
-
function isWasmBinaryFile(path) {
|
|
409
|
-
try {
|
|
410
|
-
const header = readFileSync(path);
|
|
411
|
-
return (header.length >= 4 &&
|
|
412
|
-
header[0] === 0x00 &&
|
|
413
|
-
header[1] === 0x61 &&
|
|
414
|
-
header[2] === 0x73 &&
|
|
415
|
-
header[3] === 0x6d);
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
function collectBootstrapWasmCommands(commandDirs) {
|
|
422
|
-
const commands = [];
|
|
423
|
-
const seen = new Set();
|
|
424
|
-
for (const dir of commandDirs) {
|
|
425
|
-
let entries;
|
|
426
|
-
try {
|
|
427
|
-
entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
|
|
428
|
-
}
|
|
429
|
-
catch {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
for (const entry of entries) {
|
|
433
|
-
if (entry.startsWith(".")) {
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
const fullPath = join(dir, entry);
|
|
437
|
-
try {
|
|
438
|
-
if (statSync(fullPath).isDirectory()) {
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
if (!isWasmBinaryFile(fullPath) || seen.has(entry)) {
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
seen.add(entry);
|
|
449
|
-
commands.push(entry);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return commands;
|
|
453
|
-
}
|
|
454
|
-
function resolveDeclaredCommandSource(commandDir, commandName, aliases) {
|
|
455
|
-
let current = commandName;
|
|
456
|
-
const visited = new Set();
|
|
457
|
-
while (!visited.has(current)) {
|
|
458
|
-
visited.add(current);
|
|
459
|
-
const candidatePath = join(commandDir, current);
|
|
460
|
-
if (isWasmBinaryFile(candidatePath)) {
|
|
461
|
-
return candidatePath;
|
|
462
|
-
}
|
|
463
|
-
const next = aliases[current];
|
|
464
|
-
if (!next) {
|
|
465
|
-
return null;
|
|
466
|
-
}
|
|
467
|
-
current = next;
|
|
468
|
-
}
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
function prepareCommandDirs(commandPackages) {
|
|
472
|
-
const commandDirs = [];
|
|
473
|
-
const tempDirs = [];
|
|
474
|
-
try {
|
|
475
|
-
for (const commandPackage of commandPackages) {
|
|
476
|
-
commandDirs.push(commandPackage.commandDir);
|
|
477
|
-
const aliasEntries = Object.entries(commandPackage.aliases)
|
|
478
|
-
.sort(([leftAlias], [rightAlias]) => leftAlias.localeCompare(rightAlias))
|
|
479
|
-
.flatMap(([aliasName]) => {
|
|
480
|
-
const aliasPath = join(commandPackage.commandDir, aliasName);
|
|
481
|
-
if (isWasmBinaryFile(aliasPath)) {
|
|
482
|
-
return [];
|
|
483
|
-
}
|
|
484
|
-
const sourcePath = resolveDeclaredCommandSource(commandPackage.commandDir, aliasName, commandPackage.aliases);
|
|
485
|
-
if (!sourcePath) {
|
|
486
|
-
return [];
|
|
487
|
-
}
|
|
488
|
-
return [[aliasName, sourcePath]];
|
|
489
|
-
});
|
|
490
|
-
if (aliasEntries.length === 0) {
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
const aliasDir = mkdtempSync(join(tmpdir(), "agentos-command-aliases-"));
|
|
494
|
-
for (const [aliasName, sourcePath] of aliasEntries) {
|
|
495
|
-
writeFileSync(join(aliasDir, aliasName), readFileSync(sourcePath));
|
|
496
|
-
}
|
|
497
|
-
tempDirs.push(aliasDir);
|
|
498
|
-
commandDirs.push(aliasDir);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
catch (error) {
|
|
502
|
-
for (const tempDir of tempDirs) {
|
|
503
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
504
|
-
}
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
return {
|
|
508
|
-
commandDirs,
|
|
509
|
-
dispose() {
|
|
510
|
-
for (const tempDir of tempDirs) {
|
|
511
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
512
|
-
}
|
|
513
|
-
},
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
function collectConfiguredLowerPaths(config) {
|
|
517
|
-
const paths = new Set();
|
|
518
|
-
for (const lower of config?.lowers ?? []) {
|
|
519
|
-
if (lower.kind !== "snapshot-export") {
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
for (const entry of lower.source.filesystem.entries) {
|
|
523
|
-
paths.add(entry.path);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
return paths;
|
|
527
|
-
}
|
|
528
|
-
function findBootstrapSeedEntry(config, path) {
|
|
529
|
-
for (const lower of config?.lowers ?? []) {
|
|
530
|
-
if (lower.kind !== "snapshot-export") {
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
const entry = lower.source.filesystem.entries.find((candidate) => candidate.path === path);
|
|
534
|
-
if (entry) {
|
|
535
|
-
return entry;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return getBaseFilesystemEntries().find((entry) => entry.path === path);
|
|
539
|
-
}
|
|
540
|
-
function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
|
|
541
|
-
const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
|
|
542
|
-
const existingPaths = collectConfiguredLowerPaths(config);
|
|
543
|
-
if (includesBundledBaseLayer) {
|
|
544
|
-
for (const entry of getBaseFilesystemEntries()) {
|
|
545
|
-
existingPaths.add(entry.path);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const entries = [
|
|
549
|
-
{
|
|
550
|
-
path: "/",
|
|
551
|
-
type: "directory",
|
|
552
|
-
mode: "755",
|
|
553
|
-
uid: 0,
|
|
554
|
-
gid: 0,
|
|
555
|
-
},
|
|
556
|
-
];
|
|
557
|
-
for (const dir of KERNEL_POSIX_BOOTSTRAP_DIRS) {
|
|
558
|
-
if (existingPaths.has(dir)) {
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
const seed = findBootstrapSeedEntry(config, dir);
|
|
562
|
-
entries.push({
|
|
563
|
-
path: dir,
|
|
564
|
-
type: "directory",
|
|
565
|
-
mode: seed?.type === "directory" ? seed.mode : "755",
|
|
566
|
-
uid: seed?.uid ?? 0,
|
|
567
|
-
gid: seed?.gid ?? 0,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
if (!includesBundledBaseLayer && !existingPaths.has("/usr/bin/env")) {
|
|
571
|
-
entries.push({
|
|
572
|
-
path: "/usr/bin/env",
|
|
573
|
-
type: "file",
|
|
574
|
-
mode: "644",
|
|
575
|
-
uid: 0,
|
|
576
|
-
gid: 0,
|
|
577
|
-
content: "AA==",
|
|
578
|
-
encoding: "base64",
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
const uniqueCommands = [...new Set(commandNames)].sort((a, b) => a.localeCompare(b));
|
|
582
|
-
for (const command of uniqueCommands) {
|
|
583
|
-
const stubPath = `/bin/${command}`;
|
|
584
|
-
if (existingPaths.has(stubPath)) {
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
entries.push({
|
|
588
|
-
path: stubPath,
|
|
589
|
-
type: "file",
|
|
590
|
-
mode: "755",
|
|
591
|
-
uid: 0,
|
|
592
|
-
gid: 0,
|
|
593
|
-
content: KERNEL_COMMAND_STUB,
|
|
594
|
-
encoding: "utf8",
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
for (const entry of sortFilesystemEntries(extraEntries)) {
|
|
598
|
-
if (existingPaths.has(entry.path)) {
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
entries.push(entry);
|
|
602
|
-
}
|
|
603
|
-
return entries.length > 1 ? createSnapshotExport(entries) : null;
|
|
604
|
-
}
|
|
605
|
-
function buildLiveBootstrapDirectoryEntries(existingPaths, config) {
|
|
606
|
-
const entries = [];
|
|
607
|
-
for (const dir of KERNEL_POSIX_BOOTSTRAP_DIRS) {
|
|
608
|
-
if (existingPaths.has(dir)) {
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
const seed = findBootstrapSeedEntry(config, dir);
|
|
612
|
-
entries.push({
|
|
613
|
-
path: dir,
|
|
614
|
-
kind: "directory",
|
|
615
|
-
mode: Number.parseInt(seed?.type === "directory" ? seed.mode : "755", 8),
|
|
616
|
-
uid: seed?.uid ?? 0,
|
|
617
|
-
gid: seed?.gid ?? 0,
|
|
618
|
-
executable: true,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
return entries;
|
|
622
|
-
}
|
|
623
|
-
async function bootstrapLiveBootstrapDirectories(client, session, vm, config) {
|
|
624
|
-
const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm)).map((entry) => entry.path));
|
|
625
|
-
const entries = buildLiveBootstrapDirectoryEntries(existingPaths, config);
|
|
626
|
-
if (entries.length === 0) {
|
|
627
|
-
return;
|
|
147
|
+
function requireSnapshotField(entry, field) {
|
|
148
|
+
const value = entry[field];
|
|
149
|
+
if (value === undefined || value === null) {
|
|
150
|
+
throw new Error(`sidecar root snapshot for ${entry.path} is missing ${String(field)}`);
|
|
628
151
|
}
|
|
629
|
-
|
|
152
|
+
return value;
|
|
630
153
|
}
|
|
631
|
-
function toSnapshotModeString(mode
|
|
632
|
-
|
|
633
|
-
return `0${((mode ?? fallback) & 0o7777).toString(8)}`;
|
|
154
|
+
function toSnapshotModeString(mode) {
|
|
155
|
+
return `0${(mode & 0o7777).toString(8)}`;
|
|
634
156
|
}
|
|
635
157
|
function convertSidecarRootSnapshotEntries(entries) {
|
|
636
158
|
return entries.map((entry) => {
|
|
637
159
|
const baseEntry = {
|
|
638
160
|
path: entry.path,
|
|
639
161
|
type: entry.kind,
|
|
640
|
-
mode: toSnapshotModeString(entry
|
|
641
|
-
uid: entry
|
|
642
|
-
gid: entry
|
|
162
|
+
mode: toSnapshotModeString(requireSnapshotField(entry, "mode")),
|
|
163
|
+
uid: requireSnapshotField(entry, "uid"),
|
|
164
|
+
gid: requireSnapshotField(entry, "gid"),
|
|
643
165
|
};
|
|
644
166
|
if (entry.kind === "file") {
|
|
645
167
|
return {
|
|
646
168
|
...baseEntry,
|
|
647
|
-
content: entry
|
|
648
|
-
encoding: entry
|
|
169
|
+
content: requireSnapshotField(entry, "content"),
|
|
170
|
+
encoding: requireSnapshotField(entry, "encoding"),
|
|
649
171
|
};
|
|
650
172
|
}
|
|
651
173
|
if (entry.kind === "symlink") {
|
|
652
|
-
if (entry.target === undefined) {
|
|
653
|
-
throw new Error(`sidecar root snapshot for ${entry.path} is missing a symlink target`);
|
|
654
|
-
}
|
|
655
174
|
return {
|
|
656
175
|
...baseEntry,
|
|
657
|
-
target: entry
|
|
176
|
+
target: requireSnapshotField(entry, "target"),
|
|
658
177
|
};
|
|
659
178
|
}
|
|
660
179
|
return baseEntry;
|
|
661
180
|
});
|
|
662
181
|
}
|
|
663
|
-
function ensureNativeSidecarBinary() {
|
|
664
|
-
// A published install has no in-repo Cargo workspace to build from: resolve
|
|
665
|
-
// the prebuilt platform binary (or the AGENTOS_SIDECAR_BIN override).
|
|
666
|
-
if (process.env.AGENTOS_SIDECAR_BIN ||
|
|
667
|
-
!existsSync(join(REPO_ROOT, "Cargo.toml"))) {
|
|
668
|
-
return resolvePublishedSidecarBinary();
|
|
669
|
-
}
|
|
670
|
-
if (ensuredSidecarBinary &&
|
|
671
|
-
existsSync(ensuredSidecarBinary) &&
|
|
672
|
-
!sidecarBinaryNeedsBuild()) {
|
|
673
|
-
return ensuredSidecarBinary;
|
|
674
|
-
}
|
|
675
|
-
if (sidecarBinaryNeedsBuild()) {
|
|
676
|
-
const cargoBinary = findCargoBinary();
|
|
677
|
-
if (cargoBinary) {
|
|
678
|
-
execFileSync(cargoBinary, ["build", "-q", "-p", "agentos-sidecar"], {
|
|
679
|
-
cwd: REPO_ROOT,
|
|
680
|
-
stdio: "pipe",
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
else if (!existsSync(SIDECAR_BINARY)) {
|
|
684
|
-
execFileSync(resolveCargoBinary(), ["build", "-q", "-p", "agentos-sidecar"], {
|
|
685
|
-
cwd: REPO_ROOT,
|
|
686
|
-
stdio: "pipe",
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
ensuredSidecarBinary = SIDECAR_BINARY;
|
|
691
|
-
return ensuredSidecarBinary;
|
|
692
|
-
}
|
|
693
|
-
function sidecarBinaryNeedsBuild() {
|
|
694
|
-
if (!existsSync(SIDECAR_BINARY)) {
|
|
695
|
-
return true;
|
|
696
|
-
}
|
|
697
|
-
const binaryMtimeMs = statSync(SIDECAR_BINARY).mtimeMs;
|
|
698
|
-
return SIDECAR_BUILD_INPUTS.some((path) => existsSync(path) && latestMtimeMs(path) > binaryMtimeMs);
|
|
699
|
-
}
|
|
700
|
-
function latestMtimeMs(path) {
|
|
701
|
-
const stats = statSync(path);
|
|
702
|
-
if (!stats.isDirectory()) {
|
|
703
|
-
return stats.mtimeMs;
|
|
704
|
-
}
|
|
705
|
-
let latest = stats.mtimeMs;
|
|
706
|
-
for (const entry of readdirSync(path)) {
|
|
707
|
-
latest = Math.max(latest, latestMtimeMs(join(path, entry)));
|
|
708
|
-
}
|
|
709
|
-
return latest;
|
|
710
|
-
}
|
|
711
|
-
function collectGuestCommandPaths(commandDirs) {
|
|
712
|
-
const guestPaths = new Map();
|
|
713
|
-
for (const [index, commandDir] of commandDirs.entries()) {
|
|
714
|
-
let entries;
|
|
715
|
-
try {
|
|
716
|
-
entries = readdirSync(commandDir).sort((left, right) => left.localeCompare(right));
|
|
717
|
-
}
|
|
718
|
-
catch {
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
for (const entry of entries) {
|
|
722
|
-
if (entry.startsWith(".")) {
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
|
-
if (!isWasmBinaryFile(join(commandDir, entry)) || guestPaths.has(entry)) {
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
guestPaths.set(entry, `/__secure_exec/commands/${index}/${entry}`);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return guestPaths;
|
|
732
|
-
}
|
|
733
182
|
async function resolveCompatLocalMounts(mounts) {
|
|
734
183
|
if (!mounts) {
|
|
735
184
|
return [];
|
|
@@ -743,7 +192,7 @@ async function resolveCompatLocalMounts(mounts) {
|
|
|
743
192
|
resolved.push({
|
|
744
193
|
path: posixPath.normalize(mount.path),
|
|
745
194
|
fs: mount.driver,
|
|
746
|
-
readOnly: mount.readOnly
|
|
195
|
+
readOnly: mount.readOnly,
|
|
747
196
|
});
|
|
748
197
|
continue;
|
|
749
198
|
}
|
|
@@ -760,15 +209,13 @@ async function resolveCompatLocalMounts(mounts) {
|
|
|
760
209
|
resolved.push({
|
|
761
210
|
path: posixPath.normalize(mount.path),
|
|
762
211
|
fs,
|
|
763
|
-
readOnly: mode === "read-only",
|
|
212
|
+
readOnly: mount.filesystem.mode === undefined ? undefined : mode === "read-only",
|
|
764
213
|
});
|
|
765
214
|
}
|
|
766
215
|
return resolved;
|
|
767
216
|
}
|
|
768
217
|
function collectSidecarMountPlan(options) {
|
|
769
218
|
const sidecarMounts = [];
|
|
770
|
-
const hostMounts = [];
|
|
771
|
-
const hostPathMappings = [];
|
|
772
219
|
const seenMounts = new Set();
|
|
773
220
|
function pushMount(mount) {
|
|
774
221
|
const serialized = serializeMountConfigForSidecar(mount);
|
|
@@ -778,77 +225,26 @@ function collectSidecarMountPlan(options) {
|
|
|
778
225
|
}
|
|
779
226
|
seenMounts.add(key);
|
|
780
227
|
sidecarMounts.push(serialized);
|
|
781
|
-
if (mount.plugin.id === "host_dir") {
|
|
782
|
-
const config = getHostDirMountPluginConfig(mount.plugin.config);
|
|
783
|
-
if (config) {
|
|
784
|
-
hostPathMappings.push({
|
|
785
|
-
vmPath: posixPath.normalize(mount.path),
|
|
786
|
-
hostPath: resolveHostPath(config.hostPath),
|
|
787
|
-
readOnly: mount.readOnly ?? config.readOnly ?? true,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
if (config && options.mounts?.some((candidate) => candidate === mount)) {
|
|
791
|
-
hostMounts.push({
|
|
792
|
-
vmPath: posixPath.normalize(mount.path),
|
|
793
|
-
hostPath: resolveHostPath(config.hostPath),
|
|
794
|
-
readOnly: mount.readOnly ?? config.readOnly ?? true,
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
228
|
}
|
|
799
229
|
for (const mount of options.mounts ?? []) {
|
|
800
230
|
if (!isNativeMountConfig(mount)) {
|
|
231
|
+
const readOnly = isOverlayMountConfig(mount)
|
|
232
|
+
? mount.filesystem.mode === undefined
|
|
233
|
+
? undefined
|
|
234
|
+
: mount.filesystem.mode === "read-only"
|
|
235
|
+
: mount.readOnly;
|
|
236
|
+
sidecarMounts.push({
|
|
237
|
+
guestPath: mount.path,
|
|
238
|
+
...(readOnly === undefined ? {} : { readOnly }),
|
|
239
|
+
plugin: {
|
|
240
|
+
id: "js_bridge",
|
|
241
|
+
},
|
|
242
|
+
});
|
|
801
243
|
continue;
|
|
802
244
|
}
|
|
803
245
|
pushMount(mount);
|
|
804
246
|
}
|
|
805
|
-
|
|
806
|
-
pushMount({
|
|
807
|
-
path: root.vmPath,
|
|
808
|
-
plugin: createHostDirBackend({
|
|
809
|
-
hostPath: root.hostPath,
|
|
810
|
-
readOnly: true,
|
|
811
|
-
}),
|
|
812
|
-
readOnly: true,
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
for (const [index, commandDir] of options.commandDirs.entries()) {
|
|
816
|
-
pushMount({
|
|
817
|
-
path: `/__secure_exec/commands/${index}`,
|
|
818
|
-
plugin: createHostDirBackend({
|
|
819
|
-
hostPath: commandDir,
|
|
820
|
-
readOnly: true,
|
|
821
|
-
}),
|
|
822
|
-
readOnly: true,
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
if (options.shimDir) {
|
|
826
|
-
pushMount({
|
|
827
|
-
path: "/usr/local/bin",
|
|
828
|
-
plugin: createHostDirBackend({
|
|
829
|
-
hostPath: options.shimDir,
|
|
830
|
-
readOnly: true,
|
|
831
|
-
}),
|
|
832
|
-
readOnly: true,
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
hostMounts.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
836
|
-
hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
837
|
-
return { sidecarMounts, hostMounts, hostPathMappings };
|
|
838
|
-
}
|
|
839
|
-
function materializeToolShimDir(toolKits) {
|
|
840
|
-
const shimDir = mkdtempSync(join(tmpdir(), "agentos-host-tools-shims-"));
|
|
841
|
-
writeFileSync(join(shimDir, "agentos"), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
842
|
-
for (const toolKit of toolKits) {
|
|
843
|
-
writeFileSync(join(shimDir, `agentos-${toolKit.name}`), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
844
|
-
}
|
|
845
|
-
return shimDir;
|
|
846
|
-
}
|
|
847
|
-
function collectToolkitBootstrapCommands(toolKits) {
|
|
848
|
-
if (toolKits.length === 0) {
|
|
849
|
-
return [];
|
|
850
|
-
}
|
|
851
|
-
return ["agentos", ...toolKits.map((toolKit) => `agentos-${toolKit.name}`)];
|
|
247
|
+
return { sidecarMounts };
|
|
852
248
|
}
|
|
853
249
|
function validationMessage(error) {
|
|
854
250
|
if (typeof error === "object" &&
|
|
@@ -866,6 +262,29 @@ function validationMessage(error) {
|
|
|
866
262
|
}
|
|
867
263
|
return error instanceof Error ? error.message : String(error);
|
|
868
264
|
}
|
|
265
|
+
function requireVmFetchResponsePayload(value) {
|
|
266
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
267
|
+
throw new Error("sidecar vm.fetch response must be an object");
|
|
268
|
+
}
|
|
269
|
+
const payload = value;
|
|
270
|
+
if (!Number.isInteger(payload.status)) {
|
|
271
|
+
throw new Error("sidecar vm.fetch response is missing numeric status");
|
|
272
|
+
}
|
|
273
|
+
if (typeof payload.statusText !== "string") {
|
|
274
|
+
throw new Error("sidecar vm.fetch response is missing statusText");
|
|
275
|
+
}
|
|
276
|
+
if (!Array.isArray(payload.headers) ||
|
|
277
|
+
payload.headers.some((entry) => !Array.isArray(entry) ||
|
|
278
|
+
entry.length !== 2 ||
|
|
279
|
+
typeof entry[0] !== "string" ||
|
|
280
|
+
typeof entry[1] !== "string")) {
|
|
281
|
+
throw new Error("sidecar vm.fetch response is missing valid headers");
|
|
282
|
+
}
|
|
283
|
+
if (typeof payload.body !== "string") {
|
|
284
|
+
throw new Error("sidecar vm.fetch response is missing base64 body");
|
|
285
|
+
}
|
|
286
|
+
return payload;
|
|
287
|
+
}
|
|
869
288
|
function toolToSidecarDefinition(tool) {
|
|
870
289
|
return {
|
|
871
290
|
description: tool.description,
|
|
@@ -881,126 +300,6 @@ function toolToSidecarDefinition(tool) {
|
|
|
881
300
|
: {}),
|
|
882
301
|
};
|
|
883
302
|
}
|
|
884
|
-
function combineInstructions(additionalInstructions, toolReference) {
|
|
885
|
-
const parts = [additionalInstructions, toolReference]
|
|
886
|
-
.map((part) => part?.trim())
|
|
887
|
-
.filter((part) => Boolean(part));
|
|
888
|
-
if (parts.length === 0) {
|
|
889
|
-
return null;
|
|
890
|
-
}
|
|
891
|
-
return parts.join("\n\n");
|
|
892
|
-
}
|
|
893
|
-
function buildHostToolReference(toolKits) {
|
|
894
|
-
if (toolKits.length === 0) {
|
|
895
|
-
return "";
|
|
896
|
-
}
|
|
897
|
-
const lines = [
|
|
898
|
-
"## Available Host Tools",
|
|
899
|
-
"",
|
|
900
|
-
"Run `agentos list-tools` to see all available tools.",
|
|
901
|
-
"",
|
|
902
|
-
];
|
|
903
|
-
for (const toolKit of toolKits) {
|
|
904
|
-
lines.push(`### ${toolKit.name}`);
|
|
905
|
-
lines.push("");
|
|
906
|
-
lines.push(toolKit.description);
|
|
907
|
-
lines.push("");
|
|
908
|
-
for (const [toolName, tool] of Object.entries(toolKit.tools)) {
|
|
909
|
-
const sidecarTool = toolToSidecarDefinition(tool);
|
|
910
|
-
const signature = buildToolFlagSignature(sidecarTool.inputSchema);
|
|
911
|
-
const suffix = signature.length > 0 ? ` ${signature}` : "";
|
|
912
|
-
lines.push(`- \`agentos-${toolKit.name} ${toolName}${suffix}\` — ${tool.description}`);
|
|
913
|
-
}
|
|
914
|
-
lines.push("");
|
|
915
|
-
const toolsWithExamples = Object.entries(toolKit.tools).filter(([, tool]) => tool.examples && tool.examples.length > 0);
|
|
916
|
-
if (toolsWithExamples.length > 0) {
|
|
917
|
-
lines.push("**Examples:**");
|
|
918
|
-
lines.push("");
|
|
919
|
-
for (const [toolName, tool] of toolsWithExamples) {
|
|
920
|
-
for (const example of tool.examples ?? []) {
|
|
921
|
-
const args = inputToToolFlags(example.input);
|
|
922
|
-
const suffix = args.length > 0 ? ` ${args}` : "";
|
|
923
|
-
lines.push(`- ${example.description}: \`agentos-${toolKit.name} ${toolName}${suffix}\``);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
lines.push("");
|
|
927
|
-
}
|
|
928
|
-
lines.push(`Run \`agentos-${toolKit.name} <tool> --help\` for details.`);
|
|
929
|
-
lines.push("");
|
|
930
|
-
}
|
|
931
|
-
return lines.join("\n");
|
|
932
|
-
}
|
|
933
|
-
function buildToolFlagSignature(schema) {
|
|
934
|
-
return describeToolFlags(schema)
|
|
935
|
-
.map((flag) => {
|
|
936
|
-
if (flag.required) {
|
|
937
|
-
return `${flag.name} <${flag.type}>`;
|
|
938
|
-
}
|
|
939
|
-
return `[${flag.name} <${flag.type}>]`;
|
|
940
|
-
})
|
|
941
|
-
.join(" ");
|
|
942
|
-
}
|
|
943
|
-
function describeToolFlags(schema) {
|
|
944
|
-
const schemaObject = asRecord(schema);
|
|
945
|
-
const properties = asRecord(schemaObject.properties);
|
|
946
|
-
const required = Array.isArray(schemaObject.required)
|
|
947
|
-
? new Set(schemaObject.required.filter((item) => typeof item === "string"))
|
|
948
|
-
: new Set();
|
|
949
|
-
return Object.entries(properties).map(([fieldName, fieldSchema]) => ({
|
|
950
|
-
name: `--${camelToKebab(fieldName)}`,
|
|
951
|
-
type: describeToolFlagType(fieldSchema),
|
|
952
|
-
required: required.has(fieldName),
|
|
953
|
-
}));
|
|
954
|
-
}
|
|
955
|
-
function describeToolFlagType(schema) {
|
|
956
|
-
const schemaObject = asRecord(schema);
|
|
957
|
-
const type = typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
958
|
-
if (type === "array") {
|
|
959
|
-
const itemType = describeJsonSchemaScalarType(schemaObject.items);
|
|
960
|
-
return `${itemType}[]`;
|
|
961
|
-
}
|
|
962
|
-
if (type === "string") {
|
|
963
|
-
const enumValues = Array.isArray(schemaObject.enum)
|
|
964
|
-
? schemaObject.enum.filter((item) => typeof item === "string")
|
|
965
|
-
: [];
|
|
966
|
-
return enumValues.length > 0 ? enumValues.join("|") : "string";
|
|
967
|
-
}
|
|
968
|
-
return type ?? "string";
|
|
969
|
-
}
|
|
970
|
-
function describeJsonSchemaScalarType(schema) {
|
|
971
|
-
const schemaObject = asRecord(schema);
|
|
972
|
-
return typeof schemaObject.type === "string" ? schemaObject.type : "string";
|
|
973
|
-
}
|
|
974
|
-
function inputToToolFlags(input) {
|
|
975
|
-
const inputObject = asRecord(input);
|
|
976
|
-
return Object.entries(inputObject)
|
|
977
|
-
.flatMap(([key, value]) => {
|
|
978
|
-
const flag = `--${camelToKebab(key)}`;
|
|
979
|
-
if (value === true) {
|
|
980
|
-
return [flag];
|
|
981
|
-
}
|
|
982
|
-
if (value === false) {
|
|
983
|
-
return [`--no-${camelToKebab(key)}`];
|
|
984
|
-
}
|
|
985
|
-
if (Array.isArray(value)) {
|
|
986
|
-
return value.map((item) => `${flag} ${toolCliString(item)}`);
|
|
987
|
-
}
|
|
988
|
-
return [`${flag} ${toolCliString(value)}`];
|
|
989
|
-
})
|
|
990
|
-
.join(" ");
|
|
991
|
-
}
|
|
992
|
-
function toolCliString(value) {
|
|
993
|
-
return typeof value === "string" ? value : (JSON.stringify(value) ?? "null");
|
|
994
|
-
}
|
|
995
|
-
function camelToKebab(value) {
|
|
996
|
-
return value.replace(/[A-Z]/g, (ch, index) => `${index > 0 ? "-" : ""}${ch.toLowerCase()}`);
|
|
997
|
-
}
|
|
998
|
-
function asRecord(value) {
|
|
999
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1000
|
-
return value;
|
|
1001
|
-
}
|
|
1002
|
-
return {};
|
|
1003
|
-
}
|
|
1004
303
|
async function handleHostCallback(request, context) {
|
|
1005
304
|
const payload = request.payload;
|
|
1006
305
|
if (payload.type !== "host_callback") {
|
|
@@ -1010,23 +309,6 @@ async function handleHostCallback(request, context) {
|
|
|
1010
309
|
error: `unsupported sidecar request type: ${payload.type}`,
|
|
1011
310
|
};
|
|
1012
311
|
}
|
|
1013
|
-
const command = parseHostCommandCallbackInput(payload.input);
|
|
1014
|
-
if (command) {
|
|
1015
|
-
try {
|
|
1016
|
-
return {
|
|
1017
|
-
type: "host_callback_result",
|
|
1018
|
-
invocation_id: payload.invocation_id,
|
|
1019
|
-
result: await handleHostCommandCallback(command, context),
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
catch (error) {
|
|
1023
|
-
return {
|
|
1024
|
-
type: "host_callback_result",
|
|
1025
|
-
invocation_id: payload.invocation_id,
|
|
1026
|
-
error: validationMessage(error),
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
312
|
const tool = context.toolMap.get(payload.callback_key);
|
|
1031
313
|
if (!tool) {
|
|
1032
314
|
return {
|
|
@@ -1035,14 +317,6 @@ async function handleHostCallback(request, context) {
|
|
|
1035
317
|
error: `Unknown tool "${payload.callback_key}"`,
|
|
1036
318
|
};
|
|
1037
319
|
}
|
|
1038
|
-
const permissionMode = toolPermissionMode(context.permissions, payload.callback_key);
|
|
1039
|
-
if (permissionMode !== "allow") {
|
|
1040
|
-
return {
|
|
1041
|
-
type: "host_callback_result",
|
|
1042
|
-
invocation_id: payload.invocation_id,
|
|
1043
|
-
error: `EACCES: blocked by tool.invoke policy for ${payload.callback_key}`,
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
320
|
const parsed = tool.inputSchema.safeParse(payload.input);
|
|
1047
321
|
if (!parsed.success) {
|
|
1048
322
|
return {
|
|
@@ -1055,7 +329,7 @@ async function handleHostCallback(request, context) {
|
|
|
1055
329
|
return {
|
|
1056
330
|
type: "host_callback_result",
|
|
1057
331
|
invocation_id: payload.invocation_id,
|
|
1058
|
-
result: await
|
|
332
|
+
result: await tool.execute(parsed.data),
|
|
1059
333
|
};
|
|
1060
334
|
}
|
|
1061
335
|
catch (error) {
|
|
@@ -1075,348 +349,186 @@ function buildToolMap(toolKits) {
|
|
|
1075
349
|
}
|
|
1076
350
|
return toolMap;
|
|
1077
351
|
}
|
|
1078
|
-
function
|
|
1079
|
-
|
|
1080
|
-
if (value.type !== "command" ||
|
|
1081
|
-
typeof value.command !== "string" ||
|
|
1082
|
-
typeof value.cwd !== "string" ||
|
|
1083
|
-
!Array.isArray(value.args) ||
|
|
1084
|
-
!value.args.every((arg) => typeof arg === "string")) {
|
|
1085
|
-
return null;
|
|
1086
|
-
}
|
|
1087
|
-
return {
|
|
1088
|
-
type: "command",
|
|
1089
|
-
command: value.command,
|
|
1090
|
-
args: value.args,
|
|
1091
|
-
cwd: value.cwd,
|
|
1092
|
-
};
|
|
352
|
+
function bridgeErrorMessage(error) {
|
|
353
|
+
return error instanceof Error ? error.message : String(error);
|
|
1093
354
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
return handleAgentOsRegistryCommand(command, context);
|
|
1098
|
-
}
|
|
1099
|
-
if (directToolKit) {
|
|
1100
|
-
return handleAgentOsToolkitCommand(command, context, directToolKit);
|
|
355
|
+
function toBridgeArgs(value) {
|
|
356
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
357
|
+
throw new Error("js_bridge args must be an object");
|
|
1101
358
|
}
|
|
1102
|
-
|
|
359
|
+
return value;
|
|
1103
360
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
return {
|
|
1108
|
-
usage: "agentos <command>: list-tools [toolkit], <toolkit> --help, or <toolkit> <tool> ...",
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
if (subcommand === "list-tools") {
|
|
1112
|
-
return toolkitName
|
|
1113
|
-
? describeToolkitPayload(context.toolKits, toolkitName)
|
|
1114
|
-
: listToolkitsPayload(context.toolKits);
|
|
361
|
+
function bridgePath(mountId, value) {
|
|
362
|
+
if (!mountId.startsWith("/")) {
|
|
363
|
+
throw new Error(`Unsupported js_bridge mount id: ${mountId}`);
|
|
1115
364
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
throw new Error(`No toolkit "${subcommand}". Available: ${toolkitNames(context.toolKits)}`);
|
|
365
|
+
if (typeof value !== "string") {
|
|
366
|
+
throw new Error("js_bridge path argument must be a string");
|
|
1119
367
|
}
|
|
1120
|
-
|
|
1121
|
-
return describeToolkitPayload(context.toolKits, subcommand);
|
|
1122
|
-
}
|
|
1123
|
-
if (toolName && isHelpFlag(toolName)) {
|
|
1124
|
-
return describeToolPayload(toolKit, toolkitName);
|
|
1125
|
-
}
|
|
1126
|
-
return invokeHostTool({
|
|
1127
|
-
toolKit,
|
|
1128
|
-
toolName: toolkitName,
|
|
1129
|
-
args: [toolName, ...toolArgs].filter((value) => typeof value === "string"),
|
|
1130
|
-
cwd: command.cwd,
|
|
1131
|
-
context,
|
|
1132
|
-
});
|
|
368
|
+
return posixPath.normalize(posixPath.join(mountId, value));
|
|
1133
369
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
if (helpOrFirstArg && isHelpFlag(helpOrFirstArg)) {
|
|
1140
|
-
return describeToolPayload(toolKit, toolName);
|
|
1141
|
-
}
|
|
1142
|
-
return invokeHostTool({
|
|
1143
|
-
toolKit,
|
|
1144
|
-
toolName,
|
|
1145
|
-
args: [helpOrFirstArg, ...rest].filter((value) => typeof value === "string"),
|
|
1146
|
-
cwd: command.cwd,
|
|
1147
|
-
context,
|
|
1148
|
-
});
|
|
370
|
+
function requireBridgeNumber(value, field) {
|
|
371
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
372
|
+
throw new Error(`js_bridge args.${field} must be a number`);
|
|
373
|
+
}
|
|
374
|
+
return value;
|
|
1149
375
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
throw new Error(`No tool "${toolName}" in toolkit "${toolKit.name}". Available: ${toolNames(toolKit)}`);
|
|
376
|
+
function decodeBridgeBytes(value, field) {
|
|
377
|
+
if (typeof value === "string") {
|
|
378
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
1154
379
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
throw new Error(`EACCES: blocked by tool.invoke policy for ${callbackKey}`);
|
|
380
|
+
if (Array.isArray(value) &&
|
|
381
|
+
value.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)) {
|
|
382
|
+
return new Uint8Array(value);
|
|
1159
383
|
}
|
|
1160
|
-
|
|
1161
|
-
return executeHostTool(tool, callbackKey, input);
|
|
384
|
+
throw new Error(`js_bridge args.${field} must be base64 bytes`);
|
|
1162
385
|
}
|
|
1163
|
-
async function
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const negated = rawFlag.startsWith("no-");
|
|
1217
|
-
const flagName = negated ? rawFlag.slice(3) : rawFlag;
|
|
1218
|
-
const entry = flagToField.get(flagName);
|
|
1219
|
-
if (!entry) {
|
|
1220
|
-
throw new Error(`Unknown flag: --${rawFlag}`);
|
|
1221
|
-
}
|
|
1222
|
-
const [fieldName, fieldSchema] = entry;
|
|
1223
|
-
const fieldType = jsonSchemaType(fieldSchema);
|
|
1224
|
-
if (negated) {
|
|
1225
|
-
if (fieldType !== "boolean") {
|
|
1226
|
-
throw new Error(`Unknown flag: --${rawFlag}`);
|
|
1227
|
-
}
|
|
1228
|
-
input[fieldName] = false;
|
|
1229
|
-
index += 1;
|
|
1230
|
-
continue;
|
|
1231
|
-
}
|
|
1232
|
-
if (fieldType === "boolean") {
|
|
1233
|
-
input[fieldName] = true;
|
|
1234
|
-
index += 1;
|
|
1235
|
-
continue;
|
|
1236
|
-
}
|
|
1237
|
-
const value = argv[index + 1];
|
|
1238
|
-
if (value === undefined) {
|
|
1239
|
-
throw new Error(`Flag --${rawFlag} requires a value`);
|
|
1240
|
-
}
|
|
1241
|
-
if (fieldType === "number" || fieldType === "integer") {
|
|
1242
|
-
const number = Number(value);
|
|
1243
|
-
if (!Number.isFinite(number)) {
|
|
1244
|
-
throw new Error(`Flag --${rawFlag} expects a number, got "${value}"`);
|
|
386
|
+
async function handleJsBridgeCall(request, context) {
|
|
387
|
+
try {
|
|
388
|
+
const args = toBridgeArgs(request.args);
|
|
389
|
+
const target = context.resolveTarget(request.mount_id);
|
|
390
|
+
if (!target) {
|
|
391
|
+
throw new Error(`Unknown js_bridge mount id: ${request.mount_id}`);
|
|
392
|
+
}
|
|
393
|
+
const fs = target.filesystem;
|
|
394
|
+
const path = () => bridgePath(target.rootPath, args.path);
|
|
395
|
+
let result;
|
|
396
|
+
switch (request.operation) {
|
|
397
|
+
case "readFile":
|
|
398
|
+
result = Buffer.from(await fs.readFile(path())).toString("base64");
|
|
399
|
+
break;
|
|
400
|
+
case "readDir":
|
|
401
|
+
result = await fs.readDir(path());
|
|
402
|
+
break;
|
|
403
|
+
case "readDirWithTypes":
|
|
404
|
+
result = await fs.readDirWithTypes(path());
|
|
405
|
+
break;
|
|
406
|
+
case "writeFile":
|
|
407
|
+
await fs.writeFile(path(), decodeBridgeBytes(args.content, "content"));
|
|
408
|
+
break;
|
|
409
|
+
case "createDir":
|
|
410
|
+
await fs.createDir(path());
|
|
411
|
+
break;
|
|
412
|
+
case "mkdir":
|
|
413
|
+
await fs.mkdir(path(), { recursive: args.recursive !== false });
|
|
414
|
+
break;
|
|
415
|
+
case "exists":
|
|
416
|
+
result = await fs.exists(path());
|
|
417
|
+
break;
|
|
418
|
+
case "stat":
|
|
419
|
+
result = await fs.stat(path());
|
|
420
|
+
break;
|
|
421
|
+
case "removeFile":
|
|
422
|
+
await fs.removeFile(path());
|
|
423
|
+
break;
|
|
424
|
+
case "removeDir":
|
|
425
|
+
await fs.removeDir(path());
|
|
426
|
+
break;
|
|
427
|
+
case "rename":
|
|
428
|
+
await fs.rename(bridgePath(target.rootPath, args.oldPath), bridgePath(target.rootPath, args.newPath));
|
|
429
|
+
break;
|
|
430
|
+
case "realpath":
|
|
431
|
+
result = await fs.realpath(path());
|
|
432
|
+
break;
|
|
433
|
+
case "symlink": {
|
|
434
|
+
if (typeof args.target !== "string") {
|
|
435
|
+
throw new Error("js_bridge args.target must be a string");
|
|
436
|
+
}
|
|
437
|
+
await fs.symlink(args.target, bridgePath(target.rootPath, args.linkPath));
|
|
438
|
+
break;
|
|
1245
439
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
440
|
+
case "readlink":
|
|
441
|
+
result = await fs.readlink(path());
|
|
442
|
+
break;
|
|
443
|
+
case "lstat":
|
|
444
|
+
result = await fs.lstat(path());
|
|
445
|
+
break;
|
|
446
|
+
case "link":
|
|
447
|
+
await fs.link(bridgePath(target.rootPath, args.oldPath), bridgePath(target.rootPath, args.newPath));
|
|
448
|
+
break;
|
|
449
|
+
case "chmod":
|
|
450
|
+
await fs.chmod(path(), requireBridgeNumber(args.mode, "mode"));
|
|
451
|
+
break;
|
|
452
|
+
case "chown":
|
|
453
|
+
await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"));
|
|
454
|
+
break;
|
|
455
|
+
case "utimes":
|
|
456
|
+
await fs.utimes(path(), requireBridgeNumber(args.atimeMs, "atimeMs"), requireBridgeNumber(args.mtimeMs, "mtimeMs"));
|
|
457
|
+
break;
|
|
458
|
+
case "truncate":
|
|
459
|
+
await fs.truncate(path(), requireBridgeNumber(args.length, "length"));
|
|
460
|
+
break;
|
|
461
|
+
case "pread":
|
|
462
|
+
result = Buffer.from(await fs.pread(path(), requireBridgeNumber(args.offset, "offset"), requireBridgeNumber(args.length, "length"))).toString("base64");
|
|
463
|
+
break;
|
|
464
|
+
case "pwrite":
|
|
465
|
+
await fs.pwrite(path(), requireBridgeNumber(args.offset, "offset"), decodeBridgeBytes(args.content, "content"));
|
|
466
|
+
break;
|
|
467
|
+
default:
|
|
468
|
+
throw new Error(`Unsupported js_bridge operation: ${request.operation}`);
|
|
1267
469
|
}
|
|
470
|
+
return {
|
|
471
|
+
type: "js_bridge_result",
|
|
472
|
+
call_id: request.call_id,
|
|
473
|
+
...(result === undefined ? {} : { result }),
|
|
474
|
+
};
|
|
1268
475
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
description: toolKit.description,
|
|
1276
|
-
tools: Object.keys(toolKit.tools),
|
|
1277
|
-
})),
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
function describeToolkitPayload(toolKits, toolkitName) {
|
|
1281
|
-
const toolKit = toolKits.find((kit) => kit.name === toolkitName);
|
|
1282
|
-
if (!toolKit) {
|
|
1283
|
-
throw new Error(`No toolkit "${toolkitName}". Available: ${toolkitNames(toolKits)}`);
|
|
1284
|
-
}
|
|
1285
|
-
return {
|
|
1286
|
-
name: toolKit.name,
|
|
1287
|
-
description: toolKit.description,
|
|
1288
|
-
tools: Object.fromEntries(Object.entries(toolKit.tools).map(([toolName, tool]) => [
|
|
1289
|
-
toolName,
|
|
1290
|
-
{
|
|
1291
|
-
description: tool.description,
|
|
1292
|
-
flags: describeToolFlags(toolToSidecarDefinition(tool).inputSchema),
|
|
1293
|
-
},
|
|
1294
|
-
])),
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
function describeToolPayload(toolKit, toolName) {
|
|
1298
|
-
const tool = toolKit.tools[toolName];
|
|
1299
|
-
if (!tool) {
|
|
1300
|
-
throw new Error(`No tool "${toolName}" in toolkit "${toolKit.name}". Available: ${toolNames(toolKit)}`);
|
|
1301
|
-
}
|
|
1302
|
-
return {
|
|
1303
|
-
toolkit: toolKit.name,
|
|
1304
|
-
tool: toolName,
|
|
1305
|
-
description: tool.description,
|
|
1306
|
-
flags: describeToolFlags(toolToSidecarDefinition(tool).inputSchema),
|
|
1307
|
-
examples: tool.examples?.map((example) => ({
|
|
1308
|
-
description: example.description,
|
|
1309
|
-
input: example.input,
|
|
1310
|
-
})) ?? [],
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
function toolPermissionMode(permissions, callbackKey) {
|
|
1314
|
-
const scope = permissions.tool;
|
|
1315
|
-
if (!scope) {
|
|
1316
|
-
return "deny";
|
|
1317
|
-
}
|
|
1318
|
-
if (typeof scope === "string") {
|
|
1319
|
-
return scope;
|
|
1320
|
-
}
|
|
1321
|
-
let mode = scope.default ?? "deny";
|
|
1322
|
-
for (const rule of scope.rules) {
|
|
1323
|
-
const operations = rule.operations ?? ["*"];
|
|
1324
|
-
const patterns = rule.patterns ?? ["**"];
|
|
1325
|
-
if (operations.some((operation) => operation === "*" || operation === "invoke") &&
|
|
1326
|
-
patterns.some((pattern) => permissionPatternMatches(pattern, callbackKey))) {
|
|
1327
|
-
mode = rule.mode;
|
|
1328
|
-
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
return {
|
|
478
|
+
type: "js_bridge_result",
|
|
479
|
+
call_id: request.call_id,
|
|
480
|
+
error: bridgeErrorMessage(error),
|
|
481
|
+
};
|
|
1329
482
|
}
|
|
1330
|
-
return mode;
|
|
1331
|
-
}
|
|
1332
|
-
function permissionPatternMatches(pattern, value) {
|
|
1333
|
-
if (pattern === "*" || pattern === "**" || pattern === value) {
|
|
1334
|
-
return true;
|
|
1335
|
-
}
|
|
1336
|
-
const parts = pattern.split(/(\*\*|\*)/u);
|
|
1337
|
-
const source = parts
|
|
1338
|
-
.map((part) => {
|
|
1339
|
-
if (part === "**")
|
|
1340
|
-
return ".*";
|
|
1341
|
-
if (part === "*")
|
|
1342
|
-
return "[^:]*";
|
|
1343
|
-
return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1344
|
-
})
|
|
1345
|
-
.join("");
|
|
1346
|
-
return new RegExp(`^${source}$`).test(value);
|
|
1347
483
|
}
|
|
1348
|
-
function
|
|
1349
|
-
return toolKits.map((toolKit) =>
|
|
1350
|
-
|
|
1351
|
-
function toolNames(toolKit) {
|
|
1352
|
-
return Object.keys(toolKit.tools).join(", ");
|
|
1353
|
-
}
|
|
1354
|
-
function isHelpFlag(value) {
|
|
1355
|
-
return value === "--help" || value === "-h";
|
|
1356
|
-
}
|
|
1357
|
-
function jsonSchemaType(schema) {
|
|
1358
|
-
const schemaObject = asRecord(schema);
|
|
1359
|
-
return typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
1360
|
-
}
|
|
1361
|
-
async function registerToolkitsOnSidecar(client, session, vm, toolKits) {
|
|
1362
|
-
if (toolKits.length === 0) {
|
|
1363
|
-
return "";
|
|
1364
|
-
}
|
|
1365
|
-
for (const toolKit of toolKits) {
|
|
1366
|
-
await client.registerHostCallbacks(session, vm, {
|
|
484
|
+
function serializeToolkitsForSidecar(toolKits) {
|
|
485
|
+
return toolKits.map((toolKit) => {
|
|
486
|
+
return {
|
|
1367
487
|
name: toolKit.name,
|
|
1368
488
|
description: toolKit.description,
|
|
1369
|
-
commandAliases: [`agentos-${toolKit.name}`],
|
|
1370
|
-
registryCommandAliases: ["agentos"],
|
|
1371
489
|
callbacks: Object.fromEntries(Object.entries(toolKit.tools).map(([toolName, tool]) => [
|
|
1372
490
|
toolName,
|
|
1373
491
|
toolToSidecarDefinition(tool),
|
|
1374
492
|
])),
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
return buildHostToolReference(toolKits);
|
|
493
|
+
};
|
|
494
|
+
});
|
|
1378
495
|
}
|
|
1379
496
|
export class AgentOs {
|
|
1380
497
|
#kernel;
|
|
1381
498
|
sidecar;
|
|
1382
499
|
_sessions = new Map();
|
|
1383
|
-
_closedSessionIds = new BoundedSet(CLOSED_SESSION_ID_RETENTION_LIMIT);
|
|
1384
|
-
_sessionClosePromises = new Map();
|
|
1385
|
-
_pendingSessionRequestResolvers = new Map();
|
|
1386
500
|
_processes = new Map();
|
|
1387
501
|
_shells = new Map();
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
_acpTerminals = new Map();
|
|
1392
|
-
_acpTerminalCounter = 0;
|
|
1393
|
-
_softwareRoots;
|
|
1394
|
-
_softwareAgentConfigs;
|
|
502
|
+
_pendingShellExitPromises = new Map();
|
|
503
|
+
_disposed = false;
|
|
504
|
+
_disposePromise = null;
|
|
1395
505
|
_cronManager;
|
|
1396
506
|
_toolKits = [];
|
|
1397
|
-
_toolReference = "";
|
|
1398
|
-
_permissions = allowAll;
|
|
1399
|
-
_hostMounts;
|
|
1400
|
-
_env;
|
|
1401
|
-
_rootFilesystem;
|
|
1402
507
|
_sidecarLease = null;
|
|
1403
508
|
_sidecarClient;
|
|
1404
509
|
_sidecarSession;
|
|
1405
510
|
_sidecarVm;
|
|
1406
511
|
_disposeSidecarEventListener;
|
|
1407
|
-
|
|
512
|
+
_disposeSidecarRequestHandler = null;
|
|
513
|
+
_agentStderrHandler;
|
|
514
|
+
_agentExitHandler;
|
|
515
|
+
_limitWarningHandler;
|
|
516
|
+
constructor(kernel, sidecar, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, agentStderrHandler, agentExitHandler, limitWarningHandler) {
|
|
1408
517
|
this.#kernel = kernel;
|
|
1409
518
|
this.sidecar = sidecar;
|
|
1410
|
-
this._softwareRoots = softwareRoots;
|
|
1411
|
-
this._softwareAgentConfigs = softwareAgentConfigs;
|
|
1412
|
-
this._hostMounts = hostMounts;
|
|
1413
|
-
this._env = env;
|
|
1414
|
-
this._rootFilesystem = rootFilesystem;
|
|
1415
519
|
this._sidecarClient = sidecarClient;
|
|
1416
520
|
this._sidecarSession = sidecarSession;
|
|
1417
521
|
this._sidecarVm = sidecarVm;
|
|
522
|
+
this._agentStderrHandler = agentStderrHandler;
|
|
523
|
+
this._agentExitHandler = agentExitHandler;
|
|
524
|
+
this._limitWarningHandler = limitWarningHandler;
|
|
1418
525
|
this._disposeSidecarEventListener = this._sidecarClient.onEvent((event) => {
|
|
1419
526
|
this._handleSidecarEvent(event);
|
|
527
|
+
}, {
|
|
528
|
+
scope: "vm",
|
|
529
|
+
connection_id: sidecarSession.connectionId,
|
|
530
|
+
session_id: sidecarSession.sessionId,
|
|
531
|
+
vm_id: sidecarVm.vmId,
|
|
1420
532
|
});
|
|
1421
533
|
agentOsRuntimeAdmins.set(this, {
|
|
1422
534
|
kernel,
|
|
@@ -1432,143 +544,145 @@ export class AgentOs {
|
|
|
1432
544
|
return getSharedAgentOsSidecarInternal(options);
|
|
1433
545
|
}
|
|
1434
546
|
static async create(options) {
|
|
547
|
+
options = parseAgentOsOptions(options);
|
|
548
|
+
// Default software is FULLY DYNAMIC: this package's own NON-agent
|
|
549
|
+
// @agentos-software/* dependencies (e.g. common), each default-exporting
|
|
550
|
+
// its registry-built descriptor. Agent packages are NOT projected here;
|
|
551
|
+
// callers or an upper package-manager layer must pass those packages in
|
|
552
|
+
// `software` before createSession(id). Unbuilt packages throw with build
|
|
553
|
+
// instructions; opt out via defaultSoftware: false.
|
|
554
|
+
const defaultSoftware = options?.defaultSoftware === false ? [] : resolveDefaultSoftware();
|
|
1435
555
|
const software = options?.defaultSoftware === false
|
|
1436
556
|
? (options.software ?? [])
|
|
1437
|
-
: [
|
|
1438
|
-
|
|
557
|
+
: [...defaultSoftware, ...(options?.software ?? [])];
|
|
558
|
+
// Packages are projected by the SIDECAR: the client forwards only the
|
|
559
|
+
// package `path` over `initializeVm` and the sidecar reads metadata from
|
|
560
|
+
// the packed vbare manifest (chunk1 of the `.aospkg`).
|
|
561
|
+
const flatSoftware = software.flat();
|
|
562
|
+
// Honor the AgentOsOptions.defaultSoftware contract ("entries already present
|
|
563
|
+
// in `software` are not duplicated"): the default bundle and an explicitly
|
|
564
|
+
// passed one resolve to the same package paths, so dedup by path. Without
|
|
565
|
+
// this the sidecar rejects the second projection with a duplicate-command
|
|
566
|
+
// error (e.g. coreutils' `[`).
|
|
567
|
+
const seenPackagePaths = new Set();
|
|
568
|
+
const sidecarPackages = flatSoftware.flatMap((entry) => {
|
|
569
|
+
const ref = normalizePackageRef(entry);
|
|
570
|
+
if (seenPackagePaths.has(ref.path)) {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
seenPackagePaths.add(ref.path);
|
|
574
|
+
return [{ path: ref.path }];
|
|
575
|
+
});
|
|
576
|
+
// All package software is projected into `/opt/agentos` by the sidecar. The
|
|
577
|
+
// client stages nothing host-side and parses NO package manifests: the
|
|
578
|
+
// sidecar owns agent resolution, agent enumeration, and agent snapshot
|
|
579
|
+
// bundle loading from the projected package dirs.
|
|
1439
580
|
const localMounts = await resolveCompatLocalMounts(options?.mounts);
|
|
1440
581
|
const toolKits = options?.toolKits;
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
582
|
+
// Resolve the sidecar handle up front so every VM created here leases the
|
|
583
|
+
// one shared native sidecar process owned by that handle.
|
|
584
|
+
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1444
585
|
const createVmAdmin = async () => {
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
...NODE_RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1450
|
-
...toolBootstrapCommands,
|
|
1451
|
-
]);
|
|
1452
|
-
let toolReference = "";
|
|
586
|
+
// The `/opt/agentos` projection is built by the sidecar from the
|
|
587
|
+
// forwarded `packages` (it owns the staging dir + read-only mount, and
|
|
588
|
+
// runtime `linkSoftware` appends to that live dir). The client no longer
|
|
589
|
+
// stages packages host-side.
|
|
1453
590
|
let rootBridge = null;
|
|
1454
591
|
let kernel = null;
|
|
1455
592
|
let client = null;
|
|
1456
|
-
let
|
|
593
|
+
let createdNativeVm = null;
|
|
594
|
+
let nativeSession = null;
|
|
1457
595
|
let cleanedUp = false;
|
|
1458
596
|
const cleanup = async () => {
|
|
1459
597
|
if (cleanedUp) {
|
|
1460
598
|
return;
|
|
1461
599
|
}
|
|
1462
600
|
cleanedUp = true;
|
|
1463
|
-
if (toolShimDir) {
|
|
1464
|
-
rmSync(toolShimDir, { recursive: true, force: true });
|
|
1465
|
-
toolShimDir = null;
|
|
1466
|
-
}
|
|
1467
|
-
preparedCommandDirs.dispose();
|
|
1468
601
|
};
|
|
1469
602
|
try {
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
}
|
|
1474
|
-
const commandGuestPaths = collectGuestCommandPaths(preparedCommandDirs.commandDirs);
|
|
1475
|
-
const requestedMounts = options?.moduleAccessCwd
|
|
1476
|
-
? [
|
|
1477
|
-
...(options.mounts ?? []),
|
|
1478
|
-
{
|
|
1479
|
-
path: "/root/node_modules",
|
|
1480
|
-
plugin: createHostDirBackend({
|
|
1481
|
-
hostPath: join(options.moduleAccessCwd, "node_modules"),
|
|
1482
|
-
readOnly: true,
|
|
1483
|
-
}),
|
|
1484
|
-
readOnly: true,
|
|
1485
|
-
},
|
|
1486
|
-
]
|
|
1487
|
-
: options?.mounts;
|
|
1488
|
-
const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
|
|
1489
|
-
mounts: requestedMounts,
|
|
1490
|
-
softwareRoots: processed.softwareRoots,
|
|
1491
|
-
commandDirs: preparedCommandDirs.commandDirs,
|
|
1492
|
-
shimDir: toolShimDir,
|
|
1493
|
-
});
|
|
1494
|
-
client = SidecarProcess.spawn({
|
|
1495
|
-
cwd: REPO_ROOT,
|
|
1496
|
-
command: ensureNativeSidecarBinary(),
|
|
1497
|
-
args: [],
|
|
1498
|
-
frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
|
|
603
|
+
let env = {};
|
|
604
|
+
const { sidecarMounts } = collectSidecarMountPlan({
|
|
605
|
+
mounts: options?.mounts,
|
|
1499
606
|
});
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
607
|
+
// Reuse the sidecar handle's single shared native process; this VM
|
|
608
|
+
// becomes another tenant of it rather than spawning its own process.
|
|
609
|
+
const shared = await ensureSharedSidecarNativeProcess(sidecar);
|
|
610
|
+
client = shared.client;
|
|
611
|
+
const session = shared.session;
|
|
612
|
+
nativeSession = session;
|
|
613
|
+
const sidecarPermissions = options?.permissions
|
|
614
|
+
? serializePermissionsForSidecar(options.permissions)
|
|
615
|
+
: undefined;
|
|
1506
616
|
const createVmConfig = {
|
|
1507
|
-
|
|
1508
|
-
|
|
617
|
+
...(options?.rootFilesystem !== undefined
|
|
618
|
+
? {
|
|
619
|
+
rootFilesystem: serializeRootFilesystemForSidecar(options.rootFilesystem),
|
|
620
|
+
}
|
|
621
|
+
: {}),
|
|
1509
622
|
permissions: sidecarPermissions,
|
|
1510
623
|
limits: options?.limits,
|
|
1511
|
-
|
|
624
|
+
...(options?.loopbackExemptPorts !== undefined
|
|
625
|
+
? { loopbackExemptPorts: options.loopbackExemptPorts }
|
|
626
|
+
: {}),
|
|
1512
627
|
// 0.3: the Node builtin allow-list moved from configureVm to
|
|
1513
628
|
// VM creation. `undefined` => engine default allow-list;
|
|
1514
629
|
// `[]` => deny all; `[..]` => exactly those. Platform and
|
|
1515
630
|
// module resolution keep their engine defaults (full Node
|
|
1516
631
|
// emulation), matching the prior behavior where Agent OS only
|
|
1517
632
|
// constrained the builtin allow-list.
|
|
1518
|
-
...(options?.allowedNodeBuiltins !== undefined
|
|
633
|
+
...(options?.allowedNodeBuiltins !== undefined ||
|
|
634
|
+
options?.highResolutionTime !== undefined
|
|
1519
635
|
? {
|
|
1520
636
|
jsRuntime: {
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
637
|
+
...(options?.allowedNodeBuiltins !== undefined
|
|
638
|
+
? { allowedBuiltins: options.allowedNodeBuiltins }
|
|
639
|
+
: {}),
|
|
640
|
+
...(options?.highResolutionTime !== undefined
|
|
641
|
+
? { highResolutionTime: options.highResolutionTime }
|
|
642
|
+
: {}),
|
|
1524
643
|
},
|
|
1525
644
|
}
|
|
1526
645
|
: {}),
|
|
646
|
+
...(options?.additionalInstructions === undefined
|
|
647
|
+
? {}
|
|
648
|
+
: {
|
|
649
|
+
agentAdditionalInstructions: options.additionalInstructions,
|
|
650
|
+
}),
|
|
1527
651
|
};
|
|
1528
|
-
const nativeVm = await client.
|
|
652
|
+
const nativeVm = await client.initializeVm(session, {
|
|
1529
653
|
runtime: "java_script",
|
|
1530
654
|
config: createVmConfig,
|
|
655
|
+
...(sidecarMounts.length > 0 ? { mounts: sidecarMounts } : {}),
|
|
656
|
+
...(sidecarPackages.length > 0 ? { packages: sidecarPackages } : {}),
|
|
657
|
+
...(toolKits === undefined || toolKits.length === 0
|
|
658
|
+
? {}
|
|
659
|
+
: { hostCallbacks: serializeToolkitsForSidecar(toolKits) }),
|
|
1531
660
|
});
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
await client.configureVm(session, nativeVm, {
|
|
1535
|
-
mounts: sidecarMounts,
|
|
1536
|
-
permissions: sidecarPermissions,
|
|
1537
|
-
commandPermissions: processed.commandPermissions,
|
|
1538
|
-
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1539
|
-
});
|
|
1540
|
-
if (toolKits && toolKits.length > 0) {
|
|
1541
|
-
toolReference = await registerToolkitsOnSidecar(client, session, nativeVm, toolKits);
|
|
1542
|
-
commandGuestPaths.set("agentos", "/bin/agentos");
|
|
1543
|
-
for (const toolKit of toolKits) {
|
|
1544
|
-
commandGuestPaths.set(`agentos-${toolKit.name}`, `/bin/agentos-${toolKit.name}`);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
661
|
+
env = { ...nativeVm.guestEnv };
|
|
662
|
+
createdNativeVm = nativeVm;
|
|
1547
663
|
rootBridge = new NativeSidecarKernelProxy({
|
|
1548
664
|
client,
|
|
1549
665
|
session,
|
|
1550
666
|
vm: nativeVm,
|
|
1551
|
-
env,
|
|
1552
|
-
cwd:
|
|
667
|
+
env: nativeVm.guestEnv,
|
|
668
|
+
cwd: nativeVm.guestCwd,
|
|
1553
669
|
localMounts,
|
|
1554
|
-
|
|
670
|
+
sidecarMounts,
|
|
1555
671
|
onDispose: cleanup,
|
|
672
|
+
// The native process is owned by the AgentOsSidecar handle and
|
|
673
|
+
// shared across VMs; disposing this VM must not kill the process.
|
|
674
|
+
ownsClient: false,
|
|
1556
675
|
});
|
|
1557
|
-
await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
|
|
1558
676
|
kernel = rootBridge;
|
|
1559
|
-
const snapshotClient = client;
|
|
1560
677
|
return {
|
|
1561
678
|
env,
|
|
1562
|
-
hostMounts,
|
|
1563
679
|
kernel,
|
|
1564
680
|
rootView: rootBridge.createRootView(),
|
|
681
|
+
sidecarMounts,
|
|
1565
682
|
sidecarClient: client,
|
|
1566
683
|
sidecarSession: session,
|
|
1567
684
|
sidecarVm: nativeVm,
|
|
1568
|
-
permissions: hostPermissions,
|
|
1569
|
-
snapshotRootFilesystem: async () => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm))),
|
|
1570
685
|
toolKits: toolKits ?? [],
|
|
1571
|
-
toolReference,
|
|
1572
686
|
async dispose() {
|
|
1573
687
|
if (kernel) {
|
|
1574
688
|
const currentKernel = kernel;
|
|
@@ -1586,37 +700,48 @@ export class AgentOs {
|
|
|
1586
700
|
};
|
|
1587
701
|
}
|
|
1588
702
|
catch (error) {
|
|
703
|
+
// The native process is shared and owned by the sidecar handle, so
|
|
704
|
+
// never dispose the client here — only tear down this VM's resources.
|
|
1589
705
|
if (kernel) {
|
|
1590
|
-
await kernel.dispose().catch(() => {
|
|
706
|
+
await kernel.dispose().catch((cleanupError) => {
|
|
707
|
+
console.warn("failed to dispose kernel after VM startup failure", cleanupError);
|
|
708
|
+
});
|
|
1591
709
|
}
|
|
1592
710
|
if (rootBridge) {
|
|
1593
|
-
await rootBridge.dispose().catch(() => {
|
|
711
|
+
await rootBridge.dispose().catch((cleanupError) => {
|
|
712
|
+
console.warn("failed to dispose root bridge after VM startup failure", cleanupError);
|
|
713
|
+
});
|
|
1594
714
|
}
|
|
1595
715
|
else {
|
|
1596
|
-
|
|
716
|
+
if (createdNativeVm && nativeSession && client) {
|
|
717
|
+
await client
|
|
718
|
+
.disposeVm(nativeSession, createdNativeVm)
|
|
719
|
+
.catch((cleanupError) => {
|
|
720
|
+
console.warn("failed to dispose sidecar VM after startup failure", cleanupError);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
1597
723
|
await cleanup();
|
|
1598
724
|
}
|
|
1599
725
|
throw error;
|
|
1600
726
|
}
|
|
1601
727
|
};
|
|
1602
|
-
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1603
728
|
let sidecarLease = null;
|
|
1604
729
|
try {
|
|
1605
730
|
sidecarLease = await leaseAgentOsSidecarVm(sidecar, {
|
|
1606
731
|
createVm: async () => createVmAdmin(),
|
|
1607
732
|
});
|
|
1608
733
|
const vmAdmin = sidecarLease.admin;
|
|
1609
|
-
const vm = new AgentOs(vmAdmin.kernel, sidecar,
|
|
734
|
+
const vm = new AgentOs(vmAdmin.kernel, sidecar, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.onAgentStderr ?? defaultAgentStderrHandler, options?.onAgentExit ?? defaultAgentExitHandler, options?.onLimitWarning);
|
|
1610
735
|
vm._sidecarLease = sidecarLease;
|
|
1611
736
|
vm._toolKits = vmAdmin.toolKits;
|
|
1612
|
-
vm._toolReference = vmAdmin.toolReference;
|
|
1613
|
-
vm._permissions = vmAdmin.permissions;
|
|
1614
737
|
vm._installSidecarRequestHandler();
|
|
1615
|
-
vm._cronManager = new CronManager(
|
|
738
|
+
vm._cronManager = new CronManager(vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm);
|
|
1616
739
|
return vm;
|
|
1617
740
|
}
|
|
1618
741
|
catch (error) {
|
|
1619
|
-
await sidecarLease?.dispose().catch(() => {
|
|
742
|
+
await sidecarLease?.dispose().catch((cleanupError) => {
|
|
743
|
+
console.warn("failed to dispose sidecar lease after AgentOs.create failure", cleanupError);
|
|
744
|
+
});
|
|
1620
745
|
throw error;
|
|
1621
746
|
}
|
|
1622
747
|
}
|
|
@@ -1627,23 +752,76 @@ export class AgentOs {
|
|
|
1627
752
|
const kernel = this.#kernel;
|
|
1628
753
|
return kernel.execArgv(command, args, options);
|
|
1629
754
|
}
|
|
1630
|
-
|
|
755
|
+
_pruneCompletedProcessRoutes() {
|
|
756
|
+
let completedRoutes = 0;
|
|
757
|
+
for (const entry of this._processes.values()) {
|
|
758
|
+
if (entry.state !== "running")
|
|
759
|
+
completedRoutes++;
|
|
760
|
+
}
|
|
761
|
+
let removeCount = Math.max(0, completedRoutes - this._sidecarVm.processRouteRetention);
|
|
762
|
+
for (const [pid, entry] of this._processes) {
|
|
763
|
+
if (removeCount === 0)
|
|
764
|
+
break;
|
|
765
|
+
if (entry.state !== "running") {
|
|
766
|
+
this._processes.delete(pid);
|
|
767
|
+
removeCount--;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async _trackProcess(proc, stdoutHandlers, stderrHandlers, exitHandlers) {
|
|
772
|
+
const existing = this._processes.get(proc.pid);
|
|
773
|
+
if (existing?.state === "running") {
|
|
774
|
+
const duplicateError = new Error(`Sidecar returned an already-active kernel pid: ${proc.pid}`);
|
|
775
|
+
try {
|
|
776
|
+
await proc.kill(9);
|
|
777
|
+
}
|
|
778
|
+
catch (cleanupError) {
|
|
779
|
+
throw new AggregateError([duplicateError, cleanupError], "Sidecar returned a duplicate active pid and cleanup failed");
|
|
780
|
+
}
|
|
781
|
+
throw duplicateError;
|
|
782
|
+
}
|
|
783
|
+
this._processes.delete(proc.pid);
|
|
1631
784
|
const entry = {
|
|
785
|
+
state: "running",
|
|
1632
786
|
proc,
|
|
1633
|
-
command,
|
|
1634
|
-
args,
|
|
1635
787
|
stdoutHandlers,
|
|
1636
788
|
stderrHandlers,
|
|
1637
789
|
exitHandlers,
|
|
1638
790
|
};
|
|
1639
791
|
this._processes.set(proc.pid, entry);
|
|
1640
|
-
void proc
|
|
1641
|
-
|
|
1642
|
-
|
|
792
|
+
void proc
|
|
793
|
+
.wait()
|
|
794
|
+
.then((code) => {
|
|
795
|
+
if (this._processes.get(proc.pid) !== entry)
|
|
796
|
+
return;
|
|
797
|
+
const handlers = [...exitHandlers];
|
|
798
|
+
stdoutHandlers.clear();
|
|
799
|
+
stderrHandlers.clear();
|
|
800
|
+
exitHandlers.clear();
|
|
801
|
+
this._processes.delete(proc.pid);
|
|
802
|
+
this._processes.set(proc.pid, { state: "exited", exitCode: code });
|
|
803
|
+
this._pruneCompletedProcessRoutes();
|
|
804
|
+
for (const handler of handlers)
|
|
805
|
+
handler(code);
|
|
806
|
+
})
|
|
807
|
+
.catch((error) => {
|
|
808
|
+
if (this._processes.get(proc.pid) === entry) {
|
|
809
|
+
const routeError = error instanceof Error ? error : new Error(String(error));
|
|
810
|
+
stdoutHandlers.clear();
|
|
811
|
+
stderrHandlers.clear();
|
|
812
|
+
exitHandlers.clear();
|
|
813
|
+
this._processes.delete(proc.pid);
|
|
814
|
+
this._processes.set(proc.pid, {
|
|
815
|
+
state: "failed",
|
|
816
|
+
error: routeError,
|
|
817
|
+
});
|
|
818
|
+
this._pruneCompletedProcessRoutes();
|
|
819
|
+
}
|
|
820
|
+
console.error(`[agent-os] process ${proc.pid} wait failed`, error);
|
|
1643
821
|
});
|
|
1644
822
|
return { pid: proc.pid };
|
|
1645
823
|
}
|
|
1646
|
-
spawn(command, args, options) {
|
|
824
|
+
async spawn(command, args, options) {
|
|
1647
825
|
const stdoutHandlers = new Set();
|
|
1648
826
|
const stderrHandlers = new Set();
|
|
1649
827
|
const exitHandlers = new Set();
|
|
@@ -1652,7 +830,7 @@ export class AgentOs {
|
|
|
1652
830
|
stdoutHandlers.add(options.onStdout);
|
|
1653
831
|
if (options?.onStderr)
|
|
1654
832
|
stderrHandlers.add(options.onStderr);
|
|
1655
|
-
const proc = this.#kernel.spawn(command, args, {
|
|
833
|
+
const proc = await this.#kernel.spawn(command, args, {
|
|
1656
834
|
...options,
|
|
1657
835
|
onStdout: (data) => {
|
|
1658
836
|
for (const h of stdoutHandlers)
|
|
@@ -1663,27 +841,39 @@ export class AgentOs {
|
|
|
1663
841
|
h(data);
|
|
1664
842
|
},
|
|
1665
843
|
});
|
|
1666
|
-
return this._trackProcess(proc,
|
|
844
|
+
return await this._trackProcess(proc, stdoutHandlers, stderrHandlers, exitHandlers);
|
|
1667
845
|
}
|
|
1668
846
|
/** Write data to a process's stdin. */
|
|
1669
847
|
writeProcessStdin(pid, data) {
|
|
1670
848
|
const entry = this._processes.get(pid);
|
|
1671
849
|
if (!entry)
|
|
1672
850
|
throw new Error(`Process not found: ${pid}`);
|
|
1673
|
-
entry.
|
|
851
|
+
if (entry.state === "exited")
|
|
852
|
+
return Promise.resolve();
|
|
853
|
+
if (entry.state === "failed")
|
|
854
|
+
return Promise.reject(entry.error);
|
|
855
|
+
return entry.proc.writeStdin(data);
|
|
1674
856
|
}
|
|
1675
857
|
/** Close a process's stdin stream. */
|
|
1676
858
|
closeProcessStdin(pid) {
|
|
1677
859
|
const entry = this._processes.get(pid);
|
|
1678
860
|
if (!entry)
|
|
1679
861
|
throw new Error(`Process not found: ${pid}`);
|
|
1680
|
-
entry.
|
|
862
|
+
if (entry.state === "exited")
|
|
863
|
+
return Promise.resolve();
|
|
864
|
+
if (entry.state === "failed")
|
|
865
|
+
return Promise.reject(entry.error);
|
|
866
|
+
return entry.proc.closeStdin();
|
|
1681
867
|
}
|
|
1682
868
|
/** Subscribe to stdout data from a process. Returns an unsubscribe function. */
|
|
1683
869
|
onProcessStdout(pid, handler) {
|
|
1684
870
|
const entry = this._processes.get(pid);
|
|
1685
871
|
if (!entry)
|
|
1686
872
|
throw new Error(`Process not found: ${pid}`);
|
|
873
|
+
if (entry.state === "exited")
|
|
874
|
+
return () => { };
|
|
875
|
+
if (entry.state === "failed")
|
|
876
|
+
throw entry.error;
|
|
1687
877
|
entry.stdoutHandlers.add(handler);
|
|
1688
878
|
return () => {
|
|
1689
879
|
entry.stdoutHandlers.delete(handler);
|
|
@@ -1694,6 +884,10 @@ export class AgentOs {
|
|
|
1694
884
|
const entry = this._processes.get(pid);
|
|
1695
885
|
if (!entry)
|
|
1696
886
|
throw new Error(`Process not found: ${pid}`);
|
|
887
|
+
if (entry.state === "exited")
|
|
888
|
+
return () => { };
|
|
889
|
+
if (entry.state === "failed")
|
|
890
|
+
throw entry.error;
|
|
1697
891
|
entry.stderrHandlers.add(handler);
|
|
1698
892
|
return () => {
|
|
1699
893
|
entry.stderrHandlers.delete(handler);
|
|
@@ -1705,10 +899,12 @@ export class AgentOs {
|
|
|
1705
899
|
if (!entry)
|
|
1706
900
|
throw new Error(`Process not found: ${pid}`);
|
|
1707
901
|
// If already exited, call immediately.
|
|
1708
|
-
if (entry.
|
|
1709
|
-
handler(entry.
|
|
902
|
+
if (entry.state === "exited") {
|
|
903
|
+
handler(entry.exitCode);
|
|
1710
904
|
return () => { };
|
|
1711
905
|
}
|
|
906
|
+
if (entry.state === "failed")
|
|
907
|
+
throw entry.error;
|
|
1712
908
|
entry.exitHandlers.add(handler);
|
|
1713
909
|
return () => {
|
|
1714
910
|
entry.exitHandlers.delete(handler);
|
|
@@ -1719,226 +915,68 @@ export class AgentOs {
|
|
|
1719
915
|
const entry = this._processes.get(pid);
|
|
1720
916
|
if (!entry)
|
|
1721
917
|
throw new Error(`Process not found: ${pid}`);
|
|
918
|
+
if (entry.state === "exited")
|
|
919
|
+
return Promise.resolve(entry.exitCode);
|
|
920
|
+
if (entry.state === "failed")
|
|
921
|
+
return Promise.reject(entry.error);
|
|
1722
922
|
return entry.proc.wait();
|
|
1723
923
|
}
|
|
1724
|
-
_assertSafeAbsolutePath(path) {
|
|
1725
|
-
if (!path.startsWith("/")) {
|
|
1726
|
-
throw new Error(`Path must be absolute: ${path}`);
|
|
1727
|
-
}
|
|
1728
|
-
if (posixPath.normalize(path) !== path) {
|
|
1729
|
-
throw new Error(`Path must be normalized: ${path}`);
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
_assertWritableAbsolutePath(path) {
|
|
1733
|
-
this._assertSafeAbsolutePath(path);
|
|
1734
|
-
if (path === "/proc" || path.startsWith("/proc/")) {
|
|
1735
|
-
throw new Error(`Path is read-only: ${path}`);
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
_vfs() {
|
|
1739
|
-
return this.#kernel.vfs;
|
|
1740
|
-
}
|
|
1741
|
-
async _copyPath(from, to) {
|
|
1742
|
-
const stat = await this._vfs().lstat(from);
|
|
1743
|
-
if (stat.isSymbolicLink) {
|
|
1744
|
-
const target = await this._vfs().readlink(from);
|
|
1745
|
-
await this._vfs().symlink(target, to);
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
if (stat.isDirectory) {
|
|
1749
|
-
await this._mkdirp(posixPath.dirname(to));
|
|
1750
|
-
if (!(await this.#kernel.exists(to))) {
|
|
1751
|
-
await this.#kernel.mkdir(to);
|
|
1752
|
-
}
|
|
1753
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1754
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1755
|
-
const entries = await this.#kernel.readdir(from);
|
|
1756
|
-
for (const entry of entries) {
|
|
1757
|
-
if (entry === "." || entry === "..")
|
|
1758
|
-
continue;
|
|
1759
|
-
const fromPath = from === "/" ? `/${entry}` : `${from}/${entry}`;
|
|
1760
|
-
const toPath = to === "/" ? `/${entry}` : `${to}/${entry}`;
|
|
1761
|
-
await this._copyPath(fromPath, toPath);
|
|
1762
|
-
}
|
|
1763
|
-
return;
|
|
1764
|
-
}
|
|
1765
|
-
const content = await this.#kernel.readFile(from);
|
|
1766
|
-
await this.writeFile(to, content);
|
|
1767
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1768
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1769
|
-
}
|
|
1770
924
|
async readFile(path) {
|
|
1771
|
-
this._assertSafeAbsolutePath(path);
|
|
1772
925
|
return this.#kernel.readFile(path);
|
|
1773
926
|
}
|
|
1774
927
|
async writeFile(path, content) {
|
|
1775
|
-
this._assertWritableAbsolutePath(path);
|
|
1776
928
|
return this.#kernel.writeFile(path, content);
|
|
1777
929
|
}
|
|
1778
|
-
async writeFiles(entries) {
|
|
1779
|
-
const results = [];
|
|
1780
|
-
for (const entry of entries) {
|
|
1781
|
-
try {
|
|
1782
|
-
this._assertWritableAbsolutePath(entry.path);
|
|
1783
|
-
// Create parent directories as needed
|
|
1784
|
-
const parentDir = entry.path.substring(0, entry.path.lastIndexOf("/"));
|
|
1785
|
-
if (parentDir) {
|
|
1786
|
-
await this._mkdirp(parentDir);
|
|
1787
|
-
}
|
|
1788
|
-
await this.#kernel.writeFile(entry.path, entry.content);
|
|
1789
|
-
results.push({ path: entry.path, success: true });
|
|
1790
|
-
}
|
|
1791
|
-
catch (err) {
|
|
1792
|
-
results.push({
|
|
1793
|
-
path: entry.path,
|
|
1794
|
-
success: false,
|
|
1795
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1796
|
-
});
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
return results;
|
|
1800
|
-
}
|
|
1801
|
-
async readFiles(paths) {
|
|
1802
|
-
const results = [];
|
|
1803
|
-
for (const path of paths) {
|
|
1804
|
-
try {
|
|
1805
|
-
this._assertSafeAbsolutePath(path);
|
|
1806
|
-
const content = await this.#kernel.readFile(path);
|
|
1807
|
-
results.push({ path, content });
|
|
1808
|
-
}
|
|
1809
|
-
catch (err) {
|
|
1810
|
-
results.push({
|
|
1811
|
-
path,
|
|
1812
|
-
content: null,
|
|
1813
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
return results;
|
|
1818
|
-
}
|
|
1819
|
-
/** Recursively create directories (mkdir -p). */
|
|
1820
|
-
async _mkdirp(path) {
|
|
1821
|
-
this._assertWritableAbsolutePath(path);
|
|
1822
|
-
const parts = path.split("/").filter(Boolean);
|
|
1823
|
-
let current = "";
|
|
1824
|
-
for (const part of parts) {
|
|
1825
|
-
current += `/${part}`;
|
|
1826
|
-
if (!(await this.#kernel.exists(current))) {
|
|
1827
|
-
await this.#kernel.mkdir(current);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
930
|
async mkdir(path, options) {
|
|
1832
|
-
|
|
1833
|
-
return this._mkdirp(path);
|
|
1834
|
-
}
|
|
1835
|
-
this._assertSafeAbsolutePath(path);
|
|
1836
|
-
return this.#kernel.mkdir(path);
|
|
931
|
+
return this.#kernel.mkdir(path, options);
|
|
1837
932
|
}
|
|
1838
933
|
async readdir(path) {
|
|
1839
|
-
this._assertSafeAbsolutePath(path);
|
|
1840
934
|
return this.#kernel.readdir(path);
|
|
1841
935
|
}
|
|
1842
936
|
async readdirRecursive(path, options) {
|
|
1843
|
-
this.
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
for (const name of entries) {
|
|
1856
|
-
if (name === "." || name === "..")
|
|
1857
|
-
continue;
|
|
1858
|
-
if (exclude?.has(name))
|
|
1859
|
-
continue;
|
|
1860
|
-
const fullPath = dirPath === "/" ? `/${name}` : `${dirPath}/${name}`;
|
|
1861
|
-
const s = await this.#kernel.stat(fullPath);
|
|
1862
|
-
if (s.isSymbolicLink) {
|
|
1863
|
-
results.push({
|
|
1864
|
-
path: fullPath,
|
|
1865
|
-
type: "symlink",
|
|
1866
|
-
size: s.size,
|
|
1867
|
-
});
|
|
1868
|
-
}
|
|
1869
|
-
else if (s.isDirectory) {
|
|
1870
|
-
results.push({
|
|
1871
|
-
path: fullPath,
|
|
1872
|
-
type: "directory",
|
|
1873
|
-
size: s.size,
|
|
1874
|
-
});
|
|
1875
|
-
if (maxDepth === undefined || depth < maxDepth) {
|
|
1876
|
-
queue.push([fullPath, depth + 1]);
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
else {
|
|
1880
|
-
results.push({
|
|
1881
|
-
path: fullPath,
|
|
1882
|
-
type: "file",
|
|
1883
|
-
size: s.size,
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
return results;
|
|
937
|
+
const entries = await this.#kernel.readdirRecursive(path, {
|
|
938
|
+
maxDepth: options?.maxDepth,
|
|
939
|
+
});
|
|
940
|
+
return entries.map((entry) => ({
|
|
941
|
+
path: entry.path,
|
|
942
|
+
type: entry.isSymbolicLink
|
|
943
|
+
? "symlink"
|
|
944
|
+
: entry.isDirectory
|
|
945
|
+
? "directory"
|
|
946
|
+
: "file",
|
|
947
|
+
size: entry.size,
|
|
948
|
+
}));
|
|
1889
949
|
}
|
|
1890
950
|
async stat(path) {
|
|
1891
|
-
this._assertSafeAbsolutePath(path);
|
|
1892
951
|
return this.#kernel.stat(path);
|
|
1893
952
|
}
|
|
1894
953
|
async exists(path) {
|
|
1895
|
-
this._assertSafeAbsolutePath(path);
|
|
1896
954
|
return this.#kernel.exists(path);
|
|
1897
955
|
}
|
|
1898
956
|
async snapshotRootFilesystem() {
|
|
1899
|
-
|
|
1900
|
-
if (nativeSnapshot) {
|
|
1901
|
-
return nativeSnapshot();
|
|
1902
|
-
}
|
|
1903
|
-
return createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
|
|
957
|
+
return createSnapshotExport(convertSidecarRootSnapshotEntries(await this._sidecarClient.snapshotRootFilesystem(this._sidecarSession, this._sidecarVm)));
|
|
1904
958
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
959
|
+
/**
|
|
960
|
+
* Mount a filesystem into the running VM. Resolves once the mount has been
|
|
961
|
+
* delivered to the native sidecar, so guest code can use it immediately
|
|
962
|
+
* after the returned promise settles; a delivery failure rejects instead of
|
|
963
|
+
* leaving the mount silently host-only.
|
|
964
|
+
*/
|
|
965
|
+
async mountFs(path, driver, options) {
|
|
966
|
+
await this.#kernel.mountFs(path, driver, { readOnly: options?.readOnly });
|
|
1908
967
|
}
|
|
1909
|
-
unmountFs(path) {
|
|
1910
|
-
this.
|
|
1911
|
-
this.#kernel.unmountFs(path);
|
|
968
|
+
async unmountFs(path) {
|
|
969
|
+
await this.#kernel.unmountFs(path);
|
|
1912
970
|
}
|
|
1913
971
|
async move(from, to) {
|
|
1914
|
-
this.
|
|
1915
|
-
this._assertSafeAbsolutePath(to);
|
|
1916
|
-
const sourceStat = await this._vfs().lstat(from);
|
|
1917
|
-
if (!sourceStat.isDirectory || sourceStat.isSymbolicLink) {
|
|
1918
|
-
return this.#kernel.rename(from, to);
|
|
1919
|
-
}
|
|
1920
|
-
await this._copyPath(from, to);
|
|
1921
|
-
await this.delete(from, { recursive: true });
|
|
972
|
+
await this.#kernel.movePath(from, to);
|
|
1922
973
|
}
|
|
1923
974
|
async delete(path, options) {
|
|
1924
|
-
this.
|
|
1925
|
-
const s = await this.#kernel.stat(path);
|
|
1926
|
-
if (s.isDirectory) {
|
|
1927
|
-
if (options?.recursive) {
|
|
1928
|
-
const entries = await this.#kernel.readdir(path);
|
|
1929
|
-
for (const entry of entries) {
|
|
1930
|
-
if (entry === "." || entry === "..")
|
|
1931
|
-
continue;
|
|
1932
|
-
await this.delete(`${path}/${entry}`, { recursive: true });
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
return this.#kernel.removeDir(path);
|
|
1936
|
-
}
|
|
1937
|
-
return this.#kernel.removeFile(path);
|
|
975
|
+
await this.#kernel.removePath(path, options);
|
|
1938
976
|
}
|
|
1939
977
|
async fetch(port, request) {
|
|
1940
978
|
const url = new URL(request.url);
|
|
1941
|
-
const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
979
|
+
const responsePayload = requireVmFetchResponsePayload(JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
1942
980
|
port,
|
|
1943
981
|
method: request.method,
|
|
1944
982
|
path: `${url.pathname}${url.search}`,
|
|
@@ -1946,22 +984,21 @@ export class AgentOs {
|
|
|
1946
984
|
...(request.method !== "GET" && request.method !== "HEAD"
|
|
1947
985
|
? { body: await request.text() }
|
|
1948
986
|
: {}),
|
|
1949
|
-
}));
|
|
987
|
+
})));
|
|
1950
988
|
const headers = new Headers();
|
|
1951
|
-
for (const [key, value] of responsePayload.headers
|
|
989
|
+
for (const [key, value] of responsePayload.headers) {
|
|
1952
990
|
headers.append(key, value);
|
|
1953
991
|
}
|
|
1954
|
-
return new Response(Buffer.from(responsePayload.body
|
|
992
|
+
return new Response(Buffer.from(responsePayload.body, "base64"), {
|
|
1955
993
|
status: responsePayload.status,
|
|
1956
994
|
statusText: responsePayload.statusText,
|
|
1957
995
|
headers,
|
|
1958
996
|
});
|
|
1959
997
|
}
|
|
1960
|
-
openShell(options) {
|
|
1961
|
-
const shellId = `shell-${++this._shellCounter}`;
|
|
1962
|
-
this._closedShellIds.delete(shellId);
|
|
998
|
+
async openShell(options) {
|
|
1963
999
|
const dataHandlers = new Set();
|
|
1964
|
-
const handle = this.#kernel.openShell(options);
|
|
1000
|
+
const handle = await this.#kernel.openShell(options);
|
|
1001
|
+
const shellId = handle.processId;
|
|
1965
1002
|
handle.onData = (data) => {
|
|
1966
1003
|
for (const h of dataHandlers)
|
|
1967
1004
|
h(data);
|
|
@@ -1969,152 +1006,165 @@ export class AgentOs {
|
|
|
1969
1006
|
const entry = {
|
|
1970
1007
|
handle,
|
|
1971
1008
|
dataHandlers,
|
|
1972
|
-
exitPromise: Promise.resolve(),
|
|
1009
|
+
exitPromise: Promise.resolve(0),
|
|
1010
|
+
closing: false,
|
|
1973
1011
|
};
|
|
1974
|
-
const exitPromise = handle.wait()
|
|
1975
|
-
|
|
1976
|
-
this._pendingShellExitPromises.delete(
|
|
1012
|
+
const exitPromise = handle.wait();
|
|
1013
|
+
const finalize = () => {
|
|
1014
|
+
this._pendingShellExitPromises.delete(shellId);
|
|
1977
1015
|
if (this._shells.get(shellId) === entry) {
|
|
1978
1016
|
this._shells.delete(shellId);
|
|
1979
|
-
this._closedShellIds.add(shellId);
|
|
1980
1017
|
}
|
|
1018
|
+
};
|
|
1019
|
+
entry.exitPromise = exitPromise.then((exitCode) => {
|
|
1020
|
+
finalize();
|
|
1021
|
+
return exitCode;
|
|
1022
|
+
}, (error) => {
|
|
1023
|
+
finalize();
|
|
1024
|
+
throw error;
|
|
1981
1025
|
});
|
|
1982
|
-
this._pendingShellExitPromises.
|
|
1026
|
+
this._pendingShellExitPromises.set(shellId, entry.exitPromise);
|
|
1983
1027
|
this._shells.set(shellId, entry);
|
|
1984
1028
|
return { shellId };
|
|
1985
1029
|
}
|
|
1986
|
-
async connectTerminal(options) {
|
|
1987
|
-
return this.#kernel.connectTerminal(options);
|
|
1988
|
-
}
|
|
1989
1030
|
/** Write data to a shell's PTY input. */
|
|
1990
1031
|
writeShell(shellId, data) {
|
|
1991
1032
|
const entry = this._shells.get(shellId);
|
|
1992
|
-
if (!entry)
|
|
1033
|
+
if (!entry || entry.closing)
|
|
1993
1034
|
throw new Error(`Shell not found: ${shellId}`);
|
|
1994
|
-
entry.handle.write(data);
|
|
1035
|
+
return entry.handle.write(data);
|
|
1995
1036
|
}
|
|
1996
1037
|
/** Subscribe to data output from a shell. Returns an unsubscribe function. */
|
|
1997
1038
|
onShellData(shellId, handler) {
|
|
1998
1039
|
const entry = this._shells.get(shellId);
|
|
1999
|
-
if (!entry)
|
|
1040
|
+
if (!entry || entry.closing)
|
|
2000
1041
|
throw new Error(`Shell not found: ${shellId}`);
|
|
2001
1042
|
entry.dataHandlers.add(handler);
|
|
2002
1043
|
return () => {
|
|
2003
1044
|
entry.dataHandlers.delete(handler);
|
|
2004
1045
|
};
|
|
2005
1046
|
}
|
|
2006
|
-
/** Notify a shell of terminal resize. */
|
|
2007
|
-
resizeShell(shellId, cols, rows) {
|
|
1047
|
+
/** Notify a shell of terminal resize and await the sidecar response. */
|
|
1048
|
+
async resizeShell(shellId, cols, rows) {
|
|
2008
1049
|
const entry = this._shells.get(shellId);
|
|
2009
|
-
if (!entry)
|
|
1050
|
+
if (!entry || entry.closing)
|
|
2010
1051
|
throw new Error(`Shell not found: ${shellId}`);
|
|
2011
|
-
entry.handle.resize(cols, rows);
|
|
1052
|
+
await entry.handle.resize(cols, rows);
|
|
2012
1053
|
}
|
|
2013
|
-
/**
|
|
2014
|
-
|
|
1054
|
+
/**
|
|
1055
|
+
* Wait for a shell to exit and return its sidecar-authoritative exit code.
|
|
1056
|
+
* A late wait reads the retained sidecar process snapshot.
|
|
1057
|
+
*/
|
|
1058
|
+
async waitShell(shellId) {
|
|
2015
1059
|
const entry = this._shells.get(shellId);
|
|
2016
|
-
if (
|
|
2017
|
-
|
|
2018
|
-
|
|
1060
|
+
if (entry) {
|
|
1061
|
+
return entry.exitPromise;
|
|
1062
|
+
}
|
|
1063
|
+
const pending = this._pendingShellExitPromises.get(shellId);
|
|
1064
|
+
if (pending) {
|
|
1065
|
+
return pending;
|
|
1066
|
+
}
|
|
1067
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
1068
|
+
const process = await this.#kernel.processSnapshotById(shellId);
|
|
1069
|
+
if (process?.status === "exited" && process.exitCode !== null) {
|
|
1070
|
+
return process.exitCode;
|
|
2019
1071
|
}
|
|
2020
|
-
throw new Error(`Shell not found: ${shellId}`);
|
|
2021
1072
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
if (!relativePath) {
|
|
2033
|
-
return mount.hostPath;
|
|
1073
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
1074
|
+
}
|
|
1075
|
+
/** Kill a shell process, await the sidecar response, and remove it from tracking. */
|
|
1076
|
+
async closeShell(shellId) {
|
|
1077
|
+
const entry = this._shells.get(shellId);
|
|
1078
|
+
if (!entry) {
|
|
1079
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
1080
|
+
const process = await this.#kernel.processSnapshotById(shellId);
|
|
1081
|
+
if (process?.status === "exited") {
|
|
1082
|
+
return;
|
|
2034
1083
|
}
|
|
2035
|
-
return join(mount.hostPath, ...relativePath.split("/").filter(Boolean));
|
|
2036
1084
|
}
|
|
1085
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2037
1086
|
}
|
|
2038
|
-
|
|
1087
|
+
if (entry.closing) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await entry.handle.kill();
|
|
1091
|
+
entry.closing = true;
|
|
2039
1092
|
}
|
|
2040
|
-
/** Returns info
|
|
2041
|
-
listProcesses() {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
1093
|
+
/** Returns sidecar-authoritative info for processes spawned via spawn(). */
|
|
1094
|
+
async listProcesses() {
|
|
1095
|
+
const processByPid = new Map((await this.allProcesses()).map((process) => [process.pid, process]));
|
|
1096
|
+
return [...this._processes.keys()].map((pid) => {
|
|
1097
|
+
const process = processByPid.get(pid);
|
|
1098
|
+
if (!process) {
|
|
1099
|
+
throw new Error(`Sidecar process snapshot is missing tracked process: ${pid}`);
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
pid: process.pid,
|
|
1103
|
+
command: process.command,
|
|
1104
|
+
args: process.args.slice(1),
|
|
1105
|
+
running: process.status !== "exited",
|
|
1106
|
+
exitCode: process.exitCode,
|
|
1107
|
+
};
|
|
1108
|
+
});
|
|
2049
1109
|
}
|
|
2050
1110
|
/** Returns all kernel processes across all active runtimes (WASM and Node). */
|
|
2051
|
-
allProcesses() {
|
|
1111
|
+
async allProcesses() {
|
|
2052
1112
|
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
2053
|
-
return this.#kernel.snapshotProcesses();
|
|
1113
|
+
return await this.#kernel.snapshotProcesses();
|
|
2054
1114
|
}
|
|
2055
1115
|
return [...this.#kernel.processes.values()];
|
|
2056
1116
|
}
|
|
2057
|
-
/** Returns processes organized as a tree using ppid relationships. */
|
|
2058
|
-
processTree() {
|
|
2059
|
-
const all = this.allProcesses();
|
|
2060
|
-
const nodeMap = new Map();
|
|
2061
|
-
// Index: create a tree node for each process
|
|
2062
|
-
for (const proc of all) {
|
|
2063
|
-
nodeMap.set(proc.pid, { ...proc, children: [] });
|
|
2064
|
-
}
|
|
2065
|
-
// Wire: attach each node to its parent
|
|
2066
|
-
const roots = [];
|
|
2067
|
-
for (const node of nodeMap.values()) {
|
|
2068
|
-
const parent = nodeMap.get(node.ppid);
|
|
2069
|
-
if (parent) {
|
|
2070
|
-
parent.children.push(node);
|
|
2071
|
-
}
|
|
2072
|
-
else {
|
|
2073
|
-
roots.push(node);
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
return roots;
|
|
2077
|
-
}
|
|
2078
1117
|
/** Returns info about a specific process by PID. Throws if not found. */
|
|
2079
|
-
getProcess(pid) {
|
|
2080
|
-
|
|
2081
|
-
if (!entry) {
|
|
1118
|
+
async getProcess(pid) {
|
|
1119
|
+
if (!this._processes.has(pid)) {
|
|
2082
1120
|
throw new Error(`Process not found: ${pid}`);
|
|
2083
1121
|
}
|
|
1122
|
+
const process = (await this.allProcesses()).find((candidate) => candidate.pid === pid);
|
|
1123
|
+
if (!process) {
|
|
1124
|
+
throw new Error(`Sidecar process snapshot is missing tracked process: ${pid}`);
|
|
1125
|
+
}
|
|
2084
1126
|
return {
|
|
2085
|
-
pid:
|
|
2086
|
-
command:
|
|
2087
|
-
args:
|
|
2088
|
-
running:
|
|
2089
|
-
exitCode:
|
|
1127
|
+
pid: process.pid,
|
|
1128
|
+
command: process.command,
|
|
1129
|
+
args: process.args.slice(1),
|
|
1130
|
+
running: process.status !== "exited",
|
|
1131
|
+
exitCode: process.exitCode,
|
|
2090
1132
|
};
|
|
2091
1133
|
}
|
|
2092
1134
|
/** Send SIGTERM to gracefully stop a process. No-op if already exited. */
|
|
2093
|
-
stopProcess(pid) {
|
|
1135
|
+
async stopProcess(pid) {
|
|
2094
1136
|
const entry = this._processes.get(pid);
|
|
2095
1137
|
if (!entry) {
|
|
2096
1138
|
throw new Error(`Process not found: ${pid}`);
|
|
2097
1139
|
}
|
|
2098
|
-
if (entry.
|
|
1140
|
+
if (entry.state === "exited")
|
|
2099
1141
|
return;
|
|
2100
|
-
entry.
|
|
1142
|
+
if (entry.state === "failed")
|
|
1143
|
+
throw entry.error;
|
|
1144
|
+
await entry.proc.kill();
|
|
2101
1145
|
}
|
|
2102
1146
|
/** Send SIGKILL to force-kill a process. No-op if already exited. */
|
|
2103
|
-
killProcess(pid) {
|
|
1147
|
+
async killProcess(pid) {
|
|
2104
1148
|
const entry = this._processes.get(pid);
|
|
2105
1149
|
if (!entry) {
|
|
2106
1150
|
throw new Error(`Process not found: ${pid}`);
|
|
2107
1151
|
}
|
|
2108
|
-
if (entry.
|
|
1152
|
+
if (entry.state === "exited")
|
|
2109
1153
|
return;
|
|
2110
|
-
entry.
|
|
1154
|
+
if (entry.state === "failed")
|
|
1155
|
+
throw entry.error;
|
|
1156
|
+
await entry.proc.kill(9);
|
|
2111
1157
|
}
|
|
2112
|
-
/** Returns
|
|
2113
|
-
listSessions() {
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
})
|
|
1158
|
+
/** Returns the sidecar's authoritative active-session list. */
|
|
1159
|
+
async listSessions() {
|
|
1160
|
+
const response = await this._sendAcpRequest({
|
|
1161
|
+
tag: "AcpListSessionsRequest",
|
|
1162
|
+
val: { reserved: false },
|
|
1163
|
+
});
|
|
1164
|
+
if (response.tag !== "AcpListSessionsResponse") {
|
|
1165
|
+
throw new Error(`unexpected response to AcpListSessionsRequest: ${response.tag}`);
|
|
1166
|
+
}
|
|
1167
|
+
return response.val.sessions.map((session) => ({ ...session }));
|
|
2118
1168
|
}
|
|
2119
1169
|
/** Internal helper: retrieve a session or throw. */
|
|
2120
1170
|
_requireSession(sessionId) {
|
|
@@ -2124,87 +1174,49 @@ export class AgentOs {
|
|
|
2124
1174
|
}
|
|
2125
1175
|
return session;
|
|
2126
1176
|
}
|
|
2127
|
-
/**
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
}
|
|
2163
|
-
catch {
|
|
2164
|
-
// Package not installed
|
|
2165
|
-
}
|
|
2166
|
-
return {
|
|
2167
|
-
id,
|
|
2168
|
-
acpAdapter: config.acpAdapter,
|
|
2169
|
-
agentPackage: config.agentPackage,
|
|
2170
|
-
installed,
|
|
2171
|
-
};
|
|
2172
|
-
})
|
|
2173
|
-
.filter((entry) => entry !== null);
|
|
2174
|
-
}
|
|
2175
|
-
_syncSessionState(session, state) {
|
|
2176
|
-
session.processId = state.processId;
|
|
2177
|
-
session.pid = state.pid ?? null;
|
|
2178
|
-
session.closed = state.closed;
|
|
2179
|
-
session.modes = toSessionModes(state.modes);
|
|
2180
|
-
session.configOptions = toSessionConfigOptions(state.configOptions);
|
|
2181
|
-
this._applySyntheticConfigOverrides(session);
|
|
2182
|
-
session.capabilities = toAgentCapabilities(state.agentCapabilities);
|
|
2183
|
-
session.agentInfo = toAgentInfo(state.agentInfo);
|
|
2184
|
-
}
|
|
2185
|
-
_applySessionUpdate(session, notification) {
|
|
2186
|
-
if (notification.method !== "session/update") {
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
const params = toRecord(notification.params);
|
|
2190
|
-
const update = toRecord(params.update ?? params);
|
|
2191
|
-
const sessionUpdate = update.sessionUpdate;
|
|
2192
|
-
if (sessionUpdate === "current_mode_update" &&
|
|
2193
|
-
typeof update.currentModeId === "string" &&
|
|
2194
|
-
session.modes) {
|
|
2195
|
-
session.modes = {
|
|
2196
|
-
...session.modes,
|
|
2197
|
-
currentModeId: update.currentModeId,
|
|
2198
|
-
};
|
|
2199
|
-
}
|
|
2200
|
-
if ((sessionUpdate === "config_option_update" ||
|
|
2201
|
-
sessionUpdate === "config_options_update") &&
|
|
2202
|
-
Array.isArray(update.configOptions)) {
|
|
2203
|
-
session.configOptions = update.configOptions;
|
|
1177
|
+
/**
|
|
1178
|
+
* Dynamically link a software package into the RUNNING VM. The package's
|
|
1179
|
+
* `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
|
|
1180
|
+
* pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
|
|
1181
|
+
* writing into its staging dir is reflected live with no reboot. An `agent`
|
|
1182
|
+
* block registers the package for `createSession(name)`. Persists for the VM's
|
|
1183
|
+
* lifetime (and across a snapshot iff the volume persists).
|
|
1184
|
+
*/
|
|
1185
|
+
async linkSoftware(descriptor) {
|
|
1186
|
+
const ref = normalizePackageRef(descriptor);
|
|
1187
|
+
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
1188
|
+
// appends the package to its live host-backed staging dir; the commands
|
|
1189
|
+
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
1190
|
+
// duplicate command, surfaced here as a thrown error.
|
|
1191
|
+
await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, { path: ref.path });
|
|
1192
|
+
// The client parses no manifests: an `agent` block in the linked package is
|
|
1193
|
+
// picked up by the sidecar (it owns the projected `/opt/agentos` and answers
|
|
1194
|
+
// createSession/listAgents from it). Nothing to record client-side.
|
|
1195
|
+
}
|
|
1196
|
+
async providedCommands() {
|
|
1197
|
+
return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Returns all registered agents with their installation status. Thin forwarder:
|
|
1201
|
+
* sends `AcpListAgentsRequest` and maps the response. The sidecar enumerates the
|
|
1202
|
+
* projected `/opt/agentos` packages (the client parses no manifests). Every such
|
|
1203
|
+
* agent is a package materialized into the VM, so `installed` is always `true`.
|
|
1204
|
+
*/
|
|
1205
|
+
async listAgents() {
|
|
1206
|
+
const response = await this._sendAcpRequest({
|
|
1207
|
+
tag: "AcpListAgentsRequest",
|
|
1208
|
+
val: { reserved: false },
|
|
1209
|
+
});
|
|
1210
|
+
if (response.tag !== "AcpListAgentsResponse") {
|
|
1211
|
+
throw new Error(`unexpected list_agents response: ${response.tag}`);
|
|
2204
1212
|
}
|
|
1213
|
+
return response.val.agents.map((agent) => ({
|
|
1214
|
+
id: agent.id,
|
|
1215
|
+
installed: agent.installed,
|
|
1216
|
+
adapterEntrypoint: agent.adapterEntrypoint,
|
|
1217
|
+
}));
|
|
2205
1218
|
}
|
|
2206
1219
|
_recordSessionNotification(session, notification) {
|
|
2207
|
-
this._applySessionUpdate(session, notification);
|
|
2208
1220
|
if (shouldDispatchToSessionEventHandlers(notification)) {
|
|
2209
1221
|
this._dispatchSessionEvent(session, notification);
|
|
2210
1222
|
}
|
|
@@ -2235,8 +1247,8 @@ export class AgentOs {
|
|
|
2235
1247
|
try {
|
|
2236
1248
|
subscriber.handler(notification);
|
|
2237
1249
|
}
|
|
2238
|
-
catch {
|
|
2239
|
-
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
console.warn(`ACP session event subscriber failed for ${session.sessionId}`, error);
|
|
2240
1252
|
}
|
|
2241
1253
|
}
|
|
2242
1254
|
}
|
|
@@ -2249,961 +1261,492 @@ export class AgentOs {
|
|
|
2249
1261
|
session.eventHandlers.delete(subscriber);
|
|
2250
1262
|
};
|
|
2251
1263
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
1264
|
+
/**
|
|
1265
|
+
* Warn once per session (host-visible) that a tool-permission request was
|
|
1266
|
+
* auto-denied because no `onPermissionRequest` handler is registered. Shared
|
|
1267
|
+
* by both the bare-callback and JSON-RPC permission paths so the message and
|
|
1268
|
+
* the once-per-session guard cannot drift between them.
|
|
1269
|
+
*/
|
|
1270
|
+
_warnNoPermissionHandlerOnce(session, params) {
|
|
1271
|
+
if (session.warnedNoPermissionHandler) {
|
|
2254
1272
|
return;
|
|
2255
1273
|
}
|
|
2256
|
-
session.
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
return override === undefined
|
|
2262
|
-
? option
|
|
2263
|
-
: { ...option, currentValue: override };
|
|
2264
|
-
});
|
|
1274
|
+
session.warnedNoPermissionHandler = true;
|
|
1275
|
+
this._emitSessionWarning(session, `agentos: a tool-permission request (${this._permissionToolLabel(params)}) was ` +
|
|
1276
|
+
`auto-denied because no onPermissionRequest handler is registered for session ` +
|
|
1277
|
+
`${session.sessionId}. Register one with vm.onPermissionRequest(sessionId, ...) and ` +
|
|
1278
|
+
`reply via vm.respondPermission(...) to let the agent use tools.`);
|
|
2265
1279
|
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
return;
|
|
1280
|
+
/** Best-effort human label for the tool named in a permission request. */
|
|
1281
|
+
_permissionToolLabel(params) {
|
|
1282
|
+
if (typeof params.toolName === "string") {
|
|
1283
|
+
return params.toolName;
|
|
2270
1284
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
1285
|
+
const toolCall = params.toolCall;
|
|
1286
|
+
if (toolCall &&
|
|
1287
|
+
typeof toolCall === "object" &&
|
|
1288
|
+
typeof toolCall.title === "string") {
|
|
1289
|
+
return toolCall.title;
|
|
2273
1290
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
1291
|
+
return "a tool";
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Emit a host-visible warning for a session through the same agent-process log
|
|
1295
|
+
* channel that surfaces adapter stderr (`onAgentStderr`, default: process
|
|
1296
|
+
* stderr). Used for agent-os-owned diagnostics — e.g. a permission request
|
|
1297
|
+
* that was auto-denied because no host hook is registered — so they never fire
|
|
1298
|
+
* silently inside the sidecar.
|
|
1299
|
+
*/
|
|
1300
|
+
_emitSessionWarning(session, message) {
|
|
1301
|
+
const handler = this._agentStderrHandler;
|
|
1302
|
+
if (!handler) {
|
|
2284
1303
|
return;
|
|
2285
1304
|
}
|
|
2286
1305
|
try {
|
|
2287
|
-
|
|
1306
|
+
handler({
|
|
1307
|
+
sessionId: session.sessionId,
|
|
1308
|
+
agentType: session.agentType,
|
|
1309
|
+
processId: session.processId,
|
|
1310
|
+
pid: session.pid,
|
|
1311
|
+
chunk: new TextEncoder().encode(`${message}\n`),
|
|
1312
|
+
});
|
|
2288
1313
|
}
|
|
2289
|
-
catch {
|
|
2290
|
-
|
|
1314
|
+
catch (error) {
|
|
1315
|
+
console.warn(`ACP warning handler failed for ${session.sessionId}`, error);
|
|
2291
1316
|
}
|
|
2292
1317
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
1318
|
+
_recordAgentStderr(event) {
|
|
1319
|
+
const session = this._sessions.get(event.sessionId);
|
|
1320
|
+
const handler = this._agentStderrHandler;
|
|
1321
|
+
if (!handler) {
|
|
2295
1322
|
return;
|
|
2296
1323
|
}
|
|
2297
1324
|
try {
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(event.val.notification)));
|
|
2306
|
-
return;
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
1325
|
+
handler({
|
|
1326
|
+
sessionId: event.sessionId,
|
|
1327
|
+
agentType: event.agentType,
|
|
1328
|
+
processId: event.processId,
|
|
1329
|
+
pid: session?.pid ?? null,
|
|
1330
|
+
chunk: new Uint8Array(event.chunk),
|
|
1331
|
+
});
|
|
2309
1332
|
}
|
|
2310
|
-
catch {
|
|
2311
|
-
|
|
1333
|
+
catch (error) {
|
|
1334
|
+
console.warn(`ACP stderr handler failed for ${event.sessionId}`, error);
|
|
2312
1335
|
}
|
|
2313
1336
|
}
|
|
2314
|
-
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
jsonrpc: "2.0",
|
|
2320
|
-
id: null,
|
|
2321
|
-
error: {
|
|
2322
|
-
code: -32601,
|
|
2323
|
-
message,
|
|
2324
|
-
},
|
|
2325
|
-
};
|
|
2326
|
-
}
|
|
2327
|
-
async _sendAcpRequest(request) {
|
|
2328
|
-
const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
|
|
2329
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
2330
|
-
payload: encodeAcpRequest(request),
|
|
2331
|
-
});
|
|
2332
|
-
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
2333
|
-
throw new Error(`unexpected ACP Ext namespace: ${envelope.namespace}`);
|
|
2334
|
-
}
|
|
2335
|
-
const response = decodeAcpResponse(envelope.payload);
|
|
2336
|
-
if (response.tag === "AcpErrorResponse") {
|
|
2337
|
-
const error = new Error(response.val.message);
|
|
2338
|
-
error.code = response.val.code;
|
|
2339
|
-
throw error;
|
|
1337
|
+
_recordAgentExit(event) {
|
|
1338
|
+
const session = this._sessions.get(event.sessionId);
|
|
1339
|
+
const handler = this._agentExitHandler;
|
|
1340
|
+
if (!handler) {
|
|
1341
|
+
return;
|
|
2340
1342
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
},
|
|
2352
|
-
};
|
|
2353
|
-
resolvers.add(resolver);
|
|
2354
|
-
this._pendingSessionRequestResolvers.set(sessionId, resolvers);
|
|
2355
|
-
void this._sendAcpRequest({
|
|
2356
|
-
tag: "AcpSessionRequest",
|
|
2357
|
-
val: {
|
|
2358
|
-
sessionId,
|
|
2359
|
-
method,
|
|
2360
|
-
params: params === undefined ? null : JSON.stringify(params),
|
|
2361
|
-
},
|
|
2362
|
-
})
|
|
2363
|
-
.then((response) => {
|
|
2364
|
-
if (response.tag !== "AcpSessionRpcResponse") {
|
|
2365
|
-
throw new Error(`unexpected response to AcpSessionRequest: ${response.tag}`);
|
|
2366
|
-
}
|
|
2367
|
-
return toJsonRpcResponse(JSON.parse(response.val.response));
|
|
2368
|
-
})
|
|
2369
|
-
.then(resolve, reject)
|
|
2370
|
-
.finally(() => {
|
|
2371
|
-
const nextResolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2372
|
-
if (!nextResolvers) {
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
nextResolvers.delete(resolver);
|
|
2376
|
-
if (nextResolvers.size === 0) {
|
|
2377
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2378
|
-
}
|
|
1343
|
+
try {
|
|
1344
|
+
handler({
|
|
1345
|
+
sessionId: event.sessionId,
|
|
1346
|
+
agentType: event.agentType,
|
|
1347
|
+
processId: event.processId,
|
|
1348
|
+
pid: session?.pid ?? null,
|
|
1349
|
+
exitCode: event.exitCode,
|
|
1350
|
+
restart: event.restart,
|
|
1351
|
+
restartCount: event.restartCount,
|
|
1352
|
+
maxRestarts: event.maxRestarts,
|
|
2379
1353
|
});
|
|
2380
|
-
});
|
|
2381
|
-
const liveSession = this._sessions.get(sessionId);
|
|
2382
|
-
if (liveSession && !isLocalCancelledPromptResponse(method, response)) {
|
|
2383
|
-
await this._hydrateSessionState(liveSession).catch(() => { });
|
|
2384
|
-
}
|
|
2385
|
-
if (!response.error) {
|
|
2386
|
-
if (method === "session/set_mode" &&
|
|
2387
|
-
typeof params?.modeId === "string" &&
|
|
2388
|
-
session.modes) {
|
|
2389
|
-
session.modes = {
|
|
2390
|
-
...session.modes,
|
|
2391
|
-
currentModeId: params.modeId,
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
if (method === "session/set_config_option" &&
|
|
2395
|
-
typeof params?.configId === "string" &&
|
|
2396
|
-
typeof params?.value === "string") {
|
|
2397
|
-
const nextValue = params.value;
|
|
2398
|
-
const updatedOption = session.configOptions.find((option) => option.id === params.configId);
|
|
2399
|
-
session.configOverrides.set(params.configId, nextValue);
|
|
2400
|
-
if (typeof updatedOption?.category === "string") {
|
|
2401
|
-
session.configOverrides.set(updatedOption.category, nextValue);
|
|
2402
|
-
}
|
|
2403
|
-
session.configOptions = session.configOptions.map((option) => option.id === params.configId
|
|
2404
|
-
? { ...option, currentValue: nextValue }
|
|
2405
|
-
: option);
|
|
2406
|
-
}
|
|
2407
1354
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
async _setSessionConfigByCategory(sessionId, category, value) {
|
|
2411
|
-
const session = this._requireSession(sessionId);
|
|
2412
|
-
const option = session.configOptions.find((entry) => entry.category === category);
|
|
2413
|
-
if (option?.readOnly) {
|
|
2414
|
-
return this._unsupportedConfigResponse(session.agentType, category);
|
|
1355
|
+
catch (error) {
|
|
1356
|
+
console.warn(`ACP exit handler failed for ${event.sessionId}`, error);
|
|
2415
1357
|
}
|
|
2416
|
-
const response = await this._sendSessionRequest(sessionId, "session/set_config_option", {
|
|
2417
|
-
configId: option?.id ?? category,
|
|
2418
|
-
value,
|
|
2419
|
-
});
|
|
2420
|
-
return response;
|
|
2421
1358
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
_abortPendingSessionRequests(sessionId) {
|
|
2426
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2427
|
-
if (!resolvers) {
|
|
1359
|
+
_handleSidecarEvent(event) {
|
|
1360
|
+
if (event.payload.type === "ext") {
|
|
1361
|
+
this._handleAcpExtEvent(event.payload.envelope);
|
|
2428
1362
|
return;
|
|
2429
1363
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
1364
|
+
if (event.payload.type === "cron_dispatch") {
|
|
1365
|
+
const dispatch = event.payload.dispatch;
|
|
1366
|
+
this._cronManager.consumeDispatch({
|
|
1367
|
+
alarm: {
|
|
1368
|
+
generation: dispatch.alarm.generation,
|
|
1369
|
+
...(dispatch.alarm.next_alarm_ms === undefined
|
|
1370
|
+
? {}
|
|
1371
|
+
: { nextAlarmMs: dispatch.alarm.next_alarm_ms }),
|
|
1372
|
+
},
|
|
1373
|
+
runs: dispatch.runs.map((run) => ({
|
|
1374
|
+
runId: run.run_id,
|
|
1375
|
+
jobId: run.job_id,
|
|
1376
|
+
action: run.action,
|
|
1377
|
+
})),
|
|
1378
|
+
events: dispatch.events.map((record) => ({
|
|
1379
|
+
kind: record.kind,
|
|
1380
|
+
jobId: record.job_id,
|
|
1381
|
+
timeMs: record.time_ms,
|
|
1382
|
+
...(record.duration_ms === undefined
|
|
1383
|
+
? {}
|
|
1384
|
+
: { durationMs: record.duration_ms }),
|
|
1385
|
+
...(record.error === undefined ? {} : { error: record.error }),
|
|
1386
|
+
})),
|
|
1387
|
+
});
|
|
1388
|
+
return;
|
|
2441
1389
|
}
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2445
|
-
if (!resolvers) {
|
|
2446
|
-
return false;
|
|
1390
|
+
if (event.payload.type !== "structured") {
|
|
1391
|
+
return;
|
|
2447
1392
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
result: {
|
|
2452
|
-
stopReason: "cancelled",
|
|
2453
|
-
},
|
|
2454
|
-
};
|
|
2455
|
-
let cancelledPrompt = false;
|
|
2456
|
-
for (const resolver of [...resolvers]) {
|
|
2457
|
-
if (resolver.method !== "session/prompt") {
|
|
2458
|
-
continue;
|
|
2459
|
-
}
|
|
2460
|
-
resolvers.delete(resolver);
|
|
2461
|
-
resolver.resolve(response);
|
|
2462
|
-
cancelledPrompt = true;
|
|
1393
|
+
if (event.payload.name === "limit_warning") {
|
|
1394
|
+
this._handleLimitWarning(event.payload.detail);
|
|
1395
|
+
return;
|
|
2463
1396
|
}
|
|
2464
|
-
if (
|
|
2465
|
-
|
|
1397
|
+
if (event.payload.name !== "acp.session_event") {
|
|
1398
|
+
return;
|
|
2466
1399
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
_rejectPendingPermissionReplies(sessionId) {
|
|
2470
|
-
const session = this._sessions.get(sessionId);
|
|
1400
|
+
const sessionId = event.payload.detail.session_id;
|
|
1401
|
+
const session = sessionId ? this._sessions.get(sessionId) : undefined;
|
|
2471
1402
|
if (!session) {
|
|
2472
1403
|
return;
|
|
2473
1404
|
}
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
_rejectPendingPermissionRepliesFromSession(session) {
|
|
2477
|
-
for (const [permissionId, pendingReply,] of session.pendingPermissionReplies) {
|
|
2478
|
-
clearTimeout(pendingReply.timer);
|
|
2479
|
-
pendingReply.reject(new Error(`Session closed before permission reply: ${permissionId}`));
|
|
2480
|
-
}
|
|
2481
|
-
session.pendingPermissionReplies.clear();
|
|
2482
|
-
}
|
|
2483
|
-
async _closeSessionInternal(sessionId) {
|
|
2484
|
-
const closing = this._sessionClosePromises.get(sessionId);
|
|
2485
|
-
if (closing) {
|
|
2486
|
-
return closing;
|
|
2487
|
-
}
|
|
2488
|
-
if (this._closedSessionIds.has(sessionId)) {
|
|
1405
|
+
const notificationText = event.payload.detail.notification;
|
|
1406
|
+
if (typeof notificationText !== "string") {
|
|
2489
1407
|
return;
|
|
2490
1408
|
}
|
|
2491
|
-
this._abortPendingSessionRequests(sessionId);
|
|
2492
|
-
this._rejectPendingPermissionReplies(sessionId);
|
|
2493
|
-
this._requireSession(sessionId);
|
|
2494
|
-
this._removeSession(sessionId);
|
|
2495
|
-
this._closedSessionIds.add(sessionId);
|
|
2496
|
-
const closePromise = this._sendAcpRequest({
|
|
2497
|
-
tag: "AcpCloseSessionRequest",
|
|
2498
|
-
val: { sessionId },
|
|
2499
|
-
})
|
|
2500
|
-
.then((response) => {
|
|
2501
|
-
if (response.tag !== "AcpSessionClosedResponse") {
|
|
2502
|
-
throw new Error(`unexpected response to AcpCloseSessionRequest: ${response.tag}`);
|
|
2503
|
-
}
|
|
2504
|
-
})
|
|
2505
|
-
.finally(() => {
|
|
2506
|
-
this._sessionClosePromises.delete(sessionId);
|
|
2507
|
-
});
|
|
2508
|
-
this._sessionClosePromises.set(sessionId, closePromise);
|
|
2509
|
-
await closePromise;
|
|
2510
|
-
}
|
|
2511
|
-
async _hydrateSessionState(session) {
|
|
2512
|
-
const response = await this._sendAcpRequest({
|
|
2513
|
-
tag: "AcpGetSessionStateRequest",
|
|
2514
|
-
val: { sessionId: session.sessionId },
|
|
2515
|
-
});
|
|
2516
|
-
if (response.tag !== "AcpSessionStateResponse") {
|
|
2517
|
-
throw new Error(`unexpected response to AcpGetSessionStateRequest: ${response.tag}`);
|
|
2518
|
-
}
|
|
2519
|
-
const state = sidecarSessionStateFromAcp(response.val);
|
|
2520
|
-
this._syncSessionState(session, state);
|
|
2521
|
-
}
|
|
2522
|
-
async createSession(agentType, options) {
|
|
2523
|
-
const config = this._resolveAgentConfig(agentType);
|
|
2524
|
-
if (!config) {
|
|
2525
|
-
throw new Error(`Unknown agent type: ${agentType}`);
|
|
2526
|
-
}
|
|
2527
|
-
// System-prompt assembly and injection (launch args / OPENCODE_CONTEXTPATHS) are owned by
|
|
2528
|
-
// the sidecar at AcpCreateSessionRequest. The host only forwards additionalInstructions /
|
|
2529
|
-
// skipOsInstructions plus the agent's static launch args and env.
|
|
2530
|
-
const launchArgs = [...(config.launchArgs ?? [])];
|
|
2531
|
-
let launchEnv = { ...config.defaultEnv, ...options?.env };
|
|
2532
|
-
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2533
|
-
const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
|
|
2534
|
-
if ((agentType === "pi" || agentType === "pi-cli") &&
|
|
2535
|
-
!launchEnv.PI_ACP_PI_COMMAND) {
|
|
2536
|
-
launchEnv = {
|
|
2537
|
-
...launchEnv,
|
|
2538
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
2539
|
-
};
|
|
2540
|
-
}
|
|
2541
|
-
const response = await this._sendAcpRequest({
|
|
2542
|
-
tag: "AcpCreateSessionRequest",
|
|
2543
|
-
val: {
|
|
2544
|
-
agentType: String(agentType),
|
|
2545
|
-
runtime: AcpRuntimeKind.JavaScript,
|
|
2546
|
-
adapterEntrypoint,
|
|
2547
|
-
args: launchArgs,
|
|
2548
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2549
|
-
cwd: sessionCwd,
|
|
2550
|
-
mcpServers: JSON.stringify(options?.mcpServers ?? []),
|
|
2551
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2552
|
-
clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
|
|
2553
|
-
additionalInstructions: combineInstructions(options?.additionalInstructions, this._toolReference),
|
|
2554
|
-
skipOsInstructions: options?.skipOsInstructions ?? false,
|
|
2555
|
-
},
|
|
2556
|
-
});
|
|
2557
|
-
if (response.tag !== "AcpSessionCreatedResponse") {
|
|
2558
|
-
throw new Error(`unexpected create_session response: ${response.tag}`);
|
|
2559
|
-
}
|
|
2560
|
-
const created = sidecarSessionCreatedFromAcp(response.val);
|
|
2561
|
-
// The sessionId is chosen by the (untrusted/buggy) ACP adapter or sidecar. If it collides
|
|
2562
|
-
// with a live session already in `_sessions`, blindly overwriting the entry would orphan the
|
|
2563
|
-
// first session's event/permission handlers and pending permission replies. Fail closed:
|
|
2564
|
-
// reject the colliding create and leave the original session intact. (A previously-closed,
|
|
2565
|
-
// evicted id may still be re-used; only a live, non-closed entry is a collision.)
|
|
2566
|
-
const existing = this._sessions.get(created.sessionId);
|
|
2567
|
-
if (existing !== undefined && !existing.closed) {
|
|
2568
|
-
throw new Error(`session id collision: ${created.sessionId}`);
|
|
2569
|
-
}
|
|
2570
|
-
const initData = {
|
|
2571
|
-
modes: toSessionModes(created.modes) ?? undefined,
|
|
2572
|
-
configOptions: toSessionConfigOptions(created.configOptions),
|
|
2573
|
-
capabilities: toAgentCapabilities(created.agentCapabilities),
|
|
2574
|
-
agentInfo: toAgentInfo(created.agentInfo) ?? undefined,
|
|
2575
|
-
};
|
|
2576
|
-
const session = sessionEntryFromInit(created.sessionId, String(agentType), initData);
|
|
2577
|
-
this._closedSessionIds.delete(created.sessionId);
|
|
2578
|
-
this._sessions.set(created.sessionId, session);
|
|
2579
1409
|
try {
|
|
2580
|
-
|
|
1410
|
+
this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(notificationText)));
|
|
2581
1411
|
}
|
|
2582
1412
|
catch (error) {
|
|
2583
|
-
|
|
2584
|
-
throw error;
|
|
1413
|
+
console.warn("invalid ACP session event from sidecar", error);
|
|
2585
1414
|
}
|
|
2586
|
-
return { sessionId: created.sessionId };
|
|
2587
1415
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
...launchEnv,
|
|
2615
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
1416
|
+
_handleLimitWarning(detail) {
|
|
1417
|
+
if (!this._limitWarningHandler) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
let warning;
|
|
1421
|
+
try {
|
|
1422
|
+
const requireString = (name) => {
|
|
1423
|
+
const value = detail[name];
|
|
1424
|
+
if (value === undefined) {
|
|
1425
|
+
throw new Error(`missing ${name}`);
|
|
1426
|
+
}
|
|
1427
|
+
return value;
|
|
1428
|
+
};
|
|
1429
|
+
const requireNumber = (name) => {
|
|
1430
|
+
const value = Number(requireString(name));
|
|
1431
|
+
if (!Number.isFinite(value)) {
|
|
1432
|
+
throw new Error(`invalid ${name}`);
|
|
1433
|
+
}
|
|
1434
|
+
return value;
|
|
1435
|
+
};
|
|
1436
|
+
warning = {
|
|
1437
|
+
limit: requireString("limit"),
|
|
1438
|
+
category: requireString("category"),
|
|
1439
|
+
observed: requireNumber("observed"),
|
|
1440
|
+
capacity: requireNumber("capacity"),
|
|
1441
|
+
fillPercent: requireNumber("fillPercent"),
|
|
2616
1442
|
};
|
|
2617
1443
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
launchEnv = {
|
|
2622
|
-
...launchEnv,
|
|
2623
|
-
[RESUME_ADAPTER_ENTRYPOINT_ENV]: adapterEntrypoint,
|
|
2624
|
-
};
|
|
2625
|
-
const response = await this._sendAcpRequest({
|
|
2626
|
-
tag: "AcpResumeSessionRequest",
|
|
2627
|
-
val: {
|
|
2628
|
-
sessionId,
|
|
2629
|
-
agentType: String(agentType),
|
|
2630
|
-
transcriptPath: options?.transcriptPath ?? null,
|
|
2631
|
-
cwd: sessionCwd,
|
|
2632
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2633
|
-
},
|
|
2634
|
-
});
|
|
2635
|
-
if (response.tag !== "AcpSessionResumedResponse") {
|
|
2636
|
-
throw new Error(`unexpected resume_session response: ${response.tag}`);
|
|
1444
|
+
catch (error) {
|
|
1445
|
+
console.warn("invalid limit warning from sidecar", error);
|
|
1446
|
+
return;
|
|
2637
1447
|
}
|
|
2638
|
-
const { sessionId: liveSessionId, mode } = response.val;
|
|
2639
|
-
// Register + hydrate the live session so subsequent prompts route to it.
|
|
2640
|
-
const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
|
|
2641
|
-
this._closedSessionIds.delete(liveSessionId);
|
|
2642
|
-
this._sessions.set(liveSessionId, session);
|
|
2643
1448
|
try {
|
|
2644
|
-
|
|
1449
|
+
this._limitWarningHandler(warning);
|
|
2645
1450
|
}
|
|
2646
1451
|
catch (error) {
|
|
2647
|
-
|
|
2648
|
-
throw error;
|
|
1452
|
+
console.warn("limit warning handler failed", error);
|
|
2649
1453
|
}
|
|
2650
|
-
return { sessionId: liveSessionId, mode };
|
|
2651
1454
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
* mounts; adapter package.json is read directly off the host dir backing
|
|
2656
|
-
* the relevant mount.
|
|
2657
|
-
*/
|
|
2658
|
-
_resolveAdapterBin(adapterPackage) {
|
|
2659
|
-
return this._resolvePackageBin(adapterPackage);
|
|
2660
|
-
}
|
|
2661
|
-
/**
|
|
2662
|
-
* Parent of the host directory backing the `/root/node_modules` mount, if
|
|
2663
|
-
* the caller supplied one (e.g. via `nodeModulesMount(...)`). Used as the
|
|
2664
|
-
* `resolvePackageDir` start dir for adapter/agent package resolution when no
|
|
2665
|
-
* software root matches. `nodeModulesMount` mounts `<dir>/node_modules`, so
|
|
2666
|
-
* the start dir is `<dir>` (one level above the node_modules tree).
|
|
2667
|
-
*/
|
|
2668
|
-
_nodeModulesHostRoot() {
|
|
2669
|
-
for (const mount of this._hostMounts) {
|
|
2670
|
-
if (mount.vmPath === "/root/node_modules") {
|
|
2671
|
-
return dirname(mount.hostPath);
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
return null;
|
|
2675
|
-
}
|
|
2676
|
-
_resolvePackageBin(packageName, binName) {
|
|
2677
|
-
const vmPrefix = `/root/node_modules/${packageName}`;
|
|
2678
|
-
let hostPkgJsonPath = null;
|
|
2679
|
-
for (const root of this._softwareRoots) {
|
|
2680
|
-
if (root.vmPath === vmPrefix) {
|
|
2681
|
-
hostPkgJsonPath = join(root.hostPath, "package.json");
|
|
2682
|
-
break;
|
|
2683
|
-
}
|
|
1455
|
+
_handleAcpExtEvent(envelope) {
|
|
1456
|
+
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
1457
|
+
return;
|
|
2684
1458
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
1459
|
+
try {
|
|
1460
|
+
const event = decodeAcpEvent(envelope.payload);
|
|
1461
|
+
switch (event.tag) {
|
|
1462
|
+
case "AcpSessionEvent": {
|
|
1463
|
+
const session = this._sessions.get(event.val.sessionId);
|
|
1464
|
+
if (!session) {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(event.val.notification)));
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
case "AcpAgentStderrEvent": {
|
|
1471
|
+
this._recordAgentStderr(event.val);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
case "AcpAgentExitedEvent": {
|
|
1475
|
+
this._recordAgentExit(event.val);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
2692
1478
|
}
|
|
2693
|
-
hostPkgJsonPath = join(resolvePackageDir(nodeModulesRoot, packageName), "package.json");
|
|
2694
|
-
}
|
|
2695
|
-
const pkg = JSON.parse(readFileSync(hostPkgJsonPath, "utf-8"));
|
|
2696
|
-
let binEntry;
|
|
2697
|
-
if (typeof pkg.bin === "string") {
|
|
2698
|
-
binEntry = pkg.bin;
|
|
2699
1479
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
(binName ? pkg.bin[binName] : undefined) ??
|
|
2703
|
-
pkg.bin[packageName] ??
|
|
2704
|
-
Object.values(pkg.bin)[0];
|
|
2705
|
-
}
|
|
2706
|
-
if (!binEntry) {
|
|
2707
|
-
throw new Error(`No bin entry found in ${packageName}/package.json`);
|
|
1480
|
+
catch (error) {
|
|
1481
|
+
console.warn("invalid ACP extension event from sidecar", error);
|
|
2708
1482
|
}
|
|
2709
|
-
return `${vmPrefix}/${binEntry}`;
|
|
2710
1483
|
}
|
|
2711
|
-
|
|
2712
|
-
const context = {
|
|
2713
|
-
toolKits: this._toolKits,
|
|
2714
|
-
toolMap: buildToolMap(this._toolKits),
|
|
2715
|
-
permissions: this._permissions,
|
|
2716
|
-
readFile: (path) => this.readFile(path),
|
|
2717
|
-
};
|
|
2718
|
-
this._sidecarClient.setSidecarRequestHandler((request) => {
|
|
2719
|
-
switch (request.payload.type) {
|
|
2720
|
-
case "host_callback":
|
|
2721
|
-
return handleHostCallback(request, context);
|
|
2722
|
-
case "js_bridge_call":
|
|
2723
|
-
return Promise.resolve({
|
|
2724
|
-
type: "js_bridge_result",
|
|
2725
|
-
call_id: request.payload.call_id,
|
|
2726
|
-
error: `unsupported sidecar request type: ${request.payload.type}`,
|
|
2727
|
-
});
|
|
2728
|
-
case "ext":
|
|
2729
|
-
return this._handleAcpExtSidecarRequest(request.payload.envelope);
|
|
2730
|
-
}
|
|
2731
|
-
});
|
|
2732
|
-
}
|
|
2733
|
-
async _handleAcpExtSidecarRequest(envelope) {
|
|
1484
|
+
_decodeAcpResponseEnvelope(envelope) {
|
|
2734
1485
|
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
2735
|
-
|
|
2736
|
-
type: "ext_result",
|
|
2737
|
-
envelope: {
|
|
2738
|
-
namespace: envelope.namespace,
|
|
2739
|
-
payload: Buffer.from("unknown extension namespace", "utf8"),
|
|
2740
|
-
},
|
|
2741
|
-
};
|
|
1486
|
+
throw new Error(`unexpected ACP Ext namespace: ${envelope.namespace}`);
|
|
2742
1487
|
}
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
_acpMethod: ACP_PERMISSION_METHOD,
|
|
2749
|
-
});
|
|
2750
|
-
return {
|
|
2751
|
-
type: "ext_result",
|
|
2752
|
-
envelope: {
|
|
2753
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
2754
|
-
payload: encodeAcpCallbackResponse({
|
|
2755
|
-
tag: "AcpPermissionCallbackResponse",
|
|
2756
|
-
val: {
|
|
2757
|
-
permissionId: callback.val.permissionId,
|
|
2758
|
-
reply,
|
|
2759
|
-
},
|
|
2760
|
-
}),
|
|
2761
|
-
},
|
|
2762
|
-
};
|
|
2763
|
-
}
|
|
2764
|
-
case "AcpHostRequestCallback": {
|
|
2765
|
-
const response = await this._dispatchAcpSidecarRequest(toJsonRpcRequest(JSON.parse(callback.val.request)));
|
|
2766
|
-
return {
|
|
2767
|
-
type: "ext_result",
|
|
2768
|
-
envelope: {
|
|
2769
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
2770
|
-
payload: encodeAcpCallbackResponse({
|
|
2771
|
-
tag: "AcpHostRequestCallbackResponse",
|
|
2772
|
-
val: {
|
|
2773
|
-
response: JSON.stringify(response),
|
|
2774
|
-
},
|
|
2775
|
-
}),
|
|
2776
|
-
},
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
1488
|
+
const response = decodeAcpResponse(envelope.payload);
|
|
1489
|
+
if (response.tag === "AcpErrorResponse") {
|
|
1490
|
+
const error = new Error(response.val.message);
|
|
1491
|
+
error.code = response.val.code;
|
|
1492
|
+
throw error;
|
|
2779
1493
|
}
|
|
1494
|
+
return response;
|
|
2780
1495
|
}
|
|
2781
|
-
async
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
return {
|
|
2793
|
-
jsonrpc: "2.0",
|
|
2794
|
-
id: request.id,
|
|
2795
|
-
error: {
|
|
2796
|
-
code: error.code,
|
|
2797
|
-
message: error.message,
|
|
2798
|
-
...(error.data ? { data: error.data } : {}),
|
|
2799
|
-
},
|
|
2800
|
-
};
|
|
2801
|
-
}
|
|
2802
|
-
return {
|
|
2803
|
-
jsonrpc: "2.0",
|
|
2804
|
-
id: request.id,
|
|
2805
|
-
error: {
|
|
2806
|
-
code: -32603,
|
|
2807
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1496
|
+
async _sendAcpRequest(request, onResponse) {
|
|
1497
|
+
let hookResponse;
|
|
1498
|
+
const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
|
|
1499
|
+
namespace: ACP_EXTENSION_NAMESPACE,
|
|
1500
|
+
payload: encodeAcpRequest(request),
|
|
1501
|
+
}, onResponse
|
|
1502
|
+
? {
|
|
1503
|
+
onResponse: (responseEnvelope) => {
|
|
1504
|
+
hookResponse =
|
|
1505
|
+
this._decodeAcpResponseEnvelope(responseEnvelope);
|
|
1506
|
+
onResponse(hookResponse);
|
|
2808
1507
|
},
|
|
2809
|
-
}
|
|
1508
|
+
}
|
|
1509
|
+
: undefined);
|
|
1510
|
+
if (hookResponse) {
|
|
1511
|
+
return hookResponse;
|
|
2810
1512
|
}
|
|
1513
|
+
const response = this._decodeAcpResponseEnvelope(envelope);
|
|
1514
|
+
onResponse?.(response);
|
|
1515
|
+
return response;
|
|
2811
1516
|
}
|
|
2812
|
-
async
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
case ACP_PERMISSION_METHOD:
|
|
2816
|
-
return this._handleAcpPermissionRequest(request, params);
|
|
2817
|
-
case "fs/read":
|
|
2818
|
-
case "fs/read_text_file":
|
|
2819
|
-
return this._handleAcpReadFile(params);
|
|
2820
|
-
case "fs/write":
|
|
2821
|
-
case "fs/write_text_file":
|
|
2822
|
-
return this._handleAcpWriteFile(params);
|
|
2823
|
-
case "fs/readDir":
|
|
2824
|
-
case "fs/read_dir":
|
|
2825
|
-
return this._handleAcpReadDir(params);
|
|
2826
|
-
case "terminal/create":
|
|
2827
|
-
return this._handleAcpCreateTerminal(params);
|
|
2828
|
-
case "terminal/write":
|
|
2829
|
-
return this._handleAcpWriteTerminal(params);
|
|
2830
|
-
case "terminal/output":
|
|
2831
|
-
case "terminal/read":
|
|
2832
|
-
return this._handleAcpReadTerminal(params);
|
|
2833
|
-
case "terminal/wait_for_exit":
|
|
2834
|
-
case "terminal/waitForExit":
|
|
2835
|
-
return this._handleAcpWaitForTerminalExit(params);
|
|
2836
|
-
case "terminal/kill":
|
|
2837
|
-
return this._handleAcpKillTerminal(params);
|
|
2838
|
-
case "terminal/release":
|
|
2839
|
-
case "terminal/close":
|
|
2840
|
-
return this._handleAcpReleaseTerminal(params);
|
|
2841
|
-
case "terminal/resize":
|
|
2842
|
-
return this._handleAcpResizeTerminal(params);
|
|
2843
|
-
default:
|
|
2844
|
-
throw new AcpDispatchError(-32601, `Method not found: ${request.method}`, {
|
|
2845
|
-
method: request.method,
|
|
2846
|
-
});
|
|
2847
|
-
}
|
|
1517
|
+
async _sendSessionRequest(sessionId, method, params) {
|
|
1518
|
+
return (await this._sendSessionRequestWithText(sessionId, method, params))
|
|
1519
|
+
.response;
|
|
2848
1520
|
}
|
|
2849
|
-
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
optionIds: new Set(["once", "allow_once"]),
|
|
2858
|
-
kinds: new Set(["allow_once"]),
|
|
2859
|
-
}
|
|
2860
|
-
: {
|
|
2861
|
-
optionIds: new Set(["reject", "reject_once"]),
|
|
2862
|
-
kinds: new Set(["reject_once"]),
|
|
2863
|
-
};
|
|
2864
|
-
const matched = options?.find((option) => {
|
|
2865
|
-
const optionId = typeof option.optionId === "string" ? option.optionId : undefined;
|
|
2866
|
-
const kind = typeof option.kind === "string" ? option.kind : undefined;
|
|
2867
|
-
return ((optionId !== undefined && optionTargets.optionIds.has(optionId)) ||
|
|
2868
|
-
(kind !== undefined && optionTargets.kinds.has(kind)));
|
|
1521
|
+
async _sendSessionRequestWithText(sessionId, method, params) {
|
|
1522
|
+
const acpResponse = await this._sendAcpRequest({
|
|
1523
|
+
tag: "AcpSessionRequest",
|
|
1524
|
+
val: {
|
|
1525
|
+
sessionId,
|
|
1526
|
+
method,
|
|
1527
|
+
params: params === undefined ? null : JSON.stringify(params),
|
|
1528
|
+
},
|
|
2869
1529
|
});
|
|
2870
|
-
if (
|
|
2871
|
-
|
|
2872
|
-
}
|
|
2873
|
-
if (reply === "always") {
|
|
2874
|
-
return "allow_always";
|
|
2875
|
-
}
|
|
2876
|
-
if (reply === "once") {
|
|
2877
|
-
return "allow_once";
|
|
1530
|
+
if (acpResponse.tag !== "AcpSessionRpcResponse") {
|
|
1531
|
+
throw new Error(`unexpected response to AcpSessionRequest: ${acpResponse.tag}`);
|
|
2878
1532
|
}
|
|
2879
|
-
return "reject_once";
|
|
2880
|
-
}
|
|
2881
|
-
_buildAcpPermissionResult(reply, params) {
|
|
2882
|
-
const options = Array.isArray(params.options)
|
|
2883
|
-
? params.options.filter((option) => typeof option === "object" && option !== null)
|
|
2884
|
-
: undefined;
|
|
2885
|
-
const optionId = this._normalizeAcpPermissionOptionId(options, reply);
|
|
2886
1533
|
return {
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
outcome: "selected",
|
|
2890
|
-
optionId,
|
|
2891
|
-
}
|
|
2892
|
-
: {
|
|
2893
|
-
outcome: "cancelled",
|
|
2894
|
-
},
|
|
1534
|
+
response: toJsonRpcResponse(JSON.parse(acpResponse.val.response)),
|
|
1535
|
+
text: acpResponse.val.text,
|
|
2895
1536
|
};
|
|
2896
1537
|
}
|
|
2897
|
-
async
|
|
2898
|
-
const
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
}
|
|
2902
|
-
const session = this._sessions.get(sessionId);
|
|
2903
|
-
if (!session) {
|
|
2904
|
-
throw new AcpDispatchError(-32602, `Session not found: ${sessionId}`);
|
|
2905
|
-
}
|
|
2906
|
-
const permissionId = String(request.id);
|
|
2907
|
-
const permissionParams = {
|
|
2908
|
-
...params,
|
|
2909
|
-
permissionId,
|
|
2910
|
-
_acpMethod: request.method,
|
|
2911
|
-
};
|
|
2912
|
-
if (session.permissionHandlers.size === 0) {
|
|
2913
|
-
return this._buildAcpPermissionResult("reject", permissionParams);
|
|
2914
|
-
}
|
|
2915
|
-
const reply = await new Promise((resolve, reject) => {
|
|
2916
|
-
const timer = setTimeout(() => {
|
|
2917
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
2918
|
-
reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
|
|
2919
|
-
}, 120_000);
|
|
2920
|
-
session.pendingPermissionReplies.set(permissionId, {
|
|
2921
|
-
resolve,
|
|
2922
|
-
reject,
|
|
2923
|
-
timer,
|
|
2924
|
-
});
|
|
2925
|
-
const permissionRequest = {
|
|
2926
|
-
permissionId,
|
|
2927
|
-
description: typeof permissionParams["description"] === "string"
|
|
2928
|
-
? permissionParams["description"]
|
|
2929
|
-
: undefined,
|
|
2930
|
-
params: permissionParams,
|
|
2931
|
-
};
|
|
2932
|
-
for (const handler of session.permissionHandlers) {
|
|
2933
|
-
handler(permissionRequest);
|
|
2934
|
-
}
|
|
1538
|
+
async _setSessionConfigByCategory(sessionId, category, value) {
|
|
1539
|
+
const response = await this._sendAcpRequest({
|
|
1540
|
+
tag: "AcpSetSessionConfigRequest",
|
|
1541
|
+
val: { sessionId, category, value },
|
|
2935
1542
|
});
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
_acpParams(request) {
|
|
2939
|
-
if (!request.params) {
|
|
2940
|
-
return {};
|
|
2941
|
-
}
|
|
2942
|
-
if (typeof request.params !== "object" ||
|
|
2943
|
-
request.params === null ||
|
|
2944
|
-
Array.isArray(request.params)) {
|
|
2945
|
-
throw new AcpDispatchError(-32602, `${request.method} requires object params`);
|
|
1543
|
+
if (response.tag !== "AcpSessionRpcResponse") {
|
|
1544
|
+
throw new Error(`unexpected response to AcpSetSessionConfigRequest: ${response.tag}`);
|
|
2946
1545
|
}
|
|
2947
|
-
return
|
|
1546
|
+
return toJsonRpcResponse(JSON.parse(response.val.response));
|
|
2948
1547
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
if (typeof value !== "string") {
|
|
2952
|
-
throw new AcpDispatchError(-32602, `${method} requires a string ${name}`);
|
|
2953
|
-
}
|
|
2954
|
-
return value;
|
|
1548
|
+
_removeSession(sessionId) {
|
|
1549
|
+
this._sessions.delete(sessionId);
|
|
2955
1550
|
}
|
|
2956
|
-
|
|
2957
|
-
const
|
|
2958
|
-
if (
|
|
2959
|
-
return
|
|
2960
|
-
}
|
|
2961
|
-
if (typeof value !== "string") {
|
|
2962
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be a string when provided`);
|
|
1551
|
+
_rejectPendingPermissionReplies(sessionId) {
|
|
1552
|
+
const session = this._sessions.get(sessionId);
|
|
1553
|
+
if (!session) {
|
|
1554
|
+
return;
|
|
2963
1555
|
}
|
|
2964
|
-
|
|
1556
|
+
this._rejectPendingPermissionRepliesFromSession(session);
|
|
2965
1557
|
}
|
|
2966
|
-
|
|
2967
|
-
const
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
}
|
|
2971
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2972
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be a number when provided`);
|
|
1558
|
+
_rejectPendingPermissionRepliesFromSession(session) {
|
|
1559
|
+
for (const [permissionId, pendingReply,] of session.pendingPermissionReplies) {
|
|
1560
|
+
clearTimeout(pendingReply.cleanupTimer);
|
|
1561
|
+
pendingReply.reject(new Error(`Session closed before permission reply: ${permissionId}`));
|
|
2973
1562
|
}
|
|
2974
|
-
|
|
1563
|
+
session.pendingPermissionReplies.clear();
|
|
2975
1564
|
}
|
|
2976
|
-
|
|
2977
|
-
const
|
|
2978
|
-
|
|
2979
|
-
|
|
1565
|
+
async _closeSessionInternal(sessionId) {
|
|
1566
|
+
const response = await this._sendAcpRequest({
|
|
1567
|
+
tag: "AcpCloseSessionRequest",
|
|
1568
|
+
val: { sessionId },
|
|
1569
|
+
});
|
|
1570
|
+
if (response.tag !== "AcpSessionClosedResponse") {
|
|
1571
|
+
throw new Error(`unexpected response to AcpCloseSessionRequest: ${response.tag}`);
|
|
2980
1572
|
}
|
|
2981
|
-
if (
|
|
2982
|
-
|
|
2983
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be an array of strings when provided`);
|
|
1573
|
+
if (response.val.sessionId !== sessionId) {
|
|
1574
|
+
throw new Error(`unexpected session id in AcpSessionClosedResponse: expected ${sessionId}, received ${response.val.sessionId}`);
|
|
2984
1575
|
}
|
|
2985
|
-
|
|
1576
|
+
this._rejectPendingPermissionReplies(sessionId);
|
|
1577
|
+
this._removeSession(sessionId);
|
|
2986
1578
|
}
|
|
2987
|
-
|
|
2988
|
-
const
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
}
|
|
2992
|
-
if (
|
|
2993
|
-
|
|
2994
|
-
for (const entry of value) {
|
|
2995
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
2996
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} entries to be { name, value } objects`);
|
|
2997
|
-
}
|
|
2998
|
-
const record = entry;
|
|
2999
|
-
if (typeof record.name !== "string" ||
|
|
3000
|
-
typeof record.value !== "string") {
|
|
3001
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} entries to be { name, value } objects`);
|
|
3002
|
-
}
|
|
3003
|
-
env[record.name] = record.value;
|
|
3004
|
-
}
|
|
3005
|
-
return env;
|
|
3006
|
-
}
|
|
3007
|
-
if (typeof value !== "object") {
|
|
3008
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} to be an object or name/value array`);
|
|
3009
|
-
}
|
|
3010
|
-
const env = {};
|
|
3011
|
-
for (const [key, entryValue] of Object.entries(value)) {
|
|
3012
|
-
if (typeof entryValue !== "string") {
|
|
3013
|
-
throw new AcpDispatchError(-32602, `${method} requires ${name} values to be strings`);
|
|
3014
|
-
}
|
|
3015
|
-
env[key] = entryValue;
|
|
1579
|
+
async _getSessionState(sessionId) {
|
|
1580
|
+
const response = await this._sendAcpRequest({
|
|
1581
|
+
tag: "AcpGetSessionStateRequest",
|
|
1582
|
+
val: { sessionId },
|
|
1583
|
+
});
|
|
1584
|
+
if (response.tag !== "AcpSessionStateResponse") {
|
|
1585
|
+
throw new Error(`unexpected response to AcpGetSessionStateRequest: ${response.tag}`);
|
|
3016
1586
|
}
|
|
3017
|
-
return
|
|
1587
|
+
return sidecarSessionStateFromAcp(response.val);
|
|
3018
1588
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
1589
|
+
async createSession(agentType, options) {
|
|
1590
|
+
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
1591
|
+
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
1592
|
+
// projected `/opt/agentos/<name>/current/agentos-package.json` and spawns
|
|
1593
|
+
// (including the agent's static launch args and manifest env defaults).
|
|
1594
|
+
// System-prompt assembly/injection (launch args / OPENCODE_CONTEXTPATHS) is
|
|
1595
|
+
// owned by the sidecar; the host only forwards additionalInstructions /
|
|
1596
|
+
// skipOsInstructions plus the caller's env.
|
|
1597
|
+
const response = await this._sendAcpRequest({
|
|
1598
|
+
tag: "AcpCreateSessionRequest",
|
|
1599
|
+
val: {
|
|
1600
|
+
agentType: String(agentType),
|
|
1601
|
+
runtime: null,
|
|
1602
|
+
args: null,
|
|
1603
|
+
env: options?.env ? new Map(Object.entries(options.env)) : null,
|
|
1604
|
+
cwd: options?.cwd ?? null,
|
|
1605
|
+
mcpServers: options?.mcpServers
|
|
1606
|
+
? JSON.stringify(options.mcpServers)
|
|
1607
|
+
: null,
|
|
1608
|
+
protocolVersion: null,
|
|
1609
|
+
clientCapabilities: null,
|
|
1610
|
+
additionalInstructions: options?.additionalInstructions ?? null,
|
|
1611
|
+
skipOsInstructions: options?.skipOsInstructions === true ? true : null,
|
|
1612
|
+
},
|
|
1613
|
+
}, (created) => {
|
|
1614
|
+
if (created.tag !== "AcpSessionCreatedResponse") {
|
|
1615
|
+
throw new Error(`unexpected create_session response: ${created.tag}`);
|
|
1616
|
+
}
|
|
1617
|
+
this._sessions.set(created.val.sessionId, sessionEntryFromRoute(created.val));
|
|
1618
|
+
});
|
|
1619
|
+
if (response.tag !== "AcpSessionCreatedResponse") {
|
|
1620
|
+
throw new Error(`unexpected create_session response: ${response.tag}`);
|
|
3024
1621
|
}
|
|
3025
|
-
return
|
|
1622
|
+
return { sessionId: response.val.sessionId };
|
|
3026
1623
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
1624
|
+
/**
|
|
1625
|
+
* Resume a session that exists in durable storage but is not live in this VM
|
|
1626
|
+
* (e.g. after a Rivet actor slept and woke with a fresh VM). Thin forwarder:
|
|
1627
|
+
* resolves the agent config + adapter entrypoint exactly as {@link createSession}
|
|
1628
|
+
* does, then forwards a single `AcpResumeSessionRequest` to the sidecar, which
|
|
1629
|
+
* owns the resume state machine (native `session/load` when the agent supports
|
|
1630
|
+
* it, else `session/new` + a transcript-continuation preamble). The returned
|
|
1631
|
+
* `sessionId` is the live id in this VM (equal to the requested id for native
|
|
1632
|
+
* loads, freshly assigned for the fallback); the caller remaps `external -> live`.
|
|
1633
|
+
* The new live session is registered locally only for host callbacks/events;
|
|
1634
|
+
* authoritative state remains in the sidecar.
|
|
1635
|
+
*
|
|
1636
|
+
* Resume depends on a durable root; on a non-durable (default in-memory) root
|
|
1637
|
+
* there is no surviving store and the fallback tier always runs.
|
|
1638
|
+
*/
|
|
1639
|
+
async resumeSession(sessionId, agentType, options) {
|
|
1640
|
+
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
1641
|
+
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
1642
|
+
// projected manifest, exactly as createSession does.
|
|
1643
|
+
const response = await this._sendAcpRequest({
|
|
1644
|
+
tag: "AcpResumeSessionRequest",
|
|
1645
|
+
val: {
|
|
1646
|
+
sessionId,
|
|
1647
|
+
agentType: String(agentType),
|
|
1648
|
+
transcriptPath: options?.transcriptPath ?? null,
|
|
1649
|
+
cwd: options?.cwd ?? null,
|
|
1650
|
+
env: options?.env ? new Map(Object.entries(options.env)) : null,
|
|
1651
|
+
},
|
|
1652
|
+
}, (resumed) => {
|
|
1653
|
+
if (resumed.tag !== "AcpSessionResumedResponse") {
|
|
1654
|
+
throw new Error(`unexpected resume_session response: ${resumed.tag}`);
|
|
1655
|
+
}
|
|
1656
|
+
this._sessions.set(resumed.val.sessionId, sessionEntryFromRoute(resumed.val));
|
|
1657
|
+
});
|
|
1658
|
+
if (response.tag !== "AcpSessionResumedResponse") {
|
|
1659
|
+
throw new Error(`unexpected resume_session response: ${response.tag}`);
|
|
3031
1660
|
}
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
terminal.outputByteLimit >= 0 &&
|
|
3035
|
-
terminal.output.length > terminal.outputByteLimit) {
|
|
3036
|
-
terminal.output = terminal.output.slice(terminal.output.length - terminal.outputByteLimit);
|
|
3037
|
-
terminal.truncated = true;
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
async _handleAcpReadFile(params) {
|
|
3041
|
-
const method = "fs/read";
|
|
3042
|
-
const path = this._requireAcpStringParam(params, "path", method);
|
|
3043
|
-
const line = this._optionalAcpNumberParam(params, "line", method);
|
|
3044
|
-
const limit = this._optionalAcpNumberParam(params, "limit", method);
|
|
3045
|
-
const encoding = this._optionalAcpStringParam(params, "encoding", method);
|
|
3046
|
-
const bytes = await this.readFile(path);
|
|
3047
|
-
if (encoding === "base64") {
|
|
3048
|
-
return { content: Buffer.from(bytes).toString("base64") };
|
|
3049
|
-
}
|
|
3050
|
-
const text = new TextDecoder().decode(bytes);
|
|
3051
|
-
if (line === undefined && limit === undefined) {
|
|
3052
|
-
return { content: text };
|
|
3053
|
-
}
|
|
3054
|
-
const startLine = Math.max(1, Math.trunc(line ?? 1));
|
|
3055
|
-
const lineLimit = limit === undefined
|
|
3056
|
-
? Number.POSITIVE_INFINITY
|
|
3057
|
-
: Math.max(0, Math.trunc(limit));
|
|
3058
|
-
return {
|
|
3059
|
-
content: text
|
|
3060
|
-
.split("\n")
|
|
3061
|
-
.slice(startLine - 1, startLine - 1 + lineLimit)
|
|
3062
|
-
.join("\n"),
|
|
3063
|
-
};
|
|
3064
|
-
}
|
|
3065
|
-
async _handleAcpWriteFile(params) {
|
|
3066
|
-
const method = "fs/write";
|
|
3067
|
-
const path = this._requireAcpStringParam(params, "path", method);
|
|
3068
|
-
const content = this._requireAcpStringParam(params, "content", method);
|
|
3069
|
-
const encoding = this._optionalAcpStringParam(params, "encoding", method);
|
|
3070
|
-
await this.writeFile(path, encoding === "base64" ? Buffer.from(content, "base64") : content);
|
|
3071
|
-
return null;
|
|
1661
|
+
const { sessionId: liveSessionId, mode } = response.val;
|
|
1662
|
+
return { sessionId: liveSessionId, mode };
|
|
3072
1663
|
}
|
|
3073
|
-
|
|
3074
|
-
const
|
|
3075
|
-
|
|
3076
|
-
const entries = await this._vfs().readDirWithTypes(path);
|
|
3077
|
-
return {
|
|
3078
|
-
entries: entries
|
|
3079
|
-
.filter((entry) => entry.name !== "." && entry.name !== "..")
|
|
3080
|
-
.map((entry) => ({
|
|
3081
|
-
name: entry.name,
|
|
3082
|
-
path: path === "/" ? `/${entry.name}` : `${path}/${entry.name}`,
|
|
3083
|
-
type: entry.isSymbolicLink
|
|
3084
|
-
? "symlink"
|
|
3085
|
-
: entry.isDirectory
|
|
3086
|
-
? "directory"
|
|
3087
|
-
: "file",
|
|
3088
|
-
})),
|
|
1664
|
+
_installSidecarRequestHandler() {
|
|
1665
|
+
const context = {
|
|
1666
|
+
toolMap: buildToolMap(this._toolKits),
|
|
3089
1667
|
};
|
|
1668
|
+
this._disposeSidecarRequestHandler?.();
|
|
1669
|
+
this._disposeSidecarRequestHandler =
|
|
1670
|
+
this._sidecarClient.registerSidecarRequestHandler({
|
|
1671
|
+
scope: "vm",
|
|
1672
|
+
connection_id: this._sidecarSession.connectionId,
|
|
1673
|
+
session_id: this._sidecarSession.sessionId,
|
|
1674
|
+
vm_id: this._sidecarVm.vmId,
|
|
1675
|
+
}, (request) => {
|
|
1676
|
+
switch (request.payload.type) {
|
|
1677
|
+
case "host_callback":
|
|
1678
|
+
return handleHostCallback(request, context);
|
|
1679
|
+
case "js_bridge_call":
|
|
1680
|
+
return handleJsBridgeCall(request.payload, {
|
|
1681
|
+
resolveTarget: (mountId) => {
|
|
1682
|
+
const hostMountResolver = this.#kernel.hostFilesystemForMount;
|
|
1683
|
+
if (hostMountResolver) {
|
|
1684
|
+
const filesystem = hostMountResolver.call(this.#kernel, mountId);
|
|
1685
|
+
return filesystem
|
|
1686
|
+
? { filesystem, rootPath: "/" }
|
|
1687
|
+
: undefined;
|
|
1688
|
+
}
|
|
1689
|
+
return { filesystem: this.#kernel.vfs, rootPath: mountId };
|
|
1690
|
+
},
|
|
1691
|
+
});
|
|
1692
|
+
case "ext":
|
|
1693
|
+
return this._handleAcpExtSidecarRequest(request.payload.envelope);
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
3090
1696
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
const rows = this._optionalAcpNumberParam(params, "rows", method);
|
|
3099
|
-
const outputByteLimit = Math.max(0, Math.trunc(this._optionalAcpNumberParam(params, "outputByteLimit", method) ??
|
|
3100
|
-
1_048_576));
|
|
3101
|
-
const terminalId = `acp-terminal-${++this._acpTerminalCounter}`;
|
|
3102
|
-
const terminal = {
|
|
3103
|
-
handle: this.#kernel.openShell({
|
|
3104
|
-
command,
|
|
3105
|
-
...(args ? { args } : {}),
|
|
3106
|
-
...(env ? { env } : {}),
|
|
3107
|
-
...(cwd ? { cwd } : {}),
|
|
3108
|
-
...(cols !== undefined ? { cols: Math.trunc(cols) } : {}),
|
|
3109
|
-
...(rows !== undefined ? { rows: Math.trunc(rows) } : {}),
|
|
3110
|
-
onStderr: (data) => {
|
|
3111
|
-
this._appendAcpTerminalOutput(terminal, data);
|
|
1697
|
+
async _handleAcpExtSidecarRequest(envelope) {
|
|
1698
|
+
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
1699
|
+
return {
|
|
1700
|
+
type: "ext_result",
|
|
1701
|
+
envelope: {
|
|
1702
|
+
namespace: envelope.namespace,
|
|
1703
|
+
payload: Buffer.from("unknown extension namespace", "utf8"),
|
|
3112
1704
|
},
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
};
|
|
3123
|
-
terminal.waitPromise = terminal.handle.wait().then((exitCode) => {
|
|
3124
|
-
terminal.exitCode = exitCode;
|
|
3125
|
-
return exitCode;
|
|
3126
|
-
});
|
|
3127
|
-
this._acpTerminals.set(terminalId, terminal);
|
|
3128
|
-
return { terminalId };
|
|
3129
|
-
}
|
|
3130
|
-
_handleAcpWriteTerminal(params) {
|
|
3131
|
-
const method = "terminal/write";
|
|
3132
|
-
const terminal = this._requireAcpTerminal(params, method);
|
|
3133
|
-
const data = this._requireAcpStringParam(params, "data", method);
|
|
3134
|
-
const encoding = this._optionalAcpStringParam(params, "encoding", method);
|
|
3135
|
-
terminal.handle.write(encoding === "base64" ? Buffer.from(data, "base64") : data);
|
|
3136
|
-
return null;
|
|
3137
|
-
}
|
|
3138
|
-
_handleAcpReadTerminal(params) {
|
|
3139
|
-
const terminal = this._requireAcpTerminal(params, "terminal/output");
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
const callback = decodeAcpCallback(envelope.payload);
|
|
1708
|
+
if (callback.val.cleanupAfterMs > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
1709
|
+
throw new Error("ACP permission callback cleanup deadline exceeds JS range");
|
|
1710
|
+
}
|
|
1711
|
+
const reply = await this._handleAcpPermissionCallback(callback.val.sessionId, callback.val.permissionId, {
|
|
1712
|
+
...toRecord(JSON.parse(callback.val.params)),
|
|
1713
|
+
_acpMethod: ACP_PERMISSION_METHOD,
|
|
1714
|
+
}, Number(callback.val.cleanupAfterMs));
|
|
3140
1715
|
return {
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
1716
|
+
type: "ext_result",
|
|
1717
|
+
envelope: {
|
|
1718
|
+
namespace: ACP_EXTENSION_NAMESPACE,
|
|
1719
|
+
payload: encodeAcpCallbackResponse({
|
|
1720
|
+
tag: "AcpPermissionCallbackResponse",
|
|
1721
|
+
val: {
|
|
1722
|
+
permissionId: callback.val.permissionId,
|
|
1723
|
+
reply: reply ?? null,
|
|
3148
1724
|
},
|
|
3149
|
-
}
|
|
3150
|
-
|
|
1725
|
+
}),
|
|
1726
|
+
},
|
|
3151
1727
|
};
|
|
3152
1728
|
}
|
|
3153
|
-
async
|
|
3154
|
-
const terminal = this._requireAcpTerminal(params, "terminal/wait_for_exit");
|
|
3155
|
-
const exitCode = await terminal.waitPromise;
|
|
3156
|
-
return { exitCode, signal: null };
|
|
3157
|
-
}
|
|
3158
|
-
_handleAcpKillTerminal(params) {
|
|
3159
|
-
const method = "terminal/kill";
|
|
3160
|
-
const terminal = this._requireAcpTerminal(params, method);
|
|
3161
|
-
const signal = this._optionalAcpNumberParam(params, "signal", method) ?? 15;
|
|
3162
|
-
terminal.handle.kill(Math.trunc(signal));
|
|
3163
|
-
return null;
|
|
3164
|
-
}
|
|
3165
|
-
_handleAcpReleaseTerminal(params) {
|
|
3166
|
-
const method = "terminal/release";
|
|
3167
|
-
const terminalId = this._requireAcpStringParam(params, "terminalId", method);
|
|
3168
|
-
const terminal = this._acpTerminals.get(terminalId);
|
|
3169
|
-
if (!terminal) {
|
|
3170
|
-
throw new AcpDispatchError(-32602, `ACP terminal not found: ${terminalId}`);
|
|
3171
|
-
}
|
|
3172
|
-
if (terminal.exitCode === null) {
|
|
3173
|
-
terminal.handle.kill();
|
|
3174
|
-
}
|
|
3175
|
-
this._acpTerminals.delete(terminalId);
|
|
3176
|
-
return null;
|
|
3177
|
-
}
|
|
3178
|
-
_handleAcpResizeTerminal(params) {
|
|
3179
|
-
const method = "terminal/resize";
|
|
3180
|
-
const terminal = this._requireAcpTerminal(params, method);
|
|
3181
|
-
const cols = this._optionalAcpNumberParam(params, "cols", method);
|
|
3182
|
-
const rows = this._optionalAcpNumberParam(params, "rows", method);
|
|
3183
|
-
if (cols === undefined || rows === undefined) {
|
|
3184
|
-
throw new AcpDispatchError(-32602, `${method} requires numeric cols and rows`);
|
|
3185
|
-
}
|
|
3186
|
-
terminal.handle.resize(Math.trunc(cols), Math.trunc(rows));
|
|
3187
|
-
return null;
|
|
3188
|
-
}
|
|
3189
|
-
async _handleAcpPermissionCallback(sessionId, permissionId, params) {
|
|
1729
|
+
async _handleAcpPermissionCallback(sessionId, permissionId, params, cleanupAfterMs) {
|
|
3190
1730
|
const session = this._sessions.get(sessionId);
|
|
3191
1731
|
if (!session) {
|
|
3192
|
-
return
|
|
1732
|
+
return undefined;
|
|
3193
1733
|
}
|
|
3194
1734
|
if (session.permissionHandlers.size === 0) {
|
|
3195
|
-
|
|
1735
|
+
// Surface the absent host route, then let the sidecar apply its ACP
|
|
1736
|
+
// permission default. The client does not manufacture a policy answer.
|
|
1737
|
+
this._warnNoPermissionHandlerOnce(session, params);
|
|
1738
|
+
return undefined;
|
|
3196
1739
|
}
|
|
3197
1740
|
try {
|
|
3198
1741
|
return await new Promise((resolve, reject) => {
|
|
3199
|
-
const
|
|
1742
|
+
const cleanupTimer = setTimeout(() => {
|
|
3200
1743
|
session.pendingPermissionReplies.delete(permissionId);
|
|
3201
|
-
reject(new Error(`
|
|
3202
|
-
},
|
|
1744
|
+
reject(new Error(`Permission reply route expired after the sidecar deadline: ${permissionId}`));
|
|
1745
|
+
}, cleanupAfterMs);
|
|
3203
1746
|
session.pendingPermissionReplies.set(permissionId, {
|
|
3204
1747
|
resolve,
|
|
3205
1748
|
reject,
|
|
3206
|
-
|
|
1749
|
+
cleanupTimer,
|
|
3207
1750
|
});
|
|
3208
1751
|
const permissionRequest = {
|
|
3209
1752
|
permissionId,
|
|
@@ -3217,111 +1760,42 @@ export class AgentOs {
|
|
|
3217
1760
|
}
|
|
3218
1761
|
});
|
|
3219
1762
|
}
|
|
3220
|
-
catch {
|
|
3221
|
-
|
|
1763
|
+
catch (error) {
|
|
1764
|
+
console.warn(`ACP permission callback route closed for ${sessionId}/${permissionId}; the sidecar owns the outcome`, error);
|
|
1765
|
+
return undefined;
|
|
3222
1766
|
}
|
|
3223
1767
|
}
|
|
3224
|
-
/**
|
|
3225
|
-
* Resolve an agent config by ID. Package-provided configs take
|
|
3226
|
-
* precedence over the hardcoded AGENT_CONFIGS.
|
|
3227
|
-
*/
|
|
3228
|
-
_resolveAgentConfig(agentType) {
|
|
3229
|
-
return (this._softwareAgentConfigs.get(agentType) ??
|
|
3230
|
-
AGENT_CONFIGS[agentType]);
|
|
3231
|
-
}
|
|
3232
1768
|
/**
|
|
3233
1769
|
* Gracefully destroy a session: cancel any pending work, close the client,
|
|
3234
1770
|
* and remove from tracking. Unlike close() which is abrupt, this attempts
|
|
3235
1771
|
* a graceful shutdown sequence.
|
|
3236
1772
|
*/
|
|
3237
1773
|
async destroySession(sessionId) {
|
|
3238
|
-
this._requireSession(sessionId);
|
|
3239
|
-
try {
|
|
3240
|
-
await this.cancelSession(sessionId);
|
|
3241
|
-
}
|
|
3242
|
-
catch {
|
|
3243
|
-
// Ignore cancellation failures during teardown.
|
|
3244
|
-
}
|
|
3245
1774
|
await this._closeSessionInternal(sessionId);
|
|
3246
1775
|
}
|
|
3247
1776
|
// ── Flat session API (ID-based) ───────────────────────────────
|
|
3248
1777
|
async prompt(sessionId, text) {
|
|
3249
|
-
const
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
3255
|
-
const content = toRecord(update.content);
|
|
3256
|
-
if (typeof content.text === "string") {
|
|
3257
|
-
agentText += content.text;
|
|
3258
|
-
}
|
|
3259
|
-
}
|
|
3260
|
-
};
|
|
3261
|
-
const unsubscribe = this._subscribeSessionEvents(session, handler);
|
|
3262
|
-
try {
|
|
3263
|
-
const response = await this._sendSessionRequest(sessionId, "session/prompt", {
|
|
3264
|
-
prompt: [{ type: "text", text }],
|
|
3265
|
-
});
|
|
3266
|
-
return { response, text: agentText };
|
|
3267
|
-
}
|
|
3268
|
-
finally {
|
|
3269
|
-
unsubscribe();
|
|
1778
|
+
const result = await this._sendSessionRequestWithText(sessionId, "session/prompt", {
|
|
1779
|
+
prompt: [{ type: "text", text }],
|
|
1780
|
+
});
|
|
1781
|
+
if (result.text === null) {
|
|
1782
|
+
throw new Error("sidecar prompt response is missing accumulated text");
|
|
3270
1783
|
}
|
|
1784
|
+
return { response: result.response, text: result.text };
|
|
3271
1785
|
}
|
|
3272
1786
|
/** Cancel ongoing agent work for a session. */
|
|
3273
1787
|
async cancelSession(sessionId) {
|
|
3274
|
-
this.
|
|
3275
|
-
const cancelledPendingPrompt = this._cancelPendingPromptRequests(sessionId);
|
|
3276
|
-
if (cancelledPendingPrompt) {
|
|
3277
|
-
// Session control requests share the same framed sidecar transport as an
|
|
3278
|
-
// in-flight prompt request. If the adapter is blocked in prompt I/O, a
|
|
3279
|
-
// synchronous cancel RPC can wedge behind that prompt until the transport
|
|
3280
|
-
// timeout fires. Resolve the local prompt immediately, then let the sidecar
|
|
3281
|
-
// cancellation continue in the background as best effort.
|
|
3282
|
-
void this._sendSessionRequest(sessionId, "session/cancel").catch(() => { });
|
|
3283
|
-
return {
|
|
3284
|
-
jsonrpc: "2.0",
|
|
3285
|
-
id: null,
|
|
3286
|
-
result: {
|
|
3287
|
-
cancelled: true,
|
|
3288
|
-
requested: true,
|
|
3289
|
-
via: "prompt-fallback",
|
|
3290
|
-
},
|
|
3291
|
-
};
|
|
3292
|
-
}
|
|
3293
|
-
const response = await this._sendSessionRequest(sessionId, "session/cancel");
|
|
3294
|
-
if (response.error?.code === -32601) {
|
|
3295
|
-
return {
|
|
3296
|
-
jsonrpc: "2.0",
|
|
3297
|
-
id: null,
|
|
3298
|
-
result: {
|
|
3299
|
-
cancelled: false,
|
|
3300
|
-
requested: true,
|
|
3301
|
-
via: "unsupported",
|
|
3302
|
-
},
|
|
3303
|
-
};
|
|
3304
|
-
}
|
|
3305
|
-
return response;
|
|
1788
|
+
return this._sendSessionRequest(sessionId, "session/cancel");
|
|
3306
1789
|
}
|
|
3307
|
-
closeSession(sessionId) {
|
|
3308
|
-
|
|
3309
|
-
!this._closedSessionIds.has(sessionId) &&
|
|
3310
|
-
!this._sessionClosePromises.has(sessionId)) {
|
|
3311
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
3312
|
-
}
|
|
3313
|
-
const closePromise = this._closeSessionInternal(sessionId);
|
|
3314
|
-
// `closeSession()` is intentionally fire-and-forget; suppress unhandled
|
|
3315
|
-
// rejections here and let tracked close promises surface errors to any
|
|
3316
|
-
// internal/test callers awaiting `_sessionClosePromises`.
|
|
3317
|
-
void closePromise.catch(() => { });
|
|
1790
|
+
async closeSession(sessionId) {
|
|
1791
|
+
await this._closeSessionInternal(sessionId);
|
|
3318
1792
|
}
|
|
3319
1793
|
async respondPermission(sessionId, permissionId, reply) {
|
|
3320
|
-
const session = this.
|
|
3321
|
-
const pendingReply = session
|
|
1794
|
+
const session = this._sessions.get(sessionId);
|
|
1795
|
+
const pendingReply = session?.pendingPermissionReplies.get(permissionId);
|
|
3322
1796
|
if (pendingReply) {
|
|
3323
|
-
session
|
|
3324
|
-
clearTimeout(pendingReply.
|
|
1797
|
+
session?.pendingPermissionReplies.delete(permissionId);
|
|
1798
|
+
clearTimeout(pendingReply.cleanupTimer);
|
|
3325
1799
|
pendingReply.resolve(reply);
|
|
3326
1800
|
return {
|
|
3327
1801
|
jsonrpc: "2.0",
|
|
@@ -3333,18 +1807,15 @@ export class AgentOs {
|
|
|
3333
1807
|
},
|
|
3334
1808
|
};
|
|
3335
1809
|
}
|
|
3336
|
-
|
|
3337
|
-
permissionId,
|
|
3338
|
-
reply,
|
|
3339
|
-
});
|
|
1810
|
+
throw new Error(`Permission request is not pending: ${permissionId}`);
|
|
3340
1811
|
}
|
|
3341
1812
|
async setSessionMode(sessionId, modeId) {
|
|
3342
1813
|
return this._sendSessionRequest(sessionId, "session/set_mode", {
|
|
3343
1814
|
modeId,
|
|
3344
1815
|
});
|
|
3345
1816
|
}
|
|
3346
|
-
getSessionModes(sessionId) {
|
|
3347
|
-
return this.
|
|
1817
|
+
async getSessionModes(sessionId) {
|
|
1818
|
+
return toSessionModes((await this._getSessionState(sessionId)).modes);
|
|
3348
1819
|
}
|
|
3349
1820
|
async setSessionModel(sessionId, model) {
|
|
3350
1821
|
return this._setSessionConfigByCategory(sessionId, "model", model);
|
|
@@ -3352,15 +1823,15 @@ export class AgentOs {
|
|
|
3352
1823
|
async setSessionThoughtLevel(sessionId, level) {
|
|
3353
1824
|
return this._setSessionConfigByCategory(sessionId, "thought_level", level);
|
|
3354
1825
|
}
|
|
3355
|
-
getSessionConfigOptions(sessionId) {
|
|
3356
|
-
return
|
|
1826
|
+
async getSessionConfigOptions(sessionId) {
|
|
1827
|
+
return toSessionConfigOptions((await this._getSessionState(sessionId)).configOptions);
|
|
3357
1828
|
}
|
|
3358
|
-
getSessionCapabilities(sessionId) {
|
|
3359
|
-
const caps = this.
|
|
1829
|
+
async getSessionCapabilities(sessionId) {
|
|
1830
|
+
const caps = toAgentCapabilities((await this._getSessionState(sessionId)).agentCapabilities);
|
|
3360
1831
|
return Object.keys(caps).length > 0 ? caps : null;
|
|
3361
1832
|
}
|
|
3362
|
-
getSessionAgentInfo(sessionId) {
|
|
3363
|
-
return this.
|
|
1833
|
+
async getSessionAgentInfo(sessionId) {
|
|
1834
|
+
return toAgentInfo((await this._getSessionState(sessionId)).agentInfo);
|
|
3364
1835
|
}
|
|
3365
1836
|
async rawSessionSend(sessionId, method, params) {
|
|
3366
1837
|
return this._sendSessionRequest(sessionId, method, params);
|
|
@@ -3381,45 +1852,79 @@ export class AgentOs {
|
|
|
3381
1852
|
}
|
|
3382
1853
|
// ── Cron ────────────────────────────────────────────────────
|
|
3383
1854
|
/** Schedule a cron job. Returns a handle with the job ID and a cancel method. */
|
|
3384
|
-
scheduleCron(options) {
|
|
1855
|
+
async scheduleCron(options) {
|
|
3385
1856
|
return this._cronManager.schedule(options);
|
|
3386
1857
|
}
|
|
3387
1858
|
/** List all registered cron jobs. */
|
|
3388
|
-
listCronJobs() {
|
|
1859
|
+
async listCronJobs() {
|
|
3389
1860
|
return this._cronManager.list();
|
|
3390
1861
|
}
|
|
3391
1862
|
/** Cancel a cron job by ID. */
|
|
3392
|
-
cancelCronJob(id) {
|
|
3393
|
-
this._cronManager.cancel(id);
|
|
1863
|
+
async cancelCronJob(id) {
|
|
1864
|
+
await this._cronManager.cancel(id);
|
|
3394
1865
|
}
|
|
3395
1866
|
/** Subscribe to cron lifecycle events (fire, complete, error). */
|
|
3396
1867
|
onCronEvent(handler) {
|
|
3397
1868
|
this._cronManager.onEvent(handler);
|
|
3398
1869
|
}
|
|
3399
1870
|
async dispose() {
|
|
3400
|
-
this.
|
|
3401
|
-
|
|
3402
|
-
await this._closeSessionInternal(sessionId).catch(() => { });
|
|
1871
|
+
if (this._disposed) {
|
|
1872
|
+
return;
|
|
3403
1873
|
}
|
|
3404
|
-
|
|
3405
|
-
|
|
1874
|
+
if (this._disposePromise) {
|
|
1875
|
+
return this._disposePromise;
|
|
3406
1876
|
}
|
|
3407
|
-
const
|
|
3408
|
-
this.
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
terminal.handle.kill();
|
|
3412
|
-
terminalExitPromises.push(terminal.waitPromise.then(() => undefined, () => undefined));
|
|
1877
|
+
const attempt = this._disposeOnce();
|
|
1878
|
+
this._disposePromise = attempt;
|
|
1879
|
+
try {
|
|
1880
|
+
await attempt;
|
|
3413
1881
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
1882
|
+
finally {
|
|
1883
|
+
if (this._disposePromise === attempt) {
|
|
1884
|
+
this._disposePromise = null;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
async _disposeOnce() {
|
|
1889
|
+
for (const sessionId of [...this._sessions.keys()]) {
|
|
1890
|
+
try {
|
|
1891
|
+
await this._closeSessionInternal(sessionId);
|
|
1892
|
+
}
|
|
1893
|
+
catch (error) {
|
|
1894
|
+
console.warn(`failed to close ACP session ${sessionId} during dispose`, error);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const shellKillResults = await Promise.allSettled([...this._shells.values()].map((entry) => entry.handle.kill()));
|
|
1898
|
+
for (const result of shellKillResults) {
|
|
1899
|
+
if (result.status === "rejected") {
|
|
1900
|
+
console.warn("failed to close shell during dispose", result.reason);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const shellExitPromises = [...this._pendingShellExitPromises.values()];
|
|
1904
|
+
await waitForTrackedExitPromises(shellExitPromises, SHELL_DISPOSE_TIMEOUT_MS);
|
|
3417
1905
|
const sidecarLease = this._sidecarLease;
|
|
3418
|
-
this._sidecarLease = null;
|
|
3419
1906
|
if (sidecarLease) {
|
|
3420
|
-
|
|
1907
|
+
await sidecarLease.dispose();
|
|
3421
1908
|
}
|
|
3422
|
-
|
|
1909
|
+
else {
|
|
1910
|
+
await this.#kernel.dispose();
|
|
1911
|
+
}
|
|
1912
|
+
// Only release local correlation after authoritative remote teardown. A
|
|
1913
|
+
// failed close leaves these routes and the lease intact for a safe retry.
|
|
1914
|
+
for (const session of this._sessions.values()) {
|
|
1915
|
+
this._rejectPendingPermissionRepliesFromSession(session);
|
|
1916
|
+
}
|
|
1917
|
+
this._sessions.clear();
|
|
1918
|
+
this._cronManager.dispose();
|
|
1919
|
+
this._shells.clear();
|
|
1920
|
+
this._processes.clear();
|
|
1921
|
+
this._disposeSidecarEventListener();
|
|
1922
|
+
this._disposeSidecarRequestHandler?.();
|
|
1923
|
+
this._disposeSidecarRequestHandler = null;
|
|
1924
|
+
if (this._sidecarLease === sidecarLease) {
|
|
1925
|
+
this._sidecarLease = null;
|
|
1926
|
+
}
|
|
1927
|
+
this._disposed = true;
|
|
3423
1928
|
}
|
|
3424
1929
|
}
|
|
3425
1930
|
const agentOsRuntimeAdmins = new WeakMap();
|
|
@@ -3441,6 +1946,162 @@ function resolveAgentOsSidecar(config) {
|
|
|
3441
1946
|
}
|
|
3442
1947
|
const sidecarStates = new WeakMap();
|
|
3443
1948
|
const sharedSidecars = new Map();
|
|
1949
|
+
let sidecarProcessExitHookInstalled = false;
|
|
1950
|
+
/**
|
|
1951
|
+
* Install a one-time, synchronous `process.on("exit")` hook that SIGKILLs any
|
|
1952
|
+
* pooled shared sidecar child. Once a one-shot host process is allowed to exit
|
|
1953
|
+
* (its sidecar handles are unref'd at 0 leases), this reaps the sidecar
|
|
1954
|
+
* immediately instead of waiting for its stdin-EOF grace window — no orphan, no
|
|
1955
|
+
* delay. We deliberately do NOT install SIGINT/SIGTERM handlers: a library
|
|
1956
|
+
* should not hijack the host's signal handling. SIGINT still reaches the sidecar
|
|
1957
|
+
* via the process group, and SIGTERM-driven exit still closes its stdin.
|
|
1958
|
+
*/
|
|
1959
|
+
function ensureSidecarProcessExitCleanup() {
|
|
1960
|
+
if (sidecarProcessExitHookInstalled)
|
|
1961
|
+
return;
|
|
1962
|
+
sidecarProcessExitHookInstalled = true;
|
|
1963
|
+
process.on("exit", () => {
|
|
1964
|
+
for (const sidecar of sharedSidecars.values()) {
|
|
1965
|
+
try {
|
|
1966
|
+
sidecarStates.get(sidecar)?.sharedChild?.kill?.("SIGKILL");
|
|
1967
|
+
}
|
|
1968
|
+
catch {
|
|
1969
|
+
// best-effort reap; the process is exiting regardless
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
function sidecarChildHandle(client) {
|
|
1975
|
+
// SidecarProcess -> StdioSidecarProtocolClient.child (the spawned ChildProcess).
|
|
1976
|
+
const protocolClient = client?.protocolClient;
|
|
1977
|
+
return protocolClient?.child ?? undefined;
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Apply the current hold state to the shared sidecar's child + stdio: ref them
|
|
1981
|
+
* while ≥1 hold is live so in-flight VM work keeps the host process alive; unref
|
|
1982
|
+
* them at 0 so a one-shot script exits on its own after `dispose()`. The sidecar
|
|
1983
|
+
* process itself stays running (reusable) and self-exits on stdin EOF when the
|
|
1984
|
+
* host finally goes away. Best-effort: never let ref/unref break VM lifecycle.
|
|
1985
|
+
*/
|
|
1986
|
+
function applySharedSidecarHold(state) {
|
|
1987
|
+
const child = state.sharedChild;
|
|
1988
|
+
if (!child)
|
|
1989
|
+
return;
|
|
1990
|
+
const hold = (state.eventLoopHolds ?? 0) > 0;
|
|
1991
|
+
for (const handle of [child, child.stdin, child.stdout, child.stderr]) {
|
|
1992
|
+
if (!handle)
|
|
1993
|
+
continue;
|
|
1994
|
+
try {
|
|
1995
|
+
if (hold)
|
|
1996
|
+
handle.ref?.();
|
|
1997
|
+
else
|
|
1998
|
+
handle.unref?.();
|
|
1999
|
+
}
|
|
2000
|
+
catch {
|
|
2001
|
+
// ref/unref is an optimization, not correctness-critical
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Take a hold for the entire create→use→dispose lifetime of one VM lease. Taken
|
|
2007
|
+
* BEFORE VM creation starts (not when the lease lands in `activeLeases`) so a VM
|
|
2008
|
+
* that is still mid-creation keeps the sidecar ref'd and a concurrent dispose
|
|
2009
|
+
* cannot unref it out from under the in-flight create.
|
|
2010
|
+
*/
|
|
2011
|
+
function acquireSharedSidecarHold(state) {
|
|
2012
|
+
state.eventLoopHolds = (state.eventLoopHolds ?? 0) + 1;
|
|
2013
|
+
if (state.eventLoopHolds === 1)
|
|
2014
|
+
applySharedSidecarHold(state);
|
|
2015
|
+
}
|
|
2016
|
+
/** Release a hold taken by {@link acquireSharedSidecarHold}; unref at 0. */
|
|
2017
|
+
function releaseSharedSidecarHold(state) {
|
|
2018
|
+
const current = state.eventLoopHolds ?? 0;
|
|
2019
|
+
if (current <= 0) {
|
|
2020
|
+
// The `holdReleased` guard makes each lease release exactly once, so this
|
|
2021
|
+
// should be unreachable. Warn rather than silently floor, per the repo's
|
|
2022
|
+
// no-silent-masking rule, so an accounting bug surfaces instead of hiding.
|
|
2023
|
+
state.eventLoopHolds = 0;
|
|
2024
|
+
console.warn("[agentos] shared sidecar event-loop hold released more than acquired");
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
state.eventLoopHolds = current - 1;
|
|
2028
|
+
if (state.eventLoopHolds === 0)
|
|
2029
|
+
applySharedSidecarHold(state);
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Spawn-once accessor for a sidecar handle's shared native process. Concurrent
|
|
2033
|
+
* callers await the same promise, so one `AgentOsSidecar` maps to exactly one
|
|
2034
|
+
* `agent-os-sidecar` OS process for its whole lifetime.
|
|
2035
|
+
*/
|
|
2036
|
+
function ensureSharedSidecarNativeProcess(sidecar) {
|
|
2037
|
+
const state = getSidecarState(sidecar);
|
|
2038
|
+
if (!state.nativeProcess) {
|
|
2039
|
+
ensureSidecarProcessExitCleanup();
|
|
2040
|
+
state.nativeProcess = (async () => {
|
|
2041
|
+
const client = SidecarProcess.spawn({
|
|
2042
|
+
command: resolvePublishedSidecarBinary(),
|
|
2043
|
+
args: [],
|
|
2044
|
+
});
|
|
2045
|
+
// Track the child immediately — BEFORE the handshake await — so a
|
|
2046
|
+
// failed `authenticateAndOpenSession()` can still reap it (otherwise
|
|
2047
|
+
// the spawned child is untracked, unreapable, and pins the loop).
|
|
2048
|
+
state.sharedChild = sidecarChildHandle(client);
|
|
2049
|
+
if (!state.sharedChild) {
|
|
2050
|
+
// We reached into @rivet-dev/agentos-runtime-core internals to get the child for
|
|
2051
|
+
// idle-unref. If that shape ever changes this returns undefined and
|
|
2052
|
+
// the optimization silently stops working (one-shot scripts would
|
|
2053
|
+
// hang again). Make it loud rather than a silent regression.
|
|
2054
|
+
console.warn("[agentos] could not resolve the shared sidecar child handle; " +
|
|
2055
|
+
"standalone scripts may not exit cleanly after dispose(). " +
|
|
2056
|
+
"This usually means @rivet-dev/agentos-runtime-core internals changed.");
|
|
2057
|
+
}
|
|
2058
|
+
// Apply the current hold state to the just-spawned child.
|
|
2059
|
+
applySharedSidecarHold(state);
|
|
2060
|
+
try {
|
|
2061
|
+
const session = await client.authenticateAndOpenSession();
|
|
2062
|
+
return { client, session };
|
|
2063
|
+
}
|
|
2064
|
+
catch (error) {
|
|
2065
|
+
// Spawn/handshake failed: reap the child, drop the cached handle,
|
|
2066
|
+
// and CLEAR the rejected promise so the next create() retries
|
|
2067
|
+
// instead of permanently wedging on a rejected `nativeProcess`.
|
|
2068
|
+
try {
|
|
2069
|
+
state.sharedChild?.kill?.("SIGKILL");
|
|
2070
|
+
}
|
|
2071
|
+
catch {
|
|
2072
|
+
// already gone
|
|
2073
|
+
}
|
|
2074
|
+
state.sharedChild = undefined;
|
|
2075
|
+
state.nativeProcess = undefined;
|
|
2076
|
+
throw error;
|
|
2077
|
+
}
|
|
2078
|
+
})();
|
|
2079
|
+
}
|
|
2080
|
+
return state.nativeProcess;
|
|
2081
|
+
}
|
|
2082
|
+
/** Dispose a sidecar handle's shared native process, if one was spawned. */
|
|
2083
|
+
async function disposeSharedSidecarNativeProcess(state) {
|
|
2084
|
+
const pending = state.nativeProcess;
|
|
2085
|
+
if (!pending) {
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
const { client, session } = await pending;
|
|
2089
|
+
// The TS handle shares one real wire session across all leased VMs. Close it
|
|
2090
|
+
// only after every VM lease has confirmed disposal, then terminate transport.
|
|
2091
|
+
// A rejected close retains the live process/session handle so the caller can
|
|
2092
|
+
// retry the idempotent sidecar operation.
|
|
2093
|
+
await client.closeSession(session);
|
|
2094
|
+
await client.dispose();
|
|
2095
|
+
state.nativeProcess = undefined;
|
|
2096
|
+
// The cached child is now dead; drop it (symmetric with the assignment in
|
|
2097
|
+
// ensureSharedSidecarNativeProcess). We deliberately do NOT zero
|
|
2098
|
+
// `eventLoopHolds` here: this runs only from `AgentOsSidecar.dispose()`, which
|
|
2099
|
+
// has already set the handle to `disposing` (so no new lease can acquire) and
|
|
2100
|
+
// drained `activeLeases`; the disposed handle's state is then abandoned. Force-
|
|
2101
|
+
// zeroing a shared counter could clobber a hold on a freshly re-acquired
|
|
2102
|
+
// process generation, so it is left to the balanced acquire/release pairs.
|
|
2103
|
+
state.sharedChild = undefined;
|
|
2104
|
+
}
|
|
3444
2105
|
export class AgentOsSidecar {
|
|
3445
2106
|
constructor(sidecarId, placement, sharedPool) {
|
|
3446
2107
|
sidecarStates.set(this, {
|
|
@@ -3463,6 +2124,21 @@ export class AgentOsSidecar {
|
|
|
3463
2124
|
if (state.description.state === "disposed") {
|
|
3464
2125
|
return;
|
|
3465
2126
|
}
|
|
2127
|
+
if (state.disposePromise) {
|
|
2128
|
+
return state.disposePromise;
|
|
2129
|
+
}
|
|
2130
|
+
const attempt = this.disposeOnce(state);
|
|
2131
|
+
state.disposePromise = attempt;
|
|
2132
|
+
try {
|
|
2133
|
+
await attempt;
|
|
2134
|
+
}
|
|
2135
|
+
finally {
|
|
2136
|
+
if (state.disposePromise === attempt) {
|
|
2137
|
+
state.disposePromise = undefined;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
async disposeOnce(state) {
|
|
3466
2142
|
state.description.state = "disposing";
|
|
3467
2143
|
const errors = [];
|
|
3468
2144
|
for (const lease of [...state.activeLeases]) {
|
|
@@ -3473,15 +2149,15 @@ export class AgentOsSidecar {
|
|
|
3473
2149
|
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
3474
2150
|
}
|
|
3475
2151
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
2152
|
+
if (errors.length > 0) {
|
|
2153
|
+
throw new AggregateError(errors, `failed to dispose sidecar ${state.description.sidecarId}`);
|
|
2154
|
+
}
|
|
2155
|
+
// Tear down the shared native process after all leased VMs are gone.
|
|
2156
|
+
await disposeSharedSidecarNativeProcess(state);
|
|
3478
2157
|
state.description.state = "disposed";
|
|
3479
2158
|
if (state.sharedPool && sharedSidecars.get(state.sharedPool) === this) {
|
|
3480
2159
|
sharedSidecars.delete(state.sharedPool);
|
|
3481
2160
|
}
|
|
3482
|
-
if (errors.length > 0) {
|
|
3483
|
-
throw new Error(errors.map((error) => error.message).join("; "));
|
|
3484
|
-
}
|
|
3485
2161
|
}
|
|
3486
2162
|
}
|
|
3487
2163
|
function createAgentOsSidecarInternal(options = {}) {
|
|
@@ -3536,7 +2212,20 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3536
2212
|
return transport;
|
|
3537
2213
|
},
|
|
3538
2214
|
});
|
|
2215
|
+
// Hold the shared sidecar's event-loop ref for this lease's WHOLE lifetime —
|
|
2216
|
+
// taken now, before VM creation, so a concurrent dispose cannot unref the
|
|
2217
|
+
// sidecar while this create is still in flight. Released exactly once on
|
|
2218
|
+
// dispose or on a failed create.
|
|
2219
|
+
acquireSharedSidecarHold(state);
|
|
2220
|
+
let holdReleased = false;
|
|
2221
|
+
const releaseHold = () => {
|
|
2222
|
+
if (holdReleased)
|
|
2223
|
+
return;
|
|
2224
|
+
holdReleased = true;
|
|
2225
|
+
releaseSharedSidecarHold(state);
|
|
2226
|
+
};
|
|
3539
2227
|
let disposed = false;
|
|
2228
|
+
let disposePromise = null;
|
|
3540
2229
|
let leaseRecord;
|
|
3541
2230
|
try {
|
|
3542
2231
|
const session = await client.createSession({
|
|
@@ -3556,10 +2245,27 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3556
2245
|
if (disposed) {
|
|
3557
2246
|
return;
|
|
3558
2247
|
}
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
2248
|
+
if (!disposePromise) {
|
|
2249
|
+
disposePromise = (async () => {
|
|
2250
|
+
await client.dispose();
|
|
2251
|
+
disposed = true;
|
|
2252
|
+
state.activeLeases.delete(leaseRecord);
|
|
2253
|
+
state.description.activeVmCount = state.activeLeases.size;
|
|
2254
|
+
// Release this lease's hold; the shared sidecar is unref'd only
|
|
2255
|
+
// once the last hold (across all in-flight + active leases) drops,
|
|
2256
|
+
// so a one-shot host process can then exit on its own.
|
|
2257
|
+
releaseHold();
|
|
2258
|
+
})();
|
|
2259
|
+
}
|
|
2260
|
+
const attempt = disposePromise;
|
|
2261
|
+
try {
|
|
2262
|
+
await attempt;
|
|
2263
|
+
}
|
|
2264
|
+
finally {
|
|
2265
|
+
if (disposePromise === attempt) {
|
|
2266
|
+
disposePromise = null;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
3563
2269
|
},
|
|
3564
2270
|
};
|
|
3565
2271
|
leaseRecord = {
|
|
@@ -3570,20 +2276,39 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3570
2276
|
return lease;
|
|
3571
2277
|
}
|
|
3572
2278
|
catch (error) {
|
|
3573
|
-
await client.dispose().catch(() => {
|
|
2279
|
+
await client.dispose().catch((cleanupError) => {
|
|
2280
|
+
console.warn("failed to dispose sidecar client after lease creation failure", cleanupError);
|
|
2281
|
+
});
|
|
2282
|
+
releaseHold();
|
|
3574
2283
|
throw error;
|
|
3575
2284
|
}
|
|
3576
2285
|
}
|
|
3577
2286
|
async function createInProcessSidecarTransport(sessionBootstrap, options) {
|
|
3578
2287
|
const vmAdmins = new Map();
|
|
3579
2288
|
let disposed = false;
|
|
2289
|
+
let disposePromise = null;
|
|
3580
2290
|
async function disposeVmAdmin(vmId) {
|
|
3581
2291
|
const admin = vmAdmins.get(vmId);
|
|
3582
2292
|
if (!admin) {
|
|
3583
2293
|
return;
|
|
3584
2294
|
}
|
|
3585
|
-
vmAdmins.delete(vmId);
|
|
3586
2295
|
await admin.dispose();
|
|
2296
|
+
vmAdmins.delete(vmId);
|
|
2297
|
+
}
|
|
2298
|
+
async function disposeTransport() {
|
|
2299
|
+
const errors = [];
|
|
2300
|
+
for (const vmId of [...vmAdmins.keys()]) {
|
|
2301
|
+
try {
|
|
2302
|
+
await disposeVmAdmin(vmId);
|
|
2303
|
+
}
|
|
2304
|
+
catch (error) {
|
|
2305
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (errors.length > 0) {
|
|
2309
|
+
throw new AggregateError(errors, "failed to dispose sidecar session");
|
|
2310
|
+
}
|
|
2311
|
+
disposed = true;
|
|
3587
2312
|
}
|
|
3588
2313
|
return {
|
|
3589
2314
|
async createVm(vmBootstrap) {
|
|
@@ -3600,18 +2325,17 @@ async function createInProcessSidecarTransport(sessionBootstrap, options) {
|
|
|
3600
2325
|
if (disposed) {
|
|
3601
2326
|
return;
|
|
3602
2327
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
catch (error) {
|
|
3610
|
-
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
3611
|
-
}
|
|
2328
|
+
if (!disposePromise) {
|
|
2329
|
+
disposePromise = disposeTransport();
|
|
2330
|
+
}
|
|
2331
|
+
const attempt = disposePromise;
|
|
2332
|
+
try {
|
|
2333
|
+
await attempt;
|
|
3612
2334
|
}
|
|
3613
|
-
|
|
3614
|
-
|
|
2335
|
+
finally {
|
|
2336
|
+
if (disposePromise === attempt) {
|
|
2337
|
+
disposePromise = null;
|
|
2338
|
+
}
|
|
3615
2339
|
}
|
|
3616
2340
|
},
|
|
3617
2341
|
getVmAdmin(vmId) {
|