@rivet-dev/agentos-core 0.0.0-codex-actor-plugin-contract-coverage.115ca8a → 0.0.0-eve-integration.9df6f34
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 +41 -38
- package/dist/agent-os.d.ts +196 -232
- package/dist/agent-os.js +771 -1162
- package/dist/agentos-package.d.ts +5 -3
- package/dist/agentos-package.js +5 -2
- package/dist/{host-tools-zod.d.ts → bindings-zod.d.ts} +1 -1
- package/dist/{host-tools-zod.js → bindings-zod.js} +27 -20
- package/dist/bindings.d.ts +43 -0
- package/dist/bindings.js +34 -0
- package/dist/cron/cron-manager.js +43 -10
- package/dist/cron/index.d.ts +1 -1
- package/dist/cron/types.d.ts +25 -9
- package/dist/filesystem-snapshot.d.ts +1 -2
- package/dist/filesystem-snapshot.js +0 -43
- package/dist/generated/AcpLimitsConfig.d.ts +16 -0
- package/dist/generated/BindingLimitsConfig.d.ts +10 -0
- package/dist/generated/CreateVmConfig.d.ts +2 -0
- package/dist/generated/ProcessLimitsConfig.d.ts +7 -0
- package/dist/generated/ProcessLimitsConfig.js +2 -0
- package/dist/generated/VmGroupConfig.d.ts +5 -0
- package/dist/generated/VmGroupConfig.js +2 -0
- package/dist/generated/VmLimitsConfig.d.ts +4 -2
- package/dist/generated/VmUserAccountConfig.d.ts +9 -0
- package/dist/generated/VmUserAccountConfig.js +2 -0
- package/dist/generated/VmUserConfig.d.ts +19 -0
- package/dist/generated/WasmLimitsConfig.d.ts +1 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -5
- package/dist/layers.d.ts +5 -2
- package/dist/layers.js +0 -185
- package/dist/options-schema.d.ts +133 -31
- package/dist/options-schema.js +143 -26
- package/dist/runtime-compat.d.ts +11 -37
- package/dist/runtime-compat.js +88 -376
- package/dist/runtime.d.ts +10 -3
- package/dist/sandbox.d.ts +4 -4
- package/dist/sandbox.js +4 -4
- package/dist/session-api.d.ts +180 -0
- package/dist/session-api.js +1 -0
- package/dist/sidecar/agentos-protocol.d.ts +429 -29
- package/dist/sidecar/agentos-protocol.js +691 -93
- package/dist/sidecar/native-process-client.d.ts +1 -1
- package/dist/sidecar/native-process-client.js +1 -1
- package/dist/sidecar/rpc-client.d.ts +40 -6
- package/dist/sidecar/rpc-client.js +126 -38
- 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 +4 -5
- package/package.json +29 -28
- package/dist/agent-session-types.d.ts +0 -67
- package/dist/generated/ToolLimitsConfig.d.ts +0 -10
- package/dist/host-tools.d.ts +0 -45
- package/dist/host-tools.js +0 -35
- package/dist/overlay-filesystem.d.ts +0 -19
- package/dist/overlay-filesystem.js +0 -645
- /package/dist/generated/{ToolLimitsConfig.js → BindingLimitsConfig.js} +0 -0
- /package/dist/{agent-session-types.js → generated/VmUserConfig.js} +0 -0
package/dist/agent-os.js
CHANGED
|
@@ -3,22 +3,139 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import { zodToJsonSchema } from "./
|
|
6
|
+
import { validateBindings } from "./bindings.js";
|
|
7
|
+
import { zodToJsonSchema } from "./bindings-zod.js";
|
|
8
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
|
-
function
|
|
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) {
|
|
16
22
|
return {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 } : {}),
|
|
22
139
|
};
|
|
23
140
|
}
|
|
24
141
|
async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
@@ -32,26 +149,24 @@ async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
|
32
149
|
}),
|
|
33
150
|
]);
|
|
34
151
|
}
|
|
152
|
+
import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
|
|
35
153
|
import { getBaseEnvironment } from "./base-filesystem.js";
|
|
36
154
|
import { CronManager } from "./cron/cron-manager.js";
|
|
37
155
|
import { TimerScheduleDriver } from "./cron/timer-driver.js";
|
|
156
|
+
import { resolveDefaultSoftware } from "./default-software.js";
|
|
38
157
|
import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
|
|
39
158
|
import { serializeMountConfigForSidecar, } from "./js-bridge.js";
|
|
40
159
|
import { createSnapshotExport, } from "./layers.js";
|
|
41
|
-
import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
|
|
42
|
-
import { resolveDefaultSoftware } from "./default-software.js";
|
|
43
160
|
import { allowAll } from "./runtime-compat.js";
|
|
44
|
-
import {
|
|
161
|
+
import { decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
|
|
45
162
|
import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
|
|
46
|
-
import { createAgentOsSidecarClient,
|
|
163
|
+
import { createAgentOsSidecarClient, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
|
|
47
164
|
function defaultAgentStderrHandler(event) {
|
|
48
165
|
process.stderr.write(event.chunk);
|
|
49
166
|
}
|
|
50
167
|
function defaultAgentExitHandler(event) {
|
|
51
|
-
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"}
|
|
168
|
+
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"}; restore explicitly before retrying\n`);
|
|
52
169
|
}
|
|
53
|
-
const LEGACY_PERMISSION_METHOD = "request/permission";
|
|
54
|
-
const ACP_PERMISSION_METHOD = "session/request_permission";
|
|
55
170
|
class AcpDispatchError extends Error {
|
|
56
171
|
code;
|
|
57
172
|
data;
|
|
@@ -124,51 +239,6 @@ function normalizePackageRef(value) {
|
|
|
124
239
|
}
|
|
125
240
|
return undefined;
|
|
126
241
|
}
|
|
127
|
-
function parseAcpJson(value, context) {
|
|
128
|
-
if (value === null) {
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
return JSON.parse(value);
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
throw new Error(`invalid ACP ${context} JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function parseAcpJsonList(values, context) {
|
|
139
|
-
return values.map((value, index) => parseAcpJson(value, `${context}[${index}]`));
|
|
140
|
-
}
|
|
141
|
-
function sidecarSessionCreatedFromAcp(response) {
|
|
142
|
-
return {
|
|
143
|
-
sessionId: response.sessionId,
|
|
144
|
-
...(response.pid !== null ? { pid: response.pid } : {}),
|
|
145
|
-
modes: parseAcpJson(response.modes, "modes"),
|
|
146
|
-
configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
|
|
147
|
-
agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
|
|
148
|
-
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function sidecarSessionStateFromAcp(response) {
|
|
152
|
-
return {
|
|
153
|
-
sessionId: response.sessionId,
|
|
154
|
-
agentType: response.agentType,
|
|
155
|
-
processId: response.processId,
|
|
156
|
-
...(response.pid !== null ? { pid: response.pid } : {}),
|
|
157
|
-
closed: response.closed,
|
|
158
|
-
modes: parseAcpJson(response.modes, "modes"),
|
|
159
|
-
configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
|
|
160
|
-
agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
|
|
161
|
-
agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
function isLocalCancelledPromptResponse(method, response) {
|
|
165
|
-
const result = toRecord(response.result);
|
|
166
|
-
return (method === "session/prompt" &&
|
|
167
|
-
response.id === null &&
|
|
168
|
-
response.error === undefined &&
|
|
169
|
-
result.stopReason === "cancelled");
|
|
170
|
-
}
|
|
171
|
-
const CLOSED_SESSION_ID_RETENTION_LIMIT = 2048;
|
|
172
242
|
const CLOSED_SHELL_ID_RETENTION_LIMIT = 2048;
|
|
173
243
|
class BoundedSet {
|
|
174
244
|
limit;
|
|
@@ -205,51 +275,6 @@ class BoundedSet {
|
|
|
205
275
|
return this.#entries.size;
|
|
206
276
|
}
|
|
207
277
|
}
|
|
208
|
-
function shouldDispatchToSessionEventHandlers(notification) {
|
|
209
|
-
return notification.method === "session/update";
|
|
210
|
-
}
|
|
211
|
-
function toSessionModes(value) {
|
|
212
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
return value;
|
|
216
|
-
}
|
|
217
|
-
function toSessionConfigOptions(value) {
|
|
218
|
-
return Array.isArray(value) ? value : [];
|
|
219
|
-
}
|
|
220
|
-
function toAgentCapabilities(value) {
|
|
221
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
222
|
-
return {};
|
|
223
|
-
}
|
|
224
|
-
return value;
|
|
225
|
-
}
|
|
226
|
-
function toAgentInfo(value) {
|
|
227
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
if (typeof value.name !== "string") {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
return value;
|
|
234
|
-
}
|
|
235
|
-
function sessionEntryFromInit(sessionId, agentType, initData) {
|
|
236
|
-
return {
|
|
237
|
-
sessionId,
|
|
238
|
-
agentType,
|
|
239
|
-
processId: "",
|
|
240
|
-
pid: null,
|
|
241
|
-
closed: false,
|
|
242
|
-
modes: initData.modes ?? null,
|
|
243
|
-
configOptions: initData.configOptions ?? [],
|
|
244
|
-
capabilities: initData.capabilities ?? {},
|
|
245
|
-
agentInfo: initData.agentInfo ?? null,
|
|
246
|
-
eventHandlers: new Set(),
|
|
247
|
-
permissionHandlers: new Set(),
|
|
248
|
-
warnedNoPermissionHandler: false,
|
|
249
|
-
configOverrides: new Map(),
|
|
250
|
-
pendingPermissionReplies: new Map(),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
278
|
function isOverlayMountConfig(config) {
|
|
254
279
|
return "filesystem" in config;
|
|
255
280
|
}
|
|
@@ -445,14 +470,28 @@ const SIDECAR_BUILD_INPUTS = [
|
|
|
445
470
|
join(REPO_ROOT, "Cargo.toml"),
|
|
446
471
|
join(REPO_ROOT, "Cargo.lock"),
|
|
447
472
|
join(REPO_ROOT, "crates/bridge"),
|
|
473
|
+
join(REPO_ROOT, "crates/build-support"),
|
|
448
474
|
join(REPO_ROOT, "crates/execution"),
|
|
449
475
|
join(REPO_ROOT, "crates/kernel"),
|
|
450
|
-
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"),
|
|
451
490
|
];
|
|
452
491
|
let ensuredSidecarBinary = null;
|
|
453
492
|
function collectConfiguredLowerPaths(config) {
|
|
454
493
|
const paths = new Set();
|
|
455
|
-
for (const lower of config?.lowers ?? []) {
|
|
494
|
+
for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
|
|
456
495
|
if (lower.kind !== "snapshot-export") {
|
|
457
496
|
continue;
|
|
458
497
|
}
|
|
@@ -463,7 +502,7 @@ function collectConfiguredLowerPaths(config) {
|
|
|
463
502
|
return paths;
|
|
464
503
|
}
|
|
465
504
|
function findBootstrapSeedEntry(config, path) {
|
|
466
|
-
for (const lower of config?.lowers ?? []) {
|
|
505
|
+
for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
|
|
467
506
|
if (lower.kind !== "snapshot-export") {
|
|
468
507
|
continue;
|
|
469
508
|
}
|
|
@@ -479,6 +518,9 @@ function findBootstrapSeedEntry(config, path) {
|
|
|
479
518
|
return meta ? { path, type: "directory", ...meta } : undefined;
|
|
480
519
|
}
|
|
481
520
|
function createKernelBootstrapLower(config, extraEntries = []) {
|
|
521
|
+
if (config?.type === "native") {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
482
524
|
const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
|
|
483
525
|
const existingPaths = collectConfiguredLowerPaths(config);
|
|
484
526
|
const entries = [
|
|
@@ -547,7 +589,7 @@ function buildLiveBootstrapDirectoryEntries(existingPaths, config) {
|
|
|
547
589
|
return entries;
|
|
548
590
|
}
|
|
549
591
|
async function bootstrapLiveBootstrapDirectories(client, session, vm, config) {
|
|
550
|
-
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));
|
|
551
593
|
const entries = buildLiveBootstrapDirectoryEntries(existingPaths, config);
|
|
552
594
|
if (entries.length === 0) {
|
|
553
595
|
return;
|
|
@@ -720,11 +762,14 @@ function collectSidecarMountPlan(options) {
|
|
|
720
762
|
hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
721
763
|
return { sidecarMounts, hostMounts, hostPathMappings };
|
|
722
764
|
}
|
|
723
|
-
function
|
|
724
|
-
if (
|
|
765
|
+
function collectBindingBootstrapCommands(bindings) {
|
|
766
|
+
if (bindings.length === 0) {
|
|
725
767
|
return [];
|
|
726
768
|
}
|
|
727
|
-
return [
|
|
769
|
+
return [
|
|
770
|
+
"agentos",
|
|
771
|
+
...bindings.map((bindingCollection) => `agentos-${bindingCollection.name}`),
|
|
772
|
+
];
|
|
728
773
|
}
|
|
729
774
|
function validationMessage(error) {
|
|
730
775
|
if (typeof error === "object" &&
|
|
@@ -742,14 +787,16 @@ function validationMessage(error) {
|
|
|
742
787
|
}
|
|
743
788
|
return error instanceof Error ? error.message : String(error);
|
|
744
789
|
}
|
|
745
|
-
function
|
|
790
|
+
function bindingToSidecarDefinition(definition) {
|
|
746
791
|
return {
|
|
747
|
-
description:
|
|
748
|
-
inputSchema: zodToJsonSchema(
|
|
749
|
-
...(
|
|
750
|
-
|
|
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
|
|
751
798
|
? {
|
|
752
|
-
examples:
|
|
799
|
+
examples: definition.examples.map((example) => ({
|
|
753
800
|
description: example.description,
|
|
754
801
|
input: example.input,
|
|
755
802
|
})),
|
|
@@ -757,8 +804,8 @@ function toolToSidecarDefinition(tool) {
|
|
|
757
804
|
: {}),
|
|
758
805
|
};
|
|
759
806
|
}
|
|
760
|
-
function combineInstructions(additionalInstructions,
|
|
761
|
-
const parts = [additionalInstructions,
|
|
807
|
+
function combineInstructions(additionalInstructions, bindingReference) {
|
|
808
|
+
const parts = [additionalInstructions, bindingReference]
|
|
762
809
|
.map((part) => part?.trim())
|
|
763
810
|
.filter((part) => Boolean(part));
|
|
764
811
|
if (parts.length === 0) {
|
|
@@ -766,48 +813,48 @@ function combineInstructions(additionalInstructions, toolReference) {
|
|
|
766
813
|
}
|
|
767
814
|
return parts.join("\n\n");
|
|
768
815
|
}
|
|
769
|
-
function
|
|
770
|
-
if (
|
|
816
|
+
function buildBindingReference(bindings) {
|
|
817
|
+
if (bindings.length === 0) {
|
|
771
818
|
return "";
|
|
772
819
|
}
|
|
773
820
|
const lines = [
|
|
774
|
-
"## Available Host
|
|
821
|
+
"## Available Host Bindings",
|
|
775
822
|
"",
|
|
776
|
-
"Run `agentos list-
|
|
823
|
+
"Run `agentos list-bindings` to see all available bindings.",
|
|
777
824
|
"",
|
|
778
825
|
];
|
|
779
|
-
for (const
|
|
780
|
-
lines.push(`### ${
|
|
826
|
+
for (const bindingCollection of bindings) {
|
|
827
|
+
lines.push(`### ${bindingCollection.name}`);
|
|
781
828
|
lines.push("");
|
|
782
|
-
lines.push(
|
|
829
|
+
lines.push(bindingCollection.description);
|
|
783
830
|
lines.push("");
|
|
784
|
-
for (const [
|
|
785
|
-
const
|
|
786
|
-
const signature =
|
|
831
|
+
for (const [bindingName, definition] of Object.entries(bindingCollection.bindings)) {
|
|
832
|
+
const sidecarBinding = bindingToSidecarDefinition(definition);
|
|
833
|
+
const signature = buildBindingFlagSignature(sidecarBinding.inputSchema);
|
|
787
834
|
const suffix = signature.length > 0 ? ` ${signature}` : "";
|
|
788
|
-
lines.push(`- \`agentos-${
|
|
835
|
+
lines.push(`- \`agentos-${bindingCollection.name} ${bindingName}${suffix}\` — ${definition.description}`);
|
|
789
836
|
}
|
|
790
837
|
lines.push("");
|
|
791
|
-
const
|
|
792
|
-
if (
|
|
838
|
+
const bindingsWithExamples = Object.entries(bindingCollection.bindings).filter(([, definition]) => definition.examples && definition.examples.length > 0);
|
|
839
|
+
if (bindingsWithExamples.length > 0) {
|
|
793
840
|
lines.push("**Examples:**");
|
|
794
841
|
lines.push("");
|
|
795
|
-
for (const [
|
|
796
|
-
for (const example of
|
|
797
|
-
const args =
|
|
842
|
+
for (const [bindingName, definition] of bindingsWithExamples) {
|
|
843
|
+
for (const example of definition.examples ?? []) {
|
|
844
|
+
const args = inputToBindingFlags(example.input);
|
|
798
845
|
const suffix = args.length > 0 ? ` ${args}` : "";
|
|
799
|
-
lines.push(`- ${example.description}: \`agentos-${
|
|
846
|
+
lines.push(`- ${example.description}: \`agentos-${bindingCollection.name} ${bindingName}${suffix}\``);
|
|
800
847
|
}
|
|
801
848
|
}
|
|
802
849
|
lines.push("");
|
|
803
850
|
}
|
|
804
|
-
lines.push(`Run \`agentos-${
|
|
851
|
+
lines.push(`Run \`agentos-${bindingCollection.name} <binding> --help\` for details.`);
|
|
805
852
|
lines.push("");
|
|
806
853
|
}
|
|
807
854
|
return lines.join("\n");
|
|
808
855
|
}
|
|
809
|
-
function
|
|
810
|
-
return
|
|
856
|
+
function buildBindingFlagSignature(schema) {
|
|
857
|
+
return describeBindingFlags(schema)
|
|
811
858
|
.map((flag) => {
|
|
812
859
|
if (flag.required) {
|
|
813
860
|
return `${flag.name} <${flag.type}>`;
|
|
@@ -816,7 +863,7 @@ function buildToolFlagSignature(schema) {
|
|
|
816
863
|
})
|
|
817
864
|
.join(" ");
|
|
818
865
|
}
|
|
819
|
-
function
|
|
866
|
+
function describeBindingFlags(schema) {
|
|
820
867
|
const schemaObject = asRecord(schema);
|
|
821
868
|
const properties = asRecord(schemaObject.properties);
|
|
822
869
|
const required = Array.isArray(schemaObject.required)
|
|
@@ -824,11 +871,11 @@ function describeToolFlags(schema) {
|
|
|
824
871
|
: new Set();
|
|
825
872
|
return Object.entries(properties).map(([fieldName, fieldSchema]) => ({
|
|
826
873
|
name: `--${camelToKebab(fieldName)}`,
|
|
827
|
-
type:
|
|
874
|
+
type: describeBindingFlagType(fieldSchema),
|
|
828
875
|
required: required.has(fieldName),
|
|
829
876
|
}));
|
|
830
877
|
}
|
|
831
|
-
function
|
|
878
|
+
function describeBindingFlagType(schema) {
|
|
832
879
|
const schemaObject = asRecord(schema);
|
|
833
880
|
const type = typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
834
881
|
if (type === "array") {
|
|
@@ -847,7 +894,7 @@ function describeJsonSchemaScalarType(schema) {
|
|
|
847
894
|
const schemaObject = asRecord(schema);
|
|
848
895
|
return typeof schemaObject.type === "string" ? schemaObject.type : "string";
|
|
849
896
|
}
|
|
850
|
-
function
|
|
897
|
+
function inputToBindingFlags(input) {
|
|
851
898
|
const inputObject = asRecord(input);
|
|
852
899
|
return Object.entries(inputObject)
|
|
853
900
|
.flatMap(([key, value]) => {
|
|
@@ -859,13 +906,13 @@ function inputToToolFlags(input) {
|
|
|
859
906
|
return [`--no-${camelToKebab(key)}`];
|
|
860
907
|
}
|
|
861
908
|
if (Array.isArray(value)) {
|
|
862
|
-
return value.map((item) => `${flag} ${
|
|
909
|
+
return value.map((item) => `${flag} ${bindingCliString(item)}`);
|
|
863
910
|
}
|
|
864
|
-
return [`${flag} ${
|
|
911
|
+
return [`${flag} ${bindingCliString(value)}`];
|
|
865
912
|
})
|
|
866
913
|
.join(" ");
|
|
867
914
|
}
|
|
868
|
-
function
|
|
915
|
+
function bindingCliString(value) {
|
|
869
916
|
return typeof value === "string" ? value : (JSON.stringify(value) ?? "null");
|
|
870
917
|
}
|
|
871
918
|
function camelToKebab(value) {
|
|
@@ -903,15 +950,15 @@ async function handleHostCallback(request, context) {
|
|
|
903
950
|
};
|
|
904
951
|
}
|
|
905
952
|
}
|
|
906
|
-
const
|
|
907
|
-
if (!
|
|
953
|
+
const definition = context.bindingMap.get(payload.callback_key);
|
|
954
|
+
if (!definition) {
|
|
908
955
|
return {
|
|
909
956
|
type: "host_callback_result",
|
|
910
957
|
invocation_id: payload.invocation_id,
|
|
911
|
-
error: `Unknown
|
|
958
|
+
error: `Unknown binding "${payload.callback_key}"`,
|
|
912
959
|
};
|
|
913
960
|
}
|
|
914
|
-
const permissionMode =
|
|
961
|
+
const permissionMode = bindingPermissionMode(context.permissions, payload.callback_key);
|
|
915
962
|
if (permissionMode !== "allow") {
|
|
916
963
|
return {
|
|
917
964
|
type: "host_callback_result",
|
|
@@ -919,7 +966,7 @@ async function handleHostCallback(request, context) {
|
|
|
919
966
|
error: `EACCES: blocked by binding.invoke policy for ${payload.callback_key}`,
|
|
920
967
|
};
|
|
921
968
|
}
|
|
922
|
-
const parsed =
|
|
969
|
+
const parsed = definition.inputSchema.safeParse(payload.input);
|
|
923
970
|
if (!parsed.success) {
|
|
924
971
|
return {
|
|
925
972
|
type: "host_callback_result",
|
|
@@ -931,7 +978,7 @@ async function handleHostCallback(request, context) {
|
|
|
931
978
|
return {
|
|
932
979
|
type: "host_callback_result",
|
|
933
980
|
invocation_id: payload.invocation_id,
|
|
934
|
-
result: await
|
|
981
|
+
result: await executeBinding(definition, payload.callback_key, parsed.data),
|
|
935
982
|
};
|
|
936
983
|
}
|
|
937
984
|
catch (error) {
|
|
@@ -942,14 +989,14 @@ async function handleHostCallback(request, context) {
|
|
|
942
989
|
};
|
|
943
990
|
}
|
|
944
991
|
}
|
|
945
|
-
function
|
|
946
|
-
const
|
|
947
|
-
for (const
|
|
948
|
-
for (const [
|
|
949
|
-
|
|
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);
|
|
950
997
|
}
|
|
951
998
|
}
|
|
952
|
-
return
|
|
999
|
+
return bindingMap;
|
|
953
1000
|
}
|
|
954
1001
|
function bridgeErrorMessage(error) {
|
|
955
1002
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1048,7 +1095,7 @@ async function handleJsBridgeCall(request, context) {
|
|
|
1048
1095
|
await fs.chmod(path(), requireBridgeNumber(args.mode, "mode"));
|
|
1049
1096
|
break;
|
|
1050
1097
|
case "chown":
|
|
1051
|
-
await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"));
|
|
1098
|
+
await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"), { followSymlinks: args.followSymlinks !== false });
|
|
1052
1099
|
break;
|
|
1053
1100
|
case "utimes":
|
|
1054
1101
|
await fs.utimes(path(), requireBridgeNumber(args.atimeMs, "atimeMs"), requireBridgeNumber(args.mtimeMs, "mtimeMs"));
|
|
@@ -1096,90 +1143,90 @@ function parseHostCommandCallbackInput(input) {
|
|
|
1096
1143
|
};
|
|
1097
1144
|
}
|
|
1098
1145
|
async function handleHostCommandCallback(command, context) {
|
|
1099
|
-
const
|
|
1146
|
+
const directBindings = context.bindings.find((bindingCollection) => `agentos-${bindingCollection.name}` === command.command);
|
|
1100
1147
|
if (command.command === "agentos") {
|
|
1101
1148
|
return handleAgentOsRegistryCommand(command, context);
|
|
1102
1149
|
}
|
|
1103
|
-
if (
|
|
1104
|
-
return
|
|
1150
|
+
if (directBindings) {
|
|
1151
|
+
return handleAgentOsBindingCommand(command, context, directBindings);
|
|
1105
1152
|
}
|
|
1106
1153
|
throw new Error(`Unknown host callback command "${command.command}"`);
|
|
1107
1154
|
}
|
|
1108
1155
|
async function handleAgentOsRegistryCommand(command, context) {
|
|
1109
|
-
const [subcommand,
|
|
1156
|
+
const [subcommand, collectionName, bindingName, ...bindingArgs] = command.args;
|
|
1110
1157
|
if (!subcommand || isHelpFlag(subcommand)) {
|
|
1111
1158
|
return {
|
|
1112
|
-
usage: "agentos <command>: list-
|
|
1159
|
+
usage: "agentos <command>: list-bindings [collection], <collection> --help, or <collection> <binding> ...",
|
|
1113
1160
|
};
|
|
1114
1161
|
}
|
|
1115
|
-
if (subcommand === "list-
|
|
1116
|
-
return
|
|
1117
|
-
?
|
|
1118
|
-
:
|
|
1162
|
+
if (subcommand === "list-bindings") {
|
|
1163
|
+
return collectionName
|
|
1164
|
+
? describeBindingsPayload(context.bindings, collectionName)
|
|
1165
|
+
: listBindingsPayload(context.bindings);
|
|
1119
1166
|
}
|
|
1120
|
-
const
|
|
1121
|
-
if (!
|
|
1122
|
-
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)}`);
|
|
1123
1170
|
}
|
|
1124
|
-
if (!
|
|
1125
|
-
return
|
|
1171
|
+
if (!collectionName || isHelpFlag(collectionName)) {
|
|
1172
|
+
return describeBindingsPayload(context.bindings, subcommand);
|
|
1126
1173
|
}
|
|
1127
|
-
if (
|
|
1128
|
-
return
|
|
1174
|
+
if (bindingName && isHelpFlag(bindingName)) {
|
|
1175
|
+
return describeBindingPayload(bindingCollection, collectionName);
|
|
1129
1176
|
}
|
|
1130
|
-
return
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
args: [
|
|
1177
|
+
return invokeBinding({
|
|
1178
|
+
bindingCollection,
|
|
1179
|
+
bindingName: collectionName,
|
|
1180
|
+
args: [bindingName, ...bindingArgs].filter((value) => typeof value === "string"),
|
|
1134
1181
|
cwd: command.cwd,
|
|
1135
1182
|
context,
|
|
1136
1183
|
});
|
|
1137
1184
|
}
|
|
1138
|
-
async function
|
|
1139
|
-
const [
|
|
1140
|
-
if (!
|
|
1141
|
-
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);
|
|
1142
1189
|
}
|
|
1143
1190
|
if (helpOrFirstArg && isHelpFlag(helpOrFirstArg)) {
|
|
1144
|
-
return
|
|
1191
|
+
return describeBindingPayload(bindingCollection, bindingName);
|
|
1145
1192
|
}
|
|
1146
|
-
return
|
|
1147
|
-
|
|
1148
|
-
|
|
1193
|
+
return invokeBinding({
|
|
1194
|
+
bindingCollection,
|
|
1195
|
+
bindingName,
|
|
1149
1196
|
args: [helpOrFirstArg, ...rest].filter((value) => typeof value === "string"),
|
|
1150
1197
|
cwd: command.cwd,
|
|
1151
1198
|
context,
|
|
1152
1199
|
});
|
|
1153
1200
|
}
|
|
1154
|
-
async function
|
|
1155
|
-
const
|
|
1156
|
-
if (!
|
|
1157
|
-
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)}`);
|
|
1158
1205
|
}
|
|
1159
|
-
const callbackKey = `${
|
|
1160
|
-
const permissionMode =
|
|
1206
|
+
const callbackKey = `${bindingCollection.name}:${bindingName}`;
|
|
1207
|
+
const permissionMode = bindingPermissionMode(context.permissions, callbackKey);
|
|
1161
1208
|
if (permissionMode !== "allow") {
|
|
1162
1209
|
throw new Error(`EACCES: blocked by binding.invoke policy for ${callbackKey}`);
|
|
1163
1210
|
}
|
|
1164
|
-
const input = await
|
|
1165
|
-
return
|
|
1211
|
+
const input = await parseBindingInput(definition, args, cwd, context.readFile);
|
|
1212
|
+
return executeBinding(definition, callbackKey, input);
|
|
1166
1213
|
}
|
|
1167
|
-
async function
|
|
1168
|
-
const parsed =
|
|
1214
|
+
async function executeBinding(definition, callbackKey, input) {
|
|
1215
|
+
const parsed = definition.inputSchema.safeParse(input);
|
|
1169
1216
|
if (!parsed.success) {
|
|
1170
1217
|
throw new Error(validationMessage(parsed.error));
|
|
1171
1218
|
}
|
|
1172
1219
|
return Promise.race([
|
|
1173
|
-
Promise.resolve(
|
|
1220
|
+
Promise.resolve(definition.execute(parsed.data)),
|
|
1174
1221
|
new Promise((_, reject) => {
|
|
1175
|
-
if (
|
|
1222
|
+
if (definition.timeout === undefined) {
|
|
1176
1223
|
return;
|
|
1177
1224
|
}
|
|
1178
|
-
setTimeout(() => reject(new Error(`
|
|
1225
|
+
setTimeout(() => reject(new Error(`Binding "${callbackKey}" timed out after ${definition.timeout}ms`)), definition.timeout);
|
|
1179
1226
|
}),
|
|
1180
1227
|
]);
|
|
1181
1228
|
}
|
|
1182
|
-
async function
|
|
1229
|
+
async function parseBindingInput(definition, args, cwd, readFile) {
|
|
1183
1230
|
if (args[0] === "--json") {
|
|
1184
1231
|
const value = args[1];
|
|
1185
1232
|
if (value === undefined) {
|
|
@@ -1198,9 +1245,9 @@ async function parseHostToolInput(tool, args, cwd, readFile) {
|
|
|
1198
1245
|
const text = new TextDecoder().decode(await readFile(guestPath));
|
|
1199
1246
|
return JSON.parse(text);
|
|
1200
1247
|
}
|
|
1201
|
-
return
|
|
1248
|
+
return parseBindingArgv(bindingToSidecarDefinition(definition).inputSchema, args);
|
|
1202
1249
|
}
|
|
1203
|
-
function
|
|
1250
|
+
function parseBindingArgv(schema, argv) {
|
|
1204
1251
|
const schemaObject = asRecord(schema);
|
|
1205
1252
|
const properties = asRecord(schemaObject.properties);
|
|
1206
1253
|
const required = Array.isArray(schemaObject.required)
|
|
@@ -1272,49 +1319,49 @@ function parseToolArgv(schema, argv) {
|
|
|
1272
1319
|
}
|
|
1273
1320
|
return input;
|
|
1274
1321
|
}
|
|
1275
|
-
function
|
|
1322
|
+
function listBindingsPayload(bindings) {
|
|
1276
1323
|
return {
|
|
1277
|
-
|
|
1278
|
-
name:
|
|
1279
|
-
description:
|
|
1280
|
-
|
|
1324
|
+
bindings: bindings.map((bindingCollection) => ({
|
|
1325
|
+
name: bindingCollection.name,
|
|
1326
|
+
description: bindingCollection.description,
|
|
1327
|
+
bindings: Object.keys(bindingCollection.bindings),
|
|
1281
1328
|
})),
|
|
1282
1329
|
};
|
|
1283
1330
|
}
|
|
1284
|
-
function
|
|
1285
|
-
const
|
|
1286
|
-
if (!
|
|
1287
|
-
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)}`);
|
|
1288
1335
|
}
|
|
1289
1336
|
return {
|
|
1290
|
-
name:
|
|
1291
|
-
description:
|
|
1292
|
-
|
|
1293
|
-
|
|
1337
|
+
name: bindingCollection.name,
|
|
1338
|
+
description: bindingCollection.description,
|
|
1339
|
+
bindings: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
|
|
1340
|
+
bindingName,
|
|
1294
1341
|
{
|
|
1295
|
-
description:
|
|
1296
|
-
flags:
|
|
1342
|
+
description: definition.description,
|
|
1343
|
+
flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
|
|
1297
1344
|
},
|
|
1298
1345
|
])),
|
|
1299
1346
|
};
|
|
1300
1347
|
}
|
|
1301
|
-
function
|
|
1302
|
-
const
|
|
1303
|
-
if (!
|
|
1304
|
-
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)}`);
|
|
1305
1352
|
}
|
|
1306
1353
|
return {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
description:
|
|
1310
|
-
flags:
|
|
1311
|
-
examples:
|
|
1354
|
+
collection: bindingCollection.name,
|
|
1355
|
+
binding: bindingName,
|
|
1356
|
+
description: definition.description,
|
|
1357
|
+
flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
|
|
1358
|
+
examples: definition.examples?.map((example) => ({
|
|
1312
1359
|
description: example.description,
|
|
1313
1360
|
input: example.input,
|
|
1314
1361
|
})) ?? [],
|
|
1315
1362
|
};
|
|
1316
1363
|
}
|
|
1317
|
-
function
|
|
1364
|
+
function bindingPermissionMode(permissions, callbackKey) {
|
|
1318
1365
|
const scope = permissions.binding;
|
|
1319
1366
|
if (!scope) {
|
|
1320
1367
|
return "deny";
|
|
@@ -1349,11 +1396,11 @@ function permissionPatternMatches(pattern, value) {
|
|
|
1349
1396
|
.join("");
|
|
1350
1397
|
return new RegExp(`^${source}$`).test(value);
|
|
1351
1398
|
}
|
|
1352
|
-
function
|
|
1353
|
-
return
|
|
1399
|
+
function bindingsNames(bindings) {
|
|
1400
|
+
return bindings.map((bindingCollection) => bindingCollection.name).join(", ");
|
|
1354
1401
|
}
|
|
1355
|
-
function
|
|
1356
|
-
return Object.keys(
|
|
1402
|
+
function bindingNames(bindingCollection) {
|
|
1403
|
+
return Object.keys(bindingCollection.bindings).join(", ");
|
|
1357
1404
|
}
|
|
1358
1405
|
function isHelpFlag(value) {
|
|
1359
1406
|
return value === "--help" || value === "-h";
|
|
@@ -1362,31 +1409,29 @@ function jsonSchemaType(schema) {
|
|
|
1362
1409
|
const schemaObject = asRecord(schema);
|
|
1363
1410
|
return typeof schemaObject.type === "string" ? schemaObject.type : undefined;
|
|
1364
1411
|
}
|
|
1365
|
-
async function
|
|
1366
|
-
if (
|
|
1412
|
+
async function registerBindingsOnSidecar(client, session, vm, bindings) {
|
|
1413
|
+
if (bindings.length === 0) {
|
|
1367
1414
|
return "";
|
|
1368
1415
|
}
|
|
1369
|
-
for (const
|
|
1416
|
+
for (const bindingCollection of bindings) {
|
|
1370
1417
|
await client.registerHostCallbacks(session, vm, {
|
|
1371
|
-
name:
|
|
1372
|
-
description:
|
|
1373
|
-
commandAliases: [`agentos-${
|
|
1418
|
+
name: bindingCollection.name,
|
|
1419
|
+
description: bindingCollection.description,
|
|
1420
|
+
commandAliases: [`agentos-${bindingCollection.name}`],
|
|
1374
1421
|
registryCommandAliases: ["agentos"],
|
|
1375
|
-
callbacks: Object.fromEntries(Object.entries(
|
|
1376
|
-
|
|
1377
|
-
|
|
1422
|
+
callbacks: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
|
|
1423
|
+
bindingName,
|
|
1424
|
+
bindingToSidecarDefinition(definition),
|
|
1378
1425
|
])),
|
|
1379
1426
|
});
|
|
1380
1427
|
}
|
|
1381
|
-
return
|
|
1428
|
+
return buildBindingReference(bindings);
|
|
1382
1429
|
}
|
|
1383
1430
|
export class AgentOs {
|
|
1384
1431
|
#kernel;
|
|
1385
1432
|
sidecar;
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
_sessionClosePromises = new Map();
|
|
1389
|
-
_pendingSessionRequestResolvers = new Map();
|
|
1433
|
+
_durableSessionEventHandlers = new Map();
|
|
1434
|
+
_agentExitHandlers = new Map();
|
|
1390
1435
|
_processes = new Map();
|
|
1391
1436
|
_shells = new Map();
|
|
1392
1437
|
// Value is the recorded exit code (undefined until/unless the exit
|
|
@@ -1398,13 +1443,12 @@ export class AgentOs {
|
|
|
1398
1443
|
_acpTerminalCounter = 0;
|
|
1399
1444
|
_softwareRoots;
|
|
1400
1445
|
_cronManager;
|
|
1401
|
-
|
|
1402
|
-
|
|
1446
|
+
_bindings = [];
|
|
1447
|
+
_bindingReference = "";
|
|
1403
1448
|
_permissions = allowAll;
|
|
1404
1449
|
_hostMounts;
|
|
1405
1450
|
_env;
|
|
1406
1451
|
_rootFilesystem;
|
|
1407
|
-
_additionalInstructions;
|
|
1408
1452
|
_sidecarLease = null;
|
|
1409
1453
|
_sidecarClient;
|
|
1410
1454
|
_sidecarSession;
|
|
@@ -1413,7 +1457,7 @@ export class AgentOs {
|
|
|
1413
1457
|
_agentStderrHandler;
|
|
1414
1458
|
_agentExitHandler;
|
|
1415
1459
|
_limitWarningHandler;
|
|
1416
|
-
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm,
|
|
1460
|
+
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, agentStderrHandler, agentExitHandler, limitWarningHandler) {
|
|
1417
1461
|
this.#kernel = kernel;
|
|
1418
1462
|
this.sidecar = sidecar;
|
|
1419
1463
|
this._softwareRoots = softwareRoots;
|
|
@@ -1423,7 +1467,6 @@ export class AgentOs {
|
|
|
1423
1467
|
this._sidecarClient = sidecarClient;
|
|
1424
1468
|
this._sidecarSession = sidecarSession;
|
|
1425
1469
|
this._sidecarVm = sidecarVm;
|
|
1426
|
-
this._additionalInstructions = additionalInstructions;
|
|
1427
1470
|
this._agentStderrHandler = agentStderrHandler;
|
|
1428
1471
|
this._agentExitHandler = agentExitHandler;
|
|
1429
1472
|
this._limitWarningHandler = limitWarningHandler;
|
|
@@ -1448,7 +1491,7 @@ export class AgentOs {
|
|
|
1448
1491
|
// Default software is FULLY DYNAMIC: this package's own NON-agent
|
|
1449
1492
|
// @agentos-software/* dependencies (e.g. common), each default-exporting
|
|
1450
1493
|
// its registry-built descriptor. Agent packages are NOT projected here —
|
|
1451
|
-
//
|
|
1494
|
+
// openSession({ agent: id }) links the matching agent dependency into the running
|
|
1452
1495
|
// VM on first use, so agent closures (and pi's V8 snapshot bundle) only
|
|
1453
1496
|
// enter VMs that run them. Unbuilt packages throw with build
|
|
1454
1497
|
// instructions; opt out via defaultSoftware: false.
|
|
@@ -1479,9 +1522,9 @@ export class AgentOs {
|
|
|
1479
1522
|
// sidecar owns agent resolution, agent enumeration, and agent snapshot
|
|
1480
1523
|
// bundle loading from the projected package dirs.
|
|
1481
1524
|
const localMounts = await resolveCompatLocalMounts(options?.mounts);
|
|
1482
|
-
const
|
|
1483
|
-
if (
|
|
1484
|
-
|
|
1525
|
+
const bindings = options?.bindings;
|
|
1526
|
+
if (bindings && bindings.length > 0) {
|
|
1527
|
+
validateBindings(bindings);
|
|
1485
1528
|
}
|
|
1486
1529
|
// Resolve the sidecar handle up front so every VM created here leases the
|
|
1487
1530
|
// one shared native sidecar process owned by that handle.
|
|
@@ -1491,13 +1534,13 @@ export class AgentOs {
|
|
|
1491
1534
|
// forwarded `packages` (it owns the staging dir + read-only mount, and
|
|
1492
1535
|
// runtime `linkSoftware` appends to that live dir). The client no longer
|
|
1493
1536
|
// stages packages host-side.
|
|
1494
|
-
const
|
|
1537
|
+
const bindingBootstrapCommands = collectBindingBootstrapCommands(bindings ?? []);
|
|
1495
1538
|
const bootstrapCommands = [
|
|
1496
1539
|
...RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1497
|
-
...
|
|
1540
|
+
...bindingBootstrapCommands,
|
|
1498
1541
|
];
|
|
1499
1542
|
const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem);
|
|
1500
|
-
let
|
|
1543
|
+
let bindingReference = "";
|
|
1501
1544
|
let rootBridge = null;
|
|
1502
1545
|
let kernel = null;
|
|
1503
1546
|
let client = null;
|
|
@@ -1514,7 +1557,7 @@ export class AgentOs {
|
|
|
1514
1557
|
const env = getBaseEnvironment();
|
|
1515
1558
|
// Guest command paths. The sidecar owns the `/opt/agentos` projection and
|
|
1516
1559
|
// reports the exact projected package commands after `configureVm`.
|
|
1517
|
-
//
|
|
1560
|
+
// Binding-shim commands are added below.
|
|
1518
1561
|
const commandGuestPaths = new Map();
|
|
1519
1562
|
const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
|
|
1520
1563
|
mounts: options?.mounts,
|
|
@@ -1532,11 +1575,24 @@ export class AgentOs {
|
|
|
1532
1575
|
const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
|
|
1533
1576
|
const createVmConfig = {
|
|
1534
1577
|
env,
|
|
1578
|
+
database: options?.database,
|
|
1579
|
+
...(options?.user ? { user: options.user } : {}),
|
|
1535
1580
|
rootFilesystem: serializeRootFilesystemForSidecar(options?.rootFilesystem, bootstrapLower),
|
|
1536
1581
|
permissions: sidecarPermissions,
|
|
1537
1582
|
limits: options?.limits,
|
|
1538
1583
|
loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
|
|
1539
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
|
+
: {}),
|
|
1540
1596
|
// 0.3: the Node builtin allow-list moved from configureVm to
|
|
1541
1597
|
// VM creation. `undefined` => engine default allow-list;
|
|
1542
1598
|
// `[]` => deny all; `[..]` => exactly those. Platform and
|
|
@@ -1577,16 +1633,16 @@ export class AgentOs {
|
|
|
1577
1633
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1578
1634
|
packages: sidecarPackages,
|
|
1579
1635
|
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1580
|
-
|
|
1636
|
+
bindingShimCommands: bindingBootstrapCommands,
|
|
1581
1637
|
});
|
|
1582
1638
|
for (const command of configuredVm.projectedCommands) {
|
|
1583
1639
|
commandGuestPaths.set(command.name, command.guestPath);
|
|
1584
1640
|
}
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1641
|
+
if (bindings && bindings.length > 0) {
|
|
1642
|
+
bindingReference = await registerBindingsOnSidecar(client, session, nativeVm, bindings);
|
|
1587
1643
|
commandGuestPaths.set("agentos", "/bin/agentos");
|
|
1588
|
-
for (const
|
|
1589
|
-
commandGuestPaths.set(`agentos-${
|
|
1644
|
+
for (const bindingCollection of bindings) {
|
|
1645
|
+
commandGuestPaths.set(`agentos-${bindingCollection.name}`, `/bin/agentos-${bindingCollection.name}`);
|
|
1590
1646
|
}
|
|
1591
1647
|
}
|
|
1592
1648
|
rootBridge = new NativeSidecarKernelProxy({
|
|
@@ -1600,13 +1656,21 @@ export class AgentOs {
|
|
|
1600
1656
|
permissions: sidecarPermissions,
|
|
1601
1657
|
commandPermissions: {},
|
|
1602
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,
|
|
1603
1665
|
commandGuestPaths,
|
|
1604
1666
|
onDispose: cleanup,
|
|
1605
1667
|
// The native process is owned by the AgentOsSidecar handle and
|
|
1606
1668
|
// shared across VMs; disposing this VM must not kill the process.
|
|
1607
1669
|
ownsClient: false,
|
|
1608
1670
|
});
|
|
1609
|
-
|
|
1671
|
+
if (options?.rootFilesystem?.type !== "native") {
|
|
1672
|
+
await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
|
|
1673
|
+
}
|
|
1610
1674
|
kernel = rootBridge;
|
|
1611
1675
|
const snapshotClient = client;
|
|
1612
1676
|
return {
|
|
@@ -1622,9 +1686,9 @@ export class AgentOs {
|
|
|
1622
1686
|
sidecarSession: session,
|
|
1623
1687
|
sidecarVm: nativeVm,
|
|
1624
1688
|
permissions: hostPermissions,
|
|
1625
|
-
snapshotRootFilesystem: async () => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm))),
|
|
1626
|
-
|
|
1627
|
-
|
|
1689
|
+
snapshotRootFilesystem: async (maxBytes) => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm, maxBytes))),
|
|
1690
|
+
bindings: bindings ?? [],
|
|
1691
|
+
bindingReference,
|
|
1628
1692
|
async dispose() {
|
|
1629
1693
|
if (kernel) {
|
|
1630
1694
|
const currentKernel = kernel;
|
|
@@ -1667,10 +1731,10 @@ export class AgentOs {
|
|
|
1667
1731
|
createVm: async () => createVmAdmin(),
|
|
1668
1732
|
});
|
|
1669
1733
|
const vmAdmin = sidecarLease.admin;
|
|
1670
|
-
const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.
|
|
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);
|
|
1671
1735
|
vm._sidecarLease = sidecarLease;
|
|
1672
|
-
vm.
|
|
1673
|
-
vm.
|
|
1736
|
+
vm._bindings = vmAdmin.bindings;
|
|
1737
|
+
vm._bindingReference = vmAdmin.bindingReference;
|
|
1674
1738
|
vm._permissions = vmAdmin.permissions;
|
|
1675
1739
|
vm._installSidecarRequestHandler();
|
|
1676
1740
|
vm._cronManager = new CronManager(vm, options?.scheduleDriver ?? new TimerScheduleDriver());
|
|
@@ -1688,13 +1752,12 @@ export class AgentOs {
|
|
|
1688
1752
|
const kernel = this.#kernel;
|
|
1689
1753
|
return kernel.execArgv(command, args, options);
|
|
1690
1754
|
}
|
|
1691
|
-
_trackProcess(proc, command, args,
|
|
1755
|
+
_trackProcess(proc, command, args, outputHandlers, exitHandlers) {
|
|
1692
1756
|
const entry = {
|
|
1693
1757
|
proc,
|
|
1694
1758
|
command,
|
|
1695
1759
|
args,
|
|
1696
|
-
|
|
1697
|
-
stderrHandlers,
|
|
1760
|
+
outputHandlers,
|
|
1698
1761
|
exitHandlers,
|
|
1699
1762
|
};
|
|
1700
1763
|
this._processes.set(proc.pid, entry);
|
|
@@ -1705,31 +1768,29 @@ export class AgentOs {
|
|
|
1705
1768
|
// in dispose(). (H5: the leak was that dispose() never cleared it.)
|
|
1706
1769
|
void proc.wait().then((code) => {
|
|
1707
1770
|
for (const h of exitHandlers)
|
|
1708
|
-
h(code);
|
|
1771
|
+
h({ pid: proc.pid, exitCode: code });
|
|
1709
1772
|
});
|
|
1710
1773
|
return { pid: proc.pid };
|
|
1711
1774
|
}
|
|
1712
1775
|
spawn(command, args, options) {
|
|
1713
|
-
const
|
|
1714
|
-
const stderrHandlers = new Set();
|
|
1776
|
+
const outputHandlers = new Set();
|
|
1715
1777
|
const exitHandlers = new Set();
|
|
1716
|
-
// Include caller-provided callbacks in the initial handler sets.
|
|
1717
|
-
if (options?.onStdout)
|
|
1718
|
-
stdoutHandlers.add(options.onStdout);
|
|
1719
|
-
if (options?.onStderr)
|
|
1720
|
-
stderrHandlers.add(options.onStderr);
|
|
1721
1778
|
const proc = this.#kernel.spawn(command, args, {
|
|
1722
1779
|
...options,
|
|
1723
1780
|
onStdout: (data) => {
|
|
1724
|
-
|
|
1725
|
-
|
|
1781
|
+
options?.onStdout?.(data);
|
|
1782
|
+
for (const h of outputHandlers) {
|
|
1783
|
+
h({ pid: proc.pid, stream: "stdout", data });
|
|
1784
|
+
}
|
|
1726
1785
|
},
|
|
1727
1786
|
onStderr: (data) => {
|
|
1728
|
-
|
|
1729
|
-
|
|
1787
|
+
options?.onStderr?.(data);
|
|
1788
|
+
for (const h of outputHandlers) {
|
|
1789
|
+
h({ pid: proc.pid, stream: "stderr", data });
|
|
1790
|
+
}
|
|
1730
1791
|
},
|
|
1731
1792
|
});
|
|
1732
|
-
return this._trackProcess(proc, command, args,
|
|
1793
|
+
return this._trackProcess(proc, command, args, outputHandlers, exitHandlers);
|
|
1733
1794
|
}
|
|
1734
1795
|
/** Write data to a process's stdin. */
|
|
1735
1796
|
writeProcessStdin(pid, data) {
|
|
@@ -1745,24 +1806,14 @@ export class AgentOs {
|
|
|
1745
1806
|
throw new Error(`Process not found: ${pid}`);
|
|
1746
1807
|
return entry.proc.closeStdin();
|
|
1747
1808
|
}
|
|
1748
|
-
/** Subscribe to stdout
|
|
1749
|
-
|
|
1750
|
-
const entry = this._processes.get(pid);
|
|
1751
|
-
if (!entry)
|
|
1752
|
-
throw new Error(`Process not found: ${pid}`);
|
|
1753
|
-
entry.stdoutHandlers.add(handler);
|
|
1754
|
-
return () => {
|
|
1755
|
-
entry.stdoutHandlers.delete(handler);
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
/** Subscribe to stderr data from a process. Returns an unsubscribe function. */
|
|
1759
|
-
onProcessStderr(pid, handler) {
|
|
1809
|
+
/** Subscribe to stdout and stderr from a process. */
|
|
1810
|
+
onProcessOutput(pid, handler) {
|
|
1760
1811
|
const entry = this._processes.get(pid);
|
|
1761
1812
|
if (!entry)
|
|
1762
1813
|
throw new Error(`Process not found: ${pid}`);
|
|
1763
|
-
entry.
|
|
1814
|
+
entry.outputHandlers.add(handler);
|
|
1764
1815
|
return () => {
|
|
1765
|
-
entry.
|
|
1816
|
+
entry.outputHandlers.delete(handler);
|
|
1766
1817
|
};
|
|
1767
1818
|
}
|
|
1768
1819
|
/** Subscribe to process exit. Returns an unsubscribe function. */
|
|
@@ -1772,7 +1823,7 @@ export class AgentOs {
|
|
|
1772
1823
|
throw new Error(`Process not found: ${pid}`);
|
|
1773
1824
|
// If already exited, call immediately.
|
|
1774
1825
|
if (entry.proc.exitCode !== null) {
|
|
1775
|
-
handler(entry.proc.exitCode);
|
|
1826
|
+
handler({ pid, exitCode: entry.proc.exitCode });
|
|
1776
1827
|
return () => { };
|
|
1777
1828
|
}
|
|
1778
1829
|
entry.exitHandlers.add(handler);
|
|
@@ -1875,6 +1926,15 @@ export class AgentOs {
|
|
|
1875
1926
|
this._assertSafeAbsolutePath(path);
|
|
1876
1927
|
return this.#kernel.readdir(path);
|
|
1877
1928
|
}
|
|
1929
|
+
async readdirEntries(path) {
|
|
1930
|
+
this._assertSafeAbsolutePath(path);
|
|
1931
|
+
const entries = await this.#kernel.readdirRecursive(path, { maxDepth: 0 });
|
|
1932
|
+
return entries.map((entry) => ({
|
|
1933
|
+
name: entry.name,
|
|
1934
|
+
isDirectory: entry.isDirectory,
|
|
1935
|
+
isSymbolicLink: entry.isSymbolicLink,
|
|
1936
|
+
}));
|
|
1937
|
+
}
|
|
1878
1938
|
async readdirRecursive(path, options) {
|
|
1879
1939
|
this._assertSafeAbsolutePath(path);
|
|
1880
1940
|
const exclude = options?.exclude ? new Set(options.exclude) : undefined;
|
|
@@ -1913,65 +1973,113 @@ export class AgentOs {
|
|
|
1913
1973
|
this._assertSafeAbsolutePath(path);
|
|
1914
1974
|
return this.#kernel.exists(path);
|
|
1915
1975
|
}
|
|
1916
|
-
async
|
|
1976
|
+
async exportRootFilesystem(options) {
|
|
1977
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0) {
|
|
1978
|
+
throw new RangeError("maxBytes must be a positive safe integer");
|
|
1979
|
+
}
|
|
1917
1980
|
const nativeSnapshot = this._sidecarLease?.admin.snapshotRootFilesystem;
|
|
1918
|
-
|
|
1919
|
-
|
|
1981
|
+
const snapshot = nativeSnapshot
|
|
1982
|
+
? await nativeSnapshot(options.maxBytes)
|
|
1983
|
+
: createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
|
|
1984
|
+
const size = Buffer.byteLength(JSON.stringify(snapshot));
|
|
1985
|
+
if (size > options.maxBytes) {
|
|
1986
|
+
throw new RangeError(`root filesystem export is ${size} bytes, limit is ${options.maxBytes}; raise maxBytes to export this filesystem`);
|
|
1920
1987
|
}
|
|
1921
|
-
return
|
|
1988
|
+
return snapshot;
|
|
1922
1989
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1990
|
+
/**
|
|
1991
|
+
* Mount a filesystem into the running VM. Resolves once the mount has been
|
|
1992
|
+
* delivered to the native sidecar, so guest code can use it immediately
|
|
1993
|
+
* after the returned promise settles; a delivery failure rejects instead of
|
|
1994
|
+
* leaving the mount silently host-only.
|
|
1995
|
+
*/
|
|
1996
|
+
async mountFs(descriptor) {
|
|
1997
|
+
this._assertSafeAbsolutePath(descriptor.path);
|
|
1998
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
1999
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
2000
|
+
}
|
|
2001
|
+
await this.#kernel.mountDescriptor({
|
|
2002
|
+
guestPath: descriptor.path,
|
|
2003
|
+
readOnly: descriptor.readOnly ?? false,
|
|
2004
|
+
plugin: {
|
|
2005
|
+
id: descriptor.plugin.id,
|
|
2006
|
+
config: descriptor.plugin.config ?? {},
|
|
2007
|
+
},
|
|
2008
|
+
});
|
|
1926
2009
|
}
|
|
1927
|
-
unmountFs(path) {
|
|
2010
|
+
async unmountFs(path) {
|
|
1928
2011
|
this._assertSafeAbsolutePath(path);
|
|
1929
|
-
this.#kernel
|
|
2012
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
|
|
2013
|
+
throw new Error("portable dynamic mounts require the native sidecar");
|
|
2014
|
+
}
|
|
2015
|
+
await this.#kernel.unmountDescriptor(path);
|
|
2016
|
+
}
|
|
2017
|
+
async listMounts() {
|
|
2018
|
+
if (!(this.#kernel instanceof NativeSidecarKernelProxy))
|
|
2019
|
+
return [];
|
|
2020
|
+
return this.#kernel.listMounts();
|
|
1930
2021
|
}
|
|
1931
2022
|
async move(from, to) {
|
|
1932
2023
|
this._assertWritableAbsolutePath(from);
|
|
1933
2024
|
this._assertWritableAbsolutePath(to);
|
|
1934
2025
|
await this.#kernel.movePath(from, to);
|
|
1935
2026
|
}
|
|
1936
|
-
async
|
|
2027
|
+
async remove(path, options) {
|
|
1937
2028
|
this._assertWritableAbsolutePath(path);
|
|
1938
2029
|
await this.#kernel.removePath(path, {
|
|
1939
2030
|
recursive: options?.recursive ?? false,
|
|
1940
2031
|
});
|
|
1941
2032
|
}
|
|
1942
|
-
async
|
|
1943
|
-
|
|
2033
|
+
async httpRequest(request) {
|
|
2034
|
+
this._assertSafeAbsolutePath(request.path.split("?")[0] || "/");
|
|
2035
|
+
const method = request.method?.toUpperCase() ?? "GET";
|
|
2036
|
+
const body = typeof request.body === "string"
|
|
2037
|
+
? request.body
|
|
2038
|
+
: request.body === undefined
|
|
2039
|
+
? undefined
|
|
2040
|
+
: new TextDecoder().decode(request.body);
|
|
1944
2041
|
const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
|
|
1945
|
-
port,
|
|
1946
|
-
method
|
|
1947
|
-
path:
|
|
1948
|
-
headersJson: JSON.stringify(
|
|
1949
|
-
...(
|
|
1950
|
-
? { body
|
|
2042
|
+
port: request.port,
|
|
2043
|
+
method,
|
|
2044
|
+
path: request.path,
|
|
2045
|
+
headersJson: JSON.stringify(request.headers ?? {}),
|
|
2046
|
+
...(method !== "GET" && method !== "HEAD" && body !== undefined
|
|
2047
|
+
? { body }
|
|
1951
2048
|
: {}),
|
|
1952
2049
|
}));
|
|
1953
|
-
const headers =
|
|
2050
|
+
const headers = {};
|
|
1954
2051
|
for (const [key, value] of responsePayload.headers ?? []) {
|
|
1955
|
-
headers
|
|
2052
|
+
headers[key] = value;
|
|
1956
2053
|
}
|
|
1957
|
-
return
|
|
2054
|
+
return {
|
|
1958
2055
|
status: responsePayload.status,
|
|
1959
|
-
statusText: responsePayload.statusText,
|
|
2056
|
+
statusText: responsePayload.statusText ?? "",
|
|
1960
2057
|
headers,
|
|
1961
|
-
|
|
2058
|
+
body: new Uint8Array(Buffer.from(responsePayload.body ?? "", "base64")),
|
|
2059
|
+
};
|
|
1962
2060
|
}
|
|
1963
2061
|
openShell(options) {
|
|
1964
2062
|
const shellId = `shell-${++this._shellCounter}`;
|
|
1965
2063
|
this._closedShellIds.delete(shellId);
|
|
1966
2064
|
const dataHandlers = new Set();
|
|
1967
|
-
const
|
|
2065
|
+
const stderrHandlers = new Set();
|
|
2066
|
+
const exitHandlers = new Set();
|
|
2067
|
+
const handle = this.#kernel.openShell({
|
|
2068
|
+
...options,
|
|
2069
|
+
onStderr: (data) => {
|
|
2070
|
+
for (const handler of stderrHandlers)
|
|
2071
|
+
handler({ shellId, data });
|
|
2072
|
+
},
|
|
2073
|
+
});
|
|
1968
2074
|
handle.onData = (data) => {
|
|
1969
|
-
for (const
|
|
1970
|
-
|
|
2075
|
+
for (const handler of dataHandlers)
|
|
2076
|
+
handler({ shellId, data });
|
|
1971
2077
|
};
|
|
1972
2078
|
const entry = {
|
|
1973
2079
|
handle,
|
|
1974
2080
|
dataHandlers,
|
|
2081
|
+
stderrHandlers,
|
|
2082
|
+
exitHandlers,
|
|
1975
2083
|
exitPromise: Promise.resolve(0),
|
|
1976
2084
|
};
|
|
1977
2085
|
const exitPromise = handle.wait();
|
|
@@ -1986,6 +2094,8 @@ export class AgentOs {
|
|
|
1986
2094
|
};
|
|
1987
2095
|
entry.exitPromise = exitPromise.then((exitCode) => {
|
|
1988
2096
|
finalize(exitCode);
|
|
2097
|
+
for (const handler of exitHandlers)
|
|
2098
|
+
handler({ shellId, exitCode });
|
|
1989
2099
|
return exitCode;
|
|
1990
2100
|
}, (error) => {
|
|
1991
2101
|
finalize();
|
|
@@ -2005,7 +2115,11 @@ export class AgentOs {
|
|
|
2005
2115
|
throw new Error(`Shell not found: ${shellId}`);
|
|
2006
2116
|
return entry.handle.write(data);
|
|
2007
2117
|
}
|
|
2008
|
-
/**
|
|
2118
|
+
/**
|
|
2119
|
+
* Subscribe to ordered PTY output (stdout and stderr). Returns an unsubscribe
|
|
2120
|
+
* function. `OpenShellOptions.onStderr` is a diagnostic tap for callers that
|
|
2121
|
+
* need channel identity; do not render both surfaces.
|
|
2122
|
+
*/
|
|
2009
2123
|
onShellData(shellId, handler) {
|
|
2010
2124
|
const entry = this._shells.get(shellId);
|
|
2011
2125
|
if (!entry)
|
|
@@ -2015,6 +2129,28 @@ export class AgentOs {
|
|
|
2015
2129
|
entry.dataHandlers.delete(handler);
|
|
2016
2130
|
};
|
|
2017
2131
|
}
|
|
2132
|
+
/** Subscribe to the stderr-only diagnostic stream for a shell. */
|
|
2133
|
+
onShellStderr(shellId, handler) {
|
|
2134
|
+
const entry = this._shells.get(shellId);
|
|
2135
|
+
if (!entry)
|
|
2136
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2137
|
+
entry.stderrHandlers.add(handler);
|
|
2138
|
+
return () => entry.stderrHandlers.delete(handler);
|
|
2139
|
+
}
|
|
2140
|
+
/** Subscribe to shell exit. */
|
|
2141
|
+
onShellExit(shellId, handler) {
|
|
2142
|
+
const entry = this._shells.get(shellId);
|
|
2143
|
+
if (!entry) {
|
|
2144
|
+
const exitCode = this._closedShellIds.get(shellId);
|
|
2145
|
+
if (exitCode !== undefined) {
|
|
2146
|
+
handler({ shellId, exitCode });
|
|
2147
|
+
return () => { };
|
|
2148
|
+
}
|
|
2149
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2150
|
+
}
|
|
2151
|
+
entry.exitHandlers.add(handler);
|
|
2152
|
+
return () => entry.exitHandlers.delete(handler);
|
|
2153
|
+
}
|
|
2018
2154
|
/** Notify a shell of terminal resize. */
|
|
2019
2155
|
resizeShell(shellId, cols, rows) {
|
|
2020
2156
|
const entry = this._shells.get(shellId);
|
|
@@ -2136,50 +2272,231 @@ export class AgentOs {
|
|
|
2136
2272
|
return;
|
|
2137
2273
|
entry.proc.kill(9);
|
|
2138
2274
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2275
|
+
async openSession(input) {
|
|
2276
|
+
const response = await this._sendAcpRequest({
|
|
2277
|
+
tag: "AcpOpenSessionRequest",
|
|
2278
|
+
val: {
|
|
2279
|
+
sessionId: input.sessionId ?? null,
|
|
2280
|
+
agent: input.agent,
|
|
2281
|
+
cwd: input.cwd ?? null,
|
|
2282
|
+
additionalDirectories: input.additionalDirectories === undefined
|
|
2283
|
+
? null
|
|
2284
|
+
: JSON.stringify(input.additionalDirectories),
|
|
2285
|
+
env: input.env === undefined ? null : JSON.stringify(input.env),
|
|
2286
|
+
mcpServers: input.mcpServers === undefined
|
|
2287
|
+
? null
|
|
2288
|
+
: JSON.stringify(input.mcpServers),
|
|
2289
|
+
permissionPolicy: input.permissionPolicy ?? null,
|
|
2290
|
+
skipOsInstructions: input.skipOsInstructions ?? null,
|
|
2291
|
+
additionalInstructions: combineInstructions(input.additionalInstructions, this._bindingReference) ?? null,
|
|
2292
|
+
},
|
|
2293
|
+
});
|
|
2294
|
+
if (response.tag !== "AcpOpenSessionResponse") {
|
|
2295
|
+
throw new Error(`unexpected openSession response: ${response.tag}`);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
async getSession(input) {
|
|
2299
|
+
const response = await this._sendAcpRequest({
|
|
2300
|
+
tag: "AcpGetDurableSessionRequest",
|
|
2301
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2302
|
+
});
|
|
2303
|
+
if (response.tag !== "AcpGetDurableSessionResponse") {
|
|
2304
|
+
throw new Error(`unexpected getSession response: ${response.tag}`);
|
|
2305
|
+
}
|
|
2306
|
+
return decodeDurableSessionInfo(response.val.session);
|
|
2307
|
+
}
|
|
2308
|
+
async listSessions(input) {
|
|
2309
|
+
const response = await this._sendAcpRequest({
|
|
2310
|
+
tag: "AcpListDurableSessionsRequest",
|
|
2311
|
+
val: { cursor: input?.cursor ?? null, limit: input?.limit ?? null },
|
|
2312
|
+
});
|
|
2313
|
+
if (response.tag !== "AcpListDurableSessionsResponse") {
|
|
2314
|
+
throw new Error(`unexpected listSessions response: ${response.tag}`);
|
|
2315
|
+
}
|
|
2316
|
+
return {
|
|
2317
|
+
sessions: response.val.sessions.map(decodeDurableSessionInfo),
|
|
2318
|
+
nextCursor: response.val.nextCursor,
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
async deleteSession(input = {}) {
|
|
2322
|
+
const response = await this._sendAcpRequest({
|
|
2323
|
+
tag: "AcpDeleteSessionRequest",
|
|
2324
|
+
val: { sessionId: input.sessionId ?? null },
|
|
2325
|
+
});
|
|
2326
|
+
if (response.tag !== "AcpDeleteSessionResponse") {
|
|
2327
|
+
throw new Error(`unexpected deleteSession response: ${response.tag}`);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
async unloadSession(input) {
|
|
2331
|
+
const response = await this._sendAcpRequest({
|
|
2332
|
+
tag: "AcpUnloadSessionRequest",
|
|
2333
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2334
|
+
});
|
|
2335
|
+
if (response.tag !== "AcpUnloadSessionResponse") {
|
|
2336
|
+
throw new Error(`unexpected unloadSession response: ${response.tag}`);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
async prompt(input) {
|
|
2340
|
+
const response = await this._sendAcpRequest({
|
|
2341
|
+
tag: "AcpPromptRequest",
|
|
2342
|
+
val: {
|
|
2343
|
+
sessionId: input.sessionId ?? null,
|
|
2344
|
+
idempotencyKey: input.idempotencyKey ?? null,
|
|
2345
|
+
content: JSON.stringify(input.content),
|
|
2346
|
+
},
|
|
2347
|
+
});
|
|
2348
|
+
if (response.tag !== "AcpPromptResponse") {
|
|
2349
|
+
throw new Error(`unexpected prompt response: ${response.tag}`);
|
|
2350
|
+
}
|
|
2351
|
+
return {
|
|
2352
|
+
sessionId: response.val.sessionId,
|
|
2353
|
+
message: response.val.message === null ? null : JSON.parse(response.val.message),
|
|
2354
|
+
stopReason: response.val.stopReason,
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
async cancelPrompt(input) {
|
|
2358
|
+
const response = await this._sendAcpRequest({
|
|
2359
|
+
tag: "AcpCancelPromptRequest",
|
|
2360
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2361
|
+
});
|
|
2362
|
+
if (response.tag !== "AcpCancelPromptResponse") {
|
|
2363
|
+
throw new Error(`unexpected cancelPrompt response: ${response.tag}`);
|
|
2364
|
+
}
|
|
2365
|
+
if (response.val.status !== "cancelled" &&
|
|
2366
|
+
response.val.status !== "no_active_prompt") {
|
|
2367
|
+
throw new Error(`invalid cancelPrompt status: ${response.val.status}`);
|
|
2368
|
+
}
|
|
2369
|
+
return { status: response.val.status };
|
|
2370
|
+
}
|
|
2371
|
+
async respondPermission(input) {
|
|
2372
|
+
const response = await this._sendAcpRequest({
|
|
2373
|
+
tag: "AcpRespondPermissionRequest",
|
|
2374
|
+
val: {
|
|
2375
|
+
sessionId: input.sessionId,
|
|
2376
|
+
requestId: input.requestId,
|
|
2377
|
+
optionId: input.optionId,
|
|
2378
|
+
},
|
|
2379
|
+
});
|
|
2380
|
+
if (response.tag !== "AcpRespondPermissionResponse") {
|
|
2381
|
+
throw new Error(`unexpected respondPermission response: ${response.tag}`);
|
|
2382
|
+
}
|
|
2383
|
+
if (response.val.status !== "accepted" &&
|
|
2384
|
+
response.val.status !== "not_pending") {
|
|
2385
|
+
throw new Error(`invalid respondPermission status: ${response.val.status}`);
|
|
2386
|
+
}
|
|
2387
|
+
if (response.val.status === "accepted") {
|
|
2388
|
+
if (response.val.reason !== null) {
|
|
2389
|
+
throw new Error("accepted permission response must not include a reason");
|
|
2390
|
+
}
|
|
2391
|
+
return { status: "accepted" };
|
|
2392
|
+
}
|
|
2393
|
+
return {
|
|
2394
|
+
status: "not_pending",
|
|
2395
|
+
reason: permissionTerminalReason(response.val.reason),
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
async readHistory(input) {
|
|
2399
|
+
const response = await this._sendAcpRequest({
|
|
2400
|
+
tag: "AcpReadHistoryRequest",
|
|
2401
|
+
val: {
|
|
2402
|
+
sessionId: input?.sessionId ?? null,
|
|
2403
|
+
before: input?.before === undefined ? null : BigInt(input.before),
|
|
2404
|
+
after: input?.after === undefined ? null : BigInt(input.after),
|
|
2405
|
+
limit: input?.limit ?? null,
|
|
2406
|
+
},
|
|
2407
|
+
});
|
|
2408
|
+
if (response.tag !== "AcpHistoryPageResponse") {
|
|
2409
|
+
throw new Error(`unexpected readHistory response: ${response.tag}`);
|
|
2410
|
+
}
|
|
2411
|
+
return {
|
|
2412
|
+
events: response.val.events.map(decodeDurableSessionEvent),
|
|
2413
|
+
hasMoreBefore: response.val.hasMoreBefore,
|
|
2414
|
+
hasMoreAfter: response.val.hasMoreAfter,
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
async getSessionConfig(input) {
|
|
2418
|
+
const response = await this._sendAcpRequest({
|
|
2419
|
+
tag: "AcpGetSessionConfigRequest",
|
|
2420
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2421
|
+
});
|
|
2422
|
+
if (response.tag !== "AcpSessionConfigResponse") {
|
|
2423
|
+
throw new Error(`unexpected getSessionConfig response: ${response.tag}`);
|
|
2424
|
+
}
|
|
2425
|
+
return {
|
|
2426
|
+
revision: safeWireU64(response.val.revision),
|
|
2427
|
+
options: JSON.parse(response.val.options),
|
|
2428
|
+
};
|
|
2145
2429
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2430
|
+
async setSessionConfigOption(input) {
|
|
2431
|
+
const response = await this._sendAcpRequest({
|
|
2432
|
+
tag: "AcpSetSessionConfigOptionRequest",
|
|
2433
|
+
val: {
|
|
2434
|
+
sessionId: input.sessionId ?? null,
|
|
2435
|
+
configId: input.configId,
|
|
2436
|
+
value: JSON.stringify(input.value),
|
|
2437
|
+
},
|
|
2438
|
+
});
|
|
2439
|
+
if (response.tag !== "AcpSessionConfigResponse") {
|
|
2440
|
+
throw new Error(`unexpected setSessionConfigOption response: ${response.tag}`);
|
|
2151
2441
|
}
|
|
2152
|
-
return
|
|
2442
|
+
return {
|
|
2443
|
+
revision: safeWireU64(response.val.revision),
|
|
2444
|
+
options: JSON.parse(response.val.options),
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
async getSessionCapabilities(input) {
|
|
2448
|
+
const response = await this._sendAcpRequest({
|
|
2449
|
+
tag: "AcpGetSessionCapabilitiesRequest",
|
|
2450
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2451
|
+
});
|
|
2452
|
+
if (response.tag !== "AcpSessionCapabilitiesResponse") {
|
|
2453
|
+
throw new Error(`unexpected getSessionCapabilities response: ${response.tag}`);
|
|
2454
|
+
}
|
|
2455
|
+
return response.val.capabilities === null
|
|
2456
|
+
? null
|
|
2457
|
+
: normalizeSessionCapabilities(JSON.parse(response.val.capabilities));
|
|
2458
|
+
}
|
|
2459
|
+
async getSessionAgentInfo(input) {
|
|
2460
|
+
const response = await this._sendAcpRequest({
|
|
2461
|
+
tag: "AcpGetSessionAgentInfoRequest",
|
|
2462
|
+
val: { sessionId: input?.sessionId ?? null },
|
|
2463
|
+
});
|
|
2464
|
+
if (response.tag !== "AcpSessionAgentInfoResponse") {
|
|
2465
|
+
throw new Error(`unexpected getSessionAgentInfo response: ${response.tag}`);
|
|
2466
|
+
}
|
|
2467
|
+
return response.val.agentInfo === null
|
|
2468
|
+
? null
|
|
2469
|
+
: JSON.parse(response.val.agentInfo);
|
|
2153
2470
|
}
|
|
2154
2471
|
/**
|
|
2155
2472
|
* Dynamically link a software package into the RUNNING VM. The package's
|
|
2156
2473
|
* `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
|
|
2157
2474
|
* pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
|
|
2158
2475
|
* writing into its staging dir is reflected live with no reboot. An `agent`
|
|
2159
|
-
* block registers the package for `
|
|
2476
|
+
* block registers the package for `openSession({ agent: name })`. Persists for the VM's
|
|
2160
2477
|
* lifetime (and across a snapshot iff the volume persists).
|
|
2161
2478
|
*/
|
|
2162
2479
|
async linkSoftware(descriptor) {
|
|
2163
|
-
const ref = normalizePackageRef(descriptor);
|
|
2164
|
-
if (!ref) {
|
|
2165
|
-
throw new Error("Invalid agentOS package reference");
|
|
2166
|
-
}
|
|
2167
2480
|
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
2168
2481
|
// appends the package to its live host-backed staging dir; the commands
|
|
2169
2482
|
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
2170
2483
|
// duplicate command, surfaced here as a thrown error.
|
|
2171
|
-
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm,
|
|
2484
|
+
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, descriptor);
|
|
2172
2485
|
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
2173
2486
|
this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
|
|
2174
2487
|
command.name,
|
|
2175
2488
|
command.guestPath,
|
|
2176
2489
|
])));
|
|
2490
|
+
// Retain the linked package for runtime mount reconfigures:
|
|
2491
|
+
// `configure_vm` is replace-on-write, so a later `mountFs` that
|
|
2492
|
+
// resent only the boot packages would unproject this one.
|
|
2493
|
+
this.#kernel.registerLinkedPackage(descriptor);
|
|
2177
2494
|
}
|
|
2178
2495
|
// The client parses no manifests: an `agent` block in the linked package is
|
|
2179
2496
|
// picked up by the sidecar (it owns the projected `/opt/agentos` and answers
|
|
2180
|
-
//
|
|
2497
|
+
// openSession/listAgents from it). Nothing to record client-side.
|
|
2181
2498
|
}
|
|
2182
|
-
async
|
|
2499
|
+
async listSoftware() {
|
|
2183
2500
|
return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
|
|
2184
2501
|
}
|
|
2185
2502
|
/**
|
|
@@ -2199,145 +2516,10 @@ export class AgentOs {
|
|
|
2199
2516
|
return response.val.agents.map((agent) => ({
|
|
2200
2517
|
id: agent.id,
|
|
2201
2518
|
installed: agent.installed,
|
|
2202
|
-
adapterEntrypoint: agent.adapterEntrypoint,
|
|
2203
2519
|
}));
|
|
2204
2520
|
}
|
|
2205
|
-
_syncSessionState(session, state) {
|
|
2206
|
-
session.processId = state.processId;
|
|
2207
|
-
session.pid = state.pid ?? null;
|
|
2208
|
-
session.closed = state.closed;
|
|
2209
|
-
session.modes = toSessionModes(state.modes);
|
|
2210
|
-
session.configOptions = toSessionConfigOptions(state.configOptions);
|
|
2211
|
-
this._applySyntheticConfigOverrides(session);
|
|
2212
|
-
session.capabilities = toAgentCapabilities(state.agentCapabilities);
|
|
2213
|
-
session.agentInfo = toAgentInfo(state.agentInfo);
|
|
2214
|
-
}
|
|
2215
|
-
_applySessionUpdate(session, notification) {
|
|
2216
|
-
if (notification.method !== "session/update") {
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
const params = toRecord(notification.params);
|
|
2220
|
-
const update = toRecord(params.update ?? params);
|
|
2221
|
-
const sessionUpdate = update.sessionUpdate;
|
|
2222
|
-
if (sessionUpdate === "current_mode_update" &&
|
|
2223
|
-
typeof update.currentModeId === "string" &&
|
|
2224
|
-
session.modes) {
|
|
2225
|
-
session.modes = {
|
|
2226
|
-
...session.modes,
|
|
2227
|
-
currentModeId: update.currentModeId,
|
|
2228
|
-
};
|
|
2229
|
-
}
|
|
2230
|
-
if ((sessionUpdate === "config_option_update" ||
|
|
2231
|
-
sessionUpdate === "config_options_update") &&
|
|
2232
|
-
Array.isArray(update.configOptions)) {
|
|
2233
|
-
session.configOptions = update.configOptions;
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
_recordSessionNotification(session, notification) {
|
|
2237
|
-
this._applySessionUpdate(session, notification);
|
|
2238
|
-
if (shouldDispatchToSessionEventHandlers(notification)) {
|
|
2239
|
-
this._dispatchSessionEvent(session, notification);
|
|
2240
|
-
}
|
|
2241
|
-
if (notification.method === LEGACY_PERMISSION_METHOD ||
|
|
2242
|
-
notification.method === ACP_PERMISSION_METHOD) {
|
|
2243
|
-
const params = toRecord(notification.params);
|
|
2244
|
-
const permissionId = params.permissionId;
|
|
2245
|
-
if (typeof permissionId === "string" ||
|
|
2246
|
-
typeof permissionId === "number") {
|
|
2247
|
-
const request = {
|
|
2248
|
-
permissionId: String(permissionId),
|
|
2249
|
-
description: typeof params.description === "string"
|
|
2250
|
-
? params.description
|
|
2251
|
-
: undefined,
|
|
2252
|
-
params,
|
|
2253
|
-
};
|
|
2254
|
-
for (const handler of session.permissionHandlers) {
|
|
2255
|
-
handler(request);
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
_dispatchSessionEvent(session, notification) {
|
|
2261
|
-
if (session.eventHandlers.size === 0) {
|
|
2262
|
-
return;
|
|
2263
|
-
}
|
|
2264
|
-
for (const subscriber of [...session.eventHandlers]) {
|
|
2265
|
-
try {
|
|
2266
|
-
subscriber.handler(notification);
|
|
2267
|
-
}
|
|
2268
|
-
catch {
|
|
2269
|
-
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
_subscribeSessionEvents(session, handler) {
|
|
2274
|
-
const subscriber = {
|
|
2275
|
-
handler,
|
|
2276
|
-
};
|
|
2277
|
-
session.eventHandlers.add(subscriber);
|
|
2278
|
-
return () => {
|
|
2279
|
-
session.eventHandlers.delete(subscriber);
|
|
2280
|
-
};
|
|
2281
|
-
}
|
|
2282
|
-
/**
|
|
2283
|
-
* Warn once per session (host-visible) that a tool-permission request was
|
|
2284
|
-
* auto-denied because no `onPermissionRequest` handler is registered. Shared
|
|
2285
|
-
* by both the bare-callback and JSON-RPC permission paths so the message and
|
|
2286
|
-
* the once-per-session guard cannot drift between them.
|
|
2287
|
-
*/
|
|
2288
|
-
_warnNoPermissionHandlerOnce(session, params) {
|
|
2289
|
-
if (session.warnedNoPermissionHandler) {
|
|
2290
|
-
return;
|
|
2291
|
-
}
|
|
2292
|
-
session.warnedNoPermissionHandler = true;
|
|
2293
|
-
this._emitSessionWarning(session, `agentos: a tool-permission request (${this._permissionToolLabel(params)}) was ` +
|
|
2294
|
-
`auto-denied because no onPermissionRequest handler is registered for session ` +
|
|
2295
|
-
`${session.sessionId}. Register one with vm.onPermissionRequest(sessionId, ...) and ` +
|
|
2296
|
-
`reply via vm.respondPermission(...) to let the agent use tools.`);
|
|
2297
|
-
}
|
|
2298
|
-
/** Best-effort human label for the tool named in a permission request. */
|
|
2299
|
-
_permissionToolLabel(params) {
|
|
2300
|
-
if (typeof params.toolName === "string") {
|
|
2301
|
-
return params.toolName;
|
|
2302
|
-
}
|
|
2303
|
-
const toolCall = params.toolCall;
|
|
2304
|
-
if (toolCall &&
|
|
2305
|
-
typeof toolCall === "object" &&
|
|
2306
|
-
typeof toolCall.title === "string") {
|
|
2307
|
-
return toolCall.title;
|
|
2308
|
-
}
|
|
2309
|
-
return "a tool";
|
|
2310
|
-
}
|
|
2311
|
-
/**
|
|
2312
|
-
* Emit a host-visible warning for a session through the same agent-process log
|
|
2313
|
-
* channel that surfaces adapter stderr (`onAgentStderr`, default: process
|
|
2314
|
-
* stderr). Used for agent-os-owned diagnostics — e.g. a permission request
|
|
2315
|
-
* that was auto-denied because no host hook is registered — so they never fire
|
|
2316
|
-
* silently inside the sidecar.
|
|
2317
|
-
*/
|
|
2318
|
-
_emitSessionWarning(session, message) {
|
|
2319
|
-
const handler = this._agentStderrHandler;
|
|
2320
|
-
if (!handler) {
|
|
2321
|
-
return;
|
|
2322
|
-
}
|
|
2323
|
-
try {
|
|
2324
|
-
handler({
|
|
2325
|
-
sessionId: session.sessionId,
|
|
2326
|
-
agentType: session.agentType,
|
|
2327
|
-
processId: session.processId,
|
|
2328
|
-
pid: session.pid,
|
|
2329
|
-
chunk: new TextEncoder().encode(`${message}\n`),
|
|
2330
|
-
});
|
|
2331
|
-
}
|
|
2332
|
-
catch {
|
|
2333
|
-
// A warning sink failure must never affect permission handling.
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
2521
|
_recordAgentStderr(event) {
|
|
2337
|
-
|
|
2338
|
-
[...this._sessions.values()].find((candidate) => candidate.processId === event.processId);
|
|
2339
|
-
const sessionId = event.sessionId || session?.sessionId;
|
|
2340
|
-
if (!sessionId) {
|
|
2522
|
+
if (!event.sessionId) {
|
|
2341
2523
|
return;
|
|
2342
2524
|
}
|
|
2343
2525
|
const handler = this._agentStderrHandler;
|
|
@@ -2346,52 +2528,47 @@ export class AgentOs {
|
|
|
2346
2528
|
}
|
|
2347
2529
|
try {
|
|
2348
2530
|
handler({
|
|
2349
|
-
sessionId,
|
|
2350
|
-
agentType: event.agentType
|
|
2531
|
+
sessionId: event.sessionId,
|
|
2532
|
+
agentType: event.agentType,
|
|
2351
2533
|
processId: event.processId,
|
|
2352
|
-
pid:
|
|
2534
|
+
pid: null,
|
|
2353
2535
|
chunk: new Uint8Array(event.chunk),
|
|
2354
2536
|
});
|
|
2355
2537
|
}
|
|
2356
|
-
catch {
|
|
2357
|
-
|
|
2538
|
+
catch (error) {
|
|
2539
|
+
console.error("AgentOS stderr handler failed", error);
|
|
2358
2540
|
}
|
|
2359
2541
|
}
|
|
2360
2542
|
_recordAgentExit(event) {
|
|
2361
|
-
const
|
|
2543
|
+
const publicEvent = {
|
|
2544
|
+
sessionId: event.sessionId,
|
|
2545
|
+
agentType: event.agentType,
|
|
2546
|
+
processId: event.processId,
|
|
2547
|
+
pid: event.pid,
|
|
2548
|
+
exitCode: event.exitCode,
|
|
2549
|
+
restart: event.restart,
|
|
2550
|
+
restartCount: event.restartCount,
|
|
2551
|
+
maxRestarts: event.maxRestarts,
|
|
2552
|
+
};
|
|
2362
2553
|
const handler = this._agentExitHandler;
|
|
2363
|
-
if (
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
processId: event.processId,
|
|
2371
|
-
pid: session?.pid ?? null,
|
|
2372
|
-
exitCode: event.exitCode,
|
|
2373
|
-
restart: event.restart,
|
|
2374
|
-
restartCount: event.restartCount,
|
|
2375
|
-
maxRestarts: event.maxRestarts,
|
|
2376
|
-
});
|
|
2377
|
-
}
|
|
2378
|
-
catch {
|
|
2379
|
-
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2554
|
+
if (handler) {
|
|
2555
|
+
try {
|
|
2556
|
+
handler(publicEvent);
|
|
2557
|
+
}
|
|
2558
|
+
catch (error) {
|
|
2559
|
+
console.error("AgentOS agent-exit handler failed", error);
|
|
2560
|
+
}
|
|
2380
2561
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2562
|
+
for (const key of ["*", event.sessionId]) {
|
|
2563
|
+
for (const subscription of this._agentExitHandlers.get(key) ?? []) {
|
|
2564
|
+
try {
|
|
2565
|
+
subscription(publicEvent);
|
|
2566
|
+
}
|
|
2567
|
+
catch (error) {
|
|
2568
|
+
console.error("AgentOS agent-exit subscription failed", error);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2385
2571
|
}
|
|
2386
|
-
session.configOptions = session.configOptions.map((option) => {
|
|
2387
|
-
const override = session.configOverrides.get(option.id) ??
|
|
2388
|
-
(typeof option.category === "string"
|
|
2389
|
-
? session.configOverrides.get(option.category)
|
|
2390
|
-
: undefined);
|
|
2391
|
-
return override === undefined
|
|
2392
|
-
? option
|
|
2393
|
-
: { ...option, currentValue: override };
|
|
2394
|
-
});
|
|
2395
2572
|
}
|
|
2396
2573
|
_handleSidecarEvent(event) {
|
|
2397
2574
|
if (event.payload.type === "ext") {
|
|
@@ -2403,25 +2580,6 @@ export class AgentOs {
|
|
|
2403
2580
|
}
|
|
2404
2581
|
if (event.payload.name === "limit_warning") {
|
|
2405
2582
|
this._handleLimitWarning(event.payload.detail);
|
|
2406
|
-
return;
|
|
2407
|
-
}
|
|
2408
|
-
if (event.payload.name !== "acp.session_event") {
|
|
2409
|
-
return;
|
|
2410
|
-
}
|
|
2411
|
-
const sessionId = event.payload.detail.session_id;
|
|
2412
|
-
const session = sessionId ? this._sessions.get(sessionId) : undefined;
|
|
2413
|
-
if (!session) {
|
|
2414
|
-
return;
|
|
2415
|
-
}
|
|
2416
|
-
const notificationText = event.payload.detail.notification;
|
|
2417
|
-
if (typeof notificationText !== "string") {
|
|
2418
|
-
return;
|
|
2419
|
-
}
|
|
2420
|
-
try {
|
|
2421
|
-
this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(notificationText)));
|
|
2422
|
-
}
|
|
2423
|
-
catch {
|
|
2424
|
-
// Ignore malformed event payloads from the sidecar.
|
|
2425
2583
|
}
|
|
2426
2584
|
}
|
|
2427
2585
|
_handleLimitWarning(detail) {
|
|
@@ -2441,8 +2599,8 @@ export class AgentOs {
|
|
|
2441
2599
|
fillPercent: toNumber(detail.fillPercent),
|
|
2442
2600
|
});
|
|
2443
2601
|
}
|
|
2444
|
-
catch {
|
|
2445
|
-
|
|
2602
|
+
catch (error) {
|
|
2603
|
+
console.error("AgentOS limit-warning handler failed", error);
|
|
2446
2604
|
}
|
|
2447
2605
|
}
|
|
2448
2606
|
_handleAcpExtEvent(envelope) {
|
|
@@ -2452,14 +2610,24 @@ export class AgentOs {
|
|
|
2452
2610
|
try {
|
|
2453
2611
|
const event = decodeAcpEvent(envelope.payload);
|
|
2454
2612
|
switch (event.tag) {
|
|
2455
|
-
case "
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2613
|
+
case "AcpDurableSessionEvent": {
|
|
2614
|
+
this._emitDurableSessionEvent(decodeDurableSessionEvent(event.val));
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
case "AcpEphemeralSessionUpdateEvent": {
|
|
2618
|
+
const update = JSON.parse(event.val.update);
|
|
2619
|
+
const { sessionUpdate: type, ...payload } = update;
|
|
2620
|
+
this._emitDurableSessionEvent({
|
|
2621
|
+
durability: "ephemeral",
|
|
2622
|
+
type,
|
|
2623
|
+
sessionId: event.val.sessionId,
|
|
2624
|
+
afterSequence: safeWireU64(event.val.afterSequence),
|
|
2625
|
+
...payload,
|
|
2626
|
+
});
|
|
2461
2627
|
return;
|
|
2462
2628
|
}
|
|
2629
|
+
case "AcpSessionEvent":
|
|
2630
|
+
return;
|
|
2463
2631
|
case "AcpAgentStderrEvent": {
|
|
2464
2632
|
this._recordAgentStderr(event.val);
|
|
2465
2633
|
return;
|
|
@@ -2470,22 +2638,19 @@ export class AgentOs {
|
|
|
2470
2638
|
}
|
|
2471
2639
|
}
|
|
2472
2640
|
}
|
|
2473
|
-
catch {
|
|
2474
|
-
|
|
2641
|
+
catch (error) {
|
|
2642
|
+
console.error("AgentOS failed to decode an ACP sidecar event", error);
|
|
2475
2643
|
}
|
|
2476
2644
|
}
|
|
2477
|
-
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
message,
|
|
2487
|
-
},
|
|
2488
|
-
};
|
|
2645
|
+
_emitDurableSessionEvent(entry) {
|
|
2646
|
+
for (const handler of this._durableSessionEventHandlers.get(entry.sessionId) ?? []) {
|
|
2647
|
+
try {
|
|
2648
|
+
handler(entry);
|
|
2649
|
+
}
|
|
2650
|
+
catch (error) {
|
|
2651
|
+
console.error("AgentOS session event handler failed", error);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2489
2654
|
}
|
|
2490
2655
|
async _sendAcpRequest(request) {
|
|
2491
2656
|
const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
|
|
@@ -2503,300 +2668,10 @@ export class AgentOs {
|
|
|
2503
2668
|
}
|
|
2504
2669
|
return response;
|
|
2505
2670
|
}
|
|
2506
|
-
async _sendSessionRequest(sessionId, method, params) {
|
|
2507
|
-
const session = this._requireSession(sessionId);
|
|
2508
|
-
const response = await new Promise((resolve, reject) => {
|
|
2509
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId) ?? new Set();
|
|
2510
|
-
const resolver = {
|
|
2511
|
-
method,
|
|
2512
|
-
resolve: (response) => {
|
|
2513
|
-
resolve(response);
|
|
2514
|
-
},
|
|
2515
|
-
};
|
|
2516
|
-
resolvers.add(resolver);
|
|
2517
|
-
this._pendingSessionRequestResolvers.set(sessionId, resolvers);
|
|
2518
|
-
void this._sendAcpRequest({
|
|
2519
|
-
tag: "AcpSessionRequest",
|
|
2520
|
-
val: {
|
|
2521
|
-
sessionId,
|
|
2522
|
-
method,
|
|
2523
|
-
params: params === undefined ? null : JSON.stringify(params),
|
|
2524
|
-
},
|
|
2525
|
-
})
|
|
2526
|
-
.then((response) => {
|
|
2527
|
-
if (response.tag !== "AcpSessionRpcResponse") {
|
|
2528
|
-
throw new Error(`unexpected response to AcpSessionRequest: ${response.tag}`);
|
|
2529
|
-
}
|
|
2530
|
-
return toJsonRpcResponse(JSON.parse(response.val.response));
|
|
2531
|
-
})
|
|
2532
|
-
.then(resolve, reject)
|
|
2533
|
-
.finally(() => {
|
|
2534
|
-
const nextResolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2535
|
-
if (!nextResolvers) {
|
|
2536
|
-
return;
|
|
2537
|
-
}
|
|
2538
|
-
nextResolvers.delete(resolver);
|
|
2539
|
-
if (nextResolvers.size === 0) {
|
|
2540
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2541
|
-
}
|
|
2542
|
-
});
|
|
2543
|
-
});
|
|
2544
|
-
const liveSession = this._sessions.get(sessionId);
|
|
2545
|
-
if (liveSession && !isLocalCancelledPromptResponse(method, response)) {
|
|
2546
|
-
await this._hydrateSessionState(liveSession).catch(() => { });
|
|
2547
|
-
}
|
|
2548
|
-
if (!response.error) {
|
|
2549
|
-
if (method === "session/set_mode" &&
|
|
2550
|
-
typeof params?.modeId === "string" &&
|
|
2551
|
-
session.modes) {
|
|
2552
|
-
session.modes = {
|
|
2553
|
-
...session.modes,
|
|
2554
|
-
currentModeId: params.modeId,
|
|
2555
|
-
};
|
|
2556
|
-
}
|
|
2557
|
-
if (method === "session/set_config_option" &&
|
|
2558
|
-
typeof params?.configId === "string" &&
|
|
2559
|
-
typeof params?.value === "string") {
|
|
2560
|
-
const nextValue = params.value;
|
|
2561
|
-
const updatedOption = session.configOptions.find((option) => option.id === params.configId);
|
|
2562
|
-
session.configOverrides.set(params.configId, nextValue);
|
|
2563
|
-
if (typeof updatedOption?.category === "string") {
|
|
2564
|
-
session.configOverrides.set(updatedOption.category, nextValue);
|
|
2565
|
-
}
|
|
2566
|
-
session.configOptions = session.configOptions.map((option) => option.id === params.configId
|
|
2567
|
-
? { ...option, currentValue: nextValue }
|
|
2568
|
-
: option);
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
return response;
|
|
2572
|
-
}
|
|
2573
|
-
async _setSessionConfigByCategory(sessionId, category, value) {
|
|
2574
|
-
const session = this._requireSession(sessionId);
|
|
2575
|
-
const option = session.configOptions.find((entry) => entry.category === category);
|
|
2576
|
-
if (option?.readOnly) {
|
|
2577
|
-
return this._unsupportedConfigResponse(session.agentType, category);
|
|
2578
|
-
}
|
|
2579
|
-
const response = await this._sendSessionRequest(sessionId, "session/set_config_option", {
|
|
2580
|
-
configId: option?.id ?? category,
|
|
2581
|
-
value,
|
|
2582
|
-
});
|
|
2583
|
-
return response;
|
|
2584
|
-
}
|
|
2585
|
-
_removeSession(sessionId) {
|
|
2586
|
-
this._sessions.delete(sessionId);
|
|
2587
|
-
}
|
|
2588
|
-
_abortPendingSessionRequests(sessionId) {
|
|
2589
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2590
|
-
if (!resolvers) {
|
|
2591
|
-
return;
|
|
2592
|
-
}
|
|
2593
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2594
|
-
const response = {
|
|
2595
|
-
jsonrpc: "2.0",
|
|
2596
|
-
id: null,
|
|
2597
|
-
error: {
|
|
2598
|
-
code: -32_000,
|
|
2599
|
-
message: `Session closed: ${sessionId}`,
|
|
2600
|
-
},
|
|
2601
|
-
};
|
|
2602
|
-
for (const resolver of resolvers) {
|
|
2603
|
-
resolver.resolve(response);
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
_cancelPendingPromptRequests(sessionId) {
|
|
2607
|
-
const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
|
|
2608
|
-
if (!resolvers) {
|
|
2609
|
-
return false;
|
|
2610
|
-
}
|
|
2611
|
-
const response = {
|
|
2612
|
-
jsonrpc: "2.0",
|
|
2613
|
-
id: null,
|
|
2614
|
-
result: {
|
|
2615
|
-
stopReason: "cancelled",
|
|
2616
|
-
},
|
|
2617
|
-
};
|
|
2618
|
-
let cancelledPrompt = false;
|
|
2619
|
-
for (const resolver of [...resolvers]) {
|
|
2620
|
-
if (resolver.method !== "session/prompt") {
|
|
2621
|
-
continue;
|
|
2622
|
-
}
|
|
2623
|
-
resolvers.delete(resolver);
|
|
2624
|
-
resolver.resolve(response);
|
|
2625
|
-
cancelledPrompt = true;
|
|
2626
|
-
}
|
|
2627
|
-
if (resolvers.size === 0) {
|
|
2628
|
-
this._pendingSessionRequestResolvers.delete(sessionId);
|
|
2629
|
-
}
|
|
2630
|
-
return cancelledPrompt;
|
|
2631
|
-
}
|
|
2632
|
-
_rejectPendingPermissionReplies(sessionId) {
|
|
2633
|
-
const session = this._sessions.get(sessionId);
|
|
2634
|
-
if (!session) {
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
this._rejectPendingPermissionRepliesFromSession(session);
|
|
2638
|
-
}
|
|
2639
|
-
_rejectPendingPermissionRepliesFromSession(session) {
|
|
2640
|
-
for (const [permissionId, pendingReply,] of session.pendingPermissionReplies) {
|
|
2641
|
-
clearTimeout(pendingReply.timer);
|
|
2642
|
-
pendingReply.reject(new Error(`Session closed before permission reply: ${permissionId}`));
|
|
2643
|
-
}
|
|
2644
|
-
session.pendingPermissionReplies.clear();
|
|
2645
|
-
}
|
|
2646
|
-
async _closeSessionInternal(sessionId) {
|
|
2647
|
-
const closing = this._sessionClosePromises.get(sessionId);
|
|
2648
|
-
if (closing) {
|
|
2649
|
-
return closing;
|
|
2650
|
-
}
|
|
2651
|
-
if (this._closedSessionIds.has(sessionId)) {
|
|
2652
|
-
return;
|
|
2653
|
-
}
|
|
2654
|
-
this._abortPendingSessionRequests(sessionId);
|
|
2655
|
-
this._rejectPendingPermissionReplies(sessionId);
|
|
2656
|
-
this._requireSession(sessionId);
|
|
2657
|
-
this._removeSession(sessionId);
|
|
2658
|
-
this._closedSessionIds.add(sessionId);
|
|
2659
|
-
const closePromise = this._sendAcpRequest({
|
|
2660
|
-
tag: "AcpCloseSessionRequest",
|
|
2661
|
-
val: { sessionId },
|
|
2662
|
-
})
|
|
2663
|
-
.then((response) => {
|
|
2664
|
-
if (response.tag !== "AcpSessionClosedResponse") {
|
|
2665
|
-
throw new Error(`unexpected response to AcpCloseSessionRequest: ${response.tag}`);
|
|
2666
|
-
}
|
|
2667
|
-
})
|
|
2668
|
-
.finally(() => {
|
|
2669
|
-
this._sessionClosePromises.delete(sessionId);
|
|
2670
|
-
});
|
|
2671
|
-
this._sessionClosePromises.set(sessionId, closePromise);
|
|
2672
|
-
await closePromise;
|
|
2673
|
-
}
|
|
2674
|
-
async _hydrateSessionState(session) {
|
|
2675
|
-
const response = await this._sendAcpRequest({
|
|
2676
|
-
tag: "AcpGetSessionStateRequest",
|
|
2677
|
-
val: { sessionId: session.sessionId },
|
|
2678
|
-
});
|
|
2679
|
-
if (response.tag !== "AcpSessionStateResponse") {
|
|
2680
|
-
throw new Error(`unexpected response to AcpGetSessionStateRequest: ${response.tag}`);
|
|
2681
|
-
}
|
|
2682
|
-
const state = sidecarSessionStateFromAcp(response.val);
|
|
2683
|
-
this._syncSessionState(session, state);
|
|
2684
|
-
}
|
|
2685
|
-
async createSession(agentType, options) {
|
|
2686
|
-
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
2687
|
-
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
2688
|
-
// projected `/opt/agentos/<name>/current/agentos-package.json` and spawns
|
|
2689
|
-
// (including the agent's static launch args and manifest env defaults).
|
|
2690
|
-
// System-prompt assembly/injection (launch args / OPENCODE_CONTEXTPATHS) is
|
|
2691
|
-
// owned by the sidecar; the host only forwards additionalInstructions /
|
|
2692
|
-
// skipOsInstructions plus the caller's env.
|
|
2693
|
-
const launchEnv = { ...options?.env };
|
|
2694
|
-
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2695
|
-
const response = await this._sendAcpRequest({
|
|
2696
|
-
tag: "AcpCreateSessionRequest",
|
|
2697
|
-
val: {
|
|
2698
|
-
agentType: String(agentType),
|
|
2699
|
-
runtime: AcpRuntimeKind.JavaScript,
|
|
2700
|
-
args: [],
|
|
2701
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2702
|
-
cwd: sessionCwd,
|
|
2703
|
-
mcpServers: JSON.stringify(options?.mcpServers ?? []),
|
|
2704
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2705
|
-
clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
|
|
2706
|
-
additionalInstructions: combineInstructions([this._additionalInstructions, options?.additionalInstructions]
|
|
2707
|
-
.map((part) => part?.trim())
|
|
2708
|
-
.filter((part) => Boolean(part))
|
|
2709
|
-
.join("\n\n") || undefined, this._toolReference),
|
|
2710
|
-
skipOsInstructions: options?.skipOsInstructions ?? false,
|
|
2711
|
-
},
|
|
2712
|
-
});
|
|
2713
|
-
if (response.tag !== "AcpSessionCreatedResponse") {
|
|
2714
|
-
throw new Error(`unexpected create_session response: ${response.tag}`);
|
|
2715
|
-
}
|
|
2716
|
-
const created = sidecarSessionCreatedFromAcp(response.val);
|
|
2717
|
-
// The sessionId is chosen by the (untrusted/buggy) ACP adapter or sidecar. If it collides
|
|
2718
|
-
// with a live session already in `_sessions`, blindly overwriting the entry would orphan the
|
|
2719
|
-
// first session's event/permission handlers and pending permission replies. Fail closed:
|
|
2720
|
-
// reject the colliding create and leave the original session intact. (A previously-closed,
|
|
2721
|
-
// evicted id may still be re-used; only a live, non-closed entry is a collision.)
|
|
2722
|
-
const existing = this._sessions.get(created.sessionId);
|
|
2723
|
-
if (existing !== undefined && !existing.closed) {
|
|
2724
|
-
throw new Error(`session id collision: ${created.sessionId}`);
|
|
2725
|
-
}
|
|
2726
|
-
const initData = {
|
|
2727
|
-
modes: toSessionModes(created.modes) ?? undefined,
|
|
2728
|
-
configOptions: toSessionConfigOptions(created.configOptions),
|
|
2729
|
-
capabilities: toAgentCapabilities(created.agentCapabilities),
|
|
2730
|
-
agentInfo: toAgentInfo(created.agentInfo) ?? undefined,
|
|
2731
|
-
};
|
|
2732
|
-
const session = sessionEntryFromInit(created.sessionId, String(agentType), initData);
|
|
2733
|
-
this._closedSessionIds.delete(created.sessionId);
|
|
2734
|
-
this._sessions.set(created.sessionId, session);
|
|
2735
|
-
try {
|
|
2736
|
-
await this._hydrateSessionState(session);
|
|
2737
|
-
}
|
|
2738
|
-
catch (error) {
|
|
2739
|
-
this._removeSession(created.sessionId);
|
|
2740
|
-
throw error;
|
|
2741
|
-
}
|
|
2742
|
-
return { sessionId: created.sessionId };
|
|
2743
|
-
}
|
|
2744
|
-
/**
|
|
2745
|
-
* Resume a session that exists in durable storage but is not live in this VM
|
|
2746
|
-
* (e.g. after a Rivet actor slept and woke with a fresh VM). Thin forwarder:
|
|
2747
|
-
* resolves the agent config + adapter entrypoint exactly as {@link createSession}
|
|
2748
|
-
* does, then forwards a single `AcpResumeSessionRequest` to the sidecar, which
|
|
2749
|
-
* owns the resume state machine (native `session/load` when the agent supports
|
|
2750
|
-
* it, else `session/new` + a transcript-continuation preamble). The returned
|
|
2751
|
-
* `sessionId` is the live id in this VM (equal to the requested id for native
|
|
2752
|
-
* loads, freshly assigned for the fallback); the caller remaps `external -> live`.
|
|
2753
|
-
* The new live session is registered + hydrated locally so subsequent prompts
|
|
2754
|
-
* route to it.
|
|
2755
|
-
*
|
|
2756
|
-
* Resume depends on a durable root; on a non-durable (default in-memory) root
|
|
2757
|
-
* there is no surviving store and the fallback tier always runs.
|
|
2758
|
-
*/
|
|
2759
|
-
async resumeSession(sessionId, agentType, options) {
|
|
2760
|
-
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
2761
|
-
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
2762
|
-
// projected manifest, exactly as createSession does.
|
|
2763
|
-
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2764
|
-
const launchEnv = { ...options?.env };
|
|
2765
|
-
const response = await this._sendAcpRequest({
|
|
2766
|
-
tag: "AcpResumeSessionRequest",
|
|
2767
|
-
val: {
|
|
2768
|
-
sessionId,
|
|
2769
|
-
agentType: String(agentType),
|
|
2770
|
-
transcriptPath: options?.transcriptPath ?? null,
|
|
2771
|
-
cwd: sessionCwd,
|
|
2772
|
-
env: new Map(Object.entries(launchEnv)),
|
|
2773
|
-
},
|
|
2774
|
-
});
|
|
2775
|
-
if (response.tag !== "AcpSessionResumedResponse") {
|
|
2776
|
-
throw new Error(`unexpected resume_session response: ${response.tag}`);
|
|
2777
|
-
}
|
|
2778
|
-
const { sessionId: liveSessionId, mode } = response.val;
|
|
2779
|
-
// Register + hydrate the live session so subsequent prompts route to it.
|
|
2780
|
-
const existing = this._sessions.get(liveSessionId);
|
|
2781
|
-
if (existing !== undefined && !existing.closed) {
|
|
2782
|
-
throw new Error(`session id collision: ${liveSessionId}`);
|
|
2783
|
-
}
|
|
2784
|
-
const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
|
|
2785
|
-
this._closedSessionIds.delete(liveSessionId);
|
|
2786
|
-
this._sessions.set(liveSessionId, session);
|
|
2787
|
-
try {
|
|
2788
|
-
await this._hydrateSessionState(session);
|
|
2789
|
-
}
|
|
2790
|
-
catch (error) {
|
|
2791
|
-
this._removeSession(liveSessionId);
|
|
2792
|
-
throw error;
|
|
2793
|
-
}
|
|
2794
|
-
return { sessionId: liveSessionId, mode };
|
|
2795
|
-
}
|
|
2796
2671
|
_installSidecarRequestHandler() {
|
|
2797
2672
|
const context = {
|
|
2798
|
-
|
|
2799
|
-
|
|
2673
|
+
bindings: this._bindings,
|
|
2674
|
+
bindingMap: buildBindingMap(this._bindings),
|
|
2800
2675
|
permissions: this._permissions,
|
|
2801
2676
|
readFile: (path) => this.readFile(path),
|
|
2802
2677
|
};
|
|
@@ -2825,25 +2700,6 @@ export class AgentOs {
|
|
|
2825
2700
|
}
|
|
2826
2701
|
const callback = decodeAcpCallback(envelope.payload);
|
|
2827
2702
|
switch (callback.tag) {
|
|
2828
|
-
case "AcpPermissionCallback": {
|
|
2829
|
-
const reply = await this._handleAcpPermissionCallback(callback.val.sessionId, callback.val.permissionId, {
|
|
2830
|
-
...toRecord(JSON.parse(callback.val.params)),
|
|
2831
|
-
_acpMethod: ACP_PERMISSION_METHOD,
|
|
2832
|
-
});
|
|
2833
|
-
return {
|
|
2834
|
-
type: "ext_result",
|
|
2835
|
-
envelope: {
|
|
2836
|
-
namespace: ACP_EXTENSION_NAMESPACE,
|
|
2837
|
-
payload: encodeAcpCallbackResponse({
|
|
2838
|
-
tag: "AcpPermissionCallbackResponse",
|
|
2839
|
-
val: {
|
|
2840
|
-
permissionId: callback.val.permissionId,
|
|
2841
|
-
reply,
|
|
2842
|
-
},
|
|
2843
|
-
}),
|
|
2844
|
-
},
|
|
2845
|
-
};
|
|
2846
|
-
}
|
|
2847
2703
|
case "AcpHostRequestCallback": {
|
|
2848
2704
|
const response = await this._dispatchAcpSidecarRequest(toJsonRpcRequest(JSON.parse(callback.val.request)));
|
|
2849
2705
|
return {
|
|
@@ -2895,8 +2751,6 @@ export class AgentOs {
|
|
|
2895
2751
|
async _handleSupportedAcpSidecarRequest(request) {
|
|
2896
2752
|
const params = this._acpParams(request);
|
|
2897
2753
|
switch (request.method) {
|
|
2898
|
-
case ACP_PERMISSION_METHOD:
|
|
2899
|
-
return this._handleAcpPermissionRequest(request, params);
|
|
2900
2754
|
case "fs/read":
|
|
2901
2755
|
case "fs/read_text_file":
|
|
2902
2756
|
return this._handleAcpReadFile(params);
|
|
@@ -2929,99 +2783,6 @@ export class AgentOs {
|
|
|
2929
2783
|
});
|
|
2930
2784
|
}
|
|
2931
2785
|
}
|
|
2932
|
-
_normalizeAcpPermissionOptionId(options, reply) {
|
|
2933
|
-
const optionTargets = reply === "always"
|
|
2934
|
-
? {
|
|
2935
|
-
optionIds: new Set(["always", "allow_always"]),
|
|
2936
|
-
kinds: new Set(["allow_always"]),
|
|
2937
|
-
}
|
|
2938
|
-
: reply === "once"
|
|
2939
|
-
? {
|
|
2940
|
-
optionIds: new Set(["once", "allow_once"]),
|
|
2941
|
-
kinds: new Set(["allow_once"]),
|
|
2942
|
-
}
|
|
2943
|
-
: {
|
|
2944
|
-
optionIds: new Set(["reject", "reject_once"]),
|
|
2945
|
-
kinds: new Set(["reject_once"]),
|
|
2946
|
-
};
|
|
2947
|
-
const matched = options?.find((option) => {
|
|
2948
|
-
const optionId = typeof option.optionId === "string" ? option.optionId : undefined;
|
|
2949
|
-
const kind = typeof option.kind === "string" ? option.kind : undefined;
|
|
2950
|
-
return ((optionId !== undefined && optionTargets.optionIds.has(optionId)) ||
|
|
2951
|
-
(kind !== undefined && optionTargets.kinds.has(kind)));
|
|
2952
|
-
});
|
|
2953
|
-
if (matched && typeof matched.optionId === "string") {
|
|
2954
|
-
return matched.optionId;
|
|
2955
|
-
}
|
|
2956
|
-
if (reply === "always") {
|
|
2957
|
-
return "allow_always";
|
|
2958
|
-
}
|
|
2959
|
-
if (reply === "once") {
|
|
2960
|
-
return "allow_once";
|
|
2961
|
-
}
|
|
2962
|
-
return "reject_once";
|
|
2963
|
-
}
|
|
2964
|
-
_buildAcpPermissionResult(reply, params) {
|
|
2965
|
-
const options = Array.isArray(params.options)
|
|
2966
|
-
? params.options.filter((option) => typeof option === "object" && option !== null)
|
|
2967
|
-
: undefined;
|
|
2968
|
-
const optionId = this._normalizeAcpPermissionOptionId(options, reply);
|
|
2969
|
-
return {
|
|
2970
|
-
outcome: optionId
|
|
2971
|
-
? {
|
|
2972
|
-
outcome: "selected",
|
|
2973
|
-
optionId,
|
|
2974
|
-
}
|
|
2975
|
-
: {
|
|
2976
|
-
outcome: "cancelled",
|
|
2977
|
-
},
|
|
2978
|
-
};
|
|
2979
|
-
}
|
|
2980
|
-
async _handleAcpPermissionRequest(request, params) {
|
|
2981
|
-
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
2982
|
-
if (!sessionId) {
|
|
2983
|
-
throw new AcpDispatchError(-32602, `${ACP_PERMISSION_METHOD} requires a sessionId`);
|
|
2984
|
-
}
|
|
2985
|
-
const session = this._sessions.get(sessionId);
|
|
2986
|
-
if (!session) {
|
|
2987
|
-
throw new AcpDispatchError(-32602, `Session not found: ${sessionId}`);
|
|
2988
|
-
}
|
|
2989
|
-
const permissionId = String(request.id);
|
|
2990
|
-
const permissionParams = {
|
|
2991
|
-
...params,
|
|
2992
|
-
permissionId,
|
|
2993
|
-
_acpMethod: request.method,
|
|
2994
|
-
};
|
|
2995
|
-
if (session.permissionHandlers.size === 0) {
|
|
2996
|
-
// Default-closed deny; warn once (host-visible) so a forgotten
|
|
2997
|
-
// onPermissionRequest handler is an observable cause rather than a
|
|
2998
|
-
// silent denial. See _warnNoPermissionHandlerOnce.
|
|
2999
|
-
this._warnNoPermissionHandlerOnce(session, permissionParams);
|
|
3000
|
-
return this._buildAcpPermissionResult("reject", permissionParams);
|
|
3001
|
-
}
|
|
3002
|
-
const reply = await new Promise((resolve, reject) => {
|
|
3003
|
-
const timer = setTimeout(() => {
|
|
3004
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
3005
|
-
reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
|
|
3006
|
-
}, 120_000);
|
|
3007
|
-
session.pendingPermissionReplies.set(permissionId, {
|
|
3008
|
-
resolve,
|
|
3009
|
-
reject,
|
|
3010
|
-
timer,
|
|
3011
|
-
});
|
|
3012
|
-
const permissionRequest = {
|
|
3013
|
-
permissionId,
|
|
3014
|
-
description: typeof permissionParams["description"] === "string"
|
|
3015
|
-
? permissionParams["description"]
|
|
3016
|
-
: undefined,
|
|
3017
|
-
params: permissionParams,
|
|
3018
|
-
};
|
|
3019
|
-
for (const handler of session.permissionHandlers) {
|
|
3020
|
-
handler(permissionRequest);
|
|
3021
|
-
}
|
|
3022
|
-
});
|
|
3023
|
-
return this._buildAcpPermissionResult(reply, permissionParams);
|
|
3024
|
-
}
|
|
3025
2786
|
_acpParams(request) {
|
|
3026
2787
|
if (!request.params) {
|
|
3027
2788
|
return {};
|
|
@@ -3194,9 +2955,6 @@ export class AgentOs {
|
|
|
3194
2955
|
...(cwd ? { cwd } : {}),
|
|
3195
2956
|
...(cols !== undefined ? { cols: Math.trunc(cols) } : {}),
|
|
3196
2957
|
...(rows !== undefined ? { rows: Math.trunc(rows) } : {}),
|
|
3197
|
-
onStderr: (data) => {
|
|
3198
|
-
this._appendAcpTerminalOutput(terminal, data);
|
|
3199
|
-
},
|
|
3200
2958
|
}),
|
|
3201
2959
|
output: "",
|
|
3202
2960
|
truncated: false,
|
|
@@ -3273,193 +3031,38 @@ export class AgentOs {
|
|
|
3273
3031
|
terminal.handle.resize(Math.trunc(cols), Math.trunc(rows));
|
|
3274
3032
|
return null;
|
|
3275
3033
|
}
|
|
3276
|
-
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
if (
|
|
3282
|
-
|
|
3283
|
-
// (host-visible) so a forgotten onPermissionRequest handler is not an
|
|
3284
|
-
// invisible cause of an agent that cannot use any tool.
|
|
3285
|
-
this._warnNoPermissionHandlerOnce(session, params);
|
|
3286
|
-
return "reject";
|
|
3287
|
-
}
|
|
3288
|
-
try {
|
|
3289
|
-
return await new Promise((resolve, reject) => {
|
|
3290
|
-
const timer = setTimeout(() => {
|
|
3291
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
3292
|
-
reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
|
|
3293
|
-
}, 120_000);
|
|
3294
|
-
session.pendingPermissionReplies.set(permissionId, {
|
|
3295
|
-
resolve,
|
|
3296
|
-
reject,
|
|
3297
|
-
timer,
|
|
3298
|
-
});
|
|
3299
|
-
const permissionRequest = {
|
|
3300
|
-
permissionId,
|
|
3301
|
-
description: typeof params.description === "string"
|
|
3302
|
-
? params.description
|
|
3303
|
-
: undefined,
|
|
3304
|
-
params,
|
|
3305
|
-
};
|
|
3306
|
-
for (const handler of session.permissionHandlers) {
|
|
3307
|
-
handler(permissionRequest);
|
|
3308
|
-
}
|
|
3309
|
-
});
|
|
3310
|
-
}
|
|
3311
|
-
catch {
|
|
3312
|
-
return "reject";
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
/**
|
|
3316
|
-
* Gracefully destroy a session: cancel any pending work, close the client,
|
|
3317
|
-
* and remove from tracking. Unlike close() which is abrupt, this attempts
|
|
3318
|
-
* a graceful shutdown sequence.
|
|
3319
|
-
*/
|
|
3320
|
-
async destroySession(sessionId) {
|
|
3321
|
-
this._requireSession(sessionId);
|
|
3322
|
-
try {
|
|
3323
|
-
await this.cancelSession(sessionId);
|
|
3034
|
+
onSessionEvent(sessionIdOrHandler, maybeHandler) {
|
|
3035
|
+
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "main";
|
|
3036
|
+
const handler = typeof sessionIdOrHandler === "function"
|
|
3037
|
+
? sessionIdOrHandler
|
|
3038
|
+
: maybeHandler;
|
|
3039
|
+
if (!handler) {
|
|
3040
|
+
throw new TypeError("onSessionEvent requires a handler");
|
|
3324
3041
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
const session = this._requireSession(sessionId);
|
|
3333
|
-
let agentText = "";
|
|
3334
|
-
const handler = (event) => {
|
|
3335
|
-
const params = toRecord(event.params);
|
|
3336
|
-
const update = toRecord(params.update);
|
|
3337
|
-
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
3338
|
-
const content = toRecord(update.content);
|
|
3339
|
-
if (typeof content.text === "string") {
|
|
3340
|
-
agentText += content.text;
|
|
3341
|
-
}
|
|
3042
|
+
const handlers = this._durableSessionEventHandlers.get(sessionId) ?? new Set();
|
|
3043
|
+
handlers.add(handler);
|
|
3044
|
+
this._durableSessionEventHandlers.set(sessionId, handlers);
|
|
3045
|
+
return () => {
|
|
3046
|
+
handlers.delete(handler);
|
|
3047
|
+
if (handlers.size === 0) {
|
|
3048
|
+
this._durableSessionEventHandlers.delete(sessionId);
|
|
3342
3049
|
}
|
|
3343
3050
|
};
|
|
3344
|
-
const unsubscribe = this._subscribeSessionEvents(session, handler);
|
|
3345
|
-
try {
|
|
3346
|
-
const response = await this._sendSessionRequest(sessionId, "session/prompt", {
|
|
3347
|
-
prompt: [{ type: "text", text }],
|
|
3348
|
-
});
|
|
3349
|
-
return { response, text: agentText };
|
|
3350
|
-
}
|
|
3351
|
-
finally {
|
|
3352
|
-
unsubscribe();
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
/** Cancel ongoing agent work for a session. */
|
|
3356
|
-
async cancelSession(sessionId) {
|
|
3357
|
-
this._requireSession(sessionId);
|
|
3358
|
-
const cancelledPendingPrompt = this._cancelPendingPromptRequests(sessionId);
|
|
3359
|
-
if (cancelledPendingPrompt) {
|
|
3360
|
-
// Session control requests share the same framed sidecar transport as an
|
|
3361
|
-
// in-flight prompt request. If the adapter is blocked in prompt I/O, a
|
|
3362
|
-
// synchronous cancel RPC can wedge behind that prompt until the transport
|
|
3363
|
-
// timeout fires. Resolve the local prompt immediately, then let the sidecar
|
|
3364
|
-
// cancellation continue in the background as best effort.
|
|
3365
|
-
void this._sendSessionRequest(sessionId, "session/cancel").catch(() => { });
|
|
3366
|
-
return {
|
|
3367
|
-
jsonrpc: "2.0",
|
|
3368
|
-
id: null,
|
|
3369
|
-
result: {
|
|
3370
|
-
cancelled: true,
|
|
3371
|
-
requested: true,
|
|
3372
|
-
via: "prompt-fallback",
|
|
3373
|
-
},
|
|
3374
|
-
};
|
|
3375
|
-
}
|
|
3376
|
-
const response = await this._sendSessionRequest(sessionId, "session/cancel");
|
|
3377
|
-
if (response.error?.code === -32601) {
|
|
3378
|
-
return {
|
|
3379
|
-
jsonrpc: "2.0",
|
|
3380
|
-
id: null,
|
|
3381
|
-
result: {
|
|
3382
|
-
cancelled: false,
|
|
3383
|
-
requested: true,
|
|
3384
|
-
via: "unsupported",
|
|
3385
|
-
},
|
|
3386
|
-
};
|
|
3387
|
-
}
|
|
3388
|
-
return response;
|
|
3389
3051
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
void closePromise.catch(() => { });
|
|
3401
|
-
}
|
|
3402
|
-
async respondPermission(sessionId, permissionId, reply) {
|
|
3403
|
-
const session = this._requireSession(sessionId);
|
|
3404
|
-
const pendingReply = session.pendingPermissionReplies.get(permissionId);
|
|
3405
|
-
if (pendingReply) {
|
|
3406
|
-
session.pendingPermissionReplies.delete(permissionId);
|
|
3407
|
-
clearTimeout(pendingReply.timer);
|
|
3408
|
-
pendingReply.resolve(reply);
|
|
3409
|
-
return {
|
|
3410
|
-
jsonrpc: "2.0",
|
|
3411
|
-
id: null,
|
|
3412
|
-
result: {
|
|
3413
|
-
permissionId,
|
|
3414
|
-
reply,
|
|
3415
|
-
via: "sidecar-request",
|
|
3416
|
-
},
|
|
3417
|
-
};
|
|
3418
|
-
}
|
|
3419
|
-
return this._sendSessionRequest(sessionId, LEGACY_PERMISSION_METHOD, {
|
|
3420
|
-
permissionId,
|
|
3421
|
-
reply,
|
|
3422
|
-
});
|
|
3423
|
-
}
|
|
3424
|
-
async setSessionMode(sessionId, modeId) {
|
|
3425
|
-
return this._sendSessionRequest(sessionId, "session/set_mode", {
|
|
3426
|
-
modeId,
|
|
3427
|
-
});
|
|
3428
|
-
}
|
|
3429
|
-
getSessionModes(sessionId) {
|
|
3430
|
-
return this._requireSession(sessionId).modes;
|
|
3431
|
-
}
|
|
3432
|
-
async setSessionModel(sessionId, model) {
|
|
3433
|
-
return this._setSessionConfigByCategory(sessionId, "model", model);
|
|
3434
|
-
}
|
|
3435
|
-
async setSessionThoughtLevel(sessionId, level) {
|
|
3436
|
-
return this._setSessionConfigByCategory(sessionId, "thought_level", level);
|
|
3437
|
-
}
|
|
3438
|
-
getSessionConfigOptions(sessionId) {
|
|
3439
|
-
return [...this._requireSession(sessionId).configOptions];
|
|
3440
|
-
}
|
|
3441
|
-
getSessionCapabilities(sessionId) {
|
|
3442
|
-
const caps = this._requireSession(sessionId).capabilities;
|
|
3443
|
-
return Object.keys(caps).length > 0 ? caps : null;
|
|
3444
|
-
}
|
|
3445
|
-
getSessionAgentInfo(sessionId) {
|
|
3446
|
-
return this._requireSession(sessionId).agentInfo;
|
|
3447
|
-
}
|
|
3448
|
-
async rawSessionSend(sessionId, method, params) {
|
|
3449
|
-
return this._sendSessionRequest(sessionId, method, params);
|
|
3450
|
-
}
|
|
3451
|
-
async rawSend(sessionId, method, params) {
|
|
3452
|
-
return this.rawSessionSend(sessionId, method, params);
|
|
3453
|
-
}
|
|
3454
|
-
onSessionEvent(sessionId, handler) {
|
|
3455
|
-
const session = this._requireSession(sessionId);
|
|
3456
|
-
return this._subscribeSessionEvents(session, handler);
|
|
3457
|
-
}
|
|
3458
|
-
onPermissionRequest(sessionId, handler) {
|
|
3459
|
-
const session = this._requireSession(sessionId);
|
|
3460
|
-
session.permissionHandlers.add(handler);
|
|
3052
|
+
onAgentExit(sessionIdOrHandler, maybeHandler) {
|
|
3053
|
+
const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "*";
|
|
3054
|
+
const handler = typeof sessionIdOrHandler === "function"
|
|
3055
|
+
? sessionIdOrHandler
|
|
3056
|
+
: maybeHandler;
|
|
3057
|
+
if (!handler)
|
|
3058
|
+
throw new TypeError("onAgentExit requires a handler");
|
|
3059
|
+
const handlers = this._agentExitHandlers.get(sessionId) ?? new Set();
|
|
3060
|
+
handlers.add(handler);
|
|
3061
|
+
this._agentExitHandlers.set(sessionId, handlers);
|
|
3461
3062
|
return () => {
|
|
3462
|
-
|
|
3063
|
+
handlers.delete(handler);
|
|
3064
|
+
if (handlers.size === 0)
|
|
3065
|
+
this._agentExitHandlers.delete(sessionId);
|
|
3463
3066
|
};
|
|
3464
3067
|
}
|
|
3465
3068
|
// ── Cron ────────────────────────────────────────────────────
|
|
@@ -3481,9 +3084,6 @@ export class AgentOs {
|
|
|
3481
3084
|
}
|
|
3482
3085
|
async dispose() {
|
|
3483
3086
|
this._cronManager.dispose();
|
|
3484
|
-
for (const sessionId of [...this._sessions.keys()]) {
|
|
3485
|
-
await this._closeSessionInternal(sessionId).catch(() => { });
|
|
3486
|
-
}
|
|
3487
3087
|
for (const [id, entry] of this._shells) {
|
|
3488
3088
|
entry.handle.kill();
|
|
3489
3089
|
}
|
|
@@ -3567,7 +3167,17 @@ function applySharedSidecarHold(state) {
|
|
|
3567
3167
|
if (!child)
|
|
3568
3168
|
return;
|
|
3569
3169
|
const hold = (state.eventLoopHolds ?? 0) > 0;
|
|
3570
|
-
|
|
3170
|
+
// Include the complete stdio array, not just the named fd0-fd2 aliases.
|
|
3171
|
+
// Protocol v8 carries responses over fd3; leaving that control socket
|
|
3172
|
+
// referenced pins Node's event loop after the final VM lease is disposed.
|
|
3173
|
+
const handles = new Set([
|
|
3174
|
+
child,
|
|
3175
|
+
child.stdin,
|
|
3176
|
+
child.stdout,
|
|
3177
|
+
child.stderr,
|
|
3178
|
+
...(child.stdio ?? []),
|
|
3179
|
+
]);
|
|
3180
|
+
for (const handle of handles) {
|
|
3571
3181
|
if (!handle)
|
|
3572
3182
|
continue;
|
|
3573
3183
|
try {
|
|
@@ -3621,7 +3231,6 @@ function ensureSharedSidecarNativeProcess(sidecar) {
|
|
|
3621
3231
|
cwd: REPO_ROOT,
|
|
3622
3232
|
command: ensureNativeSidecarBinary(),
|
|
3623
3233
|
args: [],
|
|
3624
|
-
frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
|
|
3625
3234
|
});
|
|
3626
3235
|
// Track the child immediately — BEFORE the handshake await — so a
|
|
3627
3236
|
// failed `authenticateAndOpenSession()` can still reap it (otherwise
|
|
@@ -3775,7 +3384,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3775
3384
|
}
|
|
3776
3385
|
let transport;
|
|
3777
3386
|
const client = createAgentOsSidecarClient({
|
|
3778
|
-
async
|
|
3387
|
+
async createOwnershipTransport(sessionBootstrap) {
|
|
3779
3388
|
transport = await createInProcessSidecarTransport(sessionBootstrap, options);
|
|
3780
3389
|
return transport;
|
|
3781
3390
|
},
|
|
@@ -3795,7 +3404,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3795
3404
|
let disposed = false;
|
|
3796
3405
|
let leaseRecord;
|
|
3797
3406
|
try {
|
|
3798
|
-
const session = await client.
|
|
3407
|
+
const session = await client.createOwnershipSession({
|
|
3799
3408
|
placement: cloneSidecarPlacement(state.description.placement),
|
|
3800
3409
|
});
|
|
3801
3410
|
const vm = await session.createVm();
|