@poncho-ai/cli 0.17.0 → 0.19.0
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +32 -0
- package/dist/{chunk-GDB5X2OS.js → chunk-7P53QSP5.js} +494 -157
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-B6JJ33SN.js → run-interactive-ink-4KVVPMR3.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +216 -124
- package/src/web-ui-client.ts +203 -36
- package/src/web-ui-store.ts +68 -1
- package/src/web-ui-styles.ts +51 -5
- package/src/web-ui.ts +6 -3
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ declare const mcpAdd: (workingDir: string, options: {
|
|
|
66
66
|
name?: string;
|
|
67
67
|
envVars?: string[];
|
|
68
68
|
authBearerEnv?: string;
|
|
69
|
+
headers?: string[];
|
|
69
70
|
}) => Promise<void>;
|
|
70
71
|
declare const mcpList: (workingDir: string) => Promise<void>;
|
|
71
72
|
declare const mcpRemove: (workingDir: string, name: string) => Promise<void>;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "CLI for building and deploying AI agents",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"react": "^19.2.4",
|
|
28
28
|
"react-devtools-core": "^6.1.5",
|
|
29
29
|
"yaml": "^2.8.1",
|
|
30
|
-
"@poncho-ai/harness": "0.
|
|
31
|
-
"@poncho-ai/messaging": "0.2.
|
|
32
|
-
"@poncho-ai/sdk": "1.
|
|
30
|
+
"@poncho-ai/harness": "0.19.0",
|
|
31
|
+
"@poncho-ai/messaging": "0.2.7",
|
|
32
|
+
"@poncho-ai/sdk": "1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/busboy": "^1.5.4",
|
package/src/index.ts
CHANGED
|
@@ -455,6 +455,10 @@ Connect remote MCP servers and expose their tools to the agent:
|
|
|
455
455
|
# Add remote MCP server
|
|
456
456
|
poncho mcp add --url https://mcp.example.com/github --name github --auth-bearer-env GITHUB_TOKEN
|
|
457
457
|
|
|
458
|
+
# Server with custom headers (e.g. Arcade)
|
|
459
|
+
poncho mcp add --url https://mcp.arcade.dev --name arcade \\
|
|
460
|
+
--auth-bearer-env ARCADE_API_KEY --header "Arcade-User-ID: user@example.com"
|
|
461
|
+
|
|
458
462
|
# List configured servers
|
|
459
463
|
poncho mcp list
|
|
460
464
|
|
|
@@ -536,14 +540,22 @@ export default {
|
|
|
536
540
|
url: "https://mcp.example.com/github",
|
|
537
541
|
auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
|
|
538
542
|
},
|
|
543
|
+
// Custom headers for servers that require them (e.g. Arcade)
|
|
544
|
+
// { name: "arcade", url: "https://mcp.arcade.dev", auth: { type: "bearer", tokenEnv: "ARCADE_API_KEY" }, headers: { "Arcade-User-ID": "user@example.com" } },
|
|
539
545
|
],
|
|
540
546
|
// Tool access: true (available), false (disabled), 'approval' (requires human approval)
|
|
541
547
|
tools: {
|
|
548
|
+
list_directory: true,
|
|
549
|
+
read_file: true,
|
|
542
550
|
write_file: true, // gated by environment for writes
|
|
551
|
+
delete_file: 'approval', // requires human approval
|
|
552
|
+
delete_directory: 'approval', // requires human approval
|
|
543
553
|
send_email: 'approval', // requires human approval
|
|
544
554
|
byEnvironment: {
|
|
545
555
|
production: {
|
|
546
556
|
write_file: false,
|
|
557
|
+
delete_file: false,
|
|
558
|
+
delete_directory: false,
|
|
547
559
|
},
|
|
548
560
|
development: {
|
|
549
561
|
send_email: true, // skip approval in dev
|
|
@@ -626,6 +638,7 @@ Connect your agent to email so users can interact by sending emails:
|
|
|
626
638
|
RESEND_API_KEY=re_...
|
|
627
639
|
RESEND_WEBHOOK_SECRET=whsec_...
|
|
628
640
|
RESEND_FROM=Agent <agent@yourdomain.com>
|
|
641
|
+
RESEND_REPLY_TO=support@yourdomain.com # optional
|
|
629
642
|
\`\`\`
|
|
630
643
|
5. Add to \`poncho.config.js\`:
|
|
631
644
|
\`\`\`javascript
|
|
@@ -1457,7 +1470,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1457
1470
|
// Pending subagent approvals: when a subagent hits an approval checkpoint,
|
|
1458
1471
|
// we store a resolver here so the approval endpoint can signal the waiting runSubagent.
|
|
1459
1472
|
type PendingSubagentApproval = {
|
|
1460
|
-
resolve: (
|
|
1473
|
+
resolve: (decidedApprovals: NonNullable<Conversation["pendingApprovals"]>) => void;
|
|
1461
1474
|
childHarness: AgentHarness;
|
|
1462
1475
|
checkpoint: NonNullable<Conversation["pendingApprovals"]>[number];
|
|
1463
1476
|
childConversationId: string;
|
|
@@ -1578,84 +1591,86 @@ export const createRequestHandler = async (options?: {
|
|
|
1578
1591
|
});
|
|
1579
1592
|
}
|
|
1580
1593
|
if (event.type === "tool:approval:checkpoint") {
|
|
1581
|
-
// Persist checkpoint so the approval endpoint can find it
|
|
1582
1594
|
const cpConv = await conversationStore.get(childConversationId);
|
|
1583
1595
|
if (cpConv) {
|
|
1584
|
-
const
|
|
1585
|
-
approvalId:
|
|
1596
|
+
const allCpData: NonNullable<Conversation["pendingApprovals"]> = event.approvals.map(a => ({
|
|
1597
|
+
approvalId: a.approvalId,
|
|
1586
1598
|
runId: latestRunId,
|
|
1587
|
-
tool:
|
|
1588
|
-
toolCallId:
|
|
1589
|
-
input:
|
|
1599
|
+
tool: a.tool,
|
|
1600
|
+
toolCallId: a.toolCallId,
|
|
1601
|
+
input: a.input,
|
|
1590
1602
|
checkpointMessages: [...historyMessages, ...event.checkpointMessages],
|
|
1591
1603
|
baseMessageCount: 0,
|
|
1592
1604
|
pendingToolCalls: event.pendingToolCalls,
|
|
1593
|
-
};
|
|
1594
|
-
cpConv.pendingApprovals =
|
|
1605
|
+
}));
|
|
1606
|
+
cpConv.pendingApprovals = allCpData;
|
|
1595
1607
|
cpConv.updatedAt = Date.now();
|
|
1596
1608
|
await conversationStore.update(cpConv);
|
|
1597
1609
|
|
|
1598
|
-
// Wait for approval
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1610
|
+
// Wait for all approval decisions (signaled by the approval endpoint
|
|
1611
|
+
// once every entry in the batch has a decision)
|
|
1612
|
+
const decidedApprovals = await new Promise<NonNullable<Conversation["pendingApprovals"]>>((resolve) => {
|
|
1613
|
+
for (const cpData of allCpData) {
|
|
1614
|
+
pendingSubagentApprovals.set(cpData.approvalId, {
|
|
1615
|
+
resolve,
|
|
1616
|
+
childHarness,
|
|
1617
|
+
checkpoint: cpData,
|
|
1618
|
+
childConversationId,
|
|
1619
|
+
parentConversationId,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1607
1622
|
});
|
|
1608
1623
|
|
|
1609
|
-
// Execute
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1624
|
+
// Execute approved tools and collect results
|
|
1625
|
+
const checkpointRef = allCpData[0]!;
|
|
1626
|
+
const toolContext = {
|
|
1627
|
+
runId: checkpointRef.runId,
|
|
1628
|
+
agentId: identity.id,
|
|
1629
|
+
step: 0,
|
|
1630
|
+
workingDir,
|
|
1631
|
+
parameters: {},
|
|
1632
|
+
conversationId: childConversationId,
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
const approvalToolCallIds = new Set(decidedApprovals.map(a => a.toolCallId));
|
|
1636
|
+
const callsToExecute: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
|
|
1637
|
+
const deniedResults: Array<{ callId: string; toolName: string; error: string }> = [];
|
|
1638
|
+
|
|
1639
|
+
for (const a of decidedApprovals) {
|
|
1640
|
+
if (a.decision === "approved" && a.toolCallId) {
|
|
1641
|
+
callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
|
|
1642
|
+
const toolText = `- done \`${a.tool}\``;
|
|
1643
|
+
toolTimeline.push(toolText);
|
|
1644
|
+
currentTools.push(toolText);
|
|
1645
|
+
} else if (a.toolCallId) {
|
|
1646
|
+
deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
|
|
1647
|
+
const toolText = `- denied \`${a.tool}\``;
|
|
1648
|
+
toolTimeline.push(toolText);
|
|
1649
|
+
currentTools.push(toolText);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Auto-approved tools that were deferred alongside approval-needing ones
|
|
1654
|
+
const pendingToolCalls = checkpointRef.pendingToolCalls ?? [];
|
|
1655
|
+
for (const tc of pendingToolCalls) {
|
|
1656
|
+
if (!approvalToolCallIds.has(tc.id)) {
|
|
1657
|
+
callsToExecute.push(tc);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
let toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }> = [...deniedResults];
|
|
1662
|
+
if (callsToExecute.length > 0) {
|
|
1663
|
+
const execResults = await childHarness.executeTools(callsToExecute, toolContext);
|
|
1664
|
+
toolResults.push(...execResults.map(r => ({
|
|
1625
1665
|
callId: r.callId,
|
|
1626
1666
|
toolName: r.tool,
|
|
1627
1667
|
result: r.output,
|
|
1628
1668
|
error: r.error,
|
|
1629
|
-
}));
|
|
1630
|
-
const toolText = `- done \`${cpData.tool}\``;
|
|
1631
|
-
toolTimeline.push(toolText);
|
|
1632
|
-
currentTools.push(toolText);
|
|
1633
|
-
} else {
|
|
1634
|
-
toolResults = [{
|
|
1635
|
-
callId: cpData.toolCallId!,
|
|
1636
|
-
toolName: cpData.tool,
|
|
1637
|
-
error: "Tool execution denied by user",
|
|
1638
|
-
}];
|
|
1639
|
-
const toolText = `- denied \`${cpData.tool}\``;
|
|
1640
|
-
toolTimeline.push(toolText);
|
|
1641
|
-
currentTools.push(toolText);
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
broadcastEvent(childConversationId,
|
|
1645
|
-
approved
|
|
1646
|
-
? { type: "tool:approval:granted", approvalId: event.approvalId }
|
|
1647
|
-
: { type: "tool:approval:denied", approvalId: event.approvalId },
|
|
1648
|
-
);
|
|
1649
|
-
|
|
1650
|
-
// Clear pending approval from conversation
|
|
1651
|
-
const cpConv2 = await conversationStore.get(childConversationId);
|
|
1652
|
-
if (cpConv2) {
|
|
1653
|
-
cpConv2.pendingApprovals = [];
|
|
1654
|
-
await conversationStore.update(cpConv2);
|
|
1669
|
+
})));
|
|
1655
1670
|
}
|
|
1656
1671
|
|
|
1657
1672
|
// Resume the run -- process continuation events
|
|
1658
|
-
const resumeMessages = [...
|
|
1673
|
+
const resumeMessages = [...checkpointRef.checkpointMessages!];
|
|
1659
1674
|
for await (const resumeEvent of childHarness.continueFromToolResult({
|
|
1660
1675
|
messages: resumeMessages,
|
|
1661
1676
|
toolResults,
|
|
@@ -1980,16 +1995,16 @@ export const createRequestHandler = async (options?: {
|
|
|
1980
1995
|
if (event.type === "tool:approval:checkpoint") {
|
|
1981
1996
|
const conv = await conversationStore.get(conversationId);
|
|
1982
1997
|
if (conv) {
|
|
1983
|
-
conv.pendingApprovals =
|
|
1984
|
-
approvalId:
|
|
1998
|
+
conv.pendingApprovals = event.approvals.map(a => ({
|
|
1999
|
+
approvalId: a.approvalId,
|
|
1985
2000
|
runId: latestRunId,
|
|
1986
|
-
tool:
|
|
1987
|
-
toolCallId:
|
|
1988
|
-
input:
|
|
2001
|
+
tool: a.tool,
|
|
2002
|
+
toolCallId: a.toolCallId,
|
|
2003
|
+
input: a.input,
|
|
1989
2004
|
checkpointMessages: [...fullCheckpointMessages, ...event.checkpointMessages],
|
|
1990
2005
|
baseMessageCount: 0,
|
|
1991
2006
|
pendingToolCalls: event.pendingToolCalls,
|
|
1992
|
-
}
|
|
2007
|
+
}));
|
|
1993
2008
|
conv.updatedAt = Date.now();
|
|
1994
2009
|
await conversationStore.update(conv);
|
|
1995
2010
|
}
|
|
@@ -2257,16 +2272,16 @@ export const createRequestHandler = async (options?: {
|
|
|
2257
2272
|
if (event.type === "tool:approval:checkpoint") {
|
|
2258
2273
|
await updateConversation((c) => {
|
|
2259
2274
|
c.messages = buildMessages();
|
|
2260
|
-
c.pendingApprovals =
|
|
2261
|
-
approvalId:
|
|
2275
|
+
c.pendingApprovals = event.approvals.map(a => ({
|
|
2276
|
+
approvalId: a.approvalId,
|
|
2262
2277
|
runId: latestRunId,
|
|
2263
|
-
tool:
|
|
2264
|
-
toolCallId:
|
|
2265
|
-
input:
|
|
2278
|
+
tool: a.tool,
|
|
2279
|
+
toolCallId: a.toolCallId,
|
|
2280
|
+
input: a.input,
|
|
2266
2281
|
checkpointMessages: event.checkpointMessages,
|
|
2267
2282
|
baseMessageCount: historyMessages.length,
|
|
2268
2283
|
pendingToolCalls: event.pendingToolCalls,
|
|
2269
|
-
}
|
|
2284
|
+
}));
|
|
2270
2285
|
});
|
|
2271
2286
|
checkpointedRun = true;
|
|
2272
2287
|
}
|
|
@@ -2346,9 +2361,9 @@ export const createRequestHandler = async (options?: {
|
|
|
2346
2361
|
waitUntil: waitUntilHook,
|
|
2347
2362
|
ownerId: "local-owner",
|
|
2348
2363
|
});
|
|
2349
|
-
adapter.registerRoutes(messagingRouteRegistrar);
|
|
2350
2364
|
try {
|
|
2351
2365
|
await bridge.start();
|
|
2366
|
+
adapter.registerRoutes(messagingRouteRegistrar);
|
|
2352
2367
|
messagingBridges.push(bridge);
|
|
2353
2368
|
console.log(` Slack messaging enabled at /api/messaging/slack`);
|
|
2354
2369
|
} catch (err) {
|
|
@@ -2361,6 +2376,7 @@ export const createRequestHandler = async (options?: {
|
|
|
2361
2376
|
apiKeyEnv: channelConfig.apiKeyEnv,
|
|
2362
2377
|
webhookSecretEnv: channelConfig.webhookSecretEnv,
|
|
2363
2378
|
fromEnv: channelConfig.fromEnv,
|
|
2379
|
+
replyToEnv: channelConfig.replyToEnv,
|
|
2364
2380
|
allowedSenders: channelConfig.allowedSenders,
|
|
2365
2381
|
mode: channelConfig.mode,
|
|
2366
2382
|
allowedRecipients: channelConfig.allowedRecipients,
|
|
@@ -2372,9 +2388,9 @@ export const createRequestHandler = async (options?: {
|
|
|
2372
2388
|
waitUntil: waitUntilHook,
|
|
2373
2389
|
ownerId: "local-owner",
|
|
2374
2390
|
});
|
|
2375
|
-
adapter.registerRoutes(messagingRouteRegistrar);
|
|
2376
2391
|
try {
|
|
2377
2392
|
await bridge.start();
|
|
2393
|
+
adapter.registerRoutes(messagingRouteRegistrar);
|
|
2378
2394
|
messagingBridges.push(bridge);
|
|
2379
2395
|
const adapterTools = adapter.getToolDefinitions?.() ?? [];
|
|
2380
2396
|
if (adapterTools.length > 0) {
|
|
@@ -2399,6 +2415,10 @@ export const createRequestHandler = async (options?: {
|
|
|
2399
2415
|
const authRequired = config?.auth?.required ?? false;
|
|
2400
2416
|
const requireAuth = authRequired && authToken.length > 0;
|
|
2401
2417
|
|
|
2418
|
+
if (requireAuth) {
|
|
2419
|
+
sessionStore.setSigningKey(authToken);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2402
2422
|
const webUiEnabled = config?.webUi !== false;
|
|
2403
2423
|
const isProduction = resolveHarnessEnvironment() === "production";
|
|
2404
2424
|
const secureCookies = isProduction;
|
|
@@ -2487,8 +2507,10 @@ export const createRequestHandler = async (options?: {
|
|
|
2487
2507
|
}
|
|
2488
2508
|
|
|
2489
2509
|
const cookies = parseCookies(request);
|
|
2490
|
-
const
|
|
2491
|
-
const session =
|
|
2510
|
+
const cookieValue = cookies.poncho_session;
|
|
2511
|
+
const session = cookieValue
|
|
2512
|
+
? (sessionStore.get(cookieValue) ?? sessionStore.restoreFromSigned(cookieValue))
|
|
2513
|
+
: undefined;
|
|
2492
2514
|
const ownerId = session?.ownerId ?? "local-owner";
|
|
2493
2515
|
const requiresCsrfValidation =
|
|
2494
2516
|
request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS";
|
|
@@ -2539,7 +2561,8 @@ export const createRequestHandler = async (options?: {
|
|
|
2539
2561
|
}
|
|
2540
2562
|
loginRateLimiter.registerSuccess(ip);
|
|
2541
2563
|
const createdSession = sessionStore.create(ownerId);
|
|
2542
|
-
|
|
2564
|
+
const signedValue = sessionStore.signSession(createdSession);
|
|
2565
|
+
setCookie(response, "poncho_session", signedValue ?? createdSession.sessionId, {
|
|
2543
2566
|
httpOnly: true,
|
|
2544
2567
|
secure: secureCookies,
|
|
2545
2568
|
sameSite: "Lax",
|
|
@@ -2781,14 +2804,32 @@ export const createRequestHandler = async (options?: {
|
|
|
2781
2804
|
// Check if this is a pending subagent approval (handled inline by runSubagent)
|
|
2782
2805
|
const pendingSubagent = pendingSubagentApprovals.get(approvalId);
|
|
2783
2806
|
if (pendingSubagent) {
|
|
2784
|
-
|
|
2785
|
-
|
|
2807
|
+
pendingSubagent.checkpoint.decision = approved ? "approved" : "denied";
|
|
2808
|
+
|
|
2809
|
+
broadcastEvent(pendingSubagent.childConversationId,
|
|
2810
|
+
approved
|
|
2811
|
+
? { type: "tool:approval:granted", approvalId }
|
|
2812
|
+
: { type: "tool:approval:denied", approvalId },
|
|
2813
|
+
);
|
|
2814
|
+
|
|
2786
2815
|
const childConv = await conversationStore.get(pendingSubagent.childConversationId);
|
|
2787
|
-
|
|
2788
|
-
|
|
2816
|
+
const allApprovals = childConv?.pendingApprovals ?? [];
|
|
2817
|
+
const allDecided = allApprovals.length > 0 && allApprovals.every(pa => pa.decision != null);
|
|
2818
|
+
|
|
2819
|
+
if (allDecided) {
|
|
2820
|
+
// All approvals in the batch are decided — resolve the subagent
|
|
2821
|
+
for (const pa of allApprovals) {
|
|
2822
|
+
pendingSubagentApprovals.delete(pa.approvalId);
|
|
2823
|
+
}
|
|
2824
|
+
if (childConv) {
|
|
2825
|
+
childConv.pendingApprovals = [];
|
|
2826
|
+
await conversationStore.update(childConv);
|
|
2827
|
+
}
|
|
2828
|
+
pendingSubagent.resolve(allApprovals);
|
|
2829
|
+
} else if (childConv) {
|
|
2789
2830
|
await conversationStore.update(childConv);
|
|
2790
2831
|
}
|
|
2791
|
-
|
|
2832
|
+
|
|
2792
2833
|
writeJson(response, 200, { ok: true, approvalId, approved });
|
|
2793
2834
|
return;
|
|
2794
2835
|
}
|
|
@@ -2818,7 +2859,6 @@ export const createRequestHandler = async (options?: {
|
|
|
2818
2859
|
const conversationId = foundConversation.conversationId;
|
|
2819
2860
|
|
|
2820
2861
|
if (!foundApproval.checkpointMessages || !foundApproval.toolCallId) {
|
|
2821
|
-
// Legacy approval without checkpoint data — cannot resume
|
|
2822
2862
|
foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? [])
|
|
2823
2863
|
.filter(a => a.approvalId !== approvalId);
|
|
2824
2864
|
await conversationStore.update(foundConversation);
|
|
@@ -2829,55 +2869,82 @@ export const createRequestHandler = async (options?: {
|
|
|
2829
2869
|
return;
|
|
2830
2870
|
}
|
|
2831
2871
|
|
|
2832
|
-
//
|
|
2833
|
-
|
|
2834
|
-
.filter(a => a.approvalId !== approvalId);
|
|
2835
|
-
await conversationStore.update(foundConversation);
|
|
2872
|
+
// Record the decision on this approval entry
|
|
2873
|
+
foundApproval.decision = approved ? "approved" : "denied";
|
|
2836
2874
|
|
|
2837
|
-
// Initialize the event stream so the web UI can connect immediately
|
|
2838
2875
|
broadcastEvent(conversationId,
|
|
2839
2876
|
approved
|
|
2840
2877
|
? { type: "tool:approval:granted", approvalId }
|
|
2841
2878
|
: { type: "tool:approval:denied", approvalId },
|
|
2842
2879
|
);
|
|
2843
2880
|
|
|
2844
|
-
|
|
2881
|
+
const allApprovals = foundConversation.pendingApprovals ?? [];
|
|
2882
|
+
const allDecided = allApprovals.length > 0 && allApprovals.every(a => a.decision != null);
|
|
2883
|
+
|
|
2884
|
+
if (!allDecided) {
|
|
2885
|
+
// Still waiting for more decisions — persist and respond
|
|
2886
|
+
await conversationStore.update(foundConversation);
|
|
2887
|
+
writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
// All approvals in the batch are decided — execute and resume
|
|
2892
|
+
foundConversation.pendingApprovals = [];
|
|
2893
|
+
await conversationStore.update(foundConversation);
|
|
2894
|
+
|
|
2895
|
+
// Use the first approval as the checkpoint reference (all share the same checkpoint data)
|
|
2896
|
+
const checkpointRef = allApprovals[0]!;
|
|
2897
|
+
|
|
2845
2898
|
void (async () => {
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2899
|
+
const toolContext = {
|
|
2900
|
+
runId: checkpointRef.runId,
|
|
2901
|
+
agentId: identity.id,
|
|
2902
|
+
step: 0,
|
|
2903
|
+
workingDir,
|
|
2904
|
+
parameters: {},
|
|
2905
|
+
};
|
|
2906
|
+
|
|
2907
|
+
// Collect tool calls to execute: approved approval-gated tools + auto-approved deferred tools
|
|
2908
|
+
const approvalToolCallIds = new Set(allApprovals.map(a => a.toolCallId));
|
|
2909
|
+
const callsToExecute: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
|
|
2910
|
+
const deniedResults: Array<{ callId: string; toolName: string; error: string }> = [];
|
|
2911
|
+
|
|
2912
|
+
for (const a of allApprovals) {
|
|
2913
|
+
if (a.decision === "approved" && a.toolCallId) {
|
|
2914
|
+
callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
|
|
2915
|
+
} else if (a.decision === "denied" && a.toolCallId) {
|
|
2916
|
+
deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// Auto-approved tools that were deferred alongside the approval-needing ones
|
|
2921
|
+
const pendingToolCalls = checkpointRef.pendingToolCalls ?? [];
|
|
2922
|
+
for (const tc of pendingToolCalls) {
|
|
2923
|
+
if (!approvalToolCallIds.has(tc.id)) {
|
|
2924
|
+
callsToExecute.push(tc);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
let toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }> = [...deniedResults];
|
|
2929
|
+
if (callsToExecute.length > 0) {
|
|
2930
|
+
const execResults = await harness.executeTools(callsToExecute, toolContext);
|
|
2931
|
+
toolResults.push(...execResults.map(r => ({
|
|
2860
2932
|
callId: r.callId,
|
|
2861
2933
|
toolName: r.tool,
|
|
2862
2934
|
result: r.output,
|
|
2863
2935
|
error: r.error,
|
|
2864
|
-
}));
|
|
2865
|
-
} else {
|
|
2866
|
-
toolResults = [{
|
|
2867
|
-
callId: foundApproval.toolCallId!,
|
|
2868
|
-
toolName: foundApproval.tool,
|
|
2869
|
-
error: "Tool execution denied by user",
|
|
2870
|
-
}];
|
|
2936
|
+
})));
|
|
2871
2937
|
}
|
|
2938
|
+
|
|
2872
2939
|
await resumeRunFromCheckpoint(
|
|
2873
2940
|
conversationId,
|
|
2874
2941
|
foundConversation!,
|
|
2875
|
-
|
|
2942
|
+
checkpointRef,
|
|
2876
2943
|
toolResults,
|
|
2877
2944
|
);
|
|
2878
2945
|
})();
|
|
2879
2946
|
|
|
2880
|
-
writeJson(response, 200, { ok: true, approvalId, approved });
|
|
2947
|
+
writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: true });
|
|
2881
2948
|
return;
|
|
2882
2949
|
}
|
|
2883
2950
|
|
|
@@ -3396,16 +3463,16 @@ export const createRequestHandler = async (options?: {
|
|
|
3396
3463
|
}]
|
|
3397
3464
|
: []),
|
|
3398
3465
|
];
|
|
3399
|
-
conversation.pendingApprovals =
|
|
3400
|
-
approvalId:
|
|
3466
|
+
conversation.pendingApprovals = event.approvals.map(a => ({
|
|
3467
|
+
approvalId: a.approvalId,
|
|
3401
3468
|
runId: latestRunId,
|
|
3402
|
-
tool:
|
|
3403
|
-
toolCallId:
|
|
3404
|
-
input:
|
|
3469
|
+
tool: a.tool,
|
|
3470
|
+
toolCallId: a.toolCallId,
|
|
3471
|
+
input: a.input,
|
|
3405
3472
|
checkpointMessages: event.checkpointMessages,
|
|
3406
3473
|
baseMessageCount: historyMessages.length,
|
|
3407
3474
|
pendingToolCalls: event.pendingToolCalls,
|
|
3408
|
-
}
|
|
3475
|
+
}));
|
|
3409
3476
|
conversation.updatedAt = Date.now();
|
|
3410
3477
|
await conversationStore.update(conversation);
|
|
3411
3478
|
checkpointedRun = true;
|
|
@@ -3753,11 +3820,14 @@ export const createRequestHandler = async (options?: {
|
|
|
3753
3820
|
handler._cronJobs = cronJobs;
|
|
3754
3821
|
handler._conversationStore = conversationStore;
|
|
3755
3822
|
|
|
3756
|
-
// Recover stale subagent runs that were "running" when the server last stopped
|
|
3823
|
+
// Recover stale subagent runs that were "running" when the server last stopped.
|
|
3824
|
+
// Pass no ownerId so we scan across all owners (not just "local-owner").
|
|
3757
3825
|
try {
|
|
3758
|
-
const
|
|
3759
|
-
|
|
3760
|
-
|
|
3826
|
+
const allSummaries = await conversationStore.listSummaries();
|
|
3827
|
+
const subagentSummaries = allSummaries.filter((s) => s.parentConversationId);
|
|
3828
|
+
for (const s of subagentSummaries) {
|
|
3829
|
+
const conv = await conversationStore.get(s.conversationId);
|
|
3830
|
+
if (conv?.subagentMeta?.status === "running") {
|
|
3761
3831
|
conv.subagentMeta.status = "stopped";
|
|
3762
3832
|
conv.subagentMeta.error = { code: "SERVER_RESTART", message: "Interrupted by server restart" };
|
|
3763
3833
|
conv.updatedAt = Date.now();
|
|
@@ -4535,6 +4605,7 @@ export const mcpAdd = async (
|
|
|
4535
4605
|
name?: string;
|
|
4536
4606
|
envVars?: string[];
|
|
4537
4607
|
authBearerEnv?: string;
|
|
4608
|
+
headers?: string[];
|
|
4538
4609
|
},
|
|
4539
4610
|
): Promise<void> => {
|
|
4540
4611
|
const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
|
|
@@ -4548,6 +4619,18 @@ export const mcpAdd = async (
|
|
|
4548
4619
|
if (!options.url.startsWith("http://") && !options.url.startsWith("https://")) {
|
|
4549
4620
|
throw new Error("Invalid MCP URL. Expected http:// or https://.");
|
|
4550
4621
|
}
|
|
4622
|
+
const parsedHeaders: Record<string, string> | undefined =
|
|
4623
|
+
options.headers && options.headers.length > 0
|
|
4624
|
+
? Object.fromEntries(
|
|
4625
|
+
options.headers.map((h) => {
|
|
4626
|
+
const idx = h.indexOf(":");
|
|
4627
|
+
if (idx < 1) {
|
|
4628
|
+
throw new Error(`Invalid header format "${h}". Expected "Name: value".`);
|
|
4629
|
+
}
|
|
4630
|
+
return [h.slice(0, idx).trim(), h.slice(idx + 1).trim()];
|
|
4631
|
+
}),
|
|
4632
|
+
)
|
|
4633
|
+
: undefined;
|
|
4551
4634
|
const serverName = options.name ?? normalizeMcpName({ url: options.url });
|
|
4552
4635
|
mcp.push({
|
|
4553
4636
|
name: serverName,
|
|
@@ -4559,6 +4642,7 @@ export const mcpAdd = async (
|
|
|
4559
4642
|
tokenEnv: options.authBearerEnv,
|
|
4560
4643
|
}
|
|
4561
4644
|
: undefined,
|
|
4645
|
+
headers: parsedHeaders,
|
|
4562
4646
|
});
|
|
4563
4647
|
|
|
4564
4648
|
await writeConfigFile(workingDir, { ...config, mcp });
|
|
@@ -4605,8 +4689,10 @@ export const mcpList = async (workingDir: string): Promise<void> => {
|
|
|
4605
4689
|
for (const entry of mcp) {
|
|
4606
4690
|
const auth =
|
|
4607
4691
|
entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
|
|
4692
|
+
const headerKeys = entry.headers ? Object.keys(entry.headers) : [];
|
|
4693
|
+
const headerInfo = headerKeys.length > 0 ? `, headers=${headerKeys.join(",")}` : "";
|
|
4608
4694
|
process.stdout.write(
|
|
4609
|
-
`- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth})\n`,
|
|
4695
|
+
`- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}${headerInfo})\n`,
|
|
4610
4696
|
);
|
|
4611
4697
|
}
|
|
4612
4698
|
};
|
|
@@ -4933,6 +5019,10 @@ export const buildCli = (): Command => {
|
|
|
4933
5019
|
all.push(value);
|
|
4934
5020
|
return all;
|
|
4935
5021
|
}, [] as string[])
|
|
5022
|
+
.option("--header <header>", "custom header as 'Name: value' (repeatable)", (value, all: string[]) => {
|
|
5023
|
+
all.push(value);
|
|
5024
|
+
return all;
|
|
5025
|
+
}, [] as string[])
|
|
4936
5026
|
.action(
|
|
4937
5027
|
async (
|
|
4938
5028
|
options: {
|
|
@@ -4940,6 +5030,7 @@ export const buildCli = (): Command => {
|
|
|
4940
5030
|
name?: string;
|
|
4941
5031
|
authBearerEnv?: string;
|
|
4942
5032
|
env: string[];
|
|
5033
|
+
header: string[];
|
|
4943
5034
|
},
|
|
4944
5035
|
) => {
|
|
4945
5036
|
await mcpAdd(process.cwd(), {
|
|
@@ -4947,6 +5038,7 @@ export const buildCli = (): Command => {
|
|
|
4947
5038
|
name: options.name,
|
|
4948
5039
|
envVars: options.env,
|
|
4949
5040
|
authBearerEnv: options.authBearerEnv,
|
|
5041
|
+
headers: options.header,
|
|
4950
5042
|
});
|
|
4951
5043
|
},
|
|
4952
5044
|
);
|