@electric-ax/agents 0.4.10 → 0.4.12
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/dist/entrypoint.js +156 -54
- package/dist/index.cjs +155 -53
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +163 -61
- package/package.json +2 -2
package/dist/entrypoint.js
CHANGED
|
@@ -4,7 +4,8 @@ import fs from "node:fs";
|
|
|
4
4
|
import pino from "pino";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
7
|
-
import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool
|
|
7
|
+
import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
|
|
8
|
+
import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import { createHash } from "node:crypto";
|
|
10
11
|
import fs$1 from "node:fs/promises";
|
|
@@ -16,28 +17,47 @@ import { getModels } from "@mariozechner/pi-ai";
|
|
|
16
17
|
import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
|
|
17
18
|
|
|
18
19
|
//#region src/log.ts
|
|
19
|
-
const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
|
|
20
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
21
|
-
const LOG_FILE = path.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
|
|
22
20
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
23
21
|
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
22
|
+
const USE_FILE_LOGS = process.env.ELECTRIC_AGENTS_LOG_FILE !== `false`;
|
|
24
23
|
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
let _logger;
|
|
25
|
+
function getLogger() {
|
|
26
|
+
if (_logger) return _logger;
|
|
27
|
+
const streams = [];
|
|
28
|
+
try {
|
|
29
|
+
if (USE_FILE_LOGS) {
|
|
30
|
+
const logDir = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
|
|
31
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
32
|
+
const logFile = path.join(logDir, `builtin-agents-${Date.now()}.jsonl`);
|
|
33
|
+
streams.push({ stream: pino.destination({
|
|
34
|
+
dest: logFile,
|
|
35
|
+
sync: IS_ELECTRON_MAIN
|
|
36
|
+
}) });
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
process.stderr.write(`[agents] Failed to initialize file logging: ${err instanceof Error ? err.message : err}\n`);
|
|
35
40
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
try {
|
|
42
|
+
if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
|
|
43
|
+
target: `pino-pretty`,
|
|
44
|
+
options: {
|
|
45
|
+
colorize: true,
|
|
46
|
+
ignore: `pid,hostname,name`,
|
|
47
|
+
translateTime: `SYS:HH:MM:ss`
|
|
48
|
+
}
|
|
49
|
+
}) });
|
|
50
|
+
} catch {}
|
|
51
|
+
_logger = streams.length > 0 ? pino({
|
|
52
|
+
base: void 0,
|
|
53
|
+
level: LOG_LEVEL
|
|
54
|
+
}, pino.multistream(streams)) : pino({
|
|
55
|
+
base: void 0,
|
|
56
|
+
enabled: false,
|
|
57
|
+
level: LOG_LEVEL
|
|
58
|
+
});
|
|
59
|
+
return _logger;
|
|
60
|
+
}
|
|
41
61
|
function formatArgs(args) {
|
|
42
62
|
const errors = [];
|
|
43
63
|
const parts = [];
|
|
@@ -51,24 +71,24 @@ function formatArgs(args) {
|
|
|
51
71
|
const serverLog = {
|
|
52
72
|
debug(...args) {
|
|
53
73
|
const { msg } = formatArgs(args);
|
|
54
|
-
|
|
74
|
+
getLogger().debug(msg);
|
|
55
75
|
},
|
|
56
76
|
info(...args) {
|
|
57
77
|
const { msg } = formatArgs(args);
|
|
58
|
-
|
|
78
|
+
getLogger().info(msg);
|
|
59
79
|
},
|
|
60
80
|
warn(...args) {
|
|
61
81
|
const { err, msg } = formatArgs(args);
|
|
62
|
-
if (err)
|
|
63
|
-
else
|
|
82
|
+
if (err) getLogger().warn({ err }, msg);
|
|
83
|
+
else getLogger().warn(msg);
|
|
64
84
|
},
|
|
65
85
|
error(...args) {
|
|
66
86
|
const { err, msg } = formatArgs(args);
|
|
67
|
-
if (err)
|
|
68
|
-
else
|
|
87
|
+
if (err) getLogger().error({ err }, msg);
|
|
88
|
+
else getLogger().error(msg);
|
|
69
89
|
},
|
|
70
90
|
event(obj, msg) {
|
|
71
|
-
|
|
91
|
+
getLogger().info(obj, msg);
|
|
72
92
|
}
|
|
73
93
|
};
|
|
74
94
|
|
|
@@ -745,7 +765,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
745
765
|
wake: {
|
|
746
766
|
on: `runFinished`,
|
|
747
767
|
includeResponse: true
|
|
748
|
-
}
|
|
768
|
+
},
|
|
769
|
+
sandbox: `inherit`
|
|
749
770
|
});
|
|
750
771
|
const workerUrl = handle.entityUrl;
|
|
751
772
|
return {
|
|
@@ -1132,19 +1153,19 @@ function getToolName(tool) {
|
|
|
1132
1153
|
const name = tool.name;
|
|
1133
1154
|
return typeof name === `string` ? name : null;
|
|
1134
1155
|
}
|
|
1135
|
-
function createHortonTools(
|
|
1156
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1136
1157
|
return [
|
|
1137
|
-
createBashTool(
|
|
1138
|
-
createReadFileTool(
|
|
1139
|
-
createWriteTool(
|
|
1140
|
-
createEditTool(
|
|
1158
|
+
createBashTool(sandbox),
|
|
1159
|
+
createReadFileTool(sandbox, readSet),
|
|
1160
|
+
createWriteTool(sandbox, readSet),
|
|
1161
|
+
createEditTool(sandbox, readSet),
|
|
1141
1162
|
braveSearchTool,
|
|
1142
|
-
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool({
|
|
1163
|
+
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool(sandbox, {
|
|
1143
1164
|
catalog: opts.modelCatalog,
|
|
1144
1165
|
modelConfig: opts.modelConfig,
|
|
1145
1166
|
log: (message) => serverLog.info(message),
|
|
1146
1167
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1147
|
-
})] : [
|
|
1168
|
+
})] : [createFetchUrlTool(sandbox)],
|
|
1148
1169
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1149
1170
|
createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1150
1171
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
@@ -1198,11 +1219,10 @@ async function extractFirstUserMessage(ctx) {
|
|
|
1198
1219
|
function messageSeq(message) {
|
|
1199
1220
|
return typeof message._seq === `number` ? message._seq : -1;
|
|
1200
1221
|
}
|
|
1201
|
-
function readAgentsMd(
|
|
1202
|
-
const agentsMdPath = path.join(workingDirectory, `AGENTS.md`);
|
|
1222
|
+
async function readAgentsMd(sandbox) {
|
|
1223
|
+
const agentsMdPath = path.posix.join(sandbox.workingDirectory, `AGENTS.md`);
|
|
1203
1224
|
try {
|
|
1204
|
-
|
|
1205
|
-
const content = fs.readFileSync(agentsMdPath, `utf8`);
|
|
1225
|
+
const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
|
|
1206
1226
|
return [
|
|
1207
1227
|
`<context_file kind="instructions" path="${agentsMdPath}">`,
|
|
1208
1228
|
content,
|
|
@@ -1213,16 +1233,16 @@ function readAgentsMd(workingDirectory) {
|
|
|
1213
1233
|
}
|
|
1214
1234
|
}
|
|
1215
1235
|
function createAssistantHandler(options) {
|
|
1216
|
-
const {
|
|
1236
|
+
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1217
1237
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1218
1238
|
return async function assistantHandler(ctx, wake) {
|
|
1219
1239
|
const readSet = new Set();
|
|
1220
|
-
const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
|
|
1221
1240
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1222
|
-
const
|
|
1241
|
+
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1242
|
+
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1223
1243
|
const tools = [
|
|
1224
1244
|
...ctx.electricTools,
|
|
1225
|
-
...createHortonTools(
|
|
1245
|
+
...createHortonTools(ctx.sandbox, ctx, readSet, {
|
|
1226
1246
|
docsSearchTool,
|
|
1227
1247
|
modelConfig,
|
|
1228
1248
|
modelCatalog,
|
|
@@ -1313,7 +1333,7 @@ function createAssistantHandler(options) {
|
|
|
1313
1333
|
}
|
|
1314
1334
|
});
|
|
1315
1335
|
ctx.useAgent({
|
|
1316
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1336
|
+
systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
|
|
1317
1337
|
hasDocsSupport: Boolean(docsSupport),
|
|
1318
1338
|
hasSkills,
|
|
1319
1339
|
docsUrl,
|
|
@@ -1341,7 +1361,6 @@ function registerHorton(registry, options) {
|
|
|
1341
1361
|
serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1342
1362
|
});
|
|
1343
1363
|
const assistantHandler = createAssistantHandler({
|
|
1344
|
-
workingDirectory,
|
|
1345
1364
|
streamFn,
|
|
1346
1365
|
docsSupport,
|
|
1347
1366
|
docsSearchTool,
|
|
@@ -1398,26 +1417,26 @@ function parseWorkerArgs(value) {
|
|
|
1398
1417
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1399
1418
|
return args;
|
|
1400
1419
|
}
|
|
1401
|
-
function buildToolsForWorker(tools,
|
|
1420
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1402
1421
|
const out = [];
|
|
1403
1422
|
for (const name of tools) switch (name) {
|
|
1404
1423
|
case `bash`:
|
|
1405
|
-
out.push(createBashTool(
|
|
1424
|
+
out.push(createBashTool(sandbox));
|
|
1406
1425
|
break;
|
|
1407
1426
|
case `read`:
|
|
1408
|
-
out.push(createReadFileTool(
|
|
1427
|
+
out.push(createReadFileTool(sandbox, readSet));
|
|
1409
1428
|
break;
|
|
1410
1429
|
case `write`:
|
|
1411
|
-
out.push(createWriteTool(
|
|
1430
|
+
out.push(createWriteTool(sandbox, readSet));
|
|
1412
1431
|
break;
|
|
1413
1432
|
case `edit`:
|
|
1414
|
-
out.push(createEditTool(
|
|
1433
|
+
out.push(createEditTool(sandbox, readSet));
|
|
1415
1434
|
break;
|
|
1416
1435
|
case `web_search`:
|
|
1417
1436
|
out.push(braveSearchTool);
|
|
1418
1437
|
break;
|
|
1419
1438
|
case `fetch_url`:
|
|
1420
|
-
out.push(
|
|
1439
|
+
out.push(createFetchUrlTool(sandbox));
|
|
1421
1440
|
break;
|
|
1422
1441
|
case `spawn_worker`:
|
|
1423
1442
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1525,13 +1544,13 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
1525
1544
|
return tools;
|
|
1526
1545
|
}
|
|
1527
1546
|
function registerWorker(registry, options) {
|
|
1528
|
-
const {
|
|
1547
|
+
const { streamFn, modelCatalog } = options;
|
|
1529
1548
|
registry.define(`worker`, {
|
|
1530
1549
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1531
1550
|
async handler(ctx) {
|
|
1532
1551
|
const args = parseWorkerArgs(ctx.args);
|
|
1533
1552
|
const readSet = new Set();
|
|
1534
|
-
const builtinTools = buildToolsForWorker(args.tools,
|
|
1553
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1535
1554
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1536
1555
|
const sharedStateTools = [];
|
|
1537
1556
|
if (args.sharedDb) {
|
|
@@ -1609,6 +1628,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1609
1628
|
modelCatalog
|
|
1610
1629
|
});
|
|
1611
1630
|
typeNames.push(`worker`);
|
|
1631
|
+
const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
|
|
1612
1632
|
const runtime = createRuntimeHandler({
|
|
1613
1633
|
baseUrl: agentServerUrl,
|
|
1614
1634
|
serveEndpoint,
|
|
@@ -1619,7 +1639,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1619
1639
|
idleTimeout: 5 * 6e4,
|
|
1620
1640
|
createElectricTools: createBuiltinElectricTools(createElectricTools),
|
|
1621
1641
|
publicUrl,
|
|
1622
|
-
name: runtimeName ?? `builtin-agents
|
|
1642
|
+
name: runtimeName ?? `builtin-agents`,
|
|
1643
|
+
sandboxProfiles
|
|
1623
1644
|
});
|
|
1624
1645
|
return {
|
|
1625
1646
|
handler: runtime.onEnter,
|
|
@@ -1633,6 +1654,85 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1633
1654
|
await bootstrap.runtime.registerTypes();
|
|
1634
1655
|
serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
|
|
1635
1656
|
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
|
|
1659
|
+
* re-run the boot sweep.
|
|
1660
|
+
*/
|
|
1661
|
+
let dockerSweptOnBoot = false;
|
|
1662
|
+
function sweepOrphanedDockerSandboxesOnce(sweep) {
|
|
1663
|
+
if (dockerSweptOnBoot) return;
|
|
1664
|
+
dockerSweptOnBoot = true;
|
|
1665
|
+
sweep().then((removed) => {
|
|
1666
|
+
if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
|
|
1667
|
+
}).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Built-in sandbox profiles. `local` is always available. `docker` is
|
|
1671
|
+
* gated on Docker being reachable so a user without Docker installed
|
|
1672
|
+
* sees only what works — the UI never offers a non-functional choice.
|
|
1673
|
+
*/
|
|
1674
|
+
async function buildBuiltinSandboxProfiles(workingDirectory) {
|
|
1675
|
+
const profiles = [{
|
|
1676
|
+
name: `local`,
|
|
1677
|
+
label: `Local`,
|
|
1678
|
+
description: `Runs on the host without isolation. Full filesystem access.`,
|
|
1679
|
+
factory: ({ args }) => chooseDefaultSandbox(resolveCwd(args, workingDirectory))
|
|
1680
|
+
}];
|
|
1681
|
+
try {
|
|
1682
|
+
const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1683
|
+
if (await isDockerAvailable()) {
|
|
1684
|
+
const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1685
|
+
sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
|
|
1686
|
+
profiles.push({
|
|
1687
|
+
name: `docker`,
|
|
1688
|
+
label: `Docker`,
|
|
1689
|
+
description: `Runs in a hardened Docker container: dropped capabilities, no privilege escalation, and CPU/memory/process limits. The chosen working directory is mounted read-write and, by default, network egress is unrestricted (allow-all).`,
|
|
1690
|
+
factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
|
|
1691
|
+
const cwd = readWorkingDirectoryArg(args);
|
|
1692
|
+
return dockerSandbox({
|
|
1693
|
+
initialNetworkPolicy: { mode: `allow-all` },
|
|
1694
|
+
extraMounts: cwd ? [{
|
|
1695
|
+
hostPath: cwd,
|
|
1696
|
+
containerPath: `/work`,
|
|
1697
|
+
readOnly: false
|
|
1698
|
+
}] : void 0,
|
|
1699
|
+
sandboxKey,
|
|
1700
|
+
persistent,
|
|
1701
|
+
owner,
|
|
1702
|
+
entityType,
|
|
1703
|
+
entityUrl
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
} else serverLog.info(`[builtin-agents] docker daemon not reachable — docker sandbox profile not registered`);
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
serverLog.warn(`[builtin-agents] failed to probe docker availability: ${err instanceof Error ? err.message : String(err)}`);
|
|
1710
|
+
}
|
|
1711
|
+
if (process.env.E2B_API_KEY) if (await isE2BAvailable()) profiles.push({
|
|
1712
|
+
name: `e2b`,
|
|
1713
|
+
label: `E2B`,
|
|
1714
|
+
description: `Runs in a remote E2B microVM. Persistent sandboxes survive across wakes and are reachable from any runner.`,
|
|
1715
|
+
remote: true,
|
|
1716
|
+
factory: ({ sandboxKey, persistent, owner }) => remoteSandbox({
|
|
1717
|
+
provider: `e2b`,
|
|
1718
|
+
apiKey: process.env.E2B_API_KEY,
|
|
1719
|
+
sandboxKey,
|
|
1720
|
+
persistent,
|
|
1721
|
+
owner,
|
|
1722
|
+
initialNetworkPolicy: { mode: `allow-all` }
|
|
1723
|
+
})
|
|
1724
|
+
});
|
|
1725
|
+
else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
|
|
1726
|
+
console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
|
|
1727
|
+
return profiles;
|
|
1728
|
+
}
|
|
1729
|
+
function readWorkingDirectoryArg(args) {
|
|
1730
|
+
const v = args.workingDirectory;
|
|
1731
|
+
return typeof v === `string` && v.trim().length > 0 ? v : null;
|
|
1732
|
+
}
|
|
1733
|
+
function resolveCwd(args, fallback) {
|
|
1734
|
+
return readWorkingDirectoryArg(args) ?? fallback;
|
|
1735
|
+
}
|
|
1636
1736
|
|
|
1637
1737
|
//#endregion
|
|
1638
1738
|
//#region src/server.ts
|
|
@@ -1841,6 +1941,7 @@ var BuiltinAgentsServer = class {
|
|
|
1841
1941
|
async registerPullWakeRunner(pullWake) {
|
|
1842
1942
|
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
1843
1943
|
headers.set(`content-type`, `application/json`);
|
|
1944
|
+
const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
|
|
1844
1945
|
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
1845
1946
|
method: `POST`,
|
|
1846
1947
|
headers,
|
|
@@ -1849,7 +1950,8 @@ var BuiltinAgentsServer = class {
|
|
|
1849
1950
|
owner_principal: pullWake.ownerPrincipal,
|
|
1850
1951
|
label: pullWake.label ?? `Built-in agents`,
|
|
1851
1952
|
kind: `local`,
|
|
1852
|
-
admin_status: `enabled
|
|
1953
|
+
admin_status: `enabled`,
|
|
1954
|
+
sandbox_profiles: profiles
|
|
1853
1955
|
})
|
|
1854
1956
|
});
|
|
1855
1957
|
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ const node_path = __toESM(require("node:path"));
|
|
|
27
27
|
const node_url = __toESM(require("node:url"));
|
|
28
28
|
const __electric_ax_agents_runtime = __toESM(require("@electric-ax/agents-runtime"));
|
|
29
29
|
const __electric_ax_agents_runtime_tools = __toESM(require("@electric-ax/agents-runtime/tools"));
|
|
30
|
+
const __electric_ax_agents_runtime_sandbox = __toESM(require("@electric-ax/agents-runtime/sandbox"));
|
|
30
31
|
const node_fs = __toESM(require("node:fs"));
|
|
31
32
|
const pino = __toESM(require("pino"));
|
|
32
33
|
const zod = __toESM(require("zod"));
|
|
@@ -40,28 +41,47 @@ const __mariozechner_pi_ai = __toESM(require("@mariozechner/pi-ai"));
|
|
|
40
41
|
const __electric_ax_agents_mcp = __toESM(require("@electric-ax/agents-mcp"));
|
|
41
42
|
|
|
42
43
|
//#region src/log.ts
|
|
43
|
-
const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? node_path.default.resolve(process.cwd(), `logs`);
|
|
44
|
-
node_fs.default.mkdirSync(LOG_DIR, { recursive: true });
|
|
45
|
-
const LOG_FILE = node_path.default.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
|
|
46
44
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
47
45
|
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
46
|
+
const USE_FILE_LOGS = process.env.ELECTRIC_AGENTS_LOG_FILE !== `false`;
|
|
48
47
|
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
let _logger;
|
|
49
|
+
function getLogger() {
|
|
50
|
+
if (_logger) return _logger;
|
|
51
|
+
const streams = [];
|
|
52
|
+
try {
|
|
53
|
+
if (USE_FILE_LOGS) {
|
|
54
|
+
const logDir = process.env.ELECTRIC_AGENTS_LOG_DIR ?? node_path.default.resolve(process.cwd(), `logs`);
|
|
55
|
+
node_fs.default.mkdirSync(logDir, { recursive: true });
|
|
56
|
+
const logFile = node_path.default.join(logDir, `builtin-agents-${Date.now()}.jsonl`);
|
|
57
|
+
streams.push({ stream: pino.default.destination({
|
|
58
|
+
dest: logFile,
|
|
59
|
+
sync: IS_ELECTRON_MAIN
|
|
60
|
+
}) });
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
process.stderr.write(`[agents] Failed to initialize file logging: ${err instanceof Error ? err.message : err}\n`);
|
|
59
64
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
try {
|
|
66
|
+
if (USE_PRETTY_LOGS) streams.push({ stream: pino.default.transport({
|
|
67
|
+
target: `pino-pretty`,
|
|
68
|
+
options: {
|
|
69
|
+
colorize: true,
|
|
70
|
+
ignore: `pid,hostname,name`,
|
|
71
|
+
translateTime: `SYS:HH:MM:ss`
|
|
72
|
+
}
|
|
73
|
+
}) });
|
|
74
|
+
} catch {}
|
|
75
|
+
_logger = streams.length > 0 ? (0, pino.default)({
|
|
76
|
+
base: void 0,
|
|
77
|
+
level: LOG_LEVEL
|
|
78
|
+
}, pino.default.multistream(streams)) : (0, pino.default)({
|
|
79
|
+
base: void 0,
|
|
80
|
+
enabled: false,
|
|
81
|
+
level: LOG_LEVEL
|
|
82
|
+
});
|
|
83
|
+
return _logger;
|
|
84
|
+
}
|
|
65
85
|
function formatArgs(args) {
|
|
66
86
|
const errors = [];
|
|
67
87
|
const parts = [];
|
|
@@ -75,24 +95,24 @@ function formatArgs(args) {
|
|
|
75
95
|
const serverLog = {
|
|
76
96
|
debug(...args) {
|
|
77
97
|
const { msg } = formatArgs(args);
|
|
78
|
-
|
|
98
|
+
getLogger().debug(msg);
|
|
79
99
|
},
|
|
80
100
|
info(...args) {
|
|
81
101
|
const { msg } = formatArgs(args);
|
|
82
|
-
|
|
102
|
+
getLogger().info(msg);
|
|
83
103
|
},
|
|
84
104
|
warn(...args) {
|
|
85
105
|
const { err, msg } = formatArgs(args);
|
|
86
|
-
if (err)
|
|
87
|
-
else
|
|
106
|
+
if (err) getLogger().warn({ err }, msg);
|
|
107
|
+
else getLogger().warn(msg);
|
|
88
108
|
},
|
|
89
109
|
error(...args) {
|
|
90
110
|
const { err, msg } = formatArgs(args);
|
|
91
|
-
if (err)
|
|
92
|
-
else
|
|
111
|
+
if (err) getLogger().error({ err }, msg);
|
|
112
|
+
else getLogger().error(msg);
|
|
93
113
|
},
|
|
94
114
|
event(obj, msg) {
|
|
95
|
-
|
|
115
|
+
getLogger().info(obj, msg);
|
|
96
116
|
}
|
|
97
117
|
};
|
|
98
118
|
|
|
@@ -769,7 +789,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
769
789
|
wake: {
|
|
770
790
|
on: `runFinished`,
|
|
771
791
|
includeResponse: true
|
|
772
|
-
}
|
|
792
|
+
},
|
|
793
|
+
sandbox: `inherit`
|
|
773
794
|
});
|
|
774
795
|
const workerUrl = handle.entityUrl;
|
|
775
796
|
return {
|
|
@@ -1157,19 +1178,19 @@ function getToolName(tool) {
|
|
|
1157
1178
|
const name = tool.name;
|
|
1158
1179
|
return typeof name === `string` ? name : null;
|
|
1159
1180
|
}
|
|
1160
|
-
function createHortonTools(
|
|
1181
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1161
1182
|
return [
|
|
1162
|
-
(0, __electric_ax_agents_runtime_tools.createBashTool)(
|
|
1163
|
-
(0, __electric_ax_agents_runtime_tools.createReadFileTool)(
|
|
1164
|
-
(0, __electric_ax_agents_runtime_tools.createWriteTool)(
|
|
1165
|
-
(0, __electric_ax_agents_runtime_tools.createEditTool)(
|
|
1183
|
+
(0, __electric_ax_agents_runtime_tools.createBashTool)(sandbox),
|
|
1184
|
+
(0, __electric_ax_agents_runtime_tools.createReadFileTool)(sandbox, readSet),
|
|
1185
|
+
(0, __electric_ax_agents_runtime_tools.createWriteTool)(sandbox, readSet),
|
|
1186
|
+
(0, __electric_ax_agents_runtime_tools.createEditTool)(sandbox, readSet),
|
|
1166
1187
|
__electric_ax_agents_runtime_tools.braveSearchTool,
|
|
1167
|
-
...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)({
|
|
1188
|
+
...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox, {
|
|
1168
1189
|
catalog: opts.modelCatalog,
|
|
1169
1190
|
modelConfig: opts.modelConfig,
|
|
1170
1191
|
log: (message) => serverLog.info(message),
|
|
1171
1192
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1172
|
-
})] : [__electric_ax_agents_runtime_tools.
|
|
1193
|
+
})] : [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox)],
|
|
1173
1194
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1174
1195
|
(0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1175
1196
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
@@ -1223,11 +1244,10 @@ async function extractFirstUserMessage(ctx) {
|
|
|
1223
1244
|
function messageSeq(message) {
|
|
1224
1245
|
return typeof message._seq === `number` ? message._seq : -1;
|
|
1225
1246
|
}
|
|
1226
|
-
function readAgentsMd(
|
|
1227
|
-
const agentsMdPath = node_path.default.join(workingDirectory, `AGENTS.md`);
|
|
1247
|
+
async function readAgentsMd(sandbox) {
|
|
1248
|
+
const agentsMdPath = node_path.default.posix.join(sandbox.workingDirectory, `AGENTS.md`);
|
|
1228
1249
|
try {
|
|
1229
|
-
|
|
1230
|
-
const content = node_fs.default.readFileSync(agentsMdPath, `utf8`);
|
|
1250
|
+
const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
|
|
1231
1251
|
return [
|
|
1232
1252
|
`<context_file kind="instructions" path="${agentsMdPath}">`,
|
|
1233
1253
|
content,
|
|
@@ -1238,16 +1258,16 @@ function readAgentsMd(workingDirectory) {
|
|
|
1238
1258
|
}
|
|
1239
1259
|
}
|
|
1240
1260
|
function createAssistantHandler(options) {
|
|
1241
|
-
const {
|
|
1261
|
+
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1242
1262
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1243
1263
|
return async function assistantHandler(ctx, wake) {
|
|
1244
1264
|
const readSet = new Set();
|
|
1245
|
-
const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
|
|
1246
1265
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1247
|
-
const
|
|
1266
|
+
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1267
|
+
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1248
1268
|
const tools = [
|
|
1249
1269
|
...ctx.electricTools,
|
|
1250
|
-
...createHortonTools(
|
|
1270
|
+
...createHortonTools(ctx.sandbox, ctx, readSet, {
|
|
1251
1271
|
docsSearchTool,
|
|
1252
1272
|
modelConfig,
|
|
1253
1273
|
modelCatalog,
|
|
@@ -1338,7 +1358,7 @@ function createAssistantHandler(options) {
|
|
|
1338
1358
|
}
|
|
1339
1359
|
});
|
|
1340
1360
|
ctx.useAgent({
|
|
1341
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1361
|
+
systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
|
|
1342
1362
|
hasDocsSupport: Boolean(docsSupport),
|
|
1343
1363
|
hasSkills,
|
|
1344
1364
|
docsUrl,
|
|
@@ -1366,7 +1386,6 @@ function registerHorton(registry, options) {
|
|
|
1366
1386
|
serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1367
1387
|
});
|
|
1368
1388
|
const assistantHandler = createAssistantHandler({
|
|
1369
|
-
workingDirectory,
|
|
1370
1389
|
streamFn,
|
|
1371
1390
|
docsSupport,
|
|
1372
1391
|
docsSearchTool,
|
|
@@ -1423,26 +1442,26 @@ function parseWorkerArgs(value) {
|
|
|
1423
1442
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1424
1443
|
return args;
|
|
1425
1444
|
}
|
|
1426
|
-
function buildToolsForWorker(tools,
|
|
1445
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1427
1446
|
const out = [];
|
|
1428
1447
|
for (const name of tools) switch (name) {
|
|
1429
1448
|
case `bash`:
|
|
1430
|
-
out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(
|
|
1449
|
+
out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(sandbox));
|
|
1431
1450
|
break;
|
|
1432
1451
|
case `read`:
|
|
1433
|
-
out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(
|
|
1452
|
+
out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(sandbox, readSet));
|
|
1434
1453
|
break;
|
|
1435
1454
|
case `write`:
|
|
1436
|
-
out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(
|
|
1455
|
+
out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(sandbox, readSet));
|
|
1437
1456
|
break;
|
|
1438
1457
|
case `edit`:
|
|
1439
|
-
out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(
|
|
1458
|
+
out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(sandbox, readSet));
|
|
1440
1459
|
break;
|
|
1441
1460
|
case `web_search`:
|
|
1442
1461
|
out.push(__electric_ax_agents_runtime_tools.braveSearchTool);
|
|
1443
1462
|
break;
|
|
1444
1463
|
case `fetch_url`:
|
|
1445
|
-
out.push(__electric_ax_agents_runtime_tools.
|
|
1464
|
+
out.push((0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox));
|
|
1446
1465
|
break;
|
|
1447
1466
|
case `spawn_worker`:
|
|
1448
1467
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1550,13 +1569,13 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
1550
1569
|
return tools;
|
|
1551
1570
|
}
|
|
1552
1571
|
function registerWorker(registry, options) {
|
|
1553
|
-
const {
|
|
1572
|
+
const { streamFn, modelCatalog } = options;
|
|
1554
1573
|
registry.define(`worker`, {
|
|
1555
1574
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1556
1575
|
async handler(ctx) {
|
|
1557
1576
|
const args = parseWorkerArgs(ctx.args);
|
|
1558
1577
|
const readSet = new Set();
|
|
1559
|
-
const builtinTools = buildToolsForWorker(args.tools,
|
|
1578
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1560
1579
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1561
1580
|
const sharedStateTools = [];
|
|
1562
1581
|
if (args.sharedDb) {
|
|
@@ -1635,6 +1654,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1635
1654
|
modelCatalog
|
|
1636
1655
|
});
|
|
1637
1656
|
typeNames.push(`worker`);
|
|
1657
|
+
const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
|
|
1638
1658
|
const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
|
|
1639
1659
|
baseUrl: agentServerUrl,
|
|
1640
1660
|
serveEndpoint,
|
|
@@ -1645,7 +1665,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1645
1665
|
idleTimeout: 5 * 6e4,
|
|
1646
1666
|
createElectricTools: createBuiltinElectricTools(createElectricTools),
|
|
1647
1667
|
publicUrl,
|
|
1648
|
-
name: runtimeName ?? `builtin-agents
|
|
1668
|
+
name: runtimeName ?? `builtin-agents`,
|
|
1669
|
+
sandboxProfiles
|
|
1649
1670
|
});
|
|
1650
1671
|
return {
|
|
1651
1672
|
handler: runtime.onEnter,
|
|
@@ -1669,6 +1690,85 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1669
1690
|
serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
|
|
1670
1691
|
}
|
|
1671
1692
|
const registerAgentTypes = registerBuiltinAgentTypes;
|
|
1693
|
+
/**
|
|
1694
|
+
* Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
|
|
1695
|
+
* re-run the boot sweep.
|
|
1696
|
+
*/
|
|
1697
|
+
let dockerSweptOnBoot = false;
|
|
1698
|
+
function sweepOrphanedDockerSandboxesOnce(sweep) {
|
|
1699
|
+
if (dockerSweptOnBoot) return;
|
|
1700
|
+
dockerSweptOnBoot = true;
|
|
1701
|
+
sweep().then((removed) => {
|
|
1702
|
+
if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
|
|
1703
|
+
}).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Built-in sandbox profiles. `local` is always available. `docker` is
|
|
1707
|
+
* gated on Docker being reachable so a user without Docker installed
|
|
1708
|
+
* sees only what works — the UI never offers a non-functional choice.
|
|
1709
|
+
*/
|
|
1710
|
+
async function buildBuiltinSandboxProfiles(workingDirectory) {
|
|
1711
|
+
const profiles = [{
|
|
1712
|
+
name: `local`,
|
|
1713
|
+
label: `Local`,
|
|
1714
|
+
description: `Runs on the host without isolation. Full filesystem access.`,
|
|
1715
|
+
factory: ({ args }) => (0, __electric_ax_agents_runtime_sandbox.chooseDefaultSandbox)(resolveCwd(args, workingDirectory))
|
|
1716
|
+
}];
|
|
1717
|
+
try {
|
|
1718
|
+
const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1719
|
+
if (await isDockerAvailable()) {
|
|
1720
|
+
const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1721
|
+
sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
|
|
1722
|
+
profiles.push({
|
|
1723
|
+
name: `docker`,
|
|
1724
|
+
label: `Docker`,
|
|
1725
|
+
description: `Runs in a hardened Docker container: dropped capabilities, no privilege escalation, and CPU/memory/process limits. The chosen working directory is mounted read-write and, by default, network egress is unrestricted (allow-all).`,
|
|
1726
|
+
factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
|
|
1727
|
+
const cwd = readWorkingDirectoryArg(args);
|
|
1728
|
+
return dockerSandbox({
|
|
1729
|
+
initialNetworkPolicy: { mode: `allow-all` },
|
|
1730
|
+
extraMounts: cwd ? [{
|
|
1731
|
+
hostPath: cwd,
|
|
1732
|
+
containerPath: `/work`,
|
|
1733
|
+
readOnly: false
|
|
1734
|
+
}] : void 0,
|
|
1735
|
+
sandboxKey,
|
|
1736
|
+
persistent,
|
|
1737
|
+
owner,
|
|
1738
|
+
entityType,
|
|
1739
|
+
entityUrl
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
} else serverLog.info(`[builtin-agents] docker daemon not reachable — docker sandbox profile not registered`);
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
serverLog.warn(`[builtin-agents] failed to probe docker availability: ${err instanceof Error ? err.message : String(err)}`);
|
|
1746
|
+
}
|
|
1747
|
+
if (process.env.E2B_API_KEY) if (await (0, __electric_ax_agents_runtime_sandbox.isE2BAvailable)()) profiles.push({
|
|
1748
|
+
name: `e2b`,
|
|
1749
|
+
label: `E2B`,
|
|
1750
|
+
description: `Runs in a remote E2B microVM. Persistent sandboxes survive across wakes and are reachable from any runner.`,
|
|
1751
|
+
remote: true,
|
|
1752
|
+
factory: ({ sandboxKey, persistent, owner }) => (0, __electric_ax_agents_runtime_sandbox.remoteSandbox)({
|
|
1753
|
+
provider: `e2b`,
|
|
1754
|
+
apiKey: process.env.E2B_API_KEY,
|
|
1755
|
+
sandboxKey,
|
|
1756
|
+
persistent,
|
|
1757
|
+
owner,
|
|
1758
|
+
initialNetworkPolicy: { mode: `allow-all` }
|
|
1759
|
+
})
|
|
1760
|
+
});
|
|
1761
|
+
else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
|
|
1762
|
+
console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
|
|
1763
|
+
return profiles;
|
|
1764
|
+
}
|
|
1765
|
+
function readWorkingDirectoryArg(args) {
|
|
1766
|
+
const v = args.workingDirectory;
|
|
1767
|
+
return typeof v === `string` && v.trim().length > 0 ? v : null;
|
|
1768
|
+
}
|
|
1769
|
+
function resolveCwd(args, fallback) {
|
|
1770
|
+
return readWorkingDirectoryArg(args) ?? fallback;
|
|
1771
|
+
}
|
|
1672
1772
|
|
|
1673
1773
|
//#endregion
|
|
1674
1774
|
//#region src/server.ts
|
|
@@ -1877,6 +1977,7 @@ var BuiltinAgentsServer = class {
|
|
|
1877
1977
|
async registerPullWakeRunner(pullWake) {
|
|
1878
1978
|
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
1879
1979
|
headers.set(`content-type`, `application/json`);
|
|
1980
|
+
const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
|
|
1880
1981
|
const response = await fetch((0, __electric_ax_agents_runtime.appendPathToUrl)(this.options.agentServerUrl, `/_electric/runners`), {
|
|
1881
1982
|
method: `POST`,
|
|
1882
1983
|
headers,
|
|
@@ -1885,7 +1986,8 @@ var BuiltinAgentsServer = class {
|
|
|
1885
1986
|
owner_principal: pullWake.ownerPrincipal,
|
|
1886
1987
|
label: pullWake.label ?? `Built-in agents`,
|
|
1887
1988
|
kind: `local`,
|
|
1888
|
-
admin_status: `enabled
|
|
1989
|
+
admin_status: `enabled`,
|
|
1990
|
+
sandbox_profiles: profiles
|
|
1889
1991
|
})
|
|
1890
1992
|
});
|
|
1891
1993
|
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,7 @@ import { AgentConfig, AgentTool, AvailableProvider, DispatchPolicy, EntityRegist
|
|
|
2
2
|
import { AgentTool as AgentTool$1, StreamFn } from "@mariozechner/pi-agent-core";
|
|
3
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
4
|
import { ListedEntry as McpListedEntry, McpConfig, McpServerConfig, McpServerConfig as McpServerConfig$1, Registry, Registry as McpRegistry, RegistrySnapshot, RegistrySubscriber } from "@electric-ax/agents-mcp";
|
|
5
|
+
import { Sandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
5
6
|
import { ChangeEvent } from "@durable-streams/state";
|
|
6
7
|
import { braveSearchTool } from "@electric-ax/agents-runtime/tools";
|
|
7
8
|
|
|
@@ -167,7 +168,7 @@ declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
|
|
|
167
168
|
modelProvider?: string;
|
|
168
169
|
modelId?: string;
|
|
169
170
|
}): string;
|
|
170
|
-
declare function createHortonTools(
|
|
171
|
+
declare function createHortonTools(sandbox: Sandbox, ctx: HandlerContext, readSet: Set<string>, opts?: {
|
|
171
172
|
docsSearchTool?: AgentTool$1;
|
|
172
173
|
modelConfig?: ReturnType<typeof resolveBuiltinModelConfig>;
|
|
173
174
|
modelCatalog?: BuiltinModelCatalog;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentConfig, AgentTool, AvailableProvider, DispatchPolicy, EntityRegistry, HandlerContext, HeadersProvider, ProcessWakeConfig, PullWakeRunnerConfig, RuntimeHandler, SkillsRegistry, WakeEvent } from "@electric-ax/agents-runtime";
|
|
2
2
|
import { braveSearchTool } from "@electric-ax/agents-runtime/tools";
|
|
3
|
+
import { Sandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
3
4
|
import { ListedEntry as McpListedEntry, McpConfig, McpServerConfig, McpServerConfig as McpServerConfig$1, Registry, Registry as McpRegistry, RegistrySnapshot, RegistrySubscriber } from "@electric-ax/agents-mcp";
|
|
4
5
|
import { AgentTool as AgentTool$1, StreamFn } from "@mariozechner/pi-agent-core";
|
|
5
6
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -167,7 +168,7 @@ declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
|
|
|
167
168
|
modelProvider?: string;
|
|
168
169
|
modelId?: string;
|
|
169
170
|
}): string;
|
|
170
|
-
declare function createHortonTools(
|
|
171
|
+
declare function createHortonTools(sandbox: Sandbox, ctx: HandlerContext, readSet: Set<string>, opts?: {
|
|
171
172
|
docsSearchTool?: AgentTool$1;
|
|
172
173
|
modelConfig?: ReturnType<typeof resolveBuiltinModelConfig>;
|
|
173
174
|
modelCatalog?: BuiltinModelCatalog;
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,13 @@ import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
5
|
-
import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool
|
|
6
|
-
import
|
|
5
|
+
import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
|
|
6
|
+
import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
|
|
7
|
+
import fsSync from "node:fs";
|
|
7
8
|
import pino from "pino";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import { createHash } from "node:crypto";
|
|
10
|
-
import fs
|
|
11
|
+
import fs from "node:fs/promises";
|
|
11
12
|
import Database from "better-sqlite3";
|
|
12
13
|
import { Type } from "@sinclair/typebox";
|
|
13
14
|
import { load } from "sqlite-vec";
|
|
@@ -16,28 +17,47 @@ import { getModels } from "@mariozechner/pi-ai";
|
|
|
16
17
|
import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
|
|
17
18
|
|
|
18
19
|
//#region src/log.ts
|
|
19
|
-
const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
|
|
20
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
21
|
-
const LOG_FILE = path.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
|
|
22
20
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
23
21
|
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
22
|
+
const USE_FILE_LOGS = process.env.ELECTRIC_AGENTS_LOG_FILE !== `false`;
|
|
24
23
|
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
let _logger;
|
|
25
|
+
function getLogger() {
|
|
26
|
+
if (_logger) return _logger;
|
|
27
|
+
const streams = [];
|
|
28
|
+
try {
|
|
29
|
+
if (USE_FILE_LOGS) {
|
|
30
|
+
const logDir = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
|
|
31
|
+
fsSync.mkdirSync(logDir, { recursive: true });
|
|
32
|
+
const logFile = path.join(logDir, `builtin-agents-${Date.now()}.jsonl`);
|
|
33
|
+
streams.push({ stream: pino.destination({
|
|
34
|
+
dest: logFile,
|
|
35
|
+
sync: IS_ELECTRON_MAIN
|
|
36
|
+
}) });
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
process.stderr.write(`[agents] Failed to initialize file logging: ${err instanceof Error ? err.message : err}\n`);
|
|
35
40
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
try {
|
|
42
|
+
if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
|
|
43
|
+
target: `pino-pretty`,
|
|
44
|
+
options: {
|
|
45
|
+
colorize: true,
|
|
46
|
+
ignore: `pid,hostname,name`,
|
|
47
|
+
translateTime: `SYS:HH:MM:ss`
|
|
48
|
+
}
|
|
49
|
+
}) });
|
|
50
|
+
} catch {}
|
|
51
|
+
_logger = streams.length > 0 ? pino({
|
|
52
|
+
base: void 0,
|
|
53
|
+
level: LOG_LEVEL
|
|
54
|
+
}, pino.multistream(streams)) : pino({
|
|
55
|
+
base: void 0,
|
|
56
|
+
enabled: false,
|
|
57
|
+
level: LOG_LEVEL
|
|
58
|
+
});
|
|
59
|
+
return _logger;
|
|
60
|
+
}
|
|
41
61
|
function formatArgs(args) {
|
|
42
62
|
const errors = [];
|
|
43
63
|
const parts = [];
|
|
@@ -51,24 +71,24 @@ function formatArgs(args) {
|
|
|
51
71
|
const serverLog = {
|
|
52
72
|
debug(...args) {
|
|
53
73
|
const { msg } = formatArgs(args);
|
|
54
|
-
|
|
74
|
+
getLogger().debug(msg);
|
|
55
75
|
},
|
|
56
76
|
info(...args) {
|
|
57
77
|
const { msg } = formatArgs(args);
|
|
58
|
-
|
|
78
|
+
getLogger().info(msg);
|
|
59
79
|
},
|
|
60
80
|
warn(...args) {
|
|
61
81
|
const { err, msg } = formatArgs(args);
|
|
62
|
-
if (err)
|
|
63
|
-
else
|
|
82
|
+
if (err) getLogger().warn({ err }, msg);
|
|
83
|
+
else getLogger().warn(msg);
|
|
64
84
|
},
|
|
65
85
|
error(...args) {
|
|
66
86
|
const { err, msg } = formatArgs(args);
|
|
67
|
-
if (err)
|
|
68
|
-
else
|
|
87
|
+
if (err) getLogger().error({ err }, msg);
|
|
88
|
+
else getLogger().error(msg);
|
|
69
89
|
},
|
|
70
90
|
event(obj, msg) {
|
|
71
|
-
|
|
91
|
+
getLogger().info(obj, msg);
|
|
72
92
|
}
|
|
73
93
|
};
|
|
74
94
|
|
|
@@ -142,7 +162,7 @@ function normalizeWhitespace(value) {
|
|
|
142
162
|
}
|
|
143
163
|
async function collectMarkdownFiles(root) {
|
|
144
164
|
async function walk(dir) {
|
|
145
|
-
const entries = await fs
|
|
165
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
146
166
|
const files = [];
|
|
147
167
|
for (const entry of entries) {
|
|
148
168
|
const fullPath = path.join(dir, entry.name);
|
|
@@ -318,7 +338,7 @@ function resolveDocsRoot(workingDirectory) {
|
|
|
318
338
|
requireIndex: false
|
|
319
339
|
}
|
|
320
340
|
].filter((value) => Boolean(value));
|
|
321
|
-
for (const candidate of candidates) if (
|
|
341
|
+
for (const candidate of candidates) if (fsSync.existsSync(candidate.path) && (!candidate.requireIndex || fsSync.existsSync(path.join(candidate.path, `index.md`)))) return candidate.path;
|
|
322
342
|
return null;
|
|
323
343
|
}
|
|
324
344
|
var DocsKnowledgeBase = class {
|
|
@@ -339,7 +359,7 @@ var DocsKnowledgeBase = class {
|
|
|
339
359
|
this.readyPromise = this.ensureIngested();
|
|
340
360
|
}
|
|
341
361
|
openDatabase() {
|
|
342
|
-
|
|
362
|
+
fsSync.mkdirSync(path.dirname(this.dbPath), { recursive: true });
|
|
343
363
|
try {
|
|
344
364
|
const db$1 = new Database(this.dbPath);
|
|
345
365
|
load(db$1);
|
|
@@ -406,11 +426,11 @@ var DocsKnowledgeBase = class {
|
|
|
406
426
|
};
|
|
407
427
|
}
|
|
408
428
|
async ensureIngested() {
|
|
409
|
-
await fs
|
|
429
|
+
await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
|
|
410
430
|
const files = (await collectMarkdownFiles(this.docsRoot)).sort();
|
|
411
431
|
const docs = await Promise.all(files.map(async (filePath) => ({
|
|
412
432
|
path: path.relative(this.docsRoot, filePath),
|
|
413
|
-
content: await fs
|
|
433
|
+
content: await fs.readFile(filePath, `utf8`)
|
|
414
434
|
})));
|
|
415
435
|
const fingerprint = createFingerprint(docs);
|
|
416
436
|
if (!this.db) {
|
|
@@ -745,7 +765,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
745
765
|
wake: {
|
|
746
766
|
on: `runFinished`,
|
|
747
767
|
includeResponse: true
|
|
748
|
-
}
|
|
768
|
+
},
|
|
769
|
+
sandbox: `inherit`
|
|
749
770
|
});
|
|
750
771
|
const workerUrl = handle.entityUrl;
|
|
751
772
|
return {
|
|
@@ -1133,19 +1154,19 @@ function getToolName(tool) {
|
|
|
1133
1154
|
const name = tool.name;
|
|
1134
1155
|
return typeof name === `string` ? name : null;
|
|
1135
1156
|
}
|
|
1136
|
-
function createHortonTools(
|
|
1157
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1137
1158
|
return [
|
|
1138
|
-
createBashTool(
|
|
1139
|
-
createReadFileTool(
|
|
1140
|
-
createWriteTool(
|
|
1141
|
-
createEditTool(
|
|
1159
|
+
createBashTool(sandbox),
|
|
1160
|
+
createReadFileTool(sandbox, readSet),
|
|
1161
|
+
createWriteTool(sandbox, readSet),
|
|
1162
|
+
createEditTool(sandbox, readSet),
|
|
1142
1163
|
braveSearchTool$1,
|
|
1143
|
-
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool({
|
|
1164
|
+
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool(sandbox, {
|
|
1144
1165
|
catalog: opts.modelCatalog,
|
|
1145
1166
|
modelConfig: opts.modelConfig,
|
|
1146
1167
|
log: (message) => serverLog.info(message),
|
|
1147
1168
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1148
|
-
})] : [
|
|
1169
|
+
})] : [createFetchUrlTool(sandbox)],
|
|
1149
1170
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1150
1171
|
createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1151
1172
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
@@ -1199,11 +1220,10 @@ async function extractFirstUserMessage(ctx) {
|
|
|
1199
1220
|
function messageSeq(message) {
|
|
1200
1221
|
return typeof message._seq === `number` ? message._seq : -1;
|
|
1201
1222
|
}
|
|
1202
|
-
function readAgentsMd(
|
|
1203
|
-
const agentsMdPath = path.join(workingDirectory, `AGENTS.md`);
|
|
1223
|
+
async function readAgentsMd(sandbox) {
|
|
1224
|
+
const agentsMdPath = path.posix.join(sandbox.workingDirectory, `AGENTS.md`);
|
|
1204
1225
|
try {
|
|
1205
|
-
|
|
1206
|
-
const content = fs.readFileSync(agentsMdPath, `utf8`);
|
|
1226
|
+
const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
|
|
1207
1227
|
return [
|
|
1208
1228
|
`<context_file kind="instructions" path="${agentsMdPath}">`,
|
|
1209
1229
|
content,
|
|
@@ -1214,16 +1234,16 @@ function readAgentsMd(workingDirectory) {
|
|
|
1214
1234
|
}
|
|
1215
1235
|
}
|
|
1216
1236
|
function createAssistantHandler(options) {
|
|
1217
|
-
const {
|
|
1237
|
+
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1218
1238
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1219
1239
|
return async function assistantHandler(ctx, wake) {
|
|
1220
1240
|
const readSet = new Set();
|
|
1221
|
-
const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
|
|
1222
1241
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1223
|
-
const
|
|
1242
|
+
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1243
|
+
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1224
1244
|
const tools = [
|
|
1225
1245
|
...ctx.electricTools,
|
|
1226
|
-
...createHortonTools(
|
|
1246
|
+
...createHortonTools(ctx.sandbox, ctx, readSet, {
|
|
1227
1247
|
docsSearchTool,
|
|
1228
1248
|
modelConfig,
|
|
1229
1249
|
modelCatalog,
|
|
@@ -1314,7 +1334,7 @@ function createAssistantHandler(options) {
|
|
|
1314
1334
|
}
|
|
1315
1335
|
});
|
|
1316
1336
|
ctx.useAgent({
|
|
1317
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1337
|
+
systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
|
|
1318
1338
|
hasDocsSupport: Boolean(docsSupport),
|
|
1319
1339
|
hasSkills,
|
|
1320
1340
|
docsUrl,
|
|
@@ -1342,7 +1362,6 @@ function registerHorton(registry, options) {
|
|
|
1342
1362
|
serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1343
1363
|
});
|
|
1344
1364
|
const assistantHandler = createAssistantHandler({
|
|
1345
|
-
workingDirectory,
|
|
1346
1365
|
streamFn,
|
|
1347
1366
|
docsSupport,
|
|
1348
1367
|
docsSearchTool,
|
|
@@ -1399,26 +1418,26 @@ function parseWorkerArgs(value) {
|
|
|
1399
1418
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1400
1419
|
return args;
|
|
1401
1420
|
}
|
|
1402
|
-
function buildToolsForWorker(tools,
|
|
1421
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1403
1422
|
const out = [];
|
|
1404
1423
|
for (const name of tools) switch (name) {
|
|
1405
1424
|
case `bash`:
|
|
1406
|
-
out.push(createBashTool(
|
|
1425
|
+
out.push(createBashTool(sandbox));
|
|
1407
1426
|
break;
|
|
1408
1427
|
case `read`:
|
|
1409
|
-
out.push(createReadFileTool(
|
|
1428
|
+
out.push(createReadFileTool(sandbox, readSet));
|
|
1410
1429
|
break;
|
|
1411
1430
|
case `write`:
|
|
1412
|
-
out.push(createWriteTool(
|
|
1431
|
+
out.push(createWriteTool(sandbox, readSet));
|
|
1413
1432
|
break;
|
|
1414
1433
|
case `edit`:
|
|
1415
|
-
out.push(createEditTool(
|
|
1434
|
+
out.push(createEditTool(sandbox, readSet));
|
|
1416
1435
|
break;
|
|
1417
1436
|
case `web_search`:
|
|
1418
1437
|
out.push(braveSearchTool$1);
|
|
1419
1438
|
break;
|
|
1420
1439
|
case `fetch_url`:
|
|
1421
|
-
out.push(
|
|
1440
|
+
out.push(createFetchUrlTool(sandbox));
|
|
1422
1441
|
break;
|
|
1423
1442
|
case `spawn_worker`:
|
|
1424
1443
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1526,13 +1545,13 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
1526
1545
|
return tools;
|
|
1527
1546
|
}
|
|
1528
1547
|
function registerWorker(registry, options) {
|
|
1529
|
-
const {
|
|
1548
|
+
const { streamFn, modelCatalog } = options;
|
|
1530
1549
|
registry.define(`worker`, {
|
|
1531
1550
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1532
1551
|
async handler(ctx) {
|
|
1533
1552
|
const args = parseWorkerArgs(ctx.args);
|
|
1534
1553
|
const readSet = new Set();
|
|
1535
|
-
const builtinTools = buildToolsForWorker(args.tools,
|
|
1554
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1536
1555
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1537
1556
|
const sharedStateTools = [];
|
|
1538
1557
|
if (args.sharedDb) {
|
|
@@ -1611,6 +1630,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1611
1630
|
modelCatalog
|
|
1612
1631
|
});
|
|
1613
1632
|
typeNames.push(`worker`);
|
|
1633
|
+
const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
|
|
1614
1634
|
const runtime = createRuntimeHandler({
|
|
1615
1635
|
baseUrl: agentServerUrl,
|
|
1616
1636
|
serveEndpoint,
|
|
@@ -1621,7 +1641,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1621
1641
|
idleTimeout: 5 * 6e4,
|
|
1622
1642
|
createElectricTools: createBuiltinElectricTools(createElectricTools),
|
|
1623
1643
|
publicUrl,
|
|
1624
|
-
name: runtimeName ?? `builtin-agents
|
|
1644
|
+
name: runtimeName ?? `builtin-agents`,
|
|
1645
|
+
sandboxProfiles
|
|
1625
1646
|
});
|
|
1626
1647
|
return {
|
|
1627
1648
|
handler: runtime.onEnter,
|
|
@@ -1645,6 +1666,85 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1645
1666
|
serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
|
|
1646
1667
|
}
|
|
1647
1668
|
const registerAgentTypes = registerBuiltinAgentTypes;
|
|
1669
|
+
/**
|
|
1670
|
+
* Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
|
|
1671
|
+
* re-run the boot sweep.
|
|
1672
|
+
*/
|
|
1673
|
+
let dockerSweptOnBoot = false;
|
|
1674
|
+
function sweepOrphanedDockerSandboxesOnce(sweep) {
|
|
1675
|
+
if (dockerSweptOnBoot) return;
|
|
1676
|
+
dockerSweptOnBoot = true;
|
|
1677
|
+
sweep().then((removed) => {
|
|
1678
|
+
if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
|
|
1679
|
+
}).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Built-in sandbox profiles. `local` is always available. `docker` is
|
|
1683
|
+
* gated on Docker being reachable so a user without Docker installed
|
|
1684
|
+
* sees only what works — the UI never offers a non-functional choice.
|
|
1685
|
+
*/
|
|
1686
|
+
async function buildBuiltinSandboxProfiles(workingDirectory) {
|
|
1687
|
+
const profiles = [{
|
|
1688
|
+
name: `local`,
|
|
1689
|
+
label: `Local`,
|
|
1690
|
+
description: `Runs on the host without isolation. Full filesystem access.`,
|
|
1691
|
+
factory: ({ args }) => chooseDefaultSandbox(resolveCwd(args, workingDirectory))
|
|
1692
|
+
}];
|
|
1693
|
+
try {
|
|
1694
|
+
const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1695
|
+
if (await isDockerAvailable()) {
|
|
1696
|
+
const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
|
|
1697
|
+
sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
|
|
1698
|
+
profiles.push({
|
|
1699
|
+
name: `docker`,
|
|
1700
|
+
label: `Docker`,
|
|
1701
|
+
description: `Runs in a hardened Docker container: dropped capabilities, no privilege escalation, and CPU/memory/process limits. The chosen working directory is mounted read-write and, by default, network egress is unrestricted (allow-all).`,
|
|
1702
|
+
factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
|
|
1703
|
+
const cwd = readWorkingDirectoryArg(args);
|
|
1704
|
+
return dockerSandbox({
|
|
1705
|
+
initialNetworkPolicy: { mode: `allow-all` },
|
|
1706
|
+
extraMounts: cwd ? [{
|
|
1707
|
+
hostPath: cwd,
|
|
1708
|
+
containerPath: `/work`,
|
|
1709
|
+
readOnly: false
|
|
1710
|
+
}] : void 0,
|
|
1711
|
+
sandboxKey,
|
|
1712
|
+
persistent,
|
|
1713
|
+
owner,
|
|
1714
|
+
entityType,
|
|
1715
|
+
entityUrl
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
} else serverLog.info(`[builtin-agents] docker daemon not reachable — docker sandbox profile not registered`);
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
serverLog.warn(`[builtin-agents] failed to probe docker availability: ${err instanceof Error ? err.message : String(err)}`);
|
|
1722
|
+
}
|
|
1723
|
+
if (process.env.E2B_API_KEY) if (await isE2BAvailable()) profiles.push({
|
|
1724
|
+
name: `e2b`,
|
|
1725
|
+
label: `E2B`,
|
|
1726
|
+
description: `Runs in a remote E2B microVM. Persistent sandboxes survive across wakes and are reachable from any runner.`,
|
|
1727
|
+
remote: true,
|
|
1728
|
+
factory: ({ sandboxKey, persistent, owner }) => remoteSandbox({
|
|
1729
|
+
provider: `e2b`,
|
|
1730
|
+
apiKey: process.env.E2B_API_KEY,
|
|
1731
|
+
sandboxKey,
|
|
1732
|
+
persistent,
|
|
1733
|
+
owner,
|
|
1734
|
+
initialNetworkPolicy: { mode: `allow-all` }
|
|
1735
|
+
})
|
|
1736
|
+
});
|
|
1737
|
+
else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
|
|
1738
|
+
console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
|
|
1739
|
+
return profiles;
|
|
1740
|
+
}
|
|
1741
|
+
function readWorkingDirectoryArg(args) {
|
|
1742
|
+
const v = args.workingDirectory;
|
|
1743
|
+
return typeof v === `string` && v.trim().length > 0 ? v : null;
|
|
1744
|
+
}
|
|
1745
|
+
function resolveCwd(args, fallback) {
|
|
1746
|
+
return readWorkingDirectoryArg(args) ?? fallback;
|
|
1747
|
+
}
|
|
1648
1748
|
|
|
1649
1749
|
//#endregion
|
|
1650
1750
|
//#region src/server.ts
|
|
@@ -1853,6 +1953,7 @@ var BuiltinAgentsServer = class {
|
|
|
1853
1953
|
async registerPullWakeRunner(pullWake) {
|
|
1854
1954
|
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
1855
1955
|
headers.set(`content-type`, `application/json`);
|
|
1956
|
+
const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
|
|
1856
1957
|
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
1857
1958
|
method: `POST`,
|
|
1858
1959
|
headers,
|
|
@@ -1861,7 +1962,8 @@ var BuiltinAgentsServer = class {
|
|
|
1861
1962
|
owner_principal: pullWake.ownerPrincipal,
|
|
1862
1963
|
label: pullWake.label ?? `Built-in agents`,
|
|
1863
1964
|
kind: `local`,
|
|
1864
|
-
admin_status: `enabled
|
|
1965
|
+
admin_status: `enabled`,
|
|
1966
|
+
sandbox_profiles: profiles
|
|
1865
1967
|
})
|
|
1866
1968
|
});
|
|
1867
1969
|
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "Built-in Electric Agents runtimes such as Horton and worker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"sqlite-vec": "^0.1.9",
|
|
50
50
|
"zod": "^4.3.6",
|
|
51
51
|
"@electric-ax/agents-mcp": "0.2.2",
|
|
52
|
-
"@electric-ax/agents-runtime": "0.3.
|
|
52
|
+
"@electric-ax/agents-runtime": "0.3.8"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/better-sqlite3": "^7.6.13",
|