@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/entrypoint.js +235 -89
- package/dist/index.cjs +233 -87
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +243 -97
- package/docs/entities/patterns/blackboard.md +1 -1
- package/docs/entities/patterns/dispatcher.md +3 -3
- package/docs/entities/patterns/manager-worker.md +11 -23
- package/docs/entities/patterns/map-reduce.md +1 -1
- package/docs/entities/patterns/pipeline.md +3 -3
- package/docs/index.md +61 -39
- package/docs/quickstart.md +26 -22
- package/docs/reference/entity-handle.md +51 -25
- package/docs/reference/handler-context.md +1 -1
- package/docs/reference/wake-event.md +1 -1
- package/docs/usage/defining-tools.md +4 -5
- package/docs/usage/overview.md +10 -6
- package/docs/usage/shared-state.md +3 -3
- package/docs/usage/spawning-and-coordinating.md +34 -18
- package/docs/usage/writing-handlers.md +1 -1
- package/docs/walkthrough.md +1156 -0
- package/package.json +4 -3
- package/skills/quickstart/scaffold/package.json +1 -1
- package/skills/quickstart.md +16 -10
package/dist/entrypoint.js
CHANGED
|
@@ -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
|
|
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(
|
|
1178
|
+
function createHortonTools(sandbox, ctx, readSet, opts = {}) {
|
|
1155
1179
|
return [
|
|
1156
|
-
createBashTool(
|
|
1157
|
-
createReadFileTool(
|
|
1158
|
-
createWriteTool(
|
|
1159
|
-
createEditTool(
|
|
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
|
-
})] : [
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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(
|
|
1456
|
+
out.push(createBashTool(sandbox));
|
|
1425
1457
|
break;
|
|
1426
1458
|
case `read`:
|
|
1427
|
-
out.push(createReadFileTool(
|
|
1459
|
+
out.push(createReadFileTool(sandbox, readSet));
|
|
1428
1460
|
break;
|
|
1429
1461
|
case `write`:
|
|
1430
|
-
out.push(createWriteTool(
|
|
1462
|
+
out.push(createWriteTool(sandbox, readSet));
|
|
1431
1463
|
break;
|
|
1432
1464
|
case `edit`:
|
|
1433
|
-
out.push(createEditTool(
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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 (
|
|
1742
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${
|
|
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 (
|
|
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()}`);
|