@poncho-ai/cli 0.18.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 +5 -5
- package/CHANGELOG.md +16 -0
- package/dist/{chunk-ETZ3YKFP.js → chunk-7P53QSP5.js} +393 -145
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-VSO7GB3Y.js → run-interactive-ink-4KVVPMR3.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +180 -119
- package/src/web-ui-client.ts +132 -30
- package/src/web-ui-store.ts +68 -1
- package/src/web-ui-styles.ts +51 -5
- package/src/web-ui.ts +6 -3
|
@@ -310,6 +310,22 @@ var WEB_UI_STYLES = `
|
|
|
310
310
|
flex-direction: column;
|
|
311
311
|
padding: 12px 8px;
|
|
312
312
|
}
|
|
313
|
+
.sidebar-header {
|
|
314
|
+
display: flex;
|
|
315
|
+
align-items: center;
|
|
316
|
+
gap: 8px;
|
|
317
|
+
}
|
|
318
|
+
.sidebar-agent-name {
|
|
319
|
+
font-size: 14px;
|
|
320
|
+
font-weight: 500;
|
|
321
|
+
color: var(--fg-strong);
|
|
322
|
+
flex: 1;
|
|
323
|
+
min-width: 0;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
text-overflow: ellipsis;
|
|
326
|
+
white-space: nowrap;
|
|
327
|
+
padding-left: 10px;
|
|
328
|
+
}
|
|
313
329
|
.new-chat-btn {
|
|
314
330
|
background: transparent;
|
|
315
331
|
border: 0;
|
|
@@ -322,6 +338,7 @@ var WEB_UI_STYLES = `
|
|
|
322
338
|
gap: 8px;
|
|
323
339
|
font-size: 13px;
|
|
324
340
|
cursor: pointer;
|
|
341
|
+
flex-shrink: 0;
|
|
325
342
|
transition: background 0.15s, color 0.15s;
|
|
326
343
|
}
|
|
327
344
|
.new-chat-btn:hover { color: var(--fg); }
|
|
@@ -824,6 +841,30 @@ var WEB_UI_STYLES = `
|
|
|
824
841
|
opacity: 0.55;
|
|
825
842
|
cursor: not-allowed;
|
|
826
843
|
}
|
|
844
|
+
.approval-batch-actions {
|
|
845
|
+
display: flex;
|
|
846
|
+
gap: 6px;
|
|
847
|
+
margin-bottom: 8px;
|
|
848
|
+
}
|
|
849
|
+
.approval-batch-btn {
|
|
850
|
+
border-radius: 6px;
|
|
851
|
+
border: 1px solid var(--border-5);
|
|
852
|
+
background: var(--surface-4);
|
|
853
|
+
color: var(--fg-approval-btn);
|
|
854
|
+
font-size: 11px;
|
|
855
|
+
font-weight: 600;
|
|
856
|
+
padding: 4px 10px;
|
|
857
|
+
cursor: pointer;
|
|
858
|
+
}
|
|
859
|
+
.approval-batch-btn:hover { background: var(--surface-7); }
|
|
860
|
+
.approval-batch-btn.approve {
|
|
861
|
+
border-color: var(--approve-border);
|
|
862
|
+
color: var(--approve);
|
|
863
|
+
}
|
|
864
|
+
.approval-batch-btn.deny {
|
|
865
|
+
border-color: var(--deny-border);
|
|
866
|
+
color: var(--deny);
|
|
867
|
+
}
|
|
827
868
|
.user-bubble {
|
|
828
869
|
background: var(--bg-elevated);
|
|
829
870
|
border: 1px solid var(--border-2);
|
|
@@ -1220,6 +1261,9 @@ var WEB_UI_STYLES = `
|
|
|
1220
1261
|
.shell.sidebar-open .sidebar { transform: translateX(0); }
|
|
1221
1262
|
.sidebar-toggle { display: grid; place-items: center; }
|
|
1222
1263
|
.topbar-new-chat { display: grid; place-items: center; }
|
|
1264
|
+
.sidebar-header { padding-right: 130px; }
|
|
1265
|
+
.sidebar-agent-name { padding-left: 0; }
|
|
1266
|
+
.new-chat-btn { order: -1; }
|
|
1223
1267
|
.poncho-badge {
|
|
1224
1268
|
display: none;
|
|
1225
1269
|
position: fixed;
|
|
@@ -1359,15 +1403,17 @@ var WEB_UI_STYLES = `
|
|
|
1359
1403
|
font-size: 13px;
|
|
1360
1404
|
}
|
|
1361
1405
|
@media (max-width: 768px) {
|
|
1406
|
+
.main-body { flex-direction: column; }
|
|
1362
1407
|
.browser-panel {
|
|
1363
|
-
position:
|
|
1364
|
-
|
|
1365
|
-
|
|
1408
|
+
position: relative;
|
|
1409
|
+
order: -1;
|
|
1410
|
+
max-height: 35vh;
|
|
1366
1411
|
flex: none !important;
|
|
1367
|
-
|
|
1412
|
+
width: auto !important;
|
|
1413
|
+
border-bottom: 1px solid var(--border-1);
|
|
1368
1414
|
}
|
|
1369
1415
|
.browser-panel-resize { display: none !important; }
|
|
1370
|
-
.main-chat.has-browser { flex: 1 1 auto !important; min-width: 0; }
|
|
1416
|
+
.main-chat.has-browser { flex: 1 1 auto !important; min-width: 0; min-height: 0; }
|
|
1371
1417
|
}
|
|
1372
1418
|
|
|
1373
1419
|
/* --- Subagent UI --- */
|
|
@@ -1649,6 +1695,62 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
1649
1695
|
}
|
|
1650
1696
|
};
|
|
1651
1697
|
|
|
1698
|
+
// During streaming, incomplete backtick sequences cause marked to
|
|
1699
|
+
// swallow all subsequent text into an invisible code element. This
|
|
1700
|
+
// helper detects unclosed fences and inline code delimiters and
|
|
1701
|
+
// appends the missing closing so marked can render partial text.
|
|
1702
|
+
const closeStreamingMarkdown = (text) => {
|
|
1703
|
+
const BT = "\\x60";
|
|
1704
|
+
let result = text;
|
|
1705
|
+
|
|
1706
|
+
// 1. Unclosed fenced code blocks (lines starting with 3+ backticks)
|
|
1707
|
+
const lines = result.split("\\n");
|
|
1708
|
+
let openFenceLen = 0;
|
|
1709
|
+
for (let li = 0; li < lines.length; li++) {
|
|
1710
|
+
const trimmed = lines[li].trimStart();
|
|
1711
|
+
let btCount = 0;
|
|
1712
|
+
while (btCount < trimmed.length && trimmed[btCount] === BT) btCount++;
|
|
1713
|
+
if (btCount >= 3) {
|
|
1714
|
+
if (openFenceLen === 0) {
|
|
1715
|
+
openFenceLen = btCount;
|
|
1716
|
+
} else if (btCount >= openFenceLen) {
|
|
1717
|
+
openFenceLen = 0;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (openFenceLen > 0) {
|
|
1722
|
+
let fence = "";
|
|
1723
|
+
for (let k = 0; k < openFenceLen; k++) fence += BT;
|
|
1724
|
+
return result + "\\n" + fence;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// 2. Unclosed inline code delimiters
|
|
1728
|
+
let idx = 0;
|
|
1729
|
+
let inCode = false;
|
|
1730
|
+
let delimLen = 0;
|
|
1731
|
+
while (idx < result.length) {
|
|
1732
|
+
if (result[idx] === BT) {
|
|
1733
|
+
let run = 0;
|
|
1734
|
+
while (idx < result.length && result[idx] === BT) { run++; idx++; }
|
|
1735
|
+
if (!inCode) {
|
|
1736
|
+
inCode = true;
|
|
1737
|
+
delimLen = run;
|
|
1738
|
+
} else if (run === delimLen) {
|
|
1739
|
+
inCode = false;
|
|
1740
|
+
}
|
|
1741
|
+
} else {
|
|
1742
|
+
idx++;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (inCode) {
|
|
1746
|
+
let closing = "";
|
|
1747
|
+
for (let k = 0; k < delimLen; k++) closing += BT;
|
|
1748
|
+
result += closing;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
return result;
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1652
1754
|
const extractToolActivity = (value) => {
|
|
1653
1755
|
const source = String(value || "");
|
|
1654
1756
|
let markerIndex = source.lastIndexOf("\\n### Tool activity\\n");
|
|
@@ -1715,8 +1817,15 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
1715
1817
|
);
|
|
1716
1818
|
})
|
|
1717
1819
|
.join("");
|
|
1820
|
+
const batchButtons = requests.length > 1
|
|
1821
|
+
? '<div class="approval-batch-actions">' +
|
|
1822
|
+
'<button class="approval-batch-btn approve" data-approval-batch="approve">Approve all (' + requests.length + ')</button>' +
|
|
1823
|
+
'<button class="approval-batch-btn deny" data-approval-batch="deny">Deny all (' + requests.length + ')</button>' +
|
|
1824
|
+
"</div>"
|
|
1825
|
+
: "";
|
|
1718
1826
|
return (
|
|
1719
1827
|
'<div class="approval-requests">' +
|
|
1828
|
+
batchButtons +
|
|
1720
1829
|
rows +
|
|
1721
1830
|
"</div>"
|
|
1722
1831
|
);
|
|
@@ -2246,7 +2355,7 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
2246
2355
|
// Show current text being typed
|
|
2247
2356
|
if (isStreaming && i === messages.length - 1 && m._currentText) {
|
|
2248
2357
|
const textDiv = document.createElement("div");
|
|
2249
|
-
textDiv.innerHTML = renderAssistantMarkdown(m._currentText);
|
|
2358
|
+
textDiv.innerHTML = renderAssistantMarkdown(closeStreamingMarkdown(m._currentText));
|
|
2250
2359
|
content.appendChild(textDiv);
|
|
2251
2360
|
}
|
|
2252
2361
|
} else {
|
|
@@ -3828,45 +3937,20 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
3828
3937
|
openLightbox(img.src);
|
|
3829
3938
|
});
|
|
3830
3939
|
|
|
3831
|
-
|
|
3832
|
-
const
|
|
3833
|
-
if (!(target instanceof Element)) {
|
|
3834
|
-
return;
|
|
3835
|
-
}
|
|
3836
|
-
const button = target.closest(".approval-action-btn");
|
|
3837
|
-
if (!button) {
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
const approvalId = button.getAttribute("data-approval-id") || "";
|
|
3841
|
-
const decision = button.getAttribute("data-approval-decision") || "";
|
|
3842
|
-
if (!approvalId || (decision !== "approve" && decision !== "deny")) {
|
|
3843
|
-
return;
|
|
3844
|
-
}
|
|
3845
|
-
if (state.approvalRequestsInFlight[approvalId]) {
|
|
3846
|
-
return;
|
|
3847
|
-
}
|
|
3940
|
+
const submitApproval = async (approvalId, decision, opts) => {
|
|
3941
|
+
const wasStreaming = opts && opts.wasStreaming;
|
|
3848
3942
|
state.approvalRequestsInFlight[approvalId] = true;
|
|
3849
|
-
const wasStreaming = state.isStreaming;
|
|
3850
|
-
if (!wasStreaming) {
|
|
3851
|
-
setStreaming(true);
|
|
3852
|
-
}
|
|
3853
3943
|
updatePendingApproval(approvalId, (request) => ({
|
|
3854
3944
|
...request,
|
|
3855
3945
|
state: "submitting",
|
|
3856
3946
|
pendingDecision: decision,
|
|
3857
3947
|
}));
|
|
3858
|
-
renderMessages(state.activeMessages, state.isStreaming);
|
|
3859
3948
|
try {
|
|
3860
3949
|
await api("/api/approvals/" + encodeURIComponent(approvalId), {
|
|
3861
3950
|
method: "POST",
|
|
3862
3951
|
body: JSON.stringify({ approved: decision === "approve" }),
|
|
3863
3952
|
});
|
|
3864
3953
|
updatePendingApproval(approvalId, () => null);
|
|
3865
|
-
renderMessages(state.activeMessages, state.isStreaming);
|
|
3866
|
-
loadConversations();
|
|
3867
|
-
if (!wasStreaming && state.activeConversationId) {
|
|
3868
|
-
await streamConversationEvents(state.activeConversationId, { liveOnly: true });
|
|
3869
|
-
}
|
|
3870
3954
|
} catch (error) {
|
|
3871
3955
|
const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
|
|
3872
3956
|
if (isStale) {
|
|
@@ -3880,13 +3964,77 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
3880
3964
|
_error: errMsg,
|
|
3881
3965
|
}));
|
|
3882
3966
|
}
|
|
3883
|
-
renderMessages(state.activeMessages, state.isStreaming);
|
|
3884
3967
|
} finally {
|
|
3968
|
+
delete state.approvalRequestsInFlight[approvalId];
|
|
3969
|
+
}
|
|
3970
|
+
};
|
|
3971
|
+
|
|
3972
|
+
elements.messages.addEventListener("click", async (event) => {
|
|
3973
|
+
const target = event.target;
|
|
3974
|
+
if (!(target instanceof Element)) {
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
// Batch approve/deny all
|
|
3979
|
+
const batchBtn = target.closest(".approval-batch-btn");
|
|
3980
|
+
if (batchBtn) {
|
|
3981
|
+
const decision = batchBtn.getAttribute("data-approval-batch") || "";
|
|
3982
|
+
if (decision !== "approve" && decision !== "deny") return;
|
|
3983
|
+
const messages = state.activeMessages || [];
|
|
3984
|
+
const pending = [];
|
|
3985
|
+
for (const m of messages) {
|
|
3986
|
+
if (Array.isArray(m._pendingApprovals)) {
|
|
3987
|
+
for (const req of m._pendingApprovals) {
|
|
3988
|
+
if (req.approvalId && req.state !== "submitting" && !state.approvalRequestsInFlight[req.approvalId]) {
|
|
3989
|
+
pending.push(req.approvalId);
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
if (pending.length === 0) return;
|
|
3995
|
+
const wasStreaming = state.isStreaming;
|
|
3996
|
+
if (!wasStreaming) setStreaming(true);
|
|
3997
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
3998
|
+
await Promise.all(pending.map((aid) => submitApproval(aid, decision, { wasStreaming })));
|
|
3999
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
4000
|
+
loadConversations();
|
|
4001
|
+
if (!wasStreaming && state.activeConversationId) {
|
|
4002
|
+
await streamConversationEvents(state.activeConversationId, { liveOnly: true });
|
|
4003
|
+
}
|
|
3885
4004
|
if (!wasStreaming) {
|
|
3886
4005
|
setStreaming(false);
|
|
3887
4006
|
renderMessages(state.activeMessages, false);
|
|
3888
4007
|
}
|
|
3889
|
-
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
// Individual approve/deny
|
|
4012
|
+
const button = target.closest(".approval-action-btn");
|
|
4013
|
+
if (!button) {
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
const approvalId = button.getAttribute("data-approval-id") || "";
|
|
4017
|
+
const decision = button.getAttribute("data-approval-decision") || "";
|
|
4018
|
+
if (!approvalId || (decision !== "approve" && decision !== "deny")) {
|
|
4019
|
+
return;
|
|
4020
|
+
}
|
|
4021
|
+
if (state.approvalRequestsInFlight[approvalId]) {
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
const wasStreaming = state.isStreaming;
|
|
4025
|
+
if (!wasStreaming) {
|
|
4026
|
+
setStreaming(true);
|
|
4027
|
+
}
|
|
4028
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
4029
|
+
await submitApproval(approvalId, decision, { wasStreaming });
|
|
4030
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
4031
|
+
loadConversations();
|
|
4032
|
+
if (!wasStreaming && state.activeConversationId) {
|
|
4033
|
+
await streamConversationEvents(state.activeConversationId, { liveOnly: true });
|
|
4034
|
+
}
|
|
4035
|
+
if (!wasStreaming) {
|
|
4036
|
+
setStreaming(false);
|
|
4037
|
+
renderMessages(state.activeMessages, false);
|
|
3890
4038
|
}
|
|
3891
4039
|
});
|
|
3892
4040
|
|
|
@@ -4443,7 +4591,7 @@ var getWebUiClientScript = (markedSource2) => `
|
|
|
4443
4591
|
`;
|
|
4444
4592
|
|
|
4445
4593
|
// src/web-ui-store.ts
|
|
4446
|
-
import { createHash, randomUUID, timingSafeEqual } from "crypto";
|
|
4594
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
4447
4595
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4448
4596
|
import { basename, dirname, resolve } from "path";
|
|
4449
4597
|
import { homedir } from "os";
|
|
@@ -4451,9 +4599,13 @@ var DEFAULT_OWNER = "local-owner";
|
|
|
4451
4599
|
var SessionStore = class {
|
|
4452
4600
|
sessions = /* @__PURE__ */ new Map();
|
|
4453
4601
|
ttlMs;
|
|
4602
|
+
signingKey;
|
|
4454
4603
|
constructor(ttlMs = 1e3 * 60 * 60 * 8) {
|
|
4455
4604
|
this.ttlMs = ttlMs;
|
|
4456
4605
|
}
|
|
4606
|
+
setSigningKey(key) {
|
|
4607
|
+
if (key) this.signingKey = key;
|
|
4608
|
+
}
|
|
4457
4609
|
create(ownerId = DEFAULT_OWNER) {
|
|
4458
4610
|
const now = Date.now();
|
|
4459
4611
|
const session = {
|
|
@@ -4482,6 +4634,57 @@ var SessionStore = class {
|
|
|
4482
4634
|
delete(sessionId) {
|
|
4483
4635
|
this.sessions.delete(sessionId);
|
|
4484
4636
|
}
|
|
4637
|
+
/**
|
|
4638
|
+
* Encode a session into a signed cookie value that survives serverless
|
|
4639
|
+
* cold starts. Format: `base64url(payload).signature`
|
|
4640
|
+
*/
|
|
4641
|
+
signSession(session) {
|
|
4642
|
+
if (!this.signingKey) return void 0;
|
|
4643
|
+
const payload = Buffer.from(
|
|
4644
|
+
JSON.stringify({
|
|
4645
|
+
sid: session.sessionId,
|
|
4646
|
+
o: session.ownerId,
|
|
4647
|
+
csrf: session.csrfToken,
|
|
4648
|
+
exp: session.expiresAt
|
|
4649
|
+
})
|
|
4650
|
+
).toString("base64url");
|
|
4651
|
+
const sig = createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
4652
|
+
return `${payload}.${sig}`;
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* Restore a session from a signed cookie value. Returns the session
|
|
4656
|
+
* (also added to the in-memory store) or undefined if invalid/expired.
|
|
4657
|
+
*/
|
|
4658
|
+
restoreFromSigned(cookieValue) {
|
|
4659
|
+
if (!this.signingKey) return void 0;
|
|
4660
|
+
const dotIdx = cookieValue.lastIndexOf(".");
|
|
4661
|
+
if (dotIdx <= 0) return void 0;
|
|
4662
|
+
const payload = cookieValue.slice(0, dotIdx);
|
|
4663
|
+
const sig = cookieValue.slice(dotIdx + 1);
|
|
4664
|
+
const expected = createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
4665
|
+
if (sig.length !== expected.length) return void 0;
|
|
4666
|
+
if (!timingSafeEqual(Buffer.from(sig, "utf8"), Buffer.from(expected, "utf8")))
|
|
4667
|
+
return void 0;
|
|
4668
|
+
try {
|
|
4669
|
+
const data = JSON.parse(
|
|
4670
|
+
Buffer.from(payload, "base64url").toString("utf8")
|
|
4671
|
+
);
|
|
4672
|
+
if (!data.sid || !data.o || !data.csrf || !data.exp) return void 0;
|
|
4673
|
+
if (Date.now() > data.exp) return void 0;
|
|
4674
|
+
const session = {
|
|
4675
|
+
sessionId: data.sid,
|
|
4676
|
+
ownerId: data.o,
|
|
4677
|
+
csrfToken: data.csrf,
|
|
4678
|
+
createdAt: data.exp - this.ttlMs,
|
|
4679
|
+
expiresAt: data.exp,
|
|
4680
|
+
lastSeenAt: Date.now()
|
|
4681
|
+
};
|
|
4682
|
+
this.sessions.set(session.sessionId, session);
|
|
4683
|
+
return session;
|
|
4684
|
+
} catch {
|
|
4685
|
+
return void 0;
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4485
4688
|
};
|
|
4486
4689
|
var LoginRateLimiter = class {
|
|
4487
4690
|
constructor(maxAttempts = 5, windowMs = 1e3 * 60 * 5, lockoutMs = 1e3 * 60 * 10) {
|
|
@@ -4698,9 +4901,12 @@ ${WEB_UI_STYLES}
|
|
|
4698
4901
|
|
|
4699
4902
|
<div id="app" class="shell hidden">
|
|
4700
4903
|
<aside class="sidebar">
|
|
4701
|
-
<
|
|
4702
|
-
<
|
|
4703
|
-
|
|
4904
|
+
<div class="sidebar-header">
|
|
4905
|
+
<span class="sidebar-agent-name">${agentName}</span>
|
|
4906
|
+
<button id="new-chat" class="new-chat-btn">
|
|
4907
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
4908
|
+
</button>
|
|
4909
|
+
</div>
|
|
4704
4910
|
<div id="conversation-list" class="conversation-list"></div>
|
|
4705
4911
|
<div class="sidebar-footer">
|
|
4706
4912
|
<button id="logout" class="logout-btn">Log out</button>
|
|
@@ -6305,11 +6511,17 @@ export default {
|
|
|
6305
6511
|
],
|
|
6306
6512
|
// Tool access: true (available), false (disabled), 'approval' (requires human approval)
|
|
6307
6513
|
tools: {
|
|
6514
|
+
list_directory: true,
|
|
6515
|
+
read_file: true,
|
|
6308
6516
|
write_file: true, // gated by environment for writes
|
|
6517
|
+
delete_file: 'approval', // requires human approval
|
|
6518
|
+
delete_directory: 'approval', // requires human approval
|
|
6309
6519
|
send_email: 'approval', // requires human approval
|
|
6310
6520
|
byEnvironment: {
|
|
6311
6521
|
production: {
|
|
6312
6522
|
write_file: false,
|
|
6523
|
+
delete_file: false,
|
|
6524
|
+
delete_directory: false,
|
|
6313
6525
|
},
|
|
6314
6526
|
development: {
|
|
6315
6527
|
send_email: true, // skip approval in dev
|
|
@@ -6392,6 +6604,7 @@ Connect your agent to email so users can interact by sending emails:
|
|
|
6392
6604
|
RESEND_API_KEY=re_...
|
|
6393
6605
|
RESEND_WEBHOOK_SECRET=whsec_...
|
|
6394
6606
|
RESEND_FROM=Agent <agent@yourdomain.com>
|
|
6607
|
+
RESEND_REPLY_TO=support@yourdomain.com # optional
|
|
6395
6608
|
\`\`\`
|
|
6396
6609
|
5. Add to \`poncho.config.js\`:
|
|
6397
6610
|
\`\`\`javascript
|
|
@@ -7228,71 +7441,72 @@ var createRequestHandler = async (options) => {
|
|
|
7228
7441
|
if (event.type === "tool:approval:checkpoint") {
|
|
7229
7442
|
const cpConv = await conversationStore.get(childConversationId);
|
|
7230
7443
|
if (cpConv) {
|
|
7231
|
-
const
|
|
7232
|
-
approvalId:
|
|
7444
|
+
const allCpData = event.approvals.map((a) => ({
|
|
7445
|
+
approvalId: a.approvalId,
|
|
7233
7446
|
runId: latestRunId,
|
|
7234
|
-
tool:
|
|
7235
|
-
toolCallId:
|
|
7236
|
-
input:
|
|
7447
|
+
tool: a.tool,
|
|
7448
|
+
toolCallId: a.toolCallId,
|
|
7449
|
+
input: a.input,
|
|
7237
7450
|
checkpointMessages: [...historyMessages, ...event.checkpointMessages],
|
|
7238
7451
|
baseMessageCount: 0,
|
|
7239
7452
|
pendingToolCalls: event.pendingToolCalls
|
|
7240
|
-
};
|
|
7241
|
-
cpConv.pendingApprovals =
|
|
7453
|
+
}));
|
|
7454
|
+
cpConv.pendingApprovals = allCpData;
|
|
7242
7455
|
cpConv.updatedAt = Date.now();
|
|
7243
7456
|
await conversationStore.update(cpConv);
|
|
7244
|
-
const
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7457
|
+
const decidedApprovals = await new Promise((resolve4) => {
|
|
7458
|
+
for (const cpData of allCpData) {
|
|
7459
|
+
pendingSubagentApprovals.set(cpData.approvalId, {
|
|
7460
|
+
resolve: resolve4,
|
|
7461
|
+
childHarness,
|
|
7462
|
+
checkpoint: cpData,
|
|
7463
|
+
childConversationId,
|
|
7464
|
+
parentConversationId
|
|
7465
|
+
});
|
|
7466
|
+
}
|
|
7252
7467
|
});
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
)
|
|
7267
|
-
|
|
7468
|
+
const checkpointRef = allCpData[0];
|
|
7469
|
+
const toolContext = {
|
|
7470
|
+
runId: checkpointRef.runId,
|
|
7471
|
+
agentId: identity.id,
|
|
7472
|
+
step: 0,
|
|
7473
|
+
workingDir,
|
|
7474
|
+
parameters: {},
|
|
7475
|
+
conversationId: childConversationId
|
|
7476
|
+
};
|
|
7477
|
+
const approvalToolCallIds = new Set(decidedApprovals.map((a) => a.toolCallId));
|
|
7478
|
+
const callsToExecute = [];
|
|
7479
|
+
const deniedResults = [];
|
|
7480
|
+
for (const a of decidedApprovals) {
|
|
7481
|
+
if (a.decision === "approved" && a.toolCallId) {
|
|
7482
|
+
callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
|
|
7483
|
+
const toolText = `- done \`${a.tool}\``;
|
|
7484
|
+
toolTimeline.push(toolText);
|
|
7485
|
+
currentTools.push(toolText);
|
|
7486
|
+
} else if (a.toolCallId) {
|
|
7487
|
+
deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
|
|
7488
|
+
const toolText = `- denied \`${a.tool}\``;
|
|
7489
|
+
toolTimeline.push(toolText);
|
|
7490
|
+
currentTools.push(toolText);
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
const pendingToolCalls = checkpointRef.pendingToolCalls ?? [];
|
|
7494
|
+
for (const tc of pendingToolCalls) {
|
|
7495
|
+
if (!approvalToolCallIds.has(tc.id)) {
|
|
7496
|
+
callsToExecute.push(tc);
|
|
7497
|
+
}
|
|
7498
|
+
}
|
|
7499
|
+
let toolResults = [...deniedResults];
|
|
7500
|
+
if (callsToExecute.length > 0) {
|
|
7501
|
+
const execResults = await childHarness.executeTools(callsToExecute, toolContext);
|
|
7502
|
+
toolResults.push(...execResults.map((r) => ({
|
|
7268
7503
|
callId: r.callId,
|
|
7269
7504
|
toolName: r.tool,
|
|
7270
7505
|
result: r.output,
|
|
7271
7506
|
error: r.error
|
|
7272
|
-
}));
|
|
7273
|
-
const toolText = `- done \`${cpData.tool}\``;
|
|
7274
|
-
toolTimeline.push(toolText);
|
|
7275
|
-
currentTools.push(toolText);
|
|
7276
|
-
} else {
|
|
7277
|
-
toolResults = [{
|
|
7278
|
-
callId: cpData.toolCallId,
|
|
7279
|
-
toolName: cpData.tool,
|
|
7280
|
-
error: "Tool execution denied by user"
|
|
7281
|
-
}];
|
|
7282
|
-
const toolText = `- denied \`${cpData.tool}\``;
|
|
7283
|
-
toolTimeline.push(toolText);
|
|
7284
|
-
currentTools.push(toolText);
|
|
7285
|
-
}
|
|
7286
|
-
broadcastEvent(
|
|
7287
|
-
childConversationId,
|
|
7288
|
-
approved ? { type: "tool:approval:granted", approvalId: event.approvalId } : { type: "tool:approval:denied", approvalId: event.approvalId }
|
|
7289
|
-
);
|
|
7290
|
-
const cpConv2 = await conversationStore.get(childConversationId);
|
|
7291
|
-
if (cpConv2) {
|
|
7292
|
-
cpConv2.pendingApprovals = [];
|
|
7293
|
-
await conversationStore.update(cpConv2);
|
|
7507
|
+
})));
|
|
7294
7508
|
}
|
|
7295
|
-
const resumeMessages = [...
|
|
7509
|
+
const resumeMessages = [...checkpointRef.checkpointMessages];
|
|
7296
7510
|
for await (const resumeEvent of childHarness.continueFromToolResult({
|
|
7297
7511
|
messages: resumeMessages,
|
|
7298
7512
|
toolResults,
|
|
@@ -7576,16 +7790,16 @@ var createRequestHandler = async (options) => {
|
|
|
7576
7790
|
if (event.type === "tool:approval:checkpoint") {
|
|
7577
7791
|
const conv = await conversationStore.get(conversationId);
|
|
7578
7792
|
if (conv) {
|
|
7579
|
-
conv.pendingApprovals =
|
|
7580
|
-
approvalId:
|
|
7793
|
+
conv.pendingApprovals = event.approvals.map((a) => ({
|
|
7794
|
+
approvalId: a.approvalId,
|
|
7581
7795
|
runId: latestRunId,
|
|
7582
|
-
tool:
|
|
7583
|
-
toolCallId:
|
|
7584
|
-
input:
|
|
7796
|
+
tool: a.tool,
|
|
7797
|
+
toolCallId: a.toolCallId,
|
|
7798
|
+
input: a.input,
|
|
7585
7799
|
checkpointMessages: [...fullCheckpointMessages, ...event.checkpointMessages],
|
|
7586
7800
|
baseMessageCount: 0,
|
|
7587
7801
|
pendingToolCalls: event.pendingToolCalls
|
|
7588
|
-
}
|
|
7802
|
+
}));
|
|
7589
7803
|
conv.updatedAt = Date.now();
|
|
7590
7804
|
await conversationStore.update(conv);
|
|
7591
7805
|
}
|
|
@@ -7820,16 +8034,16 @@ var createRequestHandler = async (options) => {
|
|
|
7820
8034
|
if (event.type === "tool:approval:checkpoint") {
|
|
7821
8035
|
await updateConversation((c) => {
|
|
7822
8036
|
c.messages = buildMessages();
|
|
7823
|
-
c.pendingApprovals =
|
|
7824
|
-
approvalId:
|
|
8037
|
+
c.pendingApprovals = event.approvals.map((a) => ({
|
|
8038
|
+
approvalId: a.approvalId,
|
|
7825
8039
|
runId: latestRunId,
|
|
7826
|
-
tool:
|
|
7827
|
-
toolCallId:
|
|
7828
|
-
input:
|
|
8040
|
+
tool: a.tool,
|
|
8041
|
+
toolCallId: a.toolCallId,
|
|
8042
|
+
input: a.input,
|
|
7829
8043
|
checkpointMessages: event.checkpointMessages,
|
|
7830
8044
|
baseMessageCount: historyMessages.length,
|
|
7831
8045
|
pendingToolCalls: event.pendingToolCalls
|
|
7832
|
-
}
|
|
8046
|
+
}));
|
|
7833
8047
|
});
|
|
7834
8048
|
checkpointedRun = true;
|
|
7835
8049
|
}
|
|
@@ -7898,9 +8112,9 @@ var createRequestHandler = async (options) => {
|
|
|
7898
8112
|
waitUntil: waitUntilHook,
|
|
7899
8113
|
ownerId: "local-owner"
|
|
7900
8114
|
});
|
|
7901
|
-
adapter.registerRoutes(messagingRouteRegistrar);
|
|
7902
8115
|
try {
|
|
7903
8116
|
await bridge.start();
|
|
8117
|
+
adapter.registerRoutes(messagingRouteRegistrar);
|
|
7904
8118
|
messagingBridges.push(bridge);
|
|
7905
8119
|
console.log(` Slack messaging enabled at /api/messaging/slack`);
|
|
7906
8120
|
} catch (err) {
|
|
@@ -7913,6 +8127,7 @@ var createRequestHandler = async (options) => {
|
|
|
7913
8127
|
apiKeyEnv: channelConfig.apiKeyEnv,
|
|
7914
8128
|
webhookSecretEnv: channelConfig.webhookSecretEnv,
|
|
7915
8129
|
fromEnv: channelConfig.fromEnv,
|
|
8130
|
+
replyToEnv: channelConfig.replyToEnv,
|
|
7916
8131
|
allowedSenders: channelConfig.allowedSenders,
|
|
7917
8132
|
mode: channelConfig.mode,
|
|
7918
8133
|
allowedRecipients: channelConfig.allowedRecipients,
|
|
@@ -7924,9 +8139,9 @@ var createRequestHandler = async (options) => {
|
|
|
7924
8139
|
waitUntil: waitUntilHook,
|
|
7925
8140
|
ownerId: "local-owner"
|
|
7926
8141
|
});
|
|
7927
|
-
adapter.registerRoutes(messagingRouteRegistrar);
|
|
7928
8142
|
try {
|
|
7929
8143
|
await bridge.start();
|
|
8144
|
+
adapter.registerRoutes(messagingRouteRegistrar);
|
|
7930
8145
|
messagingBridges.push(bridge);
|
|
7931
8146
|
const adapterTools = adapter.getToolDefinitions?.() ?? [];
|
|
7932
8147
|
if (adapterTools.length > 0) {
|
|
@@ -7948,6 +8163,9 @@ var createRequestHandler = async (options) => {
|
|
|
7948
8163
|
const authToken = process.env[authTokenEnv] ?? "";
|
|
7949
8164
|
const authRequired = config?.auth?.required ?? false;
|
|
7950
8165
|
const requireAuth = authRequired && authToken.length > 0;
|
|
8166
|
+
if (requireAuth) {
|
|
8167
|
+
sessionStore.setSigningKey(authToken);
|
|
8168
|
+
}
|
|
7951
8169
|
const webUiEnabled = config?.webUi !== false;
|
|
7952
8170
|
const isProduction = resolveHarnessEnvironment() === "production";
|
|
7953
8171
|
const secureCookies = isProduction;
|
|
@@ -8021,8 +8239,8 @@ var createRequestHandler = async (options) => {
|
|
|
8021
8239
|
}
|
|
8022
8240
|
}
|
|
8023
8241
|
const cookies = parseCookies(request);
|
|
8024
|
-
const
|
|
8025
|
-
const session =
|
|
8242
|
+
const cookieValue = cookies.poncho_session;
|
|
8243
|
+
const session = cookieValue ? sessionStore.get(cookieValue) ?? sessionStore.restoreFromSigned(cookieValue) : void 0;
|
|
8026
8244
|
const ownerId = session?.ownerId ?? "local-owner";
|
|
8027
8245
|
const requiresCsrfValidation = request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS";
|
|
8028
8246
|
if (pathname === "/api/auth/session" && request.method === "GET") {
|
|
@@ -8070,7 +8288,8 @@ var createRequestHandler = async (options) => {
|
|
|
8070
8288
|
}
|
|
8071
8289
|
loginRateLimiter.registerSuccess(ip);
|
|
8072
8290
|
const createdSession = sessionStore.create(ownerId);
|
|
8073
|
-
|
|
8291
|
+
const signedValue = sessionStore.signSession(createdSession);
|
|
8292
|
+
setCookie(response, "poncho_session", signedValue ?? createdSession.sessionId, {
|
|
8074
8293
|
httpOnly: true,
|
|
8075
8294
|
secure: secureCookies,
|
|
8076
8295
|
sameSite: "Lax",
|
|
@@ -8265,13 +8484,26 @@ data: ${JSON.stringify(data)}
|
|
|
8265
8484
|
const approved = body.approved === true;
|
|
8266
8485
|
const pendingSubagent = pendingSubagentApprovals.get(approvalId);
|
|
8267
8486
|
if (pendingSubagent) {
|
|
8268
|
-
|
|
8487
|
+
pendingSubagent.checkpoint.decision = approved ? "approved" : "denied";
|
|
8488
|
+
broadcastEvent(
|
|
8489
|
+
pendingSubagent.childConversationId,
|
|
8490
|
+
approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
|
|
8491
|
+
);
|
|
8269
8492
|
const childConv = await conversationStore.get(pendingSubagent.childConversationId);
|
|
8270
|
-
|
|
8271
|
-
|
|
8493
|
+
const allApprovals2 = childConv?.pendingApprovals ?? [];
|
|
8494
|
+
const allDecided2 = allApprovals2.length > 0 && allApprovals2.every((pa) => pa.decision != null);
|
|
8495
|
+
if (allDecided2) {
|
|
8496
|
+
for (const pa of allApprovals2) {
|
|
8497
|
+
pendingSubagentApprovals.delete(pa.approvalId);
|
|
8498
|
+
}
|
|
8499
|
+
if (childConv) {
|
|
8500
|
+
childConv.pendingApprovals = [];
|
|
8501
|
+
await conversationStore.update(childConv);
|
|
8502
|
+
}
|
|
8503
|
+
pendingSubagent.resolve(allApprovals2);
|
|
8504
|
+
} else if (childConv) {
|
|
8272
8505
|
await conversationStore.update(childConv);
|
|
8273
8506
|
}
|
|
8274
|
-
pendingSubagent.resolve(approved);
|
|
8275
8507
|
writeJson(response, 200, { ok: true, approvalId, approved });
|
|
8276
8508
|
return;
|
|
8277
8509
|
}
|
|
@@ -8304,47 +8536,63 @@ data: ${JSON.stringify(data)}
|
|
|
8304
8536
|
});
|
|
8305
8537
|
return;
|
|
8306
8538
|
}
|
|
8307
|
-
|
|
8308
|
-
await conversationStore.update(foundConversation);
|
|
8539
|
+
foundApproval.decision = approved ? "approved" : "denied";
|
|
8309
8540
|
broadcastEvent(
|
|
8310
8541
|
conversationId,
|
|
8311
8542
|
approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
|
|
8312
8543
|
);
|
|
8544
|
+
const allApprovals = foundConversation.pendingApprovals ?? [];
|
|
8545
|
+
const allDecided = allApprovals.length > 0 && allApprovals.every((a) => a.decision != null);
|
|
8546
|
+
if (!allDecided) {
|
|
8547
|
+
await conversationStore.update(foundConversation);
|
|
8548
|
+
writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: false });
|
|
8549
|
+
return;
|
|
8550
|
+
}
|
|
8551
|
+
foundConversation.pendingApprovals = [];
|
|
8552
|
+
await conversationStore.update(foundConversation);
|
|
8553
|
+
const checkpointRef = allApprovals[0];
|
|
8313
8554
|
void (async () => {
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8555
|
+
const toolContext = {
|
|
8556
|
+
runId: checkpointRef.runId,
|
|
8557
|
+
agentId: identity.id,
|
|
8558
|
+
step: 0,
|
|
8559
|
+
workingDir,
|
|
8560
|
+
parameters: {}
|
|
8561
|
+
};
|
|
8562
|
+
const approvalToolCallIds = new Set(allApprovals.map((a) => a.toolCallId));
|
|
8563
|
+
const callsToExecute = [];
|
|
8564
|
+
const deniedResults = [];
|
|
8565
|
+
for (const a of allApprovals) {
|
|
8566
|
+
if (a.decision === "approved" && a.toolCallId) {
|
|
8567
|
+
callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
|
|
8568
|
+
} else if (a.decision === "denied" && a.toolCallId) {
|
|
8569
|
+
deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
|
|
8570
|
+
}
|
|
8571
|
+
}
|
|
8572
|
+
const pendingToolCalls = checkpointRef.pendingToolCalls ?? [];
|
|
8573
|
+
for (const tc of pendingToolCalls) {
|
|
8574
|
+
if (!approvalToolCallIds.has(tc.id)) {
|
|
8575
|
+
callsToExecute.push(tc);
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
let toolResults = [...deniedResults];
|
|
8579
|
+
if (callsToExecute.length > 0) {
|
|
8580
|
+
const execResults = await harness.executeTools(callsToExecute, toolContext);
|
|
8581
|
+
toolResults.push(...execResults.map((r) => ({
|
|
8328
8582
|
callId: r.callId,
|
|
8329
8583
|
toolName: r.tool,
|
|
8330
8584
|
result: r.output,
|
|
8331
8585
|
error: r.error
|
|
8332
|
-
}));
|
|
8333
|
-
} else {
|
|
8334
|
-
toolResults = [{
|
|
8335
|
-
callId: foundApproval.toolCallId,
|
|
8336
|
-
toolName: foundApproval.tool,
|
|
8337
|
-
error: "Tool execution denied by user"
|
|
8338
|
-
}];
|
|
8586
|
+
})));
|
|
8339
8587
|
}
|
|
8340
8588
|
await resumeRunFromCheckpoint(
|
|
8341
8589
|
conversationId,
|
|
8342
8590
|
foundConversation,
|
|
8343
|
-
|
|
8591
|
+
checkpointRef,
|
|
8344
8592
|
toolResults
|
|
8345
8593
|
);
|
|
8346
8594
|
})();
|
|
8347
|
-
writeJson(response, 200, { ok: true, approvalId, approved });
|
|
8595
|
+
writeJson(response, 200, { ok: true, approvalId, approved, batchComplete: true });
|
|
8348
8596
|
return;
|
|
8349
8597
|
}
|
|
8350
8598
|
const conversationEventsMatch = pathname.match(
|
|
@@ -8828,16 +9076,16 @@ data: ${JSON.stringify(data)}
|
|
|
8828
9076
|
metadata: toolTimeline.length > 0 || checkpointSections.length > 0 ? { toolActivity: [...toolTimeline], sections: checkpointSections.length > 0 ? checkpointSections : void 0 } : void 0
|
|
8829
9077
|
}] : []
|
|
8830
9078
|
];
|
|
8831
|
-
conversation.pendingApprovals =
|
|
8832
|
-
approvalId:
|
|
9079
|
+
conversation.pendingApprovals = event.approvals.map((a) => ({
|
|
9080
|
+
approvalId: a.approvalId,
|
|
8833
9081
|
runId: latestRunId,
|
|
8834
|
-
tool:
|
|
8835
|
-
toolCallId:
|
|
8836
|
-
input:
|
|
9082
|
+
tool: a.tool,
|
|
9083
|
+
toolCallId: a.toolCallId,
|
|
9084
|
+
input: a.input,
|
|
8837
9085
|
checkpointMessages: event.checkpointMessages,
|
|
8838
9086
|
baseMessageCount: historyMessages.length,
|
|
8839
9087
|
pendingToolCalls: event.pendingToolCalls
|
|
8840
|
-
}
|
|
9088
|
+
}));
|
|
8841
9089
|
conversation.updatedAt = Date.now();
|
|
8842
9090
|
await conversationStore.update(conversation);
|
|
8843
9091
|
checkpointedRun = true;
|
|
@@ -9372,7 +9620,7 @@ var runInteractive = async (workingDir, params) => {
|
|
|
9372
9620
|
await harness.initialize();
|
|
9373
9621
|
const identity = await ensureAgentIdentity2(workingDir);
|
|
9374
9622
|
try {
|
|
9375
|
-
const { runInteractiveInk } = await import("./run-interactive-ink-
|
|
9623
|
+
const { runInteractiveInk } = await import("./run-interactive-ink-4KVVPMR3.js");
|
|
9376
9624
|
await runInteractiveInk({
|
|
9377
9625
|
harness,
|
|
9378
9626
|
params,
|