@rivet-dev/agentos-core 0.0.0-codex-agentos-native-platform-e2e.3f8b27c → 0.0.0-codex-actor-plugin-contract-coverage.115ca8a
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 +4 -7
- package/dist/agent-os.d.ts +150 -45
- package/dist/agent-os.js +769 -541
- 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 +22 -9
- package/dist/runtime-compat.js +126 -141
- package/dist/runtime.d.ts +32 -5
- package/dist/sandbox.d.ts +112 -0
- package/dist/sandbox.js +264 -0
- package/dist/sidecar/agentos-protocol.d.ts +88 -1
- package/dist/sidecar/agentos-protocol.js +134 -18
- package/dist/sidecar/native-process-client.d.ts +6 -3
- package/dist/sidecar/native-process-client.js +5 -3
- package/dist/sidecar/permissions.d.ts +1 -1
- package/dist/sidecar/permissions.js +3 -3
- package/dist/sidecar/rpc-client.d.ts +62 -4
- package/dist/sidecar/rpc-client.js +339 -85
- package/dist/types.d.ts +10 -3
- package/package.json +40 -34
- 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,26 +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
|
-
import commonSoftware from "@agentos-software/common";
|
|
8
6
|
import { validateToolkits } from "./host-tools.js";
|
|
9
7
|
import { zodToJsonSchema } from "./host-tools-zod.js";
|
|
8
|
+
import { parseAgentOsOptions } from "./options-schema.js";
|
|
10
9
|
import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
|
|
11
10
|
import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
|
|
12
11
|
export { isAcpTimeoutErrorData } from "./json-rpc.js";
|
|
13
12
|
const ACP_PROTOCOL_VERSION = 1;
|
|
14
13
|
const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
|
|
15
14
|
const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
|
|
16
|
-
/**
|
|
17
|
-
* Reserved `env` key on `AcpResumeSessionRequest` carrying the resolved adapter
|
|
18
|
-
* bin entrypoint. The resume wire request omits a dedicated `adapterEntrypoint`
|
|
19
|
-
* field; the sidecar reads the entrypoint from this key and strips it before
|
|
20
|
-
* launching the adapter. Must stay in sync with the sidecar constant of the same
|
|
21
|
-
* name in `crates/agentos-sidecar/src/acp_extension.rs`.
|
|
22
|
-
*/
|
|
23
|
-
const RESUME_ADAPTER_ENTRYPOINT_ENV = "AGENT_OS_RESUME_ADAPTER_ENTRYPOINT";
|
|
24
15
|
function defaultAcpClientCapabilities() {
|
|
25
16
|
return {
|
|
26
17
|
fs: {
|
|
@@ -41,15 +32,14 @@ async function waitForTrackedExitPromises(promises, timeoutMs) {
|
|
|
41
32
|
}),
|
|
42
33
|
]);
|
|
43
34
|
}
|
|
44
|
-
import {
|
|
45
|
-
import { getBaseEnvironment, getBaseFilesystemEntries, } from "./base-filesystem.js";
|
|
35
|
+
import { getBaseEnvironment } from "./base-filesystem.js";
|
|
46
36
|
import { CronManager } from "./cron/cron-manager.js";
|
|
47
37
|
import { TimerScheduleDriver } from "./cron/timer-driver.js";
|
|
48
38
|
import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
|
|
49
|
-
import { createHostDirBackend } from "./host-dir-mount.js";
|
|
50
39
|
import { serializeMountConfigForSidecar, } from "./js-bridge.js";
|
|
51
40
|
import { createSnapshotExport, } from "./layers.js";
|
|
52
|
-
import {
|
|
41
|
+
import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
|
|
42
|
+
import { resolveDefaultSoftware } from "./default-software.js";
|
|
53
43
|
import { allowAll } from "./runtime-compat.js";
|
|
54
44
|
import { AcpRuntimeKind, decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
|
|
55
45
|
import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
|
|
@@ -57,6 +47,9 @@ import { createAgentOsSidecarClient, NATIVE_SIDECAR_FRAME_TIMEOUT_MS, NativeSide
|
|
|
57
47
|
function defaultAgentStderrHandler(event) {
|
|
58
48
|
process.stderr.write(event.chunk);
|
|
59
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
|
+
}
|
|
60
53
|
const LEGACY_PERMISSION_METHOD = "request/permission";
|
|
61
54
|
const ACP_PERMISSION_METHOD = "session/request_permission";
|
|
62
55
|
class AcpDispatchError extends Error {
|
|
@@ -109,6 +102,28 @@ function toRecord(value) {
|
|
|
109
102
|
? value
|
|
110
103
|
: {};
|
|
111
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
|
+
}
|
|
112
127
|
function parseAcpJson(value, context) {
|
|
113
128
|
if (value === null) {
|
|
114
129
|
return undefined;
|
|
@@ -164,11 +179,11 @@ class BoundedSet {
|
|
|
164
179
|
}
|
|
165
180
|
this.limit = limit;
|
|
166
181
|
}
|
|
167
|
-
add(value) {
|
|
182
|
+
add(value, associated) {
|
|
168
183
|
if (this.#entries.has(value)) {
|
|
169
184
|
this.#entries.delete(value);
|
|
170
185
|
}
|
|
171
|
-
this.#entries.set(value,
|
|
186
|
+
this.#entries.set(value, associated);
|
|
172
187
|
if (this.#entries.size <= this.limit) {
|
|
173
188
|
return;
|
|
174
189
|
}
|
|
@@ -180,6 +195,9 @@ class BoundedSet {
|
|
|
180
195
|
has(value) {
|
|
181
196
|
return this.#entries.has(value);
|
|
182
197
|
}
|
|
198
|
+
get(value) {
|
|
199
|
+
return this.#entries.get(value);
|
|
200
|
+
}
|
|
183
201
|
delete(value) {
|
|
184
202
|
return this.#entries.delete(value);
|
|
185
203
|
}
|
|
@@ -227,6 +245,7 @@ function sessionEntryFromInit(sessionId, agentType, initData) {
|
|
|
227
245
|
agentInfo: initData.agentInfo ?? null,
|
|
228
246
|
eventHandlers: new Set(),
|
|
229
247
|
permissionHandlers: new Set(),
|
|
248
|
+
warnedNoPermissionHandler: false,
|
|
230
249
|
configOverrides: new Map(),
|
|
231
250
|
pendingPermissionReplies: new Map(),
|
|
232
251
|
};
|
|
@@ -395,8 +414,31 @@ const KERNEL_POSIX_BOOTSTRAP_DIRS = [
|
|
|
395
414
|
"/var/tmp",
|
|
396
415
|
"/etc/agentos",
|
|
397
416
|
];
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
];
|
|
400
442
|
const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
|
|
401
443
|
const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-sidecar");
|
|
402
444
|
const SIDECAR_BUILD_INPUTS = [
|
|
@@ -408,114 +450,6 @@ const SIDECAR_BUILD_INPUTS = [
|
|
|
408
450
|
join(REPO_ROOT, "crates/sidecar"),
|
|
409
451
|
];
|
|
410
452
|
let ensuredSidecarBinary = null;
|
|
411
|
-
function isWasmBinaryFile(path) {
|
|
412
|
-
try {
|
|
413
|
-
const header = readFileSync(path);
|
|
414
|
-
return (header.length >= 4 &&
|
|
415
|
-
header[0] === 0x00 &&
|
|
416
|
-
header[1] === 0x61 &&
|
|
417
|
-
header[2] === 0x73 &&
|
|
418
|
-
header[3] === 0x6d);
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
function collectBootstrapWasmCommands(commandDirs) {
|
|
425
|
-
const commands = [];
|
|
426
|
-
const seen = new Set();
|
|
427
|
-
for (const dir of commandDirs) {
|
|
428
|
-
let entries;
|
|
429
|
-
try {
|
|
430
|
-
entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
|
|
431
|
-
}
|
|
432
|
-
catch {
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
for (const entry of entries) {
|
|
436
|
-
if (entry.startsWith(".")) {
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
const fullPath = join(dir, entry);
|
|
440
|
-
try {
|
|
441
|
-
if (statSync(fullPath).isDirectory()) {
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (!isWasmBinaryFile(fullPath) || seen.has(entry)) {
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
seen.add(entry);
|
|
452
|
-
commands.push(entry);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return commands;
|
|
456
|
-
}
|
|
457
|
-
function resolveDeclaredCommandSource(commandDir, commandName, aliases) {
|
|
458
|
-
let current = commandName;
|
|
459
|
-
const visited = new Set();
|
|
460
|
-
while (!visited.has(current)) {
|
|
461
|
-
visited.add(current);
|
|
462
|
-
const candidatePath = join(commandDir, current);
|
|
463
|
-
if (isWasmBinaryFile(candidatePath)) {
|
|
464
|
-
return candidatePath;
|
|
465
|
-
}
|
|
466
|
-
const next = aliases[current];
|
|
467
|
-
if (!next) {
|
|
468
|
-
return null;
|
|
469
|
-
}
|
|
470
|
-
current = next;
|
|
471
|
-
}
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
function prepareCommandDirs(commandPackages) {
|
|
475
|
-
const commandDirs = [];
|
|
476
|
-
const tempDirs = [];
|
|
477
|
-
try {
|
|
478
|
-
for (const commandPackage of commandPackages) {
|
|
479
|
-
commandDirs.push(commandPackage.commandDir);
|
|
480
|
-
const aliasEntries = Object.entries(commandPackage.aliases)
|
|
481
|
-
.sort(([leftAlias], [rightAlias]) => leftAlias.localeCompare(rightAlias))
|
|
482
|
-
.flatMap(([aliasName]) => {
|
|
483
|
-
const aliasPath = join(commandPackage.commandDir, aliasName);
|
|
484
|
-
if (isWasmBinaryFile(aliasPath)) {
|
|
485
|
-
return [];
|
|
486
|
-
}
|
|
487
|
-
const sourcePath = resolveDeclaredCommandSource(commandPackage.commandDir, aliasName, commandPackage.aliases);
|
|
488
|
-
if (!sourcePath) {
|
|
489
|
-
return [];
|
|
490
|
-
}
|
|
491
|
-
return [[aliasName, sourcePath]];
|
|
492
|
-
});
|
|
493
|
-
if (aliasEntries.length === 0) {
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
const aliasDir = mkdtempSync(join(tmpdir(), "agentos-command-aliases-"));
|
|
497
|
-
for (const [aliasName, sourcePath] of aliasEntries) {
|
|
498
|
-
writeFileSync(join(aliasDir, aliasName), readFileSync(sourcePath));
|
|
499
|
-
}
|
|
500
|
-
tempDirs.push(aliasDir);
|
|
501
|
-
commandDirs.push(aliasDir);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
catch (error) {
|
|
505
|
-
for (const tempDir of tempDirs) {
|
|
506
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
507
|
-
}
|
|
508
|
-
throw error;
|
|
509
|
-
}
|
|
510
|
-
return {
|
|
511
|
-
commandDirs,
|
|
512
|
-
dispose() {
|
|
513
|
-
for (const tempDir of tempDirs) {
|
|
514
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
453
|
function collectConfiguredLowerPaths(config) {
|
|
520
454
|
const paths = new Set();
|
|
521
455
|
for (const lower of config?.lowers ?? []) {
|
|
@@ -538,16 +472,15 @@ function findBootstrapSeedEntry(config, path) {
|
|
|
538
472
|
return entry;
|
|
539
473
|
}
|
|
540
474
|
}
|
|
541
|
-
|
|
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;
|
|
542
480
|
}
|
|
543
|
-
function createKernelBootstrapLower(config,
|
|
481
|
+
function createKernelBootstrapLower(config, extraEntries = []) {
|
|
544
482
|
const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
|
|
545
483
|
const existingPaths = collectConfiguredLowerPaths(config);
|
|
546
|
-
if (includesBundledBaseLayer) {
|
|
547
|
-
for (const entry of getBaseFilesystemEntries()) {
|
|
548
|
-
existingPaths.add(entry.path);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
484
|
const entries = [
|
|
552
485
|
{
|
|
553
486
|
path: "/",
|
|
@@ -557,18 +490,24 @@ function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
|
|
|
557
490
|
gid: 0,
|
|
558
491
|
},
|
|
559
492
|
];
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
});
|
|
563
510
|
}
|
|
564
|
-
const seed = findBootstrapSeedEntry(config, dir);
|
|
565
|
-
entries.push({
|
|
566
|
-
path: dir,
|
|
567
|
-
type: "directory",
|
|
568
|
-
mode: seed?.type === "directory" ? seed.mode : "755",
|
|
569
|
-
uid: seed?.uid ?? 0,
|
|
570
|
-
gid: seed?.gid ?? 0,
|
|
571
|
-
});
|
|
572
511
|
}
|
|
573
512
|
if (!includesBundledBaseLayer && !existingPaths.has("/usr/bin/env")) {
|
|
574
513
|
entries.push({
|
|
@@ -581,22 +520,6 @@ function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
|
|
|
581
520
|
encoding: "base64",
|
|
582
521
|
});
|
|
583
522
|
}
|
|
584
|
-
const uniqueCommands = [...new Set(commandNames)].sort((a, b) => a.localeCompare(b));
|
|
585
|
-
for (const command of uniqueCommands) {
|
|
586
|
-
const stubPath = `/bin/${command}`;
|
|
587
|
-
if (existingPaths.has(stubPath)) {
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
entries.push({
|
|
591
|
-
path: stubPath,
|
|
592
|
-
type: "file",
|
|
593
|
-
mode: "755",
|
|
594
|
-
uid: 0,
|
|
595
|
-
gid: 0,
|
|
596
|
-
content: KERNEL_COMMAND_STUB,
|
|
597
|
-
encoding: "utf8",
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
523
|
for (const entry of sortFilesystemEntries(extraEntries)) {
|
|
601
524
|
if (existingPaths.has(entry.path)) {
|
|
602
525
|
continue;
|
|
@@ -711,28 +634,6 @@ function latestMtimeMs(path) {
|
|
|
711
634
|
}
|
|
712
635
|
return latest;
|
|
713
636
|
}
|
|
714
|
-
function collectGuestCommandPaths(commandDirs) {
|
|
715
|
-
const guestPaths = new Map();
|
|
716
|
-
for (const [index, commandDir] of commandDirs.entries()) {
|
|
717
|
-
let entries;
|
|
718
|
-
try {
|
|
719
|
-
entries = readdirSync(commandDir).sort((left, right) => left.localeCompare(right));
|
|
720
|
-
}
|
|
721
|
-
catch {
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
for (const entry of entries) {
|
|
725
|
-
if (entry.startsWith(".")) {
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
if (!isWasmBinaryFile(join(commandDir, entry)) || guestPaths.has(entry)) {
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
guestPaths.set(entry, `/__secure_exec/commands/${index}/${entry}`);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return guestPaths;
|
|
735
|
-
}
|
|
736
637
|
async function resolveCompatLocalMounts(mounts) {
|
|
737
638
|
if (!mounts) {
|
|
738
639
|
return [];
|
|
@@ -801,52 +702,24 @@ function collectSidecarMountPlan(options) {
|
|
|
801
702
|
}
|
|
802
703
|
for (const mount of options.mounts ?? []) {
|
|
803
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
|
+
});
|
|
804
715
|
continue;
|
|
805
716
|
}
|
|
806
717
|
pushMount(mount);
|
|
807
718
|
}
|
|
808
|
-
for (const root of options.softwareRoots) {
|
|
809
|
-
pushMount({
|
|
810
|
-
path: root.vmPath,
|
|
811
|
-
plugin: createHostDirBackend({
|
|
812
|
-
hostPath: root.hostPath,
|
|
813
|
-
readOnly: true,
|
|
814
|
-
}),
|
|
815
|
-
readOnly: true,
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
for (const [index, commandDir] of options.commandDirs.entries()) {
|
|
819
|
-
pushMount({
|
|
820
|
-
path: `/__secure_exec/commands/${index}`,
|
|
821
|
-
plugin: createHostDirBackend({
|
|
822
|
-
hostPath: commandDir,
|
|
823
|
-
readOnly: true,
|
|
824
|
-
}),
|
|
825
|
-
readOnly: true,
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
if (options.shimDir) {
|
|
829
|
-
pushMount({
|
|
830
|
-
path: "/usr/local/bin",
|
|
831
|
-
plugin: createHostDirBackend({
|
|
832
|
-
hostPath: options.shimDir,
|
|
833
|
-
readOnly: true,
|
|
834
|
-
}),
|
|
835
|
-
readOnly: true,
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
719
|
hostMounts.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
839
720
|
hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
|
|
840
721
|
return { sidecarMounts, hostMounts, hostPathMappings };
|
|
841
722
|
}
|
|
842
|
-
function materializeToolShimDir(toolKits) {
|
|
843
|
-
const shimDir = mkdtempSync(join(tmpdir(), "agentos-host-tools-shims-"));
|
|
844
|
-
writeFileSync(join(shimDir, "agentos"), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
845
|
-
for (const toolKit of toolKits) {
|
|
846
|
-
writeFileSync(join(shimDir, `agentos-${toolKit.name}`), KERNEL_COMMAND_STUB, { mode: 0o755 });
|
|
847
|
-
}
|
|
848
|
-
return shimDir;
|
|
849
|
-
}
|
|
850
723
|
function collectToolkitBootstrapCommands(toolKits) {
|
|
851
724
|
if (toolKits.length === 0) {
|
|
852
725
|
return [];
|
|
@@ -1043,7 +916,7 @@ async function handleHostCallback(request, context) {
|
|
|
1043
916
|
return {
|
|
1044
917
|
type: "host_callback_result",
|
|
1045
918
|
invocation_id: payload.invocation_id,
|
|
1046
|
-
error: `EACCES: blocked by
|
|
919
|
+
error: `EACCES: blocked by binding.invoke policy for ${payload.callback_key}`,
|
|
1047
920
|
};
|
|
1048
921
|
}
|
|
1049
922
|
const parsed = tool.inputSchema.safeParse(payload.input);
|
|
@@ -1078,6 +951,134 @@ function buildToolMap(toolKits) {
|
|
|
1078
951
|
}
|
|
1079
952
|
return toolMap;
|
|
1080
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
|
+
}
|
|
1081
1082
|
function parseHostCommandCallbackInput(input) {
|
|
1082
1083
|
const value = asRecord(input);
|
|
1083
1084
|
if (value.type !== "command" ||
|
|
@@ -1158,7 +1159,7 @@ async function invokeHostTool({ toolKit, toolName, args, cwd, context, }) {
|
|
|
1158
1159
|
const callbackKey = `${toolKit.name}:${toolName}`;
|
|
1159
1160
|
const permissionMode = toolPermissionMode(context.permissions, callbackKey);
|
|
1160
1161
|
if (permissionMode !== "allow") {
|
|
1161
|
-
throw new Error(`EACCES: blocked by
|
|
1162
|
+
throw new Error(`EACCES: blocked by binding.invoke policy for ${callbackKey}`);
|
|
1162
1163
|
}
|
|
1163
1164
|
const input = await parseHostToolInput(tool, args, cwd, context.readFile);
|
|
1164
1165
|
return executeHostTool(tool, callbackKey, input);
|
|
@@ -1314,7 +1315,7 @@ function describeToolPayload(toolKit, toolName) {
|
|
|
1314
1315
|
};
|
|
1315
1316
|
}
|
|
1316
1317
|
function toolPermissionMode(permissions, callbackKey) {
|
|
1317
|
-
const scope = permissions.
|
|
1318
|
+
const scope = permissions.binding;
|
|
1318
1319
|
if (!scope) {
|
|
1319
1320
|
return "deny";
|
|
1320
1321
|
}
|
|
@@ -1388,13 +1389,14 @@ export class AgentOs {
|
|
|
1388
1389
|
_pendingSessionRequestResolvers = new Map();
|
|
1389
1390
|
_processes = new Map();
|
|
1390
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.
|
|
1391
1394
|
_closedShellIds = new BoundedSet(CLOSED_SHELL_ID_RETENTION_LIMIT);
|
|
1392
1395
|
_pendingShellExitPromises = new Set();
|
|
1393
1396
|
_shellCounter = 0;
|
|
1394
1397
|
_acpTerminals = new Map();
|
|
1395
1398
|
_acpTerminalCounter = 0;
|
|
1396
1399
|
_softwareRoots;
|
|
1397
|
-
_softwareAgentConfigs;
|
|
1398
1400
|
_cronManager;
|
|
1399
1401
|
_toolKits = [];
|
|
1400
1402
|
_toolReference = "";
|
|
@@ -1402,24 +1404,29 @@ export class AgentOs {
|
|
|
1402
1404
|
_hostMounts;
|
|
1403
1405
|
_env;
|
|
1404
1406
|
_rootFilesystem;
|
|
1407
|
+
_additionalInstructions;
|
|
1405
1408
|
_sidecarLease = null;
|
|
1406
1409
|
_sidecarClient;
|
|
1407
1410
|
_sidecarSession;
|
|
1408
1411
|
_sidecarVm;
|
|
1409
1412
|
_disposeSidecarEventListener;
|
|
1410
1413
|
_agentStderrHandler;
|
|
1411
|
-
|
|
1414
|
+
_agentExitHandler;
|
|
1415
|
+
_limitWarningHandler;
|
|
1416
|
+
constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, additionalInstructions, agentStderrHandler, agentExitHandler, limitWarningHandler) {
|
|
1412
1417
|
this.#kernel = kernel;
|
|
1413
1418
|
this.sidecar = sidecar;
|
|
1414
1419
|
this._softwareRoots = softwareRoots;
|
|
1415
|
-
this._softwareAgentConfigs = softwareAgentConfigs;
|
|
1416
1420
|
this._hostMounts = hostMounts;
|
|
1417
1421
|
this._env = env;
|
|
1418
1422
|
this._rootFilesystem = rootFilesystem;
|
|
1419
1423
|
this._sidecarClient = sidecarClient;
|
|
1420
1424
|
this._sidecarSession = sidecarSession;
|
|
1421
1425
|
this._sidecarVm = sidecarVm;
|
|
1426
|
+
this._additionalInstructions = additionalInstructions;
|
|
1422
1427
|
this._agentStderrHandler = agentStderrHandler;
|
|
1428
|
+
this._agentExitHandler = agentExitHandler;
|
|
1429
|
+
this._limitWarningHandler = limitWarningHandler;
|
|
1423
1430
|
this._disposeSidecarEventListener = this._sidecarClient.onEvent((event) => {
|
|
1424
1431
|
this._handleSidecarEvent(event);
|
|
1425
1432
|
});
|
|
@@ -1437,75 +1444,90 @@ export class AgentOs {
|
|
|
1437
1444
|
return getSharedAgentOsSidecarInternal(options);
|
|
1438
1445
|
}
|
|
1439
1446
|
static async create(options) {
|
|
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();
|
|
1440
1456
|
const software = options?.defaultSoftware === false
|
|
1441
1457
|
? (options.software ?? [])
|
|
1442
|
-
: [
|
|
1443
|
-
|
|
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.
|
|
1444
1481
|
const localMounts = await resolveCompatLocalMounts(options?.mounts);
|
|
1445
1482
|
const toolKits = options?.toolKits;
|
|
1446
1483
|
if (toolKits && toolKits.length > 0) {
|
|
1447
1484
|
validateToolkits(toolKits);
|
|
1448
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);
|
|
1449
1489
|
const createVmAdmin = async () => {
|
|
1450
|
-
|
|
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.
|
|
1451
1494
|
const toolBootstrapCommands = collectToolkitBootstrapCommands(toolKits ?? []);
|
|
1452
|
-
const
|
|
1453
|
-
...
|
|
1454
|
-
...NODE_RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1495
|
+
const bootstrapCommands = [
|
|
1496
|
+
...RUNTIME_BOOTSTRAP_COMMANDS,
|
|
1455
1497
|
...toolBootstrapCommands,
|
|
1456
|
-
]
|
|
1498
|
+
];
|
|
1499
|
+
const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem);
|
|
1457
1500
|
let toolReference = "";
|
|
1458
1501
|
let rootBridge = null;
|
|
1459
1502
|
let kernel = null;
|
|
1460
1503
|
let client = null;
|
|
1461
|
-
let
|
|
1504
|
+
let createdNativeVm = null;
|
|
1505
|
+
let nativeSession = null;
|
|
1462
1506
|
let cleanedUp = false;
|
|
1463
1507
|
const cleanup = async () => {
|
|
1464
1508
|
if (cleanedUp) {
|
|
1465
1509
|
return;
|
|
1466
1510
|
}
|
|
1467
1511
|
cleanedUp = true;
|
|
1468
|
-
if (toolShimDir) {
|
|
1469
|
-
rmSync(toolShimDir, { recursive: true, force: true });
|
|
1470
|
-
toolShimDir = null;
|
|
1471
|
-
}
|
|
1472
|
-
preparedCommandDirs.dispose();
|
|
1473
1512
|
};
|
|
1474
1513
|
try {
|
|
1475
1514
|
const env = getBaseEnvironment();
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const commandGuestPaths =
|
|
1480
|
-
const requestedMounts = options?.moduleAccessCwd
|
|
1481
|
-
? [
|
|
1482
|
-
...(options.mounts ?? []),
|
|
1483
|
-
{
|
|
1484
|
-
path: "/root/node_modules",
|
|
1485
|
-
plugin: createHostDirBackend({
|
|
1486
|
-
hostPath: join(options.moduleAccessCwd, "node_modules"),
|
|
1487
|
-
readOnly: true,
|
|
1488
|
-
}),
|
|
1489
|
-
readOnly: true,
|
|
1490
|
-
},
|
|
1491
|
-
]
|
|
1492
|
-
: 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();
|
|
1493
1519
|
const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
|
|
1494
|
-
mounts:
|
|
1495
|
-
softwareRoots: processed.softwareRoots,
|
|
1496
|
-
commandDirs: preparedCommandDirs.commandDirs,
|
|
1497
|
-
shimDir: toolShimDir,
|
|
1498
|
-
});
|
|
1499
|
-
client = SidecarProcess.spawn({
|
|
1500
|
-
cwd: REPO_ROOT,
|
|
1501
|
-
command: ensureNativeSidecarBinary(),
|
|
1502
|
-
args: [],
|
|
1503
|
-
frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
|
|
1520
|
+
mounts: options?.mounts,
|
|
1504
1521
|
});
|
|
1505
|
-
|
|
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;
|
|
1506
1528
|
const hostPermissions = options?.permissions ?? {
|
|
1507
1529
|
...allowAll,
|
|
1508
|
-
|
|
1530
|
+
binding: "allow",
|
|
1509
1531
|
};
|
|
1510
1532
|
const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
|
|
1511
1533
|
const createVmConfig = {
|
|
@@ -1514,18 +1536,25 @@ export class AgentOs {
|
|
|
1514
1536
|
permissions: sidecarPermissions,
|
|
1515
1537
|
limits: options?.limits,
|
|
1516
1538
|
loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
|
|
1539
|
+
bootstrapCommands,
|
|
1517
1540
|
// 0.3: the Node builtin allow-list moved from configureVm to
|
|
1518
1541
|
// VM creation. `undefined` => engine default allow-list;
|
|
1519
1542
|
// `[]` => deny all; `[..]` => exactly those. Platform and
|
|
1520
1543
|
// module resolution keep their engine defaults (full Node
|
|
1521
1544
|
// emulation), matching the prior behavior where Agent OS only
|
|
1522
1545
|
// constrained the builtin allow-list.
|
|
1523
|
-
...(options?.allowedNodeBuiltins !== undefined
|
|
1546
|
+
...(options?.allowedNodeBuiltins !== undefined ||
|
|
1547
|
+
options?.highResolutionTime !== undefined
|
|
1524
1548
|
? {
|
|
1525
1549
|
jsRuntime: {
|
|
1526
1550
|
platform: "node",
|
|
1527
1551
|
moduleResolution: "node",
|
|
1528
|
-
|
|
1552
|
+
...(options?.allowedNodeBuiltins !== undefined
|
|
1553
|
+
? { allowedBuiltins: options.allowedNodeBuiltins }
|
|
1554
|
+
: {}),
|
|
1555
|
+
...(options?.highResolutionTime !== undefined
|
|
1556
|
+
? { highResolutionTime: options.highResolutionTime }
|
|
1557
|
+
: {}),
|
|
1529
1558
|
},
|
|
1530
1559
|
}
|
|
1531
1560
|
: {}),
|
|
@@ -1534,14 +1563,25 @@ export class AgentOs {
|
|
|
1534
1563
|
runtime: "java_script",
|
|
1535
1564
|
config: createVmConfig,
|
|
1536
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.
|
|
1537
1569
|
await client.waitForEvent((event) => event.payload.type === "vm_lifecycle" &&
|
|
1538
|
-
event.payload.state === "ready"
|
|
1539
|
-
|
|
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, {
|
|
1540
1574
|
mounts: sidecarMounts,
|
|
1541
1575
|
permissions: sidecarPermissions,
|
|
1542
|
-
commandPermissions:
|
|
1576
|
+
commandPermissions: {},
|
|
1543
1577
|
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1578
|
+
packages: sidecarPackages,
|
|
1579
|
+
packagesMountAt: OPT_AGENTOS_ROOT,
|
|
1580
|
+
toolShimCommands: toolBootstrapCommands,
|
|
1544
1581
|
});
|
|
1582
|
+
for (const command of configuredVm.projectedCommands) {
|
|
1583
|
+
commandGuestPaths.set(command.name, command.guestPath);
|
|
1584
|
+
}
|
|
1545
1585
|
if (toolKits && toolKits.length > 0) {
|
|
1546
1586
|
toolReference = await registerToolkitsOnSidecar(client, session, nativeVm, toolKits);
|
|
1547
1587
|
commandGuestPaths.set("agentos", "/bin/agentos");
|
|
@@ -1556,8 +1596,15 @@ export class AgentOs {
|
|
|
1556
1596
|
env,
|
|
1557
1597
|
cwd: "/workspace",
|
|
1558
1598
|
localMounts,
|
|
1599
|
+
sidecarMounts,
|
|
1600
|
+
permissions: sidecarPermissions,
|
|
1601
|
+
commandPermissions: {},
|
|
1602
|
+
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1559
1603
|
commandGuestPaths,
|
|
1560
1604
|
onDispose: cleanup,
|
|
1605
|
+
// The native process is owned by the AgentOsSidecar handle and
|
|
1606
|
+
// shared across VMs; disposing this VM must not kill the process.
|
|
1607
|
+
ownsClient: false,
|
|
1561
1608
|
});
|
|
1562
1609
|
await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
|
|
1563
1610
|
kernel = rootBridge;
|
|
@@ -1567,6 +1614,10 @@ export class AgentOs {
|
|
|
1567
1614
|
hostMounts,
|
|
1568
1615
|
kernel,
|
|
1569
1616
|
rootView: rootBridge.createRootView(),
|
|
1617
|
+
sidecarMounts,
|
|
1618
|
+
sidecarPermissions,
|
|
1619
|
+
commandPermissions: {},
|
|
1620
|
+
loopbackExemptPorts: options?.loopbackExemptPorts,
|
|
1570
1621
|
sidecarClient: client,
|
|
1571
1622
|
sidecarSession: session,
|
|
1572
1623
|
sidecarVm: nativeVm,
|
|
@@ -1591,6 +1642,8 @@ export class AgentOs {
|
|
|
1591
1642
|
};
|
|
1592
1643
|
}
|
|
1593
1644
|
catch (error) {
|
|
1645
|
+
// The native process is shared and owned by the sidecar handle, so
|
|
1646
|
+
// never dispose the client here — only tear down this VM's resources.
|
|
1594
1647
|
if (kernel) {
|
|
1595
1648
|
await kernel.dispose().catch(() => { });
|
|
1596
1649
|
}
|
|
@@ -1598,20 +1651,23 @@ export class AgentOs {
|
|
|
1598
1651
|
await rootBridge.dispose().catch(() => { });
|
|
1599
1652
|
}
|
|
1600
1653
|
else {
|
|
1601
|
-
|
|
1654
|
+
if (createdNativeVm && nativeSession && client) {
|
|
1655
|
+
await client
|
|
1656
|
+
.disposeVm(nativeSession, createdNativeVm)
|
|
1657
|
+
.catch(() => { });
|
|
1658
|
+
}
|
|
1602
1659
|
await cleanup();
|
|
1603
1660
|
}
|
|
1604
1661
|
throw error;
|
|
1605
1662
|
}
|
|
1606
1663
|
};
|
|
1607
|
-
const sidecar = resolveAgentOsSidecar(options?.sidecar);
|
|
1608
1664
|
let sidecarLease = null;
|
|
1609
1665
|
try {
|
|
1610
1666
|
sidecarLease = await leaseAgentOsSidecarVm(sidecar, {
|
|
1611
1667
|
createVm: async () => createVmAdmin(),
|
|
1612
1668
|
});
|
|
1613
1669
|
const vmAdmin = sidecarLease.admin;
|
|
1614
|
-
const vm = new AgentOs(vmAdmin.kernel, sidecar,
|
|
1670
|
+
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);
|
|
1615
1671
|
vm._sidecarLease = sidecarLease;
|
|
1616
1672
|
vm._toolKits = vmAdmin.toolKits;
|
|
1617
1673
|
vm._toolReference = vmAdmin.toolReference;
|
|
@@ -1642,6 +1698,11 @@ export class AgentOs {
|
|
|
1642
1698
|
exitHandlers,
|
|
1643
1699
|
};
|
|
1644
1700
|
this._processes.set(proc.pid, entry);
|
|
1701
|
+
// NOTE: do NOT delete from `_processes` on exit — the public API contract
|
|
1702
|
+
// (getProcess/listProcesses/stopProcess, see process-management.test.ts)
|
|
1703
|
+
// requires exited processes to stay queryable (running:false, exitCode set).
|
|
1704
|
+
// `_processes` is a process table for this VM's lifetime; it is freed wholesale
|
|
1705
|
+
// in dispose(). (H5: the leak was that dispose() never cleared it.)
|
|
1645
1706
|
void proc.wait().then((code) => {
|
|
1646
1707
|
for (const h of exitHandlers)
|
|
1647
1708
|
h(code);
|
|
@@ -1675,14 +1736,14 @@ export class AgentOs {
|
|
|
1675
1736
|
const entry = this._processes.get(pid);
|
|
1676
1737
|
if (!entry)
|
|
1677
1738
|
throw new Error(`Process not found: ${pid}`);
|
|
1678
|
-
entry.proc.writeStdin(data);
|
|
1739
|
+
return entry.proc.writeStdin(data);
|
|
1679
1740
|
}
|
|
1680
1741
|
/** Close a process's stdin stream. */
|
|
1681
1742
|
closeProcessStdin(pid) {
|
|
1682
1743
|
const entry = this._processes.get(pid);
|
|
1683
1744
|
if (!entry)
|
|
1684
1745
|
throw new Error(`Process not found: ${pid}`);
|
|
1685
|
-
entry.proc.closeStdin();
|
|
1746
|
+
return entry.proc.closeStdin();
|
|
1686
1747
|
}
|
|
1687
1748
|
/** Subscribe to stdout data from a process. Returns an unsubscribe function. */
|
|
1688
1749
|
onProcessStdout(pid, handler) {
|
|
@@ -1743,35 +1804,6 @@ export class AgentOs {
|
|
|
1743
1804
|
_vfs() {
|
|
1744
1805
|
return this.#kernel.vfs;
|
|
1745
1806
|
}
|
|
1746
|
-
async _copyPath(from, to) {
|
|
1747
|
-
const stat = await this._vfs().lstat(from);
|
|
1748
|
-
if (stat.isSymbolicLink) {
|
|
1749
|
-
const target = await this._vfs().readlink(from);
|
|
1750
|
-
await this._vfs().symlink(target, to);
|
|
1751
|
-
return;
|
|
1752
|
-
}
|
|
1753
|
-
if (stat.isDirectory) {
|
|
1754
|
-
await this._mkdirp(posixPath.dirname(to));
|
|
1755
|
-
if (!(await this.#kernel.exists(to))) {
|
|
1756
|
-
await this.#kernel.mkdir(to);
|
|
1757
|
-
}
|
|
1758
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1759
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1760
|
-
const entries = await this.#kernel.readdir(from);
|
|
1761
|
-
for (const entry of entries) {
|
|
1762
|
-
if (entry === "." || entry === "..")
|
|
1763
|
-
continue;
|
|
1764
|
-
const fromPath = from === "/" ? `/${entry}` : `${from}/${entry}`;
|
|
1765
|
-
const toPath = to === "/" ? `/${entry}` : `${to}/${entry}`;
|
|
1766
|
-
await this._copyPath(fromPath, toPath);
|
|
1767
|
-
}
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
const content = await this.#kernel.readFile(from);
|
|
1771
|
-
await this.writeFile(to, content);
|
|
1772
|
-
await this._vfs().chmod(to, stat.mode);
|
|
1773
|
-
await this._vfs().chown(to, stat.uid, stat.gid);
|
|
1774
|
-
}
|
|
1775
1807
|
async readFile(path) {
|
|
1776
1808
|
this._assertSafeAbsolutePath(path);
|
|
1777
1809
|
return this.#kernel.readFile(path);
|
|
@@ -1824,14 +1856,13 @@ export class AgentOs {
|
|
|
1824
1856
|
/** Recursively create directories (mkdir -p). */
|
|
1825
1857
|
async _mkdirp(path) {
|
|
1826
1858
|
this._assertWritableAbsolutePath(path);
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1859
|
+
// `kernel.mkdir` is already recursive (it defaults to recursive=true on both
|
|
1860
|
+
// the native sidecar and compat kernels) and creating an existing directory is
|
|
1861
|
+
// a no-op, so a single call is sufficient. Do NOT probe each ancestor with
|
|
1862
|
+
// `exists()` first: on the native sidecar every read-side op
|
|
1863
|
+
// (exists/stat/readFile) triggers a full shadow-tree walk, so a per-component
|
|
1864
|
+
// exists() loop makes `mkdir -p` cost O(components * tree).
|
|
1865
|
+
await this.#kernel.mkdir(path);
|
|
1835
1866
|
}
|
|
1836
1867
|
async mkdir(path, options) {
|
|
1837
1868
|
if (options?.recursive) {
|
|
@@ -1846,49 +1877,31 @@ export class AgentOs {
|
|
|
1846
1877
|
}
|
|
1847
1878
|
async readdirRecursive(path, options) {
|
|
1848
1879
|
this._assertSafeAbsolutePath(path);
|
|
1849
|
-
const maxDepth = options?.maxDepth;
|
|
1850
1880
|
const exclude = options?.exclude ? new Set(options.exclude) : undefined;
|
|
1881
|
+
const entries = await this.#kernel.readdirRecursive(path, {
|
|
1882
|
+
maxDepth: options?.maxDepth,
|
|
1883
|
+
});
|
|
1884
|
+
const excludedPrefixes = [];
|
|
1851
1885
|
const results = [];
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
const entries = await this.#kernel.readdir(dirPath);
|
|
1860
|
-
for (const name of entries) {
|
|
1861
|
-
if (name === "." || name === "..")
|
|
1862
|
-
continue;
|
|
1863
|
-
if (exclude?.has(name))
|
|
1864
|
-
continue;
|
|
1865
|
-
const fullPath = dirPath === "/" ? `/${name}` : `${dirPath}/${name}`;
|
|
1866
|
-
const s = await this.#kernel.stat(fullPath);
|
|
1867
|
-
if (s.isSymbolicLink) {
|
|
1868
|
-
results.push({
|
|
1869
|
-
path: fullPath,
|
|
1870
|
-
type: "symlink",
|
|
1871
|
-
size: s.size,
|
|
1872
|
-
});
|
|
1873
|
-
}
|
|
1874
|
-
else if (s.isDirectory) {
|
|
1875
|
-
results.push({
|
|
1876
|
-
path: fullPath,
|
|
1877
|
-
type: "directory",
|
|
1878
|
-
size: s.size,
|
|
1879
|
-
});
|
|
1880
|
-
if (maxDepth === undefined || depth < maxDepth) {
|
|
1881
|
-
queue.push([fullPath, depth + 1]);
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
else {
|
|
1885
|
-
results.push({
|
|
1886
|
-
path: fullPath,
|
|
1887
|
-
type: "file",
|
|
1888
|
-
size: s.size,
|
|
1889
|
-
});
|
|
1886
|
+
for (const entry of entries) {
|
|
1887
|
+
if (excludedPrefixes.some((prefix) => entry.path === prefix || entry.path.startsWith(`${prefix}/`))) {
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
if (exclude?.has(entry.name)) {
|
|
1891
|
+
if (entry.isDirectory && !entry.isSymbolicLink) {
|
|
1892
|
+
excludedPrefixes.push(entry.path);
|
|
1890
1893
|
}
|
|
1894
|
+
continue;
|
|
1891
1895
|
}
|
|
1896
|
+
results.push({
|
|
1897
|
+
path: entry.path,
|
|
1898
|
+
type: entry.isSymbolicLink
|
|
1899
|
+
? "symlink"
|
|
1900
|
+
: entry.isDirectory
|
|
1901
|
+
? "directory"
|
|
1902
|
+
: "file",
|
|
1903
|
+
size: entry.size,
|
|
1904
|
+
});
|
|
1892
1905
|
}
|
|
1893
1906
|
return results;
|
|
1894
1907
|
}
|
|
@@ -1916,30 +1929,15 @@ export class AgentOs {
|
|
|
1916
1929
|
this.#kernel.unmountFs(path);
|
|
1917
1930
|
}
|
|
1918
1931
|
async move(from, to) {
|
|
1919
|
-
this.
|
|
1920
|
-
this.
|
|
1921
|
-
|
|
1922
|
-
if (!sourceStat.isDirectory || sourceStat.isSymbolicLink) {
|
|
1923
|
-
return this.#kernel.rename(from, to);
|
|
1924
|
-
}
|
|
1925
|
-
await this._copyPath(from, to);
|
|
1926
|
-
await this.delete(from, { recursive: true });
|
|
1932
|
+
this._assertWritableAbsolutePath(from);
|
|
1933
|
+
this._assertWritableAbsolutePath(to);
|
|
1934
|
+
await this.#kernel.movePath(from, to);
|
|
1927
1935
|
}
|
|
1928
1936
|
async delete(path, options) {
|
|
1929
|
-
this.
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
const entries = await this.#kernel.readdir(path);
|
|
1934
|
-
for (const entry of entries) {
|
|
1935
|
-
if (entry === "." || entry === "..")
|
|
1936
|
-
continue;
|
|
1937
|
-
await this.delete(`${path}/${entry}`, { recursive: true });
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
return this.#kernel.removeDir(path);
|
|
1941
|
-
}
|
|
1942
|
-
return this.#kernel.removeFile(path);
|
|
1937
|
+
this._assertWritableAbsolutePath(path);
|
|
1938
|
+
await this.#kernel.removePath(path, {
|
|
1939
|
+
recursive: options?.recursive ?? false,
|
|
1940
|
+
});
|
|
1943
1941
|
}
|
|
1944
1942
|
async fetch(port, request) {
|
|
1945
1943
|
const url = new URL(request.url);
|
|
@@ -1974,15 +1972,24 @@ export class AgentOs {
|
|
|
1974
1972
|
const entry = {
|
|
1975
1973
|
handle,
|
|
1976
1974
|
dataHandlers,
|
|
1977
|
-
exitPromise: Promise.resolve(),
|
|
1975
|
+
exitPromise: Promise.resolve(0),
|
|
1978
1976
|
};
|
|
1979
|
-
const exitPromise = handle.wait()
|
|
1980
|
-
|
|
1977
|
+
const exitPromise = handle.wait();
|
|
1978
|
+
const finalize = (exitCode) => {
|
|
1981
1979
|
this._pendingShellExitPromises.delete(entry.exitPromise);
|
|
1982
1980
|
if (this._shells.get(shellId) === entry) {
|
|
1983
1981
|
this._shells.delete(shellId);
|
|
1984
|
-
this._closedShellIds.add(shellId);
|
|
1985
1982
|
}
|
|
1983
|
+
// Record the exit code even when closeShell already dropped the
|
|
1984
|
+
// entry, so a waitShell issued after exit still resolves with it.
|
|
1985
|
+
this._closedShellIds.add(shellId, exitCode);
|
|
1986
|
+
};
|
|
1987
|
+
entry.exitPromise = exitPromise.then((exitCode) => {
|
|
1988
|
+
finalize(exitCode);
|
|
1989
|
+
return exitCode;
|
|
1990
|
+
}, (error) => {
|
|
1991
|
+
finalize();
|
|
1992
|
+
throw error;
|
|
1986
1993
|
});
|
|
1987
1994
|
this._pendingShellExitPromises.add(entry.exitPromise);
|
|
1988
1995
|
this._shells.set(shellId, entry);
|
|
@@ -1996,7 +2003,7 @@ export class AgentOs {
|
|
|
1996
2003
|
const entry = this._shells.get(shellId);
|
|
1997
2004
|
if (!entry)
|
|
1998
2005
|
throw new Error(`Shell not found: ${shellId}`);
|
|
1999
|
-
entry.handle.write(data);
|
|
2006
|
+
return entry.handle.write(data);
|
|
2000
2007
|
}
|
|
2001
2008
|
/** Subscribe to data output from a shell. Returns an unsubscribe function. */
|
|
2002
2009
|
onShellData(shellId, handler) {
|
|
@@ -2015,6 +2022,21 @@ export class AgentOs {
|
|
|
2015
2022
|
throw new Error(`Shell not found: ${shellId}`);
|
|
2016
2023
|
entry.handle.resize(cols, rows);
|
|
2017
2024
|
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Wait for a shell to exit and return its process exit code. Resolves
|
|
2027
|
+
* immediately for a shell that has already exited (within the closed-shell
|
|
2028
|
+
* retention window).
|
|
2029
|
+
*/
|
|
2030
|
+
waitShell(shellId) {
|
|
2031
|
+
const entry = this._shells.get(shellId);
|
|
2032
|
+
if (!entry) {
|
|
2033
|
+
const exitCode = this._closedShellIds.get(shellId);
|
|
2034
|
+
if (exitCode !== undefined)
|
|
2035
|
+
return Promise.resolve(exitCode);
|
|
2036
|
+
throw new Error(`Shell not found: ${shellId}`);
|
|
2037
|
+
}
|
|
2038
|
+
return entry.exitPromise;
|
|
2039
|
+
}
|
|
2018
2040
|
/** Kill a shell process and remove it from tracking. */
|
|
2019
2041
|
closeShell(shellId) {
|
|
2020
2042
|
const entry = this._shells.get(shellId);
|
|
@@ -2129,53 +2151,56 @@ export class AgentOs {
|
|
|
2129
2151
|
}
|
|
2130
2152
|
return session;
|
|
2131
2153
|
}
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
.
|
|
2154
|
+
/**
|
|
2155
|
+
* Dynamically link a software package into the RUNNING VM. The package's
|
|
2156
|
+
* `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
|
|
2157
|
+
* pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
|
|
2158
|
+
* writing into its staging dir is reflected live with no reboot. An `agent`
|
|
2159
|
+
* block registers the package for `createSession(name)`. Persists for the VM's
|
|
2160
|
+
* lifetime (and across a snapshot iff the volume persists).
|
|
2161
|
+
*/
|
|
2162
|
+
async linkSoftware(descriptor) {
|
|
2163
|
+
const ref = normalizePackageRef(descriptor);
|
|
2164
|
+
if (!ref) {
|
|
2165
|
+
throw new Error("Invalid agentOS package reference");
|
|
2166
|
+
}
|
|
2167
|
+
// Forward to the sidecar, which owns the `/opt/agentos` projection and
|
|
2168
|
+
// appends the package to its live host-backed staging dir; the commands
|
|
2169
|
+
// appear under `/opt/agentos/bin` immediately. The sidecar rejects a
|
|
2170
|
+
// duplicate command, surfaced here as a thrown error.
|
|
2171
|
+
const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, { path: ref.path });
|
|
2172
|
+
if (this.#kernel instanceof NativeSidecarKernelProxy) {
|
|
2173
|
+
this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
|
|
2174
|
+
command.name,
|
|
2175
|
+
command.guestPath,
|
|
2176
|
+
])));
|
|
2177
|
+
}
|
|
2178
|
+
// The client parses no manifests: an `agent` block in the linked package is
|
|
2179
|
+
// picked up by the sidecar (it owns the projected `/opt/agentos` and answers
|
|
2180
|
+
// createSession/listAgents from it). Nothing to record client-side.
|
|
2181
|
+
}
|
|
2182
|
+
async providedCommands() {
|
|
2183
|
+
return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Returns all registered agents with their installation status. Thin forwarder:
|
|
2187
|
+
* sends `AcpListAgentsRequest` and maps the response. The sidecar enumerates the
|
|
2188
|
+
* projected `/opt/agentos` packages (the client parses no manifests). Every such
|
|
2189
|
+
* agent is a package materialized into the VM, so `installed` is always `true`.
|
|
2190
|
+
*/
|
|
2191
|
+
async listAgents() {
|
|
2192
|
+
const response = await this._sendAcpRequest({
|
|
2193
|
+
tag: "AcpListAgentsRequest",
|
|
2194
|
+
val: { reserved: false },
|
|
2195
|
+
});
|
|
2196
|
+
if (response.tag !== "AcpListAgentsResponse") {
|
|
2197
|
+
throw new Error(`unexpected list_agents response: ${response.tag}`);
|
|
2198
|
+
}
|
|
2199
|
+
return response.val.agents.map((agent) => ({
|
|
2200
|
+
id: agent.id,
|
|
2201
|
+
installed: agent.installed,
|
|
2202
|
+
adapterEntrypoint: agent.adapterEntrypoint,
|
|
2203
|
+
}));
|
|
2179
2204
|
}
|
|
2180
2205
|
_syncSessionState(session, state) {
|
|
2181
2206
|
session.processId = state.processId;
|
|
@@ -2254,6 +2279,60 @@ export class AgentOs {
|
|
|
2254
2279
|
session.eventHandlers.delete(subscriber);
|
|
2255
2280
|
};
|
|
2256
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
|
+
}
|
|
2257
2336
|
_recordAgentStderr(event) {
|
|
2258
2337
|
const session = (event.sessionId ? this._sessions.get(event.sessionId) : undefined) ??
|
|
2259
2338
|
[...this._sessions.values()].find((candidate) => candidate.processId === event.processId);
|
|
@@ -2278,6 +2357,28 @@ export class AgentOs {
|
|
|
2278
2357
|
// Ignore subscriber callback failures and keep event delivery moving.
|
|
2279
2358
|
}
|
|
2280
2359
|
}
|
|
2360
|
+
_recordAgentExit(event) {
|
|
2361
|
+
const session = this._sessions.get(event.sessionId);
|
|
2362
|
+
const handler = this._agentExitHandler;
|
|
2363
|
+
if (!handler) {
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
handler({
|
|
2368
|
+
sessionId: event.sessionId,
|
|
2369
|
+
agentType: event.agentType || session?.agentType || "",
|
|
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.
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2281
2382
|
_applySyntheticConfigOverrides(session) {
|
|
2282
2383
|
if (session.configOverrides.size === 0) {
|
|
2283
2384
|
return;
|
|
@@ -2300,6 +2401,10 @@ export class AgentOs {
|
|
|
2300
2401
|
if (event.payload.type !== "structured") {
|
|
2301
2402
|
return;
|
|
2302
2403
|
}
|
|
2404
|
+
if (event.payload.name === "limit_warning") {
|
|
2405
|
+
this._handleLimitWarning(event.payload.detail);
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2303
2408
|
if (event.payload.name !== "acp.session_event") {
|
|
2304
2409
|
return;
|
|
2305
2410
|
}
|
|
@@ -2319,6 +2424,27 @@ export class AgentOs {
|
|
|
2319
2424
|
// Ignore malformed event payloads from the sidecar.
|
|
2320
2425
|
}
|
|
2321
2426
|
}
|
|
2427
|
+
_handleLimitWarning(detail) {
|
|
2428
|
+
if (!this._limitWarningHandler) {
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
const toNumber = (value) => {
|
|
2432
|
+
const parsed = Number(value);
|
|
2433
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
2434
|
+
};
|
|
2435
|
+
try {
|
|
2436
|
+
this._limitWarningHandler({
|
|
2437
|
+
limit: detail.limit ?? "",
|
|
2438
|
+
category: detail.category ?? "",
|
|
2439
|
+
observed: toNumber(detail.observed),
|
|
2440
|
+
capacity: toNumber(detail.capacity),
|
|
2441
|
+
fillPercent: toNumber(detail.fillPercent),
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
catch {
|
|
2445
|
+
// A throwing handler must never break the sidecar event loop.
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2322
2448
|
_handleAcpExtEvent(envelope) {
|
|
2323
2449
|
if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
|
|
2324
2450
|
return;
|
|
@@ -2338,6 +2464,10 @@ export class AgentOs {
|
|
|
2338
2464
|
this._recordAgentStderr(event.val);
|
|
2339
2465
|
return;
|
|
2340
2466
|
}
|
|
2467
|
+
case "AcpAgentExitedEvent": {
|
|
2468
|
+
this._recordAgentExit(event.val);
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2341
2471
|
}
|
|
2342
2472
|
}
|
|
2343
2473
|
catch {
|
|
@@ -2553,37 +2683,30 @@ export class AgentOs {
|
|
|
2553
2683
|
this._syncSessionState(session, state);
|
|
2554
2684
|
}
|
|
2555
2685
|
async createSession(agentType, options) {
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
// System-prompt assembly
|
|
2561
|
-
// the sidecar
|
|
2562
|
-
// skipOsInstructions plus the
|
|
2563
|
-
const
|
|
2564
|
-
let launchEnv = { ...config.defaultEnv, ...options?.env };
|
|
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 };
|
|
2565
2694
|
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2566
|
-
const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
|
|
2567
|
-
if ((agentType === "pi" || agentType === "pi-cli") &&
|
|
2568
|
-
!launchEnv.PI_ACP_PI_COMMAND) {
|
|
2569
|
-
launchEnv = {
|
|
2570
|
-
...launchEnv,
|
|
2571
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
2572
|
-
};
|
|
2573
|
-
}
|
|
2574
2695
|
const response = await this._sendAcpRequest({
|
|
2575
2696
|
tag: "AcpCreateSessionRequest",
|
|
2576
2697
|
val: {
|
|
2577
2698
|
agentType: String(agentType),
|
|
2578
2699
|
runtime: AcpRuntimeKind.JavaScript,
|
|
2579
|
-
|
|
2580
|
-
args: launchArgs,
|
|
2700
|
+
args: [],
|
|
2581
2701
|
env: new Map(Object.entries(launchEnv)),
|
|
2582
2702
|
cwd: sessionCwd,
|
|
2583
2703
|
mcpServers: JSON.stringify(options?.mcpServers ?? []),
|
|
2584
2704
|
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2585
2705
|
clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
|
|
2586
|
-
additionalInstructions: combineInstructions(options?.additionalInstructions
|
|
2706
|
+
additionalInstructions: combineInstructions([this._additionalInstructions, options?.additionalInstructions]
|
|
2707
|
+
.map((part) => part?.trim())
|
|
2708
|
+
.filter((part) => Boolean(part))
|
|
2709
|
+
.join("\n\n") || undefined, this._toolReference),
|
|
2587
2710
|
skipOsInstructions: options?.skipOsInstructions ?? false,
|
|
2588
2711
|
},
|
|
2589
2712
|
});
|
|
@@ -2634,27 +2757,11 @@ export class AgentOs {
|
|
|
2634
2757
|
* there is no surviving store and the fallback tier always runs.
|
|
2635
2758
|
*/
|
|
2636
2759
|
async resumeSession(sessionId, agentType, options) {
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
}
|
|
2641
|
-
const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
|
|
2642
|
-
let launchEnv = { ...config.defaultEnv, ...options?.env };
|
|
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.
|
|
2643
2763
|
const sessionCwd = options?.cwd ?? "/workspace";
|
|
2644
|
-
|
|
2645
|
-
!launchEnv.PI_ACP_PI_COMMAND) {
|
|
2646
|
-
launchEnv = {
|
|
2647
|
-
...launchEnv,
|
|
2648
|
-
PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
|
|
2649
|
-
};
|
|
2650
|
-
}
|
|
2651
|
-
// The resume wire request has no dedicated `adapterEntrypoint` field; carry
|
|
2652
|
-
// the resolved entrypoint through env under the sidecar's reserved key. The
|
|
2653
|
-
// sidecar reads it and strips it before launching the adapter.
|
|
2654
|
-
launchEnv = {
|
|
2655
|
-
...launchEnv,
|
|
2656
|
-
[RESUME_ADAPTER_ENTRYPOINT_ENV]: adapterEntrypoint,
|
|
2657
|
-
};
|
|
2764
|
+
const launchEnv = { ...options?.env };
|
|
2658
2765
|
const response = await this._sendAcpRequest({
|
|
2659
2766
|
tag: "AcpResumeSessionRequest",
|
|
2660
2767
|
val: {
|
|
@@ -2670,6 +2777,10 @@ export class AgentOs {
|
|
|
2670
2777
|
}
|
|
2671
2778
|
const { sessionId: liveSessionId, mode } = response.val;
|
|
2672
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
|
+
}
|
|
2673
2784
|
const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
|
|
2674
2785
|
this._closedSessionIds.delete(liveSessionId);
|
|
2675
2786
|
this._sessions.set(liveSessionId, session);
|
|
@@ -2682,65 +2793,6 @@ export class AgentOs {
|
|
|
2682
2793
|
}
|
|
2683
2794
|
return { sessionId: liveSessionId, mode };
|
|
2684
2795
|
}
|
|
2685
|
-
/**
|
|
2686
|
-
* Resolve the VM bin entry point of an ACP adapter package.
|
|
2687
|
-
* Reads from the host filesystem since kernel.readFile() resolves through
|
|
2688
|
-
* mounts; adapter package.json is read directly off the host dir backing
|
|
2689
|
-
* the relevant mount.
|
|
2690
|
-
*/
|
|
2691
|
-
_resolveAdapterBin(adapterPackage) {
|
|
2692
|
-
return this._resolvePackageBin(adapterPackage);
|
|
2693
|
-
}
|
|
2694
|
-
/**
|
|
2695
|
-
* Parent of the host directory backing the `/root/node_modules` mount, if
|
|
2696
|
-
* the caller supplied one (e.g. via `nodeModulesMount(...)`). Used as the
|
|
2697
|
-
* `resolvePackageDir` start dir for adapter/agent package resolution when no
|
|
2698
|
-
* software root matches. `nodeModulesMount` mounts `<dir>/node_modules`, so
|
|
2699
|
-
* the start dir is `<dir>` (one level above the node_modules tree).
|
|
2700
|
-
*/
|
|
2701
|
-
_nodeModulesHostRoot() {
|
|
2702
|
-
for (const mount of this._hostMounts) {
|
|
2703
|
-
if (mount.vmPath === "/root/node_modules") {
|
|
2704
|
-
return dirname(mount.hostPath);
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
return null;
|
|
2708
|
-
}
|
|
2709
|
-
_resolvePackageBin(packageName, binName) {
|
|
2710
|
-
const vmPrefix = `/root/node_modules/${packageName}`;
|
|
2711
|
-
let hostPkgJsonPath = null;
|
|
2712
|
-
for (const root of this._softwareRoots) {
|
|
2713
|
-
if (root.vmPath === vmPrefix) {
|
|
2714
|
-
hostPkgJsonPath = join(root.hostPath, "package.json");
|
|
2715
|
-
break;
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
// Fall back to the host dir behind the `/root/node_modules` mount.
|
|
2719
|
-
if (!hostPkgJsonPath) {
|
|
2720
|
-
const nodeModulesRoot = this._nodeModulesHostRoot();
|
|
2721
|
-
if (!nodeModulesRoot) {
|
|
2722
|
-
throw new Error(`Cannot resolve package "${packageName}": no software root provides it and ` +
|
|
2723
|
-
`no /root/node_modules mount was supplied. Pass ` +
|
|
2724
|
-
`mounts: [nodeModulesMount("<host>/node_modules")] to AgentOs.create().`);
|
|
2725
|
-
}
|
|
2726
|
-
hostPkgJsonPath = join(resolvePackageDir(nodeModulesRoot, packageName), "package.json");
|
|
2727
|
-
}
|
|
2728
|
-
const pkg = JSON.parse(readFileSync(hostPkgJsonPath, "utf-8"));
|
|
2729
|
-
let binEntry;
|
|
2730
|
-
if (typeof pkg.bin === "string") {
|
|
2731
|
-
binEntry = pkg.bin;
|
|
2732
|
-
}
|
|
2733
|
-
else if (typeof pkg.bin === "object" && pkg.bin !== null) {
|
|
2734
|
-
binEntry =
|
|
2735
|
-
(binName ? pkg.bin[binName] : undefined) ??
|
|
2736
|
-
pkg.bin[packageName] ??
|
|
2737
|
-
Object.values(pkg.bin)[0];
|
|
2738
|
-
}
|
|
2739
|
-
if (!binEntry) {
|
|
2740
|
-
throw new Error(`No bin entry found in ${packageName}/package.json`);
|
|
2741
|
-
}
|
|
2742
|
-
return `${vmPrefix}/${binEntry}`;
|
|
2743
|
-
}
|
|
2744
2796
|
_installSidecarRequestHandler() {
|
|
2745
2797
|
const context = {
|
|
2746
2798
|
toolKits: this._toolKits,
|
|
@@ -2753,10 +2805,8 @@ export class AgentOs {
|
|
|
2753
2805
|
case "host_callback":
|
|
2754
2806
|
return handleHostCallback(request, context);
|
|
2755
2807
|
case "js_bridge_call":
|
|
2756
|
-
return
|
|
2757
|
-
|
|
2758
|
-
call_id: request.payload.call_id,
|
|
2759
|
-
error: `unsupported sidecar request type: ${request.payload.type}`,
|
|
2808
|
+
return handleJsBridgeCall(request.payload, {
|
|
2809
|
+
filesystem: this.#kernel.vfs,
|
|
2760
2810
|
});
|
|
2761
2811
|
case "ext":
|
|
2762
2812
|
return this._handleAcpExtSidecarRequest(request.payload.envelope);
|
|
@@ -2943,6 +2993,10 @@ export class AgentOs {
|
|
|
2943
2993
|
_acpMethod: request.method,
|
|
2944
2994
|
};
|
|
2945
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);
|
|
2946
3000
|
return this._buildAcpPermissionResult("reject", permissionParams);
|
|
2947
3001
|
}
|
|
2948
3002
|
const reply = await new Promise((resolve, reject) => {
|
|
@@ -3225,6 +3279,10 @@ export class AgentOs {
|
|
|
3225
3279
|
return "reject";
|
|
3226
3280
|
}
|
|
3227
3281
|
if (session.permissionHandlers.size === 0) {
|
|
3282
|
+
// Default-closed: deny when no host hook is listening, and warn once
|
|
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);
|
|
3228
3286
|
return "reject";
|
|
3229
3287
|
}
|
|
3230
3288
|
try {
|
|
@@ -3254,14 +3312,6 @@ export class AgentOs {
|
|
|
3254
3312
|
return "reject";
|
|
3255
3313
|
}
|
|
3256
3314
|
}
|
|
3257
|
-
/**
|
|
3258
|
-
* Resolve an agent config by ID. Package-provided configs take
|
|
3259
|
-
* precedence over the hardcoded AGENT_CONFIGS.
|
|
3260
|
-
*/
|
|
3261
|
-
_resolveAgentConfig(agentType) {
|
|
3262
|
-
return (this._softwareAgentConfigs.get(agentType) ??
|
|
3263
|
-
AGENT_CONFIGS[agentType]);
|
|
3264
|
-
}
|
|
3265
3315
|
/**
|
|
3266
3316
|
* Gracefully destroy a session: cancel any pending work, close the client,
|
|
3267
3317
|
* and remove from tracking. Unlike close() which is abrupt, this attempts
|
|
@@ -3445,6 +3495,7 @@ export class AgentOs {
|
|
|
3445
3495
|
terminalExitPromises.push(terminal.waitPromise.then(() => undefined, () => undefined));
|
|
3446
3496
|
}
|
|
3447
3497
|
this._acpTerminals.clear();
|
|
3498
|
+
this._processes.clear();
|
|
3448
3499
|
await waitForTrackedExitPromises([...shellExitPromises, ...terminalExitPromises], SHELL_DISPOSE_TIMEOUT_MS);
|
|
3449
3500
|
this._disposeSidecarEventListener();
|
|
3450
3501
|
const sidecarLease = this._sidecarLease;
|
|
@@ -3474,6 +3525,164 @@ function resolveAgentOsSidecar(config) {
|
|
|
3474
3525
|
}
|
|
3475
3526
|
const sidecarStates = new WeakMap();
|
|
3476
3527
|
const sharedSidecars = new Map();
|
|
3528
|
+
let sidecarProcessExitHookInstalled = false;
|
|
3529
|
+
/**
|
|
3530
|
+
* Install a one-time, synchronous `process.on("exit")` hook that SIGKILLs any
|
|
3531
|
+
* pooled shared sidecar child. Once a one-shot host process is allowed to exit
|
|
3532
|
+
* (its sidecar handles are unref'd at 0 leases), this reaps the sidecar
|
|
3533
|
+
* immediately instead of waiting for its stdin-EOF grace window — no orphan, no
|
|
3534
|
+
* delay. We deliberately do NOT install SIGINT/SIGTERM handlers: a library
|
|
3535
|
+
* should not hijack the host's signal handling. SIGINT still reaches the sidecar
|
|
3536
|
+
* via the process group, and SIGTERM-driven exit still closes its stdin.
|
|
3537
|
+
*/
|
|
3538
|
+
function ensureSidecarProcessExitCleanup() {
|
|
3539
|
+
if (sidecarProcessExitHookInstalled)
|
|
3540
|
+
return;
|
|
3541
|
+
sidecarProcessExitHookInstalled = true;
|
|
3542
|
+
process.on("exit", () => {
|
|
3543
|
+
for (const sidecar of sharedSidecars.values()) {
|
|
3544
|
+
try {
|
|
3545
|
+
sidecarStates.get(sidecar)?.sharedChild?.kill?.("SIGKILL");
|
|
3546
|
+
}
|
|
3547
|
+
catch {
|
|
3548
|
+
// best-effort reap; the process is exiting regardless
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
function sidecarChildHandle(client) {
|
|
3554
|
+
// SidecarProcess -> StdioSidecarProtocolClient.child (the spawned ChildProcess).
|
|
3555
|
+
const protocolClient = client?.protocolClient;
|
|
3556
|
+
return protocolClient?.child ?? undefined;
|
|
3557
|
+
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Apply the current hold state to the shared sidecar's child + stdio: ref them
|
|
3560
|
+
* while ≥1 hold is live so in-flight VM work keeps the host process alive; unref
|
|
3561
|
+
* them at 0 so a one-shot script exits on its own after `dispose()`. The sidecar
|
|
3562
|
+
* process itself stays running (reusable) and self-exits on stdin EOF when the
|
|
3563
|
+
* host finally goes away. Best-effort: never let ref/unref break VM lifecycle.
|
|
3564
|
+
*/
|
|
3565
|
+
function applySharedSidecarHold(state) {
|
|
3566
|
+
const child = state.sharedChild;
|
|
3567
|
+
if (!child)
|
|
3568
|
+
return;
|
|
3569
|
+
const hold = (state.eventLoopHolds ?? 0) > 0;
|
|
3570
|
+
for (const handle of [child, child.stdin, child.stdout, child.stderr]) {
|
|
3571
|
+
if (!handle)
|
|
3572
|
+
continue;
|
|
3573
|
+
try {
|
|
3574
|
+
if (hold)
|
|
3575
|
+
handle.ref?.();
|
|
3576
|
+
else
|
|
3577
|
+
handle.unref?.();
|
|
3578
|
+
}
|
|
3579
|
+
catch {
|
|
3580
|
+
// ref/unref is an optimization, not correctness-critical
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
/**
|
|
3585
|
+
* Take a hold for the entire create→use→dispose lifetime of one VM lease. Taken
|
|
3586
|
+
* BEFORE VM creation starts (not when the lease lands in `activeLeases`) so a VM
|
|
3587
|
+
* that is still mid-creation keeps the sidecar ref'd and a concurrent dispose
|
|
3588
|
+
* cannot unref it out from under the in-flight create.
|
|
3589
|
+
*/
|
|
3590
|
+
function acquireSharedSidecarHold(state) {
|
|
3591
|
+
state.eventLoopHolds = (state.eventLoopHolds ?? 0) + 1;
|
|
3592
|
+
if (state.eventLoopHolds === 1)
|
|
3593
|
+
applySharedSidecarHold(state);
|
|
3594
|
+
}
|
|
3595
|
+
/** Release a hold taken by {@link acquireSharedSidecarHold}; unref at 0. */
|
|
3596
|
+
function releaseSharedSidecarHold(state) {
|
|
3597
|
+
const current = state.eventLoopHolds ?? 0;
|
|
3598
|
+
if (current <= 0) {
|
|
3599
|
+
// The `holdReleased` guard makes each lease release exactly once, so this
|
|
3600
|
+
// should be unreachable. Warn rather than silently floor, per the repo's
|
|
3601
|
+
// no-silent-masking rule, so an accounting bug surfaces instead of hiding.
|
|
3602
|
+
state.eventLoopHolds = 0;
|
|
3603
|
+
console.warn("[agentos] shared sidecar event-loop hold released more than acquired");
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
state.eventLoopHolds = current - 1;
|
|
3607
|
+
if (state.eventLoopHolds === 0)
|
|
3608
|
+
applySharedSidecarHold(state);
|
|
3609
|
+
}
|
|
3610
|
+
/**
|
|
3611
|
+
* Spawn-once accessor for a sidecar handle's shared native process. Concurrent
|
|
3612
|
+
* callers await the same promise, so one `AgentOsSidecar` maps to exactly one
|
|
3613
|
+
* `agent-os-sidecar` OS process for its whole lifetime.
|
|
3614
|
+
*/
|
|
3615
|
+
function ensureSharedSidecarNativeProcess(sidecar) {
|
|
3616
|
+
const state = getSidecarState(sidecar);
|
|
3617
|
+
if (!state.nativeProcess) {
|
|
3618
|
+
ensureSidecarProcessExitCleanup();
|
|
3619
|
+
state.nativeProcess = (async () => {
|
|
3620
|
+
const client = SidecarProcess.spawn({
|
|
3621
|
+
cwd: REPO_ROOT,
|
|
3622
|
+
command: ensureNativeSidecarBinary(),
|
|
3623
|
+
args: [],
|
|
3624
|
+
frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
|
|
3625
|
+
});
|
|
3626
|
+
// Track the child immediately — BEFORE the handshake await — so a
|
|
3627
|
+
// failed `authenticateAndOpenSession()` can still reap it (otherwise
|
|
3628
|
+
// the spawned child is untracked, unreapable, and pins the loop).
|
|
3629
|
+
state.sharedChild = sidecarChildHandle(client);
|
|
3630
|
+
if (!state.sharedChild) {
|
|
3631
|
+
// We reached into @rivet-dev/agentos-runtime-core internals to get the child for
|
|
3632
|
+
// idle-unref. If that shape ever changes this returns undefined and
|
|
3633
|
+
// the optimization silently stops working (one-shot scripts would
|
|
3634
|
+
// hang again). Make it loud rather than a silent regression.
|
|
3635
|
+
console.warn("[agentos] could not resolve the shared sidecar child handle; " +
|
|
3636
|
+
"standalone scripts may not exit cleanly after dispose(). " +
|
|
3637
|
+
"This usually means @rivet-dev/agentos-runtime-core internals changed.");
|
|
3638
|
+
}
|
|
3639
|
+
// Apply the current hold state to the just-spawned child.
|
|
3640
|
+
applySharedSidecarHold(state);
|
|
3641
|
+
try {
|
|
3642
|
+
const session = await client.authenticateAndOpenSession();
|
|
3643
|
+
return { client, session };
|
|
3644
|
+
}
|
|
3645
|
+
catch (error) {
|
|
3646
|
+
// Spawn/handshake failed: reap the child, drop the cached handle,
|
|
3647
|
+
// and CLEAR the rejected promise so the next create() retries
|
|
3648
|
+
// instead of permanently wedging on a rejected `nativeProcess`.
|
|
3649
|
+
try {
|
|
3650
|
+
state.sharedChild?.kill?.("SIGKILL");
|
|
3651
|
+
}
|
|
3652
|
+
catch {
|
|
3653
|
+
// already gone
|
|
3654
|
+
}
|
|
3655
|
+
state.sharedChild = undefined;
|
|
3656
|
+
state.nativeProcess = undefined;
|
|
3657
|
+
throw error;
|
|
3658
|
+
}
|
|
3659
|
+
})();
|
|
3660
|
+
}
|
|
3661
|
+
return state.nativeProcess;
|
|
3662
|
+
}
|
|
3663
|
+
/** Dispose a sidecar handle's shared native process, if one was spawned. */
|
|
3664
|
+
async function disposeSharedSidecarNativeProcess(state) {
|
|
3665
|
+
const pending = state.nativeProcess;
|
|
3666
|
+
if (!pending) {
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
state.nativeProcess = undefined;
|
|
3670
|
+
// The cached child is now dead; drop it (symmetric with the assignment in
|
|
3671
|
+
// ensureSharedSidecarNativeProcess). We deliberately do NOT zero
|
|
3672
|
+
// `eventLoopHolds` here: this runs only from `AgentOsSidecar.dispose()`, which
|
|
3673
|
+
// has already set the handle to `disposing` (so no new lease can acquire) and
|
|
3674
|
+
// drained `activeLeases`; the disposed handle's state is then abandoned. Force-
|
|
3675
|
+
// zeroing a shared counter could clobber a hold on a freshly re-acquired
|
|
3676
|
+
// process generation, so it is left to the balanced acquire/release pairs.
|
|
3677
|
+
state.sharedChild = undefined;
|
|
3678
|
+
try {
|
|
3679
|
+
const { client } = await pending;
|
|
3680
|
+
await client.dispose();
|
|
3681
|
+
}
|
|
3682
|
+
catch {
|
|
3683
|
+
// Process may have already exited; nothing to reclaim.
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3477
3686
|
export class AgentOsSidecar {
|
|
3478
3687
|
constructor(sidecarId, placement, sharedPool) {
|
|
3479
3688
|
sidecarStates.set(this, {
|
|
@@ -3508,6 +3717,8 @@ export class AgentOsSidecar {
|
|
|
3508
3717
|
}
|
|
3509
3718
|
state.activeLeases.clear();
|
|
3510
3719
|
state.description.activeVmCount = 0;
|
|
3720
|
+
// Tear down the shared native process after all leased VMs are gone.
|
|
3721
|
+
await disposeSharedSidecarNativeProcess(state);
|
|
3511
3722
|
state.description.state = "disposed";
|
|
3512
3723
|
if (state.sharedPool && sharedSidecars.get(state.sharedPool) === this) {
|
|
3513
3724
|
sharedSidecars.delete(state.sharedPool);
|
|
@@ -3569,6 +3780,18 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3569
3780
|
return transport;
|
|
3570
3781
|
},
|
|
3571
3782
|
});
|
|
3783
|
+
// Hold the shared sidecar's event-loop ref for this lease's WHOLE lifetime —
|
|
3784
|
+
// taken now, before VM creation, so a concurrent dispose cannot unref the
|
|
3785
|
+
// sidecar while this create is still in flight. Released exactly once on
|
|
3786
|
+
// dispose or on a failed create.
|
|
3787
|
+
acquireSharedSidecarHold(state);
|
|
3788
|
+
let holdReleased = false;
|
|
3789
|
+
const releaseHold = () => {
|
|
3790
|
+
if (holdReleased)
|
|
3791
|
+
return;
|
|
3792
|
+
holdReleased = true;
|
|
3793
|
+
releaseSharedSidecarHold(state);
|
|
3794
|
+
};
|
|
3572
3795
|
let disposed = false;
|
|
3573
3796
|
let leaseRecord;
|
|
3574
3797
|
try {
|
|
@@ -3593,6 +3816,10 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3593
3816
|
state.activeLeases.delete(leaseRecord);
|
|
3594
3817
|
state.description.activeVmCount = state.activeLeases.size;
|
|
3595
3818
|
await client.dispose();
|
|
3819
|
+
// Release this lease's hold; the shared sidecar is unref'd only
|
|
3820
|
+
// once the last hold (across all in-flight + active leases) drops,
|
|
3821
|
+
// so a one-shot host process can then exit on its own.
|
|
3822
|
+
releaseHold();
|
|
3596
3823
|
},
|
|
3597
3824
|
};
|
|
3598
3825
|
leaseRecord = {
|
|
@@ -3604,6 +3831,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
|
|
|
3604
3831
|
}
|
|
3605
3832
|
catch (error) {
|
|
3606
3833
|
await client.dispose().catch(() => { });
|
|
3834
|
+
releaseHold();
|
|
3607
3835
|
throw error;
|
|
3608
3836
|
}
|
|
3609
3837
|
}
|