@rivet-dev/agentos-core 0.0.0-integrate-dylib-into-main.815fcda → 0.0.0-main.29b0797
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 +43 -43
- package/dist/agent-os.d.ts +313 -226
- package/dist/agent-os.js +1418 -1545
- package/dist/agentos-package.d.ts +31 -0
- package/dist/agentos-package.js +91 -0
- package/dist/base-filesystem.d.ts +2 -15
- package/dist/base-filesystem.js +23 -171
- package/dist/{host-tools-zod.d.ts → bindings-zod.d.ts} +1 -1
- package/dist/{host-tools-zod.js → bindings-zod.js} +52 -25
- package/dist/bindings.d.ts +43 -0
- package/dist/bindings.js +34 -0
- package/dist/cron/cron-manager.js +43 -10
- package/dist/cron/index.d.ts +1 -1
- package/dist/cron/types.d.ts +26 -10
- package/dist/default-software.d.ts +8 -0
- package/dist/default-software.js +10 -0
- package/dist/filesystem-snapshot.d.ts +1 -2
- package/dist/filesystem-snapshot.js +0 -43
- package/dist/generated/AcpLimitsConfig.d.ts +20 -0
- package/dist/generated/AcpLimitsConfig.js +2 -0
- package/dist/generated/BindingLimitsConfig.d.ts +10 -0
- package/dist/generated/BindingLimitsConfig.js +2 -0
- package/dist/generated/CreateVmConfig.d.ts +28 -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 +11 -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/ProcessLimitsConfig.d.ts +7 -0
- package/dist/generated/ProcessLimitsConfig.js +2 -0
- package/dist/generated/PythonLimitsConfig.d.ts +6 -0
- package/dist/generated/PythonLimitsConfig.js +2 -0
- package/dist/generated/ResourceLimitsConfig.d.ts +24 -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/VmDnsConfig.d.ts +6 -0
- package/dist/generated/VmDnsConfig.js +2 -0
- package/dist/generated/VmGroupConfig.d.ts +5 -0
- package/dist/generated/VmGroupConfig.js +2 -0
- package/dist/generated/VmLimitsConfig.d.ts +20 -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/VmUserAccountConfig.d.ts +9 -0
- package/dist/generated/VmUserAccountConfig.js +2 -0
- package/dist/generated/VmUserConfig.d.ts +19 -0
- package/dist/generated/VmUserConfig.js +1 -0
- package/dist/generated/WasmLimitsConfig.d.ts +8 -0
- package/dist/generated/WasmLimitsConfig.js +2 -0
- package/dist/host-dir-mount.d.ts +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -5
- package/dist/layers.d.ts +7 -4
- package/dist/layers.js +0 -176
- package/dist/options-schema.d.ts +556 -0
- package/dist/options-schema.js +386 -0
- package/dist/packages.d.ts +17 -116
- package/dist/packages.js +35 -322
- package/dist/runtime-compat.d.ts +33 -46
- package/dist/runtime-compat.js +197 -511
- package/dist/runtime.d.ts +42 -8
- package/dist/sandbox.d.ts +112 -0
- package/dist/sandbox.js +264 -0
- package/dist/session-api.d.ts +180 -0
- package/dist/session-api.js +1 -0
- package/dist/sidecar/agentos-protocol.d.ts +509 -20
- package/dist/sidecar/agentos-protocol.js +808 -81
- package/dist/sidecar/native-process-client.d.ts +6 -2
- package/dist/sidecar/native-process-client.js +8 -1
- package/dist/sidecar/permissions.d.ts +1 -1
- package/dist/sidecar/permissions.js +4 -4
- package/dist/sidecar/rpc-client.d.ts +100 -9
- package/dist/sidecar/rpc-client.js +445 -110
- package/dist/test/runtime.d.ts +2 -1
- package/dist/test/runtime.js +2 -1
- package/dist/test/terminal-harness.js +0 -3
- package/dist/types.d.ts +13 -7
- package/package.json +46 -35
- package/dist/agent-session-types.d.ts +0 -67
- package/dist/agents.d.ts +0 -63
- package/dist/agents.js +0 -42
- package/dist/host-tools.d.ts +0 -45
- package/dist/host-tools.js +0 -26
- package/dist/overlay-filesystem.d.ts +0 -19
- package/dist/overlay-filesystem.js +0 -645
- package/fixtures/AGENTOS_SYSTEM_PROMPT.md +0 -21
- /package/dist/{agent-session-types.js → generated/CreateVmConfig.js} +0 -0
package/dist/agent-os.js
CHANGED
|
@@ -1,32 +1,141 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync,
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
|
|
3
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
8
|
-
import { zodToJsonSchema } from "./
|
|
6
|
+
import { validateBindings } from "./bindings.js";
|
|
7
|
+
import { zodToJsonSchema } from "./bindings-zod.js";
|
|
8
|
+
import { parseAgentOsOptions } from "./options-schema.js";
|
|
9
9
|
import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
|
|
10
10
|
import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
|
|
11
|
-
export { isAcpTimeoutErrorData } from "./json-rpc.js";
|
|
12
11
|
const ACP_PROTOCOL_VERSION = 1;
|
|
13
12
|
const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
|
|
14
13
|
const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function defaultAcpClientCapabilities() {
|
|
14
|
+
function safeWireU64(value) {
|
|
15
|
+
const result = Number(value);
|
|
16
|
+
if (!Number.isSafeInteger(result) || result < 0) {
|
|
17
|
+
throw new RangeError(`wire integer ${value} exceeds JavaScript's safe range`);
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
function decodeDurableSessionInfo(value) {
|
|
24
22
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
sessionId: value.sessionId,
|
|
24
|
+
agent: value.agent,
|
|
25
|
+
cwd: value.cwd,
|
|
26
|
+
additionalDirectories: JSON.parse(value.additionalDirectories),
|
|
27
|
+
state: JSON.parse(value.state),
|
|
28
|
+
latestSequence: safeWireU64(value.latestSequence),
|
|
29
|
+
title: value.title ?? undefined,
|
|
30
|
+
metadata: value.metadata === null ? undefined : JSON.parse(value.metadata),
|
|
31
|
+
createdAt: value.createdAt,
|
|
32
|
+
updatedAt: value.updatedAt,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const PERMISSION_TERMINAL_REASONS = new Set([
|
|
36
|
+
"already_resolved",
|
|
37
|
+
"prompt_cancelled",
|
|
38
|
+
"adapter_exited",
|
|
39
|
+
"session_deleted",
|
|
40
|
+
"vm_shutdown",
|
|
41
|
+
"request_not_found",
|
|
42
|
+
]);
|
|
43
|
+
function permissionTerminalReason(value) {
|
|
44
|
+
if (value !== null &&
|
|
45
|
+
PERMISSION_TERMINAL_REASONS.has(value)) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`invalid permission terminal reason: ${value ?? "missing"}`);
|
|
49
|
+
}
|
|
50
|
+
function decodeDurableSessionEvent(value) {
|
|
51
|
+
const envelope = {
|
|
52
|
+
durability: "durable",
|
|
53
|
+
sessionId: value.sessionId,
|
|
54
|
+
sequence: safeWireU64(value.sequence),
|
|
55
|
+
timestamp: value.timestamp,
|
|
56
|
+
};
|
|
57
|
+
switch (value.event.tag) {
|
|
58
|
+
case "AcpDurableSessionUpdate": {
|
|
59
|
+
const update = JSON.parse(value.event.val.update);
|
|
60
|
+
const { sessionUpdate: type, ...payload } = update;
|
|
61
|
+
return {
|
|
62
|
+
...envelope,
|
|
63
|
+
type,
|
|
64
|
+
...payload,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
case "AcpDurablePermissionRequest": {
|
|
68
|
+
const request = JSON.parse(value.event.val.request);
|
|
69
|
+
const { sessionId: _sessionId, ...payload } = request;
|
|
70
|
+
return {
|
|
71
|
+
...envelope,
|
|
72
|
+
type: "permission_request",
|
|
73
|
+
requestId: value.event.val.requestId,
|
|
74
|
+
...payload,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "AcpDurablePermissionResponse": {
|
|
78
|
+
const status = value.event.val.status;
|
|
79
|
+
if (status !== "accepted" && status !== "not_pending") {
|
|
80
|
+
throw new Error(`invalid permission response event status: ${status}`);
|
|
81
|
+
}
|
|
82
|
+
const response = JSON.parse(value.event.val.response);
|
|
83
|
+
return {
|
|
84
|
+
...envelope,
|
|
85
|
+
type: "permission_response",
|
|
86
|
+
requestId: value.event.val.requestId,
|
|
87
|
+
...response,
|
|
88
|
+
status,
|
|
89
|
+
...(value.event.val.reason === null
|
|
90
|
+
? {}
|
|
91
|
+
: { reason: permissionTerminalReason(value.event.val.reason) }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function normalizeSessionCapabilities(value) {
|
|
97
|
+
const capabilities = toRecord(value);
|
|
98
|
+
const prompt = toRecord(capabilities.promptCapabilities);
|
|
99
|
+
const mcp = toRecord(capabilities.mcpCapabilities);
|
|
100
|
+
const session = toRecord(capabilities.sessionCapabilities);
|
|
101
|
+
const extensions = Object.fromEntries(Object.entries(capabilities).filter(([key]) => ![
|
|
102
|
+
"loadSession",
|
|
103
|
+
"promptCapabilities",
|
|
104
|
+
"mcpCapabilities",
|
|
105
|
+
"sessionCapabilities",
|
|
106
|
+
].includes(key)));
|
|
107
|
+
return {
|
|
108
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
109
|
+
loadSession: capabilities.loadSession === true,
|
|
110
|
+
...(Object.keys(prompt).length > 0
|
|
111
|
+
? {
|
|
112
|
+
prompt: {
|
|
113
|
+
audio: prompt.audio === true || undefined,
|
|
114
|
+
embeddedContext: prompt.embeddedContext === true || undefined,
|
|
115
|
+
image: prompt.image === true || undefined,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
...(Object.keys(mcp).length > 0
|
|
120
|
+
? {
|
|
121
|
+
mcp: {
|
|
122
|
+
http: mcp.http === true || undefined,
|
|
123
|
+
sse: mcp.sse === true || undefined,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
127
|
+
...(Object.keys(session).length > 0
|
|
128
|
+
? {
|
|
129
|
+
session: {
|
|
130
|
+
list: typeof session.list === "object" || undefined,
|
|
131
|
+
resume: typeof session.resume === "object" || undefined,
|
|
132
|
+
close: typeof session.close === "object" || undefined,
|
|
133
|
+
delete: typeof session.delete === "object" || undefined,
|
|
134
|
+
additionalDirectories: typeof session.additionalDirectories === "object" || undefined,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
: {}),
|
|
138
|
+
...(Object.keys(extensions).length > 0 ? { extensions } : {}),
|
|
30
139
|
};
|
|
31
140
|
}
|
|
32
141
|
async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
@@ -40,21 +149,24 @@ async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
|
40
149
|
}),
|
|
41
150
|
]);
|
|
42
151
|
}
|
|
43
|
-
import {
|
|
44
|
-
import { getBaseEnvironment
|
|
152
|
+
import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
|
|
153
|
+
import { getBaseEnvironment } from "./base-filesystem.js";
|
|
45
154
|
import { CronManager } from "./cron/cron-manager.js";
|
|
46
155
|
import { TimerScheduleDriver } from "./cron/timer-driver.js";
|
|
156
|
+
import { resolveDefaultSoftware } from "./default-software.js";
|
|
47
157
|
import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
|
|
48
|
-
import { createHostDirBackend } from "./host-dir-mount.js";
|
|
49
158
|
import { serializeMountConfigForSidecar, } from "./js-bridge.js";
|
|
50
159
|
import { createSnapshotExport, } from "./layers.js";
|
|
51
|
-
import { processSoftware, resolvePackageDir, } from "./packages.js";
|
|
52
160
|
import { allowAll } from "./runtime-compat.js";
|
|
53
|
-
import {
|
|
161
|
+
import { decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
|
|
54
162
|
import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
|
|
55
|
-
import { createAgentOsSidecarClient,
|
|
56
|
-
|
|
57
|
-
|
|
163
|
+
import { createAgentOsSidecarClient, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
|
|
164
|
+
function defaultAgentStderrHandler(event) {
|
|
165
|
+
process.stderr.write(event.chunk);
|
|
166
|
+
}
|
|
167
|
+
function defaultAgentExitHandler(event) {
|
|
168
|
+
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"}; restore explicitly before retrying\n`);
|
|
169
|
+
}
|
|
58
170
|
class AcpDispatchError extends Error {
|
|
59
171
|
code;
|
|
60
172
|
data;
|
|
@@ -105,51 +217,28 @@ function toRecord(value) {
|
|
|
105
217
|
? value
|
|
106
218
|
: {};
|
|
107
219
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
220
|
+
function normalizePackageRef(value) {
|
|
221
|
+
// The single package reference is `packagePath`: the packed `.aospkg` file
|
|
222
|
+
// (registry-built packages export `{ packagePath }`), or a package dir for
|
|
223
|
+
// local transition fixtures. A raw string is shorthand for the same path.
|
|
224
|
+
if (typeof value === "string") {
|
|
225
|
+
return { path: value };
|
|
111
226
|
}
|
|
112
|
-
|
|
113
|
-
|
|
227
|
+
const record = toRecord(value);
|
|
228
|
+
if (typeof record.packagePath === "string") {
|
|
229
|
+
return { path: record.packagePath };
|
|
114
230
|
}
|
|
115
|
-
|
|
116
|
-
|
|
231
|
+
// Recognizably-legacy shapes fail loudly: silently dropping a software
|
|
232
|
+
// entry boots a VM with missing packages and no diagnostic.
|
|
233
|
+
for (const legacy of ["packageTar", "packageDir", "dir"]) {
|
|
234
|
+
if (typeof record[legacy] === "string") {
|
|
235
|
+
throw new Error(`agentOS package ref uses removed field "${legacy}" (value: ${JSON.stringify(record[legacy])}); ` +
|
|
236
|
+
"packages are referenced by a single `packagePath` — update the package " +
|
|
237
|
+
"(rebuild @agentos-software/* dependencies) or pass { packagePath }");
|
|
238
|
+
}
|
|
117
239
|
}
|
|
240
|
+
return undefined;
|
|
118
241
|
}
|
|
119
|
-
function parseAcpJsonList(values, context) {
|
|
120
|
-
return values.map((value, index) => parseAcpJson(value, `${context}[${index}]`));
|
|
121
|
-
}
|
|
122
|
-
function sidecarSessionCreatedFromAcp(response) {
|
|
123
|
-
return {
|
|
124
|
-
sessionId: response.sessionId,
|
|
125
|
-
...(response.pid !== null ? { pid: response.pid } : {}),
|
|
126
|
-
modes: parseAcpJson(response.modes, "modes"),
|
|
127
|
-
configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
|
|
128
|
-
agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
|
|
129
|
-
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
function sidecarSessionStateFromAcp(response) {
|
|
133
|
-
return {
|
|
134
|
-
sessionId: response.sessionId,
|
|
135
|
-
agentType: response.agentType,
|
|
136
|
-
processId: response.processId,
|
|
137
|
-
...(response.pid !== null ? { pid: response.pid } : {}),
|
|
138
|
-
closed: response.closed,
|
|
139
|
-
modes: parseAcpJson(response.modes, "modes"),
|
|
140
|
-
configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
|
|
141
|
-
agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
|
|
142
|
-
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
function isLocalCancelledPromptResponse(method, response) {
|
|
146
|
-
const result = toRecord(response.result);
|
|
147
|
-
return (method === "session/prompt" &&
|
|
148
|
-
response.id === null &&
|
|
149
|
-
response.error === undefined &&
|
|
150
|
-
result.stopReason === "cancelled");
|
|
151
|
-
}
|
|
152
|
-
const CLOSED_SESSION_ID_RETENTION_LIMIT = 2048;
|
|
153
242
|
const CLOSED_SHELL_ID_RETENTION_LIMIT = 2048;
|
|
154
243
|
class BoundedSet {
|
|
155
244
|
limit;
|
|
@@ -160,11 +249,11 @@ class BoundedSet {
|
|
|
160
249
|
}
|
|
161
250
|
this.limit = limit;
|
|
162
251
|
}
|
|
163
|
-
add(value) {
|
|
252
|
+
add(value, associated) {
|
|
164
253
|
if (this.#entries.has(value)) {
|
|
165
254
|
this.#entries.delete(value);
|
|
166
255
|
}
|
|
167
|
-
this.#entries.set(value,
|
|
256
|
+
this.#entries.set(value, associated);
|
|
168
257
|
if (this.#entries.size <= this.limit) {
|
|
169
258
|
return;
|
|
170
259
|
}
|
|
@@ -176,6 +265,9 @@ class BoundedSet {
|
|
|
176
265
|
has(value) {
|
|
177
266
|
return this.#entries.has(value);
|
|
178
267
|
}
|
|
268
|
+
get(value) {
|
|
269
|
+
return this.#entries.get(value);
|
|
270
|
+
}
|
|
179
271
|
delete(value) {
|
|
180
272
|
return this.#entries.delete(value);
|
|
181
273
|
}
|
|
@@ -183,50 +275,6 @@ class BoundedSet {
|
|
|
183
275
|
return this.#entries.size;
|
|
184
276
|
}
|
|
185
277
|
}
|
|
186
|
-
function shouldDispatchToSessionEventHandlers(notification) {
|
|
187
|
-
return notification.method === "session/update";
|
|
188
|
-
}
|
|
189
|
-
function toSessionModes(value) {
|
|
190
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
return value;
|
|
194
|
-
}
|
|
195
|
-
function toSessionConfigOptions(value) {
|
|
196
|
-
return Array.isArray(value) ? value : [];
|
|
197
|
-
}
|
|
198
|
-
function toAgentCapabilities(value) {
|
|
199
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
200
|
-
return {};
|
|
201
|
-
}
|
|
202
|
-
return value;
|
|
203
|
-
}
|
|
204
|
-
function toAgentInfo(value) {
|
|
205
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
if (typeof value.name !== "string") {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
return value;
|
|
212
|
-
}
|
|
213
|
-
function sessionEntryFromInit(sessionId, agentType, initData) {
|
|
214
|
-
return {
|
|
215
|
-
sessionId,
|
|
216
|
-
agentType,
|
|
217
|
-
processId: "",
|
|
218
|
-
pid: null,
|
|
219
|
-
closed: false,
|
|
220
|
-
modes: initData.modes ?? null,
|
|
221
|
-
configOptions: initData.configOptions ?? [],
|
|
222
|
-
capabilities: initData.capabilities ?? {},
|
|
223
|
-
agentInfo: initData.agentInfo ?? null,
|
|
224
|
-
eventHandlers: new Set(),
|
|
225
|
-
permissionHandlers: new Set(),
|
|
226
|
-
configOverrides: new Map(),
|
|
227
|
-
pendingPermissionReplies: new Map(),
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
278
|
function isOverlayMountConfig(config) {
|
|
231
279
|
return "filesystem" in config;
|
|
232
280
|
}
|
|
@@ -366,7 +414,8 @@ const KERNEL_POSIX_BOOTSTRAP_DIRS = [
|
|
|
366
414
|
"/mnt",
|
|
367
415
|
"/media",
|
|
368
416
|
"/home",
|
|
369
|
-
"/home/
|
|
417
|
+
"/home/agentos",
|
|
418
|
+
"/workspace",
|
|
370
419
|
"/usr",
|
|
371
420
|
"/usr/bin",
|
|
372
421
|
"/usr/games",
|
|
@@ -390,130 +439,59 @@ const KERNEL_POSIX_BOOTSTRAP_DIRS = [
|
|
|
390
439
|
"/var/tmp",
|
|
391
440
|
"/etc/agentos",
|
|
392
441
|
];
|
|
393
|
-
|
|
394
|
-
|
|
442
|
+
// Standard POSIX metadata for the bootstrap dirs whose mode/owner is NOT the
|
|
443
|
+
// default (`755`, root:root). Replicated as a constant so building the no-base
|
|
444
|
+
// bootstrap layer needs no `base-filesystem.json` read — when a base IS present
|
|
445
|
+
// the sidecar's embedded base layer is authoritative and these are never emitted.
|
|
446
|
+
const KERNEL_POSIX_BOOTSTRAP_DIR_METADATA = {
|
|
447
|
+
"/tmp": { mode: "1777", uid: 0, gid: 0 },
|
|
448
|
+
"/root": { mode: "700", uid: 0, gid: 0 },
|
|
449
|
+
"/sys": { mode: "555", uid: 0, gid: 0 },
|
|
450
|
+
"/home/agentos": { mode: "2755", uid: 1000, gid: 1000 },
|
|
451
|
+
"/workspace": { mode: "755", uid: 1000, gid: 1000 },
|
|
452
|
+
"/var/empty": { mode: "555", uid: 0, gid: 0 },
|
|
453
|
+
"/var/lock": { mode: "777", uid: 0, gid: 0 },
|
|
454
|
+
"/var/run": { mode: "777", uid: 0, gid: 0 },
|
|
455
|
+
"/var/tmp": { mode: "1777", uid: 0, gid: 0 },
|
|
456
|
+
};
|
|
457
|
+
// Runtime commands that get a `/bin/<cmd>` stub at bootstrap so the guest shell
|
|
458
|
+
// resolves them on PATH (e.g. `sh -c "python ..."`, pipelines). The sidecar
|
|
459
|
+
// intercepts these by name and routes them to the embedded V8 / Pyodide runtime.
|
|
460
|
+
const RUNTIME_BOOTSTRAP_COMMANDS = [
|
|
461
|
+
"node",
|
|
462
|
+
"npm",
|
|
463
|
+
"npx",
|
|
464
|
+
"python",
|
|
465
|
+
"python3",
|
|
466
|
+
];
|
|
395
467
|
const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
|
|
396
468
|
const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-sidecar");
|
|
397
469
|
const SIDECAR_BUILD_INPUTS = [
|
|
398
470
|
join(REPO_ROOT, "Cargo.toml"),
|
|
399
471
|
join(REPO_ROOT, "Cargo.lock"),
|
|
400
472
|
join(REPO_ROOT, "crates/bridge"),
|
|
473
|
+
join(REPO_ROOT, "crates/build-support"),
|
|
401
474
|
join(REPO_ROOT, "crates/execution"),
|
|
402
475
|
join(REPO_ROOT, "crates/kernel"),
|
|
403
|
-
join(REPO_ROOT, "crates/
|
|
476
|
+
join(REPO_ROOT, "crates/agentos-protocol"),
|
|
477
|
+
join(REPO_ROOT, "crates/agentos-sidecar"),
|
|
478
|
+
join(REPO_ROOT, "crates/native-sidecar"),
|
|
479
|
+
join(REPO_ROOT, "crates/native-sidecar-core"),
|
|
480
|
+
join(REPO_ROOT, "crates/sidecar-protocol"),
|
|
481
|
+
join(REPO_ROOT, "crates/v8-runtime"),
|
|
482
|
+
join(REPO_ROOT, "crates/vfs"),
|
|
483
|
+
join(REPO_ROOT, "crates/vm-config"),
|
|
484
|
+
join(REPO_ROOT, "packages/build-tools/bridge-src"),
|
|
485
|
+
join(REPO_ROOT, "packages/build-tools/package.json"),
|
|
486
|
+
join(REPO_ROOT, "packages/build-tools/scripts/build-v8-bridge.mjs"),
|
|
487
|
+
join(REPO_ROOT, "packages/core/fixtures/base-filesystem.json"),
|
|
488
|
+
join(REPO_ROOT, "packages/runtime-core/fixtures/base-filesystem.json"),
|
|
489
|
+
join(REPO_ROOT, "pnpm-lock.yaml"),
|
|
404
490
|
];
|
|
405
491
|
let ensuredSidecarBinary = null;
|
|
406
|
-
function isWasmBinaryFile(path) {
|
|
407
|
-
try {
|
|
408
|
-
const header = readFileSync(path);
|
|
409
|
-
return (header.length >= 4 &&
|
|
410
|
-
header[0] === 0x00 &&
|
|
411
|
-
header[1] === 0x61 &&
|
|
412
|
-
header[2] === 0x73 &&
|
|
413
|
-
header[3] === 0x6d);
|
|
414
|
-
}
|
|
415
|
-
catch {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
function collectBootstrapWasmCommands(commandDirs) {
|
|
420
|
-
const commands = [];
|
|
421
|
-
const seen = new Set();
|
|
422
|
-
for (const dir of commandDirs) {
|
|
423
|
-
let entries;
|
|
424
|
-
try {
|
|
425
|
-
entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
for (const entry of entries) {
|
|
431
|
-
if (entry.startsWith(".")) {
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
const fullPath = join(dir, entry);
|
|
435
|
-
try {
|
|
436
|
-
if (statSync(fullPath).isDirectory()) {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
catch {
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (!isWasmBinaryFile(fullPath) || seen.has(entry)) {
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
seen.add(entry);
|
|
447
|
-
commands.push(entry);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return commands;
|
|
451
|
-
}
|
|
452
|
-
function resolveDeclaredCommandSource(commandDir, commandName, aliases) {
|
|
453
|
-
let current = commandName;
|
|
454
|
-
const visited = new Set();
|
|
455
|
-
while (!visited.has(current)) {
|
|
456
|
-
visited.add(current);
|
|
457
|
-
const candidatePath = join(commandDir, current);
|
|
458
|
-
if (isWasmBinaryFile(candidatePath)) {
|
|
459
|
-
return candidatePath;
|
|
460
|
-
}
|
|
461
|
-
const next = aliases[current];
|
|
462
|
-
if (!next) {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
current = next;
|
|
466
|
-
}
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
function prepareCommandDirs(commandPackages) {
|
|
470
|
-
const commandDirs = [];
|
|
471
|
-
const tempDirs = [];
|
|
472
|
-
try {
|
|
473
|
-
for (const commandPackage of commandPackages) {
|
|
474
|
-
commandDirs.push(commandPackage.commandDir);
|
|
475
|
-
const aliasEntries = Object.entries(commandPackage.aliases)
|
|
476
|
-
.sort(([leftAlias], [rightAlias]) => leftAlias.localeCompare(rightAlias))
|
|
477
|
-
.flatMap(([aliasName]) => {
|
|
478
|
-
const aliasPath = join(commandPackage.commandDir, aliasName);
|
|
479
|
-
if (isWasmBinaryFile(aliasPath)) {
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
482
|
-
const sourcePath = resolveDeclaredCommandSource(commandPackage.commandDir, aliasName, commandPackage.aliases);
|
|
483
|
-
if (!sourcePath) {
|
|
484
|
-
return [];
|
|
485
|
-
}
|
|
486
|
-
return [[aliasName, sourcePath]];
|
|
487
|
-
});
|
|
488
|
-
if (aliasEntries.length === 0) {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
const aliasDir = mkdtempSync(join(tmpdir(), "agentos-command-aliases-"));
|
|
492
|
-
for (const [aliasName, sourcePath] of aliasEntries) {
|
|
493
|
-
writeFileSync(join(aliasDir, aliasName), readFileSync(sourcePath));
|
|
494
|
-
}
|
|
495
|
-
tempDirs.push(aliasDir);
|
|
496
|
-
commandDirs.push(aliasDir);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
for (const tempDir of tempDirs) {
|
|
501
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
502
|
-
}
|
|
503
|
-
throw error;
|
|
504
|
-
}
|
|
505
|
-
return {
|
|
506
|
-
commandDirs,
|
|
507
|
-
dispose() {
|
|
508
|
-
for (const tempDir of tempDirs) {
|
|
509
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
510
|
-
}
|
|
511
|
-
},
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
492
|
function collectConfiguredLowerPaths(config) {
|
|
515
493
|
const paths = new Set();
|
|
516
|
-
for (const lower of config?.lowers ?? []) {
|
|
494
|
+
for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
|
|
517
495
|
if (lower.kind !== "snapshot-export") {
|
|
518
496
|
continue;
|
|
519
497
|
}
|
|
@@ -524,7 +502,7 @@ function collectConfiguredLowerPaths(config) {
|
|
|
524
502
|
return paths;
|
|
525
503
|
}
|
|
526
504
|
function findBootstrapSeedEntry(config, path) {
|
|
527
|
-
for (const lower of config?.lowers ?? []) {
|
|
505
|
+
for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
|
|
528
506
|
if (lower.kind !== "snapshot-export") {
|
|
529
507
|
continue;
|
|
530
508
|
}
|
|
@@ -533,16 +511,18 @@ function findBootstrapSeedEntry(config, path) {
|
|
|
533
511
|
return entry;
|
|
534
512
|
}
|
|
535
513
|
}
|
|
536
|
-
|
|
514
|
+
// No base-filesystem JSON read: standard non-default dir metadata comes from
|
|
515
|
+
// the constant table. When a base layer IS present these dirs are never
|
|
516
|
+
// emitted (see createKernelBootstrapLower), so this only seeds the no-base case.
|
|
517
|
+
const meta = KERNEL_POSIX_BOOTSTRAP_DIR_METADATA[path];
|
|
518
|
+
return meta ? { path, type: "directory", ...meta } : undefined;
|
|
537
519
|
}
|
|
538
|
-
function createKernelBootstrapLower(config,
|
|
520
|
+
function createKernelBootstrapLower(config, extraEntries = []) {
|
|
521
|
+
if (config?.type === "native") {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
539
524
|
const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
|
|
540
525
|
const existingPaths = collectConfiguredLowerPaths(config);
|
|
541
|
-
if (includesBundledBaseLayer) {
|
|
542
|
-
for (const entry of getBaseFilesystemEntries()) {
|
|
543
|
-
existingPaths.add(entry.path);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
526
|
const entries = [
|
|
547
527
|
{
|
|
548
528
|
path: "/",
|
|
@@ -552,18 +532,24 @@ function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
|
|
|
552
532
|
gid: 0,
|
|
553
533
|
},
|
|
554
534
|
];
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
535
|
+
// Only run the FS bootstrap (creating the POSIX dir tree) when there is NO
|
|
536
|
+
// base layer. When the bundled base IS present, the sidecar's embedded base
|
|
537
|
+
// layer already provides every POSIX dir with the correct mode/owner, so we
|
|
538
|
+
// emit nothing here and never read its filesystem table.
|
|
539
|
+
if (!includesBundledBaseLayer) {
|
|
540
|
+
for (const dir of KERNEL_POSIX_BOOTSTRAP_DIRS) {
|
|
541
|
+
if (existingPaths.has(dir)) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const seed = findBootstrapSeedEntry(config, dir);
|
|
545
|
+
entries.push({
|
|
546
|
+
path: dir,
|
|
547
|
+
type: "directory",
|
|
548
|
+
mode: seed?.type === "directory" ? seed.mode : "755",
|
|
549
|
+
uid: seed?.uid ?? 0,
|
|
550
|
+
gid: seed?.gid ?? 0,
|
|
551
|
+
});
|
|
558
552
|
}
|
|
559
|
-
const seed = findBootstrapSeedEntry(config, dir);
|
|
560
|
-
entries.push({
|
|
561
|
-
path: dir,
|
|
562
|
-
type: "directory",
|
|
563
|
-
mode: seed?.type === "directory" ? seed.mode : "755",
|
|
564
|
-
uid: seed?.uid ?? 0,
|
|
565
|
-
gid: seed?.gid ?? 0,
|
|
566
|
-
});
|
|
567
553
|
}
|
|
568
554
|
if (!includesBundledBaseLayer && !existingPaths.has("/usr/bin/env")) {
|
|
569
555
|
entries.push({
|
|
@@ -576,22 +562,6 @@ function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
|
|
|
576
562
|
encoding: "base64",
|
|
577
563
|
});
|
|
578
564
|
}
|
|
579
|
-
const uniqueCommands = [...new Set(commandNames)].sort((a, b) => a.localeCompare(b));
|
|
580
|
-
for (const command of uniqueCommands) {
|
|
581
|
-
const stubPath = `/bin/${command}`;
|
|
582
|
-
if (existingPaths.has(stubPath)) {
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
entries.push({
|
|
586
|
-
path: stubPath,
|
|
587
|
-
type: "file",
|
|
588
|
-
mode: "755",
|
|
589
|
-
uid: 0,
|
|
590
|
-
gid: 0,
|
|
591
|
-
content: KERNEL_COMMAND_STUB,
|
|
592
|
-
encoding: "utf8",
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
565
|
for (const entry of sortFilesystemEntries(extraEntries)) {
|
|
596
566
|
if (existingPaths.has(entry.path)) {
|
|
597
567
|
continue;
|
|
@@ -619,7 +589,7 @@ function buildLiveBootstrapDirectoryEntries(existingPaths, config) {
|
|
|
619
589
|
return entries;
|
|
620
590
|
}
|
|
621
591
|
async function bootstrapLiveBootstrapDirectories(client, session, vm, config) {
|
|
622
|
-
const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm)).map((entry) => entry.path));
|
|
592
|
+
const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm, Number.MAX_SAFE_INTEGER)).map((entry) => entry.path));
|
|
623
593
|
const entries = buildLiveBootstrapDirectoryEntries(existingPaths, config);
|
|
624
594
|
if (entries.length === 0) {
|
|
625
595
|
return;
|
|
@@ -706,28 +676,6 @@ function latestMtimeMs(path) {
|
|
|
706
676
|
}
|
|
707
677
|
return latest;
|
|
708
678
|
}
|
|
709
|
-
function collectGuestCommandPaths(commandDirs) {
|
|
710
|
-
const guestPaths = new Map();
|
|
711
|
-
for (const [index, commandDir] of commandDirs.entries()) {
|
|
712
|
-
let entries;
|
|
713
|
-
try {
|
|
714
|
-
entries = readdirSync(commandDir).sort((left, right) => left.localeCompare(right));
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
for (const entry of entries) {
|
|
720
|
-
if (entry.startsWith(".")) {
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
if (!isWasmBinaryFile(join(commandDir, entry)) || guestPaths.has(entry)) {
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
guestPaths.set(entry, `/__secure_exec/commands/${index}/${entry}`);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return guestPaths;
|
|
730
|
-
}
|
|
731
679
|
async function resolveCompatLocalMounts(mounts) {
|
|
732
680
|
if (!mounts) {
|
|
733
681
|
return [];
|
|
@@ -796,57 +744,32 @@ function collectSidecarMountPlan(options) {
|
|
|
796
744
|
}
|
|
797
745
|
for (const mount of options.mounts ?? []) {
|
|
798
746
|
if (!isNativeMountConfig(mount)) {
|
|
747
|
+
sidecarMounts.push({
|
|
748
|
+
guestPath: mount.path,
|
|
749
|
+
readOnly: isOverlayMountConfig(mount)
|
|
750
|
+
? (mount.filesystem.mode ?? "ephemeral") === "read-only"
|
|
751
|
+
: (mount.readOnly ?? false),
|
|
752
|
+
plugin: {
|
|
753
|
+
id: "js_bridge",
|
|
754
|
+
config: {},
|
|
755
|
+
},
|
|
756
|
+
});
|
|
799
757
|
continue;
|
|
800
758
|
}
|
|
801
759
|
pushMount(mount);
|
|
802
760
|
}
|
|
803
|
-
for (const root of options.softwareRoots) {
|
|
804
|
-
pushMount({
|
|
805
|
-
path: root.vmPath,
|
|
806
|
-
plugin: createHostDirBackend({
|
|
807
|
-
hostPath: root.hostPath,
|
|
808
|
-
readOnly: true,
|
|
809
|
-
}),
|
|
810
|
-
readOnly: true,
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
for (const [index, commandDir] of options.commandDirs.entries()) {
|
|
814
|
-
pushMount({
|
|
815
|
-
path: `/__secure_exec/commands/${index}`,
|
|
816
|
-
plugin: createHostDirBackend({
|
|
817
|
-
hostPath: commandDir,
|
|
818
|
-
readOnly: true,
|
|
819
|
-
}),
|
|
820
|
-
readOnly: true,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
if (options.shimDir) {
|
|
824
|
-
pushMount({
|
|
825
|
-
path: "/usr/local/bin",
|
|
826
|
-
plugin: createHostDirBackend({
|
|
827
|
-
hostPath: options.shimDir,
|
|
828
|
-
readOnly: true,
|
|
829
|
-
}),
|
|
830
|
-
readOnly: true,
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
761
|
hostMounts.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
834
762
|
hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
835
763
|
return { sidecarMounts, hostMounts, hostPathMappings };
|
|
836
764
|
}
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
writeFileSync(join(shimDir, "agentos"), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
840
|
-
for (const toolKit of toolKits) {
|
|
841
|
-
writeFileSync(join(shimDir, `agentos-${toolKit.name}`), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
842
|
-
}
|
|
843
|
-
return shimDir;
|
|
844
|
-
}
|
|
845
|
-
function collectToolkitBootstrapCommands(toolKits) {
|
|
846
|
-
if (toolKits.length === 0) {
|
|
765
|
+
function collectBindingBootstrapCommands(bindings) {
|
|
766
|
+
if (bindings.length === 0) {
|
|
847
767
|
return [];
|
|
848
768
|
}
|
|
849
|
-
return [
|
|
769
|
+
return [
|
|
770
|
+
"agentos",
|
|
771
|
+
...bindings.map((bindingCollection) => `agentos-${bindingCollection.name}`),
|
|
772
|
+
];
|
|
850
773
|
}
|
|
851
774
|
function validationMessage(error) {
|
|
852
775
|
if (typeof error === "object" &&
|
|
@@ -864,14 +787,16 @@ function validationMessage(error) {
|
|
|
864
787
|
}
|
|
865
788
|
return error instanceof Error ? error.message : String(error);
|
|
866
789
|
}
|
|
867
|
-
function
|
|
790
|
+
function bindingToSidecarDefinition(definition) {
|
|
868
791
|
return {
|
|
869
|
-
description:
|
|
870
|
-
inputSchema: zodToJsonSchema(
|
|
871
|
-
...(
|
|
872
|
-
|
|
792
|
+
description: definition.description,
|
|
793
|
+
inputSchema: zodToJsonSchema(definition.inputSchema),
|
|
794
|
+
...(definition.timeout !== undefined
|
|
795
|
+
? { timeoutMs: definition.timeout }
|
|
796
|
+
: {}),
|
|
797
|
+
...(definition.examples && definition.examples.length > 0
|
|
873
798
|
? {
|
|
874
|
-
examples:
|
|
799
|
+
examples: definition.examples.map((example) => ({
|
|
875
800
|
description: example.description,
|
|
876
801
|
input: example.input,
|
|
877
802
|
})),
|
|
@@ -879,8 +804,8 @@ function toolToSidecarDefinition(tool) {
|
|
|
879
804
|
: {}),
|
|
880
805
|
};
|
|
881
806
|
}
|
|
882
|
-
function combineInstructions(additionalInstructions,
|
|
883
|
-
const parts = [additionalInstructions,
|
|
807
|
+
function combineInstructions(additionalInstructions, bindingReference) {
|
|
808
|
+
const parts = [additionalInstructions, bindingReference]
|
|
884
809
|
.map((part) => part?.trim())
|
|
885
810
|
.filter((part) => Boolean(part));
|
|
886
811
|
if (parts.length === 0) {
|
|
@@ -888,48 +813,48 @@ function combineInstructions(additionalInstructions, toolReference) {
|
|
|
888
813
|
}
|
|
889
814
|
return parts.join("\n\n");
|
|
890
815
|
}
|
|
891
|
-
function
|
|
892
|
-
if (
|
|
816
|
+
function buildBindingReference(bindings) {
|
|
817
|
+
if (bindings.length === 0) {
|
|
893
818
|
return "";
|
|
894
819
|
}
|
|
895
820
|
const lines = [
|
|
896
|
-
"## Available Host
|
|
821
|
+
"## Available Host Bindings",
|
|
897
822
|
"",
|
|
898
|
-
"Run `agentos list-
|
|
823
|
+
"Run `agentos list-bindings` to see all available bindings.",
|
|
899
824
|
"",
|
|
900
825
|
];
|
|
901
|
-
for (const
|
|
902
|
-
lines.push(`### ${
|
|
826
|
+
for (const bindingCollection of bindings) {
|
|
827
|
+
lines.push(`### ${bindingCollection.name}`);
|
|
903
828
|
lines.push("");
|
|
904
|
-
lines.push(
|
|
829
|
+
lines.push(bindingCollection.description);
|
|
905
830
|
lines.push("");
|
|
906
|
-
for (const [
|
|
907
|
-
const
|
|
908
|
-
const signature =
|
|
831
|
+
for (const [bindingName, definition] of Object.entries(bindingCollection.bindings)) {
|
|
832
|
+
const sidecarBinding = bindingToSidecarDefinition(definition);
|
|
833
|
+
const signature = buildBindingFlagSignature(sidecarBinding.inputSchema);
|
|
909
834
|
const suffix = signature.length > 0 ? ` ${signature}` : "";
|
|
910
|
-
lines.push(`- \`agentos-${
|
|
835
|
+
lines.push(`- \`agentos-${bindingCollection.name} ${bindingName}${suffix}\` — ${definition.description}`);
|
|
911
836
|
}
|
|
912
837
|
lines.push("");
|
|
913
|
-
const
|
|
914
|
-
if (
|
|
838
|
+
const bindingsWithExamples = Object.entries(bindingCollection.bindings).filter(([, definition]) => definition.examples && definition.examples.length > 0);
|
|
839
|
+
if (bindingsWithExamples.length > 0) {
|
|
915
840
|
lines.push("**Examples:**");
|
|
916
841
|
lines.push("");
|
|
917
|
-
for (const [
|
|
918
|
-
for (const example of
|
|
919
|
-
const args =
|
|
842
|
+
for (const [bindingName, definition] of bindingsWithExamples) {
|
|
843
|
+
for (const example of definition.examples ?? []) {
|
|
844
|
+
const args = inputToBindingFlags(example.input);
|
|
920
845
|
const suffix = args.length > 0 ? ` ${args}` : "";
|
|
921
|
-
lines.push(`- ${example.description}: \`agentos-${
|
|
846
|
+
lines.push(`- ${example.description}: \`agentos-${bindingCollection.name} ${bindingName}${suffix}\``);
|
|
922
847
|
}
|
|
923
848
|
}
|
|
924
849
|
lines.push("");
|
|
925
850
|
}
|
|
926
|
-
lines.push(`Run \`agentos-${
|
|
851
|
+
lines.push(`Run \`agentos-${bindingCollection.name} <binding> --help\` for details.`);
|
|
927
852
|
lines.push("");
|
|
928
853
|
}
|
|
929
854
|
return lines.join("\n");
|
|
930
855
|
}
|
|
931
|
-
function
|
|
932
|
-
return
|
|
856
|
+
function buildBindingFlagSignature(schema) {
|
|
857
|
+
return describeBindingFlags(schema)
|
|
933
858
|
.map((flag) => {
|
|
934
859
|
if (flag.required) {
|
|
935
860
|
return `${flag.name} <${flag.type}>`;
|
|
@@ -938,7 +863,7 @@ function buildToolFlagSignature(schema) {
|
|
|
938
863
|
})
|
|
939
864
|
.join(" ");
|
|
940
865
|
}
|
|
941
|
-
function
|
|
866
|
+
function describeBindingFlags(schema) {
|
|
942
867
|
const schemaObject = asRecord(schema);
|
|
943
868
|
const properties = asRecord(schemaObject.properties);
|
|
944
869
|
const required = Array.isArray(schemaObject.required)
|
|
@@ -946,11 +871,11 @@ function describeToolFlags(schema) {
|
|
|
946
871
|
: new Set();
|
|
947
872
|
return Object.entries(properties).map(([fieldName, fieldSchema]) => ({
|
|
948
873
|
name: `--${camelToKebab(fieldName)}`,
|
|
949
|
-
type:
|
|
874
|
+
type: describeBindingFlagType(fieldSchema),
|
|
950
875
|
required: required.has(fieldName),
|
|
951
876
|
}));
|
|
952
877
|
}
|
|
953
|
-
function
|
|
878
|
+
function describeBindingFlagType(schema) {
|
|
954
879
|
const schemaObject = asRecord(schema);
|
|
955
880
|
const type = typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
956
881
|
if (type === "array") {
|
|
@@ -969,7 +894,7 @@ function describeJsonSchemaScalarType(schema) {
|
|
|
969
894
|
const schemaObject = asRecord(schema);
|
|
970
895
|
return typeof schemaObject.type === "string" ? schemaObject.type : "string";
|
|
971
896
|
}
|
|
972
|
-
function
|
|
897
|
+
function inputToBindingFlags(input) {
|
|
973
898
|
const inputObject = asRecord(input);
|
|
974
899
|
return Object.entries(inputObject)
|
|
975
900
|
.flatMap(([key, value]) => {
|
|
@@ -981,13 +906,13 @@ function inputToToolFlags(input) {
|
|
|
981
906
|
return [`--no-${camelToKebab(key)}`];
|
|
982
907
|
}
|
|
983
908
|
if (Array.isArray(value)) {
|
|
984
|
-
return value.map((item) => `${flag} ${
|
|
909
|
+
return value.map((item) => `${flag} ${bindingCliString(item)}`);
|
|
985
910
|
}
|
|
986
|
-
return [`${flag} ${
|
|
911
|
+
return [`${flag} ${bindingCliString(value)}`];
|
|
987
912
|
})
|
|
988
913
|
.join(" ");
|
|
989
914
|
}
|
|
990
|
-
function
|
|
915
|
+
function bindingCliString(value) {
|
|
991
916
|
return typeof value === "string" ? value : (JSON.stringify(value) ?? "null");
|
|
992
917
|
}
|
|
993
918
|
function camelToKebab(value) {
|
|
@@ -1025,23 +950,23 @@ async function handleHostCallback(request, context) {
|
|
|
1025
950
|
};
|
|
1026
951
|
}
|
|
1027
952
|
}
|
|
1028
|
-
const
|
|
1029
|
-
if (!
|
|
953
|
+
const definition = context.bindingMap.get(payload.callback_key);
|
|
954
|
+
if (!definition) {
|
|
1030
955
|
return {
|
|
1031
956
|
type: "host_callback_result",
|
|
1032
957
|
invocation_id: payload.invocation_id,
|
|
1033
|
-
error: `Unknown
|
|
958
|
+
error: `Unknown binding "${payload.callback_key}"`,
|
|
1034
959
|
};
|
|
1035
960
|
}
|
|
1036
|
-
const permissionMode =
|
|
961
|
+
const permissionMode = bindingPermissionMode(context.permissions, payload.callback_key);
|
|
1037
962
|
if (permissionMode !== "allow") {
|
|
1038
963
|
return {
|
|
1039
964
|
type: "host_callback_result",
|
|
1040
965
|
invocation_id: payload.invocation_id,
|
|
1041
|
-
error: `EACCES: blocked by
|
|
966
|
+
error: `EACCES: blocked by binding.invoke policy for ${payload.callback_key}`,
|
|
1042
967
|
};
|
|
1043
968
|
}
|
|
1044
|
-
const parsed =
|
|
969
|
+
const parsed = definition.inputSchema.safeParse(payload.input);
|
|
1045
970
|
if (!parsed.success) {
|
|
1046
971
|
return {
|
|
1047
972
|
type: "host_callback_result",
|
|
@@ -1053,7 +978,7 @@ async function handleHostCallback(request, context) {
|
|
|
1053
978
|
return {
|
|
1054
979
|
type: "host_callback_result",
|
|
1055
980
|
invocation_id: payload.invocation_id,
|
|
1056
|
-
result: await
|
|
981
|
+
result: await executeBinding(definition, payload.callback_key, parsed.data),
|
|
1057
982
|
};
|
|
1058
983
|
}
|
|
1059
984
|
catch (error) {
|
|
@@ -1064,14 +989,142 @@ async function handleHostCallback(request, context) {
|
|
|
1064
989
|
};
|
|
1065
990
|
}
|
|
1066
991
|
}
|
|
1067
|
-
function
|
|
1068
|
-
const
|
|
1069
|
-
for (const
|
|
1070
|
-
for (const [
|
|
1071
|
-
|
|
992
|
+
function buildBindingMap(bindings) {
|
|
993
|
+
const bindingMap = new Map();
|
|
994
|
+
for (const bindingCollection of bindings) {
|
|
995
|
+
for (const [bindingName, definition] of Object.entries(bindingCollection.bindings)) {
|
|
996
|
+
bindingMap.set(`${bindingCollection.name}:${bindingName}`, definition);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return bindingMap;
|
|
1000
|
+
}
|
|
1001
|
+
function bridgeErrorMessage(error) {
|
|
1002
|
+
return error instanceof Error ? error.message : String(error);
|
|
1003
|
+
}
|
|
1004
|
+
function toBridgeArgs(value) {
|
|
1005
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1006
|
+
throw new Error("js_bridge args must be an object");
|
|
1007
|
+
}
|
|
1008
|
+
return value;
|
|
1009
|
+
}
|
|
1010
|
+
function bridgePath(mountId, value) {
|
|
1011
|
+
if (!mountId.startsWith("/")) {
|
|
1012
|
+
throw new Error(`Unsupported js_bridge mount id: ${mountId}`);
|
|
1013
|
+
}
|
|
1014
|
+
if (typeof value !== "string") {
|
|
1015
|
+
throw new Error("js_bridge path argument must be a string");
|
|
1016
|
+
}
|
|
1017
|
+
return posixPath.normalize(posixPath.join(mountId, value));
|
|
1018
|
+
}
|
|
1019
|
+
function requireBridgeNumber(value, field) {
|
|
1020
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1021
|
+
throw new Error(`js_bridge args.${field} must be a number`);
|
|
1022
|
+
}
|
|
1023
|
+
return value;
|
|
1024
|
+
}
|
|
1025
|
+
function decodeBridgeBytes(value, field) {
|
|
1026
|
+
if (typeof value === "string") {
|
|
1027
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
1028
|
+
}
|
|
1029
|
+
if (Array.isArray(value) &&
|
|
1030
|
+
value.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)) {
|
|
1031
|
+
return new Uint8Array(value);
|
|
1032
|
+
}
|
|
1033
|
+
throw new Error(`js_bridge args.${field} must be base64 bytes`);
|
|
1034
|
+
}
|
|
1035
|
+
async function handleJsBridgeCall(request, context) {
|
|
1036
|
+
try {
|
|
1037
|
+
const args = toBridgeArgs(request.args);
|
|
1038
|
+
const fs = context.filesystem;
|
|
1039
|
+
const path = () => bridgePath(request.mount_id, args.path);
|
|
1040
|
+
let result;
|
|
1041
|
+
switch (request.operation) {
|
|
1042
|
+
case "readFile":
|
|
1043
|
+
result = Buffer.from(await fs.readFile(path())).toString("base64");
|
|
1044
|
+
break;
|
|
1045
|
+
case "readDir":
|
|
1046
|
+
result = await fs.readDir(path());
|
|
1047
|
+
break;
|
|
1048
|
+
case "readDirWithTypes":
|
|
1049
|
+
result = await fs.readDirWithTypes(path());
|
|
1050
|
+
break;
|
|
1051
|
+
case "writeFile":
|
|
1052
|
+
await fs.writeFile(path(), decodeBridgeBytes(args.content, "content"));
|
|
1053
|
+
break;
|
|
1054
|
+
case "createDir":
|
|
1055
|
+
await fs.createDir(path());
|
|
1056
|
+
break;
|
|
1057
|
+
case "mkdir":
|
|
1058
|
+
await fs.mkdir(path(), { recursive: args.recursive !== false });
|
|
1059
|
+
break;
|
|
1060
|
+
case "exists":
|
|
1061
|
+
result = await fs.exists(path());
|
|
1062
|
+
break;
|
|
1063
|
+
case "stat":
|
|
1064
|
+
result = await fs.stat(path());
|
|
1065
|
+
break;
|
|
1066
|
+
case "removeFile":
|
|
1067
|
+
await fs.removeFile(path());
|
|
1068
|
+
break;
|
|
1069
|
+
case "removeDir":
|
|
1070
|
+
await fs.removeDir(path());
|
|
1071
|
+
break;
|
|
1072
|
+
case "rename":
|
|
1073
|
+
await fs.rename(bridgePath(request.mount_id, args.oldPath), bridgePath(request.mount_id, args.newPath));
|
|
1074
|
+
break;
|
|
1075
|
+
case "realpath":
|
|
1076
|
+
result = await fs.realpath(path());
|
|
1077
|
+
break;
|
|
1078
|
+
case "symlink": {
|
|
1079
|
+
if (typeof args.target !== "string") {
|
|
1080
|
+
throw new Error("js_bridge args.target must be a string");
|
|
1081
|
+
}
|
|
1082
|
+
await fs.symlink(args.target, bridgePath(request.mount_id, args.linkPath));
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
case "readlink":
|
|
1086
|
+
result = await fs.readlink(path());
|
|
1087
|
+
break;
|
|
1088
|
+
case "lstat":
|
|
1089
|
+
result = await fs.lstat(path());
|
|
1090
|
+
break;
|
|
1091
|
+
case "link":
|
|
1092
|
+
await fs.link(bridgePath(request.mount_id, args.oldPath), bridgePath(request.mount_id, args.newPath));
|
|
1093
|
+
break;
|
|
1094
|
+
case "chmod":
|
|
1095
|
+
await fs.chmod(path(), requireBridgeNumber(args.mode, "mode"));
|
|
1096
|
+
break;
|
|
1097
|
+
case "chown":
|
|
1098
|
+
await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"), { followSymlinks: args.followSymlinks !== false });
|
|
1099
|
+
break;
|
|
1100
|
+
case "utimes":
|
|
1101
|
+
await fs.utimes(path(), requireBridgeNumber(args.atimeMs, "atimeMs"), requireBridgeNumber(args.mtimeMs, "mtimeMs"));
|
|
1102
|
+
break;
|
|
1103
|
+
case "truncate":
|
|
1104
|
+
await fs.truncate(path(), requireBridgeNumber(args.length, "length"));
|
|
1105
|
+
break;
|
|
1106
|
+
case "pread":
|
|
1107
|
+
result = Buffer.from(await fs.pread(path(), requireBridgeNumber(args.offset, "offset"), requireBridgeNumber(args.length, "length"))).toString("base64");
|
|
1108
|
+
break;
|
|
1109
|
+
case "pwrite":
|
|
1110
|
+
await fs.pwrite(path(), requireBridgeNumber(args.offset, "offset"), decodeBridgeBytes(args.content, "content"));
|
|
1111
|
+
break;
|
|
1112
|
+
default:
|
|
1113
|
+
throw new Error(`Unsupported js_bridge operation: ${request.operation}`);
|
|
1072
1114
|
}
|
|
1115
|
+
return {
|
|
1116
|
+
type: "js_bridge_result",
|
|
1117
|
+
call_id: request.call_id,
|
|
1118
|
+
...(result === undefined ? {} : { result }),
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
return {
|
|
1123
|
+
type: "js_bridge_result",
|
|
1124
|
+
call_id: request.call_id,
|
|
1125
|
+
error: bridgeErrorMessage(error),
|
|
1126
|
+
};
|
|
1073
1127
|
}
|
|
1074
|
-
return toolMap;
|
|
1075
1128
|
}
|
|
1076
1129
|
function parseHostCommandCallbackInput(input) {
|
|
1077
1130
|
const value = asRecord(input);
|
|
@@ -1090,90 +1143,90 @@ function parseHostCommandCallbackInput(input) {
|
|
|
1090
1143
|
};
|
|
1091
1144
|
}
|
|
1092
1145
|
async function handleHostCommandCallback(command, context) {
|
|
1093
|
-
const
|
|
1146
|
+
const directBindings = context.bindings.find((bindingCollection) => `agentos-${bindingCollection.name}` === command.command);
|
|
1094
1147
|
if (command.command === "agentos") {
|
|
1095
1148
|
return handleAgentOsRegistryCommand(command, context);
|
|
1096
1149
|
}
|
|
1097
|
-
if (
|
|
1098
|
-
return
|
|
1150
|
+
if (directBindings) {
|
|
1151
|
+
return handleAgentOsBindingCommand(command, context, directBindings);
|
|
1099
1152
|
}
|
|
1100
1153
|
throw new Error(`Unknown host callback command "${command.command}"`);
|
|
1101
1154
|
}
|
|
1102
1155
|
async function handleAgentOsRegistryCommand(command, context) {
|
|
1103
|
-
const [subcommand,
|
|
1156
|
+
const [subcommand, collectionName, bindingName, ...bindingArgs] = command.args;
|
|
1104
1157
|
if (!subcommand || isHelpFlag(subcommand)) {
|
|
1105
1158
|
return {
|
|
1106
|
-
usage: "agentos <command>: list-
|
|
1159
|
+
usage: "agentos <command>: list-bindings [collection], <collection> --help, or <collection> <binding> ...",
|
|
1107
1160
|
};
|
|
1108
1161
|
}
|
|
1109
|
-
if (subcommand === "list-
|
|
1110
|
-
return
|
|
1111
|
-
?
|
|
1112
|
-
:
|
|
1162
|
+
if (subcommand === "list-bindings") {
|
|
1163
|
+
return collectionName
|
|
1164
|
+
? describeBindingsPayload(context.bindings, collectionName)
|
|
1165
|
+
: listBindingsPayload(context.bindings);
|
|
1113
1166
|
}
|
|
1114
|
-
const
|
|
1115
|
-
if (!
|
|
1116
|
-
throw new Error(`No
|
|
1167
|
+
const bindingCollection = context.bindings.find((collection) => collection.name === subcommand);
|
|
1168
|
+
if (!bindingCollection) {
|
|
1169
|
+
throw new Error(`No binding collection "${subcommand}". Available: ${bindingsNames(context.bindings)}`);
|
|
1117
1170
|
}
|
|
1118
|
-
if (!
|
|
1119
|
-
return
|
|
1171
|
+
if (!collectionName || isHelpFlag(collectionName)) {
|
|
1172
|
+
return describeBindingsPayload(context.bindings, subcommand);
|
|
1120
1173
|
}
|
|
1121
|
-
if (
|
|
1122
|
-
return
|
|
1174
|
+
if (bindingName && isHelpFlag(bindingName)) {
|
|
1175
|
+
return describeBindingPayload(bindingCollection, collectionName);
|
|
1123
1176
|
}
|
|
1124
|
-
return
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
args: [
|
|
1177
|
+
return invokeBinding({
|
|
1178
|
+
bindingCollection,
|
|
1179
|
+
bindingName: collectionName,
|
|
1180
|
+
args: [bindingName, ...bindingArgs].filter((value) => typeof value === "string"),
|
|
1128
1181
|
cwd: command.cwd,
|
|
1129
1182
|
context,
|
|
1130
1183
|
});
|
|
1131
1184
|
}
|
|
1132
|
-
async function
|
|
1133
|
-
const [
|
|
1134
|
-
if (!
|
|
1135
|
-
return
|
|
1185
|
+
async function handleAgentOsBindingCommand(command, context, bindingCollection) {
|
|
1186
|
+
const [bindingName, helpOrFirstArg, ...rest] = command.args;
|
|
1187
|
+
if (!bindingName || isHelpFlag(bindingName)) {
|
|
1188
|
+
return describeBindingsPayload(context.bindings, bindingCollection.name);
|
|
1136
1189
|
}
|
|
1137
1190
|
if (helpOrFirstArg && isHelpFlag(helpOrFirstArg)) {
|
|
1138
|
-
return
|
|
1191
|
+
return describeBindingPayload(bindingCollection, bindingName);
|
|
1139
1192
|
}
|
|
1140
|
-
return
|
|
1141
|
-
|
|
1142
|
-
|
|
1193
|
+
return invokeBinding({
|
|
1194
|
+
bindingCollection,
|
|
1195
|
+
bindingName,
|
|
1143
1196
|
args: [helpOrFirstArg, ...rest].filter((value) => typeof value === "string"),
|
|
1144
1197
|
cwd: command.cwd,
|
|
1145
1198
|
context,
|
|
1146
1199
|
});
|
|
1147
1200
|
}
|
|
1148
|
-
async function
|
|
1149
|
-
const
|
|
1150
|
-
if (!
|
|
1151
|
-
throw new Error(`No
|
|
1201
|
+
async function invokeBinding({ bindingCollection, bindingName, args, cwd, context, }) {
|
|
1202
|
+
const definition = bindingCollection.bindings[bindingName];
|
|
1203
|
+
if (!definition) {
|
|
1204
|
+
throw new Error(`No binding "${bindingName}" in collection "${bindingCollection.name}". Available: ${bindingNames(bindingCollection)}`);
|
|
1152
1205
|
}
|
|
1153
|
-
const callbackKey = `${
|
|
1154
|
-
const permissionMode =
|
|
1206
|
+
const callbackKey = `${bindingCollection.name}:${bindingName}`;
|
|
1207
|
+
const permissionMode = bindingPermissionMode(context.permissions, callbackKey);
|
|
1155
1208
|
if (permissionMode !== "allow") {
|
|
1156
|
-
throw new Error(`EACCES: blocked by
|
|
1209
|
+
throw new Error(`EACCES: blocked by binding.invoke policy for ${callbackKey}`);
|
|
1157
1210
|
}
|
|
1158
|
-
const input = await
|
|
1159
|
-
return
|
|
1211
|
+
const input = await parseBindingInput(definition, args, cwd, context.readFile);
|
|
1212
|
+
return executeBinding(definition, callbackKey, input);
|
|
1160
1213
|
}
|
|
1161
|
-
async function
|
|
1162
|
-
const parsed =
|
|
1214
|
+
async function executeBinding(definition, callbackKey, input) {
|
|
1215
|
+
const parsed = definition.inputSchema.safeParse(input);
|
|
1163
1216
|
if (!parsed.success) {
|
|
1164
1217
|
throw new Error(validationMessage(parsed.error));
|
|
1165
1218
|
}
|
|
1166
1219
|
return Promise.race([
|
|
1167
|
-
Promise.resolve(
|
|
1220
|
+
Promise.resolve(definition.execute(parsed.data)),
|
|
1168
1221
|
new Promise((_, reject) => {
|
|
1169
|
-
if (
|
|
1222
|
+
if (definition.timeout === undefined) {
|
|
1170
1223
|
return;
|
|
1171
1224
|
}
|
|
1172
|
-
setTimeout(() => reject(new Error(`
|
|
1225
|
+
setTimeout(() => reject(new Error(`Binding "${callbackKey}" timed out after ${definition.timeout}ms`)), definition.timeout);
|
|
1173
1226
|
}),
|
|
1174
1227
|
]);
|
|
1175
1228
|
}
|
|
1176
|
-
async function
|
|
1229
|
+
async function parseBindingInput(definition, args, cwd, readFile) {
|
|
1177
1230
|
if (args[0] === "--json") {
|
|
1178
1231
|
const value = args[1];
|
|
1179
1232
|
if (value === undefined) {
|
|
@@ -1192,9 +1245,9 @@ async function parseHostToolInput(tool, args, cwd, readFile) {
|
|
|
1192
1245
|
const text = new TextDecoder().decode(await readFile(guestPath));
|
|
1193
1246
|
return JSON.parse(text);
|
|
1194
1247
|
}
|
|
1195
|
-
return
|
|
1248
|
+
return parseBindingArgv(bindingToSidecarDefinition(definition).inputSchema, args);
|
|
1196
1249
|
}
|
|
1197
|
-
function
|
|
1250
|
+
function parseBindingArgv(schema, argv) {
|
|
1198
1251
|
const schemaObject = asRecord(schema);
|
|
1199
1252
|
const properties = asRecord(schemaObject.properties);
|
|
1200
1253
|
const required = Array.isArray(schemaObject.required)
|
|
@@ -1266,50 +1319,50 @@ function parseToolArgv(schema, argv) {
|
|
|
1266
1319
|
}
|
|
1267
1320
|
return input;
|
|
1268
1321
|
}
|
|
1269
|
-
function
|
|
1322
|
+
function listBindingsPayload(bindings) {
|
|
1270
1323
|
return {
|
|
1271
|
-
|
|
1272
|
-
name:
|
|
1273
|
-
description:
|
|
1274
|
-
|
|
1324
|
+
bindings: bindings.map((bindingCollection) => ({
|
|
1325
|
+
name: bindingCollection.name,
|
|
1326
|
+
description: bindingCollection.description,
|
|
1327
|
+
bindings: Object.keys(bindingCollection.bindings),
|
|
1275
1328
|
})),
|
|
1276
1329
|
};
|
|
1277
1330
|
}
|
|
1278
|
-
function
|
|
1279
|
-
const
|
|
1280
|
-
if (!
|
|
1281
|
-
throw new Error(`No
|
|
1331
|
+
function describeBindingsPayload(bindings, collectionName) {
|
|
1332
|
+
const bindingCollection = bindings.find((collection) => collection.name === collectionName);
|
|
1333
|
+
if (!bindingCollection) {
|
|
1334
|
+
throw new Error(`No binding collection "${collectionName}". Available: ${bindingsNames(bindings)}`);
|
|
1282
1335
|
}
|
|
1283
1336
|
return {
|
|
1284
|
-
name:
|
|
1285
|
-
description:
|
|
1286
|
-
|
|
1287
|
-
|
|
1337
|
+
name: bindingCollection.name,
|
|
1338
|
+
description: bindingCollection.description,
|
|
1339
|
+
bindings: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
|
|
1340
|
+
bindingName,
|
|
1288
1341
|
{
|
|
1289
|
-
description:
|
|
1290
|
-
flags:
|
|
1342
|
+
description: definition.description,
|
|
1343
|
+
flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
|
|
1291
1344
|
},
|
|
1292
1345
|
])),
|
|
1293
1346
|
};
|
|
1294
1347
|
}
|
|
1295
|
-
function
|
|
1296
|
-
const
|
|
1297
|
-
if (!
|
|
1298
|
-
throw new Error(`No
|
|
1348
|
+
function describeBindingPayload(bindingCollection, bindingName) {
|
|
1349
|
+
const definition = bindingCollection.bindings[bindingName];
|
|
1350
|
+
if (!definition) {
|
|
1351
|
+
throw new Error(`No binding "${bindingName}" in collection "${bindingCollection.name}". Available: ${bindingNames(bindingCollection)}`);
|
|
1299
1352
|
}
|
|
1300
1353
|
return {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
description:
|
|
1304
|
-
flags:
|
|
1305
|
-
examples:
|
|
1354
|
+
collection: bindingCollection.name,
|
|
1355
|
+
binding: bindingName,
|
|
1356
|
+
description: definition.description,
|
|
1357
|
+
flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
|
|
1358
|
+
examples: definition.examples?.map((example) => ({
|
|
1306
1359
|
description: example.description,
|
|
1307
1360
|
input: example.input,
|
|
1308
1361
|
})) ?? [],
|
|
1309
1362
|
};
|
|
1310
1363
|
}
|
|
1311
|
-
function
|
|
1312
|
-
const scope = permissions.
|
|
1364
|
+
function bindingPermissionMode(permissions, callbackKey) {
|
|
1365
|
+
const scope = permissions.binding;
|
|
1313
1366
|
if (!scope) {
|
|
1314
1367
|
return "deny";
|
|
1315
1368
|
}
|
|
@@ -1343,11 +1396,11 @@ function permissionPatternMatches(pattern, value) {
|
|
|
1343
1396
|
.join("");
|
|
1344
1397
|
return new RegExp(`^${source}$`).test(value);
|
|
1345
1398
|
}
|
|
1346
|
-
function
|
|
1347
|
-
return
|
|
1399
|
+
function bindingsNames(bindings) {
|
|
1400
|
+
return bindings.map((bindingCollection) => bindingCollection.name).join(", ");
|
|
1348
1401
|
}
|
|
1349
|
-
function
|
|
1350
|
-
return Object.keys(
|
|
1402
|
+
function bindingNames(bindingCollection) {
|
|
1403
|
+
return Object.keys(bindingCollection.bindings).join(", ");
|
|
1351
1404
|
}
|
|
1352
1405
|
function isHelpFlag(value) {
|
|
1353
1406
|
return value === "--help" || value === "-h";
|
|
@@ -1356,43 +1409,42 @@ function jsonSchemaType(schema) {
|
|
|
1356
1409
|
const schemaObject = asRecord(schema);
|
|
1357
1410
|
return typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
1358
1411
|
}
|
|
1359
|
-
async function
|
|
1360
|
-
if (
|
|
1412
|
+
async function registerBindingsOnSidecar(client, session, vm, bindings) {
|
|
1413
|
+
if (bindings.length === 0) {
|
|
1361
1414
|
return "";
|
|
1362
1415
|
}
|
|
1363
|
-
for (const
|
|
1416
|
+
for (const bindingCollection of bindings) {
|
|
1364
1417
|
await client.registerHostCallbacks(session, vm, {
|
|
1365
|
-
name:
|
|
1366
|
-
description:
|
|
1367
|
-
commandAliases: [`agentos-${
|
|
1418
|
+
name: bindingCollection.name,
|
|
1419
|
+
description: bindingCollection.description,
|
|
1420
|
+
commandAliases: [`agentos-${bindingCollection.name}`],
|
|
1368
1421
|
registryCommandAliases: ["agentos"],
|
|
1369
|
-
callbacks: Object.fromEntries(Object.entries(
|
|
1370
|
-
|
|
1371
|
-
|
|
1422
|
+
callbacks: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
|
|
1423
|
+
bindingName,
|
|
1424
|
+
bindingToSidecarDefinition(definition),
|
|
1372
1425
|
])),
|
|
1373
1426
|
});
|
|
1374
1427
|
}
|
|
1375
|
-
return
|
|
1428
|
+
return buildBindingReference(bindings);
|
|
1376
1429
|
}
|
|
1377
1430
|
export class AgentOs {
|
|
1378
1431
|
#kernel;
|
|
1379
1432
|
sidecar;
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
_sessionClosePromises = new Map();
|
|
1383
|
-
_pendingSessionRequestResolvers = new Map();
|
|
1433
|
+
_durableSessionEventHandlers = new Map();
|
|
1434
|
+
_agentExitHandlers = new Map();
|
|
1384
1435
|
_processes = new Map();
|
|
1385
1436
|
_shells = new Map();
|
|
1437
|
+
// Value is the recorded exit code (undefined until/unless the exit
|
|
1438
|
+
// resolves) so waitShell can still report it after the entry is dropped.
|
|
1386
1439
|
_closedShellIds = new BoundedSet(CLOSED_SHELL_ID_RETENTION_LIMIT);
|
|
1387
1440
|
_pendingShellExitPromises = new Set();
|
|
1388
1441
|
_shellCounter = 0;
|
|
1389
1442
|
_acpTerminals = new Map();
|
|
1390
1443
|
_acpTerminalCounter = 0;
|
|
1391
1444
|
_softwareRoots;
|
|
1392
|
-
_softwareAgentConfigs;
|
|
1393
1445
|
_cronManager;
|
|
1394
|
-
|
|
1395
|
-
|
|
1446
|
+
_bindings = [];
|
|
1447
|
+
_bindingReference = "";
|
|
1396
1448
|
_permissions = allowAll;
|
|
1397
1449
|
_hostMounts;
|
|
1398
1450
|
_env;
|
|
@@ -1402,17 +1454,22 @@ export class AgentOs {
|
|
|
1402
1454
|
_sidecarSession;
|
|
1403
1455
|
_sidecarVm;
|
|
1404
1456
|
_disposeSidecarEventListener;
|
|
1405
|
-
|
|
1457
|
+
_agentStderrHandler;
|
|
1458
|
+
_agentExitHandler;
|
|
1459
|
+
_limitWarningHandler;
|
|
1460
|
+
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, agentStderrHandler, agentExitHandler, limitWarningHandler) {
|
|
1406
1461
|
this.#kernel = kernel;
|
|
1407
1462
|
this.sidecar = sidecar;
|
|
1408
1463
|
this._softwareRoots = softwareRoots;
|
|
1409
|
-
this._softwareAgentConfigs = softwareAgentConfigs;
|
|
1410
1464
|
this._hostMounts = hostMounts;
|
|
1411
1465
|
this._env = env;
|
|
1412
1466
|
this._rootFilesystem = rootFilesystem;
|
|
1413
1467
|
this._sidecarClient = sidecarClient;
|
|
1414
1468
|
this._sidecarSession = sidecarSession;
|
|
1415
1469
|
this._sidecarVm = sidecarVm;
|
|
1470
|
+
this._agentStderrHandler = agentStderrHandler;
|
|
1471
|
+
this._agentExitHandler = agentExitHandler;
|
|
1472
|
+
this._limitWarningHandler = limitWarningHandler;
|
|
1416
1473
|
this._disposeSidecarEventListener = this._sidecarClient.onEvent((event) => {
|
|
1417
1474
|
this._handleSidecarEvent(event);
|
|
1418
1475
|
});
|
|
@@ -1430,92 +1487,130 @@ export class AgentOs {
|
|
|
1430
1487
|
return getSharedAgentOsSidecarInternal(options);
|
|
1431
1488
|
}
|
|
1432
1489
|
static async create(options) {
|
|
1433
|
-
|
|
1490
|
+
options = parseAgentOsOptions(options);
|
|
1491
|
+
// Default software is FULLY DYNAMIC: this package's own NON-agent
|
|
1492
|
+
// @agentos-software/* dependencies (e.g. common), each default-exporting
|
|
1493
|
+
// its registry-built descriptor. Agent packages are NOT projected here —
|
|
1494
|
+
// openSession({ agent: id }) links the matching agent dependency into the running
|
|
1495
|
+
// VM on first use, so agent closures (and pi's V8 snapshot bundle) only
|
|
1496
|
+
// enter VMs that run them. Unbuilt packages throw with build
|
|
1497
|
+
// instructions; opt out via defaultSoftware: false.
|
|
1498
|
+
const defaultSoftware = options?.defaultSoftware === false ? [] : resolveDefaultSoftware();
|
|
1499
|
+
const software = options?.defaultSoftware === false
|
|
1500
|
+
? (options.software ?? [])
|
|
1501
|
+
: [...defaultSoftware, ...(options?.software ?? [])];
|
|
1502
|
+
// Packages are projected by the SIDECAR: the client forwards only the
|
|
1503
|
+
// package `path` over `configureVm` and the sidecar reads metadata from
|
|
1504
|
+
// the packed vbare manifest (chunk1 of the `.aospkg`).
|
|
1505
|
+
const flatSoftware = software.flat();
|
|
1506
|
+
// Honor the AgentOsOptions.defaultSoftware contract ("entries already present
|
|
1507
|
+
// in `software` are not duplicated"): the default bundle and an explicitly
|
|
1508
|
+
// passed one resolve to the same package paths, so dedup by path. Without
|
|
1509
|
+
// this the sidecar rejects the second projection with a duplicate-command
|
|
1510
|
+
// error (e.g. coreutils' `[`).
|
|
1511
|
+
const seenPackagePaths = new Set();
|
|
1512
|
+
const sidecarPackages = flatSoftware.flatMap((entry) => {
|
|
1513
|
+
const ref = normalizePackageRef(entry);
|
|
1514
|
+
if (!ref || seenPackagePaths.has(ref.path)) {
|
|
1515
|
+
return [];
|
|
1516
|
+
}
|
|
1517
|
+
seenPackagePaths.add(ref.path);
|
|
1518
|
+
return [{ path: ref.path }];
|
|
1519
|
+
});
|
|
1520
|
+
// All package software is projected into `/opt/agentos` by the sidecar. The
|
|
1521
|
+
// client stages nothing host-side and parses NO package manifests: the
|
|
1522
|
+
// sidecar owns agent resolution, agent enumeration, and agent snapshot
|
|
1523
|
+
// bundle loading from the projected package dirs.
|
|
1434
1524
|
const localMounts = await resolveCompatLocalMounts(options?.mounts);
|
|
1435
|
-
const
|
|
1436
|
-
if (
|
|
1437
|
-
|
|
1525
|
+
const bindings = options?.bindings;
|
|
1526
|
+
if (bindings && bindings.length > 0) {
|
|
1527
|
+
validateBindings(bindings);
|
|
1438
1528
|
}
|
|
1529
|
+
// Resolve the sidecar handle up front so every VM created here leases the
|
|
1530
|
+
// one shared native sidecar process owned by that handle.
|
|
1531
|
+
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1439
1532
|
const createVmAdmin = async () => {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1533
|
+
// The `/opt/agentos` projection is built by the sidecar from the
|
|
1534
|
+
// forwarded `packages` (it owns the staging dir + read-only mount, and
|
|
1535
|
+
// runtime `linkSoftware` appends to that live dir). The client no longer
|
|
1536
|
+
// stages packages host-side.
|
|
1537
|
+
const bindingBootstrapCommands = collectBindingBootstrapCommands(bindings ?? []);
|
|
1538
|
+
const bootstrapCommands = [
|
|
1539
|
+
...RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1540
|
+
...bindingBootstrapCommands,
|
|
1541
|
+
];
|
|
1542
|
+
const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem);
|
|
1543
|
+
let bindingReference = "";
|
|
1448
1544
|
let rootBridge = null;
|
|
1449
1545
|
let kernel = null;
|
|
1450
1546
|
let client = null;
|
|
1451
|
-
let
|
|
1547
|
+
let createdNativeVm = null;
|
|
1548
|
+
let nativeSession = null;
|
|
1452
1549
|
let cleanedUp = false;
|
|
1453
1550
|
const cleanup = async () => {
|
|
1454
1551
|
if (cleanedUp) {
|
|
1455
1552
|
return;
|
|
1456
1553
|
}
|
|
1457
1554
|
cleanedUp = true;
|
|
1458
|
-
if (toolShimDir) {
|
|
1459
|
-
rmSync(toolShimDir, { recursive: true, force: true });
|
|
1460
|
-
toolShimDir = null;
|
|
1461
|
-
}
|
|
1462
|
-
preparedCommandDirs.dispose();
|
|
1463
1555
|
};
|
|
1464
1556
|
try {
|
|
1465
1557
|
const env = getBaseEnvironment();
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
const commandGuestPaths =
|
|
1470
|
-
const requestedMounts = options?.moduleAccessCwd
|
|
1471
|
-
? [
|
|
1472
|
-
...(options.mounts ?? []),
|
|
1473
|
-
{
|
|
1474
|
-
path: "/root/node_modules",
|
|
1475
|
-
plugin: createHostDirBackend({
|
|
1476
|
-
hostPath: join(options.moduleAccessCwd, "node_modules"),
|
|
1477
|
-
readOnly: true,
|
|
1478
|
-
}),
|
|
1479
|
-
readOnly: true,
|
|
1480
|
-
},
|
|
1481
|
-
]
|
|
1482
|
-
: options?.mounts;
|
|
1558
|
+
// Guest command paths. The sidecar owns the `/opt/agentos` projection and
|
|
1559
|
+
// reports the exact projected package commands after `configureVm`.
|
|
1560
|
+
// Binding-shim commands are added below.
|
|
1561
|
+
const commandGuestPaths = new Map();
|
|
1483
1562
|
const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
|
|
1484
|
-
mounts:
|
|
1485
|
-
softwareRoots: processed.softwareRoots,
|
|
1486
|
-
commandDirs: preparedCommandDirs.commandDirs,
|
|
1487
|
-
shimDir: toolShimDir,
|
|
1488
|
-
});
|
|
1489
|
-
client = SidecarProcess.spawn({
|
|
1490
|
-
cwd: REPO_ROOT,
|
|
1491
|
-
command: ensureNativeSidecarBinary(),
|
|
1492
|
-
args: [],
|
|
1493
|
-
frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
|
|
1563
|
+
mounts: options?.mounts,
|
|
1494
1564
|
});
|
|
1495
|
-
|
|
1565
|
+
// Reuse the sidecar handle's single shared native process; this VM
|
|
1566
|
+
// becomes another tenant of it rather than spawning its own process.
|
|
1567
|
+
const shared = await ensureSharedSidecarNativeProcess(sidecar);
|
|
1568
|
+
client = shared.client;
|
|
1569
|
+
const session = shared.session;
|
|
1570
|
+
nativeSession = session;
|
|
1496
1571
|
const hostPermissions = options?.permissions ?? {
|
|
1497
1572
|
...allowAll,
|
|
1498
|
-
|
|
1573
|
+
binding: "allow",
|
|
1499
1574
|
};
|
|
1500
1575
|
const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
|
|
1501
1576
|
const createVmConfig = {
|
|
1502
1577
|
env,
|
|
1578
|
+
database: options?.database,
|
|
1579
|
+
...(options?.user ? { user: options.user } : {}),
|
|
1503
1580
|
rootFilesystem: serializeRootFilesystemForSidecar(options?.rootFilesystem, bootstrapLower),
|
|
1504
1581
|
permissions: sidecarPermissions,
|
|
1505
1582
|
limits: options?.limits,
|
|
1506
1583
|
loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
|
|
1584
|
+
bootstrapCommands,
|
|
1585
|
+
...(options?.rootFilesystem?.type === "native"
|
|
1586
|
+
? {
|
|
1587
|
+
nativeRoot: {
|
|
1588
|
+
plugin: {
|
|
1589
|
+
id: options.rootFilesystem.plugin.id,
|
|
1590
|
+
config: options.rootFilesystem.plugin.config ?? {},
|
|
1591
|
+
},
|
|
1592
|
+
readOnly: options.rootFilesystem.readOnly ?? false,
|
|
1593
|
+
},
|
|
1594
|
+
}
|
|
1595
|
+
: {}),
|
|
1507
1596
|
// 0.3: the Node builtin allow-list moved from configureVm to
|
|
1508
1597
|
// VM creation. `undefined` => engine default allow-list;
|
|
1509
1598
|
// `[]` => deny all; `[..]` => exactly those. Platform and
|
|
1510
1599
|
// module resolution keep their engine defaults (full Node
|
|
1511
1600
|
// emulation), matching the prior behavior where Agent OS only
|
|
1512
1601
|
// constrained the builtin allow-list.
|
|
1513
|
-
...(options?.allowedNodeBuiltins !== undefined
|
|
1602
|
+
...(options?.allowedNodeBuiltins !== undefined ||
|
|
1603
|
+
options?.highResolutionTime !== undefined
|
|
1514
1604
|
? {
|
|
1515
1605
|
jsRuntime: {
|
|
1516
1606
|
platform: "node",
|
|
1517
1607
|
moduleResolution: "node",
|
|
1518
|
-
|
|
1608
|
+
...(options?.allowedNodeBuiltins !== undefined
|
|
1609
|
+
? { allowedBuiltins: options.allowedNodeBuiltins }
|
|
1610
|
+
: {}),
|
|
1611
|
+
...(options?.highResolutionTime !== undefined
|
|
1612
|
+
? { highResolutionTime: options.highResolutionTime }
|
|
1613
|
+
: {}),
|
|
1519
1614
|
},
|
|
1520
1615
|
}
|
|
1521
1616
|
: {}),
|
|
@@ -1524,19 +1619,30 @@ export class AgentOs {
|
|
|
1524
1619
|
runtime: "java_script",
|
|
1525
1620
|
config: createVmConfig,
|
|
1526
1621
|
});
|
|
1622
|
+
createdNativeVm = nativeVm;
|
|
1623
|
+
// Scope the readiness wait to THIS VM's ownership; on a shared process
|
|
1624
|
+
// other VMs are emitting their own lifecycle events concurrently.
|
|
1527
1625
|
await client.waitForEvent((event) => event.payload.type === "vm_lifecycle" &&
|
|
1528
|
-
event.payload.state === "ready"
|
|
1529
|
-
|
|
1626
|
+
event.payload.state === "ready" &&
|
|
1627
|
+
event.ownership.scope === "vm" &&
|
|
1628
|
+
event.ownership.vm_id === nativeVm.vmId, 10_000);
|
|
1629
|
+
const configuredVm = await client.configureVm(session, nativeVm, {
|
|
1530
1630
|
mounts: sidecarMounts,
|
|
1531
1631
|
permissions: sidecarPermissions,
|
|
1532
|
-
commandPermissions:
|
|
1632
|
+
commandPermissions: {},
|
|
1533
1633
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1634
|
+
packages: sidecarPackages,
|
|
1635
|
+
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1636
|
+
bindingShimCommands: bindingBootstrapCommands,
|
|
1534
1637
|
});
|
|
1535
|
-
|
|
1536
|
-
|
|
1638
|
+
for (const command of configuredVm.projectedCommands) {
|
|
1639
|
+
commandGuestPaths.set(command.name, command.guestPath);
|
|
1640
|
+
}
|
|
1641
|
+
if (bindings && bindings.length > 0) {
|
|
1642
|
+
bindingReference = await registerBindingsOnSidecar(client, session, nativeVm, bindings);
|
|
1537
1643
|
commandGuestPaths.set("agentos", "/bin/agentos");
|
|
1538
|
-
for (const
|
|
1539
|
-
commandGuestPaths.set(`agentos-${
|
|
1644
|
+
for (const bindingCollection of bindings) {
|
|
1645
|
+
commandGuestPaths.set(`agentos-${bindingCollection.name}`, `/bin/agentos-${bindingCollection.name}`);
|
|
1540
1646
|
}
|
|
1541
1647
|
}
|
|
1542
1648
|
rootBridge = new NativeSidecarKernelProxy({
|
|
@@ -1544,12 +1650,27 @@ export class AgentOs {
|
|
|
1544
1650
|
session,
|
|
1545
1651
|
vm: nativeVm,
|
|
1546
1652
|
env,
|
|
1547
|
-
cwd: "/
|
|
1653
|
+
cwd: "/workspace",
|
|
1548
1654
|
localMounts,
|
|
1655
|
+
sidecarMounts,
|
|
1656
|
+
permissions: sidecarPermissions,
|
|
1657
|
+
commandPermissions: {},
|
|
1658
|
+
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1659
|
+
// Retained for runtime mount reconfigures: `configure_vm` is
|
|
1660
|
+
// replace-on-write for the whole payload, so post-boot mountFs
|
|
1661
|
+
// must resend the boot packages and binding shims.
|
|
1662
|
+
packages: sidecarPackages,
|
|
1663
|
+
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1664
|
+
bindingShimCommands: bindingBootstrapCommands,
|
|
1549
1665
|
commandGuestPaths,
|
|
1550
1666
|
onDispose: cleanup,
|
|
1667
|
+
// The native process is owned by the AgentOsSidecar handle and
|
|
1668
|
+
// shared across VMs; disposing this VM must not kill the process.
|
|
1669
|
+
ownsClient: false,
|
|
1551
1670
|
});
|
|
1552
|
-
|
|
1671
|
+
if (options?.rootFilesystem?.type !== "native") {
|
|
1672
|
+
await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
|
|
1673
|
+
}
|
|
1553
1674
|
kernel = rootBridge;
|
|
1554
1675
|
const snapshotClient = client;
|
|
1555
1676
|
return {
|
|
@@ -1557,13 +1678,17 @@ export class AgentOs {
|
|
|
1557
1678
|
hostMounts,
|
|
1558
1679
|
kernel,
|
|
1559
1680
|
rootView: rootBridge.createRootView(),
|
|
1681
|
+
sidecarMounts,
|
|
1682
|
+
sidecarPermissions,
|
|
1683
|
+
commandPermissions: {},
|
|
1684
|
+
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1560
1685
|
sidecarClient: client,
|
|
1561
1686
|
sidecarSession: session,
|
|
1562
1687
|
sidecarVm: nativeVm,
|
|
1563
1688
|
permissions: hostPermissions,
|
|
1564
|
-
snapshotRootFilesystem: async () => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm))),
|
|
1565
|
-
|
|
1566
|
-
|
|
1689
|
+
snapshotRootFilesystem: async (maxBytes) => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm, maxBytes))),
|
|
1690
|
+
bindings: bindings ?? [],
|
|
1691
|
+
bindingReference,
|
|
1567
1692
|
async dispose() {
|
|
1568
1693
|
if (kernel) {
|
|
1569
1694
|
const currentKernel = kernel;
|
|
@@ -1581,6 +1706,8 @@ export class AgentOs {
|
|
|
1581
1706
|
};
|
|
1582
1707
|
}
|
|
1583
1708
|
catch (error) {
|
|
1709
|
+
// The native process is shared and owned by the sidecar handle, so
|
|
1710
|
+
// never dispose the client here — only tear down this VM's resources.
|
|
1584
1711
|
if (kernel) {
|
|
1585
1712
|
await kernel.dispose().catch(() => { });
|
|
1586
1713
|
}
|
|
@@ -1588,23 +1715,26 @@ export class AgentOs {
|
|
|
1588
1715
|
await rootBridge.dispose().catch(() => { });
|
|
1589
1716
|
}
|
|
1590
1717
|
else {
|
|
1591
|
-
|
|
1718
|
+
if (createdNativeVm && nativeSession && client) {
|
|
1719
|
+
await client
|
|
1720
|
+
.disposeVm(nativeSession, createdNativeVm)
|
|
1721
|
+
.catch(() => { });
|
|
1722
|
+
}
|
|
1592
1723
|
await cleanup();
|
|
1593
1724
|
}
|
|
1594
1725
|
throw error;
|
|
1595
1726
|
}
|
|
1596
1727
|
};
|
|
1597
|
-
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1598
1728
|
let sidecarLease = null;
|
|
1599
1729
|
try {
|
|
1600
1730
|
sidecarLease = await leaseAgentOsSidecarVm(sidecar, {
|
|
1601
1731
|
createVm: async () => createVmAdmin(),
|
|
1602
1732
|
});
|
|
1603
1733
|
const vmAdmin = sidecarLease.admin;
|
|
1604
|
-
const vm = new AgentOs(vmAdmin.kernel, sidecar,
|
|
1734
|
+
const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.onAgentStderr ?? defaultAgentStderrHandler, options?.onAgentExit ?? defaultAgentExitHandler, options?.onLimitWarning);
|
|
1605
1735
|
vm._sidecarLease = sidecarLease;
|
|
1606
|
-
vm.
|
|
1607
|
-
vm.
|
|
1736
|
+
vm._bindings = vmAdmin.bindings;
|
|
1737
|
+
vm._bindingReference = vmAdmin.bindingReference;
|
|
1608
1738
|
vm._permissions = vmAdmin.permissions;
|
|
1609
1739
|
vm._installSidecarRequestHandler();
|
|
1610
1740
|
vm._cronManager = new CronManager(vm, options?.scheduleDriver ?? new TimerScheduleDriver());
|
|
@@ -1622,76 +1752,66 @@ export class AgentOs {
|
|
|
1622
1752
|
const kernel = this.#kernel;
|
|
1623
1753
|
return kernel.execArgv(command, args, options);
|
|
1624
1754
|
}
|
|
1625
|
-
_trackProcess(proc, command, args,
|
|
1755
|
+
_trackProcess(proc, command, args, outputHandlers, exitHandlers) {
|
|
1626
1756
|
const entry = {
|
|
1627
1757
|
proc,
|
|
1628
1758
|
command,
|
|
1629
1759
|
args,
|
|
1630
|
-
|
|
1631
|
-
stderrHandlers,
|
|
1760
|
+
outputHandlers,
|
|
1632
1761
|
exitHandlers,
|
|
1633
1762
|
};
|
|
1634
1763
|
this._processes.set(proc.pid, entry);
|
|
1764
|
+
// NOTE: do NOT delete from `_processes` on exit — the public API contract
|
|
1765
|
+
// (getProcess/listProcesses/stopProcess, see process-management.test.ts)
|
|
1766
|
+
// requires exited processes to stay queryable (running:false, exitCode set).
|
|
1767
|
+
// `_processes` is a process table for this VM's lifetime; it is freed wholesale
|
|
1768
|
+
// in dispose(). (H5: the leak was that dispose() never cleared it.)
|
|
1635
1769
|
void proc.wait().then((code) => {
|
|
1636
1770
|
for (const h of exitHandlers)
|
|
1637
|
-
h(code);
|
|
1771
|
+
h({ pid: proc.pid, exitCode: code });
|
|
1638
1772
|
});
|
|
1639
1773
|
return { pid: proc.pid };
|
|
1640
1774
|
}
|
|
1641
1775
|
spawn(command, args, options) {
|
|
1642
|
-
const
|
|
1643
|
-
const stderrHandlers = new Set();
|
|
1776
|
+
const outputHandlers = new Set();
|
|
1644
1777
|
const exitHandlers = new Set();
|
|
1645
|
-
// Include caller-provided callbacks in the initial handler sets.
|
|
1646
|
-
if (options?.onStdout)
|
|
1647
|
-
stdoutHandlers.add(options.onStdout);
|
|
1648
|
-
if (options?.onStderr)
|
|
1649
|
-
stderrHandlers.add(options.onStderr);
|
|
1650
1778
|
const proc = this.#kernel.spawn(command, args, {
|
|
1651
1779
|
...options,
|
|
1652
1780
|
onStdout: (data) => {
|
|
1653
|
-
for (const h of
|
|
1654
|
-
h(data);
|
|
1781
|
+
for (const h of outputHandlers) {
|
|
1782
|
+
h({ pid: proc.pid, stream: "stdout", data });
|
|
1783
|
+
}
|
|
1655
1784
|
},
|
|
1656
1785
|
onStderr: (data) => {
|
|
1657
|
-
for (const h of
|
|
1658
|
-
h(data);
|
|
1786
|
+
for (const h of outputHandlers) {
|
|
1787
|
+
h({ pid: proc.pid, stream: "stderr", data });
|
|
1788
|
+
}
|
|
1659
1789
|
},
|
|
1660
1790
|
});
|
|
1661
|
-
return this._trackProcess(proc, command, args,
|
|
1791
|
+
return this._trackProcess(proc, command, args, outputHandlers, exitHandlers);
|
|
1662
1792
|
}
|
|
1663
1793
|
/** Write data to a process's stdin. */
|
|
1664
1794
|
writeProcessStdin(pid, data) {
|
|
1665
1795
|
const entry = this._processes.get(pid);
|
|
1666
1796
|
if (!entry)
|
|
1667
1797
|
throw new Error(`Process not found: ${pid}`);
|
|
1668
|
-
entry.proc.writeStdin(data);
|
|
1798
|
+
return entry.proc.writeStdin(data);
|
|
1669
1799
|
}
|
|
1670
1800
|
/** Close a process's stdin stream. */
|
|
1671
1801
|
closeProcessStdin(pid) {
|
|
1672
1802
|
const entry = this._processes.get(pid);
|
|
1673
1803
|
if (!entry)
|
|
1674
1804
|
throw new Error(`Process not found: ${pid}`);
|
|
1675
|
-
entry.proc.closeStdin();
|
|
1676
|
-
}
|
|
1677
|
-
/** Subscribe to stdout data from a process. Returns an unsubscribe function. */
|
|
1678
|
-
onProcessStdout(pid, handler) {
|
|
1679
|
-
const entry = this._processes.get(pid);
|
|
1680
|
-
if (!entry)
|
|
1681
|
-
throw new Error(`Process not found: ${pid}`);
|
|
1682
|
-
entry.stdoutHandlers.add(handler);
|
|
1683
|
-
return () => {
|
|
1684
|
-
entry.stdoutHandlers.delete(handler);
|
|
1685
|
-
};
|
|
1805
|
+
return entry.proc.closeStdin();
|
|
1686
1806
|
}
|
|
1687
|
-
/** Subscribe to stderr
|
|
1688
|
-
|
|
1807
|
+
/** Subscribe to stdout and stderr from a process. */
|
|
1808
|
+
onProcessOutput(pid, handler) {
|
|
1689
1809
|
const entry = this._processes.get(pid);
|
|
1690
1810
|
if (!entry)
|
|
1691
1811
|
throw new Error(`Process not found: ${pid}`);
|
|
1692
|
-
entry.
|
|
1812
|
+
entry.outputHandlers.add(handler);
|
|
1693
1813
|
return () => {
|
|
1694
|
-
entry.
|
|
1814
|
+
entry.outputHandlers.delete(handler);
|
|
1695
1815
|
};
|
|
1696
1816
|
}
|
|
1697
1817
|
/** Subscribe to process exit. Returns an unsubscribe function. */
|
|
@@ -1701,7 +1821,7 @@ export class AgentOs {
|
|
|
1701
1821
|
throw new Error(`Process not found: ${pid}`);
|
|
1702
1822
|
// If already exited, call immediately.
|
|
1703
1823
|
if (entry.proc.exitCode !== null) {
|
|
1704
|
-
handler(entry.proc.exitCode);
|
|
1824
|
+
handler({ pid, exitCode: entry.proc.exitCode });
|
|
1705
1825
|
return () => { };
|
|
1706
1826
|
}
|
|
1707
1827
|
entry.exitHandlers.add(handler);
|
|
@@ -1733,35 +1853,6 @@ export class AgentOs {
|
|
|
1733
1853
|
_vfs() {
|
|
1734
1854
|
return this.#kernel.vfs;
|
|
1735
1855
|
}
|
|
1736
|
-
async _copyPath(from, to) {
|
|
1737
|
-
const stat = await this._vfs().lstat(from);
|
|
1738
|
-
if (stat.isSymbolicLink) {
|
|
1739
|
-
const target = await this._vfs().readlink(from);
|
|
1740
|
-
await this._vfs().symlink(target, to);
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
if (stat.isDirectory) {
|
|
1744
|
-
await this._mkdirp(posixPath.dirname(to));
|
|
1745
|
-
if (!(await this.#kernel.exists(to))) {
|
|
1746
|
-
await this.#kernel.mkdir(to);
|
|
1747
|
-
}
|
|
1748
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1749
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1750
|
-
const entries = await this.#kernel.readdir(from);
|
|
1751
|
-
for (const entry of entries) {
|
|
1752
|
-
if (entry === "." || entry === "..")
|
|
1753
|
-
continue;
|
|
1754
|
-
const fromPath = from === "/" ? `/${entry}` : `${from}/${entry}`;
|
|
1755
|
-
const toPath = to === "/" ? `/${entry}` : `${to}/${entry}`;
|
|
1756
|
-
await this._copyPath(fromPath, toPath);
|
|
1757
|
-
}
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
const content = await this.#kernel.readFile(from);
|
|
1761
|
-
await this.writeFile(to, content);
|
|
1762
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1763
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1764
|
-
}
|
|
1765
1856
|
async readFile(path) {
|
|
1766
1857
|
this._assertSafeAbsolutePath(path);
|
|
1767
1858
|
return this.#kernel.readFile(path);
|
|
@@ -1814,14 +1905,13 @@ export class AgentOs {
|
|
|
1814
1905
|
/** Recursively create directories (mkdir -p). */
|
|
1815
1906
|
async _mkdirp(path) {
|
|
1816
1907
|
this._assertWritableAbsolutePath(path);
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
}
|
|
1908
|
+
// `kernel.mkdir` is already recursive (it defaults to recursive=true on both
|
|
1909
|
+
// the native sidecar and compat kernels) and creating an existing directory is
|
|
1910
|
+
// a no-op, so a single call is sufficient. Do NOT probe each ancestor with
|
|
1911
|
+
// `exists()` first: on the native sidecar every read-side op
|
|
1912
|
+
// (exists/stat/readFile) triggers a full shadow-tree walk, so a per-component
|
|
1913
|
+
// exists() loop makes `mkdir -p` cost O(components * tree).
|
|
1914
|
+
await this.#kernel.mkdir(path);
|
|
1825
1915
|
}
|
|
1826
1916
|
async mkdir(path, options) {
|
|
1827
1917
|
if (options?.recursive) {
|
|
@@ -1834,51 +1924,42 @@ export class AgentOs {
|
|
|
1834
1924
|
this._assertSafeAbsolutePath(path);
|
|
1835
1925
|
return this.#kernel.readdir(path);
|
|
1836
1926
|
}
|
|
1927
|
+
async readdirEntries(path) {
|
|
1928
|
+
this._assertSafeAbsolutePath(path);
|
|
1929
|
+
const entries = await this.#kernel.readdirRecursive(path, { maxDepth: 0 });
|
|
1930
|
+
return entries.map((entry) => ({
|
|
1931
|
+
name: entry.name,
|
|
1932
|
+
isDirectory: entry.isDirectory,
|
|
1933
|
+
isSymbolicLink: entry.isSymbolicLink,
|
|
1934
|
+
}));
|
|
1935
|
+
}
|
|
1837
1936
|
async readdirRecursive(path, options) {
|
|
1838
1937
|
this._assertSafeAbsolutePath(path);
|
|
1839
|
-
const maxDepth = options?.maxDepth;
|
|
1840
1938
|
const exclude = options?.exclude ? new Set(options.exclude) : undefined;
|
|
1939
|
+
const entries = await this.#kernel.readdirRecursive(path, {
|
|
1940
|
+
maxDepth: options?.maxDepth,
|
|
1941
|
+
});
|
|
1942
|
+
const excludedPrefixes = [];
|
|
1841
1943
|
const results = [];
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
if (
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
const entries = await this.#kernel.readdir(dirPath);
|
|
1850
|
-
for (const name of entries) {
|
|
1851
|
-
if (name === "." || name === "..")
|
|
1852
|
-
continue;
|
|
1853
|
-
if (exclude?.has(name))
|
|
1854
|
-
continue;
|
|
1855
|
-
const fullPath = dirPath === "/" ? `/${name}` : `${dirPath}/${name}`;
|
|
1856
|
-
const s = await this.#kernel.stat(fullPath);
|
|
1857
|
-
if (s.isSymbolicLink) {
|
|
1858
|
-
results.push({
|
|
1859
|
-
path: fullPath,
|
|
1860
|
-
type: "symlink",
|
|
1861
|
-
size: s.size,
|
|
1862
|
-
});
|
|
1863
|
-
}
|
|
1864
|
-
else if (s.isDirectory) {
|
|
1865
|
-
results.push({
|
|
1866
|
-
path: fullPath,
|
|
1867
|
-
type: "directory",
|
|
1868
|
-
size: s.size,
|
|
1869
|
-
});
|
|
1870
|
-
if (maxDepth === undefined || depth < maxDepth) {
|
|
1871
|
-
queue.push([fullPath, depth + 1]);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
else {
|
|
1875
|
-
results.push({
|
|
1876
|
-
path: fullPath,
|
|
1877
|
-
type: "file",
|
|
1878
|
-
size: s.size,
|
|
1879
|
-
});
|
|
1944
|
+
for (const entry of entries) {
|
|
1945
|
+
if (excludedPrefixes.some((prefix) => entry.path === prefix || entry.path.startsWith(`${prefix}/`))) {
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
if (exclude?.has(entry.name)) {
|
|
1949
|
+
if (entry.isDirectory && !entry.isSymbolicLink) {
|
|
1950
|
+
excludedPrefixes.push(entry.path);
|
|
1880
1951
|
}
|
|
1952
|
+
continue;
|
|
1881
1953
|
}
|
|
1954
|
+
results.push({
|
|
1955
|
+
path: entry.path,
|
|
1956
|
+
type: entry.isSymbolicLink
|
|
1957
|
+
? "symlink"
|
|
1958
|
+
: entry.isDirectory
|
|
1959
|
+
? "directory"
|
|
1960
|
+
: "file",
|
|
1961
|
+
size: entry.size,
|
|
1962
|
+
});
|
|
1882
1963
|
}
|
|
1883
1964
|
return results;
|
|
1884
1965
|
}
|
|
@@ -1890,89 +1971,133 @@ export class AgentOs {
|
|
|
1890
1971
|
this._assertSafeAbsolutePath(path);
|
|
1891
1972
|
return this.#kernel.exists(path);
|
|
1892
1973
|
}
|
|
1893
|
-
async
|
|
1974
|
+
async exportRootFilesystem(options) {
|
|
1975
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0) {
|
|
1976
|
+
throw new RangeError("maxBytes must be a positive safe integer");
|
|
1977
|
+
}
|
|
1894
1978
|
const nativeSnapshot = this._sidecarLease?.admin.snapshotRootFilesystem;
|
|
1895
|
-
|
|
1896
|
-
|
|
1979
|
+
const snapshot = nativeSnapshot
|
|
1980
|
+
? await nativeSnapshot(options.maxBytes)
|
|
1981
|
+
: createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
|
|
1982
|
+
const size = Buffer.byteLength(JSON.stringify(snapshot));
|
|
1983
|
+
if (size > options.maxBytes) {
|
|
1984
|
+
throw new RangeError(`root filesystem export is ${size} bytes, limit is ${options.maxBytes}; raise maxBytes to export this filesystem`);
|
|
1897
1985
|
}
|
|
1898
|
-
return
|
|
1986
|
+
return snapshot;
|
|
1899
1987
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1988
|
+
/**
|
|
1989
|
+
* Mount a filesystem into the running VM. Resolves once the mount has been
|
|
1990
|
+
* delivered to the native sidecar, so guest code can use it immediately
|
|
1991
|
+
* after the returned promise settles; a delivery failure rejects instead of
|
|
1992
|
+
* leaving the mount silently host-only.
|
|
1993
|
+
*/
|
|
1994
|
+
async mountFs(descriptor) {
|
|
1995
|
+
this._assertSafeAbsolutePath(descriptor.path);
|
|
1996
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
1997
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
1998
|
+
}
|
|
1999
|
+
await this.#kernel.mountDescriptor({
|
|
2000
|
+
guestPath: descriptor.path,
|
|
2001
|
+
readOnly: descriptor.readOnly ?? false,
|
|
2002
|
+
plugin: {
|
|
2003
|
+
id: descriptor.plugin.id,
|
|
2004
|
+
config: descriptor.plugin.config ?? {},
|
|
2005
|
+
},
|
|
2006
|
+
});
|
|
1903
2007
|
}
|
|
1904
|
-
unmountFs(path) {
|
|
2008
|
+
async unmountFs(path) {
|
|
1905
2009
|
this._assertSafeAbsolutePath(path);
|
|
1906
|
-
this.#kernel
|
|
2010
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
2011
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
2012
|
+
}
|
|
2013
|
+
await this.#kernel.unmountDescriptor(path);
|
|
2014
|
+
}
|
|
2015
|
+
async listMounts() {
|
|
2016
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy))
|
|
2017
|
+
return [];
|
|
2018
|
+
return this.#kernel.listMounts();
|
|
1907
2019
|
}
|
|
1908
2020
|
async move(from, to) {
|
|
1909
|
-
this.
|
|
1910
|
-
this.
|
|
1911
|
-
|
|
1912
|
-
if (!sourceStat.isDirectory || sourceStat.isSymbolicLink) {
|
|
1913
|
-
return this.#kernel.rename(from, to);
|
|
1914
|
-
}
|
|
1915
|
-
await this._copyPath(from, to);
|
|
1916
|
-
await this.delete(from, { recursive: true });
|
|
2021
|
+
this._assertWritableAbsolutePath(from);
|
|
2022
|
+
this._assertWritableAbsolutePath(to);
|
|
2023
|
+
await this.#kernel.movePath(from, to);
|
|
1917
2024
|
}
|
|
1918
|
-
async
|
|
1919
|
-
this.
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const entries = await this.#kernel.readdir(path);
|
|
1924
|
-
for (const entry of entries) {
|
|
1925
|
-
if (entry === "." || entry === "..")
|
|
1926
|
-
continue;
|
|
1927
|
-
await this.delete(`${path}/${entry}`, { recursive: true });
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
return this.#kernel.removeDir(path);
|
|
1931
|
-
}
|
|
1932
|
-
return this.#kernel.removeFile(path);
|
|
2025
|
+
async remove(path, options) {
|
|
2026
|
+
this._assertWritableAbsolutePath(path);
|
|
2027
|
+
await this.#kernel.removePath(path, {
|
|
2028
|
+
recursive: options?.recursive ?? false,
|
|
2029
|
+
});
|
|
1933
2030
|
}
|
|
1934
|
-
async
|
|
1935
|
-
|
|
2031
|
+
async httpRequest(request) {
|
|
2032
|
+
this._assertSafeAbsolutePath(request.path.split("?")[0] || "/");
|
|
2033
|
+
const method = request.method?.toUpperCase() ?? "GET";
|
|
2034
|
+
const body = typeof request.body === "string"
|
|
2035
|
+
? request.body
|
|
2036
|
+
: request.body === undefined
|
|
2037
|
+
? undefined
|
|
2038
|
+
: new TextDecoder().decode(request.body);
|
|
1936
2039
|
const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
1937
|
-
port,
|
|
1938
|
-
method
|
|
1939
|
-
path:
|
|
1940
|
-
headersJson: JSON.stringify(
|
|
1941
|
-
...(
|
|
1942
|
-
? { body
|
|
2040
|
+
port: request.port,
|
|
2041
|
+
method,
|
|
2042
|
+
path: request.path,
|
|
2043
|
+
headersJson: JSON.stringify(request.headers ?? {}),
|
|
2044
|
+
...(method !== "GET" && method !== "HEAD" && body !== undefined
|
|
2045
|
+
? { body }
|
|
1943
2046
|
: {}),
|
|
1944
2047
|
}));
|
|
1945
|
-
const headers =
|
|
2048
|
+
const headers = {};
|
|
1946
2049
|
for (const [key, value] of responsePayload.headers ?? []) {
|
|
1947
|
-
headers
|
|
2050
|
+
headers[key] = value;
|
|
1948
2051
|
}
|
|
1949
|
-
return
|
|
2052
|
+
return {
|
|
1950
2053
|
status: responsePayload.status,
|
|
1951
|
-
statusText: responsePayload.statusText,
|
|
2054
|
+
statusText: responsePayload.statusText ?? "",
|
|
1952
2055
|
headers,
|
|
1953
|
-
|
|
2056
|
+
body: new Uint8Array(Buffer.from(responsePayload.body ?? "", "base64")),
|
|
2057
|
+
};
|
|
1954
2058
|
}
|
|
1955
2059
|
openShell(options) {
|
|
1956
2060
|
const shellId = `shell-${++this._shellCounter}`;
|
|
1957
2061
|
this._closedShellIds.delete(shellId);
|
|
1958
2062
|
const dataHandlers = new Set();
|
|
1959
|
-
const
|
|
2063
|
+
const stderrHandlers = new Set();
|
|
2064
|
+
const exitHandlers = new Set();
|
|
2065
|
+
const handle = this.#kernel.openShell({
|
|
2066
|
+
...options,
|
|
2067
|
+
onStderr: (data) => {
|
|
2068
|
+
for (const handler of stderrHandlers)
|
|
2069
|
+
handler({ shellId, data });
|
|
2070
|
+
},
|
|
2071
|
+
});
|
|
1960
2072
|
handle.onData = (data) => {
|
|
1961
|
-
for (const
|
|
1962
|
-
|
|
2073
|
+
for (const handler of dataHandlers)
|
|
2074
|
+
handler({ shellId, data });
|
|
1963
2075
|
};
|
|
1964
2076
|
const entry = {
|
|
1965
2077
|
handle,
|
|
1966
2078
|
dataHandlers,
|
|
1967
|
-
|
|
2079
|
+
stderrHandlers,
|
|
2080
|
+
exitHandlers,
|
|
2081
|
+
exitPromise: Promise.resolve(0),
|
|
1968
2082
|
};
|
|
1969
|
-
const exitPromise = handle.wait()
|
|
1970
|
-
|
|
2083
|
+
const exitPromise = handle.wait();
|
|
2084
|
+
const finalize = (exitCode) => {
|
|
1971
2085
|
this._pendingShellExitPromises.delete(entry.exitPromise);
|
|
1972
2086
|
if (this._shells.get(shellId) === entry) {
|
|
1973
2087
|
this._shells.delete(shellId);
|
|
1974
|
-
this._closedShellIds.add(shellId);
|
|
1975
2088
|
}
|
|
2089
|
+
// Record the exit code even when closeShell already dropped the
|
|
2090
|
+
// entry, so a waitShell issued after exit still resolves with it.
|
|
2091
|
+
this._closedShellIds.add(shellId, exitCode);
|
|
2092
|
+
};
|
|
2093
|
+
entry.exitPromise = exitPromise.then((exitCode) => {
|
|
2094
|
+
finalize(exitCode);
|
|
2095
|
+
for (const handler of exitHandlers)
|
|
2096
|
+
handler({ shellId, exitCode });
|
|
2097
|
+
return exitCode;
|
|
2098
|
+
}, (error) => {
|
|
2099
|
+
finalize();
|
|
2100
|
+
throw error;
|
|
1976
2101
|
});
|
|
1977
2102
|
this._pendingShellExitPromises.add(entry.exitPromise);
|
|
1978
2103
|
this._shells.set(shellId, entry);
|
|
@@ -1986,9 +2111,13 @@ export class AgentOs {
|
|
|
1986
2111
|
const entry = this._shells.get(shellId);
|
|
1987
2112
|
if (!entry)
|
|
1988
2113
|
throw new Error(`Shell not found: ${shellId}`);
|
|
1989
|
-
entry.handle.write(data);
|
|
2114
|
+
return entry.handle.write(data);
|
|
1990
2115
|
}
|
|
1991
|
-
/**
|
|
2116
|
+
/**
|
|
2117
|
+
* Subscribe to ordered PTY output (stdout and stderr). Returns an unsubscribe
|
|
2118
|
+
* function. `OpenShellOptions.onStderr` is a diagnostic tap for callers that
|
|
2119
|
+
* need channel identity; do not render both surfaces.
|
|
2120
|
+
*/
|
|
1992
2121
|
onShellData(shellId, handler) {
|
|
1993
2122
|
const entry = this._shells.get(shellId);
|
|
1994
2123
|
if (!entry)
|
|
@@ -1998,6 +2127,28 @@ export class AgentOs {
|
|
|
1998
2127
|
entry.dataHandlers.delete(handler);
|
|
1999
2128
|
};
|
|
2000
2129
|
}
|
|
2130
|
+
/** Subscribe to the stderr-only diagnostic stream for a shell. */
|
|
2131
|
+
onShellStderr(shellId, handler) {
|
|
2132
|
+
const entry = this._shells.get(shellId);
|
|
2133
|
+
if (!entry)
|
|
2134
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2135
|
+
entry.stderrHandlers.add(handler);
|
|
2136
|
+
return () => entry.stderrHandlers.delete(handler);
|
|
2137
|
+
}
|
|
2138
|
+
/** Subscribe to shell exit. */
|
|
2139
|
+
onShellExit(shellId, handler) {
|
|
2140
|
+
const entry = this._shells.get(shellId);
|
|
2141
|
+
if (!entry) {
|
|
2142
|
+
const exitCode = this._closedShellIds.get(shellId);
|
|
2143
|
+
if (exitCode !== undefined) {
|
|
2144
|
+
handler({ shellId, exitCode });
|
|
2145
|
+
return () => { };
|
|
2146
|
+
}
|
|
2147
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2148
|
+
}
|
|
2149
|
+
entry.exitHandlers.add(handler);
|
|
2150
|
+
return () => entry.exitHandlers.delete(handler);
|
|
2151
|
+
}
|
|
2001
2152
|
/** Notify a shell of terminal resize. */
|
|
2002
2153
|
resizeShell(shellId, cols, rows) {
|
|
2003
2154
|
const entry = this._shells.get(shellId);
|
|
@@ -2005,6 +2156,21 @@ export class AgentOs {
|
|
|
2005
2156
|
throw new Error(`Shell not found: ${shellId}`);
|
|
2006
2157
|
entry.handle.resize(cols, rows);
|
|
2007
2158
|
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Wait for a shell to exit and return its process exit code. Resolves
|
|
2161
|
+
* immediately for a shell that has already exited (within the closed-shell
|
|
2162
|
+
* retention window).
|
|
2163
|
+
*/
|
|
2164
|
+
waitShell(shellId) {
|
|
2165
|
+
const entry = this._shells.get(shellId);
|
|
2166
|
+
if (!entry) {
|
|
2167
|
+
const exitCode = this._closedShellIds.get(shellId);
|
|
2168
|
+
if (exitCode !== undefined)
|
|
2169
|
+
return Promise.resolve(exitCode);
|
|
2170
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2171
|
+
}
|
|
2172
|
+
return entry.exitPromise;
|
|
2173
|
+
}
|
|
2008
2174
|
/** Kill a shell process and remove it from tracking. */
|
|
2009
2175
|
closeShell(shellId) {
|
|
2010
2176
|
const entry = this._shells.get(shellId);
|
|
@@ -2104,609 +2270,406 @@ export class AgentOs {
|
|
|
2104
2270
|
return;
|
|
2105
2271
|
entry.proc.kill(9);
|
|
2106
2272
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
]);
|
|
2129
|
-
return [...allIds]
|
|
2130
|
-
.map((id) => {
|
|
2131
|
-
const config = this._resolveAgentConfig(id);
|
|
2132
|
-
if (!config)
|
|
2133
|
-
return null;
|
|
2134
|
-
let installed = false;
|
|
2135
|
-
try {
|
|
2136
|
-
// Check software roots first, then the host dir behind the
|
|
2137
|
-
// `/root/node_modules` mount.
|
|
2138
|
-
const vmPrefix = `/root/node_modules/${config.acpAdapter}`;
|
|
2139
|
-
let hostPkgJsonPath = null;
|
|
2140
|
-
for (const root of this._softwareRoots) {
|
|
2141
|
-
if (root.vmPath === vmPrefix) {
|
|
2142
|
-
hostPkgJsonPath = join(root.hostPath, "package.json");
|
|
2143
|
-
break;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
if (!hostPkgJsonPath) {
|
|
2147
|
-
const nodeModulesRoot = this._nodeModulesHostRoot();
|
|
2148
|
-
if (nodeModulesRoot) {
|
|
2149
|
-
hostPkgJsonPath = join(resolvePackageDir(nodeModulesRoot, config.acpAdapter), "package.json");
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
if (!hostPkgJsonPath) {
|
|
2153
|
-
throw new Error("no package source");
|
|
2154
|
-
}
|
|
2155
|
-
readFileSync(hostPkgJsonPath);
|
|
2156
|
-
installed = true;
|
|
2157
|
-
}
|
|
2158
|
-
catch {
|
|
2159
|
-
// Package not installed
|
|
2160
|
-
}
|
|
2161
|
-
return {
|
|
2162
|
-
id,
|
|
2163
|
-
acpAdapter: config.acpAdapter,
|
|
2164
|
-
agentPackage: config.agentPackage,
|
|
2165
|
-
installed,
|
|
2166
|
-
};
|
|
2167
|
-
})
|
|
2168
|
-
.filter((entry) => entry !== null);
|
|
2169
|
-
}
|
|
2170
|
-
_syncSessionState(session, state) {
|
|
2171
|
-
session.processId = state.processId;
|
|
2172
|
-
session.pid = state.pid ?? null;
|
|
2173
|
-
session.closed = state.closed;
|
|
2174
|
-
session.modes = toSessionModes(state.modes);
|
|
2175
|
-
session.configOptions = toSessionConfigOptions(state.configOptions);
|
|
2176
|
-
this._applySyntheticConfigOverrides(session);
|
|
2177
|
-
session.capabilities = toAgentCapabilities(state.agentCapabilities);
|
|
2178
|
-
session.agentInfo = toAgentInfo(state.agentInfo);
|
|
2179
|
-
}
|
|
2180
|
-
_applySessionUpdate(session, notification) {
|
|
2181
|
-
if (notification.method !== "session/update") {
|
|
2182
|
-
return;
|
|
2183
|
-
}
|
|
2184
|
-
const params = toRecord(notification.params);
|
|
2185
|
-
const update = toRecord(params.update ?? params);
|
|
2186
|
-
const sessionUpdate = update.sessionUpdate;
|
|
2187
|
-
if (sessionUpdate === "current_mode_update" &&
|
|
2188
|
-
typeof update.currentModeId === "string" &&
|
|
2189
|
-
session.modes) {
|
|
2190
|
-
session.modes = {
|
|
2191
|
-
...session.modes,
|
|
2192
|
-
currentModeId: update.currentModeId,
|
|
2193
|
-
};
|
|
2194
|
-
}
|
|
2195
|
-
if ((sessionUpdate === "config_option_update" ||
|
|
2196
|
-
sessionUpdate === "config_options_update") &&
|
|
2197
|
-
Array.isArray(update.configOptions)) {
|
|
2198
|
-
session.configOptions = update.configOptions;
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
_recordSessionNotification(session, notification) {
|
|
2202
|
-
this._applySessionUpdate(session, notification);
|
|
2203
|
-
if (shouldDispatchToSessionEventHandlers(notification)) {
|
|
2204
|
-
this._dispatchSessionEvent(session, notification);
|
|
2205
|
-
}
|
|
2206
|
-
if (notification.method === LEGACY_PERMISSION_METHOD ||
|
|
2207
|
-
notification.method === ACP_PERMISSION_METHOD) {
|
|
2208
|
-
const params = toRecord(notification.params);
|
|
2209
|
-
const permissionId = params.permissionId;
|
|
2210
|
-
if (typeof permissionId === "string" ||
|
|
2211
|
-
typeof permissionId === "number") {
|
|
2212
|
-
const request = {
|
|
2213
|
-
permissionId: String(permissionId),
|
|
2214
|
-
description: typeof params.description === "string"
|
|
2215
|
-
? params.description
|
|
2216
|
-
: undefined,
|
|
2217
|
-
params,
|
|
2218
|
-
};
|
|
2219
|
-
for (const handler of session.permissionHandlers) {
|
|
2220
|
-
handler(request);
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2273
|
+
async openSession(input) {
|
|
2274
|
+
const response = await this._sendAcpRequest({
|
|
2275
|
+
tag: "AcpOpenSessionRequest",
|
|
2276
|
+
val: {
|
|
2277
|
+
sessionId: input.sessionId ?? null,
|
|
2278
|
+
agent: input.agent,
|
|
2279
|
+
cwd: input.cwd ?? null,
|
|
2280
|
+
additionalDirectories: input.additionalDirectories === undefined
|
|
2281
|
+
? null
|
|
2282
|
+
: JSON.stringify(input.additionalDirectories),
|
|
2283
|
+
env: input.env === undefined ? null : JSON.stringify(input.env),
|
|
2284
|
+
mcpServers: input.mcpServers === undefined
|
|
2285
|
+
? null
|
|
2286
|
+
: JSON.stringify(input.mcpServers),
|
|
2287
|
+
permissionPolicy: input.permissionPolicy ?? null,
|
|
2288
|
+
skipOsInstructions: input.skipOsInstructions ?? null,
|
|
2289
|
+
additionalInstructions: combineInstructions(input.additionalInstructions, this._bindingReference) ?? null,
|
|
2290
|
+
},
|
|
2291
|
+
});
|
|
2292
|
+
if (response.tag !== "AcpOpenSessionResponse") {
|
|
2293
|
+
throw new Error(`unexpected openSession response: ${response.tag}`);
|
|
2223
2294
|
}
|
|
2224
2295
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
}
|
|
2233
|
-
catch {
|
|
2234
|
-
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2235
|
-
}
|
|
2296
|
+
async getSession(input) {
|
|
2297
|
+
const response = await this._sendAcpRequest({
|
|
2298
|
+
tag: "AcpGetDurableSessionRequest",
|
|
2299
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2300
|
+
});
|
|
2301
|
+
if (response.tag !== "AcpGetDurableSessionResponse") {
|
|
2302
|
+
throw new Error(`unexpected getSession response: ${response.tag}`);
|
|
2236
2303
|
}
|
|
2304
|
+
return decodeDurableSessionInfo(response.val.session);
|
|
2237
2305
|
}
|
|
2238
|
-
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2306
|
+
async listSessions(input) {
|
|
2307
|
+
const response = await this._sendAcpRequest({
|
|
2308
|
+
tag: "AcpListDurableSessionsRequest",
|
|
2309
|
+
val: { cursor: input?.cursor ?? null, limit: input?.limit ?? null },
|
|
2310
|
+
});
|
|
2311
|
+
if (response.tag !== "AcpListDurableSessionsResponse") {
|
|
2312
|
+
throw new Error(`unexpected listSessions response: ${response.tag}`);
|
|
2313
|
+
}
|
|
2314
|
+
return {
|
|
2315
|
+
sessions: response.val.sessions.map(decodeDurableSessionInfo),
|
|
2316
|
+
nextCursor: response.val.nextCursor,
|
|
2245
2317
|
};
|
|
2246
2318
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
session.configOptions = session.configOptions.map((option) => {
|
|
2252
|
-
const override = session.configOverrides.get(option.id) ??
|
|
2253
|
-
(typeof option.category === "string"
|
|
2254
|
-
? session.configOverrides.get(option.category)
|
|
2255
|
-
: undefined);
|
|
2256
|
-
return override === undefined
|
|
2257
|
-
? option
|
|
2258
|
-
: { ...option, currentValue: override };
|
|
2319
|
+
async deleteSession(input = {}) {
|
|
2320
|
+
const response = await this._sendAcpRequest({
|
|
2321
|
+
tag: "AcpDeleteSessionRequest",
|
|
2322
|
+
val: { sessionId: input.sessionId ?? null },
|
|
2259
2323
|
});
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
if (event.payload.type === "ext") {
|
|
2263
|
-
this._handleAcpExtEvent(event.payload.envelope);
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
if (event.payload.type !== "structured") {
|
|
2267
|
-
return;
|
|
2268
|
-
}
|
|
2269
|
-
if (event.payload.name !== "acp.session_event") {
|
|
2270
|
-
return;
|
|
2324
|
+
if (response.tag !== "AcpDeleteSessionResponse") {
|
|
2325
|
+
throw new Error(`unexpected deleteSession response: ${response.tag}`);
|
|
2271
2326
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2327
|
+
}
|
|
2328
|
+
async unloadSession(input) {
|
|
2329
|
+
const response = await this._sendAcpRequest({
|
|
2330
|
+
tag: "AcpUnloadSessionRequest",
|
|
2331
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2332
|
+
});
|
|
2333
|
+
if (response.tag !== "AcpUnloadSessionResponse") {
|
|
2334
|
+
throw new Error(`unexpected unloadSession response: ${response.tag}`);
|
|
2276
2335
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2336
|
+
}
|
|
2337
|
+
async prompt(input) {
|
|
2338
|
+
const response = await this._sendAcpRequest({
|
|
2339
|
+
tag: "AcpPromptRequest",
|
|
2340
|
+
val: {
|
|
2341
|
+
sessionId: input.sessionId ?? null,
|
|
2342
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
2343
|
+
content: JSON.stringify(input.content),
|
|
2344
|
+
},
|
|
2345
|
+
});
|
|
2346
|
+
if (response.tag !== "AcpPromptResponse") {
|
|
2347
|
+
throw new Error(`unexpected prompt response: ${response.tag}`);
|
|
2280
2348
|
}
|
|
2281
|
-
|
|
2282
|
-
|
|
2349
|
+
return {
|
|
2350
|
+
sessionId: response.val.sessionId,
|
|
2351
|
+
message: response.val.message === null ? null : JSON.parse(response.val.message),
|
|
2352
|
+
stopReason: response.val.stopReason,
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
async cancelPrompt(input) {
|
|
2356
|
+
const response = await this._sendAcpRequest({
|
|
2357
|
+
tag: "AcpCancelPromptRequest",
|
|
2358
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2359
|
+
});
|
|
2360
|
+
if (response.tag !== "AcpCancelPromptResponse") {
|
|
2361
|
+
throw new Error(`unexpected cancelPrompt response: ${response.tag}`);
|
|
2283
2362
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2363
|
+
if (response.val.status !== "cancelled" &&
|
|
2364
|
+
response.val.status !== "no_active_prompt") {
|
|
2365
|
+
throw new Error(`invalid cancelPrompt status: ${response.val.status}`);
|
|
2286
2366
|
}
|
|
2367
|
+
return { status: response.val.status };
|
|
2287
2368
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2369
|
+
async respondPermission(input) {
|
|
2370
|
+
const response = await this._sendAcpRequest({
|
|
2371
|
+
tag: "AcpRespondPermissionRequest",
|
|
2372
|
+
val: {
|
|
2373
|
+
sessionId: input.sessionId,
|
|
2374
|
+
requestId: input.requestId,
|
|
2375
|
+
optionId: input.optionId,
|
|
2376
|
+
},
|
|
2377
|
+
});
|
|
2378
|
+
if (response.tag !== "AcpRespondPermissionResponse") {
|
|
2379
|
+
throw new Error(`unexpected respondPermission response: ${response.tag}`);
|
|
2291
2380
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
case "AcpSessionEvent": {
|
|
2296
|
-
const session = this._sessions.get(event.val.sessionId);
|
|
2297
|
-
if (!session) {
|
|
2298
|
-
return;
|
|
2299
|
-
}
|
|
2300
|
-
this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(event.val.notification)));
|
|
2301
|
-
return;
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2381
|
+
if (response.val.status !== "accepted" &&
|
|
2382
|
+
response.val.status !== "not_pending") {
|
|
2383
|
+
throw new Error(`invalid respondPermission status: ${response.val.status}`);
|
|
2304
2384
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2385
|
+
if (response.val.status === "accepted") {
|
|
2386
|
+
if (response.val.reason !== null) {
|
|
2387
|
+
throw new Error("accepted permission response must not include a reason");
|
|
2388
|
+
}
|
|
2389
|
+
return { status: "accepted" };
|
|
2307
2390
|
}
|
|
2308
|
-
}
|
|
2309
|
-
_unsupportedConfigResponse(agentType, category) {
|
|
2310
|
-
const message = agentType === "opencode" && category === "model"
|
|
2311
|
-
? "OpenCode reports available models, but model switching must be configured before createSession() because ACP session/set_config_option is not implemented."
|
|
2312
|
-
: `The ${category} config option is read-only for ${agentType} sessions.`;
|
|
2313
2391
|
return {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
error: {
|
|
2317
|
-
code: -32601,
|
|
2318
|
-
message,
|
|
2319
|
-
},
|
|
2392
|
+
status: "not_pending",
|
|
2393
|
+
reason: permissionTerminalReason(response.val.reason),
|
|
2320
2394
|
};
|
|
2321
2395
|
}
|
|
2322
|
-
async
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2396
|
+
async readHistory(input) {
|
|
2397
|
+
const response = await this._sendAcpRequest({
|
|
2398
|
+
tag: "AcpReadHistoryRequest",
|
|
2399
|
+
val: {
|
|
2400
|
+
sessionId: input?.sessionId ?? null,
|
|
2401
|
+
before: input?.before === undefined ? null : BigInt(input.before),
|
|
2402
|
+
after: input?.after === undefined ? null : BigInt(input.after),
|
|
2403
|
+
limit: input?.limit ?? null,
|
|
2404
|
+
},
|
|
2326
2405
|
});
|
|
2327
|
-
if (
|
|
2328
|
-
throw new Error(`unexpected
|
|
2406
|
+
if (response.tag !== "AcpHistoryPageResponse") {
|
|
2407
|
+
throw new Error(`unexpected readHistory response: ${response.tag}`);
|
|
2329
2408
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2409
|
+
return {
|
|
2410
|
+
events: response.val.events.map(decodeDurableSessionEvent),
|
|
2411
|
+
hasMoreBefore: response.val.hasMoreBefore,
|
|
2412
|
+
hasMoreAfter: response.val.hasMoreAfter,
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
async getSessionConfig(input) {
|
|
2416
|
+
const response = await this._sendAcpRequest({
|
|
2417
|
+
tag: "AcpGetSessionConfigRequest",
|
|
2418
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2419
|
+
});
|
|
2420
|
+
if (response.tag !== "AcpSessionConfigResponse") {
|
|
2421
|
+
throw new Error(`unexpected getSessionConfig response: ${response.tag}`);
|
|
2335
2422
|
}
|
|
2336
|
-
return
|
|
2423
|
+
return {
|
|
2424
|
+
revision: safeWireU64(response.val.revision),
|
|
2425
|
+
options: JSON.parse(response.val.options),
|
|
2426
|
+
};
|
|
2337
2427
|
}
|
|
2338
|
-
async
|
|
2339
|
-
const
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
},
|
|
2347
|
-
};
|
|
2348
|
-
resolvers.add(resolver);
|
|
2349
|
-
this._pendingSessionRequestResolvers.set(sessionId, resolvers);
|
|
2350
|
-
void this._sendAcpRequest({
|
|
2351
|
-
tag: "AcpSessionRequest",
|
|
2352
|
-
val: {
|
|
2353
|
-
sessionId,
|
|
2354
|
-
method,
|
|
2355
|
-
params: params === undefined ? null : JSON.stringify(params),
|
|
2356
|
-
},
|
|
2357
|
-
})
|
|
2358
|
-
.then((response) => {
|
|
2359
|
-
if (response.tag !== "AcpSessionRpcResponse") {
|
|
2360
|
-
throw new Error(`unexpected response to AcpSessionRequest: ${response.tag}`);
|
|
2361
|
-
}
|
|
2362
|
-
return toJsonRpcResponse(JSON.parse(response.val.response));
|
|
2363
|
-
})
|
|
2364
|
-
.then(resolve, reject)
|
|
2365
|
-
.finally(() => {
|
|
2366
|
-
const nextResolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2367
|
-
if (!nextResolvers) {
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
nextResolvers.delete(resolver);
|
|
2371
|
-
if (nextResolvers.size === 0) {
|
|
2372
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2373
|
-
}
|
|
2374
|
-
});
|
|
2428
|
+
async setSessionConfigOption(input) {
|
|
2429
|
+
const response = await this._sendAcpRequest({
|
|
2430
|
+
tag: "AcpSetSessionConfigOptionRequest",
|
|
2431
|
+
val: {
|
|
2432
|
+
sessionId: input.sessionId ?? null,
|
|
2433
|
+
configId: input.configId,
|
|
2434
|
+
value: JSON.stringify(input.value),
|
|
2435
|
+
},
|
|
2375
2436
|
});
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
await this._hydrateSessionState(liveSession).catch(() => { });
|
|
2379
|
-
}
|
|
2380
|
-
if (!response.error) {
|
|
2381
|
-
if (method === "session/set_mode" &&
|
|
2382
|
-
typeof params?.modeId === "string" &&
|
|
2383
|
-
session.modes) {
|
|
2384
|
-
session.modes = {
|
|
2385
|
-
...session.modes,
|
|
2386
|
-
currentModeId: params.modeId,
|
|
2387
|
-
};
|
|
2388
|
-
}
|
|
2389
|
-
if (method === "session/set_config_option" &&
|
|
2390
|
-
typeof params?.configId === "string" &&
|
|
2391
|
-
typeof params?.value === "string") {
|
|
2392
|
-
const nextValue = params.value;
|
|
2393
|
-
const updatedOption = session.configOptions.find((option) => option.id === params.configId);
|
|
2394
|
-
session.configOverrides.set(params.configId, nextValue);
|
|
2395
|
-
if (typeof updatedOption?.category === "string") {
|
|
2396
|
-
session.configOverrides.set(updatedOption.category, nextValue);
|
|
2397
|
-
}
|
|
2398
|
-
session.configOptions = session.configOptions.map((option) => option.id === params.configId
|
|
2399
|
-
? { ...option, currentValue: nextValue }
|
|
2400
|
-
: option);
|
|
2401
|
-
}
|
|
2437
|
+
if (response.tag !== "AcpSessionConfigResponse") {
|
|
2438
|
+
throw new Error(`unexpected setSessionConfigOption response: ${response.tag}`);
|
|
2402
2439
|
}
|
|
2403
|
-
return
|
|
2440
|
+
return {
|
|
2441
|
+
revision: safeWireU64(response.val.revision),
|
|
2442
|
+
options: JSON.parse(response.val.options),
|
|
2443
|
+
};
|
|
2404
2444
|
}
|
|
2405
|
-
async
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2445
|
+
async getSessionCapabilities(input) {
|
|
2446
|
+
const response = await this._sendAcpRequest({
|
|
2447
|
+
tag: "AcpGetSessionCapabilitiesRequest",
|
|
2448
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2449
|
+
});
|
|
2450
|
+
if (response.tag !== "AcpSessionCapabilitiesResponse") {
|
|
2451
|
+
throw new Error(`unexpected getSessionCapabilities response: ${response.tag}`);
|
|
2410
2452
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2453
|
+
return response.val.capabilities === null
|
|
2454
|
+
? null
|
|
2455
|
+
: normalizeSessionCapabilities(JSON.parse(response.val.capabilities));
|
|
2456
|
+
}
|
|
2457
|
+
async getSessionAgentInfo(input) {
|
|
2458
|
+
const response = await this._sendAcpRequest({
|
|
2459
|
+
tag: "AcpGetSessionAgentInfoRequest",
|
|
2460
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2414
2461
|
});
|
|
2415
|
-
|
|
2462
|
+
if (response.tag !== "AcpSessionAgentInfoResponse") {
|
|
2463
|
+
throw new Error(`unexpected getSessionAgentInfo response: ${response.tag}`);
|
|
2464
|
+
}
|
|
2465
|
+
return response.val.agentInfo === null
|
|
2466
|
+
? null
|
|
2467
|
+
: JSON.parse(response.val.agentInfo);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Dynamically link a software package into the RUNNING VM. The package's
|
|
2471
|
+
* `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
|
|
2472
|
+
* pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
|
|
2473
|
+
* writing into its staging dir is reflected live with no reboot. An `agent`
|
|
2474
|
+
* block registers the package for `openSession({ agent: name })`. Persists for the VM's
|
|
2475
|
+
* lifetime (and across a snapshot iff the volume persists).
|
|
2476
|
+
*/
|
|
2477
|
+
async linkSoftware(descriptor) {
|
|
2478
|
+
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
2479
|
+
// appends the package to its live host-backed staging dir; the commands
|
|
2480
|
+
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
2481
|
+
// duplicate command, surfaced here as a thrown error.
|
|
2482
|
+
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, descriptor);
|
|
2483
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
2484
|
+
this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
|
|
2485
|
+
command.name,
|
|
2486
|
+
command.guestPath,
|
|
2487
|
+
])));
|
|
2488
|
+
// Retain the linked package for runtime mount reconfigures:
|
|
2489
|
+
// `configure_vm` is replace-on-write, so a later `mountFs` that
|
|
2490
|
+
// resent only the boot packages would unproject this one.
|
|
2491
|
+
this.#kernel.registerLinkedPackage(descriptor);
|
|
2492
|
+
}
|
|
2493
|
+
// The client parses no manifests: an `agent` block in the linked package is
|
|
2494
|
+
// picked up by the sidecar (it owns the projected `/opt/agentos` and answers
|
|
2495
|
+
// openSession/listAgents from it). Nothing to record client-side.
|
|
2496
|
+
}
|
|
2497
|
+
async listSoftware() {
|
|
2498
|
+
return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
|
|
2416
2499
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2500
|
+
/**
|
|
2501
|
+
* Returns all registered agents with their installation status. Thin forwarder:
|
|
2502
|
+
* sends `AcpListAgentsRequest` and maps the response. The sidecar enumerates the
|
|
2503
|
+
* projected `/opt/agentos` packages (the client parses no manifests). Every such
|
|
2504
|
+
* agent is a package materialized into the VM, so `installed` is always `true`.
|
|
2505
|
+
*/
|
|
2506
|
+
async listAgents() {
|
|
2507
|
+
const response = await this._sendAcpRequest({
|
|
2508
|
+
tag: "AcpListAgentsRequest",
|
|
2509
|
+
val: { reserved: false },
|
|
2510
|
+
});
|
|
2511
|
+
if (response.tag !== "AcpListAgentsResponse") {
|
|
2512
|
+
throw new Error(`unexpected list_agents response: ${response.tag}`);
|
|
2513
|
+
}
|
|
2514
|
+
return response.val.agents.map((agent) => ({
|
|
2515
|
+
id: agent.id,
|
|
2516
|
+
installed: agent.installed,
|
|
2517
|
+
}));
|
|
2419
2518
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
if (!resolvers) {
|
|
2519
|
+
_recordAgentStderr(event) {
|
|
2520
|
+
if (!event.sessionId) {
|
|
2423
2521
|
return;
|
|
2424
2522
|
}
|
|
2425
|
-
this.
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
id: null,
|
|
2429
|
-
error: {
|
|
2430
|
-
code: -32_000,
|
|
2431
|
-
message: `Session closed: ${sessionId}`,
|
|
2432
|
-
},
|
|
2433
|
-
};
|
|
2434
|
-
for (const resolver of resolvers) {
|
|
2435
|
-
resolver.resolve(response);
|
|
2523
|
+
const handler = this._agentStderrHandler;
|
|
2524
|
+
if (!handler) {
|
|
2525
|
+
return;
|
|
2436
2526
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2527
|
+
try {
|
|
2528
|
+
handler({
|
|
2529
|
+
sessionId: event.sessionId,
|
|
2530
|
+
agentType: event.agentType,
|
|
2531
|
+
processId: event.processId,
|
|
2532
|
+
pid: null,
|
|
2533
|
+
chunk: new Uint8Array(event.chunk),
|
|
2534
|
+
});
|
|
2442
2535
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2536
|
+
catch (error) {
|
|
2537
|
+
console.error("AgentOS stderr handler failed", error);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
_recordAgentExit(event) {
|
|
2541
|
+
const publicEvent = {
|
|
2542
|
+
sessionId: event.sessionId,
|
|
2543
|
+
agentType: event.agentType,
|
|
2544
|
+
processId: event.processId,
|
|
2545
|
+
pid: event.pid,
|
|
2546
|
+
exitCode: event.exitCode,
|
|
2547
|
+
restart: event.restart,
|
|
2548
|
+
restartCount: event.restartCount,
|
|
2549
|
+
maxRestarts: event.maxRestarts,
|
|
2449
2550
|
};
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2551
|
+
const handler = this._agentExitHandler;
|
|
2552
|
+
if (handler) {
|
|
2553
|
+
try {
|
|
2554
|
+
handler(publicEvent);
|
|
2555
|
+
}
|
|
2556
|
+
catch (error) {
|
|
2557
|
+
console.error("AgentOS agent-exit handler failed", error);
|
|
2454
2558
|
}
|
|
2455
|
-
resolvers.delete(resolver);
|
|
2456
|
-
resolver.resolve(response);
|
|
2457
|
-
cancelledPrompt = true;
|
|
2458
2559
|
}
|
|
2459
|
-
|
|
2460
|
-
this.
|
|
2560
|
+
for (const key of ["*", event.sessionId]) {
|
|
2561
|
+
for (const subscription of this._agentExitHandlers.get(key) ?? []) {
|
|
2562
|
+
try {
|
|
2563
|
+
subscription(publicEvent);
|
|
2564
|
+
}
|
|
2565
|
+
catch (error) {
|
|
2566
|
+
console.error("AgentOS agent-exit subscription failed", error);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2461
2569
|
}
|
|
2462
|
-
return cancelledPrompt;
|
|
2463
2570
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2571
|
+
_handleSidecarEvent(event) {
|
|
2572
|
+
if (event.payload.type === "ext") {
|
|
2573
|
+
this._handleAcpExtEvent(event.payload.envelope);
|
|
2467
2574
|
return;
|
|
2468
2575
|
}
|
|
2469
|
-
|
|
2470
|
-
}
|
|
2471
|
-
_rejectPendingPermissionRepliesFromSession(session) {
|
|
2472
|
-
for (const [permissionId, pendingReply,] of session.pendingPermissionReplies) {
|
|
2473
|
-
clearTimeout(pendingReply.timer);
|
|
2474
|
-
pendingReply.reject(new Error(`Session closed before permission reply: ${permissionId}`));
|
|
2475
|
-
}
|
|
2476
|
-
session.pendingPermissionReplies.clear();
|
|
2477
|
-
}
|
|
2478
|
-
async _closeSessionInternal(sessionId) {
|
|
2479
|
-
const closing = this._sessionClosePromises.get(sessionId);
|
|
2480
|
-
if (closing) {
|
|
2481
|
-
return closing;
|
|
2482
|
-
}
|
|
2483
|
-
if (this._closedSessionIds.has(sessionId)) {
|
|
2576
|
+
if (event.payload.type !== "structured") {
|
|
2484
2577
|
return;
|
|
2485
2578
|
}
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
this._removeSession(sessionId);
|
|
2490
|
-
this._closedSessionIds.add(sessionId);
|
|
2491
|
-
const closePromise = this._sendAcpRequest({
|
|
2492
|
-
tag: "AcpCloseSessionRequest",
|
|
2493
|
-
val: { sessionId },
|
|
2494
|
-
})
|
|
2495
|
-
.then((response) => {
|
|
2496
|
-
if (response.tag !== "AcpSessionClosedResponse") {
|
|
2497
|
-
throw new Error(`unexpected response to AcpCloseSessionRequest: ${response.tag}`);
|
|
2498
|
-
}
|
|
2499
|
-
})
|
|
2500
|
-
.finally(() => {
|
|
2501
|
-
this._sessionClosePromises.delete(sessionId);
|
|
2502
|
-
});
|
|
2503
|
-
this._sessionClosePromises.set(sessionId, closePromise);
|
|
2504
|
-
await closePromise;
|
|
2579
|
+
if (event.payload.name === "limit_warning") {
|
|
2580
|
+
this._handleLimitWarning(event.payload.detail);
|
|
2581
|
+
}
|
|
2505
2582
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
val: { sessionId: session.sessionId },
|
|
2510
|
-
});
|
|
2511
|
-
if (response.tag !== "AcpSessionStateResponse") {
|
|
2512
|
-
throw new Error(`unexpected response to AcpGetSessionStateRequest: ${response.tag}`);
|
|
2513
|
-
}
|
|
2514
|
-
const state = sidecarSessionStateFromAcp(response.val);
|
|
2515
|
-
this._syncSessionState(session, state);
|
|
2516
|
-
}
|
|
2517
|
-
async createSession(agentType, options) {
|
|
2518
|
-
const config = this._resolveAgentConfig(agentType);
|
|
2519
|
-
if (!config) {
|
|
2520
|
-
throw new Error(`Unknown agent type: ${agentType}`);
|
|
2521
|
-
}
|
|
2522
|
-
// System-prompt assembly and injection (launch args / OPENCODE_CONTEXTPATHS) are owned by
|
|
2523
|
-
// the sidecar at AcpCreateSessionRequest. The host only forwards additionalInstructions /
|
|
2524
|
-
// skipOsInstructions plus the agent's static launch args and env.
|
|
2525
|
-
const launchArgs = [...(config.launchArgs ?? [])];
|
|
2526
|
-
let launchEnv = { ...config.defaultEnv, ...options?.env };
|
|
2527
|
-
const sessionCwd = options?.cwd ?? "/home/user";
|
|
2528
|
-
const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
|
|
2529
|
-
if ((agentType === "pi" || agentType === "pi-cli") &&
|
|
2530
|
-
!launchEnv.PI_ACP_PI_COMMAND) {
|
|
2531
|
-
launchEnv = {
|
|
2532
|
-
...launchEnv,
|
|
2533
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
2534
|
-
};
|
|
2583
|
+
_handleLimitWarning(detail) {
|
|
2584
|
+
if (!this._limitWarningHandler) {
|
|
2585
|
+
return;
|
|
2535
2586
|
}
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
agentType: String(agentType),
|
|
2540
|
-
runtime: AcpRuntimeKind.JavaScript,
|
|
2541
|
-
adapterEntrypoint,
|
|
2542
|
-
args: launchArgs,
|
|
2543
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2544
|
-
cwd: sessionCwd,
|
|
2545
|
-
mcpServers: JSON.stringify(options?.mcpServers ?? []),
|
|
2546
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2547
|
-
clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
|
|
2548
|
-
additionalInstructions: combineInstructions(options?.additionalInstructions, this._toolReference),
|
|
2549
|
-
skipOsInstructions: options?.skipOsInstructions ?? false,
|
|
2550
|
-
},
|
|
2551
|
-
});
|
|
2552
|
-
if (response.tag !== "AcpSessionCreatedResponse") {
|
|
2553
|
-
throw new Error(`unexpected create_session response: ${response.tag}`);
|
|
2554
|
-
}
|
|
2555
|
-
const created = sidecarSessionCreatedFromAcp(response.val);
|
|
2556
|
-
// The sessionId is chosen by the (untrusted/buggy) ACP adapter or sidecar. If it collides
|
|
2557
|
-
// with a live session already in `_sessions`, blindly overwriting the entry would orphan the
|
|
2558
|
-
// first session's event/permission handlers and pending permission replies. Fail closed:
|
|
2559
|
-
// reject the colliding create and leave the original session intact. (A previously-closed,
|
|
2560
|
-
// evicted id may still be re-used; only a live, non-closed entry is a collision.)
|
|
2561
|
-
const existing = this._sessions.get(created.sessionId);
|
|
2562
|
-
if (existing !== undefined && !existing.closed) {
|
|
2563
|
-
throw new Error(`session id collision: ${created.sessionId}`);
|
|
2564
|
-
}
|
|
2565
|
-
const initData = {
|
|
2566
|
-
modes: toSessionModes(created.modes) ?? undefined,
|
|
2567
|
-
configOptions: toSessionConfigOptions(created.configOptions),
|
|
2568
|
-
capabilities: toAgentCapabilities(created.agentCapabilities),
|
|
2569
|
-
agentInfo: toAgentInfo(created.agentInfo) ?? undefined,
|
|
2587
|
+
const toNumber = (value) => {
|
|
2588
|
+
const parsed = Number(value);
|
|
2589
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
2570
2590
|
};
|
|
2571
|
-
const session = sessionEntryFromInit(created.sessionId, String(agentType), initData);
|
|
2572
|
-
this._closedSessionIds.delete(created.sessionId);
|
|
2573
|
-
this._sessions.set(created.sessionId, session);
|
|
2574
2591
|
try {
|
|
2575
|
-
|
|
2592
|
+
this._limitWarningHandler({
|
|
2593
|
+
limit: detail.limit ?? "",
|
|
2594
|
+
category: detail.category ?? "",
|
|
2595
|
+
observed: toNumber(detail.observed),
|
|
2596
|
+
capacity: toNumber(detail.capacity),
|
|
2597
|
+
fillPercent: toNumber(detail.fillPercent),
|
|
2598
|
+
});
|
|
2576
2599
|
}
|
|
2577
2600
|
catch (error) {
|
|
2578
|
-
|
|
2579
|
-
throw error;
|
|
2601
|
+
console.error("AgentOS limit-warning handler failed", error);
|
|
2580
2602
|
}
|
|
2581
|
-
return { sessionId: created.sessionId };
|
|
2582
2603
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
* resolves the agent config + adapter entrypoint exactly as {@link createSession}
|
|
2587
|
-
* does, then forwards a single `AcpResumeSessionRequest` to the sidecar, which
|
|
2588
|
-
* owns the resume state machine (native `session/load` when the agent supports
|
|
2589
|
-
* it, else `session/new` + a transcript-continuation preamble). The returned
|
|
2590
|
-
* `sessionId` is the live id in this VM (equal to the requested id for native
|
|
2591
|
-
* loads, freshly assigned for the fallback); the caller remaps `external -> live`.
|
|
2592
|
-
* The new live session is registered + hydrated locally so subsequent prompts
|
|
2593
|
-
* route to it.
|
|
2594
|
-
*
|
|
2595
|
-
* Resume depends on a durable root; on a non-durable (default in-memory) root
|
|
2596
|
-
* there is no surviving store and the fallback tier always runs.
|
|
2597
|
-
*/
|
|
2598
|
-
async resumeSession(sessionId, agentType, options) {
|
|
2599
|
-
const config = this._resolveAgentConfig(agentType);
|
|
2600
|
-
if (!config) {
|
|
2601
|
-
throw new Error(`Unknown agent type: ${agentType}`);
|
|
2602
|
-
}
|
|
2603
|
-
const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
|
|
2604
|
-
let launchEnv = { ...config.defaultEnv, ...options?.env };
|
|
2605
|
-
const sessionCwd = options?.cwd ?? "/home/user";
|
|
2606
|
-
if ((agentType === "pi" || agentType === "pi-cli") &&
|
|
2607
|
-
!launchEnv.PI_ACP_PI_COMMAND) {
|
|
2608
|
-
launchEnv = {
|
|
2609
|
-
...launchEnv,
|
|
2610
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
2611
|
-
};
|
|
2604
|
+
_handleAcpExtEvent(envelope) {
|
|
2605
|
+
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
2606
|
+
return;
|
|
2612
2607
|
}
|
|
2613
|
-
// The resume wire request has no dedicated `adapterEntrypoint` field; carry
|
|
2614
|
-
// the resolved entrypoint through env under the sidecar's reserved key. The
|
|
2615
|
-
// sidecar reads it and strips it before launching the adapter.
|
|
2616
|
-
launchEnv = {
|
|
2617
|
-
...launchEnv,
|
|
2618
|
-
[RESUME_ADAPTER_ENTRYPOINT_ENV]: adapterEntrypoint,
|
|
2619
|
-
};
|
|
2620
|
-
const response = await this._sendAcpRequest({
|
|
2621
|
-
tag: "AcpResumeSessionRequest",
|
|
2622
|
-
val: {
|
|
2623
|
-
sessionId,
|
|
2624
|
-
agentType: String(agentType),
|
|
2625
|
-
transcriptPath: options?.transcriptPath ?? null,
|
|
2626
|
-
cwd: sessionCwd,
|
|
2627
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2628
|
-
},
|
|
2629
|
-
});
|
|
2630
|
-
if (response.tag !== "AcpSessionResumedResponse") {
|
|
2631
|
-
throw new Error(`unexpected resume_session response: ${response.tag}`);
|
|
2632
|
-
}
|
|
2633
|
-
const { sessionId: liveSessionId, mode } = response.val;
|
|
2634
|
-
// Register + hydrate the live session so subsequent prompts route to it.
|
|
2635
|
-
const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
|
|
2636
|
-
this._closedSessionIds.delete(liveSessionId);
|
|
2637
|
-
this._sessions.set(liveSessionId, session);
|
|
2638
2608
|
try {
|
|
2639
|
-
|
|
2609
|
+
const event = decodeAcpEvent(envelope.payload);
|
|
2610
|
+
switch (event.tag) {
|
|
2611
|
+
case "AcpDurableSessionEvent": {
|
|
2612
|
+
this._emitDurableSessionEvent(decodeDurableSessionEvent(event.val));
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
case "AcpEphemeralSessionUpdateEvent": {
|
|
2616
|
+
const update = JSON.parse(event.val.update);
|
|
2617
|
+
const { sessionUpdate: type, ...payload } = update;
|
|
2618
|
+
this._emitDurableSessionEvent({
|
|
2619
|
+
durability: "ephemeral",
|
|
2620
|
+
type,
|
|
2621
|
+
sessionId: event.val.sessionId,
|
|
2622
|
+
afterSequence: safeWireU64(event.val.afterSequence),
|
|
2623
|
+
...payload,
|
|
2624
|
+
});
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
case "AcpSessionEvent":
|
|
2628
|
+
return;
|
|
2629
|
+
case "AcpAgentStderrEvent": {
|
|
2630
|
+
this._recordAgentStderr(event.val);
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
case "AcpAgentExitedEvent": {
|
|
2634
|
+
this._recordAgentExit(event.val);
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2640
2638
|
}
|
|
2641
2639
|
catch (error) {
|
|
2642
|
-
|
|
2643
|
-
throw error;
|
|
2640
|
+
console.error("AgentOS failed to decode an ACP sidecar event", error);
|
|
2644
2641
|
}
|
|
2645
|
-
return { sessionId: liveSessionId, mode };
|
|
2646
2642
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
* the relevant mount.
|
|
2652
|
-
*/
|
|
2653
|
-
_resolveAdapterBin(adapterPackage) {
|
|
2654
|
-
return this._resolvePackageBin(adapterPackage);
|
|
2655
|
-
}
|
|
2656
|
-
/**
|
|
2657
|
-
* Parent of the host directory backing the `/root/node_modules` mount, if
|
|
2658
|
-
* the caller supplied one (e.g. via `nodeModulesMount(...)`). Used as the
|
|
2659
|
-
* `resolvePackageDir` start dir for adapter/agent package resolution when no
|
|
2660
|
-
* software root matches. `nodeModulesMount` mounts `<dir>/node_modules`, so
|
|
2661
|
-
* the start dir is `<dir>` (one level above the node_modules tree).
|
|
2662
|
-
*/
|
|
2663
|
-
_nodeModulesHostRoot() {
|
|
2664
|
-
for (const mount of this._hostMounts) {
|
|
2665
|
-
if (mount.vmPath === "/root/node_modules") {
|
|
2666
|
-
return dirname(mount.hostPath);
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
return null;
|
|
2670
|
-
}
|
|
2671
|
-
_resolvePackageBin(packageName, binName) {
|
|
2672
|
-
const vmPrefix = `/root/node_modules/${packageName}`;
|
|
2673
|
-
let hostPkgJsonPath = null;
|
|
2674
|
-
for (const root of this._softwareRoots) {
|
|
2675
|
-
if (root.vmPath === vmPrefix) {
|
|
2676
|
-
hostPkgJsonPath = join(root.hostPath, "package.json");
|
|
2677
|
-
break;
|
|
2643
|
+
_emitDurableSessionEvent(entry) {
|
|
2644
|
+
for (const handler of this._durableSessionEventHandlers.get(entry.sessionId) ?? []) {
|
|
2645
|
+
try {
|
|
2646
|
+
handler(entry);
|
|
2678
2647
|
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
if (!hostPkgJsonPath) {
|
|
2682
|
-
const nodeModulesRoot = this._nodeModulesHostRoot();
|
|
2683
|
-
if (!nodeModulesRoot) {
|
|
2684
|
-
throw new Error(`Cannot resolve package "${packageName}": no software root provides it and ` +
|
|
2685
|
-
`no /root/node_modules mount was supplied. Pass ` +
|
|
2686
|
-
`mounts: [nodeModulesMount("<host>/node_modules")] to AgentOs.create().`);
|
|
2648
|
+
catch (error) {
|
|
2649
|
+
console.error("AgentOS session event handler failed", error);
|
|
2687
2650
|
}
|
|
2688
|
-
hostPkgJsonPath = join(resolvePackageDir(nodeModulesRoot, packageName), "package.json");
|
|
2689
|
-
}
|
|
2690
|
-
const pkg = JSON.parse(readFileSync(hostPkgJsonPath, "utf-8"));
|
|
2691
|
-
let binEntry;
|
|
2692
|
-
if (typeof pkg.bin === "string") {
|
|
2693
|
-
binEntry = pkg.bin;
|
|
2694
2651
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2652
|
+
}
|
|
2653
|
+
async _sendAcpRequest(request) {
|
|
2654
|
+
const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
|
|
2655
|
+
namespace: ACP_EXTENSION_NAMESPACE,
|
|
2656
|
+
payload: encodeAcpRequest(request),
|
|
2657
|
+
});
|
|
2658
|
+
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
2659
|
+
throw new Error(`unexpected ACP Ext namespace: ${envelope.namespace}`);
|
|
2700
2660
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2661
|
+
const response = decodeAcpResponse(envelope.payload);
|
|
2662
|
+
if (response.tag === "AcpErrorResponse") {
|
|
2663
|
+
const error = new Error(response.val.message);
|
|
2664
|
+
error.code = response.val.code;
|
|
2665
|
+
throw error;
|
|
2703
2666
|
}
|
|
2704
|
-
return
|
|
2667
|
+
return response;
|
|
2705
2668
|
}
|
|
2706
2669
|
_installSidecarRequestHandler() {
|
|
2707
2670
|
const context = {
|
|
2708
|
-
|
|
2709
|
-
|
|
2671
|
+
bindings: this._bindings,
|
|
2672
|
+
bindingMap: buildBindingMap(this._bindings),
|
|
2710
2673
|
permissions: this._permissions,
|
|
2711
2674
|
readFile: (path) => this.readFile(path),
|
|
2712
2675
|
};
|
|
@@ -2715,10 +2678,8 @@ export class AgentOs {
|
|
|
2715
2678
|
case "host_callback":
|
|
2716
2679
|
return handleHostCallback(request, context);
|
|
2717
2680
|
case "js_bridge_call":
|
|
2718
|
-
return
|
|
2719
|
-
|
|
2720
|
-
call_id: request.payload.call_id,
|
|
2721
|
-
error: `unsupported sidecar request type: ${request.payload.type}`,
|
|
2681
|
+
return handleJsBridgeCall(request.payload, {
|
|
2682
|
+
filesystem: this.#kernel.vfs,
|
|
2722
2683
|
});
|
|
2723
2684
|
case "ext":
|
|
2724
2685
|
return this._handleAcpExtSidecarRequest(request.payload.envelope);
|
|
@@ -2737,25 +2698,6 @@ export class AgentOs {
|
|
|
2737
2698
|
}
|
|
2738
2699
|
const callback = decodeAcpCallback(envelope.payload);
|
|
2739
2700
|
switch (callback.tag) {
|
|
2740
|
-
case "AcpPermissionCallback": {
|
|
2741
|
-
const reply = await this._handleAcpPermissionCallback(callback.val.sessionId, callback.val.permissionId, {
|
|
2742
|
-
...toRecord(JSON.parse(callback.val.params)),
|
|
2743
|
-
_acpMethod: ACP_PERMISSION_METHOD,
|
|
2744
|
-
});
|
|
2745
|
-
return {
|
|
2746
|
-
type: "ext_result",
|
|
2747
|
-
envelope: {
|
|
2748
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
2749
|
-
payload: encodeAcpCallbackResponse({
|
|
2750
|
-
tag: "AcpPermissionCallbackResponse",
|
|
2751
|
-
val: {
|
|
2752
|
-
permissionId: callback.val.permissionId,
|
|
2753
|
-
reply,
|
|
2754
|
-
},
|
|
2755
|
-
}),
|
|
2756
|
-
},
|
|
2757
|
-
};
|
|
2758
|
-
}
|
|
2759
2701
|
case "AcpHostRequestCallback": {
|
|
2760
2702
|
const response = await this._dispatchAcpSidecarRequest(toJsonRpcRequest(JSON.parse(callback.val.request)));
|
|
2761
2703
|
return {
|
|
@@ -2807,8 +2749,6 @@ export class AgentOs {
|
|
|
2807
2749
|
async _handleSupportedAcpSidecarRequest(request) {
|
|
2808
2750
|
const params = this._acpParams(request);
|
|
2809
2751
|
switch (request.method) {
|
|
2810
|
-
case ACP_PERMISSION_METHOD:
|
|
2811
|
-
return this._handleAcpPermissionRequest(request, params);
|
|
2812
2752
|
case "fs/read":
|
|
2813
2753
|
case "fs/read_text_file":
|
|
2814
2754
|
return this._handleAcpReadFile(params);
|
|
@@ -2841,95 +2781,6 @@ export class AgentOs {
|
|
|
2841
2781
|
});
|
|
2842
2782
|
}
|
|
2843
2783
|
}
|
|
2844
|
-
_normalizeAcpPermissionOptionId(options, reply) {
|
|
2845
|
-
const optionTargets = reply === "always"
|
|
2846
|
-
? {
|
|
2847
|
-
optionIds: new Set(["always", "allow_always"]),
|
|
2848
|
-
kinds: new Set(["allow_always"]),
|
|
2849
|
-
}
|
|
2850
|
-
: reply === "once"
|
|
2851
|
-
? {
|
|
2852
|
-
optionIds: new Set(["once", "allow_once"]),
|
|
2853
|
-
kinds: new Set(["allow_once"]),
|
|
2854
|
-
}
|
|
2855
|
-
: {
|
|
2856
|
-
optionIds: new Set(["reject", "reject_once"]),
|
|
2857
|
-
kinds: new Set(["reject_once"]),
|
|
2858
|
-
};
|
|
2859
|
-
const matched = options?.find((option) => {
|
|
2860
|
-
const optionId = typeof option.optionId === "string" ? option.optionId : undefined;
|
|
2861
|
-
const kind = typeof option.kind === "string" ? option.kind : undefined;
|
|
2862
|
-
return ((optionId !== undefined && optionTargets.optionIds.has(optionId)) ||
|
|
2863
|
-
(kind !== undefined && optionTargets.kinds.has(kind)));
|
|
2864
|
-
});
|
|
2865
|
-
if (matched && typeof matched.optionId === "string") {
|
|
2866
|
-
return matched.optionId;
|
|
2867
|
-
}
|
|
2868
|
-
if (reply === "always") {
|
|
2869
|
-
return "allow_always";
|
|
2870
|
-
}
|
|
2871
|
-
if (reply === "once") {
|
|
2872
|
-
return "allow_once";
|
|
2873
|
-
}
|
|
2874
|
-
return "reject_once";
|
|
2875
|
-
}
|
|
2876
|
-
_buildAcpPermissionResult(reply, params) {
|
|
2877
|
-
const options = Array.isArray(params.options)
|
|
2878
|
-
? params.options.filter((option) => typeof option === "object" && option !== null)
|
|
2879
|
-
: undefined;
|
|
2880
|
-
const optionId = this._normalizeAcpPermissionOptionId(options, reply);
|
|
2881
|
-
return {
|
|
2882
|
-
outcome: optionId
|
|
2883
|
-
? {
|
|
2884
|
-
outcome: "selected",
|
|
2885
|
-
optionId,
|
|
2886
|
-
}
|
|
2887
|
-
: {
|
|
2888
|
-
outcome: "cancelled",
|
|
2889
|
-
},
|
|
2890
|
-
};
|
|
2891
|
-
}
|
|
2892
|
-
async _handleAcpPermissionRequest(request, params) {
|
|
2893
|
-
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
2894
|
-
if (!sessionId) {
|
|
2895
|
-
throw new AcpDispatchError(-32602, `${ACP_PERMISSION_METHOD} requires a sessionId`);
|
|
2896
|
-
}
|
|
2897
|
-
const session = this._sessions.get(sessionId);
|
|
2898
|
-
if (!session) {
|
|
2899
|
-
throw new AcpDispatchError(-32602, `Session not found: ${sessionId}`);
|
|
2900
|
-
}
|
|
2901
|
-
const permissionId = String(request.id);
|
|
2902
|
-
const permissionParams = {
|
|
2903
|
-
...params,
|
|
2904
|
-
permissionId,
|
|
2905
|
-
_acpMethod: request.method,
|
|
2906
|
-
};
|
|
2907
|
-
if (session.permissionHandlers.size === 0) {
|
|
2908
|
-
return this._buildAcpPermissionResult("reject", permissionParams);
|
|
2909
|
-
}
|
|
2910
|
-
const reply = await new Promise((resolve, reject) => {
|
|
2911
|
-
const timer = setTimeout(() => {
|
|
2912
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
2913
|
-
reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
|
|
2914
|
-
}, 120_000);
|
|
2915
|
-
session.pendingPermissionReplies.set(permissionId, {
|
|
2916
|
-
resolve,
|
|
2917
|
-
reject,
|
|
2918
|
-
timer,
|
|
2919
|
-
});
|
|
2920
|
-
const permissionRequest = {
|
|
2921
|
-
permissionId,
|
|
2922
|
-
description: typeof permissionParams["description"] === "string"
|
|
2923
|
-
? permissionParams["description"]
|
|
2924
|
-
: undefined,
|
|
2925
|
-
params: permissionParams,
|
|
2926
|
-
};
|
|
2927
|
-
for (const handler of session.permissionHandlers) {
|
|
2928
|
-
handler(permissionRequest);
|
|
2929
|
-
}
|
|
2930
|
-
});
|
|
2931
|
-
return this._buildAcpPermissionResult(reply, permissionParams);
|
|
2932
|
-
}
|
|
2933
2784
|
_acpParams(request) {
|
|
2934
2785
|
if (!request.params) {
|
|
2935
2786
|
return {};
|
|
@@ -3102,9 +2953,6 @@ export class AgentOs {
|
|
|
3102
2953
|
...(cwd ? { cwd } : {}),
|
|
3103
2954
|
...(cols !== undefined ? { cols: Math.trunc(cols) } : {}),
|
|
3104
2955
|
...(rows !== undefined ? { rows: Math.trunc(rows) } : {}),
|
|
3105
|
-
onStderr: (data) => {
|
|
3106
|
-
this._appendAcpTerminalOutput(terminal, data);
|
|
3107
|
-
},
|
|
3108
2956
|
}),
|
|
3109
2957
|
output: "",
|
|
3110
2958
|
truncated: false,
|
|
@@ -3181,197 +3029,38 @@ export class AgentOs {
|
|
|
3181
3029
|
terminal.handle.resize(Math.trunc(cols), Math.trunc(rows));
|
|
3182
3030
|
return null;
|
|
3183
3031
|
}
|
|
3184
|
-
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
if (
|
|
3190
|
-
|
|
3191
|
-
}
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
resolve,
|
|
3200
|
-
reject,
|
|
3201
|
-
timer,
|
|
3202
|
-
});
|
|
3203
|
-
const permissionRequest = {
|
|
3204
|
-
permissionId,
|
|
3205
|
-
description: typeof params.description === "string"
|
|
3206
|
-
? params.description
|
|
3207
|
-
: undefined,
|
|
3208
|
-
params,
|
|
3209
|
-
};
|
|
3210
|
-
for (const handler of session.permissionHandlers) {
|
|
3211
|
-
handler(permissionRequest);
|
|
3212
|
-
}
|
|
3213
|
-
});
|
|
3214
|
-
}
|
|
3215
|
-
catch {
|
|
3216
|
-
return "reject";
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Resolve an agent config by ID. Package-provided configs take
|
|
3221
|
-
* precedence over the hardcoded AGENT_CONFIGS.
|
|
3222
|
-
*/
|
|
3223
|
-
_resolveAgentConfig(agentType) {
|
|
3224
|
-
return (this._softwareAgentConfigs.get(agentType) ??
|
|
3225
|
-
AGENT_CONFIGS[agentType]);
|
|
3226
|
-
}
|
|
3227
|
-
/**
|
|
3228
|
-
* Gracefully destroy a session: cancel any pending work, close the client,
|
|
3229
|
-
* and remove from tracking. Unlike close() which is abrupt, this attempts
|
|
3230
|
-
* a graceful shutdown sequence.
|
|
3231
|
-
*/
|
|
3232
|
-
async destroySession(sessionId) {
|
|
3233
|
-
this._requireSession(sessionId);
|
|
3234
|
-
try {
|
|
3235
|
-
await this.cancelSession(sessionId);
|
|
3236
|
-
}
|
|
3237
|
-
catch {
|
|
3238
|
-
// Ignore cancellation failures during teardown.
|
|
3239
|
-
}
|
|
3240
|
-
await this._closeSessionInternal(sessionId);
|
|
3241
|
-
}
|
|
3242
|
-
// ── Flat session API (ID-based) ───────────────────────────────
|
|
3243
|
-
async prompt(sessionId, text) {
|
|
3244
|
-
const session = this._requireSession(sessionId);
|
|
3245
|
-
let agentText = "";
|
|
3246
|
-
const handler = (event) => {
|
|
3247
|
-
const params = toRecord(event.params);
|
|
3248
|
-
const update = toRecord(params.update);
|
|
3249
|
-
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
3250
|
-
const content = toRecord(update.content);
|
|
3251
|
-
if (typeof content.text === "string") {
|
|
3252
|
-
agentText += content.text;
|
|
3253
|
-
}
|
|
3032
|
+
onSessionEvent(sessionIdOrHandler, maybeHandler) {
|
|
3033
|
+
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "main";
|
|
3034
|
+
const handler = typeof sessionIdOrHandler === "function"
|
|
3035
|
+
? sessionIdOrHandler
|
|
3036
|
+
: maybeHandler;
|
|
3037
|
+
if (!handler) {
|
|
3038
|
+
throw new TypeError("onSessionEvent requires a handler");
|
|
3039
|
+
}
|
|
3040
|
+
const handlers = this._durableSessionEventHandlers.get(sessionId) ?? new Set();
|
|
3041
|
+
handlers.add(handler);
|
|
3042
|
+
this._durableSessionEventHandlers.set(sessionId, handlers);
|
|
3043
|
+
return () => {
|
|
3044
|
+
handlers.delete(handler);
|
|
3045
|
+
if (handlers.size === 0) {
|
|
3046
|
+
this._durableSessionEventHandlers.delete(sessionId);
|
|
3254
3047
|
}
|
|
3255
3048
|
};
|
|
3256
|
-
const unsubscribe = this._subscribeSessionEvents(session, handler);
|
|
3257
|
-
try {
|
|
3258
|
-
const response = await this._sendSessionRequest(sessionId, "session/prompt", {
|
|
3259
|
-
prompt: [{ type: "text", text }],
|
|
3260
|
-
});
|
|
3261
|
-
return { response, text: agentText };
|
|
3262
|
-
}
|
|
3263
|
-
finally {
|
|
3264
|
-
unsubscribe();
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
/** Cancel ongoing agent work for a session. */
|
|
3268
|
-
async cancelSession(sessionId) {
|
|
3269
|
-
this._requireSession(sessionId);
|
|
3270
|
-
const cancelledPendingPrompt = this._cancelPendingPromptRequests(sessionId);
|
|
3271
|
-
if (cancelledPendingPrompt) {
|
|
3272
|
-
// Session control requests share the same framed sidecar transport as an
|
|
3273
|
-
// in-flight prompt request. If the adapter is blocked in prompt I/O, a
|
|
3274
|
-
// synchronous cancel RPC can wedge behind that prompt until the transport
|
|
3275
|
-
// timeout fires. Resolve the local prompt immediately, then let the sidecar
|
|
3276
|
-
// cancellation continue in the background as best effort.
|
|
3277
|
-
void this._sendSessionRequest(sessionId, "session/cancel").catch(() => { });
|
|
3278
|
-
return {
|
|
3279
|
-
jsonrpc: "2.0",
|
|
3280
|
-
id: null,
|
|
3281
|
-
result: {
|
|
3282
|
-
cancelled: true,
|
|
3283
|
-
requested: true,
|
|
3284
|
-
via: "prompt-fallback",
|
|
3285
|
-
},
|
|
3286
|
-
};
|
|
3287
|
-
}
|
|
3288
|
-
const response = await this._sendSessionRequest(sessionId, "session/cancel");
|
|
3289
|
-
if (response.error?.code === -32601) {
|
|
3290
|
-
return {
|
|
3291
|
-
jsonrpc: "2.0",
|
|
3292
|
-
id: null,
|
|
3293
|
-
result: {
|
|
3294
|
-
cancelled: false,
|
|
3295
|
-
requested: true,
|
|
3296
|
-
via: "unsupported",
|
|
3297
|
-
},
|
|
3298
|
-
};
|
|
3299
|
-
}
|
|
3300
|
-
return response;
|
|
3301
|
-
}
|
|
3302
|
-
closeSession(sessionId) {
|
|
3303
|
-
if (!this._sessions.has(sessionId) &&
|
|
3304
|
-
!this._closedSessionIds.has(sessionId) &&
|
|
3305
|
-
!this._sessionClosePromises.has(sessionId)) {
|
|
3306
|
-
throw new Error(`Session not found: ${sessionId}`);
|
|
3307
|
-
}
|
|
3308
|
-
const closePromise = this._closeSessionInternal(sessionId);
|
|
3309
|
-
// `closeSession()` is intentionally fire-and-forget; suppress unhandled
|
|
3310
|
-
// rejections here and let tracked close promises surface errors to any
|
|
3311
|
-
// internal/test callers awaiting `_sessionClosePromises`.
|
|
3312
|
-
void closePromise.catch(() => { });
|
|
3313
|
-
}
|
|
3314
|
-
async respondPermission(sessionId, permissionId, reply) {
|
|
3315
|
-
const session = this._requireSession(sessionId);
|
|
3316
|
-
const pendingReply = session.pendingPermissionReplies.get(permissionId);
|
|
3317
|
-
if (pendingReply) {
|
|
3318
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
3319
|
-
clearTimeout(pendingReply.timer);
|
|
3320
|
-
pendingReply.resolve(reply);
|
|
3321
|
-
return {
|
|
3322
|
-
jsonrpc: "2.0",
|
|
3323
|
-
id: null,
|
|
3324
|
-
result: {
|
|
3325
|
-
permissionId,
|
|
3326
|
-
reply,
|
|
3327
|
-
via: "sidecar-request",
|
|
3328
|
-
},
|
|
3329
|
-
};
|
|
3330
|
-
}
|
|
3331
|
-
return this._sendSessionRequest(sessionId, LEGACY_PERMISSION_METHOD, {
|
|
3332
|
-
permissionId,
|
|
3333
|
-
reply,
|
|
3334
|
-
});
|
|
3335
|
-
}
|
|
3336
|
-
async setSessionMode(sessionId, modeId) {
|
|
3337
|
-
return this._sendSessionRequest(sessionId, "session/set_mode", {
|
|
3338
|
-
modeId,
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
getSessionModes(sessionId) {
|
|
3342
|
-
return this._requireSession(sessionId).modes;
|
|
3343
|
-
}
|
|
3344
|
-
async setSessionModel(sessionId, model) {
|
|
3345
|
-
return this._setSessionConfigByCategory(sessionId, "model", model);
|
|
3346
|
-
}
|
|
3347
|
-
async setSessionThoughtLevel(sessionId, level) {
|
|
3348
|
-
return this._setSessionConfigByCategory(sessionId, "thought_level", level);
|
|
3349
|
-
}
|
|
3350
|
-
getSessionConfigOptions(sessionId) {
|
|
3351
|
-
return [...this._requireSession(sessionId).configOptions];
|
|
3352
|
-
}
|
|
3353
|
-
getSessionCapabilities(sessionId) {
|
|
3354
|
-
const caps = this._requireSession(sessionId).capabilities;
|
|
3355
|
-
return Object.keys(caps).length > 0 ? caps : null;
|
|
3356
|
-
}
|
|
3357
|
-
getSessionAgentInfo(sessionId) {
|
|
3358
|
-
return this._requireSession(sessionId).agentInfo;
|
|
3359
3049
|
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
const
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
onPermissionRequest(sessionId, handler) {
|
|
3371
|
-
const session = this._requireSession(sessionId);
|
|
3372
|
-
session.permissionHandlers.add(handler);
|
|
3050
|
+
onAgentExit(sessionIdOrHandler, maybeHandler) {
|
|
3051
|
+
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "*";
|
|
3052
|
+
const handler = typeof sessionIdOrHandler === "function"
|
|
3053
|
+
? sessionIdOrHandler
|
|
3054
|
+
: maybeHandler;
|
|
3055
|
+
if (!handler)
|
|
3056
|
+
throw new TypeError("onAgentExit requires a handler");
|
|
3057
|
+
const handlers = this._agentExitHandlers.get(sessionId) ?? new Set();
|
|
3058
|
+
handlers.add(handler);
|
|
3059
|
+
this._agentExitHandlers.set(sessionId, handlers);
|
|
3373
3060
|
return () => {
|
|
3374
|
-
|
|
3061
|
+
handlers.delete(handler);
|
|
3062
|
+
if (handlers.size === 0)
|
|
3063
|
+
this._agentExitHandlers.delete(sessionId);
|
|
3375
3064
|
};
|
|
3376
3065
|
}
|
|
3377
3066
|
// ── Cron ────────────────────────────────────────────────────
|
|
@@ -3393,9 +3082,6 @@ export class AgentOs {
|
|
|
3393
3082
|
}
|
|
3394
3083
|
async dispose() {
|
|
3395
3084
|
this._cronManager.dispose();
|
|
3396
|
-
for (const sessionId of [...this._sessions.keys()]) {
|
|
3397
|
-
await this._closeSessionInternal(sessionId).catch(() => { });
|
|
3398
|
-
}
|
|
3399
3085
|
for (const [id, entry] of this._shells) {
|
|
3400
3086
|
entry.handle.kill();
|
|
3401
3087
|
}
|
|
@@ -3407,6 +3093,7 @@ export class AgentOs {
|
|
|
3407
3093
|
terminalExitPromises.push(terminal.waitPromise.then(() => undefined, () => undefined));
|
|
3408
3094
|
}
|
|
3409
3095
|
this._acpTerminals.clear();
|
|
3096
|
+
this._processes.clear();
|
|
3410
3097
|
await waitForTrackedExitPromises([...shellExitPromises, ...terminalExitPromises], SHELL_DISPOSE_TIMEOUT_MS);
|
|
3411
3098
|
this._disposeSidecarEventListener();
|
|
3412
3099
|
const sidecarLease = this._sidecarLease;
|
|
@@ -3436,6 +3123,173 @@ function resolveAgentOsSidecar(config) {
|
|
|
3436
3123
|
}
|
|
3437
3124
|
const sidecarStates = new WeakMap();
|
|
3438
3125
|
const sharedSidecars = new Map();
|
|
3126
|
+
let sidecarProcessExitHookInstalled = false;
|
|
3127
|
+
/**
|
|
3128
|
+
* Install a one-time, synchronous `process.on("exit")` hook that SIGKILLs any
|
|
3129
|
+
* pooled shared sidecar child. Once a one-shot host process is allowed to exit
|
|
3130
|
+
* (its sidecar handles are unref'd at 0 leases), this reaps the sidecar
|
|
3131
|
+
* immediately instead of waiting for its stdin-EOF grace window — no orphan, no
|
|
3132
|
+
* delay. We deliberately do NOT install SIGINT/SIGTERM handlers: a library
|
|
3133
|
+
* should not hijack the host's signal handling. SIGINT still reaches the sidecar
|
|
3134
|
+
* via the process group, and SIGTERM-driven exit still closes its stdin.
|
|
3135
|
+
*/
|
|
3136
|
+
function ensureSidecarProcessExitCleanup() {
|
|
3137
|
+
if (sidecarProcessExitHookInstalled)
|
|
3138
|
+
return;
|
|
3139
|
+
sidecarProcessExitHookInstalled = true;
|
|
3140
|
+
process.on("exit", () => {
|
|
3141
|
+
for (const sidecar of sharedSidecars.values()) {
|
|
3142
|
+
try {
|
|
3143
|
+
sidecarStates.get(sidecar)?.sharedChild?.kill?.("SIGKILL");
|
|
3144
|
+
}
|
|
3145
|
+
catch {
|
|
3146
|
+
// best-effort reap; the process is exiting regardless
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
function sidecarChildHandle(client) {
|
|
3152
|
+
// SidecarProcess -> StdioSidecarProtocolClient.child (the spawned ChildProcess).
|
|
3153
|
+
const protocolClient = client?.protocolClient;
|
|
3154
|
+
return protocolClient?.child ?? undefined;
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Apply the current hold state to the shared sidecar's child + stdio: ref them
|
|
3158
|
+
* while ≥1 hold is live so in-flight VM work keeps the host process alive; unref
|
|
3159
|
+
* them at 0 so a one-shot script exits on its own after `dispose()`. The sidecar
|
|
3160
|
+
* process itself stays running (reusable) and self-exits on stdin EOF when the
|
|
3161
|
+
* host finally goes away. Best-effort: never let ref/unref break VM lifecycle.
|
|
3162
|
+
*/
|
|
3163
|
+
function applySharedSidecarHold(state) {
|
|
3164
|
+
const child = state.sharedChild;
|
|
3165
|
+
if (!child)
|
|
3166
|
+
return;
|
|
3167
|
+
const hold = (state.eventLoopHolds ?? 0) > 0;
|
|
3168
|
+
// Include the complete stdio array, not just the named fd0-fd2 aliases.
|
|
3169
|
+
// Protocol v8 carries responses over fd3; leaving that control socket
|
|
3170
|
+
// referenced pins Node's event loop after the final VM lease is disposed.
|
|
3171
|
+
const handles = new Set([
|
|
3172
|
+
child,
|
|
3173
|
+
child.stdin,
|
|
3174
|
+
child.stdout,
|
|
3175
|
+
child.stderr,
|
|
3176
|
+
...(child.stdio ?? []),
|
|
3177
|
+
]);
|
|
3178
|
+
for (const handle of handles) {
|
|
3179
|
+
if (!handle)
|
|
3180
|
+
continue;
|
|
3181
|
+
try {
|
|
3182
|
+
if (hold)
|
|
3183
|
+
handle.ref?.();
|
|
3184
|
+
else
|
|
3185
|
+
handle.unref?.();
|
|
3186
|
+
}
|
|
3187
|
+
catch {
|
|
3188
|
+
// ref/unref is an optimization, not correctness-critical
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
/**
|
|
3193
|
+
* Take a hold for the entire create→use→dispose lifetime of one VM lease. Taken
|
|
3194
|
+
* BEFORE VM creation starts (not when the lease lands in `activeLeases`) so a VM
|
|
3195
|
+
* that is still mid-creation keeps the sidecar ref'd and a concurrent dispose
|
|
3196
|
+
* cannot unref it out from under the in-flight create.
|
|
3197
|
+
*/
|
|
3198
|
+
function acquireSharedSidecarHold(state) {
|
|
3199
|
+
state.eventLoopHolds = (state.eventLoopHolds ?? 0) + 1;
|
|
3200
|
+
if (state.eventLoopHolds === 1)
|
|
3201
|
+
applySharedSidecarHold(state);
|
|
3202
|
+
}
|
|
3203
|
+
/** Release a hold taken by {@link acquireSharedSidecarHold}; unref at 0. */
|
|
3204
|
+
function releaseSharedSidecarHold(state) {
|
|
3205
|
+
const current = state.eventLoopHolds ?? 0;
|
|
3206
|
+
if (current <= 0) {
|
|
3207
|
+
// The `holdReleased` guard makes each lease release exactly once, so this
|
|
3208
|
+
// should be unreachable. Warn rather than silently floor, per the repo's
|
|
3209
|
+
// no-silent-masking rule, so an accounting bug surfaces instead of hiding.
|
|
3210
|
+
state.eventLoopHolds = 0;
|
|
3211
|
+
console.warn("[agentos] shared sidecar event-loop hold released more than acquired");
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
state.eventLoopHolds = current - 1;
|
|
3215
|
+
if (state.eventLoopHolds === 0)
|
|
3216
|
+
applySharedSidecarHold(state);
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Spawn-once accessor for a sidecar handle's shared native process. Concurrent
|
|
3220
|
+
* callers await the same promise, so one `AgentOsSidecar` maps to exactly one
|
|
3221
|
+
* `agent-os-sidecar` OS process for its whole lifetime.
|
|
3222
|
+
*/
|
|
3223
|
+
function ensureSharedSidecarNativeProcess(sidecar) {
|
|
3224
|
+
const state = getSidecarState(sidecar);
|
|
3225
|
+
if (!state.nativeProcess) {
|
|
3226
|
+
ensureSidecarProcessExitCleanup();
|
|
3227
|
+
state.nativeProcess = (async () => {
|
|
3228
|
+
const client = SidecarProcess.spawn({
|
|
3229
|
+
cwd: REPO_ROOT,
|
|
3230
|
+
command: ensureNativeSidecarBinary(),
|
|
3231
|
+
args: [],
|
|
3232
|
+
});
|
|
3233
|
+
// Track the child immediately — BEFORE the handshake await — so a
|
|
3234
|
+
// failed `authenticateAndOpenSession()` can still reap it (otherwise
|
|
3235
|
+
// the spawned child is untracked, unreapable, and pins the loop).
|
|
3236
|
+
state.sharedChild = sidecarChildHandle(client);
|
|
3237
|
+
if (!state.sharedChild) {
|
|
3238
|
+
// We reached into @rivet-dev/agentos-runtime-core internals to get the child for
|
|
3239
|
+
// idle-unref. If that shape ever changes this returns undefined and
|
|
3240
|
+
// the optimization silently stops working (one-shot scripts would
|
|
3241
|
+
// hang again). Make it loud rather than a silent regression.
|
|
3242
|
+
console.warn("[agentos] could not resolve the shared sidecar child handle; " +
|
|
3243
|
+
"standalone scripts may not exit cleanly after dispose(). " +
|
|
3244
|
+
"This usually means @rivet-dev/agentos-runtime-core internals changed.");
|
|
3245
|
+
}
|
|
3246
|
+
// Apply the current hold state to the just-spawned child.
|
|
3247
|
+
applySharedSidecarHold(state);
|
|
3248
|
+
try {
|
|
3249
|
+
const session = await client.authenticateAndOpenSession();
|
|
3250
|
+
return { client, session };
|
|
3251
|
+
}
|
|
3252
|
+
catch (error) {
|
|
3253
|
+
// Spawn/handshake failed: reap the child, drop the cached handle,
|
|
3254
|
+
// and CLEAR the rejected promise so the next create() retries
|
|
3255
|
+
// instead of permanently wedging on a rejected `nativeProcess`.
|
|
3256
|
+
try {
|
|
3257
|
+
state.sharedChild?.kill?.("SIGKILL");
|
|
3258
|
+
}
|
|
3259
|
+
catch {
|
|
3260
|
+
// already gone
|
|
3261
|
+
}
|
|
3262
|
+
state.sharedChild = undefined;
|
|
3263
|
+
state.nativeProcess = undefined;
|
|
3264
|
+
throw error;
|
|
3265
|
+
}
|
|
3266
|
+
})();
|
|
3267
|
+
}
|
|
3268
|
+
return state.nativeProcess;
|
|
3269
|
+
}
|
|
3270
|
+
/** Dispose a sidecar handle's shared native process, if one was spawned. */
|
|
3271
|
+
async function disposeSharedSidecarNativeProcess(state) {
|
|
3272
|
+
const pending = state.nativeProcess;
|
|
3273
|
+
if (!pending) {
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
state.nativeProcess = undefined;
|
|
3277
|
+
// The cached child is now dead; drop it (symmetric with the assignment in
|
|
3278
|
+
// ensureSharedSidecarNativeProcess). We deliberately do NOT zero
|
|
3279
|
+
// `eventLoopHolds` here: this runs only from `AgentOsSidecar.dispose()`, which
|
|
3280
|
+
// has already set the handle to `disposing` (so no new lease can acquire) and
|
|
3281
|
+
// drained `activeLeases`; the disposed handle's state is then abandoned. Force-
|
|
3282
|
+
// zeroing a shared counter could clobber a hold on a freshly re-acquired
|
|
3283
|
+
// process generation, so it is left to the balanced acquire/release pairs.
|
|
3284
|
+
state.sharedChild = undefined;
|
|
3285
|
+
try {
|
|
3286
|
+
const { client } = await pending;
|
|
3287
|
+
await client.dispose();
|
|
3288
|
+
}
|
|
3289
|
+
catch {
|
|
3290
|
+
// Process may have already exited; nothing to reclaim.
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3439
3293
|
export class AgentOsSidecar {
|
|
3440
3294
|
constructor(sidecarId, placement, sharedPool) {
|
|
3441
3295
|
sidecarStates.set(this, {
|
|
@@ -3470,6 +3324,8 @@ export class AgentOsSidecar {
|
|
|
3470
3324
|
}
|
|
3471
3325
|
state.activeLeases.clear();
|
|
3472
3326
|
state.description.activeVmCount = 0;
|
|
3327
|
+
// Tear down the shared native process after all leased VMs are gone.
|
|
3328
|
+
await disposeSharedSidecarNativeProcess(state);
|
|
3473
3329
|
state.description.state = "disposed";
|
|
3474
3330
|
if (state.sharedPool && sharedSidecars.get(state.sharedPool) === this) {
|
|
3475
3331
|
sharedSidecars.delete(state.sharedPool);
|
|
@@ -3526,15 +3382,27 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3526
3382
|
}
|
|
3527
3383
|
let transport;
|
|
3528
3384
|
const client = createAgentOsSidecarClient({
|
|
3529
|
-
async
|
|
3385
|
+
async createOwnershipTransport(sessionBootstrap) {
|
|
3530
3386
|
transport = await createInProcessSidecarTransport(sessionBootstrap, options);
|
|
3531
3387
|
return transport;
|
|
3532
3388
|
},
|
|
3533
3389
|
});
|
|
3390
|
+
// Hold the shared sidecar's event-loop ref for this lease's WHOLE lifetime —
|
|
3391
|
+
// taken now, before VM creation, so a concurrent dispose cannot unref the
|
|
3392
|
+
// sidecar while this create is still in flight. Released exactly once on
|
|
3393
|
+
// dispose or on a failed create.
|
|
3394
|
+
acquireSharedSidecarHold(state);
|
|
3395
|
+
let holdReleased = false;
|
|
3396
|
+
const releaseHold = () => {
|
|
3397
|
+
if (holdReleased)
|
|
3398
|
+
return;
|
|
3399
|
+
holdReleased = true;
|
|
3400
|
+
releaseSharedSidecarHold(state);
|
|
3401
|
+
};
|
|
3534
3402
|
let disposed = false;
|
|
3535
3403
|
let leaseRecord;
|
|
3536
3404
|
try {
|
|
3537
|
-
const session = await client.
|
|
3405
|
+
const session = await client.createOwnershipSession({
|
|
3538
3406
|
placement: cloneSidecarPlacement(state.description.placement),
|
|
3539
3407
|
});
|
|
3540
3408
|
const vm = await session.createVm();
|
|
@@ -3555,6 +3423,10 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3555
3423
|
state.activeLeases.delete(leaseRecord);
|
|
3556
3424
|
state.description.activeVmCount = state.activeLeases.size;
|
|
3557
3425
|
await client.dispose();
|
|
3426
|
+
// Release this lease's hold; the shared sidecar is unref'd only
|
|
3427
|
+
// once the last hold (across all in-flight + active leases) drops,
|
|
3428
|
+
// so a one-shot host process can then exit on its own.
|
|
3429
|
+
releaseHold();
|
|
3558
3430
|
},
|
|
3559
3431
|
};
|
|
3560
3432
|
leaseRecord = {
|
|
@@ -3566,6 +3438,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3566
3438
|
}
|
|
3567
3439
|
catch (error) {
|
|
3568
3440
|
await client.dispose().catch(() => { });
|
|
3441
|
+
releaseHold();
|
|
3569
3442
|
throw error;
|
|
3570
3443
|
}
|
|
3571
3444
|
}
|