@electric-ax/agents 0.4.11 → 0.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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"));
@@ -38,6 +39,7 @@ const sqlite_vec = __toESM(require("sqlite-vec"));
38
39
  const nanoid = __toESM(require("nanoid"));
39
40
  const __mariozechner_pi_ai = __toESM(require("@mariozechner/pi-ai"));
40
41
  const __electric_ax_agents_mcp = __toESM(require("@electric-ax/agents-mcp"));
42
+ const undici = __toESM(require("undici"));
41
43
 
42
44
  //#region src/log.ts
43
45
  const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
@@ -788,7 +790,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
788
790
  wake: {
789
791
  on: `runFinished`,
790
792
  includeResponse: true
791
- }
793
+ },
794
+ sandbox: `inherit`
792
795
  });
793
796
  const workerUrl = handle.entityUrl;
794
797
  return {
@@ -887,6 +890,15 @@ async function fetchAvailableModelIds(provider) {
887
890
  function knownModelsForProvider(provider) {
888
891
  return provider === __electric_ax_agents_runtime.MOONSHOT_PROVIDER ? (0, __electric_ax_agents_runtime.getMoonshotModels)() : (0, __mariozechner_pi_ai.getModels)(provider);
889
892
  }
893
+ function resolveBuiltinModelContextWindow(modelConfig) {
894
+ const modelId = String(modelConfig.model);
895
+ if (modelConfig.provider === __electric_ax_agents_runtime.MOONSHOT_PROVIDER) return (0, __electric_ax_agents_runtime.getMoonshotModel)(modelId)?.contextWindow ?? null;
896
+ if (!modelConfig.provider) return null;
897
+ return knownModelsForProvider(modelConfig.provider).find((model) => model.id === modelId)?.contextWindow ?? null;
898
+ }
899
+ function resolveBuiltinModelSourceBudget(modelConfig) {
900
+ return resolveBuiltinModelContextWindow(modelConfig) ?? 1e5;
901
+ }
890
902
  function choiceForKnownModel(provider, model) {
891
903
  return {
892
904
  provider,
@@ -1176,19 +1188,19 @@ function getToolName(tool) {
1176
1188
  const name = tool.name;
1177
1189
  return typeof name === `string` ? name : null;
1178
1190
  }
1179
- function createHortonTools(workingDirectory, ctx, readSet, opts = {}) {
1191
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1180
1192
  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),
1193
+ (0, __electric_ax_agents_runtime_tools.createBashTool)(sandbox),
1194
+ (0, __electric_ax_agents_runtime_tools.createReadFileTool)(sandbox, readSet),
1195
+ (0, __electric_ax_agents_runtime_tools.createWriteTool)(sandbox, readSet),
1196
+ (0, __electric_ax_agents_runtime_tools.createEditTool)(sandbox, readSet),
1185
1197
  __electric_ax_agents_runtime_tools.braveSearchTool,
1186
- ...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)({
1198
+ ...opts.modelCatalog && opts.modelConfig ? [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox, {
1187
1199
  catalog: opts.modelCatalog,
1188
1200
  modelConfig: opts.modelConfig,
1189
1201
  log: (message) => serverLog.info(message),
1190
1202
  logPrefix: opts.logPrefix ?? `[horton]`
1191
- })] : [__electric_ax_agents_runtime_tools.fetchUrlTool],
1203
+ })] : [(0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox)],
1192
1204
  createSpawnWorkerTool(ctx, opts.modelConfig),
1193
1205
  (0, __electric_ax_agents_runtime_tools.createSendTool)(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1194
1206
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
@@ -1242,11 +1254,10 @@ async function extractFirstUserMessage(ctx) {
1242
1254
  function messageSeq(message) {
1243
1255
  return typeof message._seq === `number` ? message._seq : -1;
1244
1256
  }
1245
- function readAgentsMd(workingDirectory) {
1246
- const agentsMdPath = node_path.default.join(workingDirectory, `AGENTS.md`);
1257
+ async function readAgentsMd(sandbox) {
1258
+ const agentsMdPath = node_path.default.posix.join(sandbox.workingDirectory, `AGENTS.md`);
1247
1259
  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`);
1260
+ const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
1250
1261
  return [
1251
1262
  `<context_file kind="instructions" path="${agentsMdPath}">`,
1252
1263
  content,
@@ -1257,16 +1268,17 @@ function readAgentsMd(workingDirectory) {
1257
1268
  }
1258
1269
  }
1259
1270
  function createAssistantHandler(options) {
1260
- const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
1271
+ const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
1261
1272
  const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
1262
1273
  return async function assistantHandler(ctx, wake) {
1263
1274
  const readSet = new Set();
1264
- const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
1265
1275
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
1266
- const agentsMd = readAgentsMd(effectiveCwd);
1276
+ const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
1277
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1278
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1267
1279
  const tools = [
1268
1280
  ...ctx.electricTools,
1269
- ...createHortonTools(effectiveCwd, ctx, readSet, {
1281
+ ...createHortonTools(ctx.sandbox, ctx, readSet, {
1270
1282
  docsSearchTool,
1271
1283
  modelConfig,
1272
1284
  modelCatalog,
@@ -1295,7 +1307,7 @@ function createAssistantHandler(options) {
1295
1307
  }
1296
1308
  })() : Promise.resolve();
1297
1309
  if (docsSupport) ctx.useContext({
1298
- sourceBudget: 1e5,
1310
+ sourceBudget,
1299
1311
  sources: {
1300
1312
  docs_toc: {
1301
1313
  content: () => docsSupport.renderCompressedToc(),
@@ -1324,7 +1336,7 @@ function createAssistantHandler(options) {
1324
1336
  }
1325
1337
  });
1326
1338
  else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
1327
- sourceBudget: 1e5,
1339
+ sourceBudget,
1328
1340
  sources: {
1329
1341
  skills_catalog: {
1330
1342
  content: () => skillsRegistry.renderCatalog(2e3),
@@ -1343,7 +1355,7 @@ function createAssistantHandler(options) {
1343
1355
  }
1344
1356
  });
1345
1357
  else if (agentsMd) ctx.useContext({
1346
- sourceBudget: 1e5,
1358
+ sourceBudget,
1347
1359
  sources: {
1348
1360
  conversation: {
1349
1361
  content: () => ctx.timelineMessages(),
@@ -1357,7 +1369,7 @@ function createAssistantHandler(options) {
1357
1369
  }
1358
1370
  });
1359
1371
  ctx.useAgent({
1360
- systemPrompt: buildHortonSystemPrompt(effectiveCwd, {
1372
+ systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
1361
1373
  hasDocsSupport: Boolean(docsSupport),
1362
1374
  hasSkills,
1363
1375
  docsUrl,
@@ -1385,7 +1397,6 @@ function registerHorton(registry, options) {
1385
1397
  serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
1386
1398
  });
1387
1399
  const assistantHandler = createAssistantHandler({
1388
- workingDirectory,
1389
1400
  streamFn,
1390
1401
  docsSupport,
1391
1402
  docsSearchTool,
@@ -1401,6 +1412,15 @@ function registerHorton(registry, options) {
1401
1412
  registry.define(`horton`, {
1402
1413
  description: `Friendly capable assistant — chat, code, research, dispatch`,
1403
1414
  creationSchema: hortonCreationSchema,
1415
+ permissionGrants: [{
1416
+ subject_kind: `principal_kind`,
1417
+ subject_value: `user`,
1418
+ permission: `spawn`
1419
+ }, {
1420
+ subject_kind: `principal_kind`,
1421
+ subject_value: `user`,
1422
+ permission: `manage`
1423
+ }],
1404
1424
  handler: assistantHandler
1405
1425
  });
1406
1426
  return [`horton`];
@@ -1442,26 +1462,29 @@ function parseWorkerArgs(value) {
1442
1462
  if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
1443
1463
  return args;
1444
1464
  }
1445
- function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
1465
+ function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
1446
1466
  const out = [];
1447
1467
  for (const name of tools) switch (name) {
1448
1468
  case `bash`:
1449
- out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(workingDirectory));
1469
+ out.push((0, __electric_ax_agents_runtime_tools.createBashTool)(sandbox));
1450
1470
  break;
1451
1471
  case `read`:
1452
- out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(workingDirectory, readSet));
1472
+ out.push((0, __electric_ax_agents_runtime_tools.createReadFileTool)(sandbox, readSet));
1453
1473
  break;
1454
1474
  case `write`:
1455
- out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(workingDirectory, readSet));
1475
+ out.push((0, __electric_ax_agents_runtime_tools.createWriteTool)(sandbox, readSet));
1456
1476
  break;
1457
1477
  case `edit`:
1458
- out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(workingDirectory, readSet));
1478
+ out.push((0, __electric_ax_agents_runtime_tools.createEditTool)(sandbox, readSet));
1459
1479
  break;
1460
1480
  case `web_search`:
1461
1481
  out.push(__electric_ax_agents_runtime_tools.braveSearchTool);
1462
1482
  break;
1463
1483
  case `fetch_url`:
1464
- out.push(__electric_ax_agents_runtime_tools.fetchUrlTool);
1484
+ out.push((0, __electric_ax_agents_runtime_tools.createFetchUrlTool)(sandbox, {
1485
+ catalog: opts.modelCatalog,
1486
+ modelConfig: opts.modelConfig
1487
+ }));
1465
1488
  break;
1466
1489
  case `spawn_worker`:
1467
1490
  out.push(createSpawnWorkerTool(ctx));
@@ -1569,14 +1592,26 @@ function buildSharedStateTools(shared, schema, mode) {
1569
1592
  return tools;
1570
1593
  }
1571
1594
  function registerWorker(registry, options) {
1572
- const { workingDirectory, streamFn, modelCatalog } = options;
1595
+ const { streamFn, modelCatalog } = options;
1573
1596
  registry.define(`worker`, {
1574
1597
  description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
1598
+ permissionGrants: [{
1599
+ subject_kind: `principal_kind`,
1600
+ subject_value: `user`,
1601
+ permission: `spawn`
1602
+ }, {
1603
+ subject_kind: `principal_kind`,
1604
+ subject_value: `user`,
1605
+ permission: `manage`
1606
+ }],
1575
1607
  async handler(ctx) {
1576
1608
  const args = parseWorkerArgs(ctx.args);
1577
1609
  const readSet = new Set();
1578
- const builtinTools = buildToolsForWorker(args.tools, workingDirectory, ctx, readSet);
1579
1610
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
1611
+ const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
1612
+ modelCatalog,
1613
+ modelConfig
1614
+ });
1580
1615
  const sharedStateTools = [];
1581
1616
  if (args.sharedDb) {
1582
1617
  const shared = await ctx.observe((0, __electric_ax_agents_runtime.db)(args.sharedDb.id, args.sharedDb.schema));
@@ -1654,6 +1689,7 @@ async function createBuiltinAgentHandler(options) {
1654
1689
  modelCatalog
1655
1690
  });
1656
1691
  typeNames.push(`worker`);
1692
+ const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
1657
1693
  const runtime = (0, __electric_ax_agents_runtime.createRuntimeHandler)({
1658
1694
  baseUrl: agentServerUrl,
1659
1695
  serveEndpoint,
@@ -1664,7 +1700,8 @@ async function createBuiltinAgentHandler(options) {
1664
1700
  idleTimeout: 5 * 6e4,
1665
1701
  createElectricTools: createBuiltinElectricTools(createElectricTools),
1666
1702
  publicUrl,
1667
- name: runtimeName ?? `builtin-agents`
1703
+ name: runtimeName ?? `builtin-agents`,
1704
+ sandboxProfiles
1668
1705
  });
1669
1706
  return {
1670
1707
  handler: runtime.onEnter,
@@ -1688,6 +1725,97 @@ async function registerBuiltinAgentTypes(bootstrap) {
1688
1725
  serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
1689
1726
  }
1690
1727
  const registerAgentTypes = registerBuiltinAgentTypes;
1728
+ /**
1729
+ * Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
1730
+ * re-run the boot sweep.
1731
+ */
1732
+ let dockerSweptOnBoot = false;
1733
+ function sweepOrphanedDockerSandboxesOnce(sweep) {
1734
+ if (dockerSweptOnBoot) return;
1735
+ dockerSweptOnBoot = true;
1736
+ sweep().then((removed) => {
1737
+ if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
1738
+ }).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
1739
+ }
1740
+ /**
1741
+ * Built-in sandbox profiles. `local` is always available. `docker` is
1742
+ * gated on Docker being reachable so a user without Docker installed
1743
+ * sees only what works — the UI never offers a non-functional choice.
1744
+ */
1745
+ async function buildBuiltinSandboxProfiles(workingDirectory) {
1746
+ const profiles = [{
1747
+ name: `local`,
1748
+ label: `Local`,
1749
+ description: `Runs on the host without isolation. Full filesystem access.`,
1750
+ factory: ({ args }) => (0, __electric_ax_agents_runtime_sandbox.chooseDefaultSandbox)(resolveCwd(args, workingDirectory))
1751
+ }];
1752
+ try {
1753
+ const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1754
+ if (await isDockerAvailable()) {
1755
+ const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1756
+ sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1757
+ profiles.push({
1758
+ name: `docker`,
1759
+ label: `Docker`,
1760
+ 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).`,
1761
+ factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1762
+ const cwd = readWorkingDirectoryArg(args);
1763
+ return dockerSandbox({
1764
+ initialNetworkPolicy: { mode: `allow-all` },
1765
+ extraMounts: cwd ? [{
1766
+ hostPath: cwd,
1767
+ containerPath: `/work`,
1768
+ readOnly: false
1769
+ }] : void 0,
1770
+ sandboxKey,
1771
+ persistent,
1772
+ owner,
1773
+ entityType,
1774
+ entityUrl
1775
+ });
1776
+ }
1777
+ });
1778
+ } else serverLog.info(`[builtin-agents] docker daemon not reachable — docker sandbox profile not registered`);
1779
+ } catch (err) {
1780
+ serverLog.warn(`[builtin-agents] failed to probe docker availability: ${err instanceof Error ? err.message : String(err)}`);
1781
+ }
1782
+ if (process.env.E2B_API_KEY) if (await (0, __electric_ax_agents_runtime_sandbox.isE2BAvailable)()) profiles.push({
1783
+ name: `e2b`,
1784
+ label: `E2B`,
1785
+ description: `Runs in a remote E2B microVM. Persistent sandboxes survive across wakes and are reachable from any runner.`,
1786
+ remote: true,
1787
+ factory: ({ sandboxKey, persistent, owner }) => (0, __electric_ax_agents_runtime_sandbox.remoteSandbox)({
1788
+ provider: `e2b`,
1789
+ apiKey: process.env.E2B_API_KEY,
1790
+ sandboxKey,
1791
+ persistent,
1792
+ owner,
1793
+ initialNetworkPolicy: { mode: `allow-all` }
1794
+ })
1795
+ });
1796
+ else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
1797
+ console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
1798
+ return profiles;
1799
+ }
1800
+ function readWorkingDirectoryArg(args) {
1801
+ const v = args.workingDirectory;
1802
+ return typeof v === `string` && v.trim().length > 0 ? v : null;
1803
+ }
1804
+ function resolveCwd(args, fallback) {
1805
+ return readWorkingDirectoryArg(args) ?? fallback;
1806
+ }
1807
+
1808
+ //#endregion
1809
+ //#region src/durable-streams-cache.ts
1810
+ const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
1811
+ function installDurableStreamsFetchCache(options = {}) {
1812
+ if (options === false) return;
1813
+ const store = options.store === `sqlite` || options.sqliteLocation ? new undici.cacheStores.SqliteCacheStore({
1814
+ location: options.sqliteLocation,
1815
+ maxCount: options.maxCount
1816
+ }) : new undici.cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
1817
+ (0, undici.setGlobalDispatcher)(new undici.Agent().compose(undici.interceptors.cache({ store })));
1818
+ }
1691
1819
 
1692
1820
  //#endregion
1693
1821
  //#region src/server.ts
@@ -1698,6 +1826,8 @@ var BuiltinAgentsServer = class {
1698
1826
  mcpToolProviderName = null;
1699
1827
  mcpApplyInFlight = new Set();
1700
1828
  mcpStopping = false;
1829
+ mcpExtras = [];
1830
+ mcpLastJsonConfig = null;
1701
1831
  pullWakeRunner = null;
1702
1832
  options;
1703
1833
  constructor(options) {
@@ -1707,8 +1837,70 @@ var BuiltinAgentsServer = class {
1707
1837
  get mcpRegistry() {
1708
1838
  return this._mcpRegistry;
1709
1839
  }
1840
+ /**
1841
+ * Replace the in-memory `extras` list and re-apply the merged config
1842
+ * against the last-known workspace `mcp.json` state. Workspace
1843
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
1844
+ * latched `mcpStopping`.
1845
+ */
1846
+ async setExtraMcpServers(extras) {
1847
+ if (!this._mcpRegistry || this.mcpStopping) return;
1848
+ this.mcpExtras = extras;
1849
+ await this.applyMerged(this.mcpLastJsonConfig);
1850
+ }
1851
+ async wirePersistence(cfg) {
1852
+ const servers = [];
1853
+ for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1854
+ const persist = await (0, __electric_ax_agents_mcp.keychainPersistence)({ server: s.name });
1855
+ servers.push({
1856
+ ...s,
1857
+ auth: {
1858
+ ...s.auth,
1859
+ ...persist
1860
+ }
1861
+ });
1862
+ } else servers.push(s);
1863
+ return {
1864
+ ...cfg,
1865
+ servers
1866
+ };
1867
+ }
1868
+ mergeMcp(jsonCfg) {
1869
+ const jsonServers = jsonCfg?.servers ?? [];
1870
+ const jsonNames = new Set(jsonServers.map((s) => s.name));
1871
+ const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
1872
+ return {
1873
+ servers: [...filteredExtras, ...jsonServers],
1874
+ raw: jsonCfg?.raw
1875
+ };
1876
+ }
1877
+ async runApply(jsonCfg) {
1878
+ if (this.mcpStopping) return;
1879
+ const registry = this._mcpRegistry;
1880
+ if (!registry) return;
1881
+ try {
1882
+ const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
1883
+ if (this.mcpStopping) return;
1884
+ await registry.applyConfig(wired);
1885
+ } catch (e) {
1886
+ serverLog.error(`[mcp] applyConfig:`, e);
1887
+ try {
1888
+ this.options.onConfigError?.(e);
1889
+ } catch (cbErr) {
1890
+ serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1891
+ }
1892
+ }
1893
+ }
1894
+ applyMerged(jsonCfg) {
1895
+ this.mcpLastJsonConfig = jsonCfg;
1896
+ const p = this.runApply(jsonCfg);
1897
+ this.mcpApplyInFlight.add(p);
1898
+ p.finally(() => this.mcpApplyInFlight.delete(p));
1899
+ return p;
1900
+ }
1710
1901
  async start() {
1711
1902
  if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
1903
+ installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
1712
1904
  const pullWake = this.options.pullWake;
1713
1905
  if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
1714
1906
  try {
@@ -1719,76 +1911,28 @@ var BuiltinAgentsServer = class {
1719
1911
  });
1720
1912
  this._mcpRegistry = mcpRegistry;
1721
1913
  const mcpConfigPath = this.options.loadProjectMcpConfig ? node_path.default.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
1722
- const extras = this.options.extraMcpServers ?? [];
1723
- const wirePersistence = async (cfg) => {
1724
- const servers = [];
1725
- for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1726
- const persist = await (0, __electric_ax_agents_mcp.keychainPersistence)({ server: s.name });
1727
- servers.push({
1728
- ...s,
1729
- auth: {
1730
- ...s.auth,
1731
- ...persist
1732
- }
1733
- });
1734
- } else servers.push(s);
1735
- return {
1736
- ...cfg,
1737
- servers
1738
- };
1739
- };
1740
- const merge = (jsonCfg) => {
1741
- const jsonServers = jsonCfg?.servers ?? [];
1742
- const jsonNames = new Set(jsonServers.map((s) => s.name));
1743
- const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
1744
- return {
1745
- servers: [...filteredExtras, ...jsonServers],
1746
- raw: jsonCfg?.raw
1747
- };
1748
- };
1749
- const onConfigError = this.options.onConfigError;
1750
- const runApply = async (jsonCfg) => {
1751
- if (this.mcpStopping) return;
1752
- try {
1753
- const wired = await wirePersistence(merge(jsonCfg));
1754
- if (this.mcpStopping) return;
1755
- await mcpRegistry.applyConfig(wired);
1756
- } catch (e) {
1757
- serverLog.error(`[mcp] applyConfig:`, e);
1758
- try {
1759
- onConfigError?.(e);
1760
- } catch (cbErr) {
1761
- serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1762
- }
1763
- }
1764
- };
1765
- const applyMerged = (jsonCfg) => {
1766
- const p = runApply(jsonCfg);
1767
- this.mcpApplyInFlight.add(p);
1768
- p.finally(() => this.mcpApplyInFlight.delete(p));
1769
- return p;
1770
- };
1914
+ this.mcpExtras = this.options.extraMcpServers ?? [];
1771
1915
  if (mcpConfigPath) {
1772
1916
  try {
1773
1917
  const cfg = await (0, __electric_ax_agents_mcp.loadConfig)(mcpConfigPath, process.env);
1774
- applyMerged(cfg);
1918
+ this.applyMerged(cfg);
1775
1919
  } catch (err) {
1776
1920
  if (err.code !== `ENOENT`) throw err;
1777
- if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1778
- else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
1779
- applyMerged(null);
1921
+ if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1922
+ else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
1923
+ this.applyMerged(null);
1780
1924
  }
1781
1925
  try {
1782
1926
  this.mcpWatcherCloser = await (0, __electric_ax_agents_mcp.watchConfig)(mcpConfigPath, {
1783
- onChange: (cfg) => void applyMerged(cfg),
1927
+ onChange: (cfg) => void this.applyMerged(cfg),
1784
1928
  onError: (e) => serverLog.error(`[mcp] config error:`, e)
1785
1929
  });
1786
1930
  } catch (e) {
1787
1931
  serverLog.error(`[mcp] config watcher failed to start:`, e);
1788
1932
  }
1789
1933
  } else {
1790
- if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
1791
- applyMerged(null);
1934
+ if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
1935
+ this.applyMerged(null);
1792
1936
  }
1793
1937
  this.mcpToolProviderName = `mcp`;
1794
1938
  (0, __electric_ax_agents_runtime.registerToolProvider)({
@@ -1896,6 +2040,7 @@ var BuiltinAgentsServer = class {
1896
2040
  async registerPullWakeRunner(pullWake) {
1897
2041
  const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
1898
2042
  headers.set(`content-type`, `application/json`);
2043
+ const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
1899
2044
  const response = await fetch((0, __electric_ax_agents_runtime.appendPathToUrl)(this.options.agentServerUrl, `/_electric/runners`), {
1900
2045
  method: `POST`,
1901
2046
  headers,
@@ -1904,7 +2049,8 @@ var BuiltinAgentsServer = class {
1904
2049
  owner_principal: pullWake.ownerPrincipal,
1905
2050
  label: pullWake.label ?? `Built-in agents`,
1906
2051
  kind: `local`,
1907
- admin_status: `enabled`
2052
+ admin_status: `enabled`,
2053
+ sandbox_profiles: profiles
1908
2054
  })
1909
2055
  });
1910
2056
  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
 
@@ -35,12 +36,26 @@ declare function createAgentHandler(agentServerUrl: string, workingDirectory?: s
35
36
  declare function registerBuiltinAgentTypes(bootstrap: AgentHandlerResult): Promise<void>;
36
37
  declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
37
38
 
39
+ //#endregion
40
+ //#region src/durable-streams-cache.d.ts
41
+ type DurableStreamsFetchCacheOptions = false | {
42
+ store?: `memory` | `sqlite`;
43
+ sqliteLocation?: string;
44
+ maxCount?: number;
45
+ };
46
+
38
47
  //#endregion
39
48
  //#region src/server.d.ts
40
49
  interface BuiltinAgentsServerOptions {
41
50
  agentServerUrl: string;
42
51
  workingDirectory?: string;
43
52
  mockStreamFn?: StreamFn;
53
+ /**
54
+ * Configure the process-wide HTTP cache used by Undici-backed fetch calls.
55
+ * Defaults to a 100 MiB in-memory cache. Pass `false` to leave the global
56
+ * dispatcher unchanged.
57
+ */
58
+ durableStreamsFetchCache?: DurableStreamsFetchCacheOptions;
44
59
  /** Pull-wake runner configuration for built-in agents. */
45
60
  pullWake: {
46
61
  runnerId: string;
@@ -91,11 +106,24 @@ declare class BuiltinAgentsServer {
91
106
  private mcpToolProviderName;
92
107
  private mcpApplyInFlight;
93
108
  private mcpStopping;
109
+ private mcpExtras;
110
+ private mcpLastJsonConfig;
94
111
  private pullWakeRunner;
95
112
  readonly options: BuiltinAgentsServerOptions;
96
113
  constructor(options: BuiltinAgentsServerOptions);
97
114
  /** Embedded MCP registry. `null` until `start()` has run. */
98
115
  get mcpRegistry(): Registry | null;
116
+ /**
117
+ * Replace the in-memory `extras` list and re-apply the merged config
118
+ * against the last-known workspace `mcp.json` state. Workspace
119
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
120
+ * latched `mcpStopping`.
121
+ */
122
+ setExtraMcpServers(extras: ReadonlyArray<McpServerConfig$1>): Promise<void>;
123
+ private wirePersistence;
124
+ private mergeMcp;
125
+ private runApply;
126
+ private applyMerged;
99
127
  start(): Promise<string>;
100
128
  stop(): Promise<void>;
101
129
  private registerPullWakeRunner;
@@ -167,7 +195,7 @@ declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
167
195
  modelProvider?: string;
168
196
  modelId?: string;
169
197
  }): string;
170
- declare function createHortonTools(workingDirectory: string, ctx: HandlerContext, readSet: Set<string>, opts?: {
198
+ declare function createHortonTools(sandbox: Sandbox, ctx: HandlerContext, readSet: Set<string>, opts?: {
171
199
  docsSearchTool?: AgentTool$1;
172
200
  modelConfig?: ReturnType<typeof resolveBuiltinModelConfig>;
173
201
  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";
@@ -35,12 +36,26 @@ declare function createAgentHandler(agentServerUrl: string, workingDirectory?: s
35
36
  declare function registerBuiltinAgentTypes(bootstrap: AgentHandlerResult): Promise<void>;
36
37
  declare const registerAgentTypes: typeof registerBuiltinAgentTypes;
37
38
 
39
+ //#endregion
40
+ //#region src/durable-streams-cache.d.ts
41
+ type DurableStreamsFetchCacheOptions = false | {
42
+ store?: `memory` | `sqlite`;
43
+ sqliteLocation?: string;
44
+ maxCount?: number;
45
+ };
46
+
38
47
  //#endregion
39
48
  //#region src/server.d.ts
40
49
  interface BuiltinAgentsServerOptions {
41
50
  agentServerUrl: string;
42
51
  workingDirectory?: string;
43
52
  mockStreamFn?: StreamFn;
53
+ /**
54
+ * Configure the process-wide HTTP cache used by Undici-backed fetch calls.
55
+ * Defaults to a 100 MiB in-memory cache. Pass `false` to leave the global
56
+ * dispatcher unchanged.
57
+ */
58
+ durableStreamsFetchCache?: DurableStreamsFetchCacheOptions;
44
59
  /** Pull-wake runner configuration for built-in agents. */
45
60
  pullWake: {
46
61
  runnerId: string;
@@ -91,11 +106,24 @@ declare class BuiltinAgentsServer {
91
106
  private mcpToolProviderName;
92
107
  private mcpApplyInFlight;
93
108
  private mcpStopping;
109
+ private mcpExtras;
110
+ private mcpLastJsonConfig;
94
111
  private pullWakeRunner;
95
112
  readonly options: BuiltinAgentsServerOptions;
96
113
  constructor(options: BuiltinAgentsServerOptions);
97
114
  /** Embedded MCP registry. `null` until `start()` has run. */
98
115
  get mcpRegistry(): Registry | null;
116
+ /**
117
+ * Replace the in-memory `extras` list and re-apply the merged config
118
+ * against the last-known workspace `mcp.json` state. Workspace
119
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
120
+ * latched `mcpStopping`.
121
+ */
122
+ setExtraMcpServers(extras: ReadonlyArray<McpServerConfig$1>): Promise<void>;
123
+ private wirePersistence;
124
+ private mergeMcp;
125
+ private runApply;
126
+ private applyMerged;
99
127
  start(): Promise<string>;
100
128
  stop(): Promise<void>;
101
129
  private registerPullWakeRunner;
@@ -167,7 +195,7 @@ declare function buildHortonSystemPrompt(workingDirectory: string, opts?: {
167
195
  modelProvider?: string;
168
196
  modelId?: string;
169
197
  }): string;
170
- declare function createHortonTools(workingDirectory: string, ctx: HandlerContext, readSet: Set<string>, opts?: {
198
+ declare function createHortonTools(sandbox: Sandbox, ctx: HandlerContext, readSet: Set<string>, opts?: {
171
199
  docsSearchTool?: AgentTool$1;
172
200
  modelConfig?: ReturnType<typeof resolveBuiltinModelConfig>;
173
201
  modelCatalog?: BuiltinModelCatalog;