@electric-ax/agents 0.4.11 → 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 +112 -29
- package/dist/index.cjs +111 -28
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +120 -37
- 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";
|
|
@@ -764,7 +765,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
764
765
|
wake: {
|
|
765
766
|
on: `runFinished`,
|
|
766
767
|
includeResponse: true
|
|
767
|
-
}
|
|
768
|
+
},
|
|
769
|
+
sandbox: `inherit`
|
|
768
770
|
});
|
|
769
771
|
const workerUrl = handle.entityUrl;
|
|
770
772
|
return {
|
|
@@ -1151,19 +1153,19 @@ function getToolName(tool) {
|
|
|
1151
1153
|
const name = tool.name;
|
|
1152
1154
|
return typeof name === `string` ? name : null;
|
|
1153
1155
|
}
|
|
1154
|
-
function createHortonTools(
|
|
1156
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1155
1157
|
return [
|
|
1156
|
-
createBashTool(
|
|
1157
|
-
createReadFileTool(
|
|
1158
|
-
createWriteTool(
|
|
1159
|
-
createEditTool(
|
|
1158
|
+
createBashTool(sandbox),
|
|
1159
|
+
createReadFileTool(sandbox, readSet),
|
|
1160
|
+
createWriteTool(sandbox, readSet),
|
|
1161
|
+
createEditTool(sandbox, readSet),
|
|
1160
1162
|
braveSearchTool,
|
|
1161
|
-
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool({
|
|
1163
|
+
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool(sandbox, {
|
|
1162
1164
|
catalog: opts.modelCatalog,
|
|
1163
1165
|
modelConfig: opts.modelConfig,
|
|
1164
1166
|
log: (message) => serverLog.info(message),
|
|
1165
1167
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1166
|
-
})] : [
|
|
1168
|
+
})] : [createFetchUrlTool(sandbox)],
|
|
1167
1169
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1168
1170
|
createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1169
1171
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
@@ -1217,11 +1219,10 @@ async function extractFirstUserMessage(ctx) {
|
|
|
1217
1219
|
function messageSeq(message) {
|
|
1218
1220
|
return typeof message._seq === `number` ? message._seq : -1;
|
|
1219
1221
|
}
|
|
1220
|
-
function readAgentsMd(
|
|
1221
|
-
const agentsMdPath = path.join(workingDirectory, `AGENTS.md`);
|
|
1222
|
+
async function readAgentsMd(sandbox) {
|
|
1223
|
+
const agentsMdPath = path.posix.join(sandbox.workingDirectory, `AGENTS.md`);
|
|
1222
1224
|
try {
|
|
1223
|
-
|
|
1224
|
-
const content = fs.readFileSync(agentsMdPath, `utf8`);
|
|
1225
|
+
const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
|
|
1225
1226
|
return [
|
|
1226
1227
|
`<context_file kind="instructions" path="${agentsMdPath}">`,
|
|
1227
1228
|
content,
|
|
@@ -1232,16 +1233,16 @@ function readAgentsMd(workingDirectory) {
|
|
|
1232
1233
|
}
|
|
1233
1234
|
}
|
|
1234
1235
|
function createAssistantHandler(options) {
|
|
1235
|
-
const {
|
|
1236
|
+
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1236
1237
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1237
1238
|
return async function assistantHandler(ctx, wake) {
|
|
1238
1239
|
const readSet = new Set();
|
|
1239
|
-
const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
|
|
1240
1240
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1241
|
-
const
|
|
1241
|
+
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1242
|
+
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1242
1243
|
const tools = [
|
|
1243
1244
|
...ctx.electricTools,
|
|
1244
|
-
...createHortonTools(
|
|
1245
|
+
...createHortonTools(ctx.sandbox, ctx, readSet, {
|
|
1245
1246
|
docsSearchTool,
|
|
1246
1247
|
modelConfig,
|
|
1247
1248
|
modelCatalog,
|
|
@@ -1332,7 +1333,7 @@ function createAssistantHandler(options) {
|
|
|
1332
1333
|
}
|
|
1333
1334
|
});
|
|
1334
1335
|
ctx.useAgent({
|
|
1335
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1336
|
+
systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
|
|
1336
1337
|
hasDocsSupport: Boolean(docsSupport),
|
|
1337
1338
|
hasSkills,
|
|
1338
1339
|
docsUrl,
|
|
@@ -1360,7 +1361,6 @@ function registerHorton(registry, options) {
|
|
|
1360
1361
|
serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1361
1362
|
});
|
|
1362
1363
|
const assistantHandler = createAssistantHandler({
|
|
1363
|
-
workingDirectory,
|
|
1364
1364
|
streamFn,
|
|
1365
1365
|
docsSupport,
|
|
1366
1366
|
docsSearchTool,
|
|
@@ -1417,26 +1417,26 @@ function parseWorkerArgs(value) {
|
|
|
1417
1417
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1418
1418
|
return args;
|
|
1419
1419
|
}
|
|
1420
|
-
function buildToolsForWorker(tools,
|
|
1420
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1421
1421
|
const out = [];
|
|
1422
1422
|
for (const name of tools) switch (name) {
|
|
1423
1423
|
case `bash`:
|
|
1424
|
-
out.push(createBashTool(
|
|
1424
|
+
out.push(createBashTool(sandbox));
|
|
1425
1425
|
break;
|
|
1426
1426
|
case `read`:
|
|
1427
|
-
out.push(createReadFileTool(
|
|
1427
|
+
out.push(createReadFileTool(sandbox, readSet));
|
|
1428
1428
|
break;
|
|
1429
1429
|
case `write`:
|
|
1430
|
-
out.push(createWriteTool(
|
|
1430
|
+
out.push(createWriteTool(sandbox, readSet));
|
|
1431
1431
|
break;
|
|
1432
1432
|
case `edit`:
|
|
1433
|
-
out.push(createEditTool(
|
|
1433
|
+
out.push(createEditTool(sandbox, readSet));
|
|
1434
1434
|
break;
|
|
1435
1435
|
case `web_search`:
|
|
1436
1436
|
out.push(braveSearchTool);
|
|
1437
1437
|
break;
|
|
1438
1438
|
case `fetch_url`:
|
|
1439
|
-
out.push(
|
|
1439
|
+
out.push(createFetchUrlTool(sandbox));
|
|
1440
1440
|
break;
|
|
1441
1441
|
case `spawn_worker`:
|
|
1442
1442
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1544,13 +1544,13 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
1544
1544
|
return tools;
|
|
1545
1545
|
}
|
|
1546
1546
|
function registerWorker(registry, options) {
|
|
1547
|
-
const {
|
|
1547
|
+
const { streamFn, modelCatalog } = options;
|
|
1548
1548
|
registry.define(`worker`, {
|
|
1549
1549
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1550
1550
|
async handler(ctx) {
|
|
1551
1551
|
const args = parseWorkerArgs(ctx.args);
|
|
1552
1552
|
const readSet = new Set();
|
|
1553
|
-
const builtinTools = buildToolsForWorker(args.tools,
|
|
1553
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1554
1554
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1555
1555
|
const sharedStateTools = [];
|
|
1556
1556
|
if (args.sharedDb) {
|
|
@@ -1628,6 +1628,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1628
1628
|
modelCatalog
|
|
1629
1629
|
});
|
|
1630
1630
|
typeNames.push(`worker`);
|
|
1631
|
+
const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
|
|
1631
1632
|
const runtime = createRuntimeHandler({
|
|
1632
1633
|
baseUrl: agentServerUrl,
|
|
1633
1634
|
serveEndpoint,
|
|
@@ -1638,7 +1639,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1638
1639
|
idleTimeout: 5 * 6e4,
|
|
1639
1640
|
createElectricTools: createBuiltinElectricTools(createElectricTools),
|
|
1640
1641
|
publicUrl,
|
|
1641
|
-
name: runtimeName ?? `builtin-agents
|
|
1642
|
+
name: runtimeName ?? `builtin-agents`,
|
|
1643
|
+
sandboxProfiles
|
|
1642
1644
|
});
|
|
1643
1645
|
return {
|
|
1644
1646
|
handler: runtime.onEnter,
|
|
@@ -1652,6 +1654,85 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1652
1654
|
await bootstrap.runtime.registerTypes();
|
|
1653
1655
|
serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
|
|
1654
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
|
+
}
|
|
1655
1736
|
|
|
1656
1737
|
//#endregion
|
|
1657
1738
|
//#region src/server.ts
|
|
@@ -1860,6 +1941,7 @@ var BuiltinAgentsServer = class {
|
|
|
1860
1941
|
async registerPullWakeRunner(pullWake) {
|
|
1861
1942
|
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
1862
1943
|
headers.set(`content-type`, `application/json`);
|
|
1944
|
+
const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
|
|
1863
1945
|
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
1864
1946
|
method: `POST`,
|
|
1865
1947
|
headers,
|
|
@@ -1868,7 +1950,8 @@ var BuiltinAgentsServer = class {
|
|
|
1868
1950
|
owner_principal: pullWake.ownerPrincipal,
|
|
1869
1951
|
label: pullWake.label ?? `Built-in agents`,
|
|
1870
1952
|
kind: `local`,
|
|
1871
|
-
admin_status: `enabled
|
|
1953
|
+
admin_status: `enabled`,
|
|
1954
|
+
sandbox_profiles: profiles
|
|
1872
1955
|
})
|
|
1873
1956
|
});
|
|
1874
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"));
|
|
@@ -788,7 +789,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
788
789
|
wake: {
|
|
789
790
|
on: `runFinished`,
|
|
790
791
|
includeResponse: true
|
|
791
|
-
}
|
|
792
|
+
},
|
|
793
|
+
sandbox: `inherit`
|
|
792
794
|
});
|
|
793
795
|
const workerUrl = handle.entityUrl;
|
|
794
796
|
return {
|
|
@@ -1176,19 +1178,19 @@ function getToolName(tool) {
|
|
|
1176
1178
|
const name = tool.name;
|
|
1177
1179
|
return typeof name === `string` ? name : null;
|
|
1178
1180
|
}
|
|
1179
|
-
function createHortonTools(
|
|
1181
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1180
1182
|
return [
|
|
1181
|
-
(0, __electric_ax_agents_runtime_tools.createBashTool)(
|
|
1182
|
-
(0, __electric_ax_agents_runtime_tools.createReadFileTool)(
|
|
1183
|
-
(0, __electric_ax_agents_runtime_tools.createWriteTool)(
|
|
1184
|
-
(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),
|
|
1185
1187
|
__electric_ax_agents_runtime_tools.braveSearchTool,
|
|
1186
|
-
...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)({
|
|
1188
|
+
...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox, {
|
|
1187
1189
|
catalog: opts.modelCatalog,
|
|
1188
1190
|
modelConfig: opts.modelConfig,
|
|
1189
1191
|
log: (message) => serverLog.info(message),
|
|
1190
1192
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1191
|
-
})] : [__electric_ax_agents_runtime_tools.
|
|
1193
|
+
})] : [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox)],
|
|
1192
1194
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1193
1195
|
(0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1194
1196
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
@@ -1242,11 +1244,10 @@ async function extractFirstUserMessage(ctx) {
|
|
|
1242
1244
|
function messageSeq(message) {
|
|
1243
1245
|
return typeof message._seq === `number` ? message._seq : -1;
|
|
1244
1246
|
}
|
|
1245
|
-
function readAgentsMd(
|
|
1246
|
-
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`);
|
|
1247
1249
|
try {
|
|
1248
|
-
|
|
1249
|
-
const content = node_fs.default.readFileSync(agentsMdPath, `utf8`);
|
|
1250
|
+
const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
|
|
1250
1251
|
return [
|
|
1251
1252
|
`<context_file kind="instructions" path="${agentsMdPath}">`,
|
|
1252
1253
|
content,
|
|
@@ -1257,16 +1258,16 @@ function readAgentsMd(workingDirectory) {
|
|
|
1257
1258
|
}
|
|
1258
1259
|
}
|
|
1259
1260
|
function createAssistantHandler(options) {
|
|
1260
|
-
const {
|
|
1261
|
+
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1261
1262
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1262
1263
|
return async function assistantHandler(ctx, wake) {
|
|
1263
1264
|
const readSet = new Set();
|
|
1264
|
-
const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
|
|
1265
1265
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1266
|
-
const
|
|
1266
|
+
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1267
|
+
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1267
1268
|
const tools = [
|
|
1268
1269
|
...ctx.electricTools,
|
|
1269
|
-
...createHortonTools(
|
|
1270
|
+
...createHortonTools(ctx.sandbox, ctx, readSet, {
|
|
1270
1271
|
docsSearchTool,
|
|
1271
1272
|
modelConfig,
|
|
1272
1273
|
modelCatalog,
|
|
@@ -1357,7 +1358,7 @@ function createAssistantHandler(options) {
|
|
|
1357
1358
|
}
|
|
1358
1359
|
});
|
|
1359
1360
|
ctx.useAgent({
|
|
1360
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1361
|
+
systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
|
|
1361
1362
|
hasDocsSupport: Boolean(docsSupport),
|
|
1362
1363
|
hasSkills,
|
|
1363
1364
|
docsUrl,
|
|
@@ -1385,7 +1386,6 @@ function registerHorton(registry, options) {
|
|
|
1385
1386
|
serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1386
1387
|
});
|
|
1387
1388
|
const assistantHandler = createAssistantHandler({
|
|
1388
|
-
workingDirectory,
|
|
1389
1389
|
streamFn,
|
|
1390
1390
|
docsSupport,
|
|
1391
1391
|
docsSearchTool,
|
|
@@ -1442,26 +1442,26 @@ function parseWorkerArgs(value) {
|
|
|
1442
1442
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1443
1443
|
return args;
|
|
1444
1444
|
}
|
|
1445
|
-
function buildToolsForWorker(tools,
|
|
1445
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1446
1446
|
const out = [];
|
|
1447
1447
|
for (const name of tools) switch (name) {
|
|
1448
1448
|
case `bash`:
|
|
1449
|
-
out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(
|
|
1449
|
+
out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(sandbox));
|
|
1450
1450
|
break;
|
|
1451
1451
|
case `read`:
|
|
1452
|
-
out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(
|
|
1452
|
+
out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(sandbox, readSet));
|
|
1453
1453
|
break;
|
|
1454
1454
|
case `write`:
|
|
1455
|
-
out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(
|
|
1455
|
+
out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(sandbox, readSet));
|
|
1456
1456
|
break;
|
|
1457
1457
|
case `edit`:
|
|
1458
|
-
out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(
|
|
1458
|
+
out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(sandbox, readSet));
|
|
1459
1459
|
break;
|
|
1460
1460
|
case `web_search`:
|
|
1461
1461
|
out.push(__electric_ax_agents_runtime_tools.braveSearchTool);
|
|
1462
1462
|
break;
|
|
1463
1463
|
case `fetch_url`:
|
|
1464
|
-
out.push(__electric_ax_agents_runtime_tools.
|
|
1464
|
+
out.push((0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox));
|
|
1465
1465
|
break;
|
|
1466
1466
|
case `spawn_worker`:
|
|
1467
1467
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1569,13 +1569,13 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
1569
1569
|
return tools;
|
|
1570
1570
|
}
|
|
1571
1571
|
function registerWorker(registry, options) {
|
|
1572
|
-
const {
|
|
1572
|
+
const { streamFn, modelCatalog } = options;
|
|
1573
1573
|
registry.define(`worker`, {
|
|
1574
1574
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1575
1575
|
async handler(ctx) {
|
|
1576
1576
|
const args = parseWorkerArgs(ctx.args);
|
|
1577
1577
|
const readSet = new Set();
|
|
1578
|
-
const builtinTools = buildToolsForWorker(args.tools,
|
|
1578
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1579
1579
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1580
1580
|
const sharedStateTools = [];
|
|
1581
1581
|
if (args.sharedDb) {
|
|
@@ -1654,6 +1654,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1654
1654
|
modelCatalog
|
|
1655
1655
|
});
|
|
1656
1656
|
typeNames.push(`worker`);
|
|
1657
|
+
const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
|
|
1657
1658
|
const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
|
|
1658
1659
|
baseUrl: agentServerUrl,
|
|
1659
1660
|
serveEndpoint,
|
|
@@ -1664,7 +1665,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1664
1665
|
idleTimeout: 5 * 6e4,
|
|
1665
1666
|
createElectricTools: createBuiltinElectricTools(createElectricTools),
|
|
1666
1667
|
publicUrl,
|
|
1667
|
-
name: runtimeName ?? `builtin-agents
|
|
1668
|
+
name: runtimeName ?? `builtin-agents`,
|
|
1669
|
+
sandboxProfiles
|
|
1668
1670
|
});
|
|
1669
1671
|
return {
|
|
1670
1672
|
handler: runtime.onEnter,
|
|
@@ -1688,6 +1690,85 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1688
1690
|
serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
|
|
1689
1691
|
}
|
|
1690
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
|
+
}
|
|
1691
1772
|
|
|
1692
1773
|
//#endregion
|
|
1693
1774
|
//#region src/server.ts
|
|
@@ -1896,6 +1977,7 @@ var BuiltinAgentsServer = class {
|
|
|
1896
1977
|
async registerPullWakeRunner(pullWake) {
|
|
1897
1978
|
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
1898
1979
|
headers.set(`content-type`, `application/json`);
|
|
1980
|
+
const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
|
|
1899
1981
|
const response = await fetch((0, __electric_ax_agents_runtime.appendPathToUrl)(this.options.agentServerUrl, `/_electric/runners`), {
|
|
1900
1982
|
method: `POST`,
|
|
1901
1983
|
headers,
|
|
@@ -1904,7 +1986,8 @@ var BuiltinAgentsServer = class {
|
|
|
1904
1986
|
owner_principal: pullWake.ownerPrincipal,
|
|
1905
1987
|
label: pullWake.label ?? `Built-in agents`,
|
|
1906
1988
|
kind: `local`,
|
|
1907
|
-
admin_status: `enabled
|
|
1989
|
+
admin_status: `enabled`,
|
|
1990
|
+
sandbox_profiles: profiles
|
|
1908
1991
|
})
|
|
1909
1992
|
});
|
|
1910
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";
|
|
@@ -27,7 +28,7 @@ function getLogger() {
|
|
|
27
28
|
try {
|
|
28
29
|
if (USE_FILE_LOGS) {
|
|
29
30
|
const logDir = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(), `logs`);
|
|
30
|
-
|
|
31
|
+
fsSync.mkdirSync(logDir, { recursive: true });
|
|
31
32
|
const logFile = path.join(logDir, `builtin-agents-${Date.now()}.jsonl`);
|
|
32
33
|
streams.push({ stream: pino.destination({
|
|
33
34
|
dest: logFile,
|
|
@@ -161,7 +162,7 @@ function normalizeWhitespace(value) {
|
|
|
161
162
|
}
|
|
162
163
|
async function collectMarkdownFiles(root) {
|
|
163
164
|
async function walk(dir) {
|
|
164
|
-
const entries = await fs
|
|
165
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
165
166
|
const files = [];
|
|
166
167
|
for (const entry of entries) {
|
|
167
168
|
const fullPath = path.join(dir, entry.name);
|
|
@@ -337,7 +338,7 @@ function resolveDocsRoot(workingDirectory) {
|
|
|
337
338
|
requireIndex: false
|
|
338
339
|
}
|
|
339
340
|
].filter((value) => Boolean(value));
|
|
340
|
-
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;
|
|
341
342
|
return null;
|
|
342
343
|
}
|
|
343
344
|
var DocsKnowledgeBase = class {
|
|
@@ -358,7 +359,7 @@ var DocsKnowledgeBase = class {
|
|
|
358
359
|
this.readyPromise = this.ensureIngested();
|
|
359
360
|
}
|
|
360
361
|
openDatabase() {
|
|
361
|
-
|
|
362
|
+
fsSync.mkdirSync(path.dirname(this.dbPath), { recursive: true });
|
|
362
363
|
try {
|
|
363
364
|
const db$1 = new Database(this.dbPath);
|
|
364
365
|
load(db$1);
|
|
@@ -425,11 +426,11 @@ var DocsKnowledgeBase = class {
|
|
|
425
426
|
};
|
|
426
427
|
}
|
|
427
428
|
async ensureIngested() {
|
|
428
|
-
await fs
|
|
429
|
+
await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
|
|
429
430
|
const files = (await collectMarkdownFiles(this.docsRoot)).sort();
|
|
430
431
|
const docs = await Promise.all(files.map(async (filePath) => ({
|
|
431
432
|
path: path.relative(this.docsRoot, filePath),
|
|
432
|
-
content: await fs
|
|
433
|
+
content: await fs.readFile(filePath, `utf8`)
|
|
433
434
|
})));
|
|
434
435
|
const fingerprint = createFingerprint(docs);
|
|
435
436
|
if (!this.db) {
|
|
@@ -764,7 +765,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
|
|
|
764
765
|
wake: {
|
|
765
766
|
on: `runFinished`,
|
|
766
767
|
includeResponse: true
|
|
767
|
-
}
|
|
768
|
+
},
|
|
769
|
+
sandbox: `inherit`
|
|
768
770
|
});
|
|
769
771
|
const workerUrl = handle.entityUrl;
|
|
770
772
|
return {
|
|
@@ -1152,19 +1154,19 @@ function getToolName(tool) {
|
|
|
1152
1154
|
const name = tool.name;
|
|
1153
1155
|
return typeof name === `string` ? name : null;
|
|
1154
1156
|
}
|
|
1155
|
-
function createHortonTools(
|
|
1157
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1156
1158
|
return [
|
|
1157
|
-
createBashTool(
|
|
1158
|
-
createReadFileTool(
|
|
1159
|
-
createWriteTool(
|
|
1160
|
-
createEditTool(
|
|
1159
|
+
createBashTool(sandbox),
|
|
1160
|
+
createReadFileTool(sandbox, readSet),
|
|
1161
|
+
createWriteTool(sandbox, readSet),
|
|
1162
|
+
createEditTool(sandbox, readSet),
|
|
1161
1163
|
braveSearchTool$1,
|
|
1162
|
-
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool({
|
|
1164
|
+
...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool(sandbox, {
|
|
1163
1165
|
catalog: opts.modelCatalog,
|
|
1164
1166
|
modelConfig: opts.modelConfig,
|
|
1165
1167
|
log: (message) => serverLog.info(message),
|
|
1166
1168
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1167
|
-
})] : [
|
|
1169
|
+
})] : [createFetchUrlTool(sandbox)],
|
|
1168
1170
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1169
1171
|
createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
|
|
1170
1172
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
@@ -1218,11 +1220,10 @@ async function extractFirstUserMessage(ctx) {
|
|
|
1218
1220
|
function messageSeq(message) {
|
|
1219
1221
|
return typeof message._seq === `number` ? message._seq : -1;
|
|
1220
1222
|
}
|
|
1221
|
-
function readAgentsMd(
|
|
1222
|
-
const agentsMdPath = path.join(workingDirectory, `AGENTS.md`);
|
|
1223
|
+
async function readAgentsMd(sandbox) {
|
|
1224
|
+
const agentsMdPath = path.posix.join(sandbox.workingDirectory, `AGENTS.md`);
|
|
1223
1225
|
try {
|
|
1224
|
-
|
|
1225
|
-
const content = fs.readFileSync(agentsMdPath, `utf8`);
|
|
1226
|
+
const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
|
|
1226
1227
|
return [
|
|
1227
1228
|
`<context_file kind="instructions" path="${agentsMdPath}">`,
|
|
1228
1229
|
content,
|
|
@@ -1233,16 +1234,16 @@ function readAgentsMd(workingDirectory) {
|
|
|
1233
1234
|
}
|
|
1234
1235
|
}
|
|
1235
1236
|
function createAssistantHandler(options) {
|
|
1236
|
-
const {
|
|
1237
|
+
const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
|
|
1237
1238
|
const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
|
|
1238
1239
|
return async function assistantHandler(ctx, wake) {
|
|
1239
1240
|
const readSet = new Set();
|
|
1240
|
-
const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
|
|
1241
1241
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
|
|
1242
|
-
const
|
|
1242
|
+
const sandboxCwd = ctx.sandbox.workingDirectory;
|
|
1243
|
+
const agentsMd = await readAgentsMd(ctx.sandbox);
|
|
1243
1244
|
const tools = [
|
|
1244
1245
|
...ctx.electricTools,
|
|
1245
|
-
...createHortonTools(
|
|
1246
|
+
...createHortonTools(ctx.sandbox, ctx, readSet, {
|
|
1246
1247
|
docsSearchTool,
|
|
1247
1248
|
modelConfig,
|
|
1248
1249
|
modelCatalog,
|
|
@@ -1333,7 +1334,7 @@ function createAssistantHandler(options) {
|
|
|
1333
1334
|
}
|
|
1334
1335
|
});
|
|
1335
1336
|
ctx.useAgent({
|
|
1336
|
-
systemPrompt: buildHortonSystemPrompt(
|
|
1337
|
+
systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
|
|
1337
1338
|
hasDocsSupport: Boolean(docsSupport),
|
|
1338
1339
|
hasSkills,
|
|
1339
1340
|
docsUrl,
|
|
@@ -1361,7 +1362,6 @@ function registerHorton(registry, options) {
|
|
|
1361
1362
|
serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1362
1363
|
});
|
|
1363
1364
|
const assistantHandler = createAssistantHandler({
|
|
1364
|
-
workingDirectory,
|
|
1365
1365
|
streamFn,
|
|
1366
1366
|
docsSupport,
|
|
1367
1367
|
docsSearchTool,
|
|
@@ -1418,26 +1418,26 @@ function parseWorkerArgs(value) {
|
|
|
1418
1418
|
if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
|
|
1419
1419
|
return args;
|
|
1420
1420
|
}
|
|
1421
|
-
function buildToolsForWorker(tools,
|
|
1421
|
+
function buildToolsForWorker(tools, sandbox, ctx, readSet) {
|
|
1422
1422
|
const out = [];
|
|
1423
1423
|
for (const name of tools) switch (name) {
|
|
1424
1424
|
case `bash`:
|
|
1425
|
-
out.push(createBashTool(
|
|
1425
|
+
out.push(createBashTool(sandbox));
|
|
1426
1426
|
break;
|
|
1427
1427
|
case `read`:
|
|
1428
|
-
out.push(createReadFileTool(
|
|
1428
|
+
out.push(createReadFileTool(sandbox, readSet));
|
|
1429
1429
|
break;
|
|
1430
1430
|
case `write`:
|
|
1431
|
-
out.push(createWriteTool(
|
|
1431
|
+
out.push(createWriteTool(sandbox, readSet));
|
|
1432
1432
|
break;
|
|
1433
1433
|
case `edit`:
|
|
1434
|
-
out.push(createEditTool(
|
|
1434
|
+
out.push(createEditTool(sandbox, readSet));
|
|
1435
1435
|
break;
|
|
1436
1436
|
case `web_search`:
|
|
1437
1437
|
out.push(braveSearchTool$1);
|
|
1438
1438
|
break;
|
|
1439
1439
|
case `fetch_url`:
|
|
1440
|
-
out.push(
|
|
1440
|
+
out.push(createFetchUrlTool(sandbox));
|
|
1441
1441
|
break;
|
|
1442
1442
|
case `spawn_worker`:
|
|
1443
1443
|
out.push(createSpawnWorkerTool(ctx));
|
|
@@ -1545,13 +1545,13 @@ function buildSharedStateTools(shared, schema, mode) {
|
|
|
1545
1545
|
return tools;
|
|
1546
1546
|
}
|
|
1547
1547
|
function registerWorker(registry, options) {
|
|
1548
|
-
const {
|
|
1548
|
+
const { streamFn, modelCatalog } = options;
|
|
1549
1549
|
registry.define(`worker`, {
|
|
1550
1550
|
description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
|
|
1551
1551
|
async handler(ctx) {
|
|
1552
1552
|
const args = parseWorkerArgs(ctx.args);
|
|
1553
1553
|
const readSet = new Set();
|
|
1554
|
-
const builtinTools = buildToolsForWorker(args.tools,
|
|
1554
|
+
const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet);
|
|
1555
1555
|
const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
|
|
1556
1556
|
const sharedStateTools = [];
|
|
1557
1557
|
if (args.sharedDb) {
|
|
@@ -1630,6 +1630,7 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1630
1630
|
modelCatalog
|
|
1631
1631
|
});
|
|
1632
1632
|
typeNames.push(`worker`);
|
|
1633
|
+
const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
|
|
1633
1634
|
const runtime = createRuntimeHandler({
|
|
1634
1635
|
baseUrl: agentServerUrl,
|
|
1635
1636
|
serveEndpoint,
|
|
@@ -1640,7 +1641,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1640
1641
|
idleTimeout: 5 * 6e4,
|
|
1641
1642
|
createElectricTools: createBuiltinElectricTools(createElectricTools),
|
|
1642
1643
|
publicUrl,
|
|
1643
|
-
name: runtimeName ?? `builtin-agents
|
|
1644
|
+
name: runtimeName ?? `builtin-agents`,
|
|
1645
|
+
sandboxProfiles
|
|
1644
1646
|
});
|
|
1645
1647
|
return {
|
|
1646
1648
|
handler: runtime.onEnter,
|
|
@@ -1664,6 +1666,85 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1664
1666
|
serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
|
|
1665
1667
|
}
|
|
1666
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
|
+
}
|
|
1667
1748
|
|
|
1668
1749
|
//#endregion
|
|
1669
1750
|
//#region src/server.ts
|
|
@@ -1872,6 +1953,7 @@ var BuiltinAgentsServer = class {
|
|
|
1872
1953
|
async registerPullWakeRunner(pullWake) {
|
|
1873
1954
|
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
1874
1955
|
headers.set(`content-type`, `application/json`);
|
|
1956
|
+
const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
|
|
1875
1957
|
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
1876
1958
|
method: `POST`,
|
|
1877
1959
|
headers,
|
|
@@ -1880,7 +1962,8 @@ var BuiltinAgentsServer = class {
|
|
|
1880
1962
|
owner_principal: pullWake.ownerPrincipal,
|
|
1881
1963
|
label: pullWake.label ?? `Built-in agents`,
|
|
1882
1964
|
kind: `local`,
|
|
1883
|
-
admin_status: `enabled
|
|
1965
|
+
admin_status: `enabled`,
|
|
1966
|
+
sandbox_profiles: profiles
|
|
1884
1967
|
})
|
|
1885
1968
|
});
|
|
1886
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",
|