@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.
@@ -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, fetchUrlTool } from "@electric-ax/agents-runtime/tools";
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
- const streams = [{ stream: pino.destination({
26
- dest: LOG_FILE,
27
- sync: IS_ELECTRON_MAIN
28
- }) }];
29
- if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
30
- target: `pino-pretty`,
31
- options: {
32
- colorize: true,
33
- ignore: `pid,hostname,name`,
34
- translateTime: `SYS:HH:MM:ss`
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
- const logger = pino({
38
- base: void 0,
39
- level: LOG_LEVEL
40
- }, pino.multistream(streams));
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
- logger.debug(msg);
74
+ getLogger().debug(msg);
55
75
  },
56
76
  info(...args) {
57
77
  const { msg } = formatArgs(args);
58
- logger.info(msg);
78
+ getLogger().info(msg);
59
79
  },
60
80
  warn(...args) {
61
81
  const { err, msg } = formatArgs(args);
62
- if (err) logger.warn({ err }, msg);
63
- else logger.warn(msg);
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) logger.error({ err }, msg);
68
- else logger.error(msg);
87
+ if (err) getLogger().error({ err }, msg);
88
+ else getLogger().error(msg);
69
89
  },
70
90
  event(obj, msg) {
71
- logger.info(obj, msg);
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(workingDirectory, ctx, readSet, opts = {}) {
1156
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1136
1157
  return [
1137
- createBashTool(workingDirectory),
1138
- createReadFileTool(workingDirectory, readSet),
1139
- createWriteTool(workingDirectory, readSet),
1140
- createEditTool(workingDirectory, readSet),
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
- })] : [fetchUrlTool],
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(workingDirectory) {
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
- if (!fs.existsSync(agentsMdPath) || !fs.statSync(agentsMdPath).isFile()) return null;
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 { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
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 agentsMd = readAgentsMd(effectiveCwd);
1241
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1242
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1223
1243
  const tools = [
1224
1244
  ...ctx.electricTools,
1225
- ...createHortonTools(effectiveCwd, ctx, readSet, {
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(effectiveCwd, {
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, workingDirectory, ctx, readSet) {
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(workingDirectory));
1424
+ out.push(createBashTool(sandbox));
1406
1425
  break;
1407
1426
  case `read`:
1408
- out.push(createReadFileTool(workingDirectory, readSet));
1427
+ out.push(createReadFileTool(sandbox, readSet));
1409
1428
  break;
1410
1429
  case `write`:
1411
- out.push(createWriteTool(workingDirectory, readSet));
1430
+ out.push(createWriteTool(sandbox, readSet));
1412
1431
  break;
1413
1432
  case `edit`:
1414
- out.push(createEditTool(workingDirectory, readSet));
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(fetchUrlTool);
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 { workingDirectory, streamFn, modelCatalog } = options;
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, workingDirectory, ctx, readSet);
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
- const streams = [{ stream: pino.default.destination({
50
- dest: LOG_FILE,
51
- sync: IS_ELECTRON_MAIN
52
- }) }];
53
- if (USE_PRETTY_LOGS) streams.push({ stream: pino.default.transport({
54
- target: `pino-pretty`,
55
- options: {
56
- colorize: true,
57
- ignore: `pid,hostname,name`,
58
- translateTime: `SYS:HH:MM:ss`
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
- const logger = (0, pino.default)({
62
- base: void 0,
63
- level: LOG_LEVEL
64
- }, pino.default.multistream(streams));
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
- logger.debug(msg);
98
+ getLogger().debug(msg);
79
99
  },
80
100
  info(...args) {
81
101
  const { msg } = formatArgs(args);
82
- logger.info(msg);
102
+ getLogger().info(msg);
83
103
  },
84
104
  warn(...args) {
85
105
  const { err, msg } = formatArgs(args);
86
- if (err) logger.warn({ err }, msg);
87
- else logger.warn(msg);
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) logger.error({ err }, msg);
92
- else logger.error(msg);
111
+ if (err) getLogger().error({ err }, msg);
112
+ else getLogger().error(msg);
93
113
  },
94
114
  event(obj, msg) {
95
- logger.info(obj, msg);
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(workingDirectory, ctx, readSet, opts = {}) {
1181
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1161
1182
  return [
1162
- (0, __electric_ax_agents_runtime_tools.createBashTool)(workingDirectory),
1163
- (0, __electric_ax_agents_runtime_tools.createReadFileTool)(workingDirectory, readSet),
1164
- (0, __electric_ax_agents_runtime_tools.createWriteTool)(workingDirectory, readSet),
1165
- (0, __electric_ax_agents_runtime_tools.createEditTool)(workingDirectory, readSet),
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.fetchUrlTool],
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(workingDirectory) {
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
- if (!node_fs.default.existsSync(agentsMdPath) || !node_fs.default.statSync(agentsMdPath).isFile()) return null;
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 { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
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 agentsMd = readAgentsMd(effectiveCwd);
1266
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1267
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1248
1268
  const tools = [
1249
1269
  ...ctx.electricTools,
1250
- ...createHortonTools(effectiveCwd, ctx, readSet, {
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(effectiveCwd, {
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, workingDirectory, ctx, readSet) {
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)(workingDirectory));
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)(workingDirectory, readSet));
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)(workingDirectory, readSet));
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)(workingDirectory, readSet));
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.fetchUrlTool);
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 { workingDirectory, streamFn, modelCatalog } = options;
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, workingDirectory, ctx, readSet);
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(workingDirectory: string, ctx: HandlerContext, readSet: Set<string>, opts?: {
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(workingDirectory: string, ctx: HandlerContext, readSet: Set<string>, opts?: {
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, fetchUrlTool } from "@electric-ax/agents-runtime/tools";
6
- import fs from "node:fs";
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$1 from "node:fs/promises";
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
- const streams = [{ stream: pino.destination({
26
- dest: LOG_FILE,
27
- sync: IS_ELECTRON_MAIN
28
- }) }];
29
- if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
30
- target: `pino-pretty`,
31
- options: {
32
- colorize: true,
33
- ignore: `pid,hostname,name`,
34
- translateTime: `SYS:HH:MM:ss`
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
- const logger = pino({
38
- base: void 0,
39
- level: LOG_LEVEL
40
- }, pino.multistream(streams));
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
- logger.debug(msg);
74
+ getLogger().debug(msg);
55
75
  },
56
76
  info(...args) {
57
77
  const { msg } = formatArgs(args);
58
- logger.info(msg);
78
+ getLogger().info(msg);
59
79
  },
60
80
  warn(...args) {
61
81
  const { err, msg } = formatArgs(args);
62
- if (err) logger.warn({ err }, msg);
63
- else logger.warn(msg);
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) logger.error({ err }, msg);
68
- else logger.error(msg);
87
+ if (err) getLogger().error({ err }, msg);
88
+ else getLogger().error(msg);
69
89
  },
70
90
  event(obj, msg) {
71
- logger.info(obj, msg);
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$1.readdir(dir, { withFileTypes: true });
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 (fs.existsSync(candidate.path) && (!candidate.requireIndex || fs.existsSync(path.join(candidate.path, `index.md`)))) return candidate.path;
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
- fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
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$1.mkdir(path.dirname(this.dbPath), { recursive: true });
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$1.readFile(filePath, `utf8`)
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(workingDirectory, ctx, readSet, opts = {}) {
1157
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1137
1158
  return [
1138
- createBashTool(workingDirectory),
1139
- createReadFileTool(workingDirectory, readSet),
1140
- createWriteTool(workingDirectory, readSet),
1141
- createEditTool(workingDirectory, readSet),
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
- })] : [fetchUrlTool],
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(workingDirectory) {
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
- if (!fs.existsSync(agentsMdPath) || !fs.statSync(agentsMdPath).isFile()) return null;
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 { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
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 agentsMd = readAgentsMd(effectiveCwd);
1242
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1243
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1224
1244
  const tools = [
1225
1245
  ...ctx.electricTools,
1226
- ...createHortonTools(effectiveCwd, ctx, readSet, {
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(effectiveCwd, {
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, workingDirectory, ctx, readSet) {
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(workingDirectory));
1425
+ out.push(createBashTool(sandbox));
1407
1426
  break;
1408
1427
  case `read`:
1409
- out.push(createReadFileTool(workingDirectory, readSet));
1428
+ out.push(createReadFileTool(sandbox, readSet));
1410
1429
  break;
1411
1430
  case `write`:
1412
- out.push(createWriteTool(workingDirectory, readSet));
1431
+ out.push(createWriteTool(sandbox, readSet));
1413
1432
  break;
1414
1433
  case `edit`:
1415
- out.push(createEditTool(workingDirectory, readSet));
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(fetchUrlTool);
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 { workingDirectory, streamFn, modelCatalog } = options;
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, workingDirectory, ctx, readSet);
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.10",
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.6"
52
+ "@electric-ax/agents-runtime": "0.3.8"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/better-sqlite3": "^7.6.13",