@opentag/dispatcher 0.2.0 → 0.3.1
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/callbacks.d.ts +8 -1
- package/dist/callbacks.d.ts.map +1 -1
- package/dist/index.js +1816 -217
- package/dist/index.js.map +1 -1
- package/dist/presentation.d.ts +40 -1
- package/dist/presentation.d.ts.map +1 -1
- package/dist/server.d.ts +43 -1
- package/dist/server.d.ts.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
// src/callbacks.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
createLarkReplyClient,
|
|
4
|
+
patchLarkMessageCard,
|
|
5
|
+
parseLarkThreadKey,
|
|
6
|
+
replyLarkMessage,
|
|
7
|
+
updateLarkTextMessage
|
|
8
|
+
} from "@opentag/lark";
|
|
9
|
+
import {
|
|
10
|
+
createSlackPostMessagePayload,
|
|
11
|
+
createSlackReactionPayload,
|
|
12
|
+
createSlackUpdateMessagePayload,
|
|
13
|
+
parseSlackThreadKey,
|
|
14
|
+
slackSourceReceiptReactionName
|
|
15
|
+
} from "@opentag/slack";
|
|
4
16
|
import { createTelegramSendMessageDraftPayload, createTelegramSendMessagePayload, parseTelegramThreadKey } from "@opentag/telegram";
|
|
17
|
+
var DEFAULT_SLACK_SOURCE_RECEIPT_TIMEOUT_MS = 5e3;
|
|
5
18
|
function slackUpdateUriFrom(postMessageUri) {
|
|
6
19
|
return postMessageUri.replace(/\/chat\.postMessage$/, "/chat.update");
|
|
7
20
|
}
|
|
@@ -18,6 +31,31 @@ function slackBotTokenFor(input) {
|
|
|
18
31
|
}
|
|
19
32
|
return input.botToken;
|
|
20
33
|
}
|
|
34
|
+
function metadataString(metadata, key) {
|
|
35
|
+
const value = metadata[key];
|
|
36
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
function slackSourceMessageTarget(receipt) {
|
|
39
|
+
if (receipt.provider !== "slack") return null;
|
|
40
|
+
const channelId = metadataString(receipt.event.metadata, "channelId");
|
|
41
|
+
const messageTs = metadataString(receipt.event.metadata, "messageTs");
|
|
42
|
+
return channelId && messageTs ? { channelId, messageTs } : null;
|
|
43
|
+
}
|
|
44
|
+
function isAbortError(error) {
|
|
45
|
+
return error instanceof Error && error.name === "AbortError";
|
|
46
|
+
}
|
|
47
|
+
async function fetchWithTimeout(input) {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
50
|
+
try {
|
|
51
|
+
return await input.fetchImpl(input.uri, { ...input.init, signal: controller.signal });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (isAbortError(error)) return null;
|
|
54
|
+
throw error;
|
|
55
|
+
} finally {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
21
59
|
function createGitHubCallbackSink(input) {
|
|
22
60
|
const fetchImpl = input.fetchImpl ?? fetch;
|
|
23
61
|
const commentUriByKey = /* @__PURE__ */ new Map();
|
|
@@ -70,9 +108,9 @@ function createSlackCallbackSink(input) {
|
|
|
70
108
|
async deliver(message) {
|
|
71
109
|
if (message.provider !== "slack") return;
|
|
72
110
|
const botToken = slackBotTokenFor({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
111
|
+
botToken: input.botToken,
|
|
112
|
+
botTokensByAgentId: input.botTokensByAgentId,
|
|
113
|
+
agentId: message.agentId
|
|
76
114
|
});
|
|
77
115
|
if (!botToken) return;
|
|
78
116
|
const thread = parseSlackThreadKey(message.threadKey ?? "");
|
|
@@ -117,6 +155,51 @@ function createSlackCallbackSink(input) {
|
|
|
117
155
|
}
|
|
118
156
|
};
|
|
119
157
|
}
|
|
158
|
+
function createSlackSourceReceiptSink(input) {
|
|
159
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
160
|
+
const reactionsAddUri = input.reactionsAddUri ?? "https://slack.com/api/reactions.add";
|
|
161
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_SLACK_SOURCE_RECEIPT_TIMEOUT_MS;
|
|
162
|
+
return {
|
|
163
|
+
async deliver(receipt) {
|
|
164
|
+
const target = slackSourceMessageTarget(receipt);
|
|
165
|
+
if (!target) return { delivered: false };
|
|
166
|
+
const botToken = slackBotTokenFor({
|
|
167
|
+
botToken: input.botToken,
|
|
168
|
+
botTokensByAgentId: input.botTokensByAgentId,
|
|
169
|
+
agentId: receipt.agentId
|
|
170
|
+
});
|
|
171
|
+
if (!botToken) return { delivered: false };
|
|
172
|
+
const response = await fetchWithTimeout({
|
|
173
|
+
fetchImpl,
|
|
174
|
+
uri: reactionsAddUri,
|
|
175
|
+
timeoutMs,
|
|
176
|
+
init: {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
authorization: `Bearer ${botToken}`,
|
|
180
|
+
"content-type": "application/json"
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify(
|
|
183
|
+
createSlackReactionPayload({
|
|
184
|
+
channelId: target.channelId,
|
|
185
|
+
messageTs: target.messageTs,
|
|
186
|
+
name: slackSourceReceiptReactionName(receipt.state)
|
|
187
|
+
})
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (!response) return { delivered: false };
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
throw new Error(`deliver Slack source receipt failed: ${response.status} ${await response.text()}`);
|
|
194
|
+
}
|
|
195
|
+
const body = await response.json().catch(() => ({}));
|
|
196
|
+
if (body?.ok === false && body.error !== "already_reacted") {
|
|
197
|
+
throw new Error(`deliver Slack source receipt failed: ${body?.error ?? "unknown_error"}`);
|
|
198
|
+
}
|
|
199
|
+
return { delivered: true };
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
120
203
|
function createLarkCallbackSink(input) {
|
|
121
204
|
if (!input.client && Boolean(input.appId) !== Boolean(input.appSecret)) {
|
|
122
205
|
throw new Error("Lark callback sink requires both appId and appSecret (or neither).");
|
|
@@ -131,8 +214,27 @@ function createLarkCallbackSink(input) {
|
|
|
131
214
|
if (!message.threadKey) {
|
|
132
215
|
throw new Error("Lark callback message is missing threadKey.");
|
|
133
216
|
}
|
|
217
|
+
if (message.externalMessageId) {
|
|
218
|
+
if (message.rich?.provider === "lark") {
|
|
219
|
+
await patchLarkMessageCard(client, {
|
|
220
|
+
messageId: message.externalMessageId,
|
|
221
|
+
card: message.rich.payload
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
await updateLarkTextMessage(client, {
|
|
225
|
+
messageId: message.externalMessageId,
|
|
226
|
+
text: message.body
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return { externalMessageId: message.externalMessageId };
|
|
230
|
+
}
|
|
134
231
|
const { messageId } = parseLarkThreadKey(message.threadKey);
|
|
135
|
-
await replyLarkMessage(client, {
|
|
232
|
+
const reply = await replyLarkMessage(client, {
|
|
233
|
+
messageId,
|
|
234
|
+
text: message.body,
|
|
235
|
+
...message.rich?.provider === "lark" ? { card: message.rich.payload } : {}
|
|
236
|
+
});
|
|
237
|
+
return reply.messageId ? { externalMessageId: reply.messageId } : void 0;
|
|
136
238
|
}
|
|
137
239
|
};
|
|
138
240
|
}
|
|
@@ -144,9 +246,9 @@ function createTelegramCallbackSink(input) {
|
|
|
144
246
|
async deliver(message) {
|
|
145
247
|
if (message.provider !== "telegram") return;
|
|
146
248
|
const botToken = slackBotTokenFor({
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
249
|
+
botToken: input.botToken,
|
|
250
|
+
botTokensByAgentId: input.botTokensByAgentId,
|
|
251
|
+
agentId: message.agentId
|
|
150
252
|
});
|
|
151
253
|
if (!botToken) return;
|
|
152
254
|
const thread = parseTelegramThreadKey(message.threadKey ?? "");
|
|
@@ -191,70 +293,282 @@ function createTelegramCallbackSink(input) {
|
|
|
191
293
|
function createCompositeCallbackSink(sinks) {
|
|
192
294
|
return {
|
|
193
295
|
async deliver(message) {
|
|
296
|
+
let result;
|
|
297
|
+
let delivered = false;
|
|
298
|
+
const failures = [];
|
|
194
299
|
for (const sink of sinks) {
|
|
195
|
-
|
|
300
|
+
try {
|
|
301
|
+
const deliveredResult = await sink.deliver(message);
|
|
302
|
+
delivered = true;
|
|
303
|
+
if (deliveredResult?.externalMessageId && !result?.externalMessageId) {
|
|
304
|
+
result = { externalMessageId: deliveredResult.externalMessageId };
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
failures.push(error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!delivered && failures.length > 0) {
|
|
311
|
+
throw new AggregateError(failures, "Composite callback delivery failed for every sink.");
|
|
196
312
|
}
|
|
313
|
+
return result;
|
|
197
314
|
}
|
|
198
315
|
};
|
|
199
316
|
}
|
|
200
317
|
|
|
201
318
|
// src/presentation.ts
|
|
202
|
-
import {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
319
|
+
import {
|
|
320
|
+
createFinalSummaryPresentation,
|
|
321
|
+
createRunStatusPresentation,
|
|
322
|
+
platformCapabilityForProvider,
|
|
323
|
+
renderOpenTagPresentationPlainText,
|
|
324
|
+
shouldDeliverCallbackProgress,
|
|
325
|
+
shouldDeliverCallbackRunStatus
|
|
326
|
+
} from "@opentag/core";
|
|
327
|
+
import { renderAcknowledgement, renderFinalSummaryPresentation, renderProgress } from "@opentag/github";
|
|
328
|
+
import {
|
|
329
|
+
createLarkActionReceiptCard,
|
|
330
|
+
createLarkDoctorSummaryCard,
|
|
331
|
+
createLarkFinalSummaryCard,
|
|
332
|
+
createLarkRunStatusCard,
|
|
333
|
+
createLarkSourceThreadStatusCard,
|
|
334
|
+
renderLarkActionReceiptPresentation,
|
|
335
|
+
renderLarkFinalSummaryPresentation,
|
|
336
|
+
renderLarkRunStatusPresentation
|
|
337
|
+
} from "@opentag/lark";
|
|
338
|
+
import {
|
|
339
|
+
createSlackActionReceiptBlocks,
|
|
340
|
+
createSlackDoctorSummaryBlocks,
|
|
341
|
+
createSlackFinalSummaryBlocks,
|
|
342
|
+
createSlackSourceThreadStatusBlocks,
|
|
343
|
+
renderSlackActionReceiptPresentation,
|
|
344
|
+
renderSlackAcknowledgement,
|
|
345
|
+
renderSlackFinalSummaryPresentation
|
|
346
|
+
} from "@opentag/slack";
|
|
347
|
+
import { renderTelegramAcknowledgement, renderTelegramFinalSummaryPresentation, renderTelegramProgress } from "@opentag/telegram";
|
|
348
|
+
function renderRunStatus(provider, presentation) {
|
|
349
|
+
const canRenderRich = supportsRichPresentation(provider);
|
|
350
|
+
if (canRenderRich && provider === "lark") {
|
|
351
|
+
return {
|
|
352
|
+
body: renderLarkRunStatusPresentation(presentation),
|
|
353
|
+
rich: {
|
|
354
|
+
provider: "lark",
|
|
355
|
+
payload: createLarkRunStatusCard(presentation)
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (presentation.state === "received") {
|
|
360
|
+
if (provider === "slack") {
|
|
361
|
+
return { body: renderSlackAcknowledgement(presentation.runId) };
|
|
362
|
+
}
|
|
363
|
+
if (provider === "telegram") {
|
|
364
|
+
return { body: renderTelegramAcknowledgement(presentation.runId) };
|
|
365
|
+
}
|
|
366
|
+
return { body: renderAcknowledgement(presentation.runId) };
|
|
367
|
+
}
|
|
368
|
+
const message = presentation.message ?? presentation.nextAction ?? presentation.state;
|
|
369
|
+
if (provider === "telegram") {
|
|
370
|
+
return { body: renderTelegramProgress(message) };
|
|
371
|
+
}
|
|
372
|
+
return { body: renderProgress({ runId: presentation.runId, message }) };
|
|
373
|
+
}
|
|
374
|
+
function supportsRichPresentation(provider) {
|
|
375
|
+
return platformCapabilityForProvider(provider)?.supportsRichPresentation === true;
|
|
376
|
+
}
|
|
377
|
+
function renderFinalSummary(provider, presentation) {
|
|
378
|
+
const canRenderRich = supportsRichPresentation(provider);
|
|
379
|
+
if (canRenderRich && provider === "slack") {
|
|
380
|
+
return {
|
|
381
|
+
body: renderSlackFinalSummaryPresentation(presentation),
|
|
382
|
+
blocks: createSlackFinalSummaryBlocks(presentation)
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (canRenderRich && provider === "lark") {
|
|
386
|
+
return {
|
|
387
|
+
body: renderLarkFinalSummaryPresentation(presentation),
|
|
388
|
+
rich: {
|
|
389
|
+
provider: "lark",
|
|
390
|
+
payload: createLarkFinalSummaryCard(presentation)
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (provider === "telegram") {
|
|
395
|
+
return { body: renderTelegramFinalSummaryPresentation(presentation) };
|
|
396
|
+
}
|
|
397
|
+
return { body: renderFinalSummaryPresentation(presentation) };
|
|
398
|
+
}
|
|
399
|
+
function renderDoctorSummary(provider, presentation) {
|
|
400
|
+
const body = renderOpenTagPresentationPlainText(presentation);
|
|
401
|
+
const canRenderRich = supportsRichPresentation(provider);
|
|
402
|
+
if (canRenderRich && provider === "slack") {
|
|
403
|
+
return {
|
|
404
|
+
body,
|
|
405
|
+
blocks: createSlackDoctorSummaryBlocks(presentation)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (canRenderRich && provider === "lark") {
|
|
409
|
+
return {
|
|
410
|
+
body,
|
|
411
|
+
rich: {
|
|
412
|
+
provider: "lark",
|
|
413
|
+
payload: createLarkDoctorSummaryCard(presentation)
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return { body };
|
|
418
|
+
}
|
|
419
|
+
function renderSourceThreadStatus(provider, presentation) {
|
|
420
|
+
const body = renderOpenTagPresentationPlainText(presentation);
|
|
421
|
+
const canRenderRich = supportsRichPresentation(provider);
|
|
422
|
+
if (canRenderRich && provider === "slack") {
|
|
423
|
+
return {
|
|
424
|
+
body,
|
|
425
|
+
blocks: createSlackSourceThreadStatusBlocks(presentation)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (canRenderRich && provider === "lark") {
|
|
429
|
+
return {
|
|
430
|
+
body,
|
|
431
|
+
rich: {
|
|
432
|
+
provider: "lark",
|
|
433
|
+
payload: createLarkSourceThreadStatusCard(presentation)
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return { body };
|
|
438
|
+
}
|
|
439
|
+
function renderActionReceipt(provider, presentation) {
|
|
440
|
+
const body = provider === "slack" ? renderSlackActionReceiptPresentation(presentation) : provider === "lark" ? renderLarkActionReceiptPresentation(presentation) : renderOpenTagPresentationPlainText(presentation);
|
|
441
|
+
const canRenderRich = supportsRichPresentation(provider);
|
|
442
|
+
if (canRenderRich && provider === "slack") {
|
|
443
|
+
return {
|
|
444
|
+
body,
|
|
445
|
+
blocks: createSlackActionReceiptBlocks(presentation)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (canRenderRich && provider === "lark") {
|
|
449
|
+
return {
|
|
450
|
+
body,
|
|
451
|
+
rich: {
|
|
452
|
+
provider: "lark",
|
|
453
|
+
payload: createLarkActionReceiptCard(presentation)
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return { body };
|
|
458
|
+
}
|
|
206
459
|
function createDefaultCallbackPresentation() {
|
|
207
460
|
return {
|
|
208
461
|
shouldDeliverAcknowledgement(provider) {
|
|
209
|
-
return provider
|
|
462
|
+
return shouldDeliverCallbackRunStatus(provider);
|
|
463
|
+
},
|
|
464
|
+
shouldDeliverStatusUpdate(provider) {
|
|
465
|
+
return shouldDeliverCallbackRunStatus(provider);
|
|
466
|
+
},
|
|
467
|
+
shouldDeliverRunStatusUpdate(input) {
|
|
468
|
+
if (input.provider === "lark" && input.state === "running") return false;
|
|
469
|
+
return this.shouldDeliverStatusUpdate(input.provider);
|
|
210
470
|
},
|
|
211
471
|
shouldDeliverProgress(provider) {
|
|
212
|
-
return provider
|
|
472
|
+
return shouldDeliverCallbackProgress(provider);
|
|
213
473
|
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
474
|
+
runStatusPresentation(input) {
|
|
475
|
+
return createRunStatusPresentation({
|
|
476
|
+
runId: input.runId,
|
|
477
|
+
state: input.state,
|
|
478
|
+
...input.message ? { message: input.message } : {},
|
|
479
|
+
...input.nextAction ? { nextAction: input.nextAction } : {},
|
|
480
|
+
...input.detailVisibility ? { detailVisibility: input.detailVisibility } : {}
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
acknowledgementPresentation(input) {
|
|
484
|
+
return this.runStatusPresentation({
|
|
485
|
+
runId: input.runId,
|
|
486
|
+
state: "received",
|
|
487
|
+
detailVisibility: "source_thread"
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
progressPresentation(input) {
|
|
491
|
+
return this.runStatusPresentation({
|
|
492
|
+
runId: input.runId,
|
|
493
|
+
state: "running",
|
|
494
|
+
message: input.message,
|
|
495
|
+
detailVisibility: "audit"
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
finalPresentation(input) {
|
|
499
|
+
return createFinalSummaryPresentation({
|
|
500
|
+
result: input.result,
|
|
501
|
+
...input.receiptContext ? { receiptContext: input.receiptContext } : {},
|
|
502
|
+
...input.runId ? { auditRunId: input.runId } : {}
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
render(input) {
|
|
506
|
+
if (input.presentation.kind === "run_status") {
|
|
507
|
+
return renderRunStatus(input.provider, input.presentation);
|
|
508
|
+
}
|
|
509
|
+
if (input.presentation.kind === "final_summary") {
|
|
510
|
+
return renderFinalSummary(input.provider, input.presentation);
|
|
511
|
+
}
|
|
512
|
+
if (input.presentation.kind === "doctor_summary") {
|
|
513
|
+
return renderDoctorSummary(input.provider, input.presentation);
|
|
217
514
|
}
|
|
218
|
-
if (input.
|
|
219
|
-
return
|
|
515
|
+
if (input.presentation.kind === "source_thread_status") {
|
|
516
|
+
return renderSourceThreadStatus(input.provider, input.presentation);
|
|
220
517
|
}
|
|
221
|
-
if (input.
|
|
222
|
-
return
|
|
518
|
+
if (input.presentation.kind === "action_receipt") {
|
|
519
|
+
return renderActionReceipt(input.provider, input.presentation);
|
|
223
520
|
}
|
|
224
|
-
return
|
|
521
|
+
return {
|
|
522
|
+
body: renderOpenTagPresentationPlainText(input.presentation)
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
acknowledgement(input) {
|
|
526
|
+
return this.render({ provider: input.provider, presentation: this.acknowledgementPresentation({ runId: input.runId }) }).body;
|
|
527
|
+
},
|
|
528
|
+
runStatus(input) {
|
|
529
|
+
return this.render({
|
|
530
|
+
provider: input.provider,
|
|
531
|
+
presentation: this.runStatusPresentation({
|
|
532
|
+
runId: input.runId,
|
|
533
|
+
state: input.state,
|
|
534
|
+
...input.message ? { message: input.message } : {},
|
|
535
|
+
...input.nextAction ? { nextAction: input.nextAction } : {},
|
|
536
|
+
...input.detailVisibility ? { detailVisibility: input.detailVisibility } : {}
|
|
537
|
+
})
|
|
538
|
+
});
|
|
225
539
|
},
|
|
226
540
|
progress(input) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
541
|
+
return this.runStatus({
|
|
542
|
+
provider: input.provider,
|
|
543
|
+
runId: input.runId,
|
|
544
|
+
state: "running",
|
|
545
|
+
message: input.message,
|
|
546
|
+
detailVisibility: "audit"
|
|
547
|
+
}).body;
|
|
231
548
|
},
|
|
232
549
|
final(input) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
if (input.provider === "telegram") {
|
|
243
|
-
return { body: renderTelegramFinalResult(input.result) };
|
|
244
|
-
}
|
|
245
|
-
return { body: renderFinalResult(input.result) };
|
|
550
|
+
return this.render({
|
|
551
|
+
provider: input.provider,
|
|
552
|
+
presentation: this.finalPresentation({
|
|
553
|
+
result: input.result,
|
|
554
|
+
...input.runId ? { runId: input.runId } : {},
|
|
555
|
+
...input.receiptContext ? { receiptContext: input.receiptContext } : {}
|
|
556
|
+
})
|
|
557
|
+
});
|
|
246
558
|
}
|
|
247
559
|
};
|
|
248
560
|
}
|
|
249
561
|
|
|
250
562
|
// src/server.ts
|
|
251
|
-
import { createHash } from "crypto";
|
|
563
|
+
import { createHash, randomUUID } from "crypto";
|
|
252
564
|
import {
|
|
253
565
|
AdapterMutationMappingSchema,
|
|
254
566
|
ActorIdentitySchema,
|
|
255
567
|
ActionHintSchema,
|
|
568
|
+
capabilityForMutationIntent,
|
|
256
569
|
conversationKeysFromEvent as conversationKeysFromEvent2,
|
|
257
570
|
parseThreadActionCommand,
|
|
571
|
+
permissionScopesAllowCapability,
|
|
258
572
|
projectTargetRefFromEvent as projectTargetRefFromEvent2,
|
|
259
573
|
suggestedActionCandidatesFromSnapshots,
|
|
260
574
|
createAdapterMutationCompilerRegistry,
|
|
@@ -262,7 +576,12 @@ import {
|
|
|
262
576
|
OpenTagRunResultSchema,
|
|
263
577
|
PolicyRuleSchema,
|
|
264
578
|
RunEventImportanceSchema,
|
|
265
|
-
RunEventVisibilitySchema
|
|
579
|
+
RunEventVisibilitySchema,
|
|
580
|
+
DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
581
|
+
RequestBodyTooLargeError,
|
|
582
|
+
platformCapabilityForProvider as platformCapabilityForProvider2,
|
|
583
|
+
readRequestTextWithLimit,
|
|
584
|
+
shouldDeliverSourceReceipt
|
|
266
585
|
} from "@opentag/core";
|
|
267
586
|
import {
|
|
268
587
|
applyGitHubIssueMutationOperation,
|
|
@@ -272,6 +591,7 @@ import { createOpenTagRepository, migrateSchema } from "@opentag/store";
|
|
|
272
591
|
import Database from "better-sqlite3";
|
|
273
592
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
274
593
|
import { Hono } from "hono";
|
|
594
|
+
import { HTTPException } from "hono/http-exception";
|
|
275
595
|
import { z } from "zod";
|
|
276
596
|
|
|
277
597
|
// src/admission.ts
|
|
@@ -416,6 +736,144 @@ function createAdmissionRuntime(input) {
|
|
|
416
736
|
}
|
|
417
737
|
|
|
418
738
|
// src/server.ts
|
|
739
|
+
var RequestBodyRejectedError = class extends Error {
|
|
740
|
+
reason;
|
|
741
|
+
publicError;
|
|
742
|
+
constructor(input) {
|
|
743
|
+
super(input.reason);
|
|
744
|
+
this.name = "RequestBodyRejectedError";
|
|
745
|
+
this.reason = input.reason;
|
|
746
|
+
this.publicError = input.publicError ?? input.reason;
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
function requestBodyTooLarge(c, maxBytes) {
|
|
750
|
+
return new HTTPException(413, {
|
|
751
|
+
res: c.json({ error: "request_body_too_large", maxBytes }, 413)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
async function parseBody(c, schema, options = {}) {
|
|
755
|
+
let json;
|
|
756
|
+
try {
|
|
757
|
+
const rawBody = await readRequestTextWithLimit(c.req.raw, { maxBytes: options.maxBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES });
|
|
758
|
+
json = JSON.parse(rawBody);
|
|
759
|
+
} catch (err) {
|
|
760
|
+
if (err instanceof RequestBodyTooLargeError) throw requestBodyTooLarge(c, err.maxBytes);
|
|
761
|
+
if (err instanceof HTTPException) throw err;
|
|
762
|
+
if (err instanceof SyntaxError) {
|
|
763
|
+
throw new HTTPException(400, {
|
|
764
|
+
res: c.json({ error: "invalid_json_body" }, 400),
|
|
765
|
+
cause: new RequestBodyRejectedError({ reason: "invalid_json_body" })
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
throw err;
|
|
769
|
+
}
|
|
770
|
+
const result = schema.safeParse(json);
|
|
771
|
+
if (!result.success) {
|
|
772
|
+
const publicError = options.invalidBodyError ?? "invalid_request_body";
|
|
773
|
+
throw new HTTPException(400, {
|
|
774
|
+
res: c.json({ error: publicError, issues: result.error.issues }, 400),
|
|
775
|
+
cause: new RequestBodyRejectedError({ reason: "invalid_request_body", publicError })
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
return result.data;
|
|
779
|
+
}
|
|
780
|
+
function normalizeRateLimitedEndpoint(method, path) {
|
|
781
|
+
return `${method.toUpperCase()} ${path}`.replace(/^([A-Z]+) \/v1\/runners\/[^/]+\/runs\/[^/]+/, "$1 /v1/runners/:runnerId/runs/:runId").replace(/^([A-Z]+) \/v1\/runners\/[^/]+/, "$1 /v1/runners/:runnerId").replace(/^([A-Z]+) \/v1\/repo-bindings\/[^/]+\/[^/]+\/[^/]+/, "$1 /v1/repo-bindings/:provider/:owner/:repo").replace(/^([A-Z]+) \/v1\/channel-bindings\/[^/]+\/[^/]+\/[^/]+/, "$1 /v1/channel-bindings/:provider/:accountId/:conversationId").replace(/^([A-Z]+) \/v1\/slack-channel-bindings\/[^/]+\/[^/]+/, "$1 /v1/slack-channel-bindings/:teamId/:channelId").replace(/^([A-Z]+) \/v1\/follow-up-requests\/[^/]+/, "$1 /v1/follow-up-requests/:id").replace(/^([A-Z]+) \/v1\/proposals\/[^/]+/, "$1 /v1/proposals/:proposalId").replace(/^([A-Z]+) \/v1\/approvals\/[^/]+/, "$1 /v1/approvals/:approvalDecisionId").replace(/^([A-Z]+) \/v1\/apply-plans\/[^/]+/, "$1 /v1/apply-plans/:applyPlanId").replace(/^([A-Z]+) \/v1\/runs\/[^/]+/, "$1 /v1/runs/:runId");
|
|
782
|
+
}
|
|
783
|
+
function rateLimitRunnerId(path) {
|
|
784
|
+
return path.match(/^\/v1\/runners\/([^/]+)/)?.[1] ?? "none";
|
|
785
|
+
}
|
|
786
|
+
function rateLimitSourcePlatform(path) {
|
|
787
|
+
const channelProvider = path.match(/^\/v1\/channel-bindings\/([^/]+)/)?.[1];
|
|
788
|
+
if (channelProvider) return channelProvider;
|
|
789
|
+
const repoProvider = path.match(/^\/v1\/repo-bindings\/([^/]+)/)?.[1];
|
|
790
|
+
if (repoProvider) return repoProvider;
|
|
791
|
+
if (path.startsWith("/v1/slack-channel-bindings/")) return "slack";
|
|
792
|
+
return "unknown";
|
|
793
|
+
}
|
|
794
|
+
function safeDecodeRateLimitSegment(value) {
|
|
795
|
+
if (!value) return void 0;
|
|
796
|
+
try {
|
|
797
|
+
return decodeURIComponent(value);
|
|
798
|
+
} catch {
|
|
799
|
+
return value;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function rateLimitTenant(path) {
|
|
803
|
+
const channel = path.match(/^\/v1\/channel-bindings\/([^/]+)\/([^/]+)/);
|
|
804
|
+
if (channel) {
|
|
805
|
+
const provider = safeDecodeRateLimitSegment(channel[1]) ?? "unknown";
|
|
806
|
+
const accountId = safeDecodeRateLimitSegment(channel[2]) ?? "unknown";
|
|
807
|
+
return `${provider}:${accountId}`;
|
|
808
|
+
}
|
|
809
|
+
const legacySlack = path.match(/^\/v1\/slack-channel-bindings\/([^/]+)/);
|
|
810
|
+
if (legacySlack) return `slack:${safeDecodeRateLimitSegment(legacySlack[1]) ?? "unknown"}`;
|
|
811
|
+
const repo = path.match(/^\/v1\/repo-bindings\/([^/]+)\/([^/]+)/);
|
|
812
|
+
if (repo) {
|
|
813
|
+
const provider = safeDecodeRateLimitSegment(repo[1]) ?? "unknown";
|
|
814
|
+
const owner = safeDecodeRateLimitSegment(repo[2]) ?? "unknown";
|
|
815
|
+
return `${provider}:${owner}`;
|
|
816
|
+
}
|
|
817
|
+
return "unknown";
|
|
818
|
+
}
|
|
819
|
+
function rateLimitTokenFingerprint(authorization) {
|
|
820
|
+
if (!authorization) return "none";
|
|
821
|
+
return createHash("sha256").update(authorization).digest("hex").slice(0, 16);
|
|
822
|
+
}
|
|
823
|
+
function rawTokenFingerprint(token) {
|
|
824
|
+
return createHash("sha256").update(token).digest("hex");
|
|
825
|
+
}
|
|
826
|
+
function rateLimitKey(c) {
|
|
827
|
+
const path = new URL(c.req.url).pathname;
|
|
828
|
+
return [
|
|
829
|
+
`token=${rateLimitTokenFingerprint(c.req.raw.headers.get("authorization"))}`,
|
|
830
|
+
`runner=${rateLimitRunnerId(path)}`,
|
|
831
|
+
`source=${rateLimitSourcePlatform(path)}`,
|
|
832
|
+
`tenant=${rateLimitTenant(path)}`,
|
|
833
|
+
`endpoint=${normalizeRateLimitedEndpoint(c.req.method, path)}`
|
|
834
|
+
].join("|");
|
|
835
|
+
}
|
|
836
|
+
function createDispatcherRateLimitMiddleware(options) {
|
|
837
|
+
if (!Number.isFinite(options.windowMs) || options.windowMs <= 0) {
|
|
838
|
+
throw new Error("rateLimit.windowMs must be a positive number.");
|
|
839
|
+
}
|
|
840
|
+
if (!Number.isFinite(options.maxRequests) || options.maxRequests <= 0) {
|
|
841
|
+
throw new Error("rateLimit.maxRequests must be a positive number.");
|
|
842
|
+
}
|
|
843
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
844
|
+
const now = options.now ?? (() => Date.now());
|
|
845
|
+
return async (c, next) => {
|
|
846
|
+
const currentTime = now();
|
|
847
|
+
for (const [bucketKey, bucket2] of buckets) {
|
|
848
|
+
if (bucket2.resetAt <= currentTime) buckets.delete(bucketKey);
|
|
849
|
+
}
|
|
850
|
+
const key = rateLimitKey(c);
|
|
851
|
+
const existing = buckets.get(key);
|
|
852
|
+
const bucket = existing && existing.resetAt > currentTime ? existing : { count: 0, resetAt: currentTime + options.windowMs };
|
|
853
|
+
if (bucket.count >= options.maxRequests) {
|
|
854
|
+
const retryAfterMs = Math.max(0, bucket.resetAt - currentTime);
|
|
855
|
+
c.header("retry-after", String(Math.ceil(retryAfterMs / 1e3)));
|
|
856
|
+
return c.json(
|
|
857
|
+
{
|
|
858
|
+
error: "rate_limited",
|
|
859
|
+
retryAfterMs,
|
|
860
|
+
maxRequests: options.maxRequests,
|
|
861
|
+
windowMs: options.windowMs
|
|
862
|
+
},
|
|
863
|
+
429
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
bucket.count += 1;
|
|
867
|
+
buckets.set(key, bucket);
|
|
868
|
+
await next();
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function shouldDeliverRunStatusUpdate(presentation, input) {
|
|
872
|
+
return presentation.shouldDeliverRunStatusUpdate?.(input) ?? presentation.shouldDeliverStatusUpdate(input.provider);
|
|
873
|
+
}
|
|
874
|
+
function larkLifecycleStatusMessageKey(input) {
|
|
875
|
+
return input.provider === "lark" ? `${input.runId}:status` : void 0;
|
|
876
|
+
}
|
|
419
877
|
var CreateRunnerSchema = z.object({
|
|
420
878
|
runnerId: z.string().min(1),
|
|
421
879
|
name: z.string().min(1)
|
|
@@ -455,11 +913,32 @@ var CreateRunSchema = z.object({
|
|
|
455
913
|
runId: z.string().min(1),
|
|
456
914
|
event: OpenTagEventSchema
|
|
457
915
|
});
|
|
916
|
+
var RecordControlPlaneEventSchema = z.object({
|
|
917
|
+
type: z.string().min(1),
|
|
918
|
+
severity: z.enum(["info", "warn", "error"]).optional(),
|
|
919
|
+
subject: z.string().min(1).optional(),
|
|
920
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
921
|
+
createdAt: z.string().datetime().optional()
|
|
922
|
+
});
|
|
923
|
+
var PruneSourceDeliveriesSchema = z.object({
|
|
924
|
+
olderThan: z.string().datetime(),
|
|
925
|
+
limit: z.number().int().positive().max(1e5).optional()
|
|
926
|
+
});
|
|
458
927
|
var PromoteFollowUpRequestSchema = z.object({
|
|
459
928
|
runId: z.string().min(1)
|
|
460
929
|
});
|
|
461
930
|
var CompleteRunSchema = z.object({
|
|
462
|
-
result: OpenTagRunResultSchema
|
|
931
|
+
result: OpenTagRunResultSchema,
|
|
932
|
+
idempotencyKey: z.string().min(1).max(256).optional()
|
|
933
|
+
});
|
|
934
|
+
var MarkRunningSchema = z.object({
|
|
935
|
+
executor: z.string().min(1),
|
|
936
|
+
runTimeoutMs: z.number().int().positive().optional(),
|
|
937
|
+
idempotencyKey: z.string().min(1).max(256).optional()
|
|
938
|
+
});
|
|
939
|
+
var CancelRunSchema = z.object({
|
|
940
|
+
reason: z.string().min(1).optional(),
|
|
941
|
+
requestedBy: z.string().min(1).optional()
|
|
463
942
|
});
|
|
464
943
|
var ApprovalDecisionInputSchema = z.object({
|
|
465
944
|
id: z.string().min(1).optional(),
|
|
@@ -501,13 +980,38 @@ var ChildRunInputSchema = z.object({
|
|
|
501
980
|
sourceProposalId: z.string().min(1).optional(),
|
|
502
981
|
sourceApplyPlanId: z.string().min(1).optional()
|
|
503
982
|
});
|
|
983
|
+
var CHILD_EVENT_METADATA_REPLAY_KEYS = [
|
|
984
|
+
"sourceDeliveryId",
|
|
985
|
+
"webhookDeliveryId",
|
|
986
|
+
"deliveryId",
|
|
987
|
+
"githubDeliveryId",
|
|
988
|
+
"githubDeliveryGuid",
|
|
989
|
+
"slackEventId",
|
|
990
|
+
"larkEventId",
|
|
991
|
+
"signatureState",
|
|
992
|
+
"signatureVerified",
|
|
993
|
+
"verifiedSignature",
|
|
994
|
+
"webhookSignatureVerified",
|
|
995
|
+
"githubSignatureVerified"
|
|
996
|
+
];
|
|
504
997
|
var ProgressSchema = z.object({
|
|
505
998
|
type: z.string().min(1).optional(),
|
|
506
999
|
message: z.string().min(1),
|
|
507
1000
|
at: z.string().datetime().optional(),
|
|
508
1001
|
visibility: RunEventVisibilitySchema.optional(),
|
|
509
|
-
importance: RunEventImportanceSchema.optional()
|
|
1002
|
+
importance: RunEventImportanceSchema.optional(),
|
|
1003
|
+
idempotencyKey: z.string().min(1).max(256).optional()
|
|
510
1004
|
});
|
|
1005
|
+
function childEventMetadata(parentMetadata, metadata) {
|
|
1006
|
+
const sanitized = { ...parentMetadata };
|
|
1007
|
+
for (const key of CHILD_EVENT_METADATA_REPLAY_KEYS) {
|
|
1008
|
+
delete sanitized[key];
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
...sanitized,
|
|
1012
|
+
...metadata ?? {}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
511
1015
|
function childEventFromParent(input) {
|
|
512
1016
|
return {
|
|
513
1017
|
...input.parentEvent,
|
|
@@ -523,10 +1027,8 @@ function childEventFromParent(input) {
|
|
|
523
1027
|
actionKind: input.actionKind
|
|
524
1028
|
}
|
|
525
1029
|
},
|
|
526
|
-
metadata:
|
|
527
|
-
|
|
528
|
-
...input.metadata ?? {}
|
|
529
|
-
}
|
|
1030
|
+
metadata: childEventMetadata(input.parentEvent.metadata, input.metadata),
|
|
1031
|
+
permissions: input.permissions ?? input.parentEvent.permissions
|
|
530
1032
|
};
|
|
531
1033
|
}
|
|
532
1034
|
function mappingsFromAdapterPlan(adapterPlan) {
|
|
@@ -544,13 +1046,35 @@ function metadataIssueNumber(metadata) {
|
|
|
544
1046
|
if (typeof value === "string" && /^[1-9]\d*$/.test(value)) return value;
|
|
545
1047
|
return void 0;
|
|
546
1048
|
}
|
|
547
|
-
function
|
|
1049
|
+
function metadataString2(metadata, key) {
|
|
548
1050
|
const value = metadata?.[key];
|
|
549
1051
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
550
1052
|
}
|
|
1053
|
+
function sourceContainerMetadata(input) {
|
|
1054
|
+
if (input.provider === "lark") {
|
|
1055
|
+
return { tenantKey: input.accountId, chatId: input.conversationId };
|
|
1056
|
+
}
|
|
1057
|
+
if (input.provider === "slack") {
|
|
1058
|
+
return { teamId: input.accountId, channelId: input.conversationId };
|
|
1059
|
+
}
|
|
1060
|
+
if (input.provider === "telegram") {
|
|
1061
|
+
return { botId: input.accountId, chatId: input.conversationId };
|
|
1062
|
+
}
|
|
1063
|
+
return { accountId: input.accountId, conversationId: input.conversationId };
|
|
1064
|
+
}
|
|
1065
|
+
function latestRunTimeoutMs(events) {
|
|
1066
|
+
for (const event of [...events].reverse()) {
|
|
1067
|
+
if (event.type !== "run.running" || !event.payload || typeof event.payload !== "object") continue;
|
|
1068
|
+
const runTimeoutMs = event.payload.runTimeoutMs;
|
|
1069
|
+
if (typeof runTimeoutMs === "number" && Number.isInteger(runTimeoutMs) && runTimeoutMs > 0) {
|
|
1070
|
+
return runTimeoutMs;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
551
1075
|
function githubIssueWorkItemExternalId(metadata) {
|
|
552
|
-
const owner =
|
|
553
|
-
const repo =
|
|
1076
|
+
const owner = metadataString2(metadata, "owner");
|
|
1077
|
+
const repo = metadataString2(metadata, "repo");
|
|
554
1078
|
const issueNumber = metadataIssueNumber(metadata);
|
|
555
1079
|
if (!owner || !repo || !issueNumber) return void 0;
|
|
556
1080
|
return `${owner}/${repo}#${issueNumber}`;
|
|
@@ -662,6 +1186,30 @@ async function resolveThreadAction(input) {
|
|
|
662
1186
|
});
|
|
663
1187
|
const primaryConversationKey = conversationKeys[0];
|
|
664
1188
|
const targetWorkItemExternalId = githubIssueWorkItemExternalId(input.metadata);
|
|
1189
|
+
const metadataProposalId = metadataString2(input.metadata, "proposalId");
|
|
1190
|
+
const metadataIntentId = metadataString2(input.metadata, "intentId");
|
|
1191
|
+
if (metadataProposalId && (input.command.selection.kind === "index" || input.command.selection.kind === "latest")) {
|
|
1192
|
+
const stored = await input.repo.getSuggestedChanges({ proposalId: metadataProposalId });
|
|
1193
|
+
if (!stored) {
|
|
1194
|
+
return { ok: false, reason: "no_proposal", message: `I could not find proposal \`${metadataProposalId}\`.` };
|
|
1195
|
+
}
|
|
1196
|
+
const claimed = await input.repo.getRun({ runId: stored.runId });
|
|
1197
|
+
if (!claimed) {
|
|
1198
|
+
return { ok: false, reason: "no_proposal", message: "I found the proposal but not its source run." };
|
|
1199
|
+
}
|
|
1200
|
+
const proposalConversationKeys = conversationKeysFromEvent2(claimed.event);
|
|
1201
|
+
if (!proposalConversationKeys.some((key) => conversationKeys.includes(key))) {
|
|
1202
|
+
return { ok: false, reason: "no_match", runId: stored.runId, message: "That proposal does not belong to this source thread." };
|
|
1203
|
+
}
|
|
1204
|
+
const proposal = { runId: stored.runId, run: claimed.run, event: claimed.event, snapshot: stored.snapshot };
|
|
1205
|
+
if (targetWorkItemExternalId && !proposalMatchesWorkItem(proposal, targetWorkItemExternalId)) {
|
|
1206
|
+
return { ok: false, reason: "no_match", runId: stored.runId, message: "That proposal does not belong to this source thread." };
|
|
1207
|
+
}
|
|
1208
|
+
return resolveCandidateSelection({
|
|
1209
|
+
command: metadataIntentId ? { ...input.command, selection: { kind: "intent", intentId: metadataIntentId } } : { ...input.command, selection: { kind: "proposal", proposalId: metadataProposalId } },
|
|
1210
|
+
proposals: [proposal]
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
665
1213
|
if (input.command.selection.kind === "proposal") {
|
|
666
1214
|
const stored = await input.repo.getSuggestedChanges({ proposalId: input.command.selection.proposalId });
|
|
667
1215
|
if (!stored) {
|
|
@@ -707,6 +1255,240 @@ function isRepoLevelGitHubIntent(intent) {
|
|
|
707
1255
|
function adapterForAction(input) {
|
|
708
1256
|
return hasGitHubRepoTarget(input.event) && (hasGitHubIssueOrPullTarget(input.event) || input.selectedIntents.length > 0 && input.selectedIntents.every((intent) => isRepoLevelGitHubIntent(intent))) ? "github" : input.callbackProvider;
|
|
709
1257
|
}
|
|
1258
|
+
function executorConditionsFromIntent(intent) {
|
|
1259
|
+
const value = intent.params?.["executorConditions"];
|
|
1260
|
+
if (!Array.isArray(value)) return [];
|
|
1261
|
+
return value.filter((condition) => typeof condition === "string" && condition.length > 0);
|
|
1262
|
+
}
|
|
1263
|
+
var GITHUB_PREFLIGHT_TIMEOUT_MS = 5e3;
|
|
1264
|
+
function githubPreflightCacheKey(input) {
|
|
1265
|
+
return `${input.owner}/${input.repo}${input.path}`;
|
|
1266
|
+
}
|
|
1267
|
+
function createGitHubPreflightDeadline(timeoutMs) {
|
|
1268
|
+
if (typeof AbortController === "undefined") return { clear: () => {
|
|
1269
|
+
}, didTimeout: () => false };
|
|
1270
|
+
const controller = new AbortController();
|
|
1271
|
+
let didTimeout = false;
|
|
1272
|
+
const timeout = setTimeout(() => {
|
|
1273
|
+
didTimeout = true;
|
|
1274
|
+
controller.abort();
|
|
1275
|
+
}, timeoutMs);
|
|
1276
|
+
return {
|
|
1277
|
+
signal: controller.signal,
|
|
1278
|
+
clear: () => clearTimeout(timeout),
|
|
1279
|
+
didTimeout: () => didTimeout
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
async function githubPreflight(input) {
|
|
1283
|
+
if (input.cache) {
|
|
1284
|
+
const cacheKey = githubPreflightCacheKey(input);
|
|
1285
|
+
const cached = input.cache.get(cacheKey);
|
|
1286
|
+
if (cached) return await cached;
|
|
1287
|
+
const pending = githubPreflightUncached(input);
|
|
1288
|
+
input.cache.set(cacheKey, pending);
|
|
1289
|
+
return await pending;
|
|
1290
|
+
}
|
|
1291
|
+
return await githubPreflightUncached(input);
|
|
1292
|
+
}
|
|
1293
|
+
async function githubPreflightUncached(input) {
|
|
1294
|
+
let response;
|
|
1295
|
+
const deadline = createGitHubPreflightDeadline(GITHUB_PREFLIGHT_TIMEOUT_MS);
|
|
1296
|
+
try {
|
|
1297
|
+
response = await (input.githubApply.fetchImpl ?? fetch)(`https://api.github.com/repos/${input.owner}/${input.repo}${input.path}`, {
|
|
1298
|
+
method: "GET",
|
|
1299
|
+
headers: {
|
|
1300
|
+
accept: "application/vnd.github+json",
|
|
1301
|
+
authorization: `Bearer ${input.githubApply.token}`,
|
|
1302
|
+
"x-github-api-version": "2022-11-28"
|
|
1303
|
+
},
|
|
1304
|
+
...deadline.signal ? { signal: deadline.signal } : {}
|
|
1305
|
+
});
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
if (deadline.didTimeout()) {
|
|
1308
|
+
return {
|
|
1309
|
+
state: "needs_setup",
|
|
1310
|
+
setupReason: `GitHub preflight timed out for ${input.description} after ${GITHUB_PREFLIGHT_TIMEOUT_MS}ms.`
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
state: "needs_setup",
|
|
1315
|
+
setupReason: `GitHub preflight failed for ${input.description}: ${error instanceof Error ? error.message : String(error)}.`
|
|
1316
|
+
};
|
|
1317
|
+
} finally {
|
|
1318
|
+
deadline.clear();
|
|
1319
|
+
}
|
|
1320
|
+
if (response.ok) return null;
|
|
1321
|
+
if (response.status === 401 || response.status === 403) {
|
|
1322
|
+
return {
|
|
1323
|
+
state: "needs_setup",
|
|
1324
|
+
setupReason: `GitHub apply token cannot access ${input.description}. Check repository permissions and token scopes.`
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
if (response.status === 404) {
|
|
1328
|
+
return {
|
|
1329
|
+
state: "needs_setup",
|
|
1330
|
+
setupReason: input.notFoundReason
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
state: "needs_setup",
|
|
1335
|
+
setupReason: `GitHub preflight failed for ${input.description}: HTTP ${response.status}.`
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
async function preflightGitHubOperation(input) {
|
|
1339
|
+
const base = {
|
|
1340
|
+
githubApply: input.githubApply,
|
|
1341
|
+
owner: input.target.owner,
|
|
1342
|
+
repo: input.target.repoName,
|
|
1343
|
+
...input.preflightCache ? { cache: input.preflightCache } : {}
|
|
1344
|
+
};
|
|
1345
|
+
if (input.operation.kind === "create_pull_request") {
|
|
1346
|
+
const head = encodeURIComponent(input.operation.head);
|
|
1347
|
+
const baseBranch = encodeURIComponent(input.operation.base);
|
|
1348
|
+
const [headPreflight, basePreflight] = await Promise.all([
|
|
1349
|
+
githubPreflight({
|
|
1350
|
+
...base,
|
|
1351
|
+
path: `/branches/${head}`,
|
|
1352
|
+
description: `GitHub branch ${input.operation.head}`,
|
|
1353
|
+
notFoundReason: `GitHub branch ${input.operation.head} was not found.`
|
|
1354
|
+
}),
|
|
1355
|
+
githubPreflight({
|
|
1356
|
+
...base,
|
|
1357
|
+
path: `/branches/${baseBranch}`,
|
|
1358
|
+
description: `GitHub base branch ${input.operation.base}`,
|
|
1359
|
+
notFoundReason: `GitHub base branch ${input.operation.base} was not found.`
|
|
1360
|
+
})
|
|
1361
|
+
]);
|
|
1362
|
+
return headPreflight ?? basePreflight;
|
|
1363
|
+
}
|
|
1364
|
+
if (input.operation.kind === "request_review") {
|
|
1365
|
+
if (typeof input.target.pullRequestNumber !== "number") {
|
|
1366
|
+
return {
|
|
1367
|
+
state: "needs_setup",
|
|
1368
|
+
setupReason: "The source thread does not include a GitHub pull request target."
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
return await githubPreflight({
|
|
1372
|
+
...base,
|
|
1373
|
+
path: `/pulls/${input.target.pullRequestNumber}`,
|
|
1374
|
+
description: `GitHub pull request #${input.target.pullRequestNumber}`,
|
|
1375
|
+
notFoundReason: `GitHub pull request #${input.target.pullRequestNumber} was not found.`
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
if (typeof input.target.issueNumber !== "number") {
|
|
1379
|
+
return {
|
|
1380
|
+
state: "needs_setup",
|
|
1381
|
+
setupReason: "The source thread does not include a GitHub issue or pull request target."
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
return await githubPreflight({
|
|
1385
|
+
...base,
|
|
1386
|
+
path: `/issues/${input.target.issueNumber}`,
|
|
1387
|
+
description: `GitHub issue or pull request #${input.target.issueNumber}`,
|
|
1388
|
+
notFoundReason: `GitHub issue or pull request #${input.target.issueNumber} was not found.`
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
async function directApplyReceiptCapability(input) {
|
|
1392
|
+
const capability = capabilityForMutationIntent(input.intent);
|
|
1393
|
+
if (!capability) {
|
|
1394
|
+
return {
|
|
1395
|
+
state: "unsupported",
|
|
1396
|
+
setupReason: `No source-thread apply capability is registered for ${input.intent.action}.`
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
if (capability.capabilityClass !== "external_write") {
|
|
1400
|
+
return {
|
|
1401
|
+
state: "unsupported",
|
|
1402
|
+
setupReason: "This action is audit-only for now; continue if a follow-up run should handle it."
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
const adapter = adapterForAction({
|
|
1406
|
+
event: input.event,
|
|
1407
|
+
callbackProvider: input.callbackProvider,
|
|
1408
|
+
selectedIntents: [input.intent]
|
|
1409
|
+
});
|
|
1410
|
+
if (adapter !== "github") {
|
|
1411
|
+
return {
|
|
1412
|
+
state: "needs_setup",
|
|
1413
|
+
setupReason: `Direct apply for ${adapter} actions is not configured on this dispatcher.`
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
if (!input.githubApply) {
|
|
1417
|
+
return {
|
|
1418
|
+
state: "needs_setup",
|
|
1419
|
+
setupReason: "GitHub apply is not configured on this dispatcher."
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
if (!hasGitHubRepoTarget(input.event)) {
|
|
1423
|
+
return {
|
|
1424
|
+
state: "needs_setup",
|
|
1425
|
+
setupReason: "The source thread does not include a GitHub repository target."
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
if (!isRepoLevelGitHubIntent(input.intent) && !hasGitHubIssueOrPullTarget(input.event)) {
|
|
1429
|
+
return {
|
|
1430
|
+
state: "needs_setup",
|
|
1431
|
+
setupReason: "The source thread does not include a GitHub issue or pull request target."
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
if (!permissionScopesAllowCapability(input.event.permissions ?? [], capability)) {
|
|
1435
|
+
return {
|
|
1436
|
+
state: "needs_setup",
|
|
1437
|
+
setupReason: `Missing platform permission for ${capability.id}.`
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
const missingExecutorConditions = (capability.requiredExecutorConditions ?? []).filter(
|
|
1441
|
+
(condition) => !executorConditionsFromIntent(input.intent).includes(condition)
|
|
1442
|
+
);
|
|
1443
|
+
if (missingExecutorConditions.length > 0) {
|
|
1444
|
+
return {
|
|
1445
|
+
state: "needs_setup",
|
|
1446
|
+
setupReason: `Missing executor condition: ${missingExecutorConditions.join(", ")}.`
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
const githubTarget = githubTargetFromEvent(input.event);
|
|
1450
|
+
if (!githubTarget) {
|
|
1451
|
+
return {
|
|
1452
|
+
state: "needs_setup",
|
|
1453
|
+
setupReason: "The source thread does not include a GitHub repository target."
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
const compilation = createGitHubIssueMutationCompiler({
|
|
1457
|
+
...githubTarget?.targetKind ? { targetKind: githubTarget.targetKind } : {}
|
|
1458
|
+
}).compile(input.intent);
|
|
1459
|
+
if (!compilation.ok) {
|
|
1460
|
+
return {
|
|
1461
|
+
state: compilation.outcome.outcome === "unsupported" ? "unsupported" : "needs_setup",
|
|
1462
|
+
setupReason: compilation.outcome.message ?? "GitHub cannot apply this action from the current source thread."
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
const preflight = await preflightGitHubOperation({
|
|
1466
|
+
githubApply: input.githubApply,
|
|
1467
|
+
target: githubTarget,
|
|
1468
|
+
operation: compilation.operation,
|
|
1469
|
+
...input.preflightCache ? { preflightCache: input.preflightCache } : {}
|
|
1470
|
+
});
|
|
1471
|
+
if (preflight) return preflight;
|
|
1472
|
+
return { state: "ready_to_apply" };
|
|
1473
|
+
}
|
|
1474
|
+
async function actionReceiptContextForFinal(input) {
|
|
1475
|
+
const preflightCache = /* @__PURE__ */ new Map();
|
|
1476
|
+
const capabilityEntries = await Promise.all(
|
|
1477
|
+
(input.result.suggestedChanges ?? []).flatMap(
|
|
1478
|
+
(snapshot) => snapshot.intents.map(async (intent) => {
|
|
1479
|
+
const capability = await directApplyReceiptCapability({
|
|
1480
|
+
event: input.event,
|
|
1481
|
+
callbackProvider: input.event.callback.provider,
|
|
1482
|
+
intent,
|
|
1483
|
+
...input.githubApply ? { githubApply: input.githubApply } : {},
|
|
1484
|
+
preflightCache
|
|
1485
|
+
});
|
|
1486
|
+
return [intent.intentId, capability];
|
|
1487
|
+
})
|
|
1488
|
+
)
|
|
1489
|
+
);
|
|
1490
|
+
return { capabilityByIntentId: Object.fromEntries(capabilityEntries) };
|
|
1491
|
+
}
|
|
710
1492
|
async function authorizeThreadAction(input) {
|
|
711
1493
|
const repoKey = projectTargetRefFromEvent2(input.resolved.proposal.event);
|
|
712
1494
|
if (!repoKey) {
|
|
@@ -789,6 +1571,13 @@ function selectedIntentsAlreadyApplied(input) {
|
|
|
789
1571
|
(intentId) => input.plan.outcomes?.some((outcome) => outcome.intentId === intentId && outcome.outcome === "applied")
|
|
790
1572
|
);
|
|
791
1573
|
}
|
|
1574
|
+
function selectedPlanOutcomes(input) {
|
|
1575
|
+
return (input.plan.outcomes ?? []).filter((outcome) => input.selectedIntentIds.includes(outcome.intentId));
|
|
1576
|
+
}
|
|
1577
|
+
function selectedIntentsHaveStaleOutcome(input) {
|
|
1578
|
+
const outcomes = selectedPlanOutcomes(input);
|
|
1579
|
+
return outcomes.some((outcome) => outcome.outcome === "stale") && outcomes.every((outcome) => outcome.outcome !== "applied");
|
|
1580
|
+
}
|
|
792
1581
|
function githubTargetFromEvent(event) {
|
|
793
1582
|
const owner = event.metadata["owner"];
|
|
794
1583
|
const repoName = event.metadata["repo"];
|
|
@@ -807,30 +1596,124 @@ function githubTargetFromEvent(event) {
|
|
|
807
1596
|
function selectedActionSummary(candidates) {
|
|
808
1597
|
return candidates.map((candidate) => `${candidate.index}. ${candidate.intent.summary}`).join("; ");
|
|
809
1598
|
}
|
|
810
|
-
function
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1599
|
+
function selectedActionReceiptTitle(selectionText) {
|
|
1600
|
+
return selectionText.split(";").map((part) => part.trim().replace(/^\d+\.\s*/, "")).filter(Boolean).join("; ");
|
|
1601
|
+
}
|
|
1602
|
+
function sentenceWithTerminalPunctuation(value) {
|
|
1603
|
+
return /[.!?。!?]$/u.test(value) ? value : `${value}.`;
|
|
1604
|
+
}
|
|
1605
|
+
function addPermissionGrant(permissions, grant) {
|
|
1606
|
+
if (permissions.some((permission) => permission.scope === grant.scope)) return permissions;
|
|
1607
|
+
return [...permissions, grant];
|
|
1608
|
+
}
|
|
1609
|
+
function childRunPermissionsForThreadAction(input) {
|
|
1610
|
+
let permissions = [...input.resolved.proposal.event.permissions ?? []];
|
|
1611
|
+
if (input.command.verb === "apply" || input.command.verb === "continue") {
|
|
1612
|
+
permissions = addPermissionGrant(permissions, {
|
|
1613
|
+
scope: "repo:read",
|
|
1614
|
+
reason: "inspect the repository while continuing an approved source-thread action"
|
|
1615
|
+
});
|
|
1616
|
+
permissions = addPermissionGrant(permissions, {
|
|
1617
|
+
scope: "repo:write",
|
|
1618
|
+
reason: "apply an approved source-thread mutation on a run branch"
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
if (input.resolved.selectedCandidates.some((candidate) => candidate.intent.action === "create_pull_request")) {
|
|
1622
|
+
permissions = addPermissionGrant(permissions, {
|
|
1623
|
+
scope: "pr:create",
|
|
1624
|
+
reason: "create the pull request approved in the source thread"
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
return permissions;
|
|
821
1628
|
}
|
|
822
1629
|
function renderChildRunCreatedBody(input) {
|
|
1630
|
+
const title = selectedActionReceiptTitle(input.selectionText ?? selectedActionSummary(input.resolved.selectedCandidates));
|
|
1631
|
+
if (input.provider === "slack") {
|
|
1632
|
+
return [
|
|
1633
|
+
input.lead,
|
|
1634
|
+
`Action: ${title}`,
|
|
1635
|
+
...input.fallbackReason ? [`Reason: ${input.fallbackReason}`] : []
|
|
1636
|
+
].join("\n");
|
|
1637
|
+
}
|
|
823
1638
|
return [
|
|
824
1639
|
input.lead,
|
|
825
1640
|
"",
|
|
826
|
-
`
|
|
1641
|
+
`Action: ${title}`,
|
|
827
1642
|
"",
|
|
828
|
-
|
|
829
|
-
...childRunContextLines(input),
|
|
1643
|
+
`Child run: \`${input.childRun.id}\``,
|
|
830
1644
|
"",
|
|
831
|
-
|
|
1645
|
+
...input.fallbackReason ? [`Reason: ${input.fallbackReason}`, ""] : [],
|
|
1646
|
+
`Audit: run \`opentag status --run ${input.childRun.id}\` locally.`
|
|
832
1647
|
].join("\n");
|
|
833
1648
|
}
|
|
1649
|
+
function applyOutcomeSummary(outcome) {
|
|
1650
|
+
if (outcome.externalUri) return `${outcome.outcome}: ${outcome.externalUri}`;
|
|
1651
|
+
if (outcome.message) return `${outcome.outcome}: ${outcome.message}`;
|
|
1652
|
+
return `${outcome.outcome}.`;
|
|
1653
|
+
}
|
|
1654
|
+
function applyOutcomeReceiptLines(outcomes) {
|
|
1655
|
+
if (outcomes.length === 0) return ["Result: applied."];
|
|
1656
|
+
if (outcomes.length === 1) {
|
|
1657
|
+
const outcome = outcomes[0];
|
|
1658
|
+
if (outcome.externalUri) return [`Result: ${outcome.externalUri}`];
|
|
1659
|
+
if (outcome.message) return [`Result: ${outcome.outcome}. ${outcome.message}`];
|
|
1660
|
+
return [`Result: ${outcome.outcome}.`];
|
|
1661
|
+
}
|
|
1662
|
+
return ["Results:", ...outcomes.map((outcome) => `- ${applyOutcomeSummary(outcome)}`)];
|
|
1663
|
+
}
|
|
1664
|
+
function renderAppliedThreadActionBody(input) {
|
|
1665
|
+
const selectedOutcomes = input.outcomes.filter((outcome) => input.selectedIntentIds.includes(outcome.intentId));
|
|
1666
|
+
return [`Applied: ${sentenceWithTerminalPunctuation(selectedActionReceiptTitle(input.selectionText))}`, ...applyOutcomeReceiptLines(selectedOutcomes)].join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
function renderAlreadyAppliedThreadActionBody(input) {
|
|
1669
|
+
return [`Already applied: ${sentenceWithTerminalPunctuation(selectedActionReceiptTitle(input.selectionText))}`, "No external write was repeated."].join("\n");
|
|
1670
|
+
}
|
|
1671
|
+
function renderAlreadyPlannedThreadActionBody(input) {
|
|
1672
|
+
return [`Already planned: ${sentenceWithTerminalPunctuation(selectedActionReceiptTitle(input.selectionText))}`, "OpenTag did not execute this repeated reply."].join("\n");
|
|
1673
|
+
}
|
|
1674
|
+
function renderStaleThreadActionBody(input) {
|
|
1675
|
+
return [
|
|
1676
|
+
`Stale: ${sentenceWithTerminalPunctuation(selectedActionReceiptTitle(input.selectionText))}`,
|
|
1677
|
+
"The target changed since this action was proposed.",
|
|
1678
|
+
`Reply \`continue ${input.continueIndex}\` to refresh from the current thread state.`
|
|
1679
|
+
].join("\n");
|
|
1680
|
+
}
|
|
1681
|
+
function renderThreadActionRecordedBody(input) {
|
|
1682
|
+
const title = selectedActionReceiptTitle(input.selectionText);
|
|
1683
|
+
if (input.verb === "approve") {
|
|
1684
|
+
const index = input.applyIndex ?? 1;
|
|
1685
|
+
const nextLines = input.directApply?.ready ? [`Next: reply \`apply ${index}\` to write it to the system of record, or \`continue ${index}\` to continue in OpenTag.`] : [
|
|
1686
|
+
...input.directApply?.reason ? [`Direct apply is not available yet: ${sentenceWithTerminalPunctuation(input.directApply.reason)}`] : ["Direct apply is not available yet."],
|
|
1687
|
+
`Next: reply \`continue ${index}\` to continue in OpenTag.`
|
|
1688
|
+
];
|
|
1689
|
+
return [
|
|
1690
|
+
`Approved only: ${sentenceWithTerminalPunctuation(title)}`,
|
|
1691
|
+
"No external write was performed.",
|
|
1692
|
+
...nextLines
|
|
1693
|
+
].join("\n");
|
|
1694
|
+
}
|
|
1695
|
+
return [`Rejected: ${sentenceWithTerminalPunctuation(title)}`, "No external write will be performed for this action."].join("\n");
|
|
1696
|
+
}
|
|
1697
|
+
async function selectedDirectApplyStatus(input) {
|
|
1698
|
+
if (input.candidates.length === 0) return { ready: false, reason: "No selected action was found." };
|
|
1699
|
+
const preflightCache = /* @__PURE__ */ new Map();
|
|
1700
|
+
for (const candidate of input.candidates) {
|
|
1701
|
+
const capability = await directApplyReceiptCapability({
|
|
1702
|
+
event: input.event,
|
|
1703
|
+
callbackProvider: input.callbackProvider,
|
|
1704
|
+
intent: candidate.intent,
|
|
1705
|
+
...input.githubApply ? { githubApply: input.githubApply } : {},
|
|
1706
|
+
preflightCache
|
|
1707
|
+
});
|
|
1708
|
+
if (capability.state !== "ready_to_apply") {
|
|
1709
|
+
return {
|
|
1710
|
+
ready: false,
|
|
1711
|
+
reason: capability.setupReason ?? `Receipt state is ${capability.state}.`
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return { ready: true };
|
|
1716
|
+
}
|
|
834
1717
|
function actionContextPointer(input) {
|
|
835
1718
|
const lines = [
|
|
836
1719
|
"OpenTag thread action continuation.",
|
|
@@ -895,7 +1778,8 @@ async function createChildRunForThreadAction(input) {
|
|
|
895
1778
|
...input.approvalDecisionId ? { approvalDecisionId: input.approvalDecisionId } : {},
|
|
896
1779
|
...input.sourceApplyPlanId ? { sourceApplyPlanId: input.sourceApplyPlanId } : {},
|
|
897
1780
|
...input.fallbackReason ? { fallbackReason: input.fallbackReason } : {}
|
|
898
|
-
}
|
|
1781
|
+
},
|
|
1782
|
+
permissions: childRunPermissionsForThreadAction({ resolved: input.resolved, command: input.command })
|
|
899
1783
|
}),
|
|
900
1784
|
parentRunId: input.resolved.proposal.runId,
|
|
901
1785
|
triggeredByAction: action,
|
|
@@ -972,6 +1856,11 @@ var noopCallbackSink = {
|
|
|
972
1856
|
return;
|
|
973
1857
|
}
|
|
974
1858
|
};
|
|
1859
|
+
var noopSourceReceiptSink = {
|
|
1860
|
+
async deliver() {
|
|
1861
|
+
return { delivered: false };
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
975
1864
|
function nextCallbackAttemptAt(input) {
|
|
976
1865
|
const maxAttempts = input.maxAttempts ?? 5;
|
|
977
1866
|
const nextAttempt = input.attempts + 1;
|
|
@@ -983,7 +1872,13 @@ function nextCallbackAttemptAt(input) {
|
|
|
983
1872
|
}
|
|
984
1873
|
async function deliverCallbackDelivery(input) {
|
|
985
1874
|
try {
|
|
986
|
-
await input.
|
|
1875
|
+
const externalMessageId = input.delivery.externalMessageId ?? (input.delivery.statusMessageKey ? await input.repo.findCallbackExternalMessageId({
|
|
1876
|
+
runId: input.delivery.runId,
|
|
1877
|
+
provider: input.delivery.provider,
|
|
1878
|
+
...input.delivery.threadKey ? { threadKey: input.delivery.threadKey } : {},
|
|
1879
|
+
statusMessageKey: input.delivery.statusMessageKey
|
|
1880
|
+
}) : void 0);
|
|
1881
|
+
const deliveryResult = await input.sink.deliver({
|
|
987
1882
|
runId: input.delivery.runId,
|
|
988
1883
|
kind: input.delivery.kind,
|
|
989
1884
|
provider: input.delivery.provider,
|
|
@@ -992,15 +1887,23 @@ async function deliverCallbackDelivery(input) {
|
|
|
992
1887
|
...input.delivery.threadKey ? { threadKey: input.delivery.threadKey } : {},
|
|
993
1888
|
...input.delivery.agentId ? { agentId: input.delivery.agentId } : {},
|
|
994
1889
|
...input.delivery.statusMessageKey ? { statusMessageKey: input.delivery.statusMessageKey } : {},
|
|
995
|
-
...
|
|
1890
|
+
...externalMessageId ? { externalMessageId } : {},
|
|
1891
|
+
...input.delivery.blocks ? { blocks: input.delivery.blocks } : {},
|
|
1892
|
+
...input.delivery.rich ? { rich: input.delivery.rich } : {}
|
|
1893
|
+
});
|
|
1894
|
+
const deliveredExternalMessageId = deliveryResult?.externalMessageId ?? externalMessageId;
|
|
1895
|
+
await input.repo.markCallbackDelivered({
|
|
1896
|
+
deliveryId: input.delivery.id,
|
|
1897
|
+
...deliveredExternalMessageId ? { externalMessageId: deliveredExternalMessageId } : {}
|
|
996
1898
|
});
|
|
997
|
-
await input.repo.markCallbackDelivered({ deliveryId: input.delivery.id });
|
|
998
1899
|
return true;
|
|
999
1900
|
} catch (error) {
|
|
1901
|
+
const maxAttempts = input.retry?.maxAttempts ?? 5;
|
|
1000
1902
|
const nextAttemptAt = nextCallbackAttemptAt({ attempts: input.delivery.attempts, ...input.retry ?? {} });
|
|
1001
1903
|
await input.repo.markCallbackFailed({
|
|
1002
1904
|
deliveryId: input.delivery.id,
|
|
1003
1905
|
error: error instanceof Error ? error.message : String(error),
|
|
1906
|
+
maxAttempts,
|
|
1004
1907
|
...nextAttemptAt ? { nextAttemptAt } : {}
|
|
1005
1908
|
});
|
|
1006
1909
|
return false;
|
|
@@ -1040,7 +1943,8 @@ async function deliverAndAudit(input) {
|
|
|
1040
1943
|
...input.message.threadKey ? { threadKey: input.message.threadKey } : {},
|
|
1041
1944
|
...input.message.agentId ? { agentId: input.message.agentId } : {},
|
|
1042
1945
|
...input.message.statusMessageKey ? { statusMessageKey: input.message.statusMessageKey } : {},
|
|
1043
|
-
...input.message.blocks ? { blocks: input.message.blocks } : {}
|
|
1946
|
+
...input.message.blocks ? { blocks: input.message.blocks } : {},
|
|
1947
|
+
...input.message.rich ? { rich: input.message.rich } : {}
|
|
1044
1948
|
});
|
|
1045
1949
|
await deliverCallbackDelivery({
|
|
1046
1950
|
repo: input.repo,
|
|
@@ -1049,9 +1953,113 @@ async function deliverAndAudit(input) {
|
|
|
1049
1953
|
...input.retry ? { retry: input.retry } : {}
|
|
1050
1954
|
});
|
|
1051
1955
|
}
|
|
1052
|
-
function
|
|
1053
|
-
|
|
1054
|
-
|
|
1956
|
+
async function deliverSourceReceiptBestEffort(input) {
|
|
1957
|
+
try {
|
|
1958
|
+
const result = await input.sink.deliver(input.receipt);
|
|
1959
|
+
if (!result.delivered) return result;
|
|
1960
|
+
await input.repo.appendRunEvent({
|
|
1961
|
+
runId: input.receipt.runId,
|
|
1962
|
+
type: "source_receipt.delivered",
|
|
1963
|
+
payload: {
|
|
1964
|
+
provider: input.receipt.provider,
|
|
1965
|
+
state: input.receipt.state
|
|
1966
|
+
},
|
|
1967
|
+
visibility: "audit",
|
|
1968
|
+
importance: "low",
|
|
1969
|
+
message: `Source ${input.receipt.state} receipt delivered.`
|
|
1970
|
+
});
|
|
1971
|
+
return result;
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
await input.repo.appendRunEvent({
|
|
1974
|
+
runId: input.receipt.runId,
|
|
1975
|
+
type: "source_receipt.failed",
|
|
1976
|
+
payload: {
|
|
1977
|
+
provider: input.receipt.provider,
|
|
1978
|
+
state: input.receipt.state,
|
|
1979
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1980
|
+
},
|
|
1981
|
+
visibility: "audit",
|
|
1982
|
+
importance: "low",
|
|
1983
|
+
message: `Source ${input.receipt.state} receipt failed.`
|
|
1984
|
+
});
|
|
1985
|
+
return { delivered: false };
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function isRunnerRuntimeEndpoint(method, path) {
|
|
1989
|
+
if (method !== "POST") return false;
|
|
1990
|
+
if (/^\/v1\/runners\/[^/]+\/claim$/.test(path)) return true;
|
|
1991
|
+
if (/^\/v1\/runners\/[^/]+\/runs\/[^/]+\/(running|heartbeat|progress|complete)$/.test(path)) return true;
|
|
1992
|
+
return /^\/v1\/runs\/[^/]+\/(running|progress|complete)$/.test(path);
|
|
1993
|
+
}
|
|
1994
|
+
function isRunnerOperatorEndpoint(method, path) {
|
|
1995
|
+
if (method === "GET") {
|
|
1996
|
+
if (path === "/v1/control-plane-alerts") return true;
|
|
1997
|
+
if (/^\/v1\/runners\/[^/]+$/.test(path)) return true;
|
|
1998
|
+
if (/^\/v1\/repo-bindings\/[^/]+\/[^/]+\/[^/]+$/.test(path)) return true;
|
|
1999
|
+
if (/^\/v1\/channel-bindings\/[^/]+\/[^/]+\/[^/]+(?:\/status)?$/.test(path)) return true;
|
|
2000
|
+
return /^\/v1\/runs\/[^/]+(?:\/events|\/metrics)?$/.test(path);
|
|
2001
|
+
}
|
|
2002
|
+
if (method !== "POST") return false;
|
|
2003
|
+
if (path === "/v1/source-deliveries/prune") return true;
|
|
2004
|
+
if (/^\/v1\/runs\/[^/]+\/cancel$/.test(path)) return true;
|
|
2005
|
+
return /^\/v1\/channel-bindings\/[^/]+\/[^/]+\/[^/]+\/cancel-active-run$/.test(path);
|
|
2006
|
+
}
|
|
2007
|
+
function dispatcherAuthScope(request) {
|
|
2008
|
+
const path = new URL(request.url).pathname;
|
|
2009
|
+
const method = request.method.toUpperCase();
|
|
2010
|
+
if (isRunnerRuntimeEndpoint(method, path)) return "runner_runtime";
|
|
2011
|
+
if (isRunnerOperatorEndpoint(method, path)) return "runner_operator";
|
|
2012
|
+
return "pairing";
|
|
2013
|
+
}
|
|
2014
|
+
function authMatches(request, token) {
|
|
2015
|
+
return Boolean(token) && request.headers.get("authorization") === `Bearer ${token}`;
|
|
2016
|
+
}
|
|
2017
|
+
function bearerToken(request) {
|
|
2018
|
+
const authorization = request.headers.get("authorization");
|
|
2019
|
+
if (!authorization?.startsWith("Bearer ")) return void 0;
|
|
2020
|
+
return authorization.slice("Bearer ".length);
|
|
2021
|
+
}
|
|
2022
|
+
function configuredRunnerTokens(input) {
|
|
2023
|
+
return [...new Set([input.runnerToken, ...input.runnerTokens ?? []].map((token) => token?.trim()).filter((token) => Boolean(token)))];
|
|
2024
|
+
}
|
|
2025
|
+
function normalizeRevokedRunnerTokenFingerprints(fingerprints) {
|
|
2026
|
+
return new Set((fingerprints ?? []).map((fingerprint) => fingerprint.trim().toLowerCase()).filter(Boolean));
|
|
2027
|
+
}
|
|
2028
|
+
function authMatchesAny(request, tokens) {
|
|
2029
|
+
return tokens.some((token) => authMatches(request, token));
|
|
2030
|
+
}
|
|
2031
|
+
function requestUsesRevokedRunnerToken(input) {
|
|
2032
|
+
const token = bearerToken(input.request);
|
|
2033
|
+
if (!token) return false;
|
|
2034
|
+
return input.revokedRunnerTokenFingerprints.has(rawTokenFingerprint(token).toLowerCase());
|
|
2035
|
+
}
|
|
2036
|
+
function revokedRunnerTokenResult() {
|
|
2037
|
+
return {
|
|
2038
|
+
ok: false,
|
|
2039
|
+
reason: "runner_token_revoked",
|
|
2040
|
+
message: "Runner token has been revoked or expired. Pair again or update daemon.runnerToken before retrying."
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
function authorizeDispatcherRequest(input) {
|
|
2044
|
+
if (!input.pairingToken && input.runnerTokens.length === 0) return { ok: true };
|
|
2045
|
+
const revokedRunnerToken = requestUsesRevokedRunnerToken({
|
|
2046
|
+
request: input.request,
|
|
2047
|
+
revokedRunnerTokenFingerprints: input.revokedRunnerTokenFingerprints
|
|
2048
|
+
});
|
|
2049
|
+
if (revokedRunnerToken) return revokedRunnerTokenResult();
|
|
2050
|
+
const scope = dispatcherAuthScope(input.request);
|
|
2051
|
+
const pairingMatches = authMatches(input.request, input.pairingToken);
|
|
2052
|
+
const runnerMatches = authMatchesAny(input.request, input.runnerTokens);
|
|
2053
|
+
if (scope === "pairing") {
|
|
2054
|
+
return pairingMatches ? { ok: true } : { ok: false, reason: "invalid_pairing_token" };
|
|
2055
|
+
}
|
|
2056
|
+
if (scope === "runner_runtime") {
|
|
2057
|
+
if (input.runnerTokens.length > 0) {
|
|
2058
|
+
return runnerMatches ? { ok: true } : { ok: false, reason: "invalid_runner_token" };
|
|
2059
|
+
}
|
|
2060
|
+
return pairingMatches ? { ok: true } : { ok: false, reason: "invalid_pairing_token" };
|
|
2061
|
+
}
|
|
2062
|
+
return runnerMatches || pairingMatches ? { ok: true } : { ok: false, reason: "invalid_dispatcher_token" };
|
|
1055
2063
|
}
|
|
1056
2064
|
function createDispatcherApp(input) {
|
|
1057
2065
|
const sqlite = new Database(input.databasePath);
|
|
@@ -1059,22 +2067,237 @@ function createDispatcherApp(input) {
|
|
|
1059
2067
|
const repo = createOpenTagRepository(drizzle(sqlite));
|
|
1060
2068
|
const app = new Hono();
|
|
1061
2069
|
const callbackSink = input.callbackSink ?? noopCallbackSink;
|
|
2070
|
+
const sourceReceiptSink = input.sourceReceiptSink ?? noopSourceReceiptSink;
|
|
1062
2071
|
const presentation = input.presentation ?? createDefaultCallbackPresentation();
|
|
1063
2072
|
const callbackRetry = input.callbackRetry ?? {};
|
|
2073
|
+
const maxRequestBodyBytes = input.maxRequestBodyBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES;
|
|
2074
|
+
const runnerTokens = configuredRunnerTokens(input);
|
|
2075
|
+
const revokedRunnerTokenFingerprints = normalizeRevokedRunnerTokenFingerprints(input.revokedRunnerTokenFingerprints);
|
|
2076
|
+
const requestEndpoint = (c) => normalizeRateLimitedEndpoint(c.req.method, new URL(c.req.url).pathname);
|
|
2077
|
+
const recordControlPlaneEvent = async (input2) => {
|
|
2078
|
+
await repo.appendControlPlaneEvent({
|
|
2079
|
+
type: input2.type,
|
|
2080
|
+
...input2.severity ? { severity: input2.severity } : {},
|
|
2081
|
+
...input2.subject ? { subject: input2.subject } : {},
|
|
2082
|
+
...input2.payload ? { payload: input2.payload } : {},
|
|
2083
|
+
...input2.createdAt ? { createdAt: input2.createdAt } : {}
|
|
2084
|
+
});
|
|
2085
|
+
};
|
|
2086
|
+
const parseDispatcherBody = async (c, schema, options = {}) => {
|
|
2087
|
+
try {
|
|
2088
|
+
return await parseBody(c, schema, { maxBytes: maxRequestBodyBytes, ...options });
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
if (err instanceof HTTPException && err.status === 413) {
|
|
2091
|
+
await recordControlPlaneEvent({
|
|
2092
|
+
type: "security.request_body_rejected",
|
|
2093
|
+
severity: "warn",
|
|
2094
|
+
subject: requestEndpoint(c),
|
|
2095
|
+
payload: {
|
|
2096
|
+
reason: "request_body_too_large",
|
|
2097
|
+
endpoint: requestEndpoint(c),
|
|
2098
|
+
maxBytes: maxRequestBodyBytes,
|
|
2099
|
+
contentLength: c.req.raw.headers.get("content-length") ?? null
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
if (err instanceof HTTPException && err.status === 400 && err.cause instanceof RequestBodyRejectedError) {
|
|
2104
|
+
await recordControlPlaneEvent({
|
|
2105
|
+
type: "security.request_body_rejected",
|
|
2106
|
+
severity: "warn",
|
|
2107
|
+
subject: requestEndpoint(c),
|
|
2108
|
+
payload: {
|
|
2109
|
+
reason: err.cause.reason,
|
|
2110
|
+
error: err.cause.publicError,
|
|
2111
|
+
endpoint: requestEndpoint(c),
|
|
2112
|
+
contentLength: c.req.raw.headers.get("content-length") ?? null
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
throw err;
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
const appendSuppressedRunStatusCallback = async (input2) => {
|
|
2120
|
+
const capability = platformCapabilityForProvider2(input2.provider);
|
|
2121
|
+
await repo.appendRunEvent({
|
|
2122
|
+
runId: input2.runId,
|
|
2123
|
+
type: "callback.progress.suppressed",
|
|
2124
|
+
payload: {
|
|
2125
|
+
provider: input2.provider,
|
|
2126
|
+
reason: "platform_liveness_strategy",
|
|
2127
|
+
requestedStatus: input2.state,
|
|
2128
|
+
livenessStrategy: capability?.livenessStrategy ?? "unknown"
|
|
2129
|
+
},
|
|
2130
|
+
visibility: "audit",
|
|
2131
|
+
importance: "low",
|
|
2132
|
+
message: "Run status callback suppressed by platform liveness strategy; use status or audit for details."
|
|
2133
|
+
});
|
|
2134
|
+
};
|
|
2135
|
+
async function deliverPromotedFollowUpAcknowledgement(input2) {
|
|
2136
|
+
if (!presentation.shouldDeliverAcknowledgement(input2.event.callback.provider)) return;
|
|
2137
|
+
const acknowledgementPresentation = presentation.acknowledgementPresentation({ runId: input2.run.id });
|
|
2138
|
+
const acknowledgement = presentation.render({
|
|
2139
|
+
provider: input2.event.callback.provider,
|
|
2140
|
+
presentation: acknowledgementPresentation
|
|
2141
|
+
});
|
|
2142
|
+
const statusMessageKey = larkLifecycleStatusMessageKey({ provider: input2.event.callback.provider, runId: input2.run.id });
|
|
2143
|
+
await deliverAndAudit({
|
|
2144
|
+
repo,
|
|
2145
|
+
sink: callbackSink,
|
|
2146
|
+
retry: callbackRetry,
|
|
2147
|
+
message: {
|
|
2148
|
+
runId: input2.run.id,
|
|
2149
|
+
kind: "acknowledgement",
|
|
2150
|
+
provider: input2.event.callback.provider,
|
|
2151
|
+
uri: input2.event.callback.uri,
|
|
2152
|
+
body: acknowledgement.body,
|
|
2153
|
+
...input2.event.target.agentId ? { agentId: input2.event.target.agentId } : {},
|
|
2154
|
+
...input2.event.callback.threadKey ? { threadKey: input2.event.callback.threadKey } : {},
|
|
2155
|
+
...statusMessageKey ? { statusMessageKey } : {},
|
|
2156
|
+
...acknowledgement.blocks?.length ? { blocks: acknowledgement.blocks } : {},
|
|
2157
|
+
...acknowledgement.rich ? { rich: acknowledgement.rich } : {}
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
async function promoteFollowUpRequest(input2) {
|
|
2162
|
+
const promoted = await repo.createRunFromFollowUpRequest(input2);
|
|
2163
|
+
await deliverPromotedFollowUpAcknowledgement({
|
|
2164
|
+
run: promoted.run,
|
|
2165
|
+
event: promoted.followUpRequest.event
|
|
2166
|
+
});
|
|
2167
|
+
return promoted;
|
|
2168
|
+
}
|
|
2169
|
+
async function promoteNextFollowUpAfterTerminalRun(input2) {
|
|
2170
|
+
const [next] = await repo.listQueuedFollowUpsForActiveRun({ activeRunId: input2.activeRunId });
|
|
2171
|
+
if (!next) return null;
|
|
2172
|
+
try {
|
|
2173
|
+
const promoted = await promoteFollowUpRequest({
|
|
2174
|
+
followUpRequestId: next.id,
|
|
2175
|
+
runId: `run_${randomUUID()}`
|
|
2176
|
+
});
|
|
2177
|
+
await repo.appendRunEvent({
|
|
2178
|
+
runId: input2.activeRunId,
|
|
2179
|
+
type: "follow_up_request.auto_promoted",
|
|
2180
|
+
payload: {
|
|
2181
|
+
followUpRequestId: promoted.followUpRequest.id,
|
|
2182
|
+
createdRunId: promoted.run.id
|
|
2183
|
+
},
|
|
2184
|
+
visibility: "audit",
|
|
2185
|
+
importance: "normal",
|
|
2186
|
+
message: `Promoted queued follow-up ${promoted.followUpRequest.id} into run ${promoted.run.id}.`
|
|
2187
|
+
});
|
|
2188
|
+
return promoted;
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
await repo.appendRunEvent({
|
|
2191
|
+
runId: input2.activeRunId,
|
|
2192
|
+
type: "follow_up_request.auto_promote_failed",
|
|
2193
|
+
payload: {
|
|
2194
|
+
followUpRequestId: next.id,
|
|
2195
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2196
|
+
},
|
|
2197
|
+
visibility: "audit",
|
|
2198
|
+
importance: "high",
|
|
2199
|
+
message: `Could not auto-promote queued follow-up ${next.id}.`
|
|
2200
|
+
});
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
1064
2204
|
const admission = createAdmissionRuntime({
|
|
1065
2205
|
repo,
|
|
1066
2206
|
...input.agentAccessProfileCheck ? { agentAccessProfileCheck: input.agentAccessProfileCheck } : {}
|
|
1067
2207
|
});
|
|
1068
2208
|
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
2209
|
+
if (input.rateLimit) {
|
|
2210
|
+
app.use("/v1/*", createDispatcherRateLimitMiddleware(input.rateLimit));
|
|
2211
|
+
}
|
|
1069
2212
|
app.use("/v1/*", async (c, next) => {
|
|
1070
|
-
|
|
1071
|
-
|
|
2213
|
+
const authorization = authorizeDispatcherRequest({
|
|
2214
|
+
request: c.req.raw,
|
|
2215
|
+
pairingToken: input.pairingToken,
|
|
2216
|
+
runnerTokens,
|
|
2217
|
+
revokedRunnerTokenFingerprints
|
|
2218
|
+
});
|
|
2219
|
+
if (!authorization.ok) {
|
|
2220
|
+
await recordControlPlaneEvent({
|
|
2221
|
+
type: "security.auth_failed",
|
|
2222
|
+
severity: "warn",
|
|
2223
|
+
subject: requestEndpoint(c),
|
|
2224
|
+
payload: {
|
|
2225
|
+
reason: authorization.reason,
|
|
2226
|
+
endpoint: requestEndpoint(c),
|
|
2227
|
+
hasAuthorization: Boolean(c.req.raw.headers.get("authorization")),
|
|
2228
|
+
tokenFingerprint: rateLimitTokenFingerprint(c.req.raw.headers.get("authorization"))
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
return c.json(
|
|
2232
|
+
{
|
|
2233
|
+
error: "unauthorized",
|
|
2234
|
+
reason: authorization.reason,
|
|
2235
|
+
...authorization.message ? { message: authorization.message } : {}
|
|
2236
|
+
},
|
|
2237
|
+
401
|
|
2238
|
+
);
|
|
1072
2239
|
}
|
|
1073
2240
|
await next();
|
|
1074
2241
|
});
|
|
2242
|
+
app.get("/v1/control-plane-events", async (c) => {
|
|
2243
|
+
const limitValue = Number(c.req.query("limit") ?? 100);
|
|
2244
|
+
const limit = Number.isFinite(limitValue) ? Math.max(1, Math.min(500, Math.floor(limitValue))) : 100;
|
|
2245
|
+
const eventQuery = { limit };
|
|
2246
|
+
const type = c.req.query("type");
|
|
2247
|
+
const severity = c.req.query("severity");
|
|
2248
|
+
if (type) eventQuery.type = type;
|
|
2249
|
+
if (severity === "info" || severity === "warn" || severity === "error") eventQuery.severity = severity;
|
|
2250
|
+
const events = await repo.listControlPlaneEvents(eventQuery);
|
|
2251
|
+
return c.json({ events });
|
|
2252
|
+
});
|
|
2253
|
+
app.post("/v1/control-plane-events", async (c) => {
|
|
2254
|
+
const parsed = await parseDispatcherBody(c, RecordControlPlaneEventSchema);
|
|
2255
|
+
await recordControlPlaneEvent(parsed);
|
|
2256
|
+
return c.json({ ok: true }, 201);
|
|
2257
|
+
});
|
|
2258
|
+
app.get("/v1/control-plane-alerts", async (c) => {
|
|
2259
|
+
const limitValue = Number(c.req.query("limit") ?? 5e3);
|
|
2260
|
+
const limit = Number.isFinite(limitValue) ? Math.max(1, Math.min(1e4, Math.floor(limitValue))) : 5e3;
|
|
2261
|
+
const since = c.req.query("since");
|
|
2262
|
+
const alerts = await repo.summarizeControlPlaneAlerts({
|
|
2263
|
+
limit,
|
|
2264
|
+
...since ? { since } : {}
|
|
2265
|
+
});
|
|
2266
|
+
return c.json({ alerts });
|
|
2267
|
+
});
|
|
2268
|
+
app.post("/v1/source-deliveries/prune", async (c) => {
|
|
2269
|
+
const parsed = await parseDispatcherBody(c, PruneSourceDeliveriesSchema);
|
|
2270
|
+
const pruneInput = {
|
|
2271
|
+
olderThan: parsed.olderThan,
|
|
2272
|
+
...parsed.limit !== void 0 ? { limit: parsed.limit } : {}
|
|
2273
|
+
};
|
|
2274
|
+
const result = await repo.pruneSourceDeliveries(pruneInput);
|
|
2275
|
+
await recordControlPlaneEvent({
|
|
2276
|
+
type: "maintenance.source_deliveries_pruned",
|
|
2277
|
+
severity: "info",
|
|
2278
|
+
subject: "source_deliveries",
|
|
2279
|
+
payload: {
|
|
2280
|
+
olderThan: parsed.olderThan,
|
|
2281
|
+
limit: parsed.limit ?? null,
|
|
2282
|
+
scanned: result.scanned,
|
|
2283
|
+
pruned: result.pruned,
|
|
2284
|
+
retainedActive: result.retainedActive
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
return c.json({ result });
|
|
2288
|
+
});
|
|
1075
2289
|
app.post("/v1/runners", async (c) => {
|
|
1076
|
-
const parsed =
|
|
2290
|
+
const parsed = await parseDispatcherBody(c, CreateRunnerSchema);
|
|
1077
2291
|
await repo.registerRunner(parsed);
|
|
2292
|
+
await recordControlPlaneEvent({
|
|
2293
|
+
type: "runner.registered",
|
|
2294
|
+
severity: "info",
|
|
2295
|
+
subject: parsed.runnerId,
|
|
2296
|
+
payload: {
|
|
2297
|
+
runnerId: parsed.runnerId,
|
|
2298
|
+
name: parsed.name
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
1078
2301
|
return c.json({ ok: true }, 201);
|
|
1079
2302
|
});
|
|
1080
2303
|
app.get("/v1/runners/:runnerId", async (c) => {
|
|
@@ -1083,7 +2306,7 @@ function createDispatcherApp(input) {
|
|
|
1083
2306
|
return c.json({ runner });
|
|
1084
2307
|
});
|
|
1085
2308
|
app.post("/v1/repo-bindings", async (c) => {
|
|
1086
|
-
const parsed =
|
|
2309
|
+
const parsed = await parseDispatcherBody(c, CreateRepoBindingSchema);
|
|
1087
2310
|
await repo.createRepoBinding({
|
|
1088
2311
|
provider: parsed.provider,
|
|
1089
2312
|
owner: parsed.owner,
|
|
@@ -1093,6 +2316,20 @@ function createDispatcherApp(input) {
|
|
|
1093
2316
|
...parsed.defaultExecutor ? { defaultExecutor: parsed.defaultExecutor } : {},
|
|
1094
2317
|
...parsed.allowedActors?.length ? { allowedActors: parsed.allowedActors } : {}
|
|
1095
2318
|
});
|
|
2319
|
+
await recordControlPlaneEvent({
|
|
2320
|
+
type: "binding.repository.upserted",
|
|
2321
|
+
severity: "info",
|
|
2322
|
+
subject: `${parsed.provider}:${parsed.owner}/${parsed.repo}`,
|
|
2323
|
+
payload: {
|
|
2324
|
+
provider: parsed.provider,
|
|
2325
|
+
owner: parsed.owner,
|
|
2326
|
+
repo: parsed.repo,
|
|
2327
|
+
runnerId: parsed.runnerId,
|
|
2328
|
+
hasWorkspacePath: Boolean(parsed.workspacePath),
|
|
2329
|
+
...parsed.defaultExecutor ? { defaultExecutor: parsed.defaultExecutor } : {},
|
|
2330
|
+
allowedActorsCount: parsed.allowedActors?.length ?? 0
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
1096
2333
|
return c.json({ ok: true }, 201);
|
|
1097
2334
|
});
|
|
1098
2335
|
app.get("/v1/repo-bindings/:provider/:owner/:repo", async (c) => {
|
|
@@ -1105,13 +2342,32 @@ function createDispatcherApp(input) {
|
|
|
1105
2342
|
return c.json({ binding });
|
|
1106
2343
|
});
|
|
1107
2344
|
app.post("/v1/repo-bindings/:provider/:owner/:repo/policy-rules", async (c) => {
|
|
1108
|
-
const parsed =
|
|
2345
|
+
const parsed = await parseDispatcherBody(c, UpsertPolicyRuleSchema);
|
|
2346
|
+
const provider = c.req.param("provider");
|
|
2347
|
+
const owner = c.req.param("owner");
|
|
2348
|
+
const repoName = c.req.param("repo");
|
|
1109
2349
|
const rule = await repo.upsertRepoPolicyRule({
|
|
1110
|
-
provider
|
|
1111
|
-
owner
|
|
1112
|
-
repo:
|
|
2350
|
+
provider,
|
|
2351
|
+
owner,
|
|
2352
|
+
repo: repoName,
|
|
1113
2353
|
rule: parsed.rule
|
|
1114
2354
|
});
|
|
2355
|
+
await recordControlPlaneEvent({
|
|
2356
|
+
type: "binding.repository.policy_rule.upserted",
|
|
2357
|
+
severity: "info",
|
|
2358
|
+
subject: `${provider}:${owner}/${repoName}:${rule.id}`,
|
|
2359
|
+
payload: {
|
|
2360
|
+
provider,
|
|
2361
|
+
owner,
|
|
2362
|
+
repo: repoName,
|
|
2363
|
+
ruleId: rule.id,
|
|
2364
|
+
scope: rule.scope,
|
|
2365
|
+
effect: rule.effect,
|
|
2366
|
+
...rule.capabilityId ? { capabilityId: rule.capabilityId } : {},
|
|
2367
|
+
...rule.mutationDomain ? { mutationDomain: rule.mutationDomain } : {},
|
|
2368
|
+
hasReason: Boolean(rule.reason)
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
1115
2371
|
return c.json({ rule }, 201);
|
|
1116
2372
|
});
|
|
1117
2373
|
app.get("/v1/repo-bindings/:provider/:owner/:repo/policy-rules", async (c) => {
|
|
@@ -1123,13 +2379,32 @@ function createDispatcherApp(input) {
|
|
|
1123
2379
|
return c.json({ rules });
|
|
1124
2380
|
});
|
|
1125
2381
|
app.post("/v1/repo-bindings/:provider/:owner/:repo/mutation-mappings", async (c) => {
|
|
1126
|
-
const parsed =
|
|
2382
|
+
const parsed = await parseDispatcherBody(c, UpsertMutationMappingSchema);
|
|
2383
|
+
const provider = c.req.param("provider");
|
|
2384
|
+
const owner = c.req.param("owner");
|
|
2385
|
+
const repoName = c.req.param("repo");
|
|
1127
2386
|
const mapping = await repo.upsertRepoMutationMapping({
|
|
1128
|
-
provider
|
|
1129
|
-
owner
|
|
1130
|
-
repo:
|
|
2387
|
+
provider,
|
|
2388
|
+
owner,
|
|
2389
|
+
repo: repoName,
|
|
1131
2390
|
mapping: parsed.mapping
|
|
1132
2391
|
});
|
|
2392
|
+
await recordControlPlaneEvent({
|
|
2393
|
+
type: "binding.repository.mutation_mapping.upserted",
|
|
2394
|
+
severity: "info",
|
|
2395
|
+
subject: `${provider}:${owner}/${repoName}:${mapping.id}`,
|
|
2396
|
+
payload: {
|
|
2397
|
+
provider,
|
|
2398
|
+
owner,
|
|
2399
|
+
repo: repoName,
|
|
2400
|
+
mappingId: mapping.id,
|
|
2401
|
+
adapter: mapping.adapter,
|
|
2402
|
+
domain: mapping.domain,
|
|
2403
|
+
strategy: mapping.strategy,
|
|
2404
|
+
valueCount: Object.keys(mapping.values).length,
|
|
2405
|
+
hasDescription: Boolean(mapping.description)
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
1133
2408
|
return c.json({ mapping }, 201);
|
|
1134
2409
|
});
|
|
1135
2410
|
app.get("/v1/repo-bindings/:provider/:owner/:repo/mutation-mappings", async (c) => {
|
|
@@ -1155,7 +2430,7 @@ function createDispatcherApp(input) {
|
|
|
1155
2430
|
return c.json({ metrics });
|
|
1156
2431
|
});
|
|
1157
2432
|
app.post("/v1/channel-bindings", async (c) => {
|
|
1158
|
-
const parsed =
|
|
2433
|
+
const parsed = await parseDispatcherBody(c, CreateChannelBindingSchema);
|
|
1159
2434
|
await repo.upsertChannelBinding({
|
|
1160
2435
|
provider: parsed.provider,
|
|
1161
2436
|
accountId: parsed.accountId,
|
|
@@ -1165,6 +2440,20 @@ function createDispatcherApp(input) {
|
|
|
1165
2440
|
repo: parsed.repo,
|
|
1166
2441
|
...parsed.metadata ? { metadata: parsed.metadata } : {}
|
|
1167
2442
|
});
|
|
2443
|
+
await recordControlPlaneEvent({
|
|
2444
|
+
type: "binding.channel.upserted",
|
|
2445
|
+
severity: "info",
|
|
2446
|
+
subject: `${parsed.provider}:${parsed.accountId}/${parsed.conversationId}`,
|
|
2447
|
+
payload: {
|
|
2448
|
+
provider: parsed.provider,
|
|
2449
|
+
accountId: parsed.accountId,
|
|
2450
|
+
conversationId: parsed.conversationId,
|
|
2451
|
+
repoProvider: parsed.repoProvider,
|
|
2452
|
+
owner: parsed.owner,
|
|
2453
|
+
repo: parsed.repo,
|
|
2454
|
+
hasMetadata: Boolean(parsed.metadata)
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
1168
2457
|
return c.json({ ok: true }, 201);
|
|
1169
2458
|
});
|
|
1170
2459
|
app.get("/v1/channel-bindings/:provider/:accountId/:conversationId", async (c) => {
|
|
@@ -1176,9 +2465,94 @@ function createDispatcherApp(input) {
|
|
|
1176
2465
|
if (!binding) return c.json({ error: "channel_binding_not_found" }, 404);
|
|
1177
2466
|
return c.json({ binding });
|
|
1178
2467
|
});
|
|
2468
|
+
app.get("/v1/channel-bindings/:provider/:accountId/:conversationId/status", async (c) => {
|
|
2469
|
+
const provider = c.req.param("provider");
|
|
2470
|
+
const accountId = c.req.param("accountId");
|
|
2471
|
+
const conversationId = c.req.param("conversationId");
|
|
2472
|
+
const binding = await repo.getChannelBinding({ provider, accountId, conversationId });
|
|
2473
|
+
if (!binding) return c.json({ error: "channel_binding_not_found" }, 404);
|
|
2474
|
+
const active = await repo.findCancelableRunForSourceContainer({
|
|
2475
|
+
source: provider,
|
|
2476
|
+
repoProvider: binding.repoProvider,
|
|
2477
|
+
owner: binding.owner,
|
|
2478
|
+
repo: binding.repo,
|
|
2479
|
+
metadata: sourceContainerMetadata({ provider, accountId, conversationId })
|
|
2480
|
+
});
|
|
2481
|
+
const queuedFollowUps = active ? await repo.listQueuedFollowUpsForActiveRun({ activeRunId: active.run.id }) : [];
|
|
2482
|
+
const runTimeoutMs = active ? latestRunTimeoutMs(await repo.listRunEvents({ runId: active.run.id })) : void 0;
|
|
2483
|
+
return c.json({
|
|
2484
|
+
binding,
|
|
2485
|
+
...active ? { activeRun: active.run, activeEvent: active.event } : {},
|
|
2486
|
+
...runTimeoutMs ? { runTimeoutPolicy: { hardTimeoutMs: runTimeoutMs } } : {},
|
|
2487
|
+
queuedFollowUps
|
|
2488
|
+
});
|
|
2489
|
+
});
|
|
2490
|
+
app.delete("/v1/channel-bindings/:provider/:accountId/:conversationId", async (c) => {
|
|
2491
|
+
const provider = c.req.param("provider");
|
|
2492
|
+
const accountId = c.req.param("accountId");
|
|
2493
|
+
const conversationId = c.req.param("conversationId");
|
|
2494
|
+
const deleted = await repo.deleteChannelBinding({
|
|
2495
|
+
provider,
|
|
2496
|
+
accountId,
|
|
2497
|
+
conversationId
|
|
2498
|
+
});
|
|
2499
|
+
if (!deleted) return c.json({ error: "channel_binding_not_found" }, 404);
|
|
2500
|
+
await recordControlPlaneEvent({
|
|
2501
|
+
type: "binding.channel.deleted",
|
|
2502
|
+
severity: "info",
|
|
2503
|
+
subject: `${provider}:${accountId}/${conversationId}`,
|
|
2504
|
+
payload: {
|
|
2505
|
+
provider,
|
|
2506
|
+
accountId,
|
|
2507
|
+
conversationId
|
|
2508
|
+
}
|
|
2509
|
+
});
|
|
2510
|
+
return c.body(null, 204);
|
|
2511
|
+
});
|
|
2512
|
+
app.post("/v1/channel-bindings/:provider/:accountId/:conversationId/cancel-active-run", async (c) => {
|
|
2513
|
+
const provider = c.req.param("provider");
|
|
2514
|
+
const accountId = c.req.param("accountId");
|
|
2515
|
+
const conversationId = c.req.param("conversationId");
|
|
2516
|
+
const parsed = await parseDispatcherBody(c, CancelRunSchema);
|
|
2517
|
+
const binding = await repo.getChannelBinding({ provider, accountId, conversationId });
|
|
2518
|
+
if (!binding) return c.json({ error: "channel_binding_not_found" }, 404);
|
|
2519
|
+
const active = await repo.findCancelableRunForSourceContainer({
|
|
2520
|
+
source: provider,
|
|
2521
|
+
repoProvider: binding.repoProvider,
|
|
2522
|
+
owner: binding.owner,
|
|
2523
|
+
repo: binding.repo,
|
|
2524
|
+
metadata: sourceContainerMetadata({ provider, accountId, conversationId })
|
|
2525
|
+
});
|
|
2526
|
+
if (!active) return c.json({ error: "active_run_not_found" }, 404);
|
|
2527
|
+
const outcome = await repo.cancelRun({
|
|
2528
|
+
runId: active.run.id,
|
|
2529
|
+
...parsed.reason ? { reason: parsed.reason } : {},
|
|
2530
|
+
...parsed.requestedBy ? { requestedBy: parsed.requestedBy } : {}
|
|
2531
|
+
});
|
|
2532
|
+
if (outcome.outcome === "not_found") return c.json({ error: "run_not_found" }, 404);
|
|
2533
|
+
if (outcome.outcome === "already_terminal") {
|
|
2534
|
+
return c.json({ error: "run_already_terminal", run: outcome.run }, 409);
|
|
2535
|
+
}
|
|
2536
|
+
return c.json({ outcome: "cancelled", run: outcome.run });
|
|
2537
|
+
});
|
|
1179
2538
|
app.post("/v1/slack-channel-bindings", async (c) => {
|
|
1180
|
-
const parsed =
|
|
2539
|
+
const parsed = await parseDispatcherBody(c, CreateSlackChannelBindingSchema);
|
|
1181
2540
|
await repo.createSlackChannelBinding(parsed);
|
|
2541
|
+
await recordControlPlaneEvent({
|
|
2542
|
+
type: "binding.channel.upserted",
|
|
2543
|
+
severity: "info",
|
|
2544
|
+
subject: `slack:${parsed.teamId}/${parsed.channelId}`,
|
|
2545
|
+
payload: {
|
|
2546
|
+
provider: "slack",
|
|
2547
|
+
accountId: parsed.teamId,
|
|
2548
|
+
conversationId: parsed.channelId,
|
|
2549
|
+
repoProvider: parsed.repoProvider ?? "github",
|
|
2550
|
+
owner: parsed.owner,
|
|
2551
|
+
repo: parsed.repo,
|
|
2552
|
+
compatibilityEndpoint: "/v1/slack-channel-bindings",
|
|
2553
|
+
hasMetadata: false
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
1182
2556
|
return c.json({ ok: true }, 201);
|
|
1183
2557
|
});
|
|
1184
2558
|
app.get("/v1/slack-channel-bindings/:teamId/:channelId", async (c) => {
|
|
@@ -1190,43 +2564,69 @@ function createDispatcherApp(input) {
|
|
|
1190
2564
|
return c.json({ binding });
|
|
1191
2565
|
});
|
|
1192
2566
|
app.post("/v1/runs", async (c) => {
|
|
1193
|
-
const parsed =
|
|
2567
|
+
const parsed = await parseDispatcherBody(c, CreateRunSchema);
|
|
1194
2568
|
const admitted = await admission.admitRun({ requestId: parsed.runId, event: parsed.event });
|
|
1195
2569
|
if (admitted.outcome === "needs_human_decision") {
|
|
2570
|
+
const projectTarget = projectTargetRefFromEvent2(parsed.event);
|
|
2571
|
+
await recordControlPlaneEvent({
|
|
2572
|
+
type: "admission.needs_human_decision",
|
|
2573
|
+
severity: ["repo_context_missing", "repo_not_bound"].includes(admitted.decision.reasonCode) ? "warn" : "info",
|
|
2574
|
+
subject: parsed.runId,
|
|
2575
|
+
payload: {
|
|
2576
|
+
runId: parsed.runId,
|
|
2577
|
+
decision: admitted.decision,
|
|
2578
|
+
source: parsed.event.source,
|
|
2579
|
+
sourceEventId: parsed.event.sourceEventId,
|
|
2580
|
+
projectTarget: projectTarget ? `${projectTarget.provider}:${projectTarget.owner}/${projectTarget.repo}` : null
|
|
2581
|
+
}
|
|
2582
|
+
});
|
|
1196
2583
|
return c.json({ decision: admitted.decision }, 202);
|
|
1197
2584
|
}
|
|
1198
2585
|
if (admitted.outcome === "drop_duplicate") {
|
|
1199
|
-
await repo.
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
payload: admitted.decision,
|
|
1203
|
-
visibility: "audit",
|
|
1204
|
-
importance: "normal",
|
|
1205
|
-
message: admitted.decision.reason
|
|
1206
|
-
});
|
|
1207
|
-
await repo.appendRunEvent({
|
|
1208
|
-
runId: admitted.run.id,
|
|
1209
|
-
type: "run.create_idempotent_replay",
|
|
1210
|
-
payload: { requestedRunId: parsed.runId, eventId: parsed.event.id },
|
|
1211
|
-
visibility: "audit",
|
|
1212
|
-
importance: "low"
|
|
1213
|
-
});
|
|
1214
|
-
return c.json({ decision: admitted.decision, run: admitted.run, idempotentReplay: true }, 200);
|
|
2586
|
+
const replay = await repo.createRun({ id: parsed.runId, event: parsed.event });
|
|
2587
|
+
const decision = replay.created ? admitted.decision : replay.replayDecision;
|
|
2588
|
+
return c.json({ decision, run: replay.run, idempotentReplay: true }, 200);
|
|
1215
2589
|
}
|
|
1216
2590
|
if (admitted.outcome === "follow_up_queued") {
|
|
2591
|
+
const event = admitted.followUpRequest.event;
|
|
2592
|
+
const activeRunId = admitted.followUpRequest.activeRunId;
|
|
2593
|
+
if (activeRunId && shouldDeliverRunStatusUpdate(presentation, { provider: event.callback.provider, state: "queued" })) {
|
|
2594
|
+
const queuedPresentation = presentation.runStatusPresentation({
|
|
2595
|
+
runId: activeRunId,
|
|
2596
|
+
state: "queued",
|
|
2597
|
+
message: `Queued follow-up ${admitted.followUpRequest.id} behind the active run.`,
|
|
2598
|
+
nextAction: "Wait for the active run final reply, send another follow-up to queue more context, or request cancellation with /stop.",
|
|
2599
|
+
detailVisibility: "source_thread"
|
|
2600
|
+
});
|
|
2601
|
+
const queued = presentation.render({
|
|
2602
|
+
provider: event.callback.provider,
|
|
2603
|
+
presentation: queuedPresentation
|
|
2604
|
+
});
|
|
2605
|
+
await deliverAndAudit({
|
|
2606
|
+
repo,
|
|
2607
|
+
sink: callbackSink,
|
|
2608
|
+
retry: callbackRetry,
|
|
2609
|
+
message: {
|
|
2610
|
+
runId: activeRunId,
|
|
2611
|
+
kind: "progress",
|
|
2612
|
+
provider: event.callback.provider,
|
|
2613
|
+
uri: event.callback.uri,
|
|
2614
|
+
body: queued.body,
|
|
2615
|
+
...event.target.agentId ? { agentId: event.target.agentId } : {},
|
|
2616
|
+
...event.callback.threadKey ? { threadKey: event.callback.threadKey } : {},
|
|
2617
|
+
...queued.blocks?.length ? { blocks: queued.blocks } : {},
|
|
2618
|
+
...queued.rich ? { rich: queued.rich } : {},
|
|
2619
|
+
statusMessageKey: `${activeRunId}:status`
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
1217
2623
|
return c.json({ decision: admitted.decision, followUpRequest: admitted.followUpRequest }, 202);
|
|
1218
2624
|
}
|
|
1219
2625
|
const createdRun = await repo.createRun({ id: parsed.runId, event: parsed.event });
|
|
1220
2626
|
if (!createdRun.created) {
|
|
1221
2627
|
return c.json(
|
|
1222
2628
|
{
|
|
1223
|
-
decision:
|
|
1224
|
-
...admitted.decision,
|
|
1225
|
-
action: "drop_duplicate",
|
|
1226
|
-
reason: "Source event already created a run.",
|
|
1227
|
-
reasonCode: "duplicate_source_event",
|
|
1228
|
-
activeRunId: createdRun.run.id
|
|
1229
|
-
},
|
|
2629
|
+
decision: createdRun.replayDecision,
|
|
1230
2630
|
run: createdRun.run,
|
|
1231
2631
|
idempotentReplay: true
|
|
1232
2632
|
},
|
|
@@ -1234,7 +2634,25 @@ function createDispatcherApp(input) {
|
|
|
1234
2634
|
);
|
|
1235
2635
|
}
|
|
1236
2636
|
const { run } = createdRun;
|
|
1237
|
-
|
|
2637
|
+
const sourceReceiptDelivery = await deliverSourceReceiptBestEffort({
|
|
2638
|
+
repo,
|
|
2639
|
+
sink: sourceReceiptSink,
|
|
2640
|
+
receipt: {
|
|
2641
|
+
runId: run.id,
|
|
2642
|
+
provider: parsed.event.callback.provider,
|
|
2643
|
+
state: "received",
|
|
2644
|
+
event: parsed.event,
|
|
2645
|
+
...parsed.event.target.agentId ? { agentId: parsed.event.target.agentId } : {}
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
const shouldDeliverAcknowledgement = presentation.shouldDeliverAcknowledgement(parsed.event.callback.provider) || shouldDeliverSourceReceipt(parsed.event.callback.provider) && !sourceReceiptDelivery.delivered;
|
|
2649
|
+
if (shouldDeliverAcknowledgement) {
|
|
2650
|
+
const acknowledgementPresentation = presentation.acknowledgementPresentation({ runId: run.id });
|
|
2651
|
+
const acknowledgement = presentation.render({
|
|
2652
|
+
provider: parsed.event.callback.provider,
|
|
2653
|
+
presentation: acknowledgementPresentation
|
|
2654
|
+
});
|
|
2655
|
+
const statusMessageKey = larkLifecycleStatusMessageKey({ provider: parsed.event.callback.provider, runId: run.id });
|
|
1238
2656
|
await deliverAndAudit({
|
|
1239
2657
|
repo,
|
|
1240
2658
|
sink: callbackSink,
|
|
@@ -1244,16 +2662,19 @@ function createDispatcherApp(input) {
|
|
|
1244
2662
|
kind: "acknowledgement",
|
|
1245
2663
|
provider: parsed.event.callback.provider,
|
|
1246
2664
|
uri: parsed.event.callback.uri,
|
|
1247
|
-
body:
|
|
2665
|
+
body: acknowledgement.body,
|
|
1248
2666
|
...parsed.event.target.agentId ? { agentId: parsed.event.target.agentId } : {},
|
|
1249
|
-
...parsed.event.callback.threadKey ? { threadKey: parsed.event.callback.threadKey } : {}
|
|
2667
|
+
...parsed.event.callback.threadKey ? { threadKey: parsed.event.callback.threadKey } : {},
|
|
2668
|
+
...statusMessageKey ? { statusMessageKey } : {},
|
|
2669
|
+
...acknowledgement.blocks?.length ? { blocks: acknowledgement.blocks } : {},
|
|
2670
|
+
...acknowledgement.rich ? { rich: acknowledgement.rich } : {}
|
|
1250
2671
|
}
|
|
1251
2672
|
});
|
|
1252
2673
|
}
|
|
1253
2674
|
return c.json({ decision: admitted.decision, run }, 201);
|
|
1254
2675
|
});
|
|
1255
2676
|
app.post("/v1/thread-actions", async (c) => {
|
|
1256
|
-
const parsed =
|
|
2677
|
+
const parsed = await parseDispatcherBody(c, ThreadActionInputSchema);
|
|
1257
2678
|
const command = parseThreadActionCommand(parsed.rawText);
|
|
1258
2679
|
if (!command) {
|
|
1259
2680
|
return c.json({ outcome: "ignored", reason: "not_action_command" }, 202);
|
|
@@ -1305,22 +2726,24 @@ function createDispatcherApp(input) {
|
|
|
1305
2726
|
if (existingPlan) {
|
|
1306
2727
|
const existingDecision2 = await repo.getApprovalDecision({ id: existingPlan.approvalDecisionId });
|
|
1307
2728
|
if (selectedIntentsAlreadyApplied({ plan: existingPlan, selectedIntentIds: resolved.resolved.selectedIntentIds })) {
|
|
2729
|
+
await deliverAndAudit({
|
|
2730
|
+
repo,
|
|
2731
|
+
sink: callbackSink,
|
|
2732
|
+
retry: callbackRetry,
|
|
2733
|
+
message: {
|
|
2734
|
+
runId: resolved.resolved.proposal.runId,
|
|
2735
|
+
kind: "final",
|
|
2736
|
+
provider: parsed.callback.provider,
|
|
2737
|
+
uri: parsed.callback.uri,
|
|
2738
|
+
body: renderAlreadyAppliedThreadActionBody({ selectionText }),
|
|
2739
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
2740
|
+
}
|
|
2741
|
+
});
|
|
1308
2742
|
return c.json({ outcome: "already_applied", decision: existingDecision2, plan: existingPlan }, 200);
|
|
1309
2743
|
}
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
command,
|
|
1314
|
-
resolved: resolved.resolved,
|
|
1315
|
-
runId: stableChildRunId({
|
|
1316
|
-
command,
|
|
1317
|
-
resolved: resolved.resolved,
|
|
1318
|
-
sourceApplyPlanId: existingPlan.id,
|
|
1319
|
-
fallbackReason
|
|
1320
|
-
}),
|
|
1321
|
-
approvalDecisionId: existingPlan.approvalDecisionId,
|
|
1322
|
-
sourceApplyPlanId: existingPlan.id,
|
|
1323
|
-
fallbackReason
|
|
2744
|
+
const isStale = selectedIntentsHaveStaleOutcome({
|
|
2745
|
+
plan: existingPlan,
|
|
2746
|
+
selectedIntentIds: resolved.resolved.selectedIntentIds
|
|
1324
2747
|
});
|
|
1325
2748
|
await deliverAndAudit({
|
|
1326
2749
|
repo,
|
|
@@ -1331,18 +2754,14 @@ function createDispatcherApp(input) {
|
|
|
1331
2754
|
kind: "final",
|
|
1332
2755
|
provider: parsed.callback.provider,
|
|
1333
2756
|
uri: parsed.callback.uri,
|
|
1334
|
-
body:
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
approvalDecisionId: existingPlan.approvalDecisionId,
|
|
1339
|
-
sourceApplyPlanId: existingPlan.id,
|
|
1340
|
-
fallbackReason
|
|
1341
|
-
}),
|
|
2757
|
+
body: isStale ? renderStaleThreadActionBody({
|
|
2758
|
+
selectionText,
|
|
2759
|
+
continueIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1
|
|
2760
|
+
}) : renderAlreadyPlannedThreadActionBody({ selectionText }),
|
|
1342
2761
|
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1343
2762
|
}
|
|
1344
2763
|
});
|
|
1345
|
-
return c.json({ outcome: "already_planned", decision: existingDecision2, plan: existingPlan
|
|
2764
|
+
return c.json({ outcome: isStale ? "stale" : "already_planned", decision: existingDecision2, plan: existingPlan }, 200);
|
|
1346
2765
|
}
|
|
1347
2766
|
}
|
|
1348
2767
|
const providedDecision = parsed.id ? await repo.getApprovalDecision({ id: parsed.id }) : null;
|
|
@@ -1383,7 +2802,10 @@ function createDispatcherApp(input) {
|
|
|
1383
2802
|
if (existingDecision) {
|
|
1384
2803
|
return c.json({ outcome: "already_rejected", decision }, 200);
|
|
1385
2804
|
}
|
|
1386
|
-
const body2 =
|
|
2805
|
+
const body2 = renderThreadActionRecordedBody({
|
|
2806
|
+
verb: "reject",
|
|
2807
|
+
selectionText
|
|
2808
|
+
});
|
|
1387
2809
|
await deliverAndAudit({
|
|
1388
2810
|
repo,
|
|
1389
2811
|
sink: callbackSink,
|
|
@@ -1403,9 +2825,18 @@ function createDispatcherApp(input) {
|
|
|
1403
2825
|
if (existingDecision) {
|
|
1404
2826
|
return c.json({ outcome: "already_approved", decision }, 200);
|
|
1405
2827
|
}
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
2828
|
+
const directApply = await selectedDirectApplyStatus({
|
|
2829
|
+
event: resolved.resolved.proposal.event,
|
|
2830
|
+
callbackProvider: parsed.callback.provider,
|
|
2831
|
+
candidates: resolved.resolved.selectedCandidates,
|
|
2832
|
+
...input.githubApply ? { githubApply: input.githubApply } : {}
|
|
2833
|
+
});
|
|
2834
|
+
const body2 = renderThreadActionRecordedBody({
|
|
2835
|
+
verb: "approve",
|
|
2836
|
+
selectionText,
|
|
2837
|
+
applyIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1,
|
|
2838
|
+
directApply
|
|
2839
|
+
});
|
|
1409
2840
|
await deliverAndAudit({
|
|
1410
2841
|
repo,
|
|
1411
2842
|
sink: callbackSink,
|
|
@@ -1430,9 +2861,11 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1430
2861
|
approvalDecisionId: decision.id
|
|
1431
2862
|
});
|
|
1432
2863
|
const body2 = renderChildRunCreatedBody({
|
|
1433
|
-
lead:
|
|
2864
|
+
lead: "Continuing in OpenTag from this approved action.",
|
|
1434
2865
|
resolved: resolved.resolved,
|
|
1435
2866
|
childRun: childRun2,
|
|
2867
|
+
provider: parsed.callback.provider,
|
|
2868
|
+
selectionText,
|
|
1436
2869
|
approvalDecisionId: decision.id
|
|
1437
2870
|
});
|
|
1438
2871
|
await deliverAndAudit({
|
|
@@ -1462,22 +2895,24 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1462
2895
|
}
|
|
1463
2896
|
if (!planResult.created) {
|
|
1464
2897
|
if (selectedIntentsAlreadyApplied({ plan: planResult.plan, selectedIntentIds: resolved.resolved.selectedIntentIds })) {
|
|
2898
|
+
await deliverAndAudit({
|
|
2899
|
+
repo,
|
|
2900
|
+
sink: callbackSink,
|
|
2901
|
+
retry: callbackRetry,
|
|
2902
|
+
message: {
|
|
2903
|
+
runId: resolved.resolved.proposal.runId,
|
|
2904
|
+
kind: "final",
|
|
2905
|
+
provider: parsed.callback.provider,
|
|
2906
|
+
uri: parsed.callback.uri,
|
|
2907
|
+
body: renderAlreadyAppliedThreadActionBody({ selectionText }),
|
|
2908
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
1465
2911
|
return c.json({ outcome: "already_applied", decision, plan: planResult.plan }, 200);
|
|
1466
2912
|
}
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
command,
|
|
1471
|
-
resolved: resolved.resolved,
|
|
1472
|
-
runId: stableChildRunId({
|
|
1473
|
-
command,
|
|
1474
|
-
resolved: resolved.resolved,
|
|
1475
|
-
sourceApplyPlanId: planResult.plan.id,
|
|
1476
|
-
fallbackReason
|
|
1477
|
-
}),
|
|
1478
|
-
approvalDecisionId: decision.id,
|
|
1479
|
-
sourceApplyPlanId: planResult.plan.id,
|
|
1480
|
-
fallbackReason
|
|
2913
|
+
const isStale = selectedIntentsHaveStaleOutcome({
|
|
2914
|
+
plan: planResult.plan,
|
|
2915
|
+
selectedIntentIds: resolved.resolved.selectedIntentIds
|
|
1481
2916
|
});
|
|
1482
2917
|
await deliverAndAudit({
|
|
1483
2918
|
repo,
|
|
@@ -1488,18 +2923,14 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1488
2923
|
kind: "final",
|
|
1489
2924
|
provider: parsed.callback.provider,
|
|
1490
2925
|
uri: parsed.callback.uri,
|
|
1491
|
-
body:
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
approvalDecisionId: decision.id,
|
|
1496
|
-
sourceApplyPlanId: planResult.plan.id,
|
|
1497
|
-
fallbackReason
|
|
1498
|
-
}),
|
|
2926
|
+
body: isStale ? renderStaleThreadActionBody({
|
|
2927
|
+
selectionText,
|
|
2928
|
+
continueIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1
|
|
2929
|
+
}) : renderAlreadyPlannedThreadActionBody({ selectionText }),
|
|
1499
2930
|
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1500
2931
|
}
|
|
1501
2932
|
});
|
|
1502
|
-
return c.json({ outcome: "already_planned", decision, plan: planResult.plan
|
|
2933
|
+
return c.json({ outcome: isStale ? "stale" : "already_planned", decision, plan: planResult.plan }, 200);
|
|
1503
2934
|
}
|
|
1504
2935
|
const plan = planResult.plan;
|
|
1505
2936
|
const execution = await executeGitHubApplyPlan({
|
|
@@ -1510,12 +2941,11 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1510
2941
|
});
|
|
1511
2942
|
if (execution.executed) {
|
|
1512
2943
|
const outcomes = execution.plan.outcomes ?? [];
|
|
1513
|
-
const body2 =
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
].join("\n");
|
|
2944
|
+
const body2 = renderAppliedThreadActionBody({
|
|
2945
|
+
selectionText,
|
|
2946
|
+
selectedIntentIds: resolved.resolved.selectedIntentIds,
|
|
2947
|
+
outcomes
|
|
2948
|
+
});
|
|
1519
2949
|
await deliverAndAudit({
|
|
1520
2950
|
repo,
|
|
1521
2951
|
sink: callbackSink,
|
|
@@ -1531,6 +2961,25 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1531
2961
|
});
|
|
1532
2962
|
return c.json({ outcome: "applied", decision, plan: execution.plan }, 201);
|
|
1533
2963
|
}
|
|
2964
|
+
if (selectedIntentsHaveStaleOutcome({ plan: execution.plan, selectedIntentIds: resolved.resolved.selectedIntentIds })) {
|
|
2965
|
+
await deliverAndAudit({
|
|
2966
|
+
repo,
|
|
2967
|
+
sink: callbackSink,
|
|
2968
|
+
retry: callbackRetry,
|
|
2969
|
+
message: {
|
|
2970
|
+
runId: resolved.resolved.proposal.runId,
|
|
2971
|
+
kind: "final",
|
|
2972
|
+
provider: parsed.callback.provider,
|
|
2973
|
+
uri: parsed.callback.uri,
|
|
2974
|
+
body: renderStaleThreadActionBody({
|
|
2975
|
+
selectionText,
|
|
2976
|
+
continueIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1
|
|
2977
|
+
}),
|
|
2978
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
return c.json({ outcome: "stale", decision, plan: execution.plan }, 200);
|
|
2982
|
+
}
|
|
1534
2983
|
const childRun = await createChildRunForThreadAction({
|
|
1535
2984
|
repo,
|
|
1536
2985
|
command,
|
|
@@ -1546,9 +2995,11 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1546
2995
|
fallbackReason: execution.fallbackReason ?? "OpenTag cannot directly apply this intent yet."
|
|
1547
2996
|
});
|
|
1548
2997
|
const body = renderChildRunCreatedBody({
|
|
1549
|
-
lead:
|
|
2998
|
+
lead: "Needs setup before OpenTag can apply this action directly.",
|
|
1550
2999
|
resolved: resolved.resolved,
|
|
1551
3000
|
childRun,
|
|
3001
|
+
provider: parsed.callback.provider,
|
|
3002
|
+
selectionText,
|
|
1552
3003
|
approvalDecisionId: decision.id,
|
|
1553
3004
|
sourceApplyPlanId: execution.plan.id,
|
|
1554
3005
|
fallbackReason: execution.fallbackReason ?? "The adapter could not execute the selected intent."
|
|
@@ -1574,10 +3025,10 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1574
3025
|
return c.json({ followUpRequest });
|
|
1575
3026
|
});
|
|
1576
3027
|
app.post("/v1/follow-up-requests/:id/create-run", async (c) => {
|
|
1577
|
-
const parsed =
|
|
3028
|
+
const parsed = await parseDispatcherBody(c, PromoteFollowUpRequestSchema);
|
|
1578
3029
|
let promoted;
|
|
1579
3030
|
try {
|
|
1580
|
-
promoted = await
|
|
3031
|
+
promoted = await promoteFollowUpRequest({
|
|
1581
3032
|
followUpRequestId: c.req.param("id"),
|
|
1582
3033
|
runId: parsed.runId
|
|
1583
3034
|
});
|
|
@@ -1592,23 +3043,6 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1592
3043
|
throw error;
|
|
1593
3044
|
}
|
|
1594
3045
|
const followUpRequest = promoted.followUpRequest;
|
|
1595
|
-
const event = followUpRequest.event;
|
|
1596
|
-
if (presentation.shouldDeliverAcknowledgement(event.callback.provider)) {
|
|
1597
|
-
await deliverAndAudit({
|
|
1598
|
-
repo,
|
|
1599
|
-
sink: callbackSink,
|
|
1600
|
-
retry: callbackRetry,
|
|
1601
|
-
message: {
|
|
1602
|
-
runId: promoted.run.id,
|
|
1603
|
-
kind: "acknowledgement",
|
|
1604
|
-
provider: event.callback.provider,
|
|
1605
|
-
uri: event.callback.uri,
|
|
1606
|
-
body: presentation.acknowledgement({ provider: event.callback.provider, runId: promoted.run.id }),
|
|
1607
|
-
...event.target.agentId ? { agentId: event.target.agentId } : {},
|
|
1608
|
-
...event.callback.threadKey ? { threadKey: event.callback.threadKey } : {}
|
|
1609
|
-
}
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
3046
|
return c.json({ followUpRequest, run: promoted.run }, 201);
|
|
1613
3047
|
});
|
|
1614
3048
|
app.post("/v1/runners/:runnerId/claim", async (c) => {
|
|
@@ -1628,13 +3062,67 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1628
3062
|
}, 410);
|
|
1629
3063
|
});
|
|
1630
3064
|
app.post("/v1/runners/:runnerId/runs/:runId/running", async (c) => {
|
|
1631
|
-
const
|
|
1632
|
-
const
|
|
1633
|
-
|
|
3065
|
+
const runId = c.req.param("runId");
|
|
3066
|
+
const body = await parseDispatcherBody(c, MarkRunningSchema);
|
|
3067
|
+
const headerIdempotencyKey = c.req.header("idempotency-key")?.trim();
|
|
3068
|
+
const idempotencyKey = body.idempotencyKey?.trim() || headerIdempotencyKey;
|
|
3069
|
+
const runningOutcome = await repo.markRunning({
|
|
3070
|
+
runId,
|
|
1634
3071
|
runnerId: c.req.param("runnerId"),
|
|
1635
|
-
executor: body.executor
|
|
3072
|
+
executor: body.executor,
|
|
3073
|
+
...body.runTimeoutMs ? { runTimeoutMs: body.runTimeoutMs } : {},
|
|
3074
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
1636
3075
|
});
|
|
1637
|
-
if (
|
|
3076
|
+
if (runningOutcome === "not_found") return c.json({ error: "run_not_claimed_by_runner" }, 404);
|
|
3077
|
+
if (runningOutcome === "duplicate") return c.json({ ok: true, replayed: true });
|
|
3078
|
+
const stored = await repo.getRun({ runId });
|
|
3079
|
+
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
3080
|
+
const provider = stored.event.callback.provider;
|
|
3081
|
+
if (shouldDeliverSourceReceipt(provider)) {
|
|
3082
|
+
await deliverSourceReceiptBestEffort({
|
|
3083
|
+
repo,
|
|
3084
|
+
sink: sourceReceiptSink,
|
|
3085
|
+
receipt: {
|
|
3086
|
+
runId,
|
|
3087
|
+
provider,
|
|
3088
|
+
state: "running",
|
|
3089
|
+
event: stored.event,
|
|
3090
|
+
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {}
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
if (shouldDeliverRunStatusUpdate(presentation, { provider, state: "running" })) {
|
|
3095
|
+
const runningPresentation = presentation.runStatusPresentation({
|
|
3096
|
+
runId,
|
|
3097
|
+
state: "running",
|
|
3098
|
+
message: `Running with ${body.executor}.`,
|
|
3099
|
+
nextAction: "Wait for the final reply, send a follow-up to queue more context, or request cancellation with /stop.",
|
|
3100
|
+
detailVisibility: "source_thread"
|
|
3101
|
+
});
|
|
3102
|
+
const running = presentation.render({
|
|
3103
|
+
provider,
|
|
3104
|
+
presentation: runningPresentation
|
|
3105
|
+
});
|
|
3106
|
+
await deliverAndAudit({
|
|
3107
|
+
repo,
|
|
3108
|
+
sink: callbackSink,
|
|
3109
|
+
retry: callbackRetry,
|
|
3110
|
+
message: {
|
|
3111
|
+
runId,
|
|
3112
|
+
kind: "progress",
|
|
3113
|
+
provider,
|
|
3114
|
+
uri: stored.event.callback.uri,
|
|
3115
|
+
body: running.body,
|
|
3116
|
+
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
3117
|
+
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
3118
|
+
...running.blocks?.length ? { blocks: running.blocks } : {},
|
|
3119
|
+
...running.rich ? { rich: running.rich } : {},
|
|
3120
|
+
statusMessageKey: `${runId}:status`
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
3123
|
+
} else if (presentation.shouldDeliverStatusUpdate(provider)) {
|
|
3124
|
+
await appendSuppressedRunStatusCallback({ runId, provider, state: "running" });
|
|
3125
|
+
}
|
|
1638
3126
|
return c.json({ ok: true });
|
|
1639
3127
|
});
|
|
1640
3128
|
app.post("/v1/runs/:runId/progress", async () => {
|
|
@@ -1645,20 +3133,31 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1645
3133
|
});
|
|
1646
3134
|
app.post("/v1/runners/:runnerId/runs/:runId/progress", async (c) => {
|
|
1647
3135
|
const runId = c.req.param("runId");
|
|
1648
|
-
const body =
|
|
1649
|
-
const
|
|
3136
|
+
const body = await parseDispatcherBody(c, ProgressSchema);
|
|
3137
|
+
const headerIdempotencyKey = c.req.header("idempotency-key")?.trim();
|
|
3138
|
+
const idempotencyKey = body.idempotencyKey?.trim() || headerIdempotencyKey;
|
|
3139
|
+
const progressOutcome = await repo.recordProgress({
|
|
1650
3140
|
runId,
|
|
1651
3141
|
runnerId: c.req.param("runnerId"),
|
|
1652
3142
|
message: body.message,
|
|
1653
3143
|
...body.type ? { type: body.type } : {},
|
|
1654
3144
|
...body.at ? { at: body.at } : {},
|
|
1655
3145
|
...body.visibility ? { visibility: body.visibility } : {},
|
|
1656
|
-
...body.importance ? { importance: body.importance } : {}
|
|
3146
|
+
...body.importance ? { importance: body.importance } : {},
|
|
3147
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
1657
3148
|
});
|
|
1658
|
-
if (
|
|
3149
|
+
if (progressOutcome === "not_found") return c.json({ error: "run_not_claimed_by_runner" }, 404);
|
|
3150
|
+
if (progressOutcome === "duplicate") return c.json({ ok: true, replayed: true });
|
|
1659
3151
|
const stored = await repo.getRun({ runId });
|
|
1660
3152
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1661
|
-
|
|
3153
|
+
const progressVisibility = body.visibility ?? "audit";
|
|
3154
|
+
const shouldDeliverProgress = presentation.shouldDeliverProgress(stored.event.callback.provider);
|
|
3155
|
+
if (progressVisibility === "human" && shouldDeliverProgress) {
|
|
3156
|
+
const progressPresentation = presentation.progressPresentation({ runId, message: body.message });
|
|
3157
|
+
const progress = presentation.render({
|
|
3158
|
+
provider: stored.event.callback.provider,
|
|
3159
|
+
presentation: progressPresentation
|
|
3160
|
+
});
|
|
1662
3161
|
await deliverAndAudit({
|
|
1663
3162
|
repo,
|
|
1664
3163
|
sink: callbackSink,
|
|
@@ -1668,12 +3167,29 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1668
3167
|
kind: "progress",
|
|
1669
3168
|
provider: stored.event.callback.provider,
|
|
1670
3169
|
uri: stored.event.callback.uri,
|
|
1671
|
-
body:
|
|
3170
|
+
body: progress.body,
|
|
1672
3171
|
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
1673
3172
|
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
3173
|
+
...progress.blocks?.length ? { blocks: progress.blocks } : {},
|
|
3174
|
+
...progress.rich ? { rich: progress.rich } : {},
|
|
1674
3175
|
statusMessageKey: `${runId}:status`
|
|
1675
3176
|
}
|
|
1676
3177
|
});
|
|
3178
|
+
} else if (progressVisibility === "human") {
|
|
3179
|
+
const capability = platformCapabilityForProvider2(stored.event.callback.provider);
|
|
3180
|
+
await repo.appendRunEvent({
|
|
3181
|
+
runId,
|
|
3182
|
+
type: "callback.progress.suppressed",
|
|
3183
|
+
payload: {
|
|
3184
|
+
provider: stored.event.callback.provider,
|
|
3185
|
+
reason: "platform_liveness_strategy",
|
|
3186
|
+
requestedVisibility: progressVisibility,
|
|
3187
|
+
livenessStrategy: capability?.livenessStrategy ?? "unknown"
|
|
3188
|
+
},
|
|
3189
|
+
visibility: "audit",
|
|
3190
|
+
importance: "low",
|
|
3191
|
+
message: "Progress callback suppressed by platform liveness strategy; use status or audit for details."
|
|
3192
|
+
});
|
|
1677
3193
|
}
|
|
1678
3194
|
return c.json({ ok: true });
|
|
1679
3195
|
});
|
|
@@ -1685,12 +3201,64 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1685
3201
|
});
|
|
1686
3202
|
app.post("/v1/runners/:runnerId/runs/:runId/complete", async (c) => {
|
|
1687
3203
|
const runId = c.req.param("runId");
|
|
1688
|
-
const parsed =
|
|
1689
|
-
const
|
|
1690
|
-
|
|
3204
|
+
const parsed = await parseDispatcherBody(c, CompleteRunSchema);
|
|
3205
|
+
const headerIdempotencyKey = c.req.header("idempotency-key")?.trim();
|
|
3206
|
+
const idempotencyKey = parsed.idempotencyKey?.trim() || headerIdempotencyKey;
|
|
3207
|
+
const outcome = await repo.completeRun({
|
|
3208
|
+
runId,
|
|
3209
|
+
runnerId: c.req.param("runnerId"),
|
|
3210
|
+
result: parsed.result,
|
|
3211
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
3212
|
+
});
|
|
3213
|
+
if (outcome === "not_found") return c.json({ error: "run_not_claimed_by_runner" }, 404);
|
|
3214
|
+
if (outcome === "duplicate") return c.json({ ok: true, replayed: true });
|
|
1691
3215
|
const stored = await repo.getRun({ runId });
|
|
1692
3216
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1693
|
-
const
|
|
3217
|
+
const receiptContext = await actionReceiptContextForFinal({
|
|
3218
|
+
event: stored.event,
|
|
3219
|
+
result: parsed.result,
|
|
3220
|
+
...input.githubApply ? { githubApply: input.githubApply } : {}
|
|
3221
|
+
});
|
|
3222
|
+
if (parsed.result.conclusion === "needs_human" && shouldDeliverRunStatusUpdate(presentation, { provider: stored.event.callback.provider, state: "waiting_for_approval" })) {
|
|
3223
|
+
const waitingPresentation = presentation.runStatusPresentation({
|
|
3224
|
+
runId,
|
|
3225
|
+
state: "waiting_for_approval",
|
|
3226
|
+
message: "Waiting for approval.",
|
|
3227
|
+
nextAction: "Review the source-thread action receipt, then approve, reject, apply, or continue from the source thread.",
|
|
3228
|
+
detailVisibility: "source_thread"
|
|
3229
|
+
});
|
|
3230
|
+
const waiting = presentation.render({
|
|
3231
|
+
provider: stored.event.callback.provider,
|
|
3232
|
+
presentation: waitingPresentation
|
|
3233
|
+
});
|
|
3234
|
+
await deliverAndAudit({
|
|
3235
|
+
repo,
|
|
3236
|
+
sink: callbackSink,
|
|
3237
|
+
retry: callbackRetry,
|
|
3238
|
+
message: {
|
|
3239
|
+
runId,
|
|
3240
|
+
kind: "progress",
|
|
3241
|
+
provider: stored.event.callback.provider,
|
|
3242
|
+
uri: stored.event.callback.uri,
|
|
3243
|
+
body: waiting.body,
|
|
3244
|
+
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
3245
|
+
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
3246
|
+
...waiting.blocks?.length ? { blocks: waiting.blocks } : {},
|
|
3247
|
+
...waiting.rich ? { rich: waiting.rich } : {},
|
|
3248
|
+
statusMessageKey: `${runId}:status`
|
|
3249
|
+
}
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
const finalPresentation = presentation.finalPresentation({
|
|
3253
|
+
result: parsed.result,
|
|
3254
|
+
runId,
|
|
3255
|
+
receiptContext
|
|
3256
|
+
});
|
|
3257
|
+
const finalCallback = presentation.render({
|
|
3258
|
+
provider: stored.event.callback.provider,
|
|
3259
|
+
presentation: finalPresentation
|
|
3260
|
+
});
|
|
3261
|
+
const statusMessageKey = larkLifecycleStatusMessageKey({ provider: stored.event.callback.provider, runId });
|
|
1694
3262
|
await deliverAndAudit({
|
|
1695
3263
|
repo,
|
|
1696
3264
|
sink: callbackSink,
|
|
@@ -1700,13 +3268,25 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1700
3268
|
kind: "final",
|
|
1701
3269
|
provider: stored.event.callback.provider,
|
|
1702
3270
|
uri: stored.event.callback.uri,
|
|
1703
|
-
body:
|
|
3271
|
+
body: finalCallback.body,
|
|
1704
3272
|
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
1705
3273
|
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
1706
|
-
...
|
|
3274
|
+
...statusMessageKey ? { statusMessageKey } : {},
|
|
3275
|
+
...finalCallback.blocks?.length ? { blocks: finalCallback.blocks } : {},
|
|
3276
|
+
...finalCallback.rich ? { rich: finalCallback.rich } : {}
|
|
1707
3277
|
}
|
|
1708
3278
|
});
|
|
1709
|
-
|
|
3279
|
+
const shouldPromoteFollowUp = parsed.result.conclusion !== "needs_human" && parsed.result.conclusion !== "cancelled";
|
|
3280
|
+
const promotedFollowUp = shouldPromoteFollowUp ? await promoteNextFollowUpAfterTerminalRun({ activeRunId: runId }) : null;
|
|
3281
|
+
return c.json({
|
|
3282
|
+
ok: true,
|
|
3283
|
+
...promotedFollowUp ? {
|
|
3284
|
+
promotedFollowUp: {
|
|
3285
|
+
followUpRequest: promotedFollowUp.followUpRequest,
|
|
3286
|
+
run: promotedFollowUp.run
|
|
3287
|
+
}
|
|
3288
|
+
} : {}
|
|
3289
|
+
});
|
|
1710
3290
|
});
|
|
1711
3291
|
app.get("/v1/proposals/:proposalId", async (c) => {
|
|
1712
3292
|
const proposal = await repo.getSuggestedChanges({ proposalId: c.req.param("proposalId") });
|
|
@@ -1725,9 +3305,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1725
3305
|
});
|
|
1726
3306
|
app.post("/v1/proposals/:proposalId/approvals", async (c) => {
|
|
1727
3307
|
const proposalId = c.req.param("proposalId");
|
|
1728
|
-
const
|
|
1729
|
-
if (!parsedBody.success) return c.json({ error: "invalid_approval_decision" }, 400);
|
|
1730
|
-
const body = parsedBody.data;
|
|
3308
|
+
const body = await parseDispatcherBody(c, ApprovalDecisionInputSchema, { invalidBodyError: "invalid_approval_decision" });
|
|
1731
3309
|
const decision = await repo.recordApprovalDecision({
|
|
1732
3310
|
id: body.id ?? `approval_${proposalId}_${Date.now()}`,
|
|
1733
3311
|
proposalId,
|
|
@@ -1749,7 +3327,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1749
3327
|
});
|
|
1750
3328
|
app.post("/v1/proposals/:proposalId/apply-plans", async (c) => {
|
|
1751
3329
|
const proposalId = c.req.param("proposalId");
|
|
1752
|
-
const body =
|
|
3330
|
+
const body = await parseDispatcherBody(c, ApplyPlanInputSchema);
|
|
1753
3331
|
let executableTarget;
|
|
1754
3332
|
if (body.execute) {
|
|
1755
3333
|
if (body.adapter !== "github") {
|
|
@@ -1843,7 +3421,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1843
3421
|
});
|
|
1844
3422
|
app.post("/v1/runs/:runId/child-runs", async (c) => {
|
|
1845
3423
|
const parentRunId = c.req.param("runId");
|
|
1846
|
-
const body =
|
|
3424
|
+
const body = await parseDispatcherBody(c, ChildRunInputSchema);
|
|
1847
3425
|
const parent = await repo.getRun({ runId: parentRunId });
|
|
1848
3426
|
if (!parent) return c.json({ error: "parent_run_not_found" }, 404);
|
|
1849
3427
|
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1869,6 +3447,19 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1869
3447
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1870
3448
|
return c.json(stored);
|
|
1871
3449
|
});
|
|
3450
|
+
app.post("/v1/runs/:runId/cancel", async (c) => {
|
|
3451
|
+
const parsed = await parseDispatcherBody(c, CancelRunSchema);
|
|
3452
|
+
const outcome = await repo.cancelRun({
|
|
3453
|
+
runId: c.req.param("runId"),
|
|
3454
|
+
...parsed.reason ? { reason: parsed.reason } : {},
|
|
3455
|
+
...parsed.requestedBy ? { requestedBy: parsed.requestedBy } : {}
|
|
3456
|
+
});
|
|
3457
|
+
if (outcome.outcome === "not_found") return c.json({ error: "run_not_found" }, 404);
|
|
3458
|
+
if (outcome.outcome === "already_terminal") {
|
|
3459
|
+
return c.json({ error: "run_already_terminal", run: outcome.run }, 409);
|
|
3460
|
+
}
|
|
3461
|
+
return c.json({ outcome: "cancelled", run: outcome.run });
|
|
3462
|
+
});
|
|
1872
3463
|
app.get("/v1/runs/:runId/metrics", async (c) => {
|
|
1873
3464
|
const runId = c.req.param("runId");
|
|
1874
3465
|
const stored = await repo.getRun({ runId });
|
|
@@ -1880,6 +3471,13 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1880
3471
|
const events = await repo.listRunEvents({ runId: c.req.param("runId") });
|
|
1881
3472
|
return c.json({ events });
|
|
1882
3473
|
});
|
|
3474
|
+
app.onError((err, c) => {
|
|
3475
|
+
if (err instanceof HTTPException) {
|
|
3476
|
+
return err.getResponse();
|
|
3477
|
+
}
|
|
3478
|
+
console.error("dispatcher unhandled error", err);
|
|
3479
|
+
return c.json({ error: "internal_server_error" }, 500);
|
|
3480
|
+
});
|
|
1883
3481
|
return app;
|
|
1884
3482
|
}
|
|
1885
3483
|
export {
|
|
@@ -1889,6 +3487,7 @@ export {
|
|
|
1889
3487
|
createGitHubCallbackSink,
|
|
1890
3488
|
createLarkCallbackSink,
|
|
1891
3489
|
createSlackCallbackSink,
|
|
3490
|
+
createSlackSourceReceiptSink,
|
|
1892
3491
|
createTelegramCallbackSink,
|
|
1893
3492
|
processPendingCallbacks
|
|
1894
3493
|
};
|