@rivet-dev/agentos-core 0.0.0-integrate-dylib-into-main.815fcda → 0.0.0-main.2f99c77
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 +5 -8
- package/dist/agent-os.d.ts +176 -48
- package/dist/agent-os.js +829 -548
- package/dist/agentos-package.d.ts +29 -0
- package/dist/agentos-package.js +88 -0
- package/dist/base-filesystem.d.ts +2 -15
- package/dist/base-filesystem.js +23 -171
- package/dist/cron/types.d.ts +1 -1
- package/dist/default-software.d.ts +8 -0
- package/dist/default-software.js +10 -0
- package/dist/generated/AcpLimitsConfig.d.ts +4 -0
- package/dist/generated/AcpLimitsConfig.js +2 -0
- package/dist/generated/CreateVmConfig.d.ts +26 -0
- package/dist/generated/CreateVmConfig.js +1 -0
- package/dist/generated/FsPermissionRule.d.ts +6 -0
- package/dist/generated/FsPermissionRule.js +1 -0
- package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
- package/dist/generated/FsPermissionRuleSet.js +1 -0
- package/dist/generated/FsPermissionScope.d.ts +3 -0
- package/dist/generated/FsPermissionScope.js +1 -0
- package/dist/generated/HttpLimitsConfig.d.ts +3 -0
- package/dist/generated/HttpLimitsConfig.js +2 -0
- package/dist/generated/JsModuleResolution.d.ts +1 -0
- package/dist/generated/JsModuleResolution.js +2 -0
- package/dist/generated/JsRuntimeConfig.d.ts +31 -0
- package/dist/generated/JsRuntimeConfig.js +1 -0
- package/dist/generated/JsRuntimeLimitsConfig.d.ts +11 -0
- package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
- package/dist/generated/JsRuntimePlatform.d.ts +1 -0
- package/dist/generated/JsRuntimePlatform.js +2 -0
- package/dist/generated/MountPluginDescriptor.d.ts +4 -0
- package/dist/generated/MountPluginDescriptor.js +2 -0
- package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
- package/dist/generated/NativeRootFilesystemConfig.js +1 -0
- package/dist/generated/PatternPermissionRule.d.ts +6 -0
- package/dist/generated/PatternPermissionRule.js +1 -0
- package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
- package/dist/generated/PatternPermissionRuleSet.js +1 -0
- package/dist/generated/PatternPermissionScope.d.ts +3 -0
- package/dist/generated/PatternPermissionScope.js +1 -0
- package/dist/generated/PermissionMode.d.ts +1 -0
- package/dist/generated/PermissionMode.js +2 -0
- package/dist/generated/PermissionsPolicy.d.ts +10 -0
- package/dist/generated/PermissionsPolicy.js +1 -0
- package/dist/generated/PluginLimitsConfig.d.ts +4 -0
- package/dist/generated/PluginLimitsConfig.js +2 -0
- package/dist/generated/PythonLimitsConfig.d.ts +6 -0
- package/dist/generated/PythonLimitsConfig.js +2 -0
- package/dist/generated/ResourceLimitsConfig.d.ts +24 -0
- package/dist/generated/ResourceLimitsConfig.js +2 -0
- package/dist/generated/RootFilesystemConfig.d.ts +9 -0
- package/dist/generated/RootFilesystemConfig.js +1 -0
- package/dist/generated/RootFilesystemEntry.d.ts +13 -0
- package/dist/generated/RootFilesystemEntry.js +1 -0
- package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
- package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
- package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
- package/dist/generated/RootFilesystemEntryKind.js +2 -0
- package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
- package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
- package/dist/generated/RootFilesystemMode.d.ts +1 -0
- package/dist/generated/RootFilesystemMode.js +2 -0
- package/dist/generated/ToolLimitsConfig.d.ts +10 -0
- package/dist/generated/ToolLimitsConfig.js +2 -0
- package/dist/generated/VmDnsConfig.d.ts +6 -0
- package/dist/generated/VmDnsConfig.js +2 -0
- package/dist/generated/VmLimitsConfig.d.ts +18 -0
- package/dist/generated/VmLimitsConfig.js +1 -0
- package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
- package/dist/generated/VmListenPolicyConfig.js +2 -0
- package/dist/generated/WasmLimitsConfig.d.ts +7 -0
- package/dist/generated/WasmLimitsConfig.js +2 -0
- package/dist/host-dir-mount.d.ts +1 -1
- package/dist/host-tools-zod.js +27 -7
- package/dist/host-tools.js +9 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/layers.d.ts +2 -2
- package/dist/layers.js +19 -10
- package/dist/options-schema.d.ts +453 -0
- package/dist/options-schema.js +268 -0
- package/dist/packages.d.ts +17 -116
- package/dist/packages.js +35 -322
- package/dist/runtime-compat.d.ts +27 -11
- package/dist/runtime-compat.js +133 -147
- package/dist/runtime.d.ts +37 -7
- package/dist/sandbox.d.ts +112 -0
- package/dist/sandbox.js +264 -0
- package/dist/sidecar/agentos-protocol.d.ts +99 -1
- package/dist/sidecar/agentos-protocol.js +155 -18
- package/dist/sidecar/native-process-client.d.ts +6 -2
- package/dist/sidecar/native-process-client.js +8 -1
- package/dist/sidecar/permissions.d.ts +1 -1
- package/dist/sidecar/permissions.js +4 -4
- package/dist/sidecar/rpc-client.d.ts +86 -6
- package/dist/sidecar/rpc-client.js +388 -93
- package/dist/types.d.ts +10 -3
- package/package.json +40 -33
- package/dist/agents.d.ts +0 -63
- package/dist/agents.js +0 -42
- package/fixtures/AGENTOS_SYSTEM_PROMPT.md +0 -21
package/dist/agent-os.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync,
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
|
|
3
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { validateToolkits } from "./host-tools.js";
|
|
8
7
|
import { zodToJsonSchema } from "./host-tools-zod.js";
|
|
8
|
+
import { parseAgentOsOptions } from "./options-schema.js";
|
|
9
9
|
import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
|
|
10
10
|
import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
|
|
11
11
|
export { isAcpTimeoutErrorData } from "./json-rpc.js";
|
|
12
12
|
const ACP_PROTOCOL_VERSION = 1;
|
|
13
13
|
const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
|
|
14
14
|
const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
|
|
15
|
-
/**
|
|
16
|
-
* Reserved `env` key on `AcpResumeSessionRequest` carrying the resolved adapter
|
|
17
|
-
* bin entrypoint. The resume wire request omits a dedicated `adapterEntrypoint`
|
|
18
|
-
* field; the sidecar reads the entrypoint from this key and strips it before
|
|
19
|
-
* launching the adapter. Must stay in sync with the sidecar constant of the same
|
|
20
|
-
* name in `crates/agentos-sidecar/src/acp_extension.rs`.
|
|
21
|
-
*/
|
|
22
|
-
const RESUME_ADAPTER_ENTRYPOINT_ENV = "AGENT_OS_RESUME_ADAPTER_ENTRYPOINT";
|
|
23
15
|
function defaultAcpClientCapabilities() {
|
|
24
16
|
return {
|
|
25
17
|
fs: {
|
|
@@ -40,19 +32,24 @@ async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
|
40
32
|
}),
|
|
41
33
|
]);
|
|
42
34
|
}
|
|
43
|
-
import {
|
|
44
|
-
import { getBaseEnvironment, getBaseFilesystemEntries, } from "./base-filesystem.js";
|
|
35
|
+
import { getBaseEnvironment } from "./base-filesystem.js";
|
|
45
36
|
import { CronManager } from "./cron/cron-manager.js";
|
|
46
37
|
import { TimerScheduleDriver } from "./cron/timer-driver.js";
|
|
47
38
|
import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
|
|
48
|
-
import { createHostDirBackend } from "./host-dir-mount.js";
|
|
49
39
|
import { serializeMountConfigForSidecar, } from "./js-bridge.js";
|
|
50
40
|
import { createSnapshotExport, } from "./layers.js";
|
|
51
|
-
import {
|
|
41
|
+
import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
|
|
42
|
+
import { resolveDefaultSoftware } from "./default-software.js";
|
|
52
43
|
import { allowAll } from "./runtime-compat.js";
|
|
53
44
|
import { AcpRuntimeKind, decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
|
|
54
45
|
import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
|
|
55
|
-
import { createAgentOsSidecarClient,
|
|
46
|
+
import { createAgentOsSidecarClient, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
|
|
47
|
+
function defaultAgentStderrHandler(event) {
|
|
48
|
+
process.stderr.write(event.chunk);
|
|
49
|
+
}
|
|
50
|
+
function defaultAgentExitHandler(event) {
|
|
51
|
+
process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"} restart=${event.restart} (${event.restartCount}/${event.maxRestarts})\n`);
|
|
52
|
+
}
|
|
56
53
|
const LEGACY_PERMISSION_METHOD = "request/permission";
|
|
57
54
|
const ACP_PERMISSION_METHOD = "session/request_permission";
|
|
58
55
|
class AcpDispatchError extends Error {
|
|
@@ -105,6 +102,28 @@ function toRecord(value) {
|
|
|
105
102
|
? value
|
|
106
103
|
: {};
|
|
107
104
|
}
|
|
105
|
+
function normalizePackageRef(value) {
|
|
106
|
+
// The single package reference is `packagePath`: the packed `.aospkg` file
|
|
107
|
+
// (registry-built packages export `{ packagePath }`), or a package dir for
|
|
108
|
+
// local transition fixtures. A raw string is shorthand for the same path.
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
return { path: value };
|
|
111
|
+
}
|
|
112
|
+
const record = toRecord(value);
|
|
113
|
+
if (typeof record.packagePath === "string") {
|
|
114
|
+
return { path: record.packagePath };
|
|
115
|
+
}
|
|
116
|
+
// Recognizably-legacy shapes fail loudly: silently dropping a software
|
|
117
|
+
// entry boots a VM with missing packages and no diagnostic.
|
|
118
|
+
for (const legacy of ["packageTar", "packageDir", "dir"]) {
|
|
119
|
+
if (typeof record[legacy] === "string") {
|
|
120
|
+
throw new Error(`agentOS package ref uses removed field "${legacy}" (value: ${JSON.stringify(record[legacy])}); ` +
|
|
121
|
+
"packages are referenced by a single `packagePath` — update the package " +
|
|
122
|
+
"(rebuild @agentos-software/* dependencies) or pass { packagePath }");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
108
127
|
function parseAcpJson(value, context) {
|
|
109
128
|
if (value === null) {
|
|
110
129
|
return undefined;
|
|
@@ -160,11 +179,11 @@ class BoundedSet {
|
|
|
160
179
|
}
|
|
161
180
|
this.limit = limit;
|
|
162
181
|
}
|
|
163
|
-
add(value) {
|
|
182
|
+
add(value, associated) {
|
|
164
183
|
if (this.#entries.has(value)) {
|
|
165
184
|
this.#entries.delete(value);
|
|
166
185
|
}
|
|
167
|
-
this.#entries.set(value,
|
|
186
|
+
this.#entries.set(value, associated);
|
|
168
187
|
if (this.#entries.size <= this.limit) {
|
|
169
188
|
return;
|
|
170
189
|
}
|
|
@@ -176,6 +195,9 @@ class BoundedSet {
|
|
|
176
195
|
has(value) {
|
|
177
196
|
return this.#entries.has(value);
|
|
178
197
|
}
|
|
198
|
+
get(value) {
|
|
199
|
+
return this.#entries.get(value);
|
|
200
|
+
}
|
|
179
201
|
delete(value) {
|
|
180
202
|
return this.#entries.delete(value);
|
|
181
203
|
}
|
|
@@ -223,6 +245,7 @@ function sessionEntryFromInit(sessionId, agentType, initData) {
|
|
|
223
245
|
agentInfo: initData.agentInfo ?? null,
|
|
224
246
|
eventHandlers: new Set(),
|
|
225
247
|
permissionHandlers: new Set(),
|
|
248
|
+
warnedNoPermissionHandler: false,
|
|
226
249
|
configOverrides: new Map(),
|
|
227
250
|
pendingPermissionReplies: new Map(),
|
|
228
251
|
};
|
|
@@ -366,7 +389,8 @@ const KERNEL_POSIX_BOOTSTRAP_DIRS = [
|
|
|
366
389
|
"/mnt",
|
|
367
390
|
"/media",
|
|
368
391
|
"/home",
|
|
369
|
-
"/home/
|
|
392
|
+
"/home/agentos",
|
|
393
|
+
"/workspace",
|
|
370
394
|
"/usr",
|
|
371
395
|
"/usr/bin",
|
|
372
396
|
"/usr/games",
|
|
@@ -390,8 +414,31 @@ const KERNEL_POSIX_BOOTSTRAP_DIRS = [
|
|
|
390
414
|
"/var/tmp",
|
|
391
415
|
"/etc/agentos",
|
|
392
416
|
];
|
|
393
|
-
|
|
394
|
-
|
|
417
|
+
// Standard POSIX metadata for the bootstrap dirs whose mode/owner is NOT the
|
|
418
|
+
// default (`755`, root:root). Replicated as a constant so building the no-base
|
|
419
|
+
// bootstrap layer needs no `base-filesystem.json` read — when a base IS present
|
|
420
|
+
// the sidecar's embedded base layer is authoritative and these are never emitted.
|
|
421
|
+
const KERNEL_POSIX_BOOTSTRAP_DIR_METADATA = {
|
|
422
|
+
"/tmp": { mode: "1777", uid: 0, gid: 0 },
|
|
423
|
+
"/root": { mode: "700", uid: 0, gid: 0 },
|
|
424
|
+
"/sys": { mode: "555", uid: 0, gid: 0 },
|
|
425
|
+
"/home/agentos": { mode: "2755", uid: 1000, gid: 1000 },
|
|
426
|
+
"/workspace": { mode: "755", uid: 1000, gid: 1000 },
|
|
427
|
+
"/var/empty": { mode: "555", uid: 0, gid: 0 },
|
|
428
|
+
"/var/lock": { mode: "777", uid: 0, gid: 0 },
|
|
429
|
+
"/var/run": { mode: "777", uid: 0, gid: 0 },
|
|
430
|
+
"/var/tmp": { mode: "1777", uid: 0, gid: 0 },
|
|
431
|
+
};
|
|
432
|
+
// Runtime commands that get a `/bin/<cmd>` stub at bootstrap so the guest shell
|
|
433
|
+
// resolves them on PATH (e.g. `sh -c "python ..."`, pipelines). The sidecar
|
|
434
|
+
// intercepts these by name and routes them to the embedded V8 / Pyodide runtime.
|
|
435
|
+
const RUNTIME_BOOTSTRAP_COMMANDS = [
|
|
436
|
+
"node",
|
|
437
|
+
"npm",
|
|
438
|
+
"npx",
|
|
439
|
+
"python",
|
|
440
|
+
"python3",
|
|
441
|
+
];
|
|
395
442
|
const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
|
|
396
443
|
const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-sidecar");
|
|
397
444
|
const SIDECAR_BUILD_INPUTS = [
|
|
@@ -403,114 +450,6 @@ const SIDECAR_BUILD_INPUTS = [
|
|
|
403
450
|
join(REPO_ROOT, "crates/sidecar"),
|
|
404
451
|
];
|
|
405
452
|
let ensuredSidecarBinary = null;
|
|
406
|
-
function isWasmBinaryFile(path) {
|
|
407
|
-
try {
|
|
408
|
-
const header = readFileSync(path);
|
|
409
|
-
return (header.length >= 4 &&
|
|
410
|
-
header[0] === 0x00 &&
|
|
411
|
-
header[1] === 0x61 &&
|
|
412
|
-
header[2] === 0x73 &&
|
|
413
|
-
header[3] === 0x6d);
|
|
414
|
-
}
|
|
415
|
-
catch {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
function collectBootstrapWasmCommands(commandDirs) {
|
|
420
|
-
const commands = [];
|
|
421
|
-
const seen = new Set();
|
|
422
|
-
for (const dir of commandDirs) {
|
|
423
|
-
let entries;
|
|
424
|
-
try {
|
|
425
|
-
entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
for (const entry of entries) {
|
|
431
|
-
if (entry.startsWith(".")) {
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
const fullPath = join(dir, entry);
|
|
435
|
-
try {
|
|
436
|
-
if (statSync(fullPath).isDirectory()) {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
catch {
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (!isWasmBinaryFile(fullPath) || seen.has(entry)) {
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
seen.add(entry);
|
|
447
|
-
commands.push(entry);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return commands;
|
|
451
|
-
}
|
|
452
|
-
function resolveDeclaredCommandSource(commandDir, commandName, aliases) {
|
|
453
|
-
let current = commandName;
|
|
454
|
-
const visited = new Set();
|
|
455
|
-
while (!visited.has(current)) {
|
|
456
|
-
visited.add(current);
|
|
457
|
-
const candidatePath = join(commandDir, current);
|
|
458
|
-
if (isWasmBinaryFile(candidatePath)) {
|
|
459
|
-
return candidatePath;
|
|
460
|
-
}
|
|
461
|
-
const next = aliases[current];
|
|
462
|
-
if (!next) {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
current = next;
|
|
466
|
-
}
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
function prepareCommandDirs(commandPackages) {
|
|
470
|
-
const commandDirs = [];
|
|
471
|
-
const tempDirs = [];
|
|
472
|
-
try {
|
|
473
|
-
for (const commandPackage of commandPackages) {
|
|
474
|
-
commandDirs.push(commandPackage.commandDir);
|
|
475
|
-
const aliasEntries = Object.entries(commandPackage.aliases)
|
|
476
|
-
.sort(([leftAlias], [rightAlias]) => leftAlias.localeCompare(rightAlias))
|
|
477
|
-
.flatMap(([aliasName]) => {
|
|
478
|
-
const aliasPath = join(commandPackage.commandDir, aliasName);
|
|
479
|
-
if (isWasmBinaryFile(aliasPath)) {
|
|
480
|
-
return [];
|
|
481
|
-
}
|
|
482
|
-
const sourcePath = resolveDeclaredCommandSource(commandPackage.commandDir, aliasName, commandPackage.aliases);
|
|
483
|
-
if (!sourcePath) {
|
|
484
|
-
return [];
|
|
485
|
-
}
|
|
486
|
-
return [[aliasName, sourcePath]];
|
|
487
|
-
});
|
|
488
|
-
if (aliasEntries.length === 0) {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
const aliasDir = mkdtempSync(join(tmpdir(), "agentos-command-aliases-"));
|
|
492
|
-
for (const [aliasName, sourcePath] of aliasEntries) {
|
|
493
|
-
writeFileSync(join(aliasDir, aliasName), readFileSync(sourcePath));
|
|
494
|
-
}
|
|
495
|
-
tempDirs.push(aliasDir);
|
|
496
|
-
commandDirs.push(aliasDir);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
for (const tempDir of tempDirs) {
|
|
501
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
502
|
-
}
|
|
503
|
-
throw error;
|
|
504
|
-
}
|
|
505
|
-
return {
|
|
506
|
-
commandDirs,
|
|
507
|
-
dispose() {
|
|
508
|
-
for (const tempDir of tempDirs) {
|
|
509
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
510
|
-
}
|
|
511
|
-
},
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
453
|
function collectConfiguredLowerPaths(config) {
|
|
515
454
|
const paths = new Set();
|
|
516
455
|
for (const lower of config?.lowers ?? []) {
|
|
@@ -533,16 +472,15 @@ function findBootstrapSeedEntry(config, path) {
|
|
|
533
472
|
return entry;
|
|
534
473
|
}
|
|
535
474
|
}
|
|
536
|
-
|
|
475
|
+
// No base-filesystem JSON read: standard non-default dir metadata comes from
|
|
476
|
+
// the constant table. When a base layer IS present these dirs are never
|
|
477
|
+
// emitted (see createKernelBootstrapLower), so this only seeds the no-base case.
|
|
478
|
+
const meta = KERNEL_POSIX_BOOTSTRAP_DIR_METADATA[path];
|
|
479
|
+
return meta ? { path, type: "directory", ...meta } : undefined;
|
|
537
480
|
}
|
|
538
|
-
function createKernelBootstrapLower(config,
|
|
481
|
+
function createKernelBootstrapLower(config, extraEntries = []) {
|
|
539
482
|
const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
|
|
540
483
|
const existingPaths = collectConfiguredLowerPaths(config);
|
|
541
|
-
if (includesBundledBaseLayer) {
|
|
542
|
-
for (const entry of getBaseFilesystemEntries()) {
|
|
543
|
-
existingPaths.add(entry.path);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
484
|
const entries = [
|
|
547
485
|
{
|
|
548
486
|
path: "/",
|
|
@@ -552,18 +490,24 @@ function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
|
|
|
552
490
|
gid: 0,
|
|
553
491
|
},
|
|
554
492
|
];
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
493
|
+
// Only run the FS bootstrap (creating the POSIX dir tree) when there is NO
|
|
494
|
+
// base layer. When the bundled base IS present, the sidecar's embedded base
|
|
495
|
+
// layer already provides every POSIX dir with the correct mode/owner, so we
|
|
496
|
+
// emit nothing here and never read its filesystem table.
|
|
497
|
+
if (!includesBundledBaseLayer) {
|
|
498
|
+
for (const dir of KERNEL_POSIX_BOOTSTRAP_DIRS) {
|
|
499
|
+
if (existingPaths.has(dir)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const seed = findBootstrapSeedEntry(config, dir);
|
|
503
|
+
entries.push({
|
|
504
|
+
path: dir,
|
|
505
|
+
type: "directory",
|
|
506
|
+
mode: seed?.type === "directory" ? seed.mode : "755",
|
|
507
|
+
uid: seed?.uid ?? 0,
|
|
508
|
+
gid: seed?.gid ?? 0,
|
|
509
|
+
});
|
|
558
510
|
}
|
|
559
|
-
const seed = findBootstrapSeedEntry(config, dir);
|
|
560
|
-
entries.push({
|
|
561
|
-
path: dir,
|
|
562
|
-
type: "directory",
|
|
563
|
-
mode: seed?.type === "directory" ? seed.mode : "755",
|
|
564
|
-
uid: seed?.uid ?? 0,
|
|
565
|
-
gid: seed?.gid ?? 0,
|
|
566
|
-
});
|
|
567
511
|
}
|
|
568
512
|
if (!includesBundledBaseLayer && !existingPaths.has("/usr/bin/env")) {
|
|
569
513
|
entries.push({
|
|
@@ -576,22 +520,6 @@ function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
|
|
|
576
520
|
encoding: "base64",
|
|
577
521
|
});
|
|
578
522
|
}
|
|
579
|
-
const uniqueCommands = [...new Set(commandNames)].sort((a, b) => a.localeCompare(b));
|
|
580
|
-
for (const command of uniqueCommands) {
|
|
581
|
-
const stubPath = `/bin/${command}`;
|
|
582
|
-
if (existingPaths.has(stubPath)) {
|
|
583
|
-
continue;
|
|
584
|
-
}
|
|
585
|
-
entries.push({
|
|
586
|
-
path: stubPath,
|
|
587
|
-
type: "file",
|
|
588
|
-
mode: "755",
|
|
589
|
-
uid: 0,
|
|
590
|
-
gid: 0,
|
|
591
|
-
content: KERNEL_COMMAND_STUB,
|
|
592
|
-
encoding: "utf8",
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
523
|
for (const entry of sortFilesystemEntries(extraEntries)) {
|
|
596
524
|
if (existingPaths.has(entry.path)) {
|
|
597
525
|
continue;
|
|
@@ -706,28 +634,6 @@ function latestMtimeMs(path) {
|
|
|
706
634
|
}
|
|
707
635
|
return latest;
|
|
708
636
|
}
|
|
709
|
-
function collectGuestCommandPaths(commandDirs) {
|
|
710
|
-
const guestPaths = new Map();
|
|
711
|
-
for (const [index, commandDir] of commandDirs.entries()) {
|
|
712
|
-
let entries;
|
|
713
|
-
try {
|
|
714
|
-
entries = readdirSync(commandDir).sort((left, right) => left.localeCompare(right));
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
for (const entry of entries) {
|
|
720
|
-
if (entry.startsWith(".")) {
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
if (!isWasmBinaryFile(join(commandDir, entry)) || guestPaths.has(entry)) {
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
guestPaths.set(entry, `/__secure_exec/commands/${index}/${entry}`);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return guestPaths;
|
|
730
|
-
}
|
|
731
637
|
async function resolveCompatLocalMounts(mounts) {
|
|
732
638
|
if (!mounts) {
|
|
733
639
|
return [];
|
|
@@ -796,52 +702,24 @@ function collectSidecarMountPlan(options) {
|
|
|
796
702
|
}
|
|
797
703
|
for (const mount of options.mounts ?? []) {
|
|
798
704
|
if (!isNativeMountConfig(mount)) {
|
|
705
|
+
sidecarMounts.push({
|
|
706
|
+
guestPath: mount.path,
|
|
707
|
+
readOnly: isOverlayMountConfig(mount)
|
|
708
|
+
? (mount.filesystem.mode ?? "ephemeral") === "read-only"
|
|
709
|
+
: (mount.readOnly ?? false),
|
|
710
|
+
plugin: {
|
|
711
|
+
id: "js_bridge",
|
|
712
|
+
config: {},
|
|
713
|
+
},
|
|
714
|
+
});
|
|
799
715
|
continue;
|
|
800
716
|
}
|
|
801
717
|
pushMount(mount);
|
|
802
718
|
}
|
|
803
|
-
for (const root of options.softwareRoots) {
|
|
804
|
-
pushMount({
|
|
805
|
-
path: root.vmPath,
|
|
806
|
-
plugin: createHostDirBackend({
|
|
807
|
-
hostPath: root.hostPath,
|
|
808
|
-
readOnly: true,
|
|
809
|
-
}),
|
|
810
|
-
readOnly: true,
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
for (const [index, commandDir] of options.commandDirs.entries()) {
|
|
814
|
-
pushMount({
|
|
815
|
-
path: `/__secure_exec/commands/${index}`,
|
|
816
|
-
plugin: createHostDirBackend({
|
|
817
|
-
hostPath: commandDir,
|
|
818
|
-
readOnly: true,
|
|
819
|
-
}),
|
|
820
|
-
readOnly: true,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
if (options.shimDir) {
|
|
824
|
-
pushMount({
|
|
825
|
-
path: "/usr/local/bin",
|
|
826
|
-
plugin: createHostDirBackend({
|
|
827
|
-
hostPath: options.shimDir,
|
|
828
|
-
readOnly: true,
|
|
829
|
-
}),
|
|
830
|
-
readOnly: true,
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
719
|
hostMounts.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
834
720
|
hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
835
721
|
return { sidecarMounts, hostMounts, hostPathMappings };
|
|
836
722
|
}
|
|
837
|
-
function materializeToolShimDir(toolKits) {
|
|
838
|
-
const shimDir = mkdtempSync(join(tmpdir(), "agentos-host-tools-shims-"));
|
|
839
|
-
writeFileSync(join(shimDir, "agentos"), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
840
|
-
for (const toolKit of toolKits) {
|
|
841
|
-
writeFileSync(join(shimDir, `agentos-${toolKit.name}`), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
842
|
-
}
|
|
843
|
-
return shimDir;
|
|
844
|
-
}
|
|
845
723
|
function collectToolkitBootstrapCommands(toolKits) {
|
|
846
724
|
if (toolKits.length === 0) {
|
|
847
725
|
return [];
|
|
@@ -1038,7 +916,7 @@ async function handleHostCallback(request, context) {
|
|
|
1038
916
|
return {
|
|
1039
917
|
type: "host_callback_result",
|
|
1040
918
|
invocation_id: payload.invocation_id,
|
|
1041
|
-
error: `EACCES: blocked by
|
|
919
|
+
error: `EACCES: blocked by binding.invoke policy for ${payload.callback_key}`,
|
|
1042
920
|
};
|
|
1043
921
|
}
|
|
1044
922
|
const parsed = tool.inputSchema.safeParse(payload.input);
|
|
@@ -1073,6 +951,134 @@ function buildToolMap(toolKits) {
|
|
|
1073
951
|
}
|
|
1074
952
|
return toolMap;
|
|
1075
953
|
}
|
|
954
|
+
function bridgeErrorMessage(error) {
|
|
955
|
+
return error instanceof Error ? error.message : String(error);
|
|
956
|
+
}
|
|
957
|
+
function toBridgeArgs(value) {
|
|
958
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
959
|
+
throw new Error("js_bridge args must be an object");
|
|
960
|
+
}
|
|
961
|
+
return value;
|
|
962
|
+
}
|
|
963
|
+
function bridgePath(mountId, value) {
|
|
964
|
+
if (!mountId.startsWith("/")) {
|
|
965
|
+
throw new Error(`Unsupported js_bridge mount id: ${mountId}`);
|
|
966
|
+
}
|
|
967
|
+
if (typeof value !== "string") {
|
|
968
|
+
throw new Error("js_bridge path argument must be a string");
|
|
969
|
+
}
|
|
970
|
+
return posixPath.normalize(posixPath.join(mountId, value));
|
|
971
|
+
}
|
|
972
|
+
function requireBridgeNumber(value, field) {
|
|
973
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
974
|
+
throw new Error(`js_bridge args.${field} must be a number`);
|
|
975
|
+
}
|
|
976
|
+
return value;
|
|
977
|
+
}
|
|
978
|
+
function decodeBridgeBytes(value, field) {
|
|
979
|
+
if (typeof value === "string") {
|
|
980
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
981
|
+
}
|
|
982
|
+
if (Array.isArray(value) &&
|
|
983
|
+
value.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)) {
|
|
984
|
+
return new Uint8Array(value);
|
|
985
|
+
}
|
|
986
|
+
throw new Error(`js_bridge args.${field} must be base64 bytes`);
|
|
987
|
+
}
|
|
988
|
+
async function handleJsBridgeCall(request, context) {
|
|
989
|
+
try {
|
|
990
|
+
const args = toBridgeArgs(request.args);
|
|
991
|
+
const fs = context.filesystem;
|
|
992
|
+
const path = () => bridgePath(request.mount_id, args.path);
|
|
993
|
+
let result;
|
|
994
|
+
switch (request.operation) {
|
|
995
|
+
case "readFile":
|
|
996
|
+
result = Buffer.from(await fs.readFile(path())).toString("base64");
|
|
997
|
+
break;
|
|
998
|
+
case "readDir":
|
|
999
|
+
result = await fs.readDir(path());
|
|
1000
|
+
break;
|
|
1001
|
+
case "readDirWithTypes":
|
|
1002
|
+
result = await fs.readDirWithTypes(path());
|
|
1003
|
+
break;
|
|
1004
|
+
case "writeFile":
|
|
1005
|
+
await fs.writeFile(path(), decodeBridgeBytes(args.content, "content"));
|
|
1006
|
+
break;
|
|
1007
|
+
case "createDir":
|
|
1008
|
+
await fs.createDir(path());
|
|
1009
|
+
break;
|
|
1010
|
+
case "mkdir":
|
|
1011
|
+
await fs.mkdir(path(), { recursive: args.recursive !== false });
|
|
1012
|
+
break;
|
|
1013
|
+
case "exists":
|
|
1014
|
+
result = await fs.exists(path());
|
|
1015
|
+
break;
|
|
1016
|
+
case "stat":
|
|
1017
|
+
result = await fs.stat(path());
|
|
1018
|
+
break;
|
|
1019
|
+
case "removeFile":
|
|
1020
|
+
await fs.removeFile(path());
|
|
1021
|
+
break;
|
|
1022
|
+
case "removeDir":
|
|
1023
|
+
await fs.removeDir(path());
|
|
1024
|
+
break;
|
|
1025
|
+
case "rename":
|
|
1026
|
+
await fs.rename(bridgePath(request.mount_id, args.oldPath), bridgePath(request.mount_id, args.newPath));
|
|
1027
|
+
break;
|
|
1028
|
+
case "realpath":
|
|
1029
|
+
result = await fs.realpath(path());
|
|
1030
|
+
break;
|
|
1031
|
+
case "symlink": {
|
|
1032
|
+
if (typeof args.target !== "string") {
|
|
1033
|
+
throw new Error("js_bridge args.target must be a string");
|
|
1034
|
+
}
|
|
1035
|
+
await fs.symlink(args.target, bridgePath(request.mount_id, args.linkPath));
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
case "readlink":
|
|
1039
|
+
result = await fs.readlink(path());
|
|
1040
|
+
break;
|
|
1041
|
+
case "lstat":
|
|
1042
|
+
result = await fs.lstat(path());
|
|
1043
|
+
break;
|
|
1044
|
+
case "link":
|
|
1045
|
+
await fs.link(bridgePath(request.mount_id, args.oldPath), bridgePath(request.mount_id, args.newPath));
|
|
1046
|
+
break;
|
|
1047
|
+
case "chmod":
|
|
1048
|
+
await fs.chmod(path(), requireBridgeNumber(args.mode, "mode"));
|
|
1049
|
+
break;
|
|
1050
|
+
case "chown":
|
|
1051
|
+
await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"));
|
|
1052
|
+
break;
|
|
1053
|
+
case "utimes":
|
|
1054
|
+
await fs.utimes(path(), requireBridgeNumber(args.atimeMs, "atimeMs"), requireBridgeNumber(args.mtimeMs, "mtimeMs"));
|
|
1055
|
+
break;
|
|
1056
|
+
case "truncate":
|
|
1057
|
+
await fs.truncate(path(), requireBridgeNumber(args.length, "length"));
|
|
1058
|
+
break;
|
|
1059
|
+
case "pread":
|
|
1060
|
+
result = Buffer.from(await fs.pread(path(), requireBridgeNumber(args.offset, "offset"), requireBridgeNumber(args.length, "length"))).toString("base64");
|
|
1061
|
+
break;
|
|
1062
|
+
case "pwrite":
|
|
1063
|
+
await fs.pwrite(path(), requireBridgeNumber(args.offset, "offset"), decodeBridgeBytes(args.content, "content"));
|
|
1064
|
+
break;
|
|
1065
|
+
default:
|
|
1066
|
+
throw new Error(`Unsupported js_bridge operation: ${request.operation}`);
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
type: "js_bridge_result",
|
|
1070
|
+
call_id: request.call_id,
|
|
1071
|
+
...(result === undefined ? {} : { result }),
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
return {
|
|
1076
|
+
type: "js_bridge_result",
|
|
1077
|
+
call_id: request.call_id,
|
|
1078
|
+
error: bridgeErrorMessage(error),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1076
1082
|
function parseHostCommandCallbackInput(input) {
|
|
1077
1083
|
const value = asRecord(input);
|
|
1078
1084
|
if (value.type !== "command" ||
|
|
@@ -1153,7 +1159,7 @@ async function invokeHostTool({ toolKit, toolName, args, cwd, context, }) {
|
|
|
1153
1159
|
const callbackKey = `${toolKit.name}:${toolName}`;
|
|
1154
1160
|
const permissionMode = toolPermissionMode(context.permissions, callbackKey);
|
|
1155
1161
|
if (permissionMode !== "allow") {
|
|
1156
|
-
throw new Error(`EACCES: blocked by
|
|
1162
|
+
throw new Error(`EACCES: blocked by binding.invoke policy for ${callbackKey}`);
|
|
1157
1163
|
}
|
|
1158
1164
|
const input = await parseHostToolInput(tool, args, cwd, context.readFile);
|
|
1159
1165
|
return executeHostTool(tool, callbackKey, input);
|
|
@@ -1309,7 +1315,7 @@ function describeToolPayload(toolKit, toolName) {
|
|
|
1309
1315
|
};
|
|
1310
1316
|
}
|
|
1311
1317
|
function toolPermissionMode(permissions, callbackKey) {
|
|
1312
|
-
const scope = permissions.
|
|
1318
|
+
const scope = permissions.binding;
|
|
1313
1319
|
if (!scope) {
|
|
1314
1320
|
return "deny";
|
|
1315
1321
|
}
|
|
@@ -1383,13 +1389,14 @@ export class AgentOs {
|
|
|
1383
1389
|
_pendingSessionRequestResolvers = new Map();
|
|
1384
1390
|
_processes = new Map();
|
|
1385
1391
|
_shells = new Map();
|
|
1392
|
+
// Value is the recorded exit code (undefined until/unless the exit
|
|
1393
|
+
// resolves) so waitShell can still report it after the entry is dropped.
|
|
1386
1394
|
_closedShellIds = new BoundedSet(CLOSED_SHELL_ID_RETENTION_LIMIT);
|
|
1387
1395
|
_pendingShellExitPromises = new Set();
|
|
1388
1396
|
_shellCounter = 0;
|
|
1389
1397
|
_acpTerminals = new Map();
|
|
1390
1398
|
_acpTerminalCounter = 0;
|
|
1391
1399
|
_softwareRoots;
|
|
1392
|
-
_softwareAgentConfigs;
|
|
1393
1400
|
_cronManager;
|
|
1394
1401
|
_toolKits = [];
|
|
1395
1402
|
_toolReference = "";
|
|
@@ -1397,22 +1404,29 @@ export class AgentOs {
|
|
|
1397
1404
|
_hostMounts;
|
|
1398
1405
|
_env;
|
|
1399
1406
|
_rootFilesystem;
|
|
1407
|
+
_additionalInstructions;
|
|
1400
1408
|
_sidecarLease = null;
|
|
1401
1409
|
_sidecarClient;
|
|
1402
1410
|
_sidecarSession;
|
|
1403
1411
|
_sidecarVm;
|
|
1404
1412
|
_disposeSidecarEventListener;
|
|
1405
|
-
|
|
1413
|
+
_agentStderrHandler;
|
|
1414
|
+
_agentExitHandler;
|
|
1415
|
+
_limitWarningHandler;
|
|
1416
|
+
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, additionalInstructions, agentStderrHandler, agentExitHandler, limitWarningHandler) {
|
|
1406
1417
|
this.#kernel = kernel;
|
|
1407
1418
|
this.sidecar = sidecar;
|
|
1408
1419
|
this._softwareRoots = softwareRoots;
|
|
1409
|
-
this._softwareAgentConfigs = softwareAgentConfigs;
|
|
1410
1420
|
this._hostMounts = hostMounts;
|
|
1411
1421
|
this._env = env;
|
|
1412
1422
|
this._rootFilesystem = rootFilesystem;
|
|
1413
1423
|
this._sidecarClient = sidecarClient;
|
|
1414
1424
|
this._sidecarSession = sidecarSession;
|
|
1415
1425
|
this._sidecarVm = sidecarVm;
|
|
1426
|
+
this._additionalInstructions = additionalInstructions;
|
|
1427
|
+
this._agentStderrHandler = agentStderrHandler;
|
|
1428
|
+
this._agentExitHandler = agentExitHandler;
|
|
1429
|
+
this._limitWarningHandler = limitWarningHandler;
|
|
1416
1430
|
this._disposeSidecarEventListener = this._sidecarClient.onEvent((event) => {
|
|
1417
1431
|
this._handleSidecarEvent(event);
|
|
1418
1432
|
});
|
|
@@ -1430,72 +1444,90 @@ export class AgentOs {
|
|
|
1430
1444
|
return getSharedAgentOsSidecarInternal(options);
|
|
1431
1445
|
}
|
|
1432
1446
|
static async create(options) {
|
|
1433
|
-
|
|
1447
|
+
options = parseAgentOsOptions(options);
|
|
1448
|
+
// Default software is FULLY DYNAMIC: this package's own NON-agent
|
|
1449
|
+
// @agentos-software/* dependencies (e.g. common), each default-exporting
|
|
1450
|
+
// its registry-built descriptor. Agent packages are NOT projected here —
|
|
1451
|
+
// createSession(id) links the matching agent dependency into the running
|
|
1452
|
+
// VM on first use, so agent closures (and pi's V8 snapshot bundle) only
|
|
1453
|
+
// enter VMs that run them. Unbuilt packages throw with build
|
|
1454
|
+
// instructions; opt out via defaultSoftware: false.
|
|
1455
|
+
const defaultSoftware = options?.defaultSoftware === false ? [] : resolveDefaultSoftware();
|
|
1456
|
+
const software = options?.defaultSoftware === false
|
|
1457
|
+
? (options.software ?? [])
|
|
1458
|
+
: [...defaultSoftware, ...(options?.software ?? [])];
|
|
1459
|
+
// Packages are projected by the SIDECAR: the client forwards only the
|
|
1460
|
+
// package `path` over `configureVm` and the sidecar reads metadata from
|
|
1461
|
+
// the packed vbare manifest (chunk1 of the `.aospkg`).
|
|
1462
|
+
const flatSoftware = software.flat();
|
|
1463
|
+
// Honor the AgentOsOptions.defaultSoftware contract ("entries already present
|
|
1464
|
+
// in `software` are not duplicated"): the default bundle and an explicitly
|
|
1465
|
+
// passed one resolve to the same package paths, so dedup by path. Without
|
|
1466
|
+
// this the sidecar rejects the second projection with a duplicate-command
|
|
1467
|
+
// error (e.g. coreutils' `[`).
|
|
1468
|
+
const seenPackagePaths = new Set();
|
|
1469
|
+
const sidecarPackages = flatSoftware.flatMap((entry) => {
|
|
1470
|
+
const ref = normalizePackageRef(entry);
|
|
1471
|
+
if (!ref || seenPackagePaths.has(ref.path)) {
|
|
1472
|
+
return [];
|
|
1473
|
+
}
|
|
1474
|
+
seenPackagePaths.add(ref.path);
|
|
1475
|
+
return [{ path: ref.path }];
|
|
1476
|
+
});
|
|
1477
|
+
// All package software is projected into `/opt/agentos` by the sidecar. The
|
|
1478
|
+
// client stages nothing host-side and parses NO package manifests: the
|
|
1479
|
+
// sidecar owns agent resolution, agent enumeration, and agent snapshot
|
|
1480
|
+
// bundle loading from the projected package dirs.
|
|
1434
1481
|
const localMounts = await resolveCompatLocalMounts(options?.mounts);
|
|
1435
1482
|
const toolKits = options?.toolKits;
|
|
1436
1483
|
if (toolKits && toolKits.length > 0) {
|
|
1437
1484
|
validateToolkits(toolKits);
|
|
1438
1485
|
}
|
|
1486
|
+
// Resolve the sidecar handle up front so every VM created here leases the
|
|
1487
|
+
// one shared native sidecar process owned by that handle.
|
|
1488
|
+
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1439
1489
|
const createVmAdmin = async () => {
|
|
1440
|
-
|
|
1490
|
+
// The `/opt/agentos` projection is built by the sidecar from the
|
|
1491
|
+
// forwarded `packages` (it owns the staging dir + read-only mount, and
|
|
1492
|
+
// runtime `linkSoftware` appends to that live dir). The client no longer
|
|
1493
|
+
// stages packages host-side.
|
|
1441
1494
|
const toolBootstrapCommands = collectToolkitBootstrapCommands(toolKits ?? []);
|
|
1442
|
-
const
|
|
1443
|
-
...
|
|
1444
|
-
...NODE_RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1495
|
+
const bootstrapCommands = [
|
|
1496
|
+
...RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1445
1497
|
...toolBootstrapCommands,
|
|
1446
|
-
]
|
|
1498
|
+
];
|
|
1499
|
+
const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem);
|
|
1447
1500
|
let toolReference = "";
|
|
1448
1501
|
let rootBridge = null;
|
|
1449
1502
|
let kernel = null;
|
|
1450
1503
|
let client = null;
|
|
1451
|
-
let
|
|
1504
|
+
let createdNativeVm = null;
|
|
1505
|
+
let nativeSession = null;
|
|
1452
1506
|
let cleanedUp = false;
|
|
1453
1507
|
const cleanup = async () => {
|
|
1454
1508
|
if (cleanedUp) {
|
|
1455
1509
|
return;
|
|
1456
1510
|
}
|
|
1457
1511
|
cleanedUp = true;
|
|
1458
|
-
if (toolShimDir) {
|
|
1459
|
-
rmSync(toolShimDir, { recursive: true, force: true });
|
|
1460
|
-
toolShimDir = null;
|
|
1461
|
-
}
|
|
1462
|
-
preparedCommandDirs.dispose();
|
|
1463
1512
|
};
|
|
1464
1513
|
try {
|
|
1465
1514
|
const env = getBaseEnvironment();
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
const commandGuestPaths =
|
|
1470
|
-
const requestedMounts = options?.moduleAccessCwd
|
|
1471
|
-
? [
|
|
1472
|
-
...(options.mounts ?? []),
|
|
1473
|
-
{
|
|
1474
|
-
path: "/root/node_modules",
|
|
1475
|
-
plugin: createHostDirBackend({
|
|
1476
|
-
hostPath: join(options.moduleAccessCwd, "node_modules"),
|
|
1477
|
-
readOnly: true,
|
|
1478
|
-
}),
|
|
1479
|
-
readOnly: true,
|
|
1480
|
-
},
|
|
1481
|
-
]
|
|
1482
|
-
: options?.mounts;
|
|
1515
|
+
// Guest command paths. The sidecar owns the `/opt/agentos` projection and
|
|
1516
|
+
// reports the exact projected package commands after `configureVm`.
|
|
1517
|
+
// Tool-shim commands are added below.
|
|
1518
|
+
const commandGuestPaths = new Map();
|
|
1483
1519
|
const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
|
|
1484
|
-
mounts:
|
|
1485
|
-
softwareRoots: processed.softwareRoots,
|
|
1486
|
-
commandDirs: preparedCommandDirs.commandDirs,
|
|
1487
|
-
shimDir: toolShimDir,
|
|
1488
|
-
});
|
|
1489
|
-
client = SidecarProcess.spawn({
|
|
1490
|
-
cwd: REPO_ROOT,
|
|
1491
|
-
command: ensureNativeSidecarBinary(),
|
|
1492
|
-
args: [],
|
|
1493
|
-
frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
|
|
1520
|
+
mounts: options?.mounts,
|
|
1494
1521
|
});
|
|
1495
|
-
|
|
1522
|
+
// Reuse the sidecar handle's single shared native process; this VM
|
|
1523
|
+
// becomes another tenant of it rather than spawning its own process.
|
|
1524
|
+
const shared = await ensureSharedSidecarNativeProcess(sidecar);
|
|
1525
|
+
client = shared.client;
|
|
1526
|
+
const session = shared.session;
|
|
1527
|
+
nativeSession = session;
|
|
1496
1528
|
const hostPermissions = options?.permissions ?? {
|
|
1497
1529
|
...allowAll,
|
|
1498
|
-
|
|
1530
|
+
binding: "allow",
|
|
1499
1531
|
};
|
|
1500
1532
|
const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
|
|
1501
1533
|
const createVmConfig = {
|
|
@@ -1504,18 +1536,25 @@ export class AgentOs {
|
|
|
1504
1536
|
permissions: sidecarPermissions,
|
|
1505
1537
|
limits: options?.limits,
|
|
1506
1538
|
loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
|
|
1539
|
+
bootstrapCommands,
|
|
1507
1540
|
// 0.3: the Node builtin allow-list moved from configureVm to
|
|
1508
1541
|
// VM creation. `undefined` => engine default allow-list;
|
|
1509
1542
|
// `[]` => deny all; `[..]` => exactly those. Platform and
|
|
1510
1543
|
// module resolution keep their engine defaults (full Node
|
|
1511
1544
|
// emulation), matching the prior behavior where Agent OS only
|
|
1512
1545
|
// constrained the builtin allow-list.
|
|
1513
|
-
...(options?.allowedNodeBuiltins !== undefined
|
|
1546
|
+
...(options?.allowedNodeBuiltins !== undefined ||
|
|
1547
|
+
options?.highResolutionTime !== undefined
|
|
1514
1548
|
? {
|
|
1515
1549
|
jsRuntime: {
|
|
1516
1550
|
platform: "node",
|
|
1517
1551
|
moduleResolution: "node",
|
|
1518
|
-
|
|
1552
|
+
...(options?.allowedNodeBuiltins !== undefined
|
|
1553
|
+
? { allowedBuiltins: options.allowedNodeBuiltins }
|
|
1554
|
+
: {}),
|
|
1555
|
+
...(options?.highResolutionTime !== undefined
|
|
1556
|
+
? { highResolutionTime: options.highResolutionTime }
|
|
1557
|
+
: {}),
|
|
1519
1558
|
},
|
|
1520
1559
|
}
|
|
1521
1560
|
: {}),
|
|
@@ -1524,14 +1563,25 @@ export class AgentOs {
|
|
|
1524
1563
|
runtime: "java_script",
|
|
1525
1564
|
config: createVmConfig,
|
|
1526
1565
|
});
|
|
1566
|
+
createdNativeVm = nativeVm;
|
|
1567
|
+
// Scope the readiness wait to THIS VM's ownership; on a shared process
|
|
1568
|
+
// other VMs are emitting their own lifecycle events concurrently.
|
|
1527
1569
|
await client.waitForEvent((event) => event.payload.type === "vm_lifecycle" &&
|
|
1528
|
-
event.payload.state === "ready"
|
|
1529
|
-
|
|
1570
|
+
event.payload.state === "ready" &&
|
|
1571
|
+
event.ownership.scope === "vm" &&
|
|
1572
|
+
event.ownership.vm_id === nativeVm.vmId, 10_000);
|
|
1573
|
+
const configuredVm = await client.configureVm(session, nativeVm, {
|
|
1530
1574
|
mounts: sidecarMounts,
|
|
1531
1575
|
permissions: sidecarPermissions,
|
|
1532
|
-
commandPermissions:
|
|
1576
|
+
commandPermissions: {},
|
|
1533
1577
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1578
|
+
packages: sidecarPackages,
|
|
1579
|
+
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1580
|
+
toolShimCommands: toolBootstrapCommands,
|
|
1534
1581
|
});
|
|
1582
|
+
for (const command of configuredVm.projectedCommands) {
|
|
1583
|
+
commandGuestPaths.set(command.name, command.guestPath);
|
|
1584
|
+
}
|
|
1535
1585
|
if (toolKits && toolKits.length > 0) {
|
|
1536
1586
|
toolReference = await registerToolkitsOnSidecar(client, session, nativeVm, toolKits);
|
|
1537
1587
|
commandGuestPaths.set("agentos", "/bin/agentos");
|
|
@@ -1544,10 +1594,23 @@ export class AgentOs {
|
|
|
1544
1594
|
session,
|
|
1545
1595
|
vm: nativeVm,
|
|
1546
1596
|
env,
|
|
1547
|
-
cwd: "/
|
|
1597
|
+
cwd: "/workspace",
|
|
1548
1598
|
localMounts,
|
|
1599
|
+
sidecarMounts,
|
|
1600
|
+
permissions: sidecarPermissions,
|
|
1601
|
+
commandPermissions: {},
|
|
1602
|
+
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1603
|
+
// Retained for runtime mount reconfigures: `configure_vm` is
|
|
1604
|
+
// replace-on-write for the whole payload, so post-boot mountFs
|
|
1605
|
+
// must resend the boot packages and tool shims.
|
|
1606
|
+
packages: sidecarPackages,
|
|
1607
|
+
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1608
|
+
toolShimCommands: toolBootstrapCommands,
|
|
1549
1609
|
commandGuestPaths,
|
|
1550
1610
|
onDispose: cleanup,
|
|
1611
|
+
// The native process is owned by the AgentOsSidecar handle and
|
|
1612
|
+
// shared across VMs; disposing this VM must not kill the process.
|
|
1613
|
+
ownsClient: false,
|
|
1551
1614
|
});
|
|
1552
1615
|
await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
|
|
1553
1616
|
kernel = rootBridge;
|
|
@@ -1557,6 +1620,10 @@ export class AgentOs {
|
|
|
1557
1620
|
hostMounts,
|
|
1558
1621
|
kernel,
|
|
1559
1622
|
rootView: rootBridge.createRootView(),
|
|
1623
|
+
sidecarMounts,
|
|
1624
|
+
sidecarPermissions,
|
|
1625
|
+
commandPermissions: {},
|
|
1626
|
+
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1560
1627
|
sidecarClient: client,
|
|
1561
1628
|
sidecarSession: session,
|
|
1562
1629
|
sidecarVm: nativeVm,
|
|
@@ -1581,6 +1648,8 @@ export class AgentOs {
|
|
|
1581
1648
|
};
|
|
1582
1649
|
}
|
|
1583
1650
|
catch (error) {
|
|
1651
|
+
// The native process is shared and owned by the sidecar handle, so
|
|
1652
|
+
// never dispose the client here — only tear down this VM's resources.
|
|
1584
1653
|
if (kernel) {
|
|
1585
1654
|
await kernel.dispose().catch(() => { });
|
|
1586
1655
|
}
|
|
@@ -1588,20 +1657,23 @@ export class AgentOs {
|
|
|
1588
1657
|
await rootBridge.dispose().catch(() => { });
|
|
1589
1658
|
}
|
|
1590
1659
|
else {
|
|
1591
|
-
|
|
1660
|
+
if (createdNativeVm && nativeSession && client) {
|
|
1661
|
+
await client
|
|
1662
|
+
.disposeVm(nativeSession, createdNativeVm)
|
|
1663
|
+
.catch(() => { });
|
|
1664
|
+
}
|
|
1592
1665
|
await cleanup();
|
|
1593
1666
|
}
|
|
1594
1667
|
throw error;
|
|
1595
1668
|
}
|
|
1596
1669
|
};
|
|
1597
|
-
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1598
1670
|
let sidecarLease = null;
|
|
1599
1671
|
try {
|
|
1600
1672
|
sidecarLease = await leaseAgentOsSidecarVm(sidecar, {
|
|
1601
1673
|
createVm: async () => createVmAdmin(),
|
|
1602
1674
|
});
|
|
1603
1675
|
const vmAdmin = sidecarLease.admin;
|
|
1604
|
-
const vm = new AgentOs(vmAdmin.kernel, sidecar,
|
|
1676
|
+
const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.additionalInstructions, options?.onAgentStderr ?? defaultAgentStderrHandler, options?.onAgentExit ?? defaultAgentExitHandler, options?.onLimitWarning);
|
|
1605
1677
|
vm._sidecarLease = sidecarLease;
|
|
1606
1678
|
vm._toolKits = vmAdmin.toolKits;
|
|
1607
1679
|
vm._toolReference = vmAdmin.toolReference;
|
|
@@ -1632,6 +1704,11 @@ export class AgentOs {
|
|
|
1632
1704
|
exitHandlers,
|
|
1633
1705
|
};
|
|
1634
1706
|
this._processes.set(proc.pid, entry);
|
|
1707
|
+
// NOTE: do NOT delete from `_processes` on exit — the public API contract
|
|
1708
|
+
// (getProcess/listProcesses/stopProcess, see process-management.test.ts)
|
|
1709
|
+
// requires exited processes to stay queryable (running:false, exitCode set).
|
|
1710
|
+
// `_processes` is a process table for this VM's lifetime; it is freed wholesale
|
|
1711
|
+
// in dispose(). (H5: the leak was that dispose() never cleared it.)
|
|
1635
1712
|
void proc.wait().then((code) => {
|
|
1636
1713
|
for (const h of exitHandlers)
|
|
1637
1714
|
h(code);
|
|
@@ -1665,14 +1742,14 @@ export class AgentOs {
|
|
|
1665
1742
|
const entry = this._processes.get(pid);
|
|
1666
1743
|
if (!entry)
|
|
1667
1744
|
throw new Error(`Process not found: ${pid}`);
|
|
1668
|
-
entry.proc.writeStdin(data);
|
|
1745
|
+
return entry.proc.writeStdin(data);
|
|
1669
1746
|
}
|
|
1670
1747
|
/** Close a process's stdin stream. */
|
|
1671
1748
|
closeProcessStdin(pid) {
|
|
1672
1749
|
const entry = this._processes.get(pid);
|
|
1673
1750
|
if (!entry)
|
|
1674
1751
|
throw new Error(`Process not found: ${pid}`);
|
|
1675
|
-
entry.proc.closeStdin();
|
|
1752
|
+
return entry.proc.closeStdin();
|
|
1676
1753
|
}
|
|
1677
1754
|
/** Subscribe to stdout data from a process. Returns an unsubscribe function. */
|
|
1678
1755
|
onProcessStdout(pid, handler) {
|
|
@@ -1733,35 +1810,6 @@ export class AgentOs {
|
|
|
1733
1810
|
_vfs() {
|
|
1734
1811
|
return this.#kernel.vfs;
|
|
1735
1812
|
}
|
|
1736
|
-
async _copyPath(from, to) {
|
|
1737
|
-
const stat = await this._vfs().lstat(from);
|
|
1738
|
-
if (stat.isSymbolicLink) {
|
|
1739
|
-
const target = await this._vfs().readlink(from);
|
|
1740
|
-
await this._vfs().symlink(target, to);
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
if (stat.isDirectory) {
|
|
1744
|
-
await this._mkdirp(posixPath.dirname(to));
|
|
1745
|
-
if (!(await this.#kernel.exists(to))) {
|
|
1746
|
-
await this.#kernel.mkdir(to);
|
|
1747
|
-
}
|
|
1748
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1749
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1750
|
-
const entries = await this.#kernel.readdir(from);
|
|
1751
|
-
for (const entry of entries) {
|
|
1752
|
-
if (entry === "." || entry === "..")
|
|
1753
|
-
continue;
|
|
1754
|
-
const fromPath = from === "/" ? `/${entry}` : `${from}/${entry}`;
|
|
1755
|
-
const toPath = to === "/" ? `/${entry}` : `${to}/${entry}`;
|
|
1756
|
-
await this._copyPath(fromPath, toPath);
|
|
1757
|
-
}
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
const content = await this.#kernel.readFile(from);
|
|
1761
|
-
await this.writeFile(to, content);
|
|
1762
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1763
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1764
|
-
}
|
|
1765
1813
|
async readFile(path) {
|
|
1766
1814
|
this._assertSafeAbsolutePath(path);
|
|
1767
1815
|
return this.#kernel.readFile(path);
|
|
@@ -1814,14 +1862,13 @@ export class AgentOs {
|
|
|
1814
1862
|
/** Recursively create directories (mkdir -p). */
|
|
1815
1863
|
async _mkdirp(path) {
|
|
1816
1864
|
this._assertWritableAbsolutePath(path);
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
}
|
|
1865
|
+
// `kernel.mkdir` is already recursive (it defaults to recursive=true on both
|
|
1866
|
+
// the native sidecar and compat kernels) and creating an existing directory is
|
|
1867
|
+
// a no-op, so a single call is sufficient. Do NOT probe each ancestor with
|
|
1868
|
+
// `exists()` first: on the native sidecar every read-side op
|
|
1869
|
+
// (exists/stat/readFile) triggers a full shadow-tree walk, so a per-component
|
|
1870
|
+
// exists() loop makes `mkdir -p` cost O(components * tree).
|
|
1871
|
+
await this.#kernel.mkdir(path);
|
|
1825
1872
|
}
|
|
1826
1873
|
async mkdir(path, options) {
|
|
1827
1874
|
if (options?.recursive) {
|
|
@@ -1836,49 +1883,31 @@ export class AgentOs {
|
|
|
1836
1883
|
}
|
|
1837
1884
|
async readdirRecursive(path, options) {
|
|
1838
1885
|
this._assertSafeAbsolutePath(path);
|
|
1839
|
-
const maxDepth = options?.maxDepth;
|
|
1840
1886
|
const exclude = options?.exclude ? new Set(options.exclude) : undefined;
|
|
1887
|
+
const entries = await this.#kernel.readdirRecursive(path, {
|
|
1888
|
+
maxDepth: options?.maxDepth,
|
|
1889
|
+
});
|
|
1890
|
+
const excludedPrefixes = [];
|
|
1841
1891
|
const results = [];
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
if (
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
const entries = await this.#kernel.readdir(dirPath);
|
|
1850
|
-
for (const name of entries) {
|
|
1851
|
-
if (name === "." || name === "..")
|
|
1852
|
-
continue;
|
|
1853
|
-
if (exclude?.has(name))
|
|
1854
|
-
continue;
|
|
1855
|
-
const fullPath = dirPath === "/" ? `/${name}` : `${dirPath}/${name}`;
|
|
1856
|
-
const s = await this.#kernel.stat(fullPath);
|
|
1857
|
-
if (s.isSymbolicLink) {
|
|
1858
|
-
results.push({
|
|
1859
|
-
path: fullPath,
|
|
1860
|
-
type: "symlink",
|
|
1861
|
-
size: s.size,
|
|
1862
|
-
});
|
|
1863
|
-
}
|
|
1864
|
-
else if (s.isDirectory) {
|
|
1865
|
-
results.push({
|
|
1866
|
-
path: fullPath,
|
|
1867
|
-
type: "directory",
|
|
1868
|
-
size: s.size,
|
|
1869
|
-
});
|
|
1870
|
-
if (maxDepth === undefined || depth < maxDepth) {
|
|
1871
|
-
queue.push([fullPath, depth + 1]);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
else {
|
|
1875
|
-
results.push({
|
|
1876
|
-
path: fullPath,
|
|
1877
|
-
type: "file",
|
|
1878
|
-
size: s.size,
|
|
1879
|
-
});
|
|
1892
|
+
for (const entry of entries) {
|
|
1893
|
+
if (excludedPrefixes.some((prefix) => entry.path === prefix || entry.path.startsWith(`${prefix}/`))) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (exclude?.has(entry.name)) {
|
|
1897
|
+
if (entry.isDirectory && !entry.isSymbolicLink) {
|
|
1898
|
+
excludedPrefixes.push(entry.path);
|
|
1880
1899
|
}
|
|
1900
|
+
continue;
|
|
1881
1901
|
}
|
|
1902
|
+
results.push({
|
|
1903
|
+
path: entry.path,
|
|
1904
|
+
type: entry.isSymbolicLink
|
|
1905
|
+
? "symlink"
|
|
1906
|
+
: entry.isDirectory
|
|
1907
|
+
? "directory"
|
|
1908
|
+
: "file",
|
|
1909
|
+
size: entry.size,
|
|
1910
|
+
});
|
|
1882
1911
|
}
|
|
1883
1912
|
return results;
|
|
1884
1913
|
}
|
|
@@ -1897,39 +1926,30 @@ export class AgentOs {
|
|
|
1897
1926
|
}
|
|
1898
1927
|
return createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
|
|
1899
1928
|
}
|
|
1900
|
-
|
|
1929
|
+
/**
|
|
1930
|
+
* Mount a filesystem into the running VM. Resolves once the mount has been
|
|
1931
|
+
* delivered to the native sidecar, so guest code can use it immediately
|
|
1932
|
+
* after the returned promise settles; a delivery failure rejects instead of
|
|
1933
|
+
* leaving the mount silently host-only.
|
|
1934
|
+
*/
|
|
1935
|
+
async mountFs(path, driver, options) {
|
|
1901
1936
|
this._assertSafeAbsolutePath(path);
|
|
1902
|
-
this.#kernel.mountFs(path, driver, { readOnly: options?.readOnly });
|
|
1937
|
+
await this.#kernel.mountFs(path, driver, { readOnly: options?.readOnly });
|
|
1903
1938
|
}
|
|
1904
|
-
unmountFs(path) {
|
|
1939
|
+
async unmountFs(path) {
|
|
1905
1940
|
this._assertSafeAbsolutePath(path);
|
|
1906
|
-
this.#kernel.unmountFs(path);
|
|
1941
|
+
await this.#kernel.unmountFs(path);
|
|
1907
1942
|
}
|
|
1908
1943
|
async move(from, to) {
|
|
1909
|
-
this.
|
|
1910
|
-
this.
|
|
1911
|
-
|
|
1912
|
-
if (!sourceStat.isDirectory || sourceStat.isSymbolicLink) {
|
|
1913
|
-
return this.#kernel.rename(from, to);
|
|
1914
|
-
}
|
|
1915
|
-
await this._copyPath(from, to);
|
|
1916
|
-
await this.delete(from, { recursive: true });
|
|
1944
|
+
this._assertWritableAbsolutePath(from);
|
|
1945
|
+
this._assertWritableAbsolutePath(to);
|
|
1946
|
+
await this.#kernel.movePath(from, to);
|
|
1917
1947
|
}
|
|
1918
1948
|
async delete(path, options) {
|
|
1919
|
-
this.
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const entries = await this.#kernel.readdir(path);
|
|
1924
|
-
for (const entry of entries) {
|
|
1925
|
-
if (entry === "." || entry === "..")
|
|
1926
|
-
continue;
|
|
1927
|
-
await this.delete(`${path}/${entry}`, { recursive: true });
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
return this.#kernel.removeDir(path);
|
|
1931
|
-
}
|
|
1932
|
-
return this.#kernel.removeFile(path);
|
|
1949
|
+
this._assertWritableAbsolutePath(path);
|
|
1950
|
+
await this.#kernel.removePath(path, {
|
|
1951
|
+
recursive: options?.recursive ?? false,
|
|
1952
|
+
});
|
|
1933
1953
|
}
|
|
1934
1954
|
async fetch(port, request) {
|
|
1935
1955
|
const url = new URL(request.url);
|
|
@@ -1964,15 +1984,24 @@ export class AgentOs {
|
|
|
1964
1984
|
const entry = {
|
|
1965
1985
|
handle,
|
|
1966
1986
|
dataHandlers,
|
|
1967
|
-
exitPromise: Promise.resolve(),
|
|
1987
|
+
exitPromise: Promise.resolve(0),
|
|
1968
1988
|
};
|
|
1969
|
-
const exitPromise = handle.wait()
|
|
1970
|
-
|
|
1989
|
+
const exitPromise = handle.wait();
|
|
1990
|
+
const finalize = (exitCode) => {
|
|
1971
1991
|
this._pendingShellExitPromises.delete(entry.exitPromise);
|
|
1972
1992
|
if (this._shells.get(shellId) === entry) {
|
|
1973
1993
|
this._shells.delete(shellId);
|
|
1974
|
-
this._closedShellIds.add(shellId);
|
|
1975
1994
|
}
|
|
1995
|
+
// Record the exit code even when closeShell already dropped the
|
|
1996
|
+
// entry, so a waitShell issued after exit still resolves with it.
|
|
1997
|
+
this._closedShellIds.add(shellId, exitCode);
|
|
1998
|
+
};
|
|
1999
|
+
entry.exitPromise = exitPromise.then((exitCode) => {
|
|
2000
|
+
finalize(exitCode);
|
|
2001
|
+
return exitCode;
|
|
2002
|
+
}, (error) => {
|
|
2003
|
+
finalize();
|
|
2004
|
+
throw error;
|
|
1976
2005
|
});
|
|
1977
2006
|
this._pendingShellExitPromises.add(entry.exitPromise);
|
|
1978
2007
|
this._shells.set(shellId, entry);
|
|
@@ -1986,7 +2015,7 @@ export class AgentOs {
|
|
|
1986
2015
|
const entry = this._shells.get(shellId);
|
|
1987
2016
|
if (!entry)
|
|
1988
2017
|
throw new Error(`Shell not found: ${shellId}`);
|
|
1989
|
-
entry.handle.write(data);
|
|
2018
|
+
return entry.handle.write(data);
|
|
1990
2019
|
}
|
|
1991
2020
|
/** Subscribe to data output from a shell. Returns an unsubscribe function. */
|
|
1992
2021
|
onShellData(shellId, handler) {
|
|
@@ -2005,6 +2034,21 @@ export class AgentOs {
|
|
|
2005
2034
|
throw new Error(`Shell not found: ${shellId}`);
|
|
2006
2035
|
entry.handle.resize(cols, rows);
|
|
2007
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Wait for a shell to exit and return its process exit code. Resolves
|
|
2039
|
+
* immediately for a shell that has already exited (within the closed-shell
|
|
2040
|
+
* retention window).
|
|
2041
|
+
*/
|
|
2042
|
+
waitShell(shellId) {
|
|
2043
|
+
const entry = this._shells.get(shellId);
|
|
2044
|
+
if (!entry) {
|
|
2045
|
+
const exitCode = this._closedShellIds.get(shellId);
|
|
2046
|
+
if (exitCode !== undefined)
|
|
2047
|
+
return Promise.resolve(exitCode);
|
|
2048
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2049
|
+
}
|
|
2050
|
+
return entry.exitPromise;
|
|
2051
|
+
}
|
|
2008
2052
|
/** Kill a shell process and remove it from tracking. */
|
|
2009
2053
|
closeShell(shellId) {
|
|
2010
2054
|
const entry = this._shells.get(shellId);
|
|
@@ -2119,53 +2163,60 @@ export class AgentOs {
|
|
|
2119
2163
|
}
|
|
2120
2164
|
return session;
|
|
2121
2165
|
}
|
|
2122
|
-
/**
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
}
|
|
2167
|
-
})
|
|
2168
|
-
|
|
2166
|
+
/**
|
|
2167
|
+
* Dynamically link a software package into the RUNNING VM. The package's
|
|
2168
|
+
* `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
|
|
2169
|
+
* pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
|
|
2170
|
+
* writing into its staging dir is reflected live with no reboot. An `agent`
|
|
2171
|
+
* block registers the package for `createSession(name)`. Persists for the VM's
|
|
2172
|
+
* lifetime (and across a snapshot iff the volume persists).
|
|
2173
|
+
*/
|
|
2174
|
+
async linkSoftware(descriptor) {
|
|
2175
|
+
const ref = normalizePackageRef(descriptor);
|
|
2176
|
+
if (!ref) {
|
|
2177
|
+
throw new Error("Invalid agentOS package reference");
|
|
2178
|
+
}
|
|
2179
|
+
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
2180
|
+
// appends the package to its live host-backed staging dir; the commands
|
|
2181
|
+
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
2182
|
+
// duplicate command, surfaced here as a thrown error.
|
|
2183
|
+
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, { path: ref.path });
|
|
2184
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
2185
|
+
this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
|
|
2186
|
+
command.name,
|
|
2187
|
+
command.guestPath,
|
|
2188
|
+
])));
|
|
2189
|
+
// Retain the linked package for runtime mount reconfigures:
|
|
2190
|
+
// `configure_vm` is replace-on-write, so a later `mountFs` that
|
|
2191
|
+
// resent only the boot packages would unproject this one.
|
|
2192
|
+
this.#kernel.registerLinkedPackage({ path: ref.path });
|
|
2193
|
+
}
|
|
2194
|
+
// The client parses no manifests: an `agent` block in the linked package is
|
|
2195
|
+
// picked up by the sidecar (it owns the projected `/opt/agentos` and answers
|
|
2196
|
+
// createSession/listAgents from it). Nothing to record client-side.
|
|
2197
|
+
}
|
|
2198
|
+
async providedCommands() {
|
|
2199
|
+
return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Returns all registered agents with their installation status. Thin forwarder:
|
|
2203
|
+
* sends `AcpListAgentsRequest` and maps the response. The sidecar enumerates the
|
|
2204
|
+
* projected `/opt/agentos` packages (the client parses no manifests). Every such
|
|
2205
|
+
* agent is a package materialized into the VM, so `installed` is always `true`.
|
|
2206
|
+
*/
|
|
2207
|
+
async listAgents() {
|
|
2208
|
+
const response = await this._sendAcpRequest({
|
|
2209
|
+
tag: "AcpListAgentsRequest",
|
|
2210
|
+
val: { reserved: false },
|
|
2211
|
+
});
|
|
2212
|
+
if (response.tag !== "AcpListAgentsResponse") {
|
|
2213
|
+
throw new Error(`unexpected list_agents response: ${response.tag}`);
|
|
2214
|
+
}
|
|
2215
|
+
return response.val.agents.map((agent) => ({
|
|
2216
|
+
id: agent.id,
|
|
2217
|
+
installed: agent.installed,
|
|
2218
|
+
adapterEntrypoint: agent.adapterEntrypoint,
|
|
2219
|
+
}));
|
|
2169
2220
|
}
|
|
2170
2221
|
_syncSessionState(session, state) {
|
|
2171
2222
|
session.processId = state.processId;
|
|
@@ -2244,6 +2295,106 @@ export class AgentOs {
|
|
|
2244
2295
|
session.eventHandlers.delete(subscriber);
|
|
2245
2296
|
};
|
|
2246
2297
|
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Warn once per session (host-visible) that a tool-permission request was
|
|
2300
|
+
* auto-denied because no `onPermissionRequest` handler is registered. Shared
|
|
2301
|
+
* by both the bare-callback and JSON-RPC permission paths so the message and
|
|
2302
|
+
* the once-per-session guard cannot drift between them.
|
|
2303
|
+
*/
|
|
2304
|
+
_warnNoPermissionHandlerOnce(session, params) {
|
|
2305
|
+
if (session.warnedNoPermissionHandler) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
session.warnedNoPermissionHandler = true;
|
|
2309
|
+
this._emitSessionWarning(session, `agentos: a tool-permission request (${this._permissionToolLabel(params)}) was ` +
|
|
2310
|
+
`auto-denied because no onPermissionRequest handler is registered for session ` +
|
|
2311
|
+
`${session.sessionId}. Register one with vm.onPermissionRequest(sessionId, ...) and ` +
|
|
2312
|
+
`reply via vm.respondPermission(...) to let the agent use tools.`);
|
|
2313
|
+
}
|
|
2314
|
+
/** Best-effort human label for the tool named in a permission request. */
|
|
2315
|
+
_permissionToolLabel(params) {
|
|
2316
|
+
if (typeof params.toolName === "string") {
|
|
2317
|
+
return params.toolName;
|
|
2318
|
+
}
|
|
2319
|
+
const toolCall = params.toolCall;
|
|
2320
|
+
if (toolCall &&
|
|
2321
|
+
typeof toolCall === "object" &&
|
|
2322
|
+
typeof toolCall.title === "string") {
|
|
2323
|
+
return toolCall.title;
|
|
2324
|
+
}
|
|
2325
|
+
return "a tool";
|
|
2326
|
+
}
|
|
2327
|
+
/**
|
|
2328
|
+
* Emit a host-visible warning for a session through the same agent-process log
|
|
2329
|
+
* channel that surfaces adapter stderr (`onAgentStderr`, default: process
|
|
2330
|
+
* stderr). Used for agent-os-owned diagnostics — e.g. a permission request
|
|
2331
|
+
* that was auto-denied because no host hook is registered — so they never fire
|
|
2332
|
+
* silently inside the sidecar.
|
|
2333
|
+
*/
|
|
2334
|
+
_emitSessionWarning(session, message) {
|
|
2335
|
+
const handler = this._agentStderrHandler;
|
|
2336
|
+
if (!handler) {
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
try {
|
|
2340
|
+
handler({
|
|
2341
|
+
sessionId: session.sessionId,
|
|
2342
|
+
agentType: session.agentType,
|
|
2343
|
+
processId: session.processId,
|
|
2344
|
+
pid: session.pid,
|
|
2345
|
+
chunk: new TextEncoder().encode(`${message}\n`),
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
catch {
|
|
2349
|
+
// A warning sink failure must never affect permission handling.
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
_recordAgentStderr(event) {
|
|
2353
|
+
const session = (event.sessionId ? this._sessions.get(event.sessionId) : undefined) ??
|
|
2354
|
+
[...this._sessions.values()].find((candidate) => candidate.processId === event.processId);
|
|
2355
|
+
const sessionId = event.sessionId || session?.sessionId;
|
|
2356
|
+
if (!sessionId) {
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
const handler = this._agentStderrHandler;
|
|
2360
|
+
if (!handler) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
try {
|
|
2364
|
+
handler({
|
|
2365
|
+
sessionId,
|
|
2366
|
+
agentType: event.agentType || session?.agentType || "",
|
|
2367
|
+
processId: event.processId,
|
|
2368
|
+
pid: session?.pid ?? null,
|
|
2369
|
+
chunk: new Uint8Array(event.chunk),
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
catch {
|
|
2373
|
+
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
_recordAgentExit(event) {
|
|
2377
|
+
const session = this._sessions.get(event.sessionId);
|
|
2378
|
+
const handler = this._agentExitHandler;
|
|
2379
|
+
if (!handler) {
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
handler({
|
|
2384
|
+
sessionId: event.sessionId,
|
|
2385
|
+
agentType: event.agentType || session?.agentType || "",
|
|
2386
|
+
processId: event.processId,
|
|
2387
|
+
pid: session?.pid ?? null,
|
|
2388
|
+
exitCode: event.exitCode,
|
|
2389
|
+
restart: event.restart,
|
|
2390
|
+
restartCount: event.restartCount,
|
|
2391
|
+
maxRestarts: event.maxRestarts,
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
catch {
|
|
2395
|
+
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2247
2398
|
_applySyntheticConfigOverrides(session) {
|
|
2248
2399
|
if (session.configOverrides.size === 0) {
|
|
2249
2400
|
return;
|
|
@@ -2266,6 +2417,10 @@ export class AgentOs {
|
|
|
2266
2417
|
if (event.payload.type !== "structured") {
|
|
2267
2418
|
return;
|
|
2268
2419
|
}
|
|
2420
|
+
if (event.payload.name === "limit_warning") {
|
|
2421
|
+
this._handleLimitWarning(event.payload.detail);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2269
2424
|
if (event.payload.name !== "acp.session_event") {
|
|
2270
2425
|
return;
|
|
2271
2426
|
}
|
|
@@ -2285,6 +2440,27 @@ export class AgentOs {
|
|
|
2285
2440
|
// Ignore malformed event payloads from the sidecar.
|
|
2286
2441
|
}
|
|
2287
2442
|
}
|
|
2443
|
+
_handleLimitWarning(detail) {
|
|
2444
|
+
if (!this._limitWarningHandler) {
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
const toNumber = (value) => {
|
|
2448
|
+
const parsed = Number(value);
|
|
2449
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
2450
|
+
};
|
|
2451
|
+
try {
|
|
2452
|
+
this._limitWarningHandler({
|
|
2453
|
+
limit: detail.limit ?? "",
|
|
2454
|
+
category: detail.category ?? "",
|
|
2455
|
+
observed: toNumber(detail.observed),
|
|
2456
|
+
capacity: toNumber(detail.capacity),
|
|
2457
|
+
fillPercent: toNumber(detail.fillPercent),
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
catch {
|
|
2461
|
+
// A throwing handler must never break the sidecar event loop.
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2288
2464
|
_handleAcpExtEvent(envelope) {
|
|
2289
2465
|
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
2290
2466
|
return;
|
|
@@ -2300,6 +2476,14 @@ export class AgentOs {
|
|
|
2300
2476
|
this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(event.val.notification)));
|
|
2301
2477
|
return;
|
|
2302
2478
|
}
|
|
2479
|
+
case "AcpAgentStderrEvent": {
|
|
2480
|
+
this._recordAgentStderr(event.val);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
case "AcpAgentExitedEvent": {
|
|
2484
|
+
this._recordAgentExit(event.val);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2303
2487
|
}
|
|
2304
2488
|
}
|
|
2305
2489
|
catch {
|
|
@@ -2515,37 +2699,30 @@ export class AgentOs {
|
|
|
2515
2699
|
this._syncSessionState(session, state);
|
|
2516
2700
|
}
|
|
2517
2701
|
async createSession(agentType, options) {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
// System-prompt assembly
|
|
2523
|
-
// the sidecar
|
|
2524
|
-
// skipOsInstructions plus the
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
const sessionCwd = options?.cwd ?? "/home/user";
|
|
2528
|
-
const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
|
|
2529
|
-
if ((agentType === "pi" || agentType === "pi-cli") &&
|
|
2530
|
-
!launchEnv.PI_ACP_PI_COMMAND) {
|
|
2531
|
-
launchEnv = {
|
|
2532
|
-
...launchEnv,
|
|
2533
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
2534
|
-
};
|
|
2535
|
-
}
|
|
2702
|
+
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
2703
|
+
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
2704
|
+
// projected `/opt/agentos/<name>/current/agentos-package.json` and spawns
|
|
2705
|
+
// (including the agent's static launch args and manifest env defaults).
|
|
2706
|
+
// System-prompt assembly/injection (launch args / OPENCODE_CONTEXTPATHS) is
|
|
2707
|
+
// owned by the sidecar; the host only forwards additionalInstructions /
|
|
2708
|
+
// skipOsInstructions plus the caller's env.
|
|
2709
|
+
const launchEnv = { ...options?.env };
|
|
2710
|
+
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2536
2711
|
const response = await this._sendAcpRequest({
|
|
2537
2712
|
tag: "AcpCreateSessionRequest",
|
|
2538
2713
|
val: {
|
|
2539
2714
|
agentType: String(agentType),
|
|
2540
2715
|
runtime: AcpRuntimeKind.JavaScript,
|
|
2541
|
-
|
|
2542
|
-
args: launchArgs,
|
|
2716
|
+
args: [],
|
|
2543
2717
|
env: new Map(Object.entries(launchEnv)),
|
|
2544
2718
|
cwd: sessionCwd,
|
|
2545
2719
|
mcpServers: JSON.stringify(options?.mcpServers ?? []),
|
|
2546
2720
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2547
2721
|
clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
|
|
2548
|
-
additionalInstructions: combineInstructions(options?.additionalInstructions
|
|
2722
|
+
additionalInstructions: combineInstructions([this._additionalInstructions, options?.additionalInstructions]
|
|
2723
|
+
.map((part) => part?.trim())
|
|
2724
|
+
.filter((part) => Boolean(part))
|
|
2725
|
+
.join("\n\n") || undefined, this._toolReference),
|
|
2549
2726
|
skipOsInstructions: options?.skipOsInstructions ?? false,
|
|
2550
2727
|
},
|
|
2551
2728
|
});
|
|
@@ -2596,27 +2773,11 @@ export class AgentOs {
|
|
|
2596
2773
|
* there is no surviving store and the fallback tier always runs.
|
|
2597
2774
|
*/
|
|
2598
2775
|
async resumeSession(sessionId, agentType, options) {
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
const
|
|
2604
|
-
let launchEnv = { ...config.defaultEnv, ...options?.env };
|
|
2605
|
-
const sessionCwd = options?.cwd ?? "/home/user";
|
|
2606
|
-
if ((agentType === "pi" || agentType === "pi-cli") &&
|
|
2607
|
-
!launchEnv.PI_ACP_PI_COMMAND) {
|
|
2608
|
-
launchEnv = {
|
|
2609
|
-
...launchEnv,
|
|
2610
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
2611
|
-
};
|
|
2612
|
-
}
|
|
2613
|
-
// The resume wire request has no dedicated `adapterEntrypoint` field; carry
|
|
2614
|
-
// the resolved entrypoint through env under the sidecar's reserved key. The
|
|
2615
|
-
// sidecar reads it and strips it before launching the adapter.
|
|
2616
|
-
launchEnv = {
|
|
2617
|
-
...launchEnv,
|
|
2618
|
-
[RESUME_ADAPTER_ENTRYPOINT_ENV]: adapterEntrypoint,
|
|
2619
|
-
};
|
|
2776
|
+
// The client is npm-agnostic: it sends only the agent NAME. The sidecar
|
|
2777
|
+
// resolves the name -> package -> entrypoint/env/launchArgs from the
|
|
2778
|
+
// projected manifest, exactly as createSession does.
|
|
2779
|
+
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2780
|
+
const launchEnv = { ...options?.env };
|
|
2620
2781
|
const response = await this._sendAcpRequest({
|
|
2621
2782
|
tag: "AcpResumeSessionRequest",
|
|
2622
2783
|
val: {
|
|
@@ -2632,6 +2793,10 @@ export class AgentOs {
|
|
|
2632
2793
|
}
|
|
2633
2794
|
const { sessionId: liveSessionId, mode } = response.val;
|
|
2634
2795
|
// Register + hydrate the live session so subsequent prompts route to it.
|
|
2796
|
+
const existing = this._sessions.get(liveSessionId);
|
|
2797
|
+
if (existing !== undefined && !existing.closed) {
|
|
2798
|
+
throw new Error(`session id collision: ${liveSessionId}`);
|
|
2799
|
+
}
|
|
2635
2800
|
const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
|
|
2636
2801
|
this._closedSessionIds.delete(liveSessionId);
|
|
2637
2802
|
this._sessions.set(liveSessionId, session);
|
|
@@ -2644,65 +2809,6 @@ export class AgentOs {
|
|
|
2644
2809
|
}
|
|
2645
2810
|
return { sessionId: liveSessionId, mode };
|
|
2646
2811
|
}
|
|
2647
|
-
/**
|
|
2648
|
-
* Resolve the VM bin entry point of an ACP adapter package.
|
|
2649
|
-
* Reads from the host filesystem since kernel.readFile() resolves through
|
|
2650
|
-
* mounts; adapter package.json is read directly off the host dir backing
|
|
2651
|
-
* the relevant mount.
|
|
2652
|
-
*/
|
|
2653
|
-
_resolveAdapterBin(adapterPackage) {
|
|
2654
|
-
return this._resolvePackageBin(adapterPackage);
|
|
2655
|
-
}
|
|
2656
|
-
/**
|
|
2657
|
-
* Parent of the host directory backing the `/root/node_modules` mount, if
|
|
2658
|
-
* the caller supplied one (e.g. via `nodeModulesMount(...)`). Used as the
|
|
2659
|
-
* `resolvePackageDir` start dir for adapter/agent package resolution when no
|
|
2660
|
-
* software root matches. `nodeModulesMount` mounts `<dir>/node_modules`, so
|
|
2661
|
-
* the start dir is `<dir>` (one level above the node_modules tree).
|
|
2662
|
-
*/
|
|
2663
|
-
_nodeModulesHostRoot() {
|
|
2664
|
-
for (const mount of this._hostMounts) {
|
|
2665
|
-
if (mount.vmPath === "/root/node_modules") {
|
|
2666
|
-
return dirname(mount.hostPath);
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
return null;
|
|
2670
|
-
}
|
|
2671
|
-
_resolvePackageBin(packageName, binName) {
|
|
2672
|
-
const vmPrefix = `/root/node_modules/${packageName}`;
|
|
2673
|
-
let hostPkgJsonPath = null;
|
|
2674
|
-
for (const root of this._softwareRoots) {
|
|
2675
|
-
if (root.vmPath === vmPrefix) {
|
|
2676
|
-
hostPkgJsonPath = join(root.hostPath, "package.json");
|
|
2677
|
-
break;
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
// Fall back to the host dir behind the `/root/node_modules` mount.
|
|
2681
|
-
if (!hostPkgJsonPath) {
|
|
2682
|
-
const nodeModulesRoot = this._nodeModulesHostRoot();
|
|
2683
|
-
if (!nodeModulesRoot) {
|
|
2684
|
-
throw new Error(`Cannot resolve package "${packageName}": no software root provides it and ` +
|
|
2685
|
-
`no /root/node_modules mount was supplied. Pass ` +
|
|
2686
|
-
`mounts: [nodeModulesMount("<host>/node_modules")] to AgentOs.create().`);
|
|
2687
|
-
}
|
|
2688
|
-
hostPkgJsonPath = join(resolvePackageDir(nodeModulesRoot, packageName), "package.json");
|
|
2689
|
-
}
|
|
2690
|
-
const pkg = JSON.parse(readFileSync(hostPkgJsonPath, "utf-8"));
|
|
2691
|
-
let binEntry;
|
|
2692
|
-
if (typeof pkg.bin === "string") {
|
|
2693
|
-
binEntry = pkg.bin;
|
|
2694
|
-
}
|
|
2695
|
-
else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
2696
|
-
binEntry =
|
|
2697
|
-
(binName ? pkg.bin[binName] : undefined) ??
|
|
2698
|
-
pkg.bin[packageName] ??
|
|
2699
|
-
Object.values(pkg.bin)[0];
|
|
2700
|
-
}
|
|
2701
|
-
if (!binEntry) {
|
|
2702
|
-
throw new Error(`No bin entry found in ${packageName}/package.json`);
|
|
2703
|
-
}
|
|
2704
|
-
return `${vmPrefix}/${binEntry}`;
|
|
2705
|
-
}
|
|
2706
2812
|
_installSidecarRequestHandler() {
|
|
2707
2813
|
const context = {
|
|
2708
2814
|
toolKits: this._toolKits,
|
|
@@ -2715,10 +2821,8 @@ export class AgentOs {
|
|
|
2715
2821
|
case "host_callback":
|
|
2716
2822
|
return handleHostCallback(request, context);
|
|
2717
2823
|
case "js_bridge_call":
|
|
2718
|
-
return
|
|
2719
|
-
|
|
2720
|
-
call_id: request.payload.call_id,
|
|
2721
|
-
error: `unsupported sidecar request type: ${request.payload.type}`,
|
|
2824
|
+
return handleJsBridgeCall(request.payload, {
|
|
2825
|
+
filesystem: this.#kernel.vfs,
|
|
2722
2826
|
});
|
|
2723
2827
|
case "ext":
|
|
2724
2828
|
return this._handleAcpExtSidecarRequest(request.payload.envelope);
|
|
@@ -2905,6 +3009,10 @@ export class AgentOs {
|
|
|
2905
3009
|
_acpMethod: request.method,
|
|
2906
3010
|
};
|
|
2907
3011
|
if (session.permissionHandlers.size === 0) {
|
|
3012
|
+
// Default-closed deny; warn once (host-visible) so a forgotten
|
|
3013
|
+
// onPermissionRequest handler is an observable cause rather than a
|
|
3014
|
+
// silent denial. See _warnNoPermissionHandlerOnce.
|
|
3015
|
+
this._warnNoPermissionHandlerOnce(session, permissionParams);
|
|
2908
3016
|
return this._buildAcpPermissionResult("reject", permissionParams);
|
|
2909
3017
|
}
|
|
2910
3018
|
const reply = await new Promise((resolve, reject) => {
|
|
@@ -3187,6 +3295,10 @@ export class AgentOs {
|
|
|
3187
3295
|
return "reject";
|
|
3188
3296
|
}
|
|
3189
3297
|
if (session.permissionHandlers.size === 0) {
|
|
3298
|
+
// Default-closed: deny when no host hook is listening, and warn once
|
|
3299
|
+
// (host-visible) so a forgotten onPermissionRequest handler is not an
|
|
3300
|
+
// invisible cause of an agent that cannot use any tool.
|
|
3301
|
+
this._warnNoPermissionHandlerOnce(session, params);
|
|
3190
3302
|
return "reject";
|
|
3191
3303
|
}
|
|
3192
3304
|
try {
|
|
@@ -3216,14 +3328,6 @@ export class AgentOs {
|
|
|
3216
3328
|
return "reject";
|
|
3217
3329
|
}
|
|
3218
3330
|
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Resolve an agent config by ID. Package-provided configs take
|
|
3221
|
-
* precedence over the hardcoded AGENT_CONFIGS.
|
|
3222
|
-
*/
|
|
3223
|
-
_resolveAgentConfig(agentType) {
|
|
3224
|
-
return (this._softwareAgentConfigs.get(agentType) ??
|
|
3225
|
-
AGENT_CONFIGS[agentType]);
|
|
3226
|
-
}
|
|
3227
3331
|
/**
|
|
3228
3332
|
* Gracefully destroy a session: cancel any pending work, close the client,
|
|
3229
3333
|
* and remove from tracking. Unlike close() which is abrupt, this attempts
|
|
@@ -3407,6 +3511,7 @@ export class AgentOs {
|
|
|
3407
3511
|
terminalExitPromises.push(terminal.waitPromise.then(() => undefined, () => undefined));
|
|
3408
3512
|
}
|
|
3409
3513
|
this._acpTerminals.clear();
|
|
3514
|
+
this._processes.clear();
|
|
3410
3515
|
await waitForTrackedExitPromises([...shellExitPromises, ...terminalExitPromises], SHELL_DISPOSE_TIMEOUT_MS);
|
|
3411
3516
|
this._disposeSidecarEventListener();
|
|
3412
3517
|
const sidecarLease = this._sidecarLease;
|
|
@@ -3436,6 +3541,163 @@ function resolveAgentOsSidecar(config) {
|
|
|
3436
3541
|
}
|
|
3437
3542
|
const sidecarStates = new WeakMap();
|
|
3438
3543
|
const sharedSidecars = new Map();
|
|
3544
|
+
let sidecarProcessExitHookInstalled = false;
|
|
3545
|
+
/**
|
|
3546
|
+
* Install a one-time, synchronous `process.on("exit")` hook that SIGKILLs any
|
|
3547
|
+
* pooled shared sidecar child. Once a one-shot host process is allowed to exit
|
|
3548
|
+
* (its sidecar handles are unref'd at 0 leases), this reaps the sidecar
|
|
3549
|
+
* immediately instead of waiting for its stdin-EOF grace window — no orphan, no
|
|
3550
|
+
* delay. We deliberately do NOT install SIGINT/SIGTERM handlers: a library
|
|
3551
|
+
* should not hijack the host's signal handling. SIGINT still reaches the sidecar
|
|
3552
|
+
* via the process group, and SIGTERM-driven exit still closes its stdin.
|
|
3553
|
+
*/
|
|
3554
|
+
function ensureSidecarProcessExitCleanup() {
|
|
3555
|
+
if (sidecarProcessExitHookInstalled)
|
|
3556
|
+
return;
|
|
3557
|
+
sidecarProcessExitHookInstalled = true;
|
|
3558
|
+
process.on("exit", () => {
|
|
3559
|
+
for (const sidecar of sharedSidecars.values()) {
|
|
3560
|
+
try {
|
|
3561
|
+
sidecarStates.get(sidecar)?.sharedChild?.kill?.("SIGKILL");
|
|
3562
|
+
}
|
|
3563
|
+
catch {
|
|
3564
|
+
// best-effort reap; the process is exiting regardless
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3569
|
+
function sidecarChildHandle(client) {
|
|
3570
|
+
// SidecarProcess -> StdioSidecarProtocolClient.child (the spawned ChildProcess).
|
|
3571
|
+
const protocolClient = client?.protocolClient;
|
|
3572
|
+
return protocolClient?.child ?? undefined;
|
|
3573
|
+
}
|
|
3574
|
+
/**
|
|
3575
|
+
* Apply the current hold state to the shared sidecar's child + stdio: ref them
|
|
3576
|
+
* while ≥1 hold is live so in-flight VM work keeps the host process alive; unref
|
|
3577
|
+
* them at 0 so a one-shot script exits on its own after `dispose()`. The sidecar
|
|
3578
|
+
* process itself stays running (reusable) and self-exits on stdin EOF when the
|
|
3579
|
+
* host finally goes away. Best-effort: never let ref/unref break VM lifecycle.
|
|
3580
|
+
*/
|
|
3581
|
+
function applySharedSidecarHold(state) {
|
|
3582
|
+
const child = state.sharedChild;
|
|
3583
|
+
if (!child)
|
|
3584
|
+
return;
|
|
3585
|
+
const hold = (state.eventLoopHolds ?? 0) > 0;
|
|
3586
|
+
for (const handle of [child, child.stdin, child.stdout, child.stderr]) {
|
|
3587
|
+
if (!handle)
|
|
3588
|
+
continue;
|
|
3589
|
+
try {
|
|
3590
|
+
if (hold)
|
|
3591
|
+
handle.ref?.();
|
|
3592
|
+
else
|
|
3593
|
+
handle.unref?.();
|
|
3594
|
+
}
|
|
3595
|
+
catch {
|
|
3596
|
+
// ref/unref is an optimization, not correctness-critical
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
/**
|
|
3601
|
+
* Take a hold for the entire create→use→dispose lifetime of one VM lease. Taken
|
|
3602
|
+
* BEFORE VM creation starts (not when the lease lands in `activeLeases`) so a VM
|
|
3603
|
+
* that is still mid-creation keeps the sidecar ref'd and a concurrent dispose
|
|
3604
|
+
* cannot unref it out from under the in-flight create.
|
|
3605
|
+
*/
|
|
3606
|
+
function acquireSharedSidecarHold(state) {
|
|
3607
|
+
state.eventLoopHolds = (state.eventLoopHolds ?? 0) + 1;
|
|
3608
|
+
if (state.eventLoopHolds === 1)
|
|
3609
|
+
applySharedSidecarHold(state);
|
|
3610
|
+
}
|
|
3611
|
+
/** Release a hold taken by {@link acquireSharedSidecarHold}; unref at 0. */
|
|
3612
|
+
function releaseSharedSidecarHold(state) {
|
|
3613
|
+
const current = state.eventLoopHolds ?? 0;
|
|
3614
|
+
if (current <= 0) {
|
|
3615
|
+
// The `holdReleased` guard makes each lease release exactly once, so this
|
|
3616
|
+
// should be unreachable. Warn rather than silently floor, per the repo's
|
|
3617
|
+
// no-silent-masking rule, so an accounting bug surfaces instead of hiding.
|
|
3618
|
+
state.eventLoopHolds = 0;
|
|
3619
|
+
console.warn("[agentos] shared sidecar event-loop hold released more than acquired");
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3622
|
+
state.eventLoopHolds = current - 1;
|
|
3623
|
+
if (state.eventLoopHolds === 0)
|
|
3624
|
+
applySharedSidecarHold(state);
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* Spawn-once accessor for a sidecar handle's shared native process. Concurrent
|
|
3628
|
+
* callers await the same promise, so one `AgentOsSidecar` maps to exactly one
|
|
3629
|
+
* `agent-os-sidecar` OS process for its whole lifetime.
|
|
3630
|
+
*/
|
|
3631
|
+
function ensureSharedSidecarNativeProcess(sidecar) {
|
|
3632
|
+
const state = getSidecarState(sidecar);
|
|
3633
|
+
if (!state.nativeProcess) {
|
|
3634
|
+
ensureSidecarProcessExitCleanup();
|
|
3635
|
+
state.nativeProcess = (async () => {
|
|
3636
|
+
const client = SidecarProcess.spawn({
|
|
3637
|
+
cwd: REPO_ROOT,
|
|
3638
|
+
command: ensureNativeSidecarBinary(),
|
|
3639
|
+
args: [],
|
|
3640
|
+
});
|
|
3641
|
+
// Track the child immediately — BEFORE the handshake await — so a
|
|
3642
|
+
// failed `authenticateAndOpenSession()` can still reap it (otherwise
|
|
3643
|
+
// the spawned child is untracked, unreapable, and pins the loop).
|
|
3644
|
+
state.sharedChild = sidecarChildHandle(client);
|
|
3645
|
+
if (!state.sharedChild) {
|
|
3646
|
+
// We reached into @rivet-dev/agentos-runtime-core internals to get the child for
|
|
3647
|
+
// idle-unref. If that shape ever changes this returns undefined and
|
|
3648
|
+
// the optimization silently stops working (one-shot scripts would
|
|
3649
|
+
// hang again). Make it loud rather than a silent regression.
|
|
3650
|
+
console.warn("[agentos] could not resolve the shared sidecar child handle; " +
|
|
3651
|
+
"standalone scripts may not exit cleanly after dispose(). " +
|
|
3652
|
+
"This usually means @rivet-dev/agentos-runtime-core internals changed.");
|
|
3653
|
+
}
|
|
3654
|
+
// Apply the current hold state to the just-spawned child.
|
|
3655
|
+
applySharedSidecarHold(state);
|
|
3656
|
+
try {
|
|
3657
|
+
const session = await client.authenticateAndOpenSession();
|
|
3658
|
+
return { client, session };
|
|
3659
|
+
}
|
|
3660
|
+
catch (error) {
|
|
3661
|
+
// Spawn/handshake failed: reap the child, drop the cached handle,
|
|
3662
|
+
// and CLEAR the rejected promise so the next create() retries
|
|
3663
|
+
// instead of permanently wedging on a rejected `nativeProcess`.
|
|
3664
|
+
try {
|
|
3665
|
+
state.sharedChild?.kill?.("SIGKILL");
|
|
3666
|
+
}
|
|
3667
|
+
catch {
|
|
3668
|
+
// already gone
|
|
3669
|
+
}
|
|
3670
|
+
state.sharedChild = undefined;
|
|
3671
|
+
state.nativeProcess = undefined;
|
|
3672
|
+
throw error;
|
|
3673
|
+
}
|
|
3674
|
+
})();
|
|
3675
|
+
}
|
|
3676
|
+
return state.nativeProcess;
|
|
3677
|
+
}
|
|
3678
|
+
/** Dispose a sidecar handle's shared native process, if one was spawned. */
|
|
3679
|
+
async function disposeSharedSidecarNativeProcess(state) {
|
|
3680
|
+
const pending = state.nativeProcess;
|
|
3681
|
+
if (!pending) {
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
state.nativeProcess = undefined;
|
|
3685
|
+
// The cached child is now dead; drop it (symmetric with the assignment in
|
|
3686
|
+
// ensureSharedSidecarNativeProcess). We deliberately do NOT zero
|
|
3687
|
+
// `eventLoopHolds` here: this runs only from `AgentOsSidecar.dispose()`, which
|
|
3688
|
+
// has already set the handle to `disposing` (so no new lease can acquire) and
|
|
3689
|
+
// drained `activeLeases`; the disposed handle's state is then abandoned. Force-
|
|
3690
|
+
// zeroing a shared counter could clobber a hold on a freshly re-acquired
|
|
3691
|
+
// process generation, so it is left to the balanced acquire/release pairs.
|
|
3692
|
+
state.sharedChild = undefined;
|
|
3693
|
+
try {
|
|
3694
|
+
const { client } = await pending;
|
|
3695
|
+
await client.dispose();
|
|
3696
|
+
}
|
|
3697
|
+
catch {
|
|
3698
|
+
// Process may have already exited; nothing to reclaim.
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3439
3701
|
export class AgentOsSidecar {
|
|
3440
3702
|
constructor(sidecarId, placement, sharedPool) {
|
|
3441
3703
|
sidecarStates.set(this, {
|
|
@@ -3470,6 +3732,8 @@ export class AgentOsSidecar {
|
|
|
3470
3732
|
}
|
|
3471
3733
|
state.activeLeases.clear();
|
|
3472
3734
|
state.description.activeVmCount = 0;
|
|
3735
|
+
// Tear down the shared native process after all leased VMs are gone.
|
|
3736
|
+
await disposeSharedSidecarNativeProcess(state);
|
|
3473
3737
|
state.description.state = "disposed";
|
|
3474
3738
|
if (state.sharedPool && sharedSidecars.get(state.sharedPool) === this) {
|
|
3475
3739
|
sharedSidecars.delete(state.sharedPool);
|
|
@@ -3531,6 +3795,18 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3531
3795
|
return transport;
|
|
3532
3796
|
},
|
|
3533
3797
|
});
|
|
3798
|
+
// Hold the shared sidecar's event-loop ref for this lease's WHOLE lifetime —
|
|
3799
|
+
// taken now, before VM creation, so a concurrent dispose cannot unref the
|
|
3800
|
+
// sidecar while this create is still in flight. Released exactly once on
|
|
3801
|
+
// dispose or on a failed create.
|
|
3802
|
+
acquireSharedSidecarHold(state);
|
|
3803
|
+
let holdReleased = false;
|
|
3804
|
+
const releaseHold = () => {
|
|
3805
|
+
if (holdReleased)
|
|
3806
|
+
return;
|
|
3807
|
+
holdReleased = true;
|
|
3808
|
+
releaseSharedSidecarHold(state);
|
|
3809
|
+
};
|
|
3534
3810
|
let disposed = false;
|
|
3535
3811
|
let leaseRecord;
|
|
3536
3812
|
try {
|
|
@@ -3555,6 +3831,10 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3555
3831
|
state.activeLeases.delete(leaseRecord);
|
|
3556
3832
|
state.description.activeVmCount = state.activeLeases.size;
|
|
3557
3833
|
await client.dispose();
|
|
3834
|
+
// Release this lease's hold; the shared sidecar is unref'd only
|
|
3835
|
+
// once the last hold (across all in-flight + active leases) drops,
|
|
3836
|
+
// so a one-shot host process can then exit on its own.
|
|
3837
|
+
releaseHold();
|
|
3558
3838
|
},
|
|
3559
3839
|
};
|
|
3560
3840
|
leaseRecord = {
|
|
@@ -3566,6 +3846,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3566
3846
|
}
|
|
3567
3847
|
catch (error) {
|
|
3568
3848
|
await client.dispose().catch(() => { });
|
|
3849
|
+
releaseHold();
|
|
3569
3850
|
throw error;
|
|
3570
3851
|
}
|
|
3571
3852
|
}
|