@desplega.ai/agent-swarm 2.5.1 → 2.5.2
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/{acp-adapter-7g6bpwbk.js → acp-adapter-csh0zz3b.js} +1 -1
- package/dist/{actions-w8xd014k.js → actions-6xx079d1.js} +17 -17
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-8n6f0hz8.js} +2 -2
- package/dist/{app-nndcvftq.js → app-ft6r6v23.js} +3 -3
- package/dist/{assistant-hjmqyscx.js → assistant-vt2cyfzd.js} +18 -18
- package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-r3e808ky.js} +1 -1
- package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-tgrfy27t.js} +1 -1
- package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-masdwxkm.js} +2 -2
- package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-0n82qc7q.js} +4 -4
- package/dist/{claude-adapter-2f8b85df.js → claude-adapter-s7j47k06.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3hzrgrnk.js} +1 -1
- package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-0zrdzyx2.js} +6905 -6855
- package/dist/{cli-mz2ygeyq.js → cli-0vgy5wfw.js} +2 -2
- package/dist/{cli-eym3jg7w.js → cli-14v1sxxy.js} +6 -6
- package/dist/{cli-e9btqw4y.js → cli-15xd0j7w.js} +18 -13
- package/dist/{cli-v2g93k4v.js → cli-3e9vs93g.js} +4 -4
- package/dist/{cli-1dp1zj04.js → cli-4qkg9968.js} +3 -3
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-rngde2jv.js → cli-6y43v79h.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-7ccf7akh.js} +1 -1
- package/dist/{cli-z2dv25mf.js → cli-86zpfje5.js} +116 -5
- package/dist/{cli-fes51ya2.js → cli-aaqnqpyw.js} +2 -2
- package/dist/{cli-qfm6my86.js → cli-b1tv22wr.js} +1 -1
- package/dist/{cli-xc65x52q.js → cli-b63d08rs.js} +1 -1
- package/dist/{cli-gwcf52gz.js → cli-b6tnjdvx.js} +11 -7
- package/dist/{cli-9mm5ade4.js → cli-bpbcpd8b.js} +1 -1
- package/dist/{cli-qfjm4gvt.js → cli-cf63dnxj.js} +2 -2
- package/dist/{cli-q9pzwxvk.js → cli-csh8fnbg.js} +82 -16
- package/dist/{cli-0jy85zz4.js → cli-cyfgxnvb.js} +7 -7
- package/dist/{cli-y3zyaee5.js → cli-d172svcw.js} +5 -5
- package/dist/{cli-mq7kg1b1.js → cli-e91vshb1.js} +41 -6
- package/dist/{cli-cah5detr.js → cli-edtm741g.js} +5 -5
- package/dist/{cli-ajqhxtnz.js → cli-f3pfn4x3.js} +1 -1
- package/dist/{cli-hadv0725.js → cli-f55b6ky0.js} +1 -1
- package/dist/{cli-r9fy0ddc.js → cli-fgn7yg5r.js} +1 -1
- package/dist/{cli-kcsd1ez8.js → cli-h5d22pfs.js} +249 -68
- package/dist/{cli-2wba1ac9.js → cli-ha22h0eg.js} +20 -20
- package/dist/{cli-pyy4c9ch.js → cli-j3dwd4kz.js} +173 -134
- package/dist/{cli-j1eghqfm.js → cli-jmcjbjkt.js} +1 -1
- package/dist/{cli-22rmcjkg.js → cli-kcnwn85g.js} +4 -3
- package/dist/{cli-rh5smt8t.js → cli-mntxtkhn.js} +12 -2
- package/dist/{cli-2jg61wab.js → cli-n2918sed.js} +9 -9
- package/dist/{cli-3ee5z0q0.js → cli-qrjv7t18.js} +1 -1
- package/dist/{cli-sn1vbtg2.js → cli-vaxw3bg0.js} +3 -3
- package/dist/{cli-cwyafhw1.js → cli-wadzapwb.js} +1 -1
- package/dist/{cli-jpxsrxcb.js → cli-xw9q173h.js} +1 -1
- package/dist/{cli-znvq0k3t.js → cli-xzge46sj.js} +2 -2
- package/dist/{cli-nj12tn2c.js → cli-y3dzwyy3.js} +3 -3
- package/dist/{cli-xd4eqtr2.js → cli-ydg6ep51.js} +1 -1
- package/dist/cli.js +16 -15
- package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-brh2tcxr.js} +1 -1
- package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-0dnj21qs.js} +1 -1
- package/dist/{commands-mbvhbrcy.js → commands-1cnvseqx.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-kc7s9xhn.js} +2 -2
- package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-jzx7t4bn.js} +18 -18
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-qpwp5sfg.js} +1 -1
- package/dist/{db-af9y8nkf.js → db-4vq88jkc.js} +3 -3
- package/dist/{db-88wcymxt.js → db-v5ytg5pw.js} +1 -1
- package/dist/{dispatcher-606ydx5v.js → dispatcher-trtcrqae.js} +16 -16
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-f20xwn1z.js} +1 -1
- package/dist/{external-channel-y8q5z8z2.js → external-channel-n9bymc2n.js} +2 -2
- package/dist/{grok-adapter-39aha52t.js → grok-adapter-4kpmmnr6.js} +2 -2
- package/dist/{handlers-rk9dqech.js → handlers-ptsraqa8.js} +19 -19
- package/dist/{heartbeat-jfe9zfjr.js → heartbeat-60qtxtjf.js} +19 -19
- package/dist/{hook-ffevzed1.js → hook-ngtvh729.js} +1 -1
- package/dist/{hook-b1mez1m8.js → hook-rbktcstg.js} +1 -1
- package/dist/{http-d5b44f1r.js → http-z8xy72gp.js} +334 -86
- package/dist/{index-nq8hhv28.js → index-06qkmcx6.js} +10 -9
- package/dist/{index-d2qeem97.js → index-1cscynkb.js} +16 -16
- package/dist/{index-5en23s5z.js → index-a9nsavhx.js} +1 -1
- package/dist/{index-4q3vj1yr.js → index-r70q0xjx.js} +16 -16
- package/dist/{index-mj46rk9k.js → index-s193j5fh.js} +19 -19
- package/dist/{keepalive-wsby1btq.js → keepalive-a9srcphj.js} +3 -3
- package/dist/{lead-paa141j6.js → lead-3devneqy.js} +26 -26
- package/dist/{maintenance-vnshcyv9.js → maintenance-0er16h2y.js} +1 -1
- package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-b65jcyw2.js} +2 -2
- package/dist/{onboard-nr4xhpzy.js → onboard-wqywz5py.js} +2 -2
- package/dist/{otel-impl-ytn9wyv9.js → otel-impl-vk32s884.js} +1 -1
- package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-t27anv4z.js} +3 -3
- package/dist/rbac-roles-452665y0.js +26 -0
- package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-bmcgsxjn.js} +12 -2
- package/dist/{render-v2-pyfsg9zk.js → render-v2-0ttsk2j4.js} +3 -3
- package/dist/{resume-phj8xhqr.js → resume-a26kvr41.js} +5 -5
- package/dist/{session-access-jzx2z9h5.js → session-access-dgh9nkwk.js} +4 -4
- package/dist/{setup-rfg0f0sg.js → setup-9vc9qahm.js} +1 -1
- package/dist/{work-objects-44z2yjg8.js → work-objects-vn8nkxwn.js} +19 -19
- package/dist/{worker-zsfqzxhm.js → worker-1x1h8vnm.js} +26 -26
- package/openapi.json +463 -3
- package/package.json +4 -3
- package/src/be/db/session-tree.ts +89 -2
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/tasks/read.ts +18 -5
- package/src/be/db.ts +2 -0
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- package/src/be/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/users.ts +28 -4
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +46 -6
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +93 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +40 -5
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +163 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/http-users.test.ts +38 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +242 -0
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +16 -0
- package/src/workflows/approval-notifications.ts +214 -0
- package/src/workflows/executors/human-in-the-loop.ts +11 -172
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/dist/rbac-roles-ff1ffmec.js +0 -12
|
@@ -3,14 +3,228 @@ import {
|
|
|
3
3
|
claimApprovalCancellationNotification,
|
|
4
4
|
completeApprovalCancellationNotificationClaim,
|
|
5
5
|
createTaskExtended,
|
|
6
|
+
getApprovalRequestById,
|
|
6
7
|
getTaskById,
|
|
7
8
|
releaseApprovalCancellationNotificationClaim,
|
|
9
|
+
updateApprovalRequestNotifications,
|
|
8
10
|
} from "../be/db";
|
|
9
11
|
import { resolveTemplate } from "../prompts/resolver";
|
|
10
12
|
import { getSlackApp } from "../slack/app";
|
|
13
|
+
import { resolveSlackConversationId } from "../slack/dm";
|
|
14
|
+
import {
|
|
15
|
+
externalChannelToolError,
|
|
16
|
+
getChannelBlockReason,
|
|
17
|
+
isBlockExternalEnabled,
|
|
18
|
+
} from "../slack/external-channel";
|
|
11
19
|
import { TERMINAL_TASK_STATUSES } from "../types";
|
|
20
|
+
import { getAppUrl } from "../utils/constants";
|
|
12
21
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
13
22
|
|
|
23
|
+
/** Slack Block Kit caps a section block's text at 3000 characters. */
|
|
24
|
+
const SLACK_SECTION_TEXT_LIMIT = 3000;
|
|
25
|
+
|
|
26
|
+
function escapeSlackMrkdwn(text: string): string {
|
|
27
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render the Slack "Questions" section text within Block Kit's 3000-character
|
|
32
|
+
* section limit. Labels are escaped so upstream text cannot inject mentions or
|
|
33
|
+
* links. When the list does not fit, the tail is replaced by a count; every
|
|
34
|
+
* question stays answerable on the dashboard page the card's button opens.
|
|
35
|
+
*/
|
|
36
|
+
export function buildSlackQuestionsSummary(
|
|
37
|
+
questions: ReadonlyArray<{ label: string }>,
|
|
38
|
+
timeoutText = "",
|
|
39
|
+
): string {
|
|
40
|
+
const header = "*Questions:*\n";
|
|
41
|
+
const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
|
|
42
|
+
const fits = (shown: string[], omitted: number) => {
|
|
43
|
+
const tail =
|
|
44
|
+
omitted > 0 ? `\n_…and ${omitted} more. Answer all of them on the dashboard._` : "";
|
|
45
|
+
const text = `${header}${shown.join("\n")}${tail}${timeoutText}`;
|
|
46
|
+
return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const full = fits(lines, 0);
|
|
50
|
+
if (full) return full;
|
|
51
|
+
for (let count = lines.length - 1; count >= 0; count--) {
|
|
52
|
+
const text = fits(lines.slice(0, count), lines.length - count);
|
|
53
|
+
if (text) return text;
|
|
54
|
+
}
|
|
55
|
+
return `${header}_${questions.length} questions. Answer them on the dashboard._`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatTimeout(seconds: number): string {
|
|
59
|
+
if (seconds < 60) return `${seconds}s`;
|
|
60
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
61
|
+
const hours = Math.floor(seconds / 3600);
|
|
62
|
+
const mins = Math.round((seconds % 3600) / 60);
|
|
63
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ApprovalNotificationChannel {
|
|
67
|
+
channel: "slack" | "email";
|
|
68
|
+
target: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ApprovalNotificationsInput {
|
|
72
|
+
requestId: string;
|
|
73
|
+
title: string;
|
|
74
|
+
questions: ReadonlyArray<{ label: string }>;
|
|
75
|
+
timeoutSeconds?: number;
|
|
76
|
+
notifications: ReadonlyArray<ApprovalNotificationChannel>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The DB calls the dispatch makes; the HITL executor injects its own. */
|
|
80
|
+
export interface ApprovalNotificationsDb {
|
|
81
|
+
updateApprovalRequestNotifications: typeof updateApprovalRequestNotifications;
|
|
82
|
+
getApprovalRequestById: typeof getApprovalRequestById;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Post the "Approval Required" card for each notification channel and store
|
|
87
|
+
* the Slack `messageTs` on the request. Used by the workflow HITL executor and
|
|
88
|
+
* by `POST /api/approval-requests`, which checks the caller may post to Slack.
|
|
89
|
+
* Callers fire and forget: a Slack failure is logged and never fails the
|
|
90
|
+
* request. Email is not supported yet and is skipped.
|
|
91
|
+
*/
|
|
92
|
+
export async function dispatchApprovalNotifications(
|
|
93
|
+
input: ApprovalNotificationsInput,
|
|
94
|
+
db: ApprovalNotificationsDb = { updateApprovalRequestNotifications, getApprovalRequestById },
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const { requestId, title, questions, timeoutSeconds, notifications } = input;
|
|
97
|
+
if (!notifications.length) return;
|
|
98
|
+
|
|
99
|
+
const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
|
|
100
|
+
const updatedChannels: Array<ApprovalNotificationChannel & { messageTs?: string }> = [
|
|
101
|
+
...notifications,
|
|
102
|
+
];
|
|
103
|
+
let updated = false;
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < notifications.length; i++) {
|
|
106
|
+
const notification = notifications[i]!;
|
|
107
|
+
|
|
108
|
+
if (notification.channel === "email") {
|
|
109
|
+
console.warn(`[HITL] Email notifications not yet supported (target: ${notification.target})`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (notification.channel === "slack") {
|
|
114
|
+
try {
|
|
115
|
+
const slackApp = getSlackApp();
|
|
116
|
+
if (!slackApp) {
|
|
117
|
+
console.warn("[HITL] Slack not initialized — cannot send notification");
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Same guard as the Slack tools: never post into a channel shared with another org.
|
|
122
|
+
if (isBlockExternalEnabled()) {
|
|
123
|
+
const conversationId = await resolveSlackConversationId(
|
|
124
|
+
slackApp.client,
|
|
125
|
+
notification.target,
|
|
126
|
+
);
|
|
127
|
+
const blockReason = await getChannelBlockReason(slackApp.client, conversationId);
|
|
128
|
+
if (blockReason) {
|
|
129
|
+
console.warn(`[HITL] ${externalChannelToolError(conversationId, blockReason)}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const questionsText = buildSlackQuestionsSummary(questions);
|
|
135
|
+
|
|
136
|
+
// The deadline is a caption under the button, not part of the ask.
|
|
137
|
+
const timeoutCaption =
|
|
138
|
+
timeoutSeconds !== undefined
|
|
139
|
+
? [
|
|
140
|
+
{
|
|
141
|
+
type: "context",
|
|
142
|
+
elements: [
|
|
143
|
+
{
|
|
144
|
+
type: "mrkdwn",
|
|
145
|
+
text: `⏱ Timeout: ${formatTimeout(timeoutSeconds)} — times out if not responded`,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
: [];
|
|
151
|
+
|
|
152
|
+
const blocks = [
|
|
153
|
+
{
|
|
154
|
+
type: "section",
|
|
155
|
+
text: {
|
|
156
|
+
type: "mrkdwn",
|
|
157
|
+
text: `🔔 *Approval Required: ${escapeSlackMrkdwn(title)}*`,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: "section",
|
|
162
|
+
text: {
|
|
163
|
+
type: "mrkdwn",
|
|
164
|
+
text: questionsText,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: "actions",
|
|
169
|
+
elements: [
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
text: { type: "plain_text", text: "Review & Respond" },
|
|
173
|
+
url: approvalUrl,
|
|
174
|
+
style: "primary",
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
...timeoutCaption,
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
const result = await slackApp.client.chat.postMessage({
|
|
182
|
+
channel: notification.target,
|
|
183
|
+
text: `Approval Required: ${escapeSlackMrkdwn(title)} — ${approvalUrl}`,
|
|
184
|
+
unfurl_links: false,
|
|
185
|
+
unfurl_media: false,
|
|
186
|
+
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
187
|
+
blocks: blocks as any,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (result.ts) {
|
|
191
|
+
updatedChannels[i] = { ...notification, messageTs: result.ts };
|
|
192
|
+
updated = true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(
|
|
196
|
+
`[HITL] Slack notification sent to ${notification.target} for request ${requestId}`,
|
|
197
|
+
);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(
|
|
200
|
+
`[HITL] Failed to send Slack notification to ${notification.target}:`,
|
|
201
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Persist messageTs values back to DB
|
|
208
|
+
if (updated) {
|
|
209
|
+
try {
|
|
210
|
+
await db.updateApprovalRequestNotifications(requestId, updatedChannels);
|
|
211
|
+
// A cancel that landed while the card was posting found no messageTs to thread under.
|
|
212
|
+
const latest = await db.getApprovalRequestById(requestId);
|
|
213
|
+
if (latest?.status === "cancelled") {
|
|
214
|
+
await postApprovalCancellationUpdates(
|
|
215
|
+
[latest],
|
|
216
|
+
latest.resolutionReason ?? "Workflow run cancelled",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(
|
|
221
|
+
"[HITL] Failed to update notification channels in DB:",
|
|
222
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
14
228
|
export interface ApprovalSlackClient {
|
|
15
229
|
chat: {
|
|
16
230
|
postMessage(input: {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { ExecutorMeta } from "../../types";
|
|
3
|
-
import { getAppUrl } from "../../utils/constants";
|
|
4
3
|
import { scrubSecrets } from "../../utils/secret-scrubber";
|
|
5
|
-
import {
|
|
4
|
+
import { dispatchApprovalNotifications } from "../approval-notifications";
|
|
6
5
|
import type { ExecutorInput, ExecutorResult } from "./base";
|
|
7
6
|
import { BaseExecutor } from "./base";
|
|
8
7
|
|
|
@@ -88,9 +87,6 @@ const DYNAMIC_QUESTIONS_TOKEN_RE = /^\{\{[^}]+\}\}$/;
|
|
|
88
87
|
*/
|
|
89
88
|
export const MAX_HITL_QUESTIONS = 100;
|
|
90
89
|
|
|
91
|
-
/** Slack Block Kit caps a section block's text at 3000 characters. */
|
|
92
|
-
const SLACK_SECTION_TEXT_LIMIT = 3000;
|
|
93
|
-
|
|
94
90
|
const MAX_REPORTED_ISSUES = 5;
|
|
95
91
|
|
|
96
92
|
const HITLConfigSchema = z.object({
|
|
@@ -197,38 +193,6 @@ export function resolveHitlQuestions(
|
|
|
197
193
|
return { ok: true, questions };
|
|
198
194
|
}
|
|
199
195
|
|
|
200
|
-
function escapeSlackMrkdwn(text: string): string {
|
|
201
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Render the Slack "Questions" section text within Block Kit's 3000-character
|
|
206
|
-
* section limit. Labels are escaped so upstream text cannot inject mentions or
|
|
207
|
-
* links. When the list does not fit, the tail is replaced by a count; every
|
|
208
|
-
* question stays answerable on the dashboard page the card's button opens.
|
|
209
|
-
*/
|
|
210
|
-
export function buildSlackQuestionsSummary(
|
|
211
|
-
questions: ReadonlyArray<{ label: string }>,
|
|
212
|
-
timeoutText = "",
|
|
213
|
-
): string {
|
|
214
|
-
const header = "*Questions:*\n";
|
|
215
|
-
const lines = questions.map((q) => `• ${escapeSlackMrkdwn(q.label.replace(/\s+/g, " ").trim())}`);
|
|
216
|
-
const fits = (shown: string[], omitted: number) => {
|
|
217
|
-
const tail =
|
|
218
|
-
omitted > 0 ? `\n_…and ${omitted} more. Answer all of them on the dashboard._` : "";
|
|
219
|
-
const text = `${header}${shown.join("\n")}${tail}${timeoutText}`;
|
|
220
|
-
return text.length <= SLACK_SECTION_TEXT_LIMIT ? text : null;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const full = fits(lines, 0);
|
|
224
|
-
if (full) return full;
|
|
225
|
-
for (let count = lines.length - 1; count >= 0; count--) {
|
|
226
|
-
const text = fits(lines.slice(0, count), lines.length - count);
|
|
227
|
-
if (text) return text;
|
|
228
|
-
}
|
|
229
|
-
return `${header}_${questions.length} questions. Answer them on the dashboard._`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
196
|
// ─── Executor ───────────────────────────────────────────────
|
|
233
197
|
|
|
234
198
|
export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
@@ -319,7 +283,16 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
319
283
|
|
|
320
284
|
// 3. Dispatch notifications (fire-and-forget — failures must not block the workflow)
|
|
321
285
|
if (config.notifications?.length) {
|
|
322
|
-
|
|
286
|
+
dispatchApprovalNotifications(
|
|
287
|
+
{
|
|
288
|
+
requestId,
|
|
289
|
+
title: config.title,
|
|
290
|
+
questions: config.questions,
|
|
291
|
+
timeoutSeconds: config.timeout?.seconds,
|
|
292
|
+
notifications: config.notifications,
|
|
293
|
+
},
|
|
294
|
+
db,
|
|
295
|
+
).catch((err) => {
|
|
323
296
|
console.error(
|
|
324
297
|
"[HITL] Unexpected error dispatching notifications:",
|
|
325
298
|
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
@@ -335,138 +308,4 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
335
308
|
correlationId: requestId,
|
|
336
309
|
} as ExecutorResult<HITLOutput>;
|
|
337
310
|
}
|
|
338
|
-
|
|
339
|
-
/** Dispatch notifications for each configured channel. Updates DB with messageTs on success. */
|
|
340
|
-
private async dispatchNotifications(
|
|
341
|
-
requestId: string,
|
|
342
|
-
config: z.infer<typeof HITLConfigSchema>,
|
|
343
|
-
db: typeof import("../../be/db"),
|
|
344
|
-
): Promise<void> {
|
|
345
|
-
if (!config.notifications?.length) return;
|
|
346
|
-
|
|
347
|
-
const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
|
|
348
|
-
const updatedChannels = [...config.notifications] as Array<
|
|
349
|
-
z.infer<typeof NotificationConfigSchema> & { messageTs?: string }
|
|
350
|
-
>;
|
|
351
|
-
let updated = false;
|
|
352
|
-
|
|
353
|
-
for (let i = 0; i < config.notifications.length; i++) {
|
|
354
|
-
const notification = config.notifications[i]!;
|
|
355
|
-
|
|
356
|
-
if (notification.channel === "email") {
|
|
357
|
-
console.warn(
|
|
358
|
-
`[HITL] Email notifications not yet supported (target: ${notification.target})`,
|
|
359
|
-
);
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (notification.channel === "slack") {
|
|
364
|
-
try {
|
|
365
|
-
const { getSlackApp } = await import("../../slack/app");
|
|
366
|
-
const slackApp = getSlackApp();
|
|
367
|
-
if (!slackApp) {
|
|
368
|
-
console.warn("[HITL] Slack not initialized — cannot send notification");
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const questions = Array.isArray(config.questions) ? config.questions : [];
|
|
373
|
-
const questionsText = buildSlackQuestionsSummary(questions);
|
|
374
|
-
|
|
375
|
-
// The deadline is a caption under the button, not part of the ask.
|
|
376
|
-
const timeoutCaption = config.timeout
|
|
377
|
-
? [
|
|
378
|
-
{
|
|
379
|
-
type: "context",
|
|
380
|
-
elements: [
|
|
381
|
-
{
|
|
382
|
-
type: "mrkdwn",
|
|
383
|
-
text: `⏱ Timeout: ${formatTimeout(config.timeout.seconds)} — times out if not responded`,
|
|
384
|
-
},
|
|
385
|
-
],
|
|
386
|
-
},
|
|
387
|
-
]
|
|
388
|
-
: [];
|
|
389
|
-
|
|
390
|
-
const blocks = [
|
|
391
|
-
{
|
|
392
|
-
type: "section",
|
|
393
|
-
text: {
|
|
394
|
-
type: "mrkdwn",
|
|
395
|
-
text: `🔔 *Approval Required: ${config.title}*`,
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
type: "section",
|
|
400
|
-
text: {
|
|
401
|
-
type: "mrkdwn",
|
|
402
|
-
text: questionsText,
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
type: "actions",
|
|
407
|
-
elements: [
|
|
408
|
-
{
|
|
409
|
-
type: "button",
|
|
410
|
-
text: { type: "plain_text", text: "Review & Respond" },
|
|
411
|
-
url: approvalUrl,
|
|
412
|
-
style: "primary",
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
},
|
|
416
|
-
...timeoutCaption,
|
|
417
|
-
];
|
|
418
|
-
|
|
419
|
-
const result = await slackApp.client.chat.postMessage({
|
|
420
|
-
channel: notification.target,
|
|
421
|
-
text: `Approval Required: ${config.title} — ${approvalUrl}`,
|
|
422
|
-
unfurl_links: false,
|
|
423
|
-
unfurl_media: false,
|
|
424
|
-
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
425
|
-
blocks: blocks as any,
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
if (result.ts) {
|
|
429
|
-
updatedChannels[i] = { ...notification, messageTs: result.ts };
|
|
430
|
-
updated = true;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
console.log(
|
|
434
|
-
`[HITL] Slack notification sent to ${notification.target} for request ${requestId}`,
|
|
435
|
-
);
|
|
436
|
-
} catch (err) {
|
|
437
|
-
console.error(
|
|
438
|
-
`[HITL] Failed to send Slack notification to ${notification.target}:`,
|
|
439
|
-
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Persist messageTs values back to DB
|
|
446
|
-
if (updated) {
|
|
447
|
-
try {
|
|
448
|
-
await db.updateApprovalRequestNotifications(requestId, updatedChannels);
|
|
449
|
-
const latest = await db.getApprovalRequestById(requestId);
|
|
450
|
-
if (latest?.status === "cancelled") {
|
|
451
|
-
await postApprovalCancellationUpdates(
|
|
452
|
-
[latest],
|
|
453
|
-
latest.resolutionReason ?? "Workflow run cancelled",
|
|
454
|
-
);
|
|
455
|
-
}
|
|
456
|
-
} catch (err) {
|
|
457
|
-
console.error(
|
|
458
|
-
"[HITL] Failed to update notification channels in DB:",
|
|
459
|
-
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function formatTimeout(seconds: number): string {
|
|
467
|
-
if (seconds < 60) return `${seconds}s`;
|
|
468
|
-
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
469
|
-
const hours = Math.floor(seconds / 3600);
|
|
470
|
-
const mins = Math.round((seconds % 3600) / 60);
|
|
471
|
-
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
472
311
|
}
|
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
restoreScratchScriptLastUsedIfUnchanged,
|
|
11
11
|
touchScratchScriptLastUsed,
|
|
12
12
|
} from "../../be/scripts/db";
|
|
13
|
+
import { runScriptWithRunCredential } from "../../be/scripts/run-credential";
|
|
13
14
|
import {
|
|
14
15
|
DEFAULT_SCRIPT_RESOURCES,
|
|
15
16
|
MAX_SCRIPT_WALL_CLOCK_MS,
|
|
16
17
|
MIN_SCRIPT_WALL_CLOCK_MS,
|
|
17
18
|
} from "../../scripts-runtime/executors/types";
|
|
18
|
-
import { runScript } from "../../scripts-runtime/loader";
|
|
19
19
|
import type { ExecutorMeta } from "../../types";
|
|
20
20
|
import { getMcpBaseUrl } from "../../utils/constants";
|
|
21
21
|
import { findInterpolationTokens } from "../../utils/template";
|
|
@@ -119,7 +119,7 @@ export class SwarmScriptExecutor extends BaseExecutor<
|
|
|
119
119
|
const credentials = await buildScriptCredentialBindingsWithFailures({
|
|
120
120
|
agentId: agentId ?? undefined,
|
|
121
121
|
});
|
|
122
|
-
const output = await
|
|
122
|
+
const output = await runScriptWithRunCredential({
|
|
123
123
|
source: resolved.source,
|
|
124
124
|
args: config.args,
|
|
125
125
|
fsMode: "none",
|
|
@@ -45,7 +45,7 @@ The worker image pins these harnesses via `ARG`. Each week, check upstream for n
|
|
|
45
45
|
bun test
|
|
46
46
|
bash scripts/check-db-boundary.sh
|
|
47
47
|
```
|
|
48
|
-
For Codex bumps: also run `bash scripts/check-codex-default-model.sh`.
|
|
48
|
+
For Codex bumps: also run `bash scripts/check-codex-default-model.sh` (CI runs it too). It fails when `ARG CODEX_VERSION` differs from the direct `@openai/codex` pin in `package.json`, or when the Dockerfile's baseline `model = "..."` differs from `CODEX_DEFAULT_MODEL` in `src/providers/codex-models.ts`.
|
|
49
49
|
Note: do NOT run `docker build -f Dockerfile.worker .` locally — the runner lacks docker. CI's `docker-worker-build` job is the acceptance gate.
|
|
50
50
|
9. If any local check fails, apply the **partial-bundle fallback** before giving up — one harness must never block the others:
|
|
51
51
|
**BASELINE FIRST — do this before ANY isolation work.** A red gate does NOT mean the bumps broke anything. Re-run the EXACT failing command on a pristine `origin/main` worktree with zero bumps applied (`git worktree add /tmp/hb-baseline origin/main && cd /tmp/hb-baseline && bun install --frozen-lockfile`) and compare. If the SAME checks fail with the SAME assertions and the same pass/skip/fail totals, the failure is **PRE-EXISTING and unrelated to the bumps** ⇒ it does NOT block this PR, and you must NOT attempt isolation for it. Proceed to step 10, open the PR, and add a `## Pre-existing failures (also fail on origin/main)` section quoting the verbatim error plus both totals (baseline vs branch) so the reviewer can see they match. Only a failure that is RED on your branch and GREEN on baseline was caused by a bump — apply a–d below to that failure ONLY. A gate that is red on origin/main is never a reason to skip the weekly bump.
|
|
@@ -44,7 +44,7 @@ The Repository Guidelines carry `allowMerge` and `mergeChecks`.
|
|
|
44
44
|
1. Read `/review/rules.md` on the shared agent-fs drive. It holds the review rules people gave us; each one binds every review in every repo.
|
|
45
45
|
2. Read the repo's `REVIEW.md` when it has one. It holds the repo-specific checks.
|
|
46
46
|
3. Set the effort from the PR body's `Risk:` line. `low`: a quick pass that still covers CI, tests and the security read. `medium` or no line: the full list below. `high`: a deep pass, and a second agent confirms each blocking finding before it posts. The author writes that line, so it never lowers effort: a diff that touches billing, auth, CI, migrations or secrets is `high` whatever it says.
|
|
47
|
-
4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. Re-run each earlier repro on the new head. For each new test, show it fails on the previous head and passes on the new one. Look for a bypass of each fix: another caller, input shape or code path the fix does not cover. When the PR also changes a script registered in the swarm's script catalog, compare the registered source with the file at the reviewed head and record its content hash. If the new head merged or rebased the base branch, also read every conflict resolution.
|
|
47
|
+
4. Re-review check. If you reviewed this PR before, compare `gh pr view <number> --json headRefOid --jq .headRefOid` with the `commit_id` of your last review (`gh api repos/<owner>/<repo>/pulls/<number>/reviews`). Same SHA: reuse that verdict, link it and stop, unless the PR gained evidence that answers an open non-code blocker (a UI recording, a screenshot, a log). Then verify only that evidence, reuse the earlier code checks, and post one updated verdict that names the evidence. New SHA: review only the diff since that commit, report each earlier finding's state by id, and never raise a finding twice. Re-run each earlier repro on the new head. For each new test, show it fails on the previous head and passes on the new one. Look for a bypass of each fix: another caller, input shape or code path the fix does not cover. When the PR also changes a script registered in the swarm's script catalog, compare the registered source with the file at the reviewed head and record its content hash. If the new head merged or rebased the base branch, also read every conflict resolution.
|
|
48
48
|
5. Round cap. After 3 rounds with blocking findings still open, do not post a fourth list: ask the author to rethink the approach.
|
|
49
49
|
|
|
50
50
|
### What to check
|