@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.
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
+ import { Agent, cacheStores, interceptors, setGlobalDispatcher } from "undici";
3
4
  import fs from "node:fs";
4
5
  import pino from "pino";
5
6
  import { fileURLToPath } from "node:url";
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 { MOONSHOT_API_BASE_URL, MOONSHOT_PROVIDER, appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, getMoonshotApiKey, getMoonshotModel, getMoonshotModels, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
8
+ import { braveSearchTool, createBashTool, createEditTool, createEventSourceTools, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool } from "@electric-ax/agents-runtime/tools";
9
+ import { chooseDefaultSandbox, isE2BAvailable, remoteSandbox } from "@electric-ax/agents-runtime/sandbox";
8
10
  import { z } from "zod";
9
11
  import { createHash } from "node:crypto";
10
12
  import fs$1 from "node:fs/promises";
@@ -15,6 +17,18 @@ import { nanoid } from "nanoid";
15
17
  import { getModels } from "@mariozechner/pi-ai";
16
18
  import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
17
19
 
20
+ //#region src/durable-streams-cache.ts
21
+ const MEMORY_CACHE_SIZE_BYTES = 100 * 1024 * 1024;
22
+ function installDurableStreamsFetchCache(options = {}) {
23
+ if (options === false) return;
24
+ const store = options.store === `sqlite` || options.sqliteLocation ? new cacheStores.SqliteCacheStore({
25
+ location: options.sqliteLocation,
26
+ maxCount: options.maxCount
27
+ }) : new cacheStores.MemoryCacheStore({ maxSize: MEMORY_CACHE_SIZE_BYTES });
28
+ setGlobalDispatcher(new Agent().compose(interceptors.cache({ store })));
29
+ }
30
+
31
+ //#endregion
18
32
  //#region src/log.ts
19
33
  const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
20
34
  const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
@@ -764,7 +778,8 @@ function createSpawnWorkerTool(ctx, modelConfig) {
764
778
  wake: {
765
779
  on: `runFinished`,
766
780
  includeResponse: true
767
- }
781
+ },
782
+ sandbox: `inherit`
768
783
  });
769
784
  const workerUrl = handle.entityUrl;
770
785
  return {
@@ -863,6 +878,15 @@ async function fetchAvailableModelIds(provider) {
863
878
  function knownModelsForProvider(provider) {
864
879
  return provider === MOONSHOT_PROVIDER ? getMoonshotModels() : getModels(provider);
865
880
  }
881
+ function resolveBuiltinModelContextWindow(modelConfig) {
882
+ const modelId = String(modelConfig.model);
883
+ if (modelConfig.provider === MOONSHOT_PROVIDER) return getMoonshotModel(modelId)?.contextWindow ?? null;
884
+ if (!modelConfig.provider) return null;
885
+ return knownModelsForProvider(modelConfig.provider).find((model) => model.id === modelId)?.contextWindow ?? null;
886
+ }
887
+ function resolveBuiltinModelSourceBudget(modelConfig) {
888
+ return resolveBuiltinModelContextWindow(modelConfig) ?? 1e5;
889
+ }
866
890
  function choiceForKnownModel(provider, model) {
867
891
  return {
868
892
  provider,
@@ -1151,19 +1175,19 @@ function getToolName(tool) {
1151
1175
  const name = tool.name;
1152
1176
  return typeof name === `string` ? name : null;
1153
1177
  }
1154
- function createHortonTools(workingDirectory, ctx, readSet, opts = {}) {
1178
+ function createHortonTools(sandbox, ctx, readSet, opts = {}) {
1155
1179
  return [
1156
- createBashTool(workingDirectory),
1157
- createReadFileTool(workingDirectory, readSet),
1158
- createWriteTool(workingDirectory, readSet),
1159
- createEditTool(workingDirectory, readSet),
1180
+ createBashTool(sandbox),
1181
+ createReadFileTool(sandbox, readSet),
1182
+ createWriteTool(sandbox, readSet),
1183
+ createEditTool(sandbox, readSet),
1160
1184
  braveSearchTool,
1161
- ...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool({
1185
+ ...opts.modelCatalog && opts.modelConfig ? [createFetchUrlTool(sandbox, {
1162
1186
  catalog: opts.modelCatalog,
1163
1187
  modelConfig: opts.modelConfig,
1164
1188
  log: (message) => serverLog.info(message),
1165
1189
  logPrefix: opts.logPrefix ?? `[horton]`
1166
- })] : [fetchUrlTool],
1190
+ })] : [createFetchUrlTool(sandbox)],
1167
1191
  createSpawnWorkerTool(ctx, opts.modelConfig),
1168
1192
  createSendTool(ctx.send, { selfEntityUrl: ctx.entityUrl }),
1169
1193
  ...opts.docsSearchTool ? [opts.docsSearchTool] : []
@@ -1217,11 +1241,10 @@ async function extractFirstUserMessage(ctx) {
1217
1241
  function messageSeq(message) {
1218
1242
  return typeof message._seq === `number` ? message._seq : -1;
1219
1243
  }
1220
- function readAgentsMd(workingDirectory) {
1221
- const agentsMdPath = path.join(workingDirectory, `AGENTS.md`);
1244
+ async function readAgentsMd(sandbox) {
1245
+ const agentsMdPath = path.posix.join(sandbox.workingDirectory, `AGENTS.md`);
1222
1246
  try {
1223
- if (!fs.existsSync(agentsMdPath) || !fs.statSync(agentsMdPath).isFile()) return null;
1224
- const content = fs.readFileSync(agentsMdPath, `utf8`);
1247
+ const content = (await sandbox.readFile(agentsMdPath)).toString(`utf8`);
1225
1248
  return [
1226
1249
  `<context_file kind="instructions" path="${agentsMdPath}">`,
1227
1250
  content,
@@ -1232,16 +1255,17 @@ function readAgentsMd(workingDirectory) {
1232
1255
  }
1233
1256
  }
1234
1257
  function createAssistantHandler(options) {
1235
- const { workingDirectory, streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
1258
+ const { streamFn, docsSupport, docsSearchTool, skillsRegistry, modelCatalog, docsUrl } = options;
1236
1259
  const hasSkills = Boolean(skillsRegistry && skillsRegistry.catalog.size > 0);
1237
1260
  return async function assistantHandler(ctx, wake) {
1238
1261
  const readSet = new Set();
1239
- const effectiveCwd = typeof ctx.args.workingDirectory === `string` && ctx.args.workingDirectory.trim().length > 0 ? ctx.args.workingDirectory : workingDirectory;
1240
1262
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, ctx.args);
1241
- const agentsMd = readAgentsMd(effectiveCwd);
1263
+ const sourceBudget = resolveBuiltinModelSourceBudget(modelConfig);
1264
+ const sandboxCwd = ctx.sandbox.workingDirectory;
1265
+ const agentsMd = await readAgentsMd(ctx.sandbox);
1242
1266
  const tools = [
1243
1267
  ...ctx.electricTools,
1244
- ...createHortonTools(effectiveCwd, ctx, readSet, {
1268
+ ...createHortonTools(ctx.sandbox, ctx, readSet, {
1245
1269
  docsSearchTool,
1246
1270
  modelConfig,
1247
1271
  modelCatalog,
@@ -1270,7 +1294,7 @@ function createAssistantHandler(options) {
1270
1294
  }
1271
1295
  })() : Promise.resolve();
1272
1296
  if (docsSupport) ctx.useContext({
1273
- sourceBudget: 1e5,
1297
+ sourceBudget,
1274
1298
  sources: {
1275
1299
  docs_toc: {
1276
1300
  content: () => docsSupport.renderCompressedToc(),
@@ -1299,7 +1323,7 @@ function createAssistantHandler(options) {
1299
1323
  }
1300
1324
  });
1301
1325
  else if (skillsRegistry && skillsRegistry.catalog.size > 0) ctx.useContext({
1302
- sourceBudget: 1e5,
1326
+ sourceBudget,
1303
1327
  sources: {
1304
1328
  skills_catalog: {
1305
1329
  content: () => skillsRegistry.renderCatalog(2e3),
@@ -1318,7 +1342,7 @@ function createAssistantHandler(options) {
1318
1342
  }
1319
1343
  });
1320
1344
  else if (agentsMd) ctx.useContext({
1321
- sourceBudget: 1e5,
1345
+ sourceBudget,
1322
1346
  sources: {
1323
1347
  conversation: {
1324
1348
  content: () => ctx.timelineMessages(),
@@ -1332,7 +1356,7 @@ function createAssistantHandler(options) {
1332
1356
  }
1333
1357
  });
1334
1358
  ctx.useAgent({
1335
- systemPrompt: buildHortonSystemPrompt(effectiveCwd, {
1359
+ systemPrompt: buildHortonSystemPrompt(sandboxCwd, {
1336
1360
  hasDocsSupport: Boolean(docsSupport),
1337
1361
  hasSkills,
1338
1362
  docsUrl,
@@ -1360,7 +1384,6 @@ function registerHorton(registry, options) {
1360
1384
  serverLog.warn(`[horton-docs] warmup failed: ${error instanceof Error ? error.message : String(error)}`);
1361
1385
  });
1362
1386
  const assistantHandler = createAssistantHandler({
1363
- workingDirectory,
1364
1387
  streamFn,
1365
1388
  docsSupport,
1366
1389
  docsSearchTool,
@@ -1376,6 +1399,15 @@ function registerHorton(registry, options) {
1376
1399
  registry.define(`horton`, {
1377
1400
  description: `Friendly capable assistant — chat, code, research, dispatch`,
1378
1401
  creationSchema: hortonCreationSchema,
1402
+ permissionGrants: [{
1403
+ subject_kind: `principal_kind`,
1404
+ subject_value: `user`,
1405
+ permission: `spawn`
1406
+ }, {
1407
+ subject_kind: `principal_kind`,
1408
+ subject_value: `user`,
1409
+ permission: `manage`
1410
+ }],
1379
1411
  handler: assistantHandler
1380
1412
  });
1381
1413
  return [`horton`];
@@ -1417,26 +1449,29 @@ function parseWorkerArgs(value) {
1417
1449
  if (typeof value.reasoningEffort === `string` && REASONING_EFFORT_VALUES.includes(value.reasoningEffort)) args.reasoningEffort = value.reasoningEffort;
1418
1450
  return args;
1419
1451
  }
1420
- function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
1452
+ function buildToolsForWorker(tools, sandbox, ctx, readSet, opts) {
1421
1453
  const out = [];
1422
1454
  for (const name of tools) switch (name) {
1423
1455
  case `bash`:
1424
- out.push(createBashTool(workingDirectory));
1456
+ out.push(createBashTool(sandbox));
1425
1457
  break;
1426
1458
  case `read`:
1427
- out.push(createReadFileTool(workingDirectory, readSet));
1459
+ out.push(createReadFileTool(sandbox, readSet));
1428
1460
  break;
1429
1461
  case `write`:
1430
- out.push(createWriteTool(workingDirectory, readSet));
1462
+ out.push(createWriteTool(sandbox, readSet));
1431
1463
  break;
1432
1464
  case `edit`:
1433
- out.push(createEditTool(workingDirectory, readSet));
1465
+ out.push(createEditTool(sandbox, readSet));
1434
1466
  break;
1435
1467
  case `web_search`:
1436
1468
  out.push(braveSearchTool);
1437
1469
  break;
1438
1470
  case `fetch_url`:
1439
- out.push(fetchUrlTool);
1471
+ out.push(createFetchUrlTool(sandbox, {
1472
+ catalog: opts.modelCatalog,
1473
+ modelConfig: opts.modelConfig
1474
+ }));
1440
1475
  break;
1441
1476
  case `spawn_worker`:
1442
1477
  out.push(createSpawnWorkerTool(ctx));
@@ -1544,14 +1579,26 @@ function buildSharedStateTools(shared, schema, mode) {
1544
1579
  return tools;
1545
1580
  }
1546
1581
  function registerWorker(registry, options) {
1547
- const { workingDirectory, streamFn, modelCatalog } = options;
1582
+ const { streamFn, modelCatalog } = options;
1548
1583
  registry.define(`worker`, {
1549
1584
  description: `Internal — generic worker spawned by other agents. Configure via spawn args (systemPrompt + tools + optional sharedDb).`,
1585
+ permissionGrants: [{
1586
+ subject_kind: `principal_kind`,
1587
+ subject_value: `user`,
1588
+ permission: `spawn`
1589
+ }, {
1590
+ subject_kind: `principal_kind`,
1591
+ subject_value: `user`,
1592
+ permission: `manage`
1593
+ }],
1550
1594
  async handler(ctx) {
1551
1595
  const args = parseWorkerArgs(ctx.args);
1552
1596
  const readSet = new Set();
1553
- const builtinTools = buildToolsForWorker(args.tools, workingDirectory, ctx, readSet);
1554
1597
  const modelConfig = resolveBuiltinModelConfig(modelCatalog, args);
1598
+ const builtinTools = buildToolsForWorker(args.tools, ctx.sandbox, ctx, readSet, {
1599
+ modelCatalog,
1600
+ modelConfig
1601
+ });
1555
1602
  const sharedStateTools = [];
1556
1603
  if (args.sharedDb) {
1557
1604
  const shared = await ctx.observe(db(args.sharedDb.id, args.sharedDb.schema));
@@ -1628,6 +1675,7 @@ async function createBuiltinAgentHandler(options) {
1628
1675
  modelCatalog
1629
1676
  });
1630
1677
  typeNames.push(`worker`);
1678
+ const sandboxProfiles = await buildBuiltinSandboxProfiles(cwd);
1631
1679
  const runtime = createRuntimeHandler({
1632
1680
  baseUrl: agentServerUrl,
1633
1681
  serveEndpoint,
@@ -1638,7 +1686,8 @@ async function createBuiltinAgentHandler(options) {
1638
1686
  idleTimeout: 5 * 6e4,
1639
1687
  createElectricTools: createBuiltinElectricTools(createElectricTools),
1640
1688
  publicUrl,
1641
- name: runtimeName ?? `builtin-agents`
1689
+ name: runtimeName ?? `builtin-agents`,
1690
+ sandboxProfiles
1642
1691
  });
1643
1692
  return {
1644
1693
  handler: runtime.onEnter,
@@ -1652,6 +1701,85 @@ async function registerBuiltinAgentTypes(bootstrap) {
1652
1701
  await bootstrap.runtime.registerTypes();
1653
1702
  serverLog.info(`[builtin-agents] ${bootstrap.typeNames.length} built-in agent types ready: ${bootstrap.typeNames.join(`, `)}`);
1654
1703
  }
1704
+ /**
1705
+ * Guard so repeated `buildBuiltinSandboxProfiles` calls in one process don't
1706
+ * re-run the boot sweep.
1707
+ */
1708
+ let dockerSweptOnBoot = false;
1709
+ function sweepOrphanedDockerSandboxesOnce(sweep) {
1710
+ if (dockerSweptOnBoot) return;
1711
+ dockerSweptOnBoot = true;
1712
+ sweep().then((removed) => {
1713
+ if (removed.length > 0) serverLog.info(`[builtin-agents] docker sandbox boot sweep removed ${removed.length} leftover container(s)`);
1714
+ }).catch((err) => serverLog.warn(`[builtin-agents] docker sandbox boot sweep error: ${err instanceof Error ? err.message : String(err)}`));
1715
+ }
1716
+ /**
1717
+ * Built-in sandbox profiles. `local` is always available. `docker` is
1718
+ * gated on Docker being reachable so a user without Docker installed
1719
+ * sees only what works — the UI never offers a non-functional choice.
1720
+ */
1721
+ async function buildBuiltinSandboxProfiles(workingDirectory) {
1722
+ const profiles = [{
1723
+ name: `local`,
1724
+ label: `Local`,
1725
+ description: `Runs on the host without isolation. Full filesystem access.`,
1726
+ factory: ({ args }) => chooseDefaultSandbox(resolveCwd(args, workingDirectory))
1727
+ }];
1728
+ try {
1729
+ const { isDockerAvailable } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1730
+ if (await isDockerAvailable()) {
1731
+ const { dockerSandbox, sweepOrphanedDockerSandboxes } = await import(`@electric-ax/agents-runtime/sandbox/docker`);
1732
+ sweepOrphanedDockerSandboxesOnce(sweepOrphanedDockerSandboxes);
1733
+ profiles.push({
1734
+ name: `docker`,
1735
+ label: `Docker`,
1736
+ 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).`,
1737
+ factory: ({ args, sandboxKey, persistent, owner, entityType, entityUrl }) => {
1738
+ const cwd = readWorkingDirectoryArg(args);
1739
+ return dockerSandbox({
1740
+ initialNetworkPolicy: { mode: `allow-all` },
1741
+ extraMounts: cwd ? [{
1742
+ hostPath: cwd,
1743
+ containerPath: `/work`,
1744
+ readOnly: false
1745
+ }] : void 0,
1746
+ sandboxKey,
1747
+ persistent,
1748
+ owner,
1749
+ entityType,
1750
+ entityUrl
1751
+ });
1752
+ }
1753
+ });
1754
+ } else serverLog.info(`[builtin-agents] docker daemon not reachable — docker sandbox profile not registered`);
1755
+ } catch (err) {
1756
+ serverLog.warn(`[builtin-agents] failed to probe docker availability: ${err instanceof Error ? err.message : String(err)}`);
1757
+ }
1758
+ if (process.env.E2B_API_KEY) if (await isE2BAvailable()) profiles.push({
1759
+ name: `e2b`,
1760
+ label: `E2B`,
1761
+ description: `Runs in a remote E2B microVM. Persistent sandboxes survive across wakes and are reachable from any runner.`,
1762
+ remote: true,
1763
+ factory: ({ sandboxKey, persistent, owner }) => remoteSandbox({
1764
+ provider: `e2b`,
1765
+ apiKey: process.env.E2B_API_KEY,
1766
+ sandboxKey,
1767
+ persistent,
1768
+ owner,
1769
+ initialNetworkPolicy: { mode: `allow-all` }
1770
+ })
1771
+ });
1772
+ else serverLog.info(`[builtin-agents] E2B_API_KEY set but the "e2b" package is not installed — e2b sandbox profile not registered`);
1773
+ console.log(`[builtin-agents] sandbox profiles advertised: ${profiles.map((p) => p.name).join(`, `)}`);
1774
+ return profiles;
1775
+ }
1776
+ function readWorkingDirectoryArg(args) {
1777
+ const v = args.workingDirectory;
1778
+ return typeof v === `string` && v.trim().length > 0 ? v : null;
1779
+ }
1780
+ function resolveCwd(args, fallback) {
1781
+ return readWorkingDirectoryArg(args) ?? fallback;
1782
+ }
1655
1783
 
1656
1784
  //#endregion
1657
1785
  //#region src/server.ts
@@ -1662,6 +1790,8 @@ var BuiltinAgentsServer = class {
1662
1790
  mcpToolProviderName = null;
1663
1791
  mcpApplyInFlight = new Set();
1664
1792
  mcpStopping = false;
1793
+ mcpExtras = [];
1794
+ mcpLastJsonConfig = null;
1665
1795
  pullWakeRunner = null;
1666
1796
  options;
1667
1797
  constructor(options) {
@@ -1671,8 +1801,70 @@ var BuiltinAgentsServer = class {
1671
1801
  get mcpRegistry() {
1672
1802
  return this._mcpRegistry;
1673
1803
  }
1804
+ /**
1805
+ * Replace the in-memory `extras` list and re-apply the merged config
1806
+ * against the last-known workspace `mcp.json` state. Workspace
1807
+ * `mcp.json` still wins on name collision. No-op once `stop()` has
1808
+ * latched `mcpStopping`.
1809
+ */
1810
+ async setExtraMcpServers(extras) {
1811
+ if (!this._mcpRegistry || this.mcpStopping) return;
1812
+ this.mcpExtras = extras;
1813
+ await this.applyMerged(this.mcpLastJsonConfig);
1814
+ }
1815
+ async wirePersistence(cfg) {
1816
+ const servers = [];
1817
+ for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1818
+ const persist = await keychainPersistence({ server: s.name });
1819
+ servers.push({
1820
+ ...s,
1821
+ auth: {
1822
+ ...s.auth,
1823
+ ...persist
1824
+ }
1825
+ });
1826
+ } else servers.push(s);
1827
+ return {
1828
+ ...cfg,
1829
+ servers
1830
+ };
1831
+ }
1832
+ mergeMcp(jsonCfg) {
1833
+ const jsonServers = jsonCfg?.servers ?? [];
1834
+ const jsonNames = new Set(jsonServers.map((s) => s.name));
1835
+ const filteredExtras = this.mcpExtras.filter((s) => !jsonNames.has(s.name));
1836
+ return {
1837
+ servers: [...filteredExtras, ...jsonServers],
1838
+ raw: jsonCfg?.raw
1839
+ };
1840
+ }
1841
+ async runApply(jsonCfg) {
1842
+ if (this.mcpStopping) return;
1843
+ const registry = this._mcpRegistry;
1844
+ if (!registry) return;
1845
+ try {
1846
+ const wired = await this.wirePersistence(this.mergeMcp(jsonCfg));
1847
+ if (this.mcpStopping) return;
1848
+ await registry.applyConfig(wired);
1849
+ } catch (e) {
1850
+ serverLog.error(`[mcp] applyConfig:`, e);
1851
+ try {
1852
+ this.options.onConfigError?.(e);
1853
+ } catch (cbErr) {
1854
+ serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1855
+ }
1856
+ }
1857
+ }
1858
+ applyMerged(jsonCfg) {
1859
+ this.mcpLastJsonConfig = jsonCfg;
1860
+ const p = this.runApply(jsonCfg);
1861
+ this.mcpApplyInFlight.add(p);
1862
+ p.finally(() => this.mcpApplyInFlight.delete(p));
1863
+ return p;
1864
+ }
1674
1865
  async start() {
1675
1866
  if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
1867
+ installDurableStreamsFetchCache(this.options.durableStreamsFetchCache);
1676
1868
  const pullWake = this.options.pullWake;
1677
1869
  if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
1678
1870
  try {
@@ -1683,76 +1875,28 @@ var BuiltinAgentsServer = class {
1683
1875
  });
1684
1876
  this._mcpRegistry = mcpRegistry;
1685
1877
  const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
1686
- const extras = this.options.extraMcpServers ?? [];
1687
- const wirePersistence = async (cfg) => {
1688
- const servers = [];
1689
- for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
1690
- const persist = await keychainPersistence({ server: s.name });
1691
- servers.push({
1692
- ...s,
1693
- auth: {
1694
- ...s.auth,
1695
- ...persist
1696
- }
1697
- });
1698
- } else servers.push(s);
1699
- return {
1700
- ...cfg,
1701
- servers
1702
- };
1703
- };
1704
- const merge = (jsonCfg) => {
1705
- const jsonServers = jsonCfg?.servers ?? [];
1706
- const jsonNames = new Set(jsonServers.map((s) => s.name));
1707
- const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
1708
- return {
1709
- servers: [...filteredExtras, ...jsonServers],
1710
- raw: jsonCfg?.raw
1711
- };
1712
- };
1713
- const onConfigError = this.options.onConfigError;
1714
- const runApply = async (jsonCfg) => {
1715
- if (this.mcpStopping) return;
1716
- try {
1717
- const wired = await wirePersistence(merge(jsonCfg));
1718
- if (this.mcpStopping) return;
1719
- await mcpRegistry.applyConfig(wired);
1720
- } catch (e) {
1721
- serverLog.error(`[mcp] applyConfig:`, e);
1722
- try {
1723
- onConfigError?.(e);
1724
- } catch (cbErr) {
1725
- serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
1726
- }
1727
- }
1728
- };
1729
- const applyMerged = (jsonCfg) => {
1730
- const p = runApply(jsonCfg);
1731
- this.mcpApplyInFlight.add(p);
1732
- p.finally(() => this.mcpApplyInFlight.delete(p));
1733
- return p;
1734
- };
1878
+ this.mcpExtras = this.options.extraMcpServers ?? [];
1735
1879
  if (mcpConfigPath) {
1736
1880
  try {
1737
1881
  const cfg = await loadConfig(mcpConfigPath, process.env);
1738
- applyMerged(cfg);
1882
+ this.applyMerged(cfg);
1739
1883
  } catch (err) {
1740
1884
  if (err.code !== `ENOENT`) throw err;
1741
- if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1742
- else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
1743
- applyMerged(null);
1885
+ if (this.mcpExtras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
1886
+ else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${this.mcpExtras.length} server(s) from extras`);
1887
+ this.applyMerged(null);
1744
1888
  }
1745
1889
  try {
1746
1890
  this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
1747
- onChange: (cfg) => void applyMerged(cfg),
1891
+ onChange: (cfg) => void this.applyMerged(cfg),
1748
1892
  onError: (e) => serverLog.error(`[mcp] config error:`, e)
1749
1893
  });
1750
1894
  } catch (e) {
1751
1895
  serverLog.error(`[mcp] config watcher failed to start:`, e);
1752
1896
  }
1753
1897
  } else {
1754
- if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
1755
- applyMerged(null);
1898
+ if (this.mcpExtras.length > 0) serverLog.info(`[mcp] starting with ${this.mcpExtras.length} server(s) from extras`);
1899
+ this.applyMerged(null);
1756
1900
  }
1757
1901
  this.mcpToolProviderName = `mcp`;
1758
1902
  registerToolProvider({
@@ -1860,6 +2004,7 @@ var BuiltinAgentsServer = class {
1860
2004
  async registerPullWakeRunner(pullWake) {
1861
2005
  const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
1862
2006
  headers.set(`content-type`, `application/json`);
2007
+ const profiles = this.bootstrap?.runtime.sandboxProfileDescriptors ?? [];
1863
2008
  const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
1864
2009
  method: `POST`,
1865
2010
  headers,
@@ -1868,7 +2013,8 @@ var BuiltinAgentsServer = class {
1868
2013
  owner_principal: pullWake.ownerPrincipal,
1869
2014
  label: pullWake.label ?? `Built-in agents`,
1870
2015
  kind: `local`,
1871
- admin_status: `enabled`
2016
+ admin_status: `enabled`,
2017
+ sandbox_profiles: profiles
1872
2018
  })
1873
2019
  });
1874
2020
  if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);