@opentag/dispatcher 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/callbacks.d.ts +8 -1
- package/dist/callbacks.d.ts.map +1 -1
- package/dist/index.js +288 -39
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -1
- package/package.json +7 -7
package/dist/callbacks.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LarkReplyClient } from "@opentag/lark";
|
|
2
|
-
import type { CallbackSink } from "./server.js";
|
|
2
|
+
import type { CallbackSink, SourceReceiptSink } from "./server.js";
|
|
3
3
|
export type FetchLike = typeof fetch;
|
|
4
4
|
export declare function createGitHubCallbackSink(input: {
|
|
5
5
|
token?: string;
|
|
@@ -10,6 +10,13 @@ export declare function createSlackCallbackSink(input: {
|
|
|
10
10
|
botTokensByAgentId?: Record<string, string>;
|
|
11
11
|
fetchImpl?: FetchLike;
|
|
12
12
|
}): CallbackSink;
|
|
13
|
+
export declare function createSlackSourceReceiptSink(input: {
|
|
14
|
+
botToken?: string;
|
|
15
|
+
botTokensByAgentId?: Record<string, string>;
|
|
16
|
+
fetchImpl?: FetchLike;
|
|
17
|
+
reactionsAddUri?: string;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}): SourceReceiptSink;
|
|
13
20
|
export declare function createLarkCallbackSink(input: {
|
|
14
21
|
appId?: string;
|
|
15
22
|
appSecret?: string;
|
package/dist/callbacks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callbacks.d.ts","sourceRoot":"","sources":["../src/callbacks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,eAAe,EAAwC,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"callbacks.d.ts","sourceRoot":"","sources":["../src/callbacks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,eAAe,EAAwC,MAAM,eAAe,CAAC;AASlH,OAAO,KAAK,EAAmB,YAAY,EAAiB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEnG,MAAM,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC;AAkErC,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG,YAAY,CA+CvG;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,GAAG,YAAY,CA0Df;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,iBAAiB,CAgDpB;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC3B,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B,GAAG,YAAY,CA0Bf;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,GAAG,YAAY,CAyDf;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,YAAY,CAQ/E"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// src/callbacks.ts
|
|
2
2
|
import { createLarkReplyClient, parseLarkThreadKey, replyLarkMessage } from "@opentag/lark";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createSlackPostMessagePayload,
|
|
5
|
+
createSlackReactionPayload,
|
|
6
|
+
createSlackUpdateMessagePayload,
|
|
7
|
+
parseSlackThreadKey,
|
|
8
|
+
slackSourceReceiptReactionName
|
|
9
|
+
} from "@opentag/slack";
|
|
4
10
|
import { createTelegramSendMessageDraftPayload, createTelegramSendMessagePayload, parseTelegramThreadKey } from "@opentag/telegram";
|
|
11
|
+
var DEFAULT_SLACK_SOURCE_RECEIPT_TIMEOUT_MS = 5e3;
|
|
5
12
|
function slackUpdateUriFrom(postMessageUri) {
|
|
6
13
|
return postMessageUri.replace(/\/chat\.postMessage$/, "/chat.update");
|
|
7
14
|
}
|
|
@@ -18,6 +25,31 @@ function slackBotTokenFor(input) {
|
|
|
18
25
|
}
|
|
19
26
|
return input.botToken;
|
|
20
27
|
}
|
|
28
|
+
function metadataString(metadata, key) {
|
|
29
|
+
const value = metadata[key];
|
|
30
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
31
|
+
}
|
|
32
|
+
function slackSourceMessageTarget(receipt) {
|
|
33
|
+
if (receipt.provider !== "slack") return null;
|
|
34
|
+
const channelId = metadataString(receipt.event.metadata, "channelId");
|
|
35
|
+
const messageTs = metadataString(receipt.event.metadata, "messageTs");
|
|
36
|
+
return channelId && messageTs ? { channelId, messageTs } : null;
|
|
37
|
+
}
|
|
38
|
+
function isAbortError(error) {
|
|
39
|
+
return error instanceof Error && error.name === "AbortError";
|
|
40
|
+
}
|
|
41
|
+
async function fetchWithTimeout(input) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
44
|
+
try {
|
|
45
|
+
return await input.fetchImpl(input.uri, { ...input.init, signal: controller.signal });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (isAbortError(error)) return null;
|
|
48
|
+
throw error;
|
|
49
|
+
} finally {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
21
53
|
function createGitHubCallbackSink(input) {
|
|
22
54
|
const fetchImpl = input.fetchImpl ?? fetch;
|
|
23
55
|
const commentUriByKey = /* @__PURE__ */ new Map();
|
|
@@ -70,9 +102,9 @@ function createSlackCallbackSink(input) {
|
|
|
70
102
|
async deliver(message) {
|
|
71
103
|
if (message.provider !== "slack") return;
|
|
72
104
|
const botToken = slackBotTokenFor({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
botToken: input.botToken,
|
|
106
|
+
botTokensByAgentId: input.botTokensByAgentId,
|
|
107
|
+
agentId: message.agentId
|
|
76
108
|
});
|
|
77
109
|
if (!botToken) return;
|
|
78
110
|
const thread = parseSlackThreadKey(message.threadKey ?? "");
|
|
@@ -117,6 +149,51 @@ function createSlackCallbackSink(input) {
|
|
|
117
149
|
}
|
|
118
150
|
};
|
|
119
151
|
}
|
|
152
|
+
function createSlackSourceReceiptSink(input) {
|
|
153
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
154
|
+
const reactionsAddUri = input.reactionsAddUri ?? "https://slack.com/api/reactions.add";
|
|
155
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_SLACK_SOURCE_RECEIPT_TIMEOUT_MS;
|
|
156
|
+
return {
|
|
157
|
+
async deliver(receipt) {
|
|
158
|
+
const target = slackSourceMessageTarget(receipt);
|
|
159
|
+
if (!target) return { delivered: false };
|
|
160
|
+
const botToken = slackBotTokenFor({
|
|
161
|
+
botToken: input.botToken,
|
|
162
|
+
botTokensByAgentId: input.botTokensByAgentId,
|
|
163
|
+
agentId: receipt.agentId
|
|
164
|
+
});
|
|
165
|
+
if (!botToken) return { delivered: false };
|
|
166
|
+
const response = await fetchWithTimeout({
|
|
167
|
+
fetchImpl,
|
|
168
|
+
uri: reactionsAddUri,
|
|
169
|
+
timeoutMs,
|
|
170
|
+
init: {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
authorization: `Bearer ${botToken}`,
|
|
174
|
+
"content-type": "application/json"
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify(
|
|
177
|
+
createSlackReactionPayload({
|
|
178
|
+
channelId: target.channelId,
|
|
179
|
+
messageTs: target.messageTs,
|
|
180
|
+
name: slackSourceReceiptReactionName(receipt.state)
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
if (!response) return { delivered: false };
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new Error(`deliver Slack source receipt failed: ${response.status} ${await response.text()}`);
|
|
188
|
+
}
|
|
189
|
+
const body = await response.json().catch(() => ({}));
|
|
190
|
+
if (body?.ok === false && body.error !== "already_reacted") {
|
|
191
|
+
throw new Error(`deliver Slack source receipt failed: ${body?.error ?? "unknown_error"}`);
|
|
192
|
+
}
|
|
193
|
+
return { delivered: true };
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
120
197
|
function createLarkCallbackSink(input) {
|
|
121
198
|
if (!input.client && Boolean(input.appId) !== Boolean(input.appSecret)) {
|
|
122
199
|
throw new Error("Lark callback sink requires both appId and appSecret (or neither).");
|
|
@@ -144,9 +221,9 @@ function createTelegramCallbackSink(input) {
|
|
|
144
221
|
async deliver(message) {
|
|
145
222
|
if (message.provider !== "telegram") return;
|
|
146
223
|
const botToken = slackBotTokenFor({
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
224
|
+
botToken: input.botToken,
|
|
225
|
+
botTokensByAgentId: input.botTokensByAgentId,
|
|
226
|
+
agentId: message.agentId
|
|
150
227
|
});
|
|
151
228
|
if (!botToken) return;
|
|
152
229
|
const thread = parseTelegramThreadKey(message.threadKey ?? "");
|
|
@@ -206,7 +283,7 @@ import { renderTelegramAcknowledgement, renderTelegramFinalResult, renderTelegra
|
|
|
206
283
|
function createDefaultCallbackPresentation() {
|
|
207
284
|
return {
|
|
208
285
|
shouldDeliverAcknowledgement(provider) {
|
|
209
|
-
return provider !== "lark";
|
|
286
|
+
return provider !== "lark" && provider !== "slack";
|
|
210
287
|
},
|
|
211
288
|
shouldDeliverProgress(provider) {
|
|
212
289
|
return provider !== "slack" && provider !== "lark";
|
|
@@ -272,6 +349,7 @@ import { createOpenTagRepository, migrateSchema } from "@opentag/store";
|
|
|
272
349
|
import Database from "better-sqlite3";
|
|
273
350
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
274
351
|
import { Hono } from "hono";
|
|
352
|
+
import { HTTPException } from "hono/http-exception";
|
|
275
353
|
import { z } from "zod";
|
|
276
354
|
|
|
277
355
|
// src/admission.ts
|
|
@@ -416,6 +494,26 @@ function createAdmissionRuntime(input) {
|
|
|
416
494
|
}
|
|
417
495
|
|
|
418
496
|
// src/server.ts
|
|
497
|
+
async function parseBody(c, schema) {
|
|
498
|
+
let json;
|
|
499
|
+
try {
|
|
500
|
+
json = await c.req.json();
|
|
501
|
+
} catch (err) {
|
|
502
|
+
if (err instanceof SyntaxError) {
|
|
503
|
+
throw new HTTPException(400, {
|
|
504
|
+
res: c.json({ error: "invalid_json_body" }, 400)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
const result = schema.safeParse(json);
|
|
510
|
+
if (!result.success) {
|
|
511
|
+
throw new HTTPException(400, {
|
|
512
|
+
res: c.json({ error: "invalid_request_body", issues: result.error.issues }, 400)
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return result.data;
|
|
516
|
+
}
|
|
419
517
|
var CreateRunnerSchema = z.object({
|
|
420
518
|
runnerId: z.string().min(1),
|
|
421
519
|
name: z.string().min(1)
|
|
@@ -526,7 +624,8 @@ function childEventFromParent(input) {
|
|
|
526
624
|
metadata: {
|
|
527
625
|
...input.parentEvent.metadata,
|
|
528
626
|
...input.metadata ?? {}
|
|
529
|
-
}
|
|
627
|
+
},
|
|
628
|
+
permissions: input.permissions ?? input.parentEvent.permissions
|
|
530
629
|
};
|
|
531
630
|
}
|
|
532
631
|
function mappingsFromAdapterPlan(adapterPlan) {
|
|
@@ -544,13 +643,13 @@ function metadataIssueNumber(metadata) {
|
|
|
544
643
|
if (typeof value === "string" && /^[1-9]\d*$/.test(value)) return value;
|
|
545
644
|
return void 0;
|
|
546
645
|
}
|
|
547
|
-
function
|
|
646
|
+
function metadataString2(metadata, key) {
|
|
548
647
|
const value = metadata?.[key];
|
|
549
648
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
550
649
|
}
|
|
551
650
|
function githubIssueWorkItemExternalId(metadata) {
|
|
552
|
-
const owner =
|
|
553
|
-
const repo =
|
|
651
|
+
const owner = metadataString2(metadata, "owner");
|
|
652
|
+
const repo = metadataString2(metadata, "repo");
|
|
554
653
|
const issueNumber = metadataIssueNumber(metadata);
|
|
555
654
|
if (!owner || !repo || !issueNumber) return void 0;
|
|
556
655
|
return `${owner}/${repo}#${issueNumber}`;
|
|
@@ -807,6 +906,30 @@ function githubTargetFromEvent(event) {
|
|
|
807
906
|
function selectedActionSummary(candidates) {
|
|
808
907
|
return candidates.map((candidate) => `${candidate.index}. ${candidate.intent.summary}`).join("; ");
|
|
809
908
|
}
|
|
909
|
+
function addPermissionGrant(permissions, grant) {
|
|
910
|
+
if (permissions.some((permission) => permission.scope === grant.scope)) return permissions;
|
|
911
|
+
return [...permissions, grant];
|
|
912
|
+
}
|
|
913
|
+
function childRunPermissionsForThreadAction(input) {
|
|
914
|
+
let permissions = [...input.resolved.proposal.event.permissions ?? []];
|
|
915
|
+
if (input.command.verb === "apply" || input.command.verb === "continue") {
|
|
916
|
+
permissions = addPermissionGrant(permissions, {
|
|
917
|
+
scope: "repo:read",
|
|
918
|
+
reason: "inspect the repository while continuing an approved source-thread action"
|
|
919
|
+
});
|
|
920
|
+
permissions = addPermissionGrant(permissions, {
|
|
921
|
+
scope: "repo:write",
|
|
922
|
+
reason: "apply an approved source-thread mutation on a run branch"
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (input.resolved.selectedCandidates.some((candidate) => candidate.intent.action === "create_pull_request")) {
|
|
926
|
+
permissions = addPermissionGrant(permissions, {
|
|
927
|
+
scope: "pr:create",
|
|
928
|
+
reason: "create the pull request approved in the source thread"
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
return permissions;
|
|
932
|
+
}
|
|
810
933
|
function childRunContextLines(input) {
|
|
811
934
|
const previousSummary = input.resolved.proposal.run.result?.summary ?? input.resolved.proposal.snapshot.summary;
|
|
812
935
|
return [
|
|
@@ -820,6 +943,12 @@ function childRunContextLines(input) {
|
|
|
820
943
|
];
|
|
821
944
|
}
|
|
822
945
|
function renderChildRunCreatedBody(input) {
|
|
946
|
+
if (input.provider === "slack") {
|
|
947
|
+
return [
|
|
948
|
+
"Approved. OpenTag will continue from this proposal in a follow-up run.",
|
|
949
|
+
...input.fallbackReason ? [`Reason: ${input.fallbackReason}`] : []
|
|
950
|
+
].join("\n");
|
|
951
|
+
}
|
|
823
952
|
return [
|
|
824
953
|
input.lead,
|
|
825
954
|
"",
|
|
@@ -831,6 +960,44 @@ function renderChildRunCreatedBody(input) {
|
|
|
831
960
|
"The model will continue from this approved proposal instead of starting from a fresh mention."
|
|
832
961
|
].join("\n");
|
|
833
962
|
}
|
|
963
|
+
function renderAppliedThreadActionBody(input) {
|
|
964
|
+
const selectedOutcomes = input.outcomes.filter((outcome) => input.selectedIntentIds.includes(outcome.intentId));
|
|
965
|
+
if (input.provider === "slack") {
|
|
966
|
+
const lines = [`Applied ${input.selectionText}.`];
|
|
967
|
+
for (const outcome of selectedOutcomes) {
|
|
968
|
+
if (outcome.externalUri) {
|
|
969
|
+
lines.push(`Result: ${outcome.externalUri}`);
|
|
970
|
+
} else if (outcome.message) {
|
|
971
|
+
lines.push(`Result: ${outcome.outcome}. ${outcome.message}`);
|
|
972
|
+
} else {
|
|
973
|
+
lines.push(`Result: ${outcome.outcome}.`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return lines.join("\n");
|
|
977
|
+
}
|
|
978
|
+
return [
|
|
979
|
+
`Applied ${input.selectionText} from \`${input.proposalId}\`.`,
|
|
980
|
+
"",
|
|
981
|
+
"Result:",
|
|
982
|
+
...selectedOutcomes.map((outcome) => `- \`${outcome.intentId}\`: ${outcome.outcome}${outcome.externalUri ? ` (${outcome.externalUri})` : ""}`)
|
|
983
|
+
].join("\n");
|
|
984
|
+
}
|
|
985
|
+
function renderThreadActionRecordedBody(input) {
|
|
986
|
+
const pastTense = input.verb === "approve" ? "Approved" : "Rejected";
|
|
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
|
+
}
|
|
994
|
+
if (input.verb === "approve") {
|
|
995
|
+
return `${pastTense} ${input.selectionText} from \`${input.proposalId}\`.
|
|
996
|
+
|
|
997
|
+
Reply with \`apply ${input.applyIndex ?? 1}\` to apply it, or \`continue ${input.applyIndex ?? 1}\` to continue with a follow-up run.`;
|
|
998
|
+
}
|
|
999
|
+
return `${pastTense} ${input.selectionText} from \`${input.proposalId}\`.`;
|
|
1000
|
+
}
|
|
834
1001
|
function actionContextPointer(input) {
|
|
835
1002
|
const lines = [
|
|
836
1003
|
"OpenTag thread action continuation.",
|
|
@@ -895,7 +1062,8 @@ async function createChildRunForThreadAction(input) {
|
|
|
895
1062
|
...input.approvalDecisionId ? { approvalDecisionId: input.approvalDecisionId } : {},
|
|
896
1063
|
...input.sourceApplyPlanId ? { sourceApplyPlanId: input.sourceApplyPlanId } : {},
|
|
897
1064
|
...input.fallbackReason ? { fallbackReason: input.fallbackReason } : {}
|
|
898
|
-
}
|
|
1065
|
+
},
|
|
1066
|
+
permissions: childRunPermissionsForThreadAction({ resolved: input.resolved, command: input.command })
|
|
899
1067
|
}),
|
|
900
1068
|
parentRunId: input.resolved.proposal.runId,
|
|
901
1069
|
triggeredByAction: action,
|
|
@@ -972,6 +1140,11 @@ var noopCallbackSink = {
|
|
|
972
1140
|
return;
|
|
973
1141
|
}
|
|
974
1142
|
};
|
|
1143
|
+
var noopSourceReceiptSink = {
|
|
1144
|
+
async deliver() {
|
|
1145
|
+
return { delivered: false };
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
975
1148
|
function nextCallbackAttemptAt(input) {
|
|
976
1149
|
const maxAttempts = input.maxAttempts ?? 5;
|
|
977
1150
|
const nextAttempt = input.attempts + 1;
|
|
@@ -1049,6 +1222,38 @@ async function deliverAndAudit(input) {
|
|
|
1049
1222
|
...input.retry ? { retry: input.retry } : {}
|
|
1050
1223
|
});
|
|
1051
1224
|
}
|
|
1225
|
+
async function deliverSourceReceiptBestEffort(input) {
|
|
1226
|
+
try {
|
|
1227
|
+
const result = await input.sink.deliver(input.receipt);
|
|
1228
|
+
if (!result.delivered) return result;
|
|
1229
|
+
await input.repo.appendRunEvent({
|
|
1230
|
+
runId: input.receipt.runId,
|
|
1231
|
+
type: "source_receipt.delivered",
|
|
1232
|
+
payload: {
|
|
1233
|
+
provider: input.receipt.provider,
|
|
1234
|
+
state: input.receipt.state
|
|
1235
|
+
},
|
|
1236
|
+
visibility: "audit",
|
|
1237
|
+
importance: "low",
|
|
1238
|
+
message: `Source ${input.receipt.state} receipt delivered.`
|
|
1239
|
+
});
|
|
1240
|
+
return result;
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
await input.repo.appendRunEvent({
|
|
1243
|
+
runId: input.receipt.runId,
|
|
1244
|
+
type: "source_receipt.failed",
|
|
1245
|
+
payload: {
|
|
1246
|
+
provider: input.receipt.provider,
|
|
1247
|
+
state: input.receipt.state,
|
|
1248
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1249
|
+
},
|
|
1250
|
+
visibility: "audit",
|
|
1251
|
+
importance: "low",
|
|
1252
|
+
message: `Source ${input.receipt.state} receipt failed.`
|
|
1253
|
+
});
|
|
1254
|
+
return { delivered: false };
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1052
1257
|
function isAuthorized(request, pairingToken) {
|
|
1053
1258
|
if (!pairingToken) return true;
|
|
1054
1259
|
return request.headers.get("authorization") === `Bearer ${pairingToken}`;
|
|
@@ -1059,6 +1264,7 @@ function createDispatcherApp(input) {
|
|
|
1059
1264
|
const repo = createOpenTagRepository(drizzle(sqlite));
|
|
1060
1265
|
const app = new Hono();
|
|
1061
1266
|
const callbackSink = input.callbackSink ?? noopCallbackSink;
|
|
1267
|
+
const sourceReceiptSink = input.sourceReceiptSink ?? noopSourceReceiptSink;
|
|
1062
1268
|
const presentation = input.presentation ?? createDefaultCallbackPresentation();
|
|
1063
1269
|
const callbackRetry = input.callbackRetry ?? {};
|
|
1064
1270
|
const admission = createAdmissionRuntime({
|
|
@@ -1073,7 +1279,7 @@ function createDispatcherApp(input) {
|
|
|
1073
1279
|
await next();
|
|
1074
1280
|
});
|
|
1075
1281
|
app.post("/v1/runners", async (c) => {
|
|
1076
|
-
const parsed =
|
|
1282
|
+
const parsed = await parseBody(c, CreateRunnerSchema);
|
|
1077
1283
|
await repo.registerRunner(parsed);
|
|
1078
1284
|
return c.json({ ok: true }, 201);
|
|
1079
1285
|
});
|
|
@@ -1083,7 +1289,7 @@ function createDispatcherApp(input) {
|
|
|
1083
1289
|
return c.json({ runner });
|
|
1084
1290
|
});
|
|
1085
1291
|
app.post("/v1/repo-bindings", async (c) => {
|
|
1086
|
-
const parsed =
|
|
1292
|
+
const parsed = await parseBody(c, CreateRepoBindingSchema);
|
|
1087
1293
|
await repo.createRepoBinding({
|
|
1088
1294
|
provider: parsed.provider,
|
|
1089
1295
|
owner: parsed.owner,
|
|
@@ -1105,7 +1311,7 @@ function createDispatcherApp(input) {
|
|
|
1105
1311
|
return c.json({ binding });
|
|
1106
1312
|
});
|
|
1107
1313
|
app.post("/v1/repo-bindings/:provider/:owner/:repo/policy-rules", async (c) => {
|
|
1108
|
-
const parsed =
|
|
1314
|
+
const parsed = await parseBody(c, UpsertPolicyRuleSchema);
|
|
1109
1315
|
const rule = await repo.upsertRepoPolicyRule({
|
|
1110
1316
|
provider: c.req.param("provider"),
|
|
1111
1317
|
owner: c.req.param("owner"),
|
|
@@ -1123,7 +1329,7 @@ function createDispatcherApp(input) {
|
|
|
1123
1329
|
return c.json({ rules });
|
|
1124
1330
|
});
|
|
1125
1331
|
app.post("/v1/repo-bindings/:provider/:owner/:repo/mutation-mappings", async (c) => {
|
|
1126
|
-
const parsed =
|
|
1332
|
+
const parsed = await parseBody(c, UpsertMutationMappingSchema);
|
|
1127
1333
|
const mapping = await repo.upsertRepoMutationMapping({
|
|
1128
1334
|
provider: c.req.param("provider"),
|
|
1129
1335
|
owner: c.req.param("owner"),
|
|
@@ -1155,7 +1361,7 @@ function createDispatcherApp(input) {
|
|
|
1155
1361
|
return c.json({ metrics });
|
|
1156
1362
|
});
|
|
1157
1363
|
app.post("/v1/channel-bindings", async (c) => {
|
|
1158
|
-
const parsed =
|
|
1364
|
+
const parsed = await parseBody(c, CreateChannelBindingSchema);
|
|
1159
1365
|
await repo.upsertChannelBinding({
|
|
1160
1366
|
provider: parsed.provider,
|
|
1161
1367
|
accountId: parsed.accountId,
|
|
@@ -1177,7 +1383,7 @@ function createDispatcherApp(input) {
|
|
|
1177
1383
|
return c.json({ binding });
|
|
1178
1384
|
});
|
|
1179
1385
|
app.post("/v1/slack-channel-bindings", async (c) => {
|
|
1180
|
-
const parsed =
|
|
1386
|
+
const parsed = await parseBody(c, CreateSlackChannelBindingSchema);
|
|
1181
1387
|
await repo.createSlackChannelBinding(parsed);
|
|
1182
1388
|
return c.json({ ok: true }, 201);
|
|
1183
1389
|
});
|
|
@@ -1190,7 +1396,7 @@ function createDispatcherApp(input) {
|
|
|
1190
1396
|
return c.json({ binding });
|
|
1191
1397
|
});
|
|
1192
1398
|
app.post("/v1/runs", async (c) => {
|
|
1193
|
-
const parsed =
|
|
1399
|
+
const parsed = await parseBody(c, CreateRunSchema);
|
|
1194
1400
|
const admitted = await admission.admitRun({ requestId: parsed.runId, event: parsed.event });
|
|
1195
1401
|
if (admitted.outcome === "needs_human_decision") {
|
|
1196
1402
|
return c.json({ decision: admitted.decision }, 202);
|
|
@@ -1234,7 +1440,19 @@ function createDispatcherApp(input) {
|
|
|
1234
1440
|
);
|
|
1235
1441
|
}
|
|
1236
1442
|
const { run } = createdRun;
|
|
1237
|
-
|
|
1443
|
+
const sourceReceiptDelivery = await deliverSourceReceiptBestEffort({
|
|
1444
|
+
repo,
|
|
1445
|
+
sink: sourceReceiptSink,
|
|
1446
|
+
receipt: {
|
|
1447
|
+
runId: run.id,
|
|
1448
|
+
provider: parsed.event.callback.provider,
|
|
1449
|
+
state: "received",
|
|
1450
|
+
event: parsed.event,
|
|
1451
|
+
...parsed.event.target.agentId ? { agentId: parsed.event.target.agentId } : {}
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
const shouldDeliverAcknowledgement = presentation.shouldDeliverAcknowledgement(parsed.event.callback.provider) || parsed.event.callback.provider === "slack" && !sourceReceiptDelivery.delivered;
|
|
1455
|
+
if (shouldDeliverAcknowledgement) {
|
|
1238
1456
|
await deliverAndAudit({
|
|
1239
1457
|
repo,
|
|
1240
1458
|
sink: callbackSink,
|
|
@@ -1253,7 +1471,7 @@ function createDispatcherApp(input) {
|
|
|
1253
1471
|
return c.json({ decision: admitted.decision, run }, 201);
|
|
1254
1472
|
});
|
|
1255
1473
|
app.post("/v1/thread-actions", async (c) => {
|
|
1256
|
-
const parsed =
|
|
1474
|
+
const parsed = await parseBody(c, ThreadActionInputSchema);
|
|
1257
1475
|
const command = parseThreadActionCommand(parsed.rawText);
|
|
1258
1476
|
if (!command) {
|
|
1259
1477
|
return c.json({ outcome: "ignored", reason: "not_action_command" }, 202);
|
|
@@ -1335,6 +1553,7 @@ function createDispatcherApp(input) {
|
|
|
1335
1553
|
lead: "This action was already planned, so OpenTag will not execute the external write again.",
|
|
1336
1554
|
resolved: resolved.resolved,
|
|
1337
1555
|
childRun: childRun2,
|
|
1556
|
+
provider: parsed.callback.provider,
|
|
1338
1557
|
approvalDecisionId: existingPlan.approvalDecisionId,
|
|
1339
1558
|
sourceApplyPlanId: existingPlan.id,
|
|
1340
1559
|
fallbackReason
|
|
@@ -1383,7 +1602,12 @@ function createDispatcherApp(input) {
|
|
|
1383
1602
|
if (existingDecision) {
|
|
1384
1603
|
return c.json({ outcome: "already_rejected", decision }, 200);
|
|
1385
1604
|
}
|
|
1386
|
-
const body2 =
|
|
1605
|
+
const body2 = renderThreadActionRecordedBody({
|
|
1606
|
+
provider: parsed.callback.provider,
|
|
1607
|
+
verb: "reject",
|
|
1608
|
+
selectionText,
|
|
1609
|
+
proposalId: resolved.resolved.proposal.snapshot.proposalId
|
|
1610
|
+
});
|
|
1387
1611
|
await deliverAndAudit({
|
|
1388
1612
|
repo,
|
|
1389
1613
|
sink: callbackSink,
|
|
@@ -1403,9 +1627,13 @@ function createDispatcherApp(input) {
|
|
|
1403
1627
|
if (existingDecision) {
|
|
1404
1628
|
return c.json({ outcome: "already_approved", decision }, 200);
|
|
1405
1629
|
}
|
|
1406
|
-
const body2 =
|
|
1407
|
-
|
|
1408
|
-
|
|
1630
|
+
const body2 = renderThreadActionRecordedBody({
|
|
1631
|
+
provider: parsed.callback.provider,
|
|
1632
|
+
verb: "approve",
|
|
1633
|
+
selectionText,
|
|
1634
|
+
proposalId: resolved.resolved.proposal.snapshot.proposalId,
|
|
1635
|
+
applyIndex: resolved.resolved.selectedCandidates[0]?.index ?? 1
|
|
1636
|
+
});
|
|
1409
1637
|
await deliverAndAudit({
|
|
1410
1638
|
repo,
|
|
1411
1639
|
sink: callbackSink,
|
|
@@ -1433,6 +1661,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1433
1661
|
lead: `Continuing from ${selectionText} in \`${resolved.resolved.proposal.snapshot.proposalId}\`.`,
|
|
1434
1662
|
resolved: resolved.resolved,
|
|
1435
1663
|
childRun: childRun2,
|
|
1664
|
+
provider: parsed.callback.provider,
|
|
1436
1665
|
approvalDecisionId: decision.id
|
|
1437
1666
|
});
|
|
1438
1667
|
await deliverAndAudit({
|
|
@@ -1492,6 +1721,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1492
1721
|
lead: "This action was already planned, so OpenTag will not execute the external write again.",
|
|
1493
1722
|
resolved: resolved.resolved,
|
|
1494
1723
|
childRun: childRun2,
|
|
1724
|
+
provider: parsed.callback.provider,
|
|
1495
1725
|
approvalDecisionId: decision.id,
|
|
1496
1726
|
sourceApplyPlanId: planResult.plan.id,
|
|
1497
1727
|
fallbackReason
|
|
@@ -1510,12 +1740,13 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1510
1740
|
});
|
|
1511
1741
|
if (execution.executed) {
|
|
1512
1742
|
const outcomes = execution.plan.outcomes ?? [];
|
|
1513
|
-
const body2 =
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1743
|
+
const body2 = renderAppliedThreadActionBody({
|
|
1744
|
+
provider: parsed.callback.provider,
|
|
1745
|
+
selectionText,
|
|
1746
|
+
proposalId: resolved.resolved.proposal.snapshot.proposalId,
|
|
1747
|
+
selectedIntentIds: resolved.resolved.selectedIntentIds,
|
|
1748
|
+
outcomes
|
|
1749
|
+
});
|
|
1519
1750
|
await deliverAndAudit({
|
|
1520
1751
|
repo,
|
|
1521
1752
|
sink: callbackSink,
|
|
@@ -1549,6 +1780,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1549
1780
|
lead: `Action ${selectionText} was approved, but OpenTag cannot directly apply it yet.`,
|
|
1550
1781
|
resolved: resolved.resolved,
|
|
1551
1782
|
childRun,
|
|
1783
|
+
provider: parsed.callback.provider,
|
|
1552
1784
|
approvalDecisionId: decision.id,
|
|
1553
1785
|
sourceApplyPlanId: execution.plan.id,
|
|
1554
1786
|
fallbackReason: execution.fallbackReason ?? "The adapter could not execute the selected intent."
|
|
@@ -1574,7 +1806,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1574
1806
|
return c.json({ followUpRequest });
|
|
1575
1807
|
});
|
|
1576
1808
|
app.post("/v1/follow-up-requests/:id/create-run", async (c) => {
|
|
1577
|
-
const parsed =
|
|
1809
|
+
const parsed = await parseBody(c, PromoteFollowUpRequestSchema);
|
|
1578
1810
|
let promoted;
|
|
1579
1811
|
try {
|
|
1580
1812
|
promoted = await repo.createRunFromFollowUpRequest({
|
|
@@ -1628,7 +1860,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1628
1860
|
}, 410);
|
|
1629
1861
|
});
|
|
1630
1862
|
app.post("/v1/runners/:runnerId/runs/:runId/running", async (c) => {
|
|
1631
|
-
const body = z.object({ executor: z.string().min(1) })
|
|
1863
|
+
const body = await parseBody(c, z.object({ executor: z.string().min(1) }));
|
|
1632
1864
|
const ok = await repo.markRunning({
|
|
1633
1865
|
runId: c.req.param("runId"),
|
|
1634
1866
|
runnerId: c.req.param("runnerId"),
|
|
@@ -1645,7 +1877,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1645
1877
|
});
|
|
1646
1878
|
app.post("/v1/runners/:runnerId/runs/:runId/progress", async (c) => {
|
|
1647
1879
|
const runId = c.req.param("runId");
|
|
1648
|
-
const body =
|
|
1880
|
+
const body = await parseBody(c, ProgressSchema);
|
|
1649
1881
|
const ok = await repo.recordProgress({
|
|
1650
1882
|
runId,
|
|
1651
1883
|
runnerId: c.req.param("runnerId"),
|
|
@@ -1685,7 +1917,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1685
1917
|
});
|
|
1686
1918
|
app.post("/v1/runners/:runnerId/runs/:runId/complete", async (c) => {
|
|
1687
1919
|
const runId = c.req.param("runId");
|
|
1688
|
-
const parsed =
|
|
1920
|
+
const parsed = await parseBody(c, CompleteRunSchema);
|
|
1689
1921
|
const ok = await repo.completeRun({ runId, runnerId: c.req.param("runnerId"), result: parsed.result });
|
|
1690
1922
|
if (!ok) return c.json({ error: "run_not_claimed_by_runner" }, 404);
|
|
1691
1923
|
const stored = await repo.getRun({ runId });
|
|
@@ -1725,7 +1957,16 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1725
1957
|
});
|
|
1726
1958
|
app.post("/v1/proposals/:proposalId/approvals", async (c) => {
|
|
1727
1959
|
const proposalId = c.req.param("proposalId");
|
|
1728
|
-
|
|
1960
|
+
let rawBody;
|
|
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);
|
|
1729
1970
|
if (!parsedBody.success) return c.json({ error: "invalid_approval_decision" }, 400);
|
|
1730
1971
|
const body = parsedBody.data;
|
|
1731
1972
|
const decision = await repo.recordApprovalDecision({
|
|
@@ -1749,7 +1990,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1749
1990
|
});
|
|
1750
1991
|
app.post("/v1/proposals/:proposalId/apply-plans", async (c) => {
|
|
1751
1992
|
const proposalId = c.req.param("proposalId");
|
|
1752
|
-
const body =
|
|
1993
|
+
const body = await parseBody(c, ApplyPlanInputSchema);
|
|
1753
1994
|
let executableTarget;
|
|
1754
1995
|
if (body.execute) {
|
|
1755
1996
|
if (body.adapter !== "github") {
|
|
@@ -1843,7 +2084,7 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1843
2084
|
});
|
|
1844
2085
|
app.post("/v1/runs/:runId/child-runs", async (c) => {
|
|
1845
2086
|
const parentRunId = c.req.param("runId");
|
|
1846
|
-
const body =
|
|
2087
|
+
const body = await parseBody(c, ChildRunInputSchema);
|
|
1847
2088
|
const parent = await repo.getRun({ runId: parentRunId });
|
|
1848
2089
|
if (!parent) return c.json({ error: "parent_run_not_found" }, 404);
|
|
1849
2090
|
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1880,6 +2121,13 @@ Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to a
|
|
|
1880
2121
|
const events = await repo.listRunEvents({ runId: c.req.param("runId") });
|
|
1881
2122
|
return c.json({ events });
|
|
1882
2123
|
});
|
|
2124
|
+
app.onError((err, c) => {
|
|
2125
|
+
if (err instanceof HTTPException) {
|
|
2126
|
+
return err.getResponse();
|
|
2127
|
+
}
|
|
2128
|
+
console.error("dispatcher unhandled error", err);
|
|
2129
|
+
return c.json({ error: "internal_server_error" }, 500);
|
|
2130
|
+
});
|
|
1883
2131
|
return app;
|
|
1884
2132
|
}
|
|
1885
2133
|
export {
|
|
@@ -1889,6 +2137,7 @@ export {
|
|
|
1889
2137
|
createGitHubCallbackSink,
|
|
1890
2138
|
createLarkCallbackSink,
|
|
1891
2139
|
createSlackCallbackSink,
|
|
2140
|
+
createSlackSourceReceiptSink,
|
|
1892
2141
|
createTelegramCallbackSink,
|
|
1893
2142
|
processPendingCallbacks
|
|
1894
2143
|
};
|