@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.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +47 -0
- package/agent/access/discord-grant-click.ts +237 -0
- package/agent/approvals/discord-admin.ts +227 -0
- package/agent/approvals/discord-card.ts +248 -0
- package/agent/approvals/discord-flow.ts +469 -0
- package/agent/approvals/discord-scope-label.ts +27 -0
- package/agent/channels/discord-approval-card.ts +373 -0
- package/agent/channels/discord-backscroll.ts +593 -0
- package/agent/channels/discord-bot-name.ts +113 -0
- package/agent/channels/discord-channel-info.ts +83 -0
- package/agent/channels/discord-directory.ts +179 -0
- package/agent/channels/discord-dispatch-target.ts +103 -0
- package/agent/channels/discord-events.ts +175 -0
- package/agent/channels/discord-front.ts +355 -0
- package/agent/channels/discord-gateway.ts +3256 -0
- package/agent/channels/discord-identity.ts +269 -0
- package/agent/channels/discord-interaction-defer.ts +123 -0
- package/agent/channels/discord-message-ops.ts +127 -0
- package/agent/channels/discord-origin-codec.ts +105 -0
- package/agent/channels/discord-post-arm.ts +230 -0
- package/agent/channels/discord-post.ts +463 -0
- package/agent/channels/discord-read.ts +140 -0
- package/agent/channels/discord-rest.ts +85 -0
- package/agent/channels/discord.ts +244 -0
- package/agent/commands/discord-command-sync.ts +285 -0
- package/agent/commands/discord-commands.ts +291 -0
- package/agent/contributions.ts +31 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/identity/discord-link-consent.ts +124 -0
- package/agent/ops.ts +14 -0
- package/agent/provider-events.ts +86 -0
- package/agent/register.ts +147 -0
- package/agent/surface-parity.ts +95 -0
- package/dist/agent/access/discord-grant-click.d.ts +25 -0
- package/dist/agent/access/discord-grant-click.d.ts.map +1 -0
- package/dist/agent/access/discord-grant-click.js +210 -0
- package/dist/agent/access/discord-grant-click.js.map +1 -0
- package/dist/agent/approvals/discord-admin.d.ts +63 -0
- package/dist/agent/approvals/discord-admin.d.ts.map +1 -0
- package/dist/agent/approvals/discord-admin.js +219 -0
- package/dist/agent/approvals/discord-admin.js.map +1 -0
- package/dist/agent/approvals/discord-card.d.ts +102 -0
- package/dist/agent/approvals/discord-card.d.ts.map +1 -0
- package/dist/agent/approvals/discord-card.js +217 -0
- package/dist/agent/approvals/discord-card.js.map +1 -0
- package/dist/agent/approvals/discord-flow.d.ts +109 -0
- package/dist/agent/approvals/discord-flow.d.ts.map +1 -0
- package/dist/agent/approvals/discord-flow.js +375 -0
- package/dist/agent/approvals/discord-flow.js.map +1 -0
- package/dist/agent/approvals/discord-scope-label.d.ts +3 -0
- package/dist/agent/approvals/discord-scope-label.d.ts.map +1 -0
- package/dist/agent/approvals/discord-scope-label.js +27 -0
- package/dist/agent/approvals/discord-scope-label.js.map +1 -0
- package/dist/agent/channels/discord-approval-card.d.ts +81 -0
- package/dist/agent/channels/discord-approval-card.d.ts.map +1 -0
- package/dist/agent/channels/discord-approval-card.js +303 -0
- package/dist/agent/channels/discord-approval-card.js.map +1 -0
- package/dist/agent/channels/discord-backscroll.d.ts +209 -0
- package/dist/agent/channels/discord-backscroll.d.ts.map +1 -0
- package/dist/agent/channels/discord-backscroll.js +505 -0
- package/dist/agent/channels/discord-backscroll.js.map +1 -0
- package/dist/agent/channels/discord-bot-name.d.ts +19 -0
- package/dist/agent/channels/discord-bot-name.d.ts.map +1 -0
- package/dist/agent/channels/discord-bot-name.js +98 -0
- package/dist/agent/channels/discord-bot-name.js.map +1 -0
- package/dist/agent/channels/discord-channel-info.d.ts +22 -0
- package/dist/agent/channels/discord-channel-info.d.ts.map +1 -0
- package/dist/agent/channels/discord-channel-info.js +65 -0
- package/dist/agent/channels/discord-channel-info.js.map +1 -0
- package/dist/agent/channels/discord-directory.d.ts +11 -0
- package/dist/agent/channels/discord-directory.d.ts.map +1 -0
- package/dist/agent/channels/discord-directory.js +170 -0
- package/dist/agent/channels/discord-directory.js.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts +15 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.js +99 -0
- package/dist/agent/channels/discord-dispatch-target.js.map +1 -0
- package/dist/agent/channels/discord-events.d.ts +58 -0
- package/dist/agent/channels/discord-events.d.ts.map +1 -0
- package/dist/agent/channels/discord-events.js +150 -0
- package/dist/agent/channels/discord-events.js.map +1 -0
- package/dist/agent/channels/discord-front.d.ts +115 -0
- package/dist/agent/channels/discord-front.d.ts.map +1 -0
- package/dist/agent/channels/discord-front.js +279 -0
- package/dist/agent/channels/discord-front.js.map +1 -0
- package/dist/agent/channels/discord-gateway.d.ts +783 -0
- package/dist/agent/channels/discord-gateway.d.ts.map +1 -0
- package/dist/agent/channels/discord-gateway.js +2591 -0
- package/dist/agent/channels/discord-gateway.js.map +1 -0
- package/dist/agent/channels/discord-identity.d.ts +110 -0
- package/dist/agent/channels/discord-identity.d.ts.map +1 -0
- package/dist/agent/channels/discord-identity.js +201 -0
- package/dist/agent/channels/discord-identity.js.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts +33 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.js +102 -0
- package/dist/agent/channels/discord-interaction-defer.js.map +1 -0
- package/dist/agent/channels/discord-message-ops.d.ts +3 -0
- package/dist/agent/channels/discord-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/discord-message-ops.js +123 -0
- package/dist/agent/channels/discord-message-ops.js.map +1 -0
- package/dist/agent/channels/discord-origin-codec.d.ts +7 -0
- package/dist/agent/channels/discord-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/discord-origin-codec.js +98 -0
- package/dist/agent/channels/discord-origin-codec.js.map +1 -0
- package/dist/agent/channels/discord-post-arm.d.ts +3 -0
- package/dist/agent/channels/discord-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/discord-post-arm.js +195 -0
- package/dist/agent/channels/discord-post-arm.js.map +1 -0
- package/dist/agent/channels/discord-post.d.ts +120 -0
- package/dist/agent/channels/discord-post.d.ts.map +1 -0
- package/dist/agent/channels/discord-post.js +426 -0
- package/dist/agent/channels/discord-post.js.map +1 -0
- package/dist/agent/channels/discord-read.d.ts +40 -0
- package/dist/agent/channels/discord-read.d.ts.map +1 -0
- package/dist/agent/channels/discord-read.js +128 -0
- package/dist/agent/channels/discord-read.js.map +1 -0
- package/dist/agent/channels/discord-rest.d.ts +33 -0
- package/dist/agent/channels/discord-rest.d.ts.map +1 -0
- package/dist/agent/channels/discord-rest.js +75 -0
- package/dist/agent/channels/discord-rest.js.map +1 -0
- package/dist/agent/channels/discord.d.ts +3 -0
- package/dist/agent/channels/discord.d.ts.map +1 -0
- package/dist/agent/channels/discord.js +230 -0
- package/dist/agent/channels/discord.js.map +1 -0
- package/dist/agent/commands/discord-command-sync.d.ts +66 -0
- package/dist/agent/commands/discord-command-sync.d.ts.map +1 -0
- package/dist/agent/commands/discord-command-sync.js +224 -0
- package/dist/agent/commands/discord-command-sync.js.map +1 -0
- package/dist/agent/commands/discord-commands.d.ts +45 -0
- package/dist/agent/commands/discord-commands.d.ts.map +1 -0
- package/dist/agent/commands/discord-commands.js +266 -0
- package/dist/agent/commands/discord-commands.js.map +1 -0
- package/dist/agent/contributions.d.ts +17 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +31 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/identity/discord-link-consent.d.ts +13 -0
- package/dist/agent/identity/discord-link-consent.d.ts.map +1 -0
- package/dist/agent/identity/discord-link-consent.js +111 -0
- package/dist/agent/identity/discord-link-consent.js.map +1 -0
- package/dist/agent/ops.d.ts +2 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +13 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +78 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +5 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +125 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +90 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +185 -0
- package/tsconfig.build.json +22 -0
- 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 @@
|
|
|
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"}
|