@opentag/dispatcher 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/callbacks.d.ts.map +1 -1
- package/dist/index.js +1600 -250
- 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 +28 -2
- package/dist/server.d.ts.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// src/callbacks.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createLarkReplyClient,
|
|
4
|
+
patchLarkMessageCard,
|
|
5
|
+
parseLarkThreadKey,
|
|
6
|
+
replyLarkMessage,
|
|
7
|
+
updateLarkTextMessage
|
|
8
|
+
} from "@opentag/lark";
|
|
3
9
|
import {
|
|
4
10
|
createSlackPostMessagePayload,
|
|
5
11
|
createSlackReactionPayload,
|
|
@@ -208,8 +214,27 @@ function createLarkCallbackSink(input) {
|
|
|
208
214
|
if (!message.threadKey) {
|
|
209
215
|
throw new Error("Lark callback message is missing threadKey.");
|
|
210
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
|
+
}
|
|
211
231
|
const { messageId } = parseLarkThreadKey(message.threadKey);
|
|
212
|
-
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;
|
|
213
238
|
}
|
|
214
239
|
};
|
|
215
240
|
}
|
|
@@ -268,70 +293,282 @@ function createTelegramCallbackSink(input) {
|
|
|
268
293
|
function createCompositeCallbackSink(sinks) {
|
|
269
294
|
return {
|
|
270
295
|
async deliver(message) {
|
|
296
|
+
let result;
|
|
297
|
+
let delivered = false;
|
|
298
|
+
const failures = [];
|
|
271
299
|
for (const sink of sinks) {
|
|
272
|
-
|
|
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
|
+
}
|
|
273
309
|
}
|
|
310
|
+
if (!delivered && failures.length > 0) {
|
|
311
|
+
throw new AggregateError(failures, "Composite callback delivery failed for every sink.");
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
274
314
|
}
|
|
275
315
|
};
|
|
276
316
|
}
|
|
277
317
|
|
|
278
318
|
// src/presentation.ts
|
|
279
|
-
import {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
}
|
|
283
459
|
function createDefaultCallbackPresentation() {
|
|
284
460
|
return {
|
|
285
461
|
shouldDeliverAcknowledgement(provider) {
|
|
286
|
-
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);
|
|
287
470
|
},
|
|
288
471
|
shouldDeliverProgress(provider) {
|
|
289
|
-
return provider
|
|
472
|
+
return shouldDeliverCallbackProgress(provider);
|
|
290
473
|
},
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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);
|
|
294
511
|
}
|
|
295
|
-
if (input.
|
|
296
|
-
return
|
|
512
|
+
if (input.presentation.kind === "doctor_summary") {
|
|
513
|
+
return renderDoctorSummary(input.provider, input.presentation);
|
|
297
514
|
}
|
|
298
|
-
if (input.
|
|
299
|
-
return
|
|
515
|
+
if (input.presentation.kind === "source_thread_status") {
|
|
516
|
+
return renderSourceThreadStatus(input.provider, input.presentation);
|
|
300
517
|
}
|
|
301
|
-
|
|
518
|
+
if (input.presentation.kind === "action_receipt") {
|
|
519
|
+
return renderActionReceipt(input.provider, input.presentation);
|
|
520
|
+
}
|
|
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
|
+
});
|
|
302
539
|
},
|
|
303
540
|
progress(input) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
541
|
+
return this.runStatus({
|
|
542
|
+
provider: input.provider,
|
|
543
|
+
runId: input.runId,
|
|
544
|
+
state: "running",
|
|
545
|
+
message: input.message,
|
|
546
|
+
detailVisibility: "audit"
|
|
547
|
+
}).body;
|
|
308
548
|
},
|
|
309
549
|
final(input) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
if (input.provider === "telegram") {
|
|
320
|
-
return { body: renderTelegramFinalResult(input.result) };
|
|
321
|
-
}
|
|
322
|
-
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
|
+
});
|
|
323
558
|
}
|
|
324
559
|
};
|
|
325
560
|
}
|
|
326
561
|
|
|
327
562
|
// src/server.ts
|
|
328
|
-
import { createHash } from "crypto";
|
|
563
|
+
import { createHash, randomUUID } from "crypto";
|
|
329
564
|
import {
|
|
330
565
|
AdapterMutationMappingSchema,
|
|
331
566
|
ActorIdentitySchema,
|
|
332
567
|
ActionHintSchema,
|
|
568
|
+
capabilityForMutationIntent,
|
|
333
569
|
conversationKeysFromEvent as conversationKeysFromEvent2,
|
|
334
570
|
parseThreadActionCommand,
|
|
571
|
+
permissionScopesAllowCapability,
|
|
335
572
|
projectTargetRefFromEvent as projectTargetRefFromEvent2,
|
|
336
573
|
suggestedActionCandidatesFromSnapshots,
|
|
337
574
|
createAdapterMutationCompilerRegistry,
|
|
@@ -339,7 +576,12 @@ import {
|
|
|
339
576
|
OpenTagRunResultSchema,
|
|
340
577
|
PolicyRuleSchema,
|
|
341
578
|
RunEventImportanceSchema,
|
|
342
|
-
RunEventVisibilitySchema
|
|
579
|
+
RunEventVisibilitySchema,
|
|
580
|
+
DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
581
|
+
RequestBodyTooLargeError,
|
|
582
|
+
platformCapabilityForProvider as platformCapabilityForProvider2,
|
|
583
|
+
readRequestTextWithLimit,
|
|
584
|
+
shouldDeliverSourceReceipt
|
|
343
585
|
} from "@opentag/core";
|
|
344
586
|
import {
|
|
345
587
|
applyGitHubIssueMutationOperation,
|
|
@@ -494,26 +736,144 @@ function createAdmissionRuntime(input) {
|
|
|
494
736
|
}
|
|
495
737
|
|
|
496
738
|
// src/server.ts
|
|
497
|
-
|
|
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 = {}) {
|
|
498
755
|
let json;
|
|
499
756
|
try {
|
|
500
|
-
|
|
757
|
+
const rawBody = await readRequestTextWithLimit(c.req.raw, { maxBytes: options.maxBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES });
|
|
758
|
+
json = JSON.parse(rawBody);
|
|
501
759
|
} catch (err) {
|
|
760
|
+
if (err instanceof RequestBodyTooLargeError) throw requestBodyTooLarge(c, err.maxBytes);
|
|
761
|
+
if (err instanceof HTTPException) throw err;
|
|
502
762
|
if (err instanceof SyntaxError) {
|
|
503
763
|
throw new HTTPException(400, {
|
|
504
|
-
res: c.json({ error: "invalid_json_body" }, 400)
|
|
764
|
+
res: c.json({ error: "invalid_json_body" }, 400),
|
|
765
|
+
cause: new RequestBodyRejectedError({ reason: "invalid_json_body" })
|
|
505
766
|
});
|
|
506
767
|
}
|
|
507
768
|
throw err;
|
|
508
769
|
}
|
|
509
770
|
const result = schema.safeParse(json);
|
|
510
771
|
if (!result.success) {
|
|
772
|
+
const publicError = options.invalidBodyError ?? "invalid_request_body";
|
|
511
773
|
throw new HTTPException(400, {
|
|
512
|
-
res: c.json({ error:
|
|
774
|
+
res: c.json({ error: publicError, issues: result.error.issues }, 400),
|
|
775
|
+
cause: new RequestBodyRejectedError({ reason: "invalid_request_body", publicError })
|
|
513
776
|
});
|
|
514
777
|
}
|
|
515
778
|
return result.data;
|
|
516
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
|
+
}
|
|
517
877
|
var CreateRunnerSchema = z.object({
|
|
518
878
|
runnerId: z.string().min(1),
|
|
519
879
|
name: z.string().min(1)
|
|
@@ -553,11 +913,32 @@ var CreateRunSchema = z.object({
|
|
|
553
913
|
runId: z.string().min(1),
|
|
554
914
|
event: OpenTagEventSchema
|
|
555
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
|
+
});
|
|
556
927
|
var PromoteFollowUpRequestSchema = z.object({
|
|
557
928
|
runId: z.string().min(1)
|
|
558
929
|
});
|
|
559
930
|
var CompleteRunSchema = z.object({
|
|
560
|
-
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()
|
|
561
942
|
});
|
|
562
943
|
var ApprovalDecisionInputSchema = z.object({
|
|
563
944
|
id: z.string().min(1).optional(),
|
|
@@ -599,13 +980,38 @@ var ChildRunInputSchema = z.object({
|
|
|
599
980
|
sourceProposalId: z.string().min(1).optional(),
|
|
600
981
|
sourceApplyPlanId: z.string().min(1).optional()
|
|
601
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
|
+
];
|
|
602
997
|
var ProgressSchema = z.object({
|
|
603
998
|
type: z.string().min(1).optional(),
|
|
604
999
|
message: z.string().min(1),
|
|
605
1000
|
at: z.string().datetime().optional(),
|
|
606
1001
|
visibility: RunEventVisibilitySchema.optional(),
|
|
607
|
-
importance: RunEventImportanceSchema.optional()
|
|
1002
|
+
importance: RunEventImportanceSchema.optional(),
|
|
1003
|
+
idempotencyKey: z.string().min(1).max(256).optional()
|
|
608
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
|
+
}
|
|
609
1015
|
function childEventFromParent(input) {
|
|
610
1016
|
return {
|
|
611
1017
|
...input.parentEvent,
|
|
@@ -621,10 +1027,7 @@ function childEventFromParent(input) {
|
|
|
621
1027
|
actionKind: input.actionKind
|
|
622
1028
|
}
|
|
623
1029
|
},
|
|
624
|
-
metadata:
|
|
625
|
-
...input.parentEvent.metadata,
|
|
626
|
-
...input.metadata ?? {}
|
|
627
|
-
},
|
|
1030
|
+
metadata: childEventMetadata(input.parentEvent.metadata, input.metadata),
|
|
628
1031
|
permissions: input.permissions ?? input.parentEvent.permissions
|
|
629
1032
|
};
|
|
630
1033
|
}
|
|
@@ -647,6 +1050,28 @@ function metadataString2(metadata, key) {
|
|
|
647
1050
|
const value = metadata?.[key];
|
|
648
1051
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
649
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
|
+
}
|
|
650
1075
|
function githubIssueWorkItemExternalId(metadata) {
|
|
651
1076
|
const owner = metadataString2(metadata, "owner");
|
|
652
1077
|
const repo = metadataString2(metadata, "repo");
|
|
@@ -761,6 +1186,30 @@ async function resolveThreadAction(input) {
|
|
|
761
1186
|
});
|
|
762
1187
|
const primaryConversationKey = conversationKeys[0];
|
|
763
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
|
+
}
|
|
764
1213
|
if (input.command.selection.kind === "proposal") {
|
|
765
1214
|
const stored = await input.repo.getSuggestedChanges({ proposalId: input.command.selection.proposalId });
|
|
766
1215
|
if (!stored) {
|
|
@@ -806,6 +1255,240 @@ function isRepoLevelGitHubIntent(intent) {
|
|
|
806
1255
|
function adapterForAction(input) {
|
|
807
1256
|
return hasGitHubRepoTarget(input.event) && (hasGitHubIssueOrPullTarget(input.event) || input.selectedIntents.length > 0 && input.selectedIntents.every((intent) => isRepoLevelGitHubIntent(intent))) ? "github" : input.callbackProvider;
|
|
808
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
|
+
}
|
|
809
1492
|
async function authorizeThreadAction(input) {
|
|
810
1493
|
const repoKey = projectTargetRefFromEvent2(input.resolved.proposal.event);
|
|
811
1494
|
if (!repoKey) {
|
|
@@ -888,6 +1571,13 @@ function selectedIntentsAlreadyApplied(input) {
|
|
|
888
1571
|
(intentId) => input.plan.outcomes?.some((outcome) => outcome.intentId === intentId && outcome.outcome === "applied")
|
|
889
1572
|
);
|
|
890
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
|
+
}
|
|
891
1581
|
function githubTargetFromEvent(event) {
|
|
892
1582
|
const owner = event.metadata["owner"];
|
|
893
1583
|
const repoName = event.metadata["repo"];
|
|
@@ -906,6 +1596,12 @@ function githubTargetFromEvent(event) {
|
|
|
906
1596
|
function selectedActionSummary(candidates) {
|
|
907
1597
|
return candidates.map((candidate) => `${candidate.index}. ${candidate.intent.summary}`).join("; ");
|
|
908
1598
|
}
|
|
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
|
+
}
|
|
909
1605
|
function addPermissionGrant(permissions, grant) {
|
|
910
1606
|
if (permissions.some((permission) => permission.scope === grant.scope)) return permissions;
|
|
911
1607
|
return [...permissions, grant];
|
|
@@ -930,73 +1626,93 @@ function childRunPermissionsForThreadAction(input) {
|
|
|
930
1626
|
}
|
|
931
1627
|
return permissions;
|
|
932
1628
|
}
|
|
933
|
-
function childRunContextLines(input) {
|
|
934
|
-
const previousSummary = input.resolved.proposal.run.result?.summary ?? input.resolved.proposal.snapshot.summary;
|
|
935
|
-
return [
|
|
936
|
-
`- Proposal: \`${input.resolved.proposal.snapshot.proposalId}\``,
|
|
937
|
-
`- Selected intents: ${input.resolved.selectedIntentIds.map((intentId) => `\`${intentId}\``).join(", ")}`,
|
|
938
|
-
`- Previous run: \`${input.resolved.proposal.runId}\``,
|
|
939
|
-
...input.approvalDecisionId ? [`- Approval decision: \`${input.approvalDecisionId}\``] : [],
|
|
940
|
-
`- Previous result: ${previousSummary}`,
|
|
941
|
-
...input.sourceApplyPlanId ? [`- Apply plan: \`${input.sourceApplyPlanId}\``] : [],
|
|
942
|
-
...input.fallbackReason ? [`- Fallback reason: ${input.fallbackReason}`] : []
|
|
943
|
-
];
|
|
944
|
-
}
|
|
945
1629
|
function renderChildRunCreatedBody(input) {
|
|
1630
|
+
const title = selectedActionReceiptTitle(input.selectionText ?? selectedActionSummary(input.resolved.selectedCandidates));
|
|
946
1631
|
if (input.provider === "slack") {
|
|
947
1632
|
return [
|
|
948
|
-
|
|
1633
|
+
input.lead,
|
|
1634
|
+
`Action: ${title}`,
|
|
949
1635
|
...input.fallbackReason ? [`Reason: ${input.fallbackReason}`] : []
|
|
950
1636
|
].join("\n");
|
|
951
1637
|
}
|
|
952
1638
|
return [
|
|
953
1639
|
input.lead,
|
|
954
1640
|
"",
|
|
955
|
-
`
|
|
1641
|
+
`Action: ${title}`,
|
|
956
1642
|
"",
|
|
957
|
-
|
|
958
|
-
...childRunContextLines(input),
|
|
1643
|
+
`Child run: \`${input.childRun.id}\``,
|
|
959
1644
|
"",
|
|
960
|
-
|
|
1645
|
+
...input.fallbackReason ? [`Reason: ${input.fallbackReason}`, ""] : [],
|
|
1646
|
+
`Audit: run \`opentag status --run ${input.childRun.id}\` locally.`
|
|
961
1647
|
].join("\n");
|
|
962
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
|
+
}
|
|
963
1664
|
function renderAppliedThreadActionBody(input) {
|
|
964
1665
|
const selectedOutcomes = input.outcomes.filter((outcome) => input.selectedIntentIds.includes(outcome.intentId));
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
return lines.join("\n");
|
|
977
|
-
}
|
|
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) {
|
|
978
1675
|
return [
|
|
979
|
-
`
|
|
980
|
-
"",
|
|
981
|
-
|
|
982
|
-
...selectedOutcomes.map((outcome) => `- \`${outcome.intentId}\`: ${outcome.outcome}${outcome.externalUri ? ` (${outcome.externalUri})` : ""}`)
|
|
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.`
|
|
983
1679
|
].join("\n");
|
|
984
1680
|
}
|
|
985
1681
|
function renderThreadActionRecordedBody(input) {
|
|
986
|
-
const
|
|
987
|
-
if (input.provider === "slack") {
|
|
988
|
-
if (input.verb === "approve") {
|
|
989
|
-
return `${pastTense} ${input.selectionText}.
|
|
990
|
-
Next: use Apply ${input.applyIndex ?? 1} when you want OpenTag to perform it.`;
|
|
991
|
-
}
|
|
992
|
-
return `${pastTense} ${input.selectionText}.`;
|
|
993
|
-
}
|
|
1682
|
+
const title = selectedActionReceiptTitle(input.selectionText);
|
|
994
1683
|
if (input.verb === "approve") {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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");
|
|
998
1694
|
}
|
|
999
|
-
return
|
|
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 };
|
|
1000
1716
|
}
|
|
1001
1717
|
function actionContextPointer(input) {
|
|
1002
1718
|
const lines = [
|
|
@@ -1156,7 +1872,13 @@ function nextCallbackAttemptAt(input) {
|
|
|
1156
1872
|
}
|
|
1157
1873
|
async function deliverCallbackDelivery(input) {
|
|
1158
1874
|
try {
|
|
1159
|
-
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({
|
|
1160
1882
|
runId: input.delivery.runId,
|
|
1161
1883
|
kind: input.delivery.kind,
|
|
1162
1884
|
provider: input.delivery.provider,
|
|
@@ -1165,15 +1887,23 @@ async function deliverCallbackDelivery(input) {
|
|
|
1165
1887
|
...input.delivery.threadKey ? { threadKey: input.delivery.threadKey } : {},
|
|
1166
1888
|
...input.delivery.agentId ? { agentId: input.delivery.agentId } : {},
|
|
1167
1889
|
...input.delivery.statusMessageKey ? { statusMessageKey: input.delivery.statusMessageKey } : {},
|
|
1168
|
-
...
|
|
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 } : {}
|
|
1169
1898
|
});
|
|
1170
|
-
await input.repo.markCallbackDelivered({ deliveryId: input.delivery.id });
|
|
1171
1899
|
return true;
|
|
1172
1900
|
} catch (error) {
|
|
1901
|
+
const maxAttempts = input.retry?.maxAttempts ?? 5;
|
|
1173
1902
|
const nextAttemptAt = nextCallbackAttemptAt({ attempts: input.delivery.attempts, ...input.retry ?? {} });
|
|
1174
1903
|
await input.repo.markCallbackFailed({
|
|
1175
1904
|
deliveryId: input.delivery.id,
|
|
1176
1905
|
error: error instanceof Error ? error.message : String(error),
|
|
1906
|
+
maxAttempts,
|
|
1177
1907
|
...nextAttemptAt ? { nextAttemptAt } : {}
|
|
1178
1908
|
});
|
|
1179
1909
|
return false;
|
|
@@ -1213,7 +1943,8 @@ async function deliverAndAudit(input) {
|
|
|
1213
1943
|
...input.message.threadKey ? { threadKey: input.message.threadKey } : {},
|
|
1214
1944
|
...input.message.agentId ? { agentId: input.message.agentId } : {},
|
|
1215
1945
|
...input.message.statusMessageKey ? { statusMessageKey: input.message.statusMessageKey } : {},
|
|
1216
|
-
...input.message.blocks ? { blocks: input.message.blocks } : {}
|
|
1946
|
+
...input.message.blocks ? { blocks: input.message.blocks } : {},
|
|
1947
|
+
...input.message.rich ? { rich: input.message.rich } : {}
|
|
1217
1948
|
});
|
|
1218
1949
|
await deliverCallbackDelivery({
|
|
1219
1950
|
repo: input.repo,
|
|
@@ -1254,9 +1985,81 @@ async function deliverSourceReceiptBestEffort(input) {
|
|
|
1254
1985
|
return { delivered: false };
|
|
1255
1986
|
}
|
|
1256
1987
|
}
|
|
1257
|
-
function
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
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" };
|
|
1260
2063
|
}
|
|
1261
2064
|
function createDispatcherApp(input) {
|
|
1262
2065
|
const sqlite = new Database(input.databasePath);
|
|
@@ -1267,20 +2070,234 @@ function createDispatcherApp(input) {
|
|
|
1267
2070
|
const sourceReceiptSink = input.sourceReceiptSink ?? noopSourceReceiptSink;
|
|
1268
2071
|
const presentation = input.presentation ?? createDefaultCallbackPresentation();
|
|
1269
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
|
+
}
|
|
1270
2204
|
const admission = createAdmissionRuntime({
|
|
1271
2205
|
repo,
|
|
1272
2206
|
...input.agentAccessProfileCheck ? { agentAccessProfileCheck: input.agentAccessProfileCheck } : {}
|
|
1273
2207
|
});
|
|
1274
2208
|
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
2209
|
+
if (input.rateLimit) {
|
|
2210
|
+
app.use("/v1/*", createDispatcherRateLimitMiddleware(input.rateLimit));
|
|
2211
|
+
}
|
|
1275
2212
|
app.use("/v1/*", async (c, next) => {
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
+
);
|
|
1278
2239
|
}
|
|
1279
2240
|
await next();
|
|
1280
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
|
+
});
|
|
1281
2289
|
app.post("/v1/runners", async (c) => {
|
|
1282
|
-
const parsed = await
|
|
2290
|
+
const parsed = await parseDispatcherBody(c, CreateRunnerSchema);
|
|
1283
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
|
+
});
|
|
1284
2301
|
return c.json({ ok: true }, 201);
|
|
1285
2302
|
});
|
|
1286
2303
|
app.get("/v1/runners/:runnerId", async (c) => {
|
|
@@ -1289,7 +2306,7 @@ function createDispatcherApp(input) {
|
|
|
1289
2306
|
return c.json({ runner });
|
|
1290
2307
|
});
|
|
1291
2308
|
app.post("/v1/repo-bindings", async (c) => {
|
|
1292
|
-
const parsed = await
|
|
2309
|
+
const parsed = await parseDispatcherBody(c, CreateRepoBindingSchema);
|
|
1293
2310
|
await repo.createRepoBinding({
|
|
1294
2311
|
provider: parsed.provider,
|
|
1295
2312
|
owner: parsed.owner,
|
|
@@ -1299,6 +2316,20 @@ function createDispatcherApp(input) {
|
|
|
1299
2316
|
...parsed.defaultExecutor ? { defaultExecutor: parsed.defaultExecutor } : {},
|
|
1300
2317
|
...parsed.allowedActors?.length ? { allowedActors: parsed.allowedActors } : {}
|
|
1301
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
|
+
});
|
|
1302
2333
|
return c.json({ ok: true }, 201);
|
|
1303
2334
|
});
|
|
1304
2335
|
app.get("/v1/repo-bindings/:provider/:owner/:repo", async (c) => {
|
|
@@ -1311,13 +2342,32 @@ function createDispatcherApp(input) {
|
|
|
1311
2342
|
return c.json({ binding });
|
|
1312
2343
|
});
|
|
1313
2344
|
app.post("/v1/repo-bindings/:provider/:owner/:repo/policy-rules", async (c) => {
|
|
1314
|
-
const parsed = await
|
|
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");
|
|
1315
2349
|
const rule = await repo.upsertRepoPolicyRule({
|
|
1316
|
-
provider
|
|
1317
|
-
owner
|
|
1318
|
-
repo:
|
|
2350
|
+
provider,
|
|
2351
|
+
owner,
|
|
2352
|
+
repo: repoName,
|
|
1319
2353
|
rule: parsed.rule
|
|
1320
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
|
+
});
|
|
1321
2371
|
return c.json({ rule }, 201);
|
|
1322
2372
|
});
|
|
1323
2373
|
app.get("/v1/repo-bindings/:provider/:owner/:repo/policy-rules", async (c) => {
|
|
@@ -1329,13 +2379,32 @@ function createDispatcherApp(input) {
|
|
|
1329
2379
|
return c.json({ rules });
|
|
1330
2380
|
});
|
|
1331
2381
|
app.post("/v1/repo-bindings/:provider/:owner/:repo/mutation-mappings", async (c) => {
|
|
1332
|
-
const parsed = await
|
|
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");
|
|
1333
2386
|
const mapping = await repo.upsertRepoMutationMapping({
|
|
1334
|
-
provider
|
|
1335
|
-
owner
|
|
1336
|
-
repo:
|
|
2387
|
+
provider,
|
|
2388
|
+
owner,
|
|
2389
|
+
repo: repoName,
|
|
1337
2390
|
mapping: parsed.mapping
|
|
1338
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
|
+
});
|
|
1339
2408
|
return c.json({ mapping }, 201);
|
|
1340
2409
|
});
|
|
1341
2410
|
app.get("/v1/repo-bindings/:provider/:owner/:repo/mutation-mappings", async (c) => {
|
|
@@ -1361,7 +2430,7 @@ function createDispatcherApp(input) {
|
|
|
1361
2430
|
return c.json({ metrics });
|
|
1362
2431
|
});
|
|
1363
2432
|
app.post("/v1/channel-bindings", async (c) => {
|
|
1364
|
-
const parsed = await
|
|
2433
|
+
const parsed = await parseDispatcherBody(c, CreateChannelBindingSchema);
|
|
1365
2434
|
await repo.upsertChannelBinding({
|
|
1366
2435
|
provider: parsed.provider,
|
|
1367
2436
|
accountId: parsed.accountId,
|
|
@@ -1371,6 +2440,20 @@ function createDispatcherApp(input) {
|
|
|
1371
2440
|
repo: parsed.repo,
|
|
1372
2441
|
...parsed.metadata ? { metadata: parsed.metadata } : {}
|
|
1373
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
|
+
});
|
|
1374
2457
|
return c.json({ ok: true }, 201);
|
|
1375
2458
|
});
|
|
1376
2459
|
app.get("/v1/channel-bindings/:provider/:accountId/:conversationId", async (c) => {
|
|
@@ -1382,9 +2465,94 @@ function createDispatcherApp(input) {
|
|
|
1382
2465
|
if (!binding) return c.json({ error: "channel_binding_not_found" }, 404);
|
|
1383
2466
|
return c.json({ binding });
|
|
1384
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
|
+
});
|
|
1385
2538
|
app.post("/v1/slack-channel-bindings", async (c) => {
|
|
1386
|
-
const parsed = await
|
|
2539
|
+
const parsed = await parseDispatcherBody(c, CreateSlackChannelBindingSchema);
|
|
1387
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
|
+
});
|
|
1388
2556
|
return c.json({ ok: true }, 201);
|
|
1389
2557
|
});
|
|
1390
2558
|
app.get("/v1/slack-channel-bindings/:teamId/:channelId", async (c) => {
|
|
@@ -1396,43 +2564,69 @@ function createDispatcherApp(input) {
|
|
|
1396
2564
|
return c.json({ binding });
|
|
1397
2565
|
});
|
|
1398
2566
|
app.post("/v1/runs", async (c) => {
|
|
1399
|
-
const parsed = await
|
|
2567
|
+
const parsed = await parseDispatcherBody(c, CreateRunSchema);
|
|
1400
2568
|
const admitted = await admission.admitRun({ requestId: parsed.runId, event: parsed.event });
|
|
1401
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
|
+
});
|
|
1402
2583
|
return c.json({ decision: admitted.decision }, 202);
|
|
1403
2584
|
}
|
|
1404
2585
|
if (admitted.outcome === "drop_duplicate") {
|
|
1405
|
-
await repo.
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
payload: admitted.decision,
|
|
1409
|
-
visibility: "audit",
|
|
1410
|
-
importance: "normal",
|
|
1411
|
-
message: admitted.decision.reason
|
|
1412
|
-
});
|
|
1413
|
-
await repo.appendRunEvent({
|
|
1414
|
-
runId: admitted.run.id,
|
|
1415
|
-
type: "run.create_idempotent_replay",
|
|
1416
|
-
payload: { requestedRunId: parsed.runId, eventId: parsed.event.id },
|
|
1417
|
-
visibility: "audit",
|
|
1418
|
-
importance: "low"
|
|
1419
|
-
});
|
|
1420
|
-
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);
|
|
1421
2589
|
}
|
|
1422
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
|
+
}
|
|
1423
2623
|
return c.json({ decision: admitted.decision, followUpRequest: admitted.followUpRequest }, 202);
|
|
1424
2624
|
}
|
|
1425
2625
|
const createdRun = await repo.createRun({ id: parsed.runId, event: parsed.event });
|
|
1426
2626
|
if (!createdRun.created) {
|
|
1427
2627
|
return c.json(
|
|
1428
2628
|
{
|
|
1429
|
-
decision:
|
|
1430
|
-
...admitted.decision,
|
|
1431
|
-
action: "drop_duplicate",
|
|
1432
|
-
reason: "Source event already created a run.",
|
|
1433
|
-
reasonCode: "duplicate_source_event",
|
|
1434
|
-
activeRunId: createdRun.run.id
|
|
1435
|
-
},
|
|
2629
|
+
decision: createdRun.replayDecision,
|
|
1436
2630
|
run: createdRun.run,
|
|
1437
2631
|
idempotentReplay: true
|
|
1438
2632
|
},
|
|
@@ -1451,8 +2645,14 @@ function createDispatcherApp(input) {
|
|
|
1451
2645
|
...parsed.event.target.agentId ? { agentId: parsed.event.target.agentId } : {}
|
|
1452
2646
|
}
|
|
1453
2647
|
});
|
|
1454
|
-
const shouldDeliverAcknowledgement = presentation.shouldDeliverAcknowledgement(parsed.event.callback.provider) || parsed.event.callback.provider
|
|
2648
|
+
const shouldDeliverAcknowledgement = presentation.shouldDeliverAcknowledgement(parsed.event.callback.provider) || shouldDeliverSourceReceipt(parsed.event.callback.provider) && !sourceReceiptDelivery.delivered;
|
|
1455
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 });
|
|
1456
2656
|
await deliverAndAudit({
|
|
1457
2657
|
repo,
|
|
1458
2658
|
sink: callbackSink,
|
|
@@ -1462,16 +2662,19 @@ function createDispatcherApp(input) {
|
|
|
1462
2662
|
kind: "acknowledgement",
|
|
1463
2663
|
provider: parsed.event.callback.provider,
|
|
1464
2664
|
uri: parsed.event.callback.uri,
|
|
1465
|
-
body:
|
|
2665
|
+
body: acknowledgement.body,
|
|
1466
2666
|
...parsed.event.target.agentId ? { agentId: parsed.event.target.agentId } : {},
|
|
1467
|
-
...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 } : {}
|
|
1468
2671
|
}
|
|
1469
2672
|
});
|
|
1470
2673
|
}
|
|
1471
2674
|
return c.json({ decision: admitted.decision, run }, 201);
|
|
1472
2675
|
});
|
|
1473
2676
|
app.post("/v1/thread-actions", async (c) => {
|
|
1474
|
-
const parsed = await
|
|
2677
|
+
const parsed = await parseDispatcherBody(c, ThreadActionInputSchema);
|
|
1475
2678
|
const command = parseThreadActionCommand(parsed.rawText);
|
|
1476
2679
|
if (!command) {
|
|
1477
2680
|
return c.json({ outcome: "ignored", reason: "not_action_command" }, 202);
|
|
@@ -1523,22 +2726,24 @@ function createDispatcherApp(input) {
|
|
|
1523
2726
|
if (existingPlan) {
|
|
1524
2727
|
const existingDecision2 = await repo.getApprovalDecision({ id: existingPlan.approvalDecisionId });
|
|
1525
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
|
+
});
|
|
1526
2742
|
return c.json({ outcome: "already_applied", decision: existingDecision2, plan: existingPlan }, 200);
|
|
1527
2743
|
}
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
command,
|
|
1532
|
-
resolved: resolved.resolved,
|
|
1533
|
-
runId: stableChildRunId({
|
|
1534
|
-
command,
|
|
1535
|
-
resolved: resolved.resolved,
|
|
1536
|
-
sourceApplyPlanId: existingPlan.id,
|
|
1537
|
-
fallbackReason
|
|
1538
|
-
}),
|
|
1539
|
-
approvalDecisionId: existingPlan.approvalDecisionId,
|
|
1540
|
-
sourceApplyPlanId: existingPlan.id,
|
|
1541
|
-
fallbackReason
|
|
2744
|
+
const isStale = selectedIntentsHaveStaleOutcome({
|
|
2745
|
+
plan: existingPlan,
|
|
2746
|
+
selectedIntentIds: resolved.resolved.selectedIntentIds
|
|
1542
2747
|
});
|
|
1543
2748
|
await deliverAndAudit({
|
|
1544
2749
|
repo,
|
|
@@ -1549,19 +2754,14 @@ function createDispatcherApp(input) {
|
|
|
1549
2754
|
kind: "final",
|
|
1550
2755
|
provider: parsed.callback.provider,
|
|
1551
2756
|
uri: parsed.callback.uri,
|
|
1552
|
-
body:
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
provider: parsed.callback.provider,
|
|
1557
|
-
approvalDecisionId: existingPlan.approvalDecisionId,
|
|
1558
|
-
sourceApplyPlanId: existingPlan.id,
|
|
1559
|
-
fallbackReason
|
|
1560
|
-
}),
|
|
2757
|
+
body: isStale ? renderStaleThreadActionBody({
|
|
2758
|
+
selectionText,
|
|
2759
|
+
continueIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1
|
|
2760
|
+
}) : renderAlreadyPlannedThreadActionBody({ selectionText }),
|
|
1561
2761
|
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1562
2762
|
}
|
|
1563
2763
|
});
|
|
1564
|
-
return c.json({ outcome: "already_planned", decision: existingDecision2, plan: existingPlan
|
|
2764
|
+
return c.json({ outcome: isStale ? "stale" : "already_planned", decision: existingDecision2, plan: existingPlan }, 200);
|
|
1565
2765
|
}
|
|
1566
2766
|
}
|
|
1567
2767
|
const providedDecision = parsed.id ? await repo.getApprovalDecision({ id: parsed.id }) : null;
|
|
@@ -1603,10 +2803,8 @@ function createDispatcherApp(input) {
|
|
|
1603
2803
|
return c.json({ outcome: "already_rejected", decision }, 200);
|
|
1604
2804
|
}
|
|
1605
2805
|
const body2 = renderThreadActionRecordedBody({
|
|
1606
|
-
provider: parsed.callback.provider,
|
|
1607
2806
|
verb: "reject",
|
|
1608
|
-
selectionText
|
|
1609
|
-
proposalId: resolved.resolved.proposal.snapshot.proposalId
|
|
2807
|
+
selectionText
|
|
1610
2808
|
});
|
|
1611
2809
|
await deliverAndAudit({
|
|
1612
2810
|
repo,
|
|
@@ -1627,12 +2825,17 @@ function createDispatcherApp(input) {
|
|
|
1627
2825
|
if (existingDecision) {
|
|
1628
2826
|
return c.json({ outcome: "already_approved", decision }, 200);
|
|
1629
2827
|
}
|
|
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
|
+
});
|
|
1630
2834
|
const body2 = renderThreadActionRecordedBody({
|
|
1631
|
-
provider: parsed.callback.provider,
|
|
1632
2835
|
verb: "approve",
|
|
1633
2836
|
selectionText,
|
|
1634
|
-
|
|
1635
|
-
|
|
2837
|
+
applyIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1,
|
|
2838
|
+
directApply
|
|
1636
2839
|
});
|
|
1637
2840
|
await deliverAndAudit({
|
|
1638
2841
|
repo,
|
|
@@ -1658,10 +2861,11 @@ function createDispatcherApp(input) {
|
|
|
1658
2861
|
approvalDecisionId: decision.id
|
|
1659
2862
|
});
|
|
1660
2863
|
const body2 = renderChildRunCreatedBody({
|
|
1661
|
-
lead:
|
|
2864
|
+
lead: "Continuing in OpenTag from this approved action.",
|
|
1662
2865
|
resolved: resolved.resolved,
|
|
1663
2866
|
childRun: childRun2,
|
|
1664
2867
|
provider: parsed.callback.provider,
|
|
2868
|
+
selectionText,
|
|
1665
2869
|
approvalDecisionId: decision.id
|
|
1666
2870
|
});
|
|
1667
2871
|
await deliverAndAudit({
|
|
@@ -1691,22 +2895,24 @@ function createDispatcherApp(input) {
|
|
|
1691
2895
|
}
|
|
1692
2896
|
if (!planResult.created) {
|
|
1693
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
|
+
});
|
|
1694
2911
|
return c.json({ outcome: "already_applied", decision, plan: planResult.plan }, 200);
|
|
1695
2912
|
}
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
command,
|
|
1700
|
-
resolved: resolved.resolved,
|
|
1701
|
-
runId: stableChildRunId({
|
|
1702
|
-
command,
|
|
1703
|
-
resolved: resolved.resolved,
|
|
1704
|
-
sourceApplyPlanId: planResult.plan.id,
|
|
1705
|
-
fallbackReason
|
|
1706
|
-
}),
|
|
1707
|
-
approvalDecisionId: decision.id,
|
|
1708
|
-
sourceApplyPlanId: planResult.plan.id,
|
|
1709
|
-
fallbackReason
|
|
2913
|
+
const isStale = selectedIntentsHaveStaleOutcome({
|
|
2914
|
+
plan: planResult.plan,
|
|
2915
|
+
selectedIntentIds: resolved.resolved.selectedIntentIds
|
|
1710
2916
|
});
|
|
1711
2917
|
await deliverAndAudit({
|
|
1712
2918
|
repo,
|
|
@@ -1717,19 +2923,14 @@ function createDispatcherApp(input) {
|
|
|
1717
2923
|
kind: "final",
|
|
1718
2924
|
provider: parsed.callback.provider,
|
|
1719
2925
|
uri: parsed.callback.uri,
|
|
1720
|
-
body:
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
provider: parsed.callback.provider,
|
|
1725
|
-
approvalDecisionId: decision.id,
|
|
1726
|
-
sourceApplyPlanId: planResult.plan.id,
|
|
1727
|
-
fallbackReason
|
|
1728
|
-
}),
|
|
2926
|
+
body: isStale ? renderStaleThreadActionBody({
|
|
2927
|
+
selectionText,
|
|
2928
|
+
continueIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1
|
|
2929
|
+
}) : renderAlreadyPlannedThreadActionBody({ selectionText }),
|
|
1729
2930
|
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1730
2931
|
}
|
|
1731
2932
|
});
|
|
1732
|
-
return c.json({ outcome: "already_planned", decision, plan: planResult.plan
|
|
2933
|
+
return c.json({ outcome: isStale ? "stale" : "already_planned", decision, plan: planResult.plan }, 200);
|
|
1733
2934
|
}
|
|
1734
2935
|
const plan = planResult.plan;
|
|
1735
2936
|
const execution = await executeGitHubApplyPlan({
|
|
@@ -1741,9 +2942,7 @@ function createDispatcherApp(input) {
|
|
|
1741
2942
|
if (execution.executed) {
|
|
1742
2943
|
const outcomes = execution.plan.outcomes ?? [];
|
|
1743
2944
|
const body2 = renderAppliedThreadActionBody({
|
|
1744
|
-
provider: parsed.callback.provider,
|
|
1745
2945
|
selectionText,
|
|
1746
|
-
proposalId: resolved.resolved.proposal.snapshot.proposalId,
|
|
1747
2946
|
selectedIntentIds: resolved.resolved.selectedIntentIds,
|
|
1748
2947
|
outcomes
|
|
1749
2948
|
});
|
|
@@ -1762,6 +2961,25 @@ function createDispatcherApp(input) {
|
|
|
1762
2961
|
});
|
|
1763
2962
|
return c.json({ outcome: "applied", decision, plan: execution.plan }, 201);
|
|
1764
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
|
+
}
|
|
1765
2983
|
const childRun = await createChildRunForThreadAction({
|
|
1766
2984
|
repo,
|
|
1767
2985
|
command,
|
|
@@ -1777,10 +2995,11 @@ function createDispatcherApp(input) {
|
|
|
1777
2995
|
fallbackReason: execution.fallbackReason ?? "OpenTag cannot directly apply this intent yet."
|
|
1778
2996
|
});
|
|
1779
2997
|
const body = renderChildRunCreatedBody({
|
|
1780
|
-
lead:
|
|
2998
|
+
lead: "Needs setup before OpenTag can apply this action directly.",
|
|
1781
2999
|
resolved: resolved.resolved,
|
|
1782
3000
|
childRun,
|
|
1783
3001
|
provider: parsed.callback.provider,
|
|
3002
|
+
selectionText,
|
|
1784
3003
|
approvalDecisionId: decision.id,
|
|
1785
3004
|
sourceApplyPlanId: execution.plan.id,
|
|
1786
3005
|
fallbackReason: execution.fallbackReason ?? "The adapter could not execute the selected intent."
|
|
@@ -1806,10 +3025,10 @@ function createDispatcherApp(input) {
|
|
|
1806
3025
|
return c.json({ followUpRequest });
|
|
1807
3026
|
});
|
|
1808
3027
|
app.post("/v1/follow-up-requests/:id/create-run", async (c) => {
|
|
1809
|
-
const parsed = await
|
|
3028
|
+
const parsed = await parseDispatcherBody(c, PromoteFollowUpRequestSchema);
|
|
1810
3029
|
let promoted;
|
|
1811
3030
|
try {
|
|
1812
|
-
promoted = await
|
|
3031
|
+
promoted = await promoteFollowUpRequest({
|
|
1813
3032
|
followUpRequestId: c.req.param("id"),
|
|
1814
3033
|
runId: parsed.runId
|
|
1815
3034
|
});
|
|
@@ -1824,23 +3043,6 @@ function createDispatcherApp(input) {
|
|
|
1824
3043
|
throw error;
|
|
1825
3044
|
}
|
|
1826
3045
|
const followUpRequest = promoted.followUpRequest;
|
|
1827
|
-
const event = followUpRequest.event;
|
|
1828
|
-
if (presentation.shouldDeliverAcknowledgement(event.callback.provider)) {
|
|
1829
|
-
await deliverAndAudit({
|
|
1830
|
-
repo,
|
|
1831
|
-
sink: callbackSink,
|
|
1832
|
-
retry: callbackRetry,
|
|
1833
|
-
message: {
|
|
1834
|
-
runId: promoted.run.id,
|
|
1835
|
-
kind: "acknowledgement",
|
|
1836
|
-
provider: event.callback.provider,
|
|
1837
|
-
uri: event.callback.uri,
|
|
1838
|
-
body: presentation.acknowledgement({ provider: event.callback.provider, runId: promoted.run.id }),
|
|
1839
|
-
...event.target.agentId ? { agentId: event.target.agentId } : {},
|
|
1840
|
-
...event.callback.threadKey ? { threadKey: event.callback.threadKey } : {}
|
|
1841
|
-
}
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
3046
|
return c.json({ followUpRequest, run: promoted.run }, 201);
|
|
1845
3047
|
});
|
|
1846
3048
|
app.post("/v1/runners/:runnerId/claim", async (c) => {
|
|
@@ -1860,13 +3062,67 @@ function createDispatcherApp(input) {
|
|
|
1860
3062
|
}, 410);
|
|
1861
3063
|
});
|
|
1862
3064
|
app.post("/v1/runners/:runnerId/runs/:runId/running", async (c) => {
|
|
1863
|
-
const
|
|
1864
|
-
const
|
|
1865
|
-
|
|
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,
|
|
1866
3071
|
runnerId: c.req.param("runnerId"),
|
|
1867
|
-
executor: body.executor
|
|
3072
|
+
executor: body.executor,
|
|
3073
|
+
...body.runTimeoutMs ? { runTimeoutMs: body.runTimeoutMs } : {},
|
|
3074
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
1868
3075
|
});
|
|
1869
|
-
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
|
+
}
|
|
1870
3126
|
return c.json({ ok: true });
|
|
1871
3127
|
});
|
|
1872
3128
|
app.post("/v1/runs/:runId/progress", async () => {
|
|
@@ -1877,20 +3133,31 @@ function createDispatcherApp(input) {
|
|
|
1877
3133
|
});
|
|
1878
3134
|
app.post("/v1/runners/:runnerId/runs/:runId/progress", async (c) => {
|
|
1879
3135
|
const runId = c.req.param("runId");
|
|
1880
|
-
const body = await
|
|
1881
|
-
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({
|
|
1882
3140
|
runId,
|
|
1883
3141
|
runnerId: c.req.param("runnerId"),
|
|
1884
3142
|
message: body.message,
|
|
1885
3143
|
...body.type ? { type: body.type } : {},
|
|
1886
3144
|
...body.at ? { at: body.at } : {},
|
|
1887
3145
|
...body.visibility ? { visibility: body.visibility } : {},
|
|
1888
|
-
...body.importance ? { importance: body.importance } : {}
|
|
3146
|
+
...body.importance ? { importance: body.importance } : {},
|
|
3147
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
1889
3148
|
});
|
|
1890
|
-
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 });
|
|
1891
3151
|
const stored = await repo.getRun({ runId });
|
|
1892
3152
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1893
|
-
|
|
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
|
+
});
|
|
1894
3161
|
await deliverAndAudit({
|
|
1895
3162
|
repo,
|
|
1896
3163
|
sink: callbackSink,
|
|
@@ -1900,12 +3167,29 @@ function createDispatcherApp(input) {
|
|
|
1900
3167
|
kind: "progress",
|
|
1901
3168
|
provider: stored.event.callback.provider,
|
|
1902
3169
|
uri: stored.event.callback.uri,
|
|
1903
|
-
body:
|
|
3170
|
+
body: progress.body,
|
|
1904
3171
|
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
1905
3172
|
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
3173
|
+
...progress.blocks?.length ? { blocks: progress.blocks } : {},
|
|
3174
|
+
...progress.rich ? { rich: progress.rich } : {},
|
|
1906
3175
|
statusMessageKey: `${runId}:status`
|
|
1907
3176
|
}
|
|
1908
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
|
+
});
|
|
1909
3193
|
}
|
|
1910
3194
|
return c.json({ ok: true });
|
|
1911
3195
|
});
|
|
@@ -1917,12 +3201,64 @@ function createDispatcherApp(input) {
|
|
|
1917
3201
|
});
|
|
1918
3202
|
app.post("/v1/runners/:runnerId/runs/:runId/complete", async (c) => {
|
|
1919
3203
|
const runId = c.req.param("runId");
|
|
1920
|
-
const parsed = await
|
|
1921
|
-
const
|
|
1922
|
-
|
|
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 });
|
|
1923
3215
|
const stored = await repo.getRun({ runId });
|
|
1924
3216
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1925
|
-
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 });
|
|
1926
3262
|
await deliverAndAudit({
|
|
1927
3263
|
repo,
|
|
1928
3264
|
sink: callbackSink,
|
|
@@ -1932,13 +3268,25 @@ function createDispatcherApp(input) {
|
|
|
1932
3268
|
kind: "final",
|
|
1933
3269
|
provider: stored.event.callback.provider,
|
|
1934
3270
|
uri: stored.event.callback.uri,
|
|
1935
|
-
body:
|
|
3271
|
+
body: finalCallback.body,
|
|
1936
3272
|
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
1937
3273
|
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
1938
|
-
...
|
|
3274
|
+
...statusMessageKey ? { statusMessageKey } : {},
|
|
3275
|
+
...finalCallback.blocks?.length ? { blocks: finalCallback.blocks } : {},
|
|
3276
|
+
...finalCallback.rich ? { rich: finalCallback.rich } : {}
|
|
1939
3277
|
}
|
|
1940
3278
|
});
|
|
1941
|
-
|
|
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
|
+
});
|
|
1942
3290
|
});
|
|
1943
3291
|
app.get("/v1/proposals/:proposalId", async (c) => {
|
|
1944
3292
|
const proposal = await repo.getSuggestedChanges({ proposalId: c.req.param("proposalId") });
|
|
@@ -1957,18 +3305,7 @@ function createDispatcherApp(input) {
|
|
|
1957
3305
|
});
|
|
1958
3306
|
app.post("/v1/proposals/:proposalId/approvals", async (c) => {
|
|
1959
3307
|
const proposalId = c.req.param("proposalId");
|
|
1960
|
-
|
|
1961
|
-
try {
|
|
1962
|
-
rawBody = await c.req.json();
|
|
1963
|
-
} catch (err) {
|
|
1964
|
-
if (err instanceof SyntaxError) {
|
|
1965
|
-
return c.json({ error: "invalid_json_body" }, 400);
|
|
1966
|
-
}
|
|
1967
|
-
throw err;
|
|
1968
|
-
}
|
|
1969
|
-
const parsedBody = ApprovalDecisionInputSchema.safeParse(rawBody);
|
|
1970
|
-
if (!parsedBody.success) return c.json({ error: "invalid_approval_decision" }, 400);
|
|
1971
|
-
const body = parsedBody.data;
|
|
3308
|
+
const body = await parseDispatcherBody(c, ApprovalDecisionInputSchema, { invalidBodyError: "invalid_approval_decision" });
|
|
1972
3309
|
const decision = await repo.recordApprovalDecision({
|
|
1973
3310
|
id: body.id ?? `approval_${proposalId}_${Date.now()}`,
|
|
1974
3311
|
proposalId,
|
|
@@ -1990,7 +3327,7 @@ function createDispatcherApp(input) {
|
|
|
1990
3327
|
});
|
|
1991
3328
|
app.post("/v1/proposals/:proposalId/apply-plans", async (c) => {
|
|
1992
3329
|
const proposalId = c.req.param("proposalId");
|
|
1993
|
-
const body = await
|
|
3330
|
+
const body = await parseDispatcherBody(c, ApplyPlanInputSchema);
|
|
1994
3331
|
let executableTarget;
|
|
1995
3332
|
if (body.execute) {
|
|
1996
3333
|
if (body.adapter !== "github") {
|
|
@@ -2084,7 +3421,7 @@ function createDispatcherApp(input) {
|
|
|
2084
3421
|
});
|
|
2085
3422
|
app.post("/v1/runs/:runId/child-runs", async (c) => {
|
|
2086
3423
|
const parentRunId = c.req.param("runId");
|
|
2087
|
-
const body = await
|
|
3424
|
+
const body = await parseDispatcherBody(c, ChildRunInputSchema);
|
|
2088
3425
|
const parent = await repo.getRun({ runId: parentRunId });
|
|
2089
3426
|
if (!parent) return c.json({ error: "parent_run_not_found" }, 404);
|
|
2090
3427
|
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2110,6 +3447,19 @@ function createDispatcherApp(input) {
|
|
|
2110
3447
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
2111
3448
|
return c.json(stored);
|
|
2112
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
|
+
});
|
|
2113
3463
|
app.get("/v1/runs/:runId/metrics", async (c) => {
|
|
2114
3464
|
const runId = c.req.param("runId");
|
|
2115
3465
|
const stored = await repo.getRun({ runId });
|