@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.
@@ -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";
@@ -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(workingDirectory, ctx, readSet, opts = {}) {
1156
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1155
1157
  return [
1156
- createBashTool(workingDirectory),
1157
- createReadFileTool(workingDirectory, readSet),
1158
- createWriteTool(workingDirectory, readSet),
1159
- createEditTool(workingDirectory, readSet),
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
- })] : [fetchUrlTool],
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(workingDirectory) {
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
- if (!fs.existsSync(agentsMdPath) || !fs.statSync(agentsMdPath).isFile()) return null;
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 { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
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 agentsMd = readAgentsMd(effectiveCwd);
1241
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1242
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1242
1243
  const tools = [
1243
1244
  ...ctx.electricTools,
1244
- ...createHortonTools(effectiveCwd, ctx, readSet, {
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(effectiveCwd, {
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, workingDirectory, ctx, readSet) {
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(workingDirectory));
1424
+ out.push(createBashTool(sandbox));
1425
1425
  break;
1426
1426
  case `read`:
1427
- out.push(createReadFileTool(workingDirectory, readSet));
1427
+ out.push(createReadFileTool(sandbox, readSet));
1428
1428
  break;
1429
1429
  case `write`:
1430
- out.push(createWriteTool(workingDirectory, readSet));
1430
+ out.push(createWriteTool(sandbox, readSet));
1431
1431
  break;
1432
1432
  case `edit`:
1433
- out.push(createEditTool(workingDirectory, readSet));
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(fetchUrlTool);
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 { workingDirectory, streamFn, modelCatalog } = options;
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, workingDirectory, ctx, readSet);
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(workingDirectory, ctx, readSet, opts = {}) {
1181
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1180
1182
  return [
1181
- (0, __electric_ax_agents_runtime_tools.createBashTool)(workingDirectory),
1182
- (0, __electric_ax_agents_runtime_tools.createReadFileTool)(workingDirectory, readSet),
1183
- (0, __electric_ax_agents_runtime_tools.createWriteTool)(workingDirectory, readSet),
1184
- (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),
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.fetchUrlTool],
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(workingDirectory) {
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
- if (!node_fs.default.existsSync(agentsMdPath) || !node_fs.default.statSync(agentsMdPath).isFile()) return null;
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 { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
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 agentsMd = readAgentsMd(effectiveCwd);
1266
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1267
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1267
1268
  const tools = [
1268
1269
  ...ctx.electricTools,
1269
- ...createHortonTools(effectiveCwd, ctx, readSet, {
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(effectiveCwd, {
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, workingDirectory, ctx, readSet) {
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)(workingDirectory));
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)(workingDirectory, readSet));
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)(workingDirectory, readSet));
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)(workingDirectory, readSet));
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.fetchUrlTool);
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 { workingDirectory, streamFn, modelCatalog } = options;
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, workingDirectory, ctx, readSet);
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(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";
@@ -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
- fs.mkdirSync(logDir, { recursive: true });
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$1.readdir(dir, { withFileTypes: true });
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 (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;
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
- fs.mkdirSync(path.dirname(this.dbPath), { recursive: true });
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$1.mkdir(path.dirname(this.dbPath), { recursive: true });
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$1.readFile(filePath, `utf8`)
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(workingDirectory, ctx, readSet, opts = {}) {
1157
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1156
1158
  return [
1157
- createBashTool(workingDirectory),
1158
- createReadFileTool(workingDirectory, readSet),
1159
- createWriteTool(workingDirectory, readSet),
1160
- createEditTool(workingDirectory, readSet),
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
- })] : [fetchUrlTool],
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(workingDirectory) {
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
- if (!fs.existsSync(agentsMdPath) || !fs.statSync(agentsMdPath).isFile()) return null;
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 { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
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 agentsMd = readAgentsMd(effectiveCwd);
1242
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1243
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1243
1244
  const tools = [
1244
1245
  ...ctx.electricTools,
1245
- ...createHortonTools(effectiveCwd, ctx, readSet, {
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(effectiveCwd, {
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, workingDirectory, ctx, readSet) {
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(workingDirectory));
1425
+ out.push(createBashTool(sandbox));
1426
1426
  break;
1427
1427
  case `read`:
1428
- out.push(createReadFileTool(workingDirectory, readSet));
1428
+ out.push(createReadFileTool(sandbox, readSet));
1429
1429
  break;
1430
1430
  case `write`:
1431
- out.push(createWriteTool(workingDirectory, readSet));
1431
+ out.push(createWriteTool(sandbox, readSet));
1432
1432
  break;
1433
1433
  case `edit`:
1434
- out.push(createEditTool(workingDirectory, readSet));
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(fetchUrlTool);
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 { workingDirectory, streamFn, modelCatalog } = options;
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, workingDirectory, ctx, readSet);
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.11",
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.7"
52
+ "@electric-ax/agents-runtime": "0.3.8"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/better-sqlite3": "^7.6.13",