@open-everywhere/provider-discord 0.1.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.
Files changed (166) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +47 -0
  4. package/agent/access/discord-grant-click.ts +237 -0
  5. package/agent/approvals/discord-admin.ts +227 -0
  6. package/agent/approvals/discord-card.ts +248 -0
  7. package/agent/approvals/discord-flow.ts +469 -0
  8. package/agent/approvals/discord-scope-label.ts +27 -0
  9. package/agent/channels/discord-approval-card.ts +373 -0
  10. package/agent/channels/discord-backscroll.ts +593 -0
  11. package/agent/channels/discord-bot-name.ts +113 -0
  12. package/agent/channels/discord-channel-info.ts +83 -0
  13. package/agent/channels/discord-directory.ts +179 -0
  14. package/agent/channels/discord-dispatch-target.ts +103 -0
  15. package/agent/channels/discord-events.ts +175 -0
  16. package/agent/channels/discord-front.ts +355 -0
  17. package/agent/channels/discord-gateway.ts +3256 -0
  18. package/agent/channels/discord-identity.ts +269 -0
  19. package/agent/channels/discord-interaction-defer.ts +123 -0
  20. package/agent/channels/discord-message-ops.ts +127 -0
  21. package/agent/channels/discord-origin-codec.ts +105 -0
  22. package/agent/channels/discord-post-arm.ts +230 -0
  23. package/agent/channels/discord-post.ts +463 -0
  24. package/agent/channels/discord-read.ts +140 -0
  25. package/agent/channels/discord-rest.ts +85 -0
  26. package/agent/channels/discord.ts +244 -0
  27. package/agent/commands/discord-command-sync.ts +285 -0
  28. package/agent/commands/discord-commands.ts +291 -0
  29. package/agent/contributions.ts +31 -0
  30. package/agent/docs-generated.ts +15 -0
  31. package/agent/identity/discord-link-consent.ts +124 -0
  32. package/agent/ops.ts +14 -0
  33. package/agent/provider-events.ts +86 -0
  34. package/agent/register.ts +147 -0
  35. package/agent/surface-parity.ts +95 -0
  36. package/dist/agent/access/discord-grant-click.d.ts +25 -0
  37. package/dist/agent/access/discord-grant-click.d.ts.map +1 -0
  38. package/dist/agent/access/discord-grant-click.js +210 -0
  39. package/dist/agent/access/discord-grant-click.js.map +1 -0
  40. package/dist/agent/approvals/discord-admin.d.ts +63 -0
  41. package/dist/agent/approvals/discord-admin.d.ts.map +1 -0
  42. package/dist/agent/approvals/discord-admin.js +219 -0
  43. package/dist/agent/approvals/discord-admin.js.map +1 -0
  44. package/dist/agent/approvals/discord-card.d.ts +102 -0
  45. package/dist/agent/approvals/discord-card.d.ts.map +1 -0
  46. package/dist/agent/approvals/discord-card.js +217 -0
  47. package/dist/agent/approvals/discord-card.js.map +1 -0
  48. package/dist/agent/approvals/discord-flow.d.ts +109 -0
  49. package/dist/agent/approvals/discord-flow.d.ts.map +1 -0
  50. package/dist/agent/approvals/discord-flow.js +375 -0
  51. package/dist/agent/approvals/discord-flow.js.map +1 -0
  52. package/dist/agent/approvals/discord-scope-label.d.ts +3 -0
  53. package/dist/agent/approvals/discord-scope-label.d.ts.map +1 -0
  54. package/dist/agent/approvals/discord-scope-label.js +27 -0
  55. package/dist/agent/approvals/discord-scope-label.js.map +1 -0
  56. package/dist/agent/channels/discord-approval-card.d.ts +81 -0
  57. package/dist/agent/channels/discord-approval-card.d.ts.map +1 -0
  58. package/dist/agent/channels/discord-approval-card.js +303 -0
  59. package/dist/agent/channels/discord-approval-card.js.map +1 -0
  60. package/dist/agent/channels/discord-backscroll.d.ts +209 -0
  61. package/dist/agent/channels/discord-backscroll.d.ts.map +1 -0
  62. package/dist/agent/channels/discord-backscroll.js +505 -0
  63. package/dist/agent/channels/discord-backscroll.js.map +1 -0
  64. package/dist/agent/channels/discord-bot-name.d.ts +19 -0
  65. package/dist/agent/channels/discord-bot-name.d.ts.map +1 -0
  66. package/dist/agent/channels/discord-bot-name.js +98 -0
  67. package/dist/agent/channels/discord-bot-name.js.map +1 -0
  68. package/dist/agent/channels/discord-channel-info.d.ts +22 -0
  69. package/dist/agent/channels/discord-channel-info.d.ts.map +1 -0
  70. package/dist/agent/channels/discord-channel-info.js +65 -0
  71. package/dist/agent/channels/discord-channel-info.js.map +1 -0
  72. package/dist/agent/channels/discord-directory.d.ts +11 -0
  73. package/dist/agent/channels/discord-directory.d.ts.map +1 -0
  74. package/dist/agent/channels/discord-directory.js +170 -0
  75. package/dist/agent/channels/discord-directory.js.map +1 -0
  76. package/dist/agent/channels/discord-dispatch-target.d.ts +15 -0
  77. package/dist/agent/channels/discord-dispatch-target.d.ts.map +1 -0
  78. package/dist/agent/channels/discord-dispatch-target.js +99 -0
  79. package/dist/agent/channels/discord-dispatch-target.js.map +1 -0
  80. package/dist/agent/channels/discord-events.d.ts +58 -0
  81. package/dist/agent/channels/discord-events.d.ts.map +1 -0
  82. package/dist/agent/channels/discord-events.js +150 -0
  83. package/dist/agent/channels/discord-events.js.map +1 -0
  84. package/dist/agent/channels/discord-front.d.ts +115 -0
  85. package/dist/agent/channels/discord-front.d.ts.map +1 -0
  86. package/dist/agent/channels/discord-front.js +279 -0
  87. package/dist/agent/channels/discord-front.js.map +1 -0
  88. package/dist/agent/channels/discord-gateway.d.ts +783 -0
  89. package/dist/agent/channels/discord-gateway.d.ts.map +1 -0
  90. package/dist/agent/channels/discord-gateway.js +2591 -0
  91. package/dist/agent/channels/discord-gateway.js.map +1 -0
  92. package/dist/agent/channels/discord-identity.d.ts +110 -0
  93. package/dist/agent/channels/discord-identity.d.ts.map +1 -0
  94. package/dist/agent/channels/discord-identity.js +201 -0
  95. package/dist/agent/channels/discord-identity.js.map +1 -0
  96. package/dist/agent/channels/discord-interaction-defer.d.ts +33 -0
  97. package/dist/agent/channels/discord-interaction-defer.d.ts.map +1 -0
  98. package/dist/agent/channels/discord-interaction-defer.js +102 -0
  99. package/dist/agent/channels/discord-interaction-defer.js.map +1 -0
  100. package/dist/agent/channels/discord-message-ops.d.ts +3 -0
  101. package/dist/agent/channels/discord-message-ops.d.ts.map +1 -0
  102. package/dist/agent/channels/discord-message-ops.js +123 -0
  103. package/dist/agent/channels/discord-message-ops.js.map +1 -0
  104. package/dist/agent/channels/discord-origin-codec.d.ts +7 -0
  105. package/dist/agent/channels/discord-origin-codec.d.ts.map +1 -0
  106. package/dist/agent/channels/discord-origin-codec.js +98 -0
  107. package/dist/agent/channels/discord-origin-codec.js.map +1 -0
  108. package/dist/agent/channels/discord-post-arm.d.ts +3 -0
  109. package/dist/agent/channels/discord-post-arm.d.ts.map +1 -0
  110. package/dist/agent/channels/discord-post-arm.js +195 -0
  111. package/dist/agent/channels/discord-post-arm.js.map +1 -0
  112. package/dist/agent/channels/discord-post.d.ts +120 -0
  113. package/dist/agent/channels/discord-post.d.ts.map +1 -0
  114. package/dist/agent/channels/discord-post.js +426 -0
  115. package/dist/agent/channels/discord-post.js.map +1 -0
  116. package/dist/agent/channels/discord-read.d.ts +40 -0
  117. package/dist/agent/channels/discord-read.d.ts.map +1 -0
  118. package/dist/agent/channels/discord-read.js +128 -0
  119. package/dist/agent/channels/discord-read.js.map +1 -0
  120. package/dist/agent/channels/discord-rest.d.ts +33 -0
  121. package/dist/agent/channels/discord-rest.d.ts.map +1 -0
  122. package/dist/agent/channels/discord-rest.js +75 -0
  123. package/dist/agent/channels/discord-rest.js.map +1 -0
  124. package/dist/agent/channels/discord.d.ts +3 -0
  125. package/dist/agent/channels/discord.d.ts.map +1 -0
  126. package/dist/agent/channels/discord.js +230 -0
  127. package/dist/agent/channels/discord.js.map +1 -0
  128. package/dist/agent/commands/discord-command-sync.d.ts +66 -0
  129. package/dist/agent/commands/discord-command-sync.d.ts.map +1 -0
  130. package/dist/agent/commands/discord-command-sync.js +224 -0
  131. package/dist/agent/commands/discord-command-sync.js.map +1 -0
  132. package/dist/agent/commands/discord-commands.d.ts +45 -0
  133. package/dist/agent/commands/discord-commands.d.ts.map +1 -0
  134. package/dist/agent/commands/discord-commands.js +266 -0
  135. package/dist/agent/commands/discord-commands.js.map +1 -0
  136. package/dist/agent/contributions.d.ts +17 -0
  137. package/dist/agent/contributions.d.ts.map +1 -0
  138. package/dist/agent/contributions.js +31 -0
  139. package/dist/agent/contributions.js.map +1 -0
  140. package/dist/agent/docs-generated.d.ts +3 -0
  141. package/dist/agent/docs-generated.d.ts.map +1 -0
  142. package/dist/agent/docs-generated.js +13 -0
  143. package/dist/agent/docs-generated.js.map +1 -0
  144. package/dist/agent/identity/discord-link-consent.d.ts +13 -0
  145. package/dist/agent/identity/discord-link-consent.d.ts.map +1 -0
  146. package/dist/agent/identity/discord-link-consent.js +111 -0
  147. package/dist/agent/identity/discord-link-consent.js.map +1 -0
  148. package/dist/agent/ops.d.ts +2 -0
  149. package/dist/agent/ops.d.ts.map +1 -0
  150. package/dist/agent/ops.js +13 -0
  151. package/dist/agent/ops.js.map +1 -0
  152. package/dist/agent/provider-events.d.ts +3 -0
  153. package/dist/agent/provider-events.d.ts.map +1 -0
  154. package/dist/agent/provider-events.js +78 -0
  155. package/dist/agent/provider-events.js.map +1 -0
  156. package/dist/agent/register.d.ts +5 -0
  157. package/dist/agent/register.d.ts.map +1 -0
  158. package/dist/agent/register.js +125 -0
  159. package/dist/agent/register.js.map +1 -0
  160. package/dist/agent/surface-parity.d.ts +3 -0
  161. package/dist/agent/surface-parity.d.ts.map +1 -0
  162. package/dist/agent/surface-parity.js +90 -0
  163. package/dist/agent/surface-parity.js.map +1 -0
  164. package/package.json +185 -0
  165. package/tsconfig.build.json +22 -0
  166. package/tsconfig.json +15 -0
@@ -0,0 +1,128 @@
1
+ // The Discord read adapter — EXTRACTED from core channels/channel-read.ts (OPE-114 stage 4). A real
2
+ // `GET /channels/{id}/messages` (bot token) mapped through linesFromDiscordMessages into the neutral
3
+ // RenderedLine[]; registered via registerReadAdapter from registerDiscordProvider(). Discord threads
4
+ // ARE their own channel ids (one endpoint covers a base channel AND a thread — see
5
+ // discord-backscroll.ts), so readThread reads the thread's OWN id, carried in `thread_ts` (the
6
+ // generic verb's per-surface "thread identifier"). The generic per-surface allowlist gate
7
+ // (assertSurfaceAllowed) is SHARED from core (barrel) — it is platform-parameterised.
8
+ //
9
+ // The bot token is a SECRET — header only, never logged. Fail-LOUD (unlike fail-soft backscroll
10
+ // enrichment): a read must surface its error. The requester is injectable for offline tests.
11
+ import { assertSurfaceAllowed, bindFrom, resolvePartition, refutedReadAuthorIds, } from "@open-everywhere/core/agent/adapter-api.js";
12
+ import { linesFromDiscordMessages } from "./discord-backscroll.js";
13
+ import { discordAuthorRoles } from "../approvals/discord-admin.js";
14
+ const DISCORD_CHANNEL_DEFAULT_LIMIT = 50;
15
+ const DISCORD_THREAD_DEFAULT_LIMIT = 100;
16
+ const DISCORD_MAX_PAGE = 100;
17
+ const liveDiscordRead = async (channelId, limit) => {
18
+ const token = process.env.DISCORD_BOT_TOKEN;
19
+ if (!token)
20
+ throw new Error("read: DISCORD_BOT_TOKEN is not set — the Discord read tools need the bot token.");
21
+ const qs = new URLSearchParams({ limit: String(Math.min(limit, DISCORD_MAX_PAGE)) }).toString();
22
+ const res = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages?${qs}`, {
23
+ headers: { authorization: `Bot ${token}` },
24
+ });
25
+ if (!res.ok)
26
+ throw new Error(`read: Discord GET messages failed (HTTP ${res.status}) for channel ${channelId}.`);
27
+ return res.json();
28
+ };
29
+ /**
30
+ * Map a raw Discord messages array (newest-first) to oldest-first RenderedLine[] — reusing
31
+ * linesFromDiscordMessages for the exact system/empty drop + sender classification. Pure; exported
32
+ * for the render test.
33
+ *
34
+ * `author` IS UNCHANGED (the sender id, else `(unknown)`). OPE-474 adds the two GRADING fields
35
+ * beside it, taken from the sender classification linesFromDiscordMessages already made: a `user`
36
+ * line carries its `authorId`, and a `bot`/`agent` line is marked a machine and drops out of the
37
+ * probe. An `unknown` line carries neither, which is the fail-open answer for an author nobody can
38
+ * name.
39
+ */
40
+ export function discordLinesFromRaw(raw) {
41
+ if (!Array.isArray(raw))
42
+ return [];
43
+ const ordered = [...raw].reverse(); // Discord: newest-first → oldest-first
44
+ return linesFromDiscordMessages(ordered).map((l) => ({
45
+ author: l.senderId ?? "(unknown)",
46
+ ts: l.ts,
47
+ text: l.text,
48
+ ...(l.senderType === "bot" || l.senderType === "agent"
49
+ ? { bot: true }
50
+ : l.senderType === "user" && l.senderId !== undefined
51
+ ? { authorId: l.senderId }
52
+ : {}),
53
+ }));
54
+ }
55
+ // --- the OPE-474 non-member author leg ---------------------------------------------------------
56
+ //
57
+ // 🔴 DISCORD CANNOT REFUTE AN AUTHOR ITSELF, at all. It has no organisation concept — guild
58
+ // membership IS the boundary, and a community guild admits the world — so the ONLY author-grain
59
+ // evidence here is the operator's author-trust register (identity/author-trust.ts, OPE-516). This is
60
+ // the same conclusion discord-backscroll.ts's `gradeDiscordContextLines` reached for the wake leg,
61
+ // and this adapter deliberately reproduces its shape rather than a second one.
62
+ //
63
+ // `member.pending` — the trigger's own screening signal — is NOT available here either, and the
64
+ // reason is structural rather than an omission: it rides the gateway relay's MESSAGE_CREATE member
65
+ // object, and Discord's documented Message object (what `GET /channels/{id}/messages` returns) has
66
+ // no `member` field at all.
67
+ //
68
+ // THE ROLE PROBE IS COST-GATED THEN FAIL-OPEN, in that order — {@link refutedReadAuthorIds} runs the
69
+ // gate and this supplies the probe:
70
+ // 1. UNARMED guild (the default posture): no register query is worth building, so NO role fetch
71
+ // runs. Zero new I/O.
72
+ // 2. ARMED guild: each DISTINCT author gets ONE TTL-cached {@link discordAuthorRoles} call.
73
+ // 3. `undefined` from the probe means CANNOT CHECK, and the join skips that author entirely —
74
+ // never an unverified `[]`. On a guild armed the documented-usual way
75
+ // (`{ membership: "discord/<g>/role:*" }`) an unverified empty array would make every author
76
+ // look role-less and therefore `external`, which marked ordinary colleagues' messages. That
77
+ // exact bug was caught in OPE-475; the contract that prevents it is in read-author-trust.ts.
78
+ export const discordReadAdapter = {
79
+ platform: "discord",
80
+ /**
81
+ * OPE-929 — Discord's own spelling of "which room and which thread is this turn in".
82
+ *
83
+ * 🔴 A DISCORD THREAD IS ITS OWN CHANNEL. There is no thread id inside a channel to carry, so the
84
+ * CONTAINER and the THREAD ROOT are the same snowflake on a thread turn, and `readThread` is the
85
+ * verb that reads it (it fetches `threadTs` directly — see below). The turn is a thread turn when
86
+ * the ingress stamped a PARENT channel id, which is exactly what `domainSurface` carries: the
87
+ * mapper sets it only from `parent_channel_id`, so its presence IS "this is a thread".
88
+ *
89
+ * A base-channel turn has no parent stamp, so it reports no thread root and `read_thread` refuses
90
+ * it honestly.
91
+ */
92
+ wakeAddress(ctx) {
93
+ const b = bindFrom(ctx);
94
+ const container = typeof b.target?.["channelId"] === "string" ? b.target["channelId"] : undefined;
95
+ if (!container)
96
+ return undefined;
97
+ return { container, ...(b.domainSurface ? { threadRoot: container } : {}) };
98
+ },
99
+ async readChannel(ctx, channel, limit, req = liveDiscordRead) {
100
+ await assertSurfaceAllowed(ctx, "discord", channel);
101
+ return discordLinesFromRaw(await req(channel, limit ?? DISCORD_CHANNEL_DEFAULT_LIMIT));
102
+ },
103
+ async readThread(ctx, channel, threadTs, limit, req = liveDiscordRead) {
104
+ // A Discord thread is its own channel id — read `thread_ts` (the thread's id), allowlist-checked.
105
+ await assertSurfaceAllowed(ctx, "discord", threadTs);
106
+ return discordLinesFromRaw(await req(threadTs, limit ?? DISCORD_THREAD_DEFAULT_LIMIT));
107
+ },
108
+ /** OPE-474 / OPE-534 — the register leg only (see the note above). It answers with the refuted
109
+ * AUTHOR IDS, because core reads the one answer twice: the turn mark and the `withhold` body
110
+ * replacement. `roleProbe` is injected ONLY so a test can drive the grading offline; every live
111
+ * call defaults to the TTL-cached live probe. */
112
+ async refutedAuthorIds(ctx, lines, roleProbe = discordAuthorRoles) {
113
+ const p = resolvePartition(ctx);
114
+ // `workspace` is the GUILD id for a guild surface, and the DM partner's user id for a DM
115
+ // (discord-identity.ts `bindDiscord`). A DM's key can never be an armed tenant — arming is by
116
+ // guild — so the cost gate answers `false` there without a lookup, which is the right answer:
117
+ // a DM has no guild membership to refute against.
118
+ const tenantKey = p.workspaceKey;
119
+ const guildId = p.workspace;
120
+ return await refutedReadAuthorIds(lines, tenantKey, {
121
+ memberships: async (authorId) => {
122
+ const roles = await roleProbe(guildId, authorId);
123
+ return roles === undefined ? undefined : roles.map((r) => `${tenantKey}/role:${r}`);
124
+ },
125
+ });
126
+ },
127
+ };
128
+ //# sourceMappingURL=discord-read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-read.js","sourceRoot":"","sources":["../../../agent/channels/discord-read.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { type RetryPolicy } from "@open-everywhere/core/agent/adapter-api.js";
2
+ /** The provider surface name on the NAMED rate-limit failure. */
3
+ export declare const DISCORD_SURFACE = "discord";
4
+ /** The subset of `fetch` this seam drives — injectable so a test scripts the 429 sequence. */
5
+ export type DiscordFetchLike = (url: string, init: RequestInit) => Promise<Response>;
6
+ /** Transport and retry seams. A test injects both; live code passes neither. */
7
+ export interface DiscordFetchDeps {
8
+ readonly fetch?: DiscordFetchLike;
9
+ readonly retry?: RetryPolicy;
10
+ }
11
+ /**
12
+ * Read the honoured delay from a Discord 429 body, in milliseconds, or `undefined` when the body
13
+ * carries no usable `retry_after` (then the seam falls back to the header). Discord states
14
+ * `retry_after` in SECONDS — fractional, e.g. `0.75` — so this multiplies by 1000 rather than
15
+ * routing through the header parser, which reads integer seconds only.
16
+ *
17
+ * 🔴 NEVER THROWS, and reads a CLONE. The primitive cancels the original body right after this
18
+ * returns; reading the clone leaves the original intact for that cancel. A body that is missing,
19
+ * not JSON, or carries no numeric `retry_after` answers `undefined` — never a wrong delay.
20
+ */
21
+ export declare function discordBodyRetryAfterMs(res: Response): Promise<number | undefined>;
22
+ /**
23
+ * Issue ONE Discord REST call with the shared 429 backoff, honouring the body's `retry_after` over
24
+ * the header. `operation` is the id-free route shape for the named failure. `deps.retry` lets a test
25
+ * inject a stub sleep/clock; live callers pass nothing.
26
+ *
27
+ * Returns the FIRST non-429 response untouched — the caller reads `res.ok`/`res.status`/`res.json()`
28
+ * exactly as it did before this seam existed. Throws `RateLimitExhaustedError` when a 429 outlasts
29
+ * the bounded attempts, and re-throws a transport error unretried (an unanswered write may have
30
+ * applied).
31
+ */
32
+ export declare function discordFetch(operation: string, url: string, init: RequestInit, deps?: DiscordFetchDeps): Promise<Response>;
33
+ //# sourceMappingURL=discord-rest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-rest.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-rest.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,4CAA4C,CAAC;AAE9F,iEAAiE;AACjE,eAAO,MAAM,eAAe,YAAY,CAAC;AAEzC,8FAA8F;AAC9F,MAAM,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAErF,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CASxF;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQ9H"}
@@ -0,0 +1,75 @@
1
+ // discord-rest — THE ONE place a Discord REST WRITE call leaves this host (OPE-1236).
2
+ //
3
+ // WHY THIS SEAM EXISTS. Every Discord write in the reply/message-ops path used to be a bare `fetch`
4
+ // with no 429 handling. Discord answers a rate-limited call with HTTP 429, and it states the delay
5
+ // TWICE: in the `Retry-After` header AND in a JSON `retry_after` body field (fractional seconds).
6
+ // The body field is authoritative — Discord's own docs put the precise delay there — so the shared
7
+ // header-only backoff would miss it. This funnel wraps every write call in the shared
8
+ // `withRetryAfter` primitive and feeds it a body-precedence override, so a Discord write honours the
9
+ // delay the vendor actually named. See `docs/extending/provider-rate-limits.md`.
10
+ //
11
+ // 🔴 WHAT THIS SEAM COVERS, EXACTLY. Every Discord WRITE in this package: the reply feature's write
12
+ // seam (`discord-post.ts`), the ancillary message ops (`discord-message-ops.ts`), and — since
13
+ // OPE-1311 — the GATEWAY's own five writes (`discord-gateway.ts`: the completion post-back, the
14
+ // open-DM, the typing indicator, the reaction and the thread open). Every `fetch` in those three
15
+ // files goes through here.
16
+ //
17
+ // It does NOT cover the four READ channels (`discord-read.ts`, `discord-backscroll.ts`,
18
+ // `discord-directory.ts`, `discord-bot-name.ts`), which still hold their own raw `fetch`.
19
+ // `discord-rest-seam-test.ts` § 3 holds that KNOWN-UNCOVERED inventory and reds when a NEW file grows
20
+ // its own bare `fetch(` (a write that would bypass the seam) or a covered file regrows one, so the
21
+ // gap cannot widen unnoticed.
22
+ //
23
+ // The gateway's writes are fail-silent, so they do not all take the shared policy: the two ephemeral
24
+ // affordances (typing, the reaction) pass a tighter one, because both are awaited on the turn's own
25
+ // latency path. `discord-gateway.ts`'s `AFFORDANCE_RETRY` carries that reasoning.
26
+ //
27
+ // The bot token is a SECRET — it rides only the caller's header, never through here, and no response
28
+ // body ever reaches an error string. `operation` is the id-free vendor route SHAPE (e.g.
29
+ // `POST /channels/{id}/messages`) that rides the NAMED `RateLimitExhaustedError`; keep ids out of it,
30
+ // so every field on that error stays safe to log.
31
+ import { withRetryAfter } from "@open-everywhere/core/agent/adapter-api.js";
32
+ /** The provider surface name on the NAMED rate-limit failure. */
33
+ export const DISCORD_SURFACE = "discord";
34
+ /**
35
+ * Read the honoured delay from a Discord 429 body, in milliseconds, or `undefined` when the body
36
+ * carries no usable `retry_after` (then the seam falls back to the header). Discord states
37
+ * `retry_after` in SECONDS — fractional, e.g. `0.75` — so this multiplies by 1000 rather than
38
+ * routing through the header parser, which reads integer seconds only.
39
+ *
40
+ * 🔴 NEVER THROWS, and reads a CLONE. The primitive cancels the original body right after this
41
+ * returns; reading the clone leaves the original intact for that cancel. A body that is missing,
42
+ * not JSON, or carries no numeric `retry_after` answers `undefined` — never a wrong delay.
43
+ */
44
+ export async function discordBodyRetryAfterMs(res) {
45
+ try {
46
+ const body = (await res.clone().json());
47
+ const seconds = body?.retry_after;
48
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0)
49
+ return undefined;
50
+ return seconds * 1000;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /**
57
+ * Issue ONE Discord REST call with the shared 429 backoff, honouring the body's `retry_after` over
58
+ * the header. `operation` is the id-free route shape for the named failure. `deps.retry` lets a test
59
+ * inject a stub sleep/clock; live callers pass nothing.
60
+ *
61
+ * Returns the FIRST non-429 response untouched — the caller reads `res.ok`/`res.status`/`res.json()`
62
+ * exactly as it did before this seam existed. Throws `RateLimitExhaustedError` when a 429 outlasts
63
+ * the bounded attempts, and re-throws a transport error unretried (an unanswered write may have
64
+ * applied).
65
+ */
66
+ export function discordFetch(operation, url, init, deps = {}) {
67
+ const send = () => (deps.fetch ?? globalThis.fetch)(url, init);
68
+ return withRetryAfter(send, {
69
+ surface: DISCORD_SURFACE,
70
+ operation,
71
+ retryAfterMs: (res) => discordBodyRetryAfterMs(res),
72
+ ...deps.retry,
73
+ });
74
+ }
75
+ //# sourceMappingURL=discord-rest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-rest.js","sourceRoot":"","sources":["../../../agent/channels/discord-rest.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,EAAE;AACF,oGAAoG;AACpG,mGAAmG;AACnG,kGAAkG;AAClG,mGAAmG;AACnG,sFAAsF;AACtF,qGAAqG;AACrG,iFAAiF;AACjF,EAAE;AACF,oGAAoG;AACpG,8FAA8F;AAC9F,gGAAgG;AAChG,iGAAiG;AACjG,2BAA2B;AAC3B,EAAE;AACF,wFAAwF;AACxF,0FAA0F;AAC1F,sGAAsG;AACtG,mGAAmG;AACnG,8BAA8B;AAC9B,EAAE;AACF,qGAAqG;AACrG,oGAAoG;AACpG,kFAAkF;AAClF,EAAE;AACF,qGAAqG;AACrG,yFAAyF;AACzF,sGAAsG;AACtG,kDAAkD;AAElD,OAAO,EAAE,cAAc,EAAoB,MAAM,4CAA4C,CAAC;AAE9F,iEAAiE;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;AAWzC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAa;IACzD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAqC,CAAC;QAC5E,MAAM,OAAO,GAAG,IAAI,EAAE,WAAW,CAAC;QAClC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC9F,OAAO,OAAO,GAAG,IAAI,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB,EAAE,GAAW,EAAE,IAAiB,EAAE,IAAI,GAAqB,EAAE;IACzG,MAAM,IAAI,GAAG,GAAsB,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,IAAK,UAAU,CAAC,KAA0B,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACxG,OAAO,cAAc,CAAC,IAAI,EAAE;QAC1B,OAAO,EAAE,eAAe;QACxB,SAAS;QACT,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,uBAAuB,CAAC,GAAe,CAAC;QAC/D,GAAG,IAAI,CAAC,KAAK;KACd,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,3 @@
1
+ declare const _default: import("eve/channels/discord").DiscordChannel;
2
+ export default _default;
3
+ //# sourceMappingURL=discord.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord.ts"],"names":[],"mappings":""}
@@ -0,0 +1,230 @@
1
+ // The Discord surface. Wires Discord's HTTP Interactions (slash commands) → an
2
+ // OpenEverywhere session, and — the point of the "Discord-binding" slice — populates the
3
+ // session auth context with the REAL Discord identity (guild/channel/user ids + the
4
+ // channel type) so the identity seam (agent/identity/binding.ts) resolves the actual
5
+ // workspace (the guild/server — the org boundary), surface (the channel), mode
6
+ // (channel/dm/mpim), and visibility (fail-closed private) instead of OE_* env
7
+ // stand-ins. Everything downstream — memory partition, access resolver, routine
8
+ // dispatch — then keys off real Discord identity with no further change.
9
+ //
10
+ // Eve's Discord channel is interaction-based, not message-based: there is no
11
+ // app_mention / message event, only `onCommand(ctx, interaction)` for an explicitly
12
+ // invoked slash command. A slash command is already a bot-directed act — you can't
13
+ // invoke the app without targeting it — so it is the natural analogue of Slack's
14
+ // mention-gating: every onCommand is, by construction, "mentioned". We accept both
15
+ // DMs and guild commands and still drop bot-authored invocations, then override
16
+ // onCommand only to attach OUR richer auth via buildDiscordAuth (the eve default
17
+ // derives a thinner, guild-scoped auth). Kept MINIMAL for launch — identity binding +
18
+ // dispatch only; no status-UX / onboarding extras (cf. slack.ts).
19
+ //
20
+ // Credentials: omitted here, so eve falls back to DISCORD_PUBLIC_KEY /
21
+ // DISCORD_APPLICATION_ID / DISCORD_BOT_TOKEN (see .env.example). Swap the credential
22
+ // broker later without touching identity. Capturing the channel type / guild presence is a
23
+ // RAW-fact job; the security-critical interpretation (fail-closed to private) lives in
24
+ // the binding (discordVisibility).
25
+ //
26
+ // FRONTED (see discord-front.ts): the wrapped eve channel sits on an internal route; the
27
+ // public Interactions Endpoint URL is our front, which answers `oe-approval:` approval-card
28
+ // button clicks (admin approvals — approvals/discord-flow.ts) and forwards everything else
29
+ // (PING, slash commands, eve's own HITL clicks) here unchanged. Same option-(c) pattern as
30
+ // the Slack channel (slack-front.ts) and for the same reason: eve exposes no onComponent hook.
31
+ //
32
+ // A THIRD, separate URL (see discord-events.ts) carries Discord's app-level WEBHOOK EVENTS —
33
+ // APPLICATION_AUTHORIZED / APPLICATION_DEAUTHORIZED, i.e. install/uninstall — into the audit
34
+ // trail. Configured on the developer portal's Webhooks page, not the Interactions one.
35
+ import { discordChannel, } from "eve/channels/discord";
36
+ import { buildDiscordAuth, threadParentFromRawChannel, discordMemberPendingFromRaw } from "./discord-identity.js";
37
+ import { finalMessageActionForTurn, NO_REPLY_SENTINEL_REASON, auditNoReplySentinelObserved, isBareStop, consumeRepliedThisTurn, dropClosingProse, resolvePartition, authPrincipalId, } from "@open-everywhere/core/agent/adapter-api.js";
38
+ import { frontDiscordChannel, INTERNAL_DISCORD_ROUTE } from "./discord-front.js";
39
+ import { withDiscordEventsRoute } from "./discord-events.js";
40
+ // The audit partition for a terminal event, rebuilt from the LIVE session state the same
41
+ // way slack.ts rebuilds its own (the completion handler has no auth context): guild →
42
+ // workspace, channel → surface, via the REAL identity seam so rows land exactly where the
43
+ // dispatch partition did. Null on an unidentifiable state (no channel id) — the caller
44
+ // then skips the row (fail-soft; auditing must never fail or post into the turn).
45
+ function auditPartitionForState(state) {
46
+ if (!state.channelId)
47
+ return null;
48
+ try {
49
+ return resolvePartition({
50
+ channel: { kind: "discord" },
51
+ session: { auth: { current: buildDiscordAuth({ userId: "", guildId: state.guildId ?? undefined, channelId: state.channelId }) } },
52
+ });
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ // Read the Discord numeric channel `type` from the raw interaction payload. The parsed
59
+ // interaction exposes ids (channel/guild/user) but not the channel kind, which lives in
60
+ // the payload's `channel` object. Used only to tell a DM (1) / group DM (3) from a guild
61
+ // text channel or thread; absent → the binding falls back to guild-presence for mode and
62
+ // fails closed on visibility. Defensive: the payload is Discord's raw JSON.
63
+ function channelTypeFromRaw(raw) {
64
+ const ch = raw["channel"];
65
+ if (ch && typeof ch === "object") {
66
+ const t = ch["type"];
67
+ if (typeof t === "number")
68
+ return String(t);
69
+ }
70
+ return undefined;
71
+ }
72
+ // Attach OUR richer Discord auth: raw guild/channel/user ids + the channel type (and,
73
+ // for a THREAD, its parent channel id — the raw payload's channel partial carries
74
+ // `parent_id`, so the thread→parent domain/spend grain of item 22 is FREE here, no REST
75
+ // lookup like the gateway's message path needs), which the binding then interprets into
76
+ // workspace/surface/mode/visibility. Drops invocations by another bot — we never act on
77
+ // a bot's command.
78
+ function authFromInteraction(interaction) {
79
+ if (interaction.user.isBot)
80
+ return null; // bot actor — drop
81
+ return buildDiscordAuth({
82
+ userId: interaction.user.id,
83
+ guildId: interaction.guildId,
84
+ channelId: interaction.channelId,
85
+ channelType: channelTypeFromRaw(interaction.raw),
86
+ parentChannelId: threadParentFromRawChannel(interaction.raw),
87
+ // Guest/unverified detection (OPE-60): the interaction payload carries the guild `member`
88
+ // object (incl. `member.pending`) for FREE — no privileged intent, no REST call. A pending
89
+ // member hasn't cleared membership screening → the binding demotes trust to external.
90
+ memberPending: discordMemberPendingFromRaw(interaction.raw),
91
+ });
92
+ }
93
+ const discordInteractionChannel = discordChannel({
94
+ // Internal mount — the public URL is the approval-click front (frontDiscordChannel below).
95
+ route: INTERNAL_DISCORD_ROUTE,
96
+ // 🔴 QUEUE, NEVER STEER (OPE-412) — eve's default `steer` cancels the turn in flight when a
97
+ // message arrives. Same reasoning as the Slack channel; see slack.ts for the full note.
98
+ turnPolicy: "queue",
99
+ onCommand: async (ctx, interaction) => {
100
+ const auth = authFromInteraction(interaction);
101
+ if (auth === null)
102
+ return null; // bot author / unactionable → ack without dispatch
103
+ try {
104
+ await ctx.discord.startTyping();
105
+ }
106
+ catch {
107
+ /* typing indicator is best-effort */
108
+ }
109
+ return { auth };
110
+ },
111
+ events: {
112
+ // Authoring this handler REPLACES eve's Discord default for "turn.started" (verified in
113
+ // node_modules/eve/dist/.../discord/defaults.js: `async "turn.started"(e,t,n){await
114
+ // t.discord.startTyping()}`), so we replicate that default body verbatim first, matching the
115
+ // discipline channels/slack.ts's own turn.started comment documents.
116
+ //
117
+ // OPE-645: REPLY PRINCIPAL snapshot — the same T1.4 pattern channels/slack.ts,
118
+ // channels/teams.ts and discord-gateway.ts each use for their own: message.completed
119
+ // has no live auth ctx (eve rewrites session.auth.current on every delivery, and this channel
120
+ // runs `turnPolicy: "queue"`, so a later queued delivery can overwrite it before this turn's
121
+ // own message.completed fires), but turn.started does. authPrincipalId(ctx) is the exact read
122
+ // bindFrom(ctx).principalId falls back to for an ordinary human turn (identity/binding.ts) —
123
+ // snapshotting it here means the DROP row this handler writes since OPE-2006 names the same
124
+ // person the turn's egress.policy rows already do. It is no longer a `reply.post` row: nothing
125
+ // posts here.
126
+ "turn.started": async (_data, channel, ctx) => {
127
+ // Snapshot FIRST — the attribution must not depend on the typing call succeeding (the
128
+ // same discipline the Slack sibling states for its supervisor target snapshot).
129
+ channel.state.oeReplyPrincipalId = authPrincipalId(ctx);
130
+ await channel.discord.startTyping();
131
+ },
132
+ // Override the reply-posting handler for ONE addition: a fired `watch` routine
133
+ // ends its turn with the silence sentinel when the watched state is unchanged
134
+ // (routines/watch.ts) — swallow it, never post it. Mirrors the Slack override
135
+ // (channels/slack.ts) and the Teams one, via the shared, unit-tested
136
+ // finalMessageAction() decision. Everything else matches eve's Discord default
137
+ // (tool-calls and empty final messages post nothing). Only routine fires teach
138
+ // the sentinel on this surface; slash-command replies are unaffected.
139
+ "message.completed": async (data, channel, ctx) => {
140
+ const sessionId = ctx?.session?.id; // stamped into the terminal audit rows → joins to the transcript
141
+ // OPE-1647: the TURN ID, so the stand-down silence mark (channels/stand-down-silence.ts) can
142
+ // prove it belongs to THIS turn. Same extraction idiom this file already uses for the §POSTED
143
+ // consume below.
144
+ // 🔴 SINCE OPE-2006 IT SERVES THE SILENCE DECISION ALONE. It is read once here and handed to
145
+ // `finalMessageActionForTurn`, whose `consumeStandDownSilenceThisTurn(turnId)` is its only
146
+ // reader on this path. There is no reply audit below to stamp it onto: this surface DROPS its
147
+ // closing prose, so the durable latch that guarded the old post has no door left to shut here.
148
+ const turnId = ctx?.session?.turn?.id;
149
+ const action = finalMessageActionForTurn(data.finishReason, data.message, turnId);
150
+ // An interim tool-call step, not the turn's end — don't consume the reply signal here:
151
+ // the tool this step is about to run may be the `reply` call itself (mirrors slack.ts).
152
+ if (action.kind === "buffer-tool-status")
153
+ return;
154
+ // Terminal decision — read-and-reset the reply-tool signal once, on every terminal
155
+ // branch (post OR silent), so it never bleeds into the next turn (same contract as
156
+ // slack.ts's reactedThisTurn/repliedThisTurn consume). TURN-ID KEYED (OPE-805): only THIS
157
+ // turn's own mark answers true — see reply-signal.ts's header.
158
+ const repliedThisTurn = consumeRepliedThisTurn(ctx?.session?.turn?.id);
159
+ if (action.kind !== "post") {
160
+ // Only log a genuinely JUDGED silence (the NO_REPLY sentinel, or the no_reply
161
+ // tool) — an ordinary empty completion (nothing to say, no judgment made) isn't
162
+ // worth a log line.
163
+ if (action.kind === "silent" && action.reason !== "empty final message") {
164
+ console.log(`[open-everywhere:discord] judged silence (${action.reason}) — nothing posted (channel ${channel.state.channelId ?? "?"})`);
165
+ }
166
+ // ACTIONS-ONLY posture (audit-wedge build plan §1c, 2026-07-14): a judged silence is not
167
+ // an audited action — the transcript answers "why did it stay quiet?", not the audit log.
168
+ // The console line above stays; the bare-stop WARN below stays (an anomaly, not a silence).
169
+ //
170
+ // ONE exception (OPE-890): the model ended the turn on the bare NO_REPLY sentinel — the
171
+ // TOOL route already carries an always-on `reply.no_reply_observed` row (tools/no_reply.ts,
172
+ // OPE-649); this is the sentinel route's twin, same action name, best-effort. Reached only
173
+ // for a genuine bare-sentinel turn — finalMessageActionForTurn already resolves the tool
174
+ // route to its own `no_reply tool: …` reason, so a tool-called turn never lands here.
175
+ if (action.kind === "silent" && action.reason === NO_REPLY_SENTINEL_REASON) {
176
+ const p = auditPartitionForState(channel.state);
177
+ if (p)
178
+ await auditNoReplySentinelObserved(p, sessionId).catch(() => undefined);
179
+ }
180
+ // Bare-stop enforcement (see slack-events.ts's isBareStop header for the full
181
+ // rationale — DETECT-AND-LOG, not a hard gate). `react` is Slack-only (no
182
+ // Discord equivalent wired), but the reply-tool family (reply-signal.ts) IS armed
183
+ // here, so a `reply`-then-silent turn must not be mis-flagged. The bare stop is
184
+ // ALSO covered by the discord.no_reply row above (reason "empty final message").
185
+ if (action.kind === "silent" && isBareStop(action, repliedThisTurn)) {
186
+ console.warn(`[open-everywhere:discord] BARE STOP — turn ended with no reply and no no_reply (channel ${channel.state.channelId ?? "?"}) — this usually means a judgment call went missing, not that one was made.`);
187
+ }
188
+ return;
189
+ }
190
+ // 🔴 THE CLOSING PROSE IS DROPPED, NOT POSTED (OPE-2006). This branch used to route the
191
+ // terminal reply through `postTerminalProse`, with a raw `channel.discord.post` fallback for an
192
+ // unidentifiable state. Both are gone: the `reply` tool is the only channel a seat's words
193
+ // leave by, so a turn that ended in prose and never called `reply` has said nothing here. The
194
+ // row is the record — no re-send, no queue, no host-side re-post.
195
+ // WHAT TELLS THE MODEL is the seat prompt: Discord declares `closingProseDropped`, so a
196
+ // conversational seat here reads the `reply-only` delivery arm, whose copy states that the
197
+ // turn-final text is discarded — inside its persona when the token is supervisor-shaped, and
198
+ // through the `delivery` slot when it is not (instructions/access.ts `renderDeliverySlot`).
199
+ {
200
+ const p = auditPartitionForState(channel.state);
201
+ if (p) {
202
+ await dropClosingProse({
203
+ row: {
204
+ org: p.org,
205
+ platform: p.platform,
206
+ workspace: p.workspace,
207
+ surface: p.surface,
208
+ principalId: channel.state.oeReplyPrincipalId,
209
+ },
210
+ chars: action.text.length,
211
+ ...(sessionId ? { sessionId } : {}),
212
+ anchor: { channelId: channel.state.channelId ?? null },
213
+ log: (m) => console.warn(`[open-everywhere:discord] ${m} (channel ${channel.state.channelId ?? "?"})`),
214
+ });
215
+ }
216
+ else {
217
+ // No resolvable partition, so there is no row to file it under — the drop still happens and
218
+ // the log line is the whole record, the same honest degradation the silence branches above
219
+ // take on an unidentifiable state.
220
+ console.warn(`[open-everywhere:discord] closing prose DROPPED (${action.text.length}c) — nothing posted, and no partition to audit it under (channel ${channel.state.channelId ?? "?"})`);
221
+ }
222
+ }
223
+ },
224
+ },
225
+ });
226
+ // FRONTED (interactions) + the app-level WEBHOOK EVENTS route (discord-events.ts): a separate,
227
+ // additive URL that audits APPLICATION_AUTHORIZED / APPLICATION_DEAUTHORIZED so install/uninstall
228
+ // is observable. Observability only — it gates nothing and never reaches a session.
229
+ export default withDiscordEventsRoute(frontDiscordChannel(discordInteractionChannel));
230
+ //# sourceMappingURL=discord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord.js","sourceRoot":"","sources":["../../../agent/channels/discord.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import type { SurfaceCommandOutcome, SurfaceCommandScope } from "@open-everywhere/core/agent/adapter-api.js";
2
+ import { type DiscordFetchLike } from "../channels/discord-rest.js";
3
+ /** The projected shape both sides are compared in — exactly the fields this deployment declares. */
4
+ export interface ProjectedOption {
5
+ readonly name: string;
6
+ readonly description: string;
7
+ readonly type: number;
8
+ readonly required: boolean;
9
+ readonly min_value?: number;
10
+ readonly max_value?: number;
11
+ readonly options: readonly ProjectedOption[];
12
+ }
13
+ export interface ProjectedCommand {
14
+ readonly name: string;
15
+ readonly type: number;
16
+ readonly description: string;
17
+ readonly options: readonly ProjectedOption[];
18
+ }
19
+ /**
20
+ * Project one ApplicationCommand — declared or vendor-held — onto the fields this deployment sets.
21
+ * Returns undefined for anything that is not an object with a name, which is what a malformed or
22
+ * unexpected list entry looks like.
23
+ *
24
+ * OPTION ORDER IS PART OF THE PROJECTION. Discord renders a command's options in the order they were
25
+ * registered, so a reordered list is a real user-visible difference and must be written. Discord
26
+ * returns them in the registered order, so a steady state never reorders on its own.
27
+ */
28
+ export declare function projectCommand(raw: unknown): ProjectedCommand | undefined;
29
+ export interface DiscordCommandDrift {
30
+ /** Does Discord need a write for this command? */
31
+ readonly changed: boolean;
32
+ /** Why — one id-free line for the boot log. */
33
+ readonly reason: string;
34
+ }
35
+ /**
36
+ * Compare the command this deployment DECLARES against the list Discord HOLDS at one scope.
37
+ *
38
+ * `held` is the raw array Discord's GET returned. Only the entry whose name matches the declared
39
+ * command is considered: every other command at that scope belongs to someone else and this plane
40
+ * neither reads nor writes it.
41
+ */
42
+ export declare function discordCommandDrift(declared: unknown, held: unknown): DiscordCommandDrift;
43
+ export interface DiscordCommandSyncDeps {
44
+ readonly fetch?: DiscordFetchLike;
45
+ /** Overrides the env reads. Live boot passes neither; a test passes both. */
46
+ readonly applicationId?: string;
47
+ readonly botToken?: string;
48
+ /** The ceiling on ONE call. Defaults to {@link CALL_TIMEOUT_MS}. */
49
+ readonly timeoutMs?: number;
50
+ /** The ceiling on the WHOLE reconcile. Defaults to {@link RECONCILE_BUDGET_MS}. */
51
+ readonly budgetMs?: number;
52
+ }
53
+ /**
54
+ * THE RECONCILER the discord bundle registers with core's command plane.
55
+ *
56
+ * Runs at boot, so it is gated on BOTH deployment credentials being present: `DISCORD_APPLICATION_ID`
57
+ * says which app this is and `DISCORD_BOT_TOKEN` authenticates the call. Either missing ⇒ `skipped`,
58
+ * which is the correct reading of a deployment that carries the bundle but no Discord app — and it is
59
+ * also what keeps every offline test tier network-free, since no test publishes an application id.
60
+ *
61
+ * 🔴 THESE TWO STAY ENV. They are the credential pair this deployment speaks as, which is exactly the
62
+ * `secret`/`identity` half of the config-as-code line. The GUILD ids do not: they are tracked literals
63
+ * in `agent/commands/discord-commands.ts` and arrive here as the declared scope.
64
+ */
65
+ export declare function reconcileDiscordCommands(scope: SurfaceCommandScope, deps?: DiscordCommandSyncDeps): Promise<SurfaceCommandOutcome>;
66
+ //# sourceMappingURL=discord-command-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-command-sync.d.ts","sourceRoot":"","sources":["../../../agent/commands/discord-command-sync.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAe,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,4CAA4C,CAAC;AAC1H,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAiClF,oGAAoG;AACpG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CAC9C;AAoCD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,CAWzE;AAQD,MAAM,WAAW,mBAAmB;IAClC,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,mBAAmB,CAgBzF;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AA6DD;;;;;;;;;;;GAWG;AACH,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,mBAAmB,EAAE,IAAI,GAAE,sBAA2B,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAsB5I"}