@spinabot/brigade 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/agent-loop.d.ts.map +1 -1
- package/dist/agents/agent-loop.js +51 -1
- package/dist/agents/agent-loop.js.map +1 -1
- package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
- package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
- package/dist/agents/channels/bundled-channel-metas.js +11 -0
- package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
- package/dist/agents/channels/discord/account-config.d.ts +177 -0
- package/dist/agents/channels/discord/account-config.d.ts.map +1 -0
- package/dist/agents/channels/discord/account-config.js +349 -0
- package/dist/agents/channels/discord/account-config.js.map +1 -0
- package/dist/agents/channels/discord/adapter.d.ts +79 -0
- package/dist/agents/channels/discord/adapter.d.ts.map +1 -0
- package/dist/agents/channels/discord/adapter.js +693 -0
- package/dist/agents/channels/discord/adapter.js.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts +43 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.js +71 -0
- package/dist/agents/channels/discord/approval-authorize.js.map +1 -0
- package/dist/agents/channels/discord/approval-native.d.ts +68 -0
- package/dist/agents/channels/discord/approval-native.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-native.js +81 -0
- package/dist/agents/channels/discord/approval-native.js.map +1 -0
- package/dist/agents/channels/discord/command-menu.d.ts +49 -0
- package/dist/agents/channels/discord/command-menu.d.ts.map +1 -0
- package/dist/agents/channels/discord/command-menu.js +73 -0
- package/dist/agents/channels/discord/command-menu.js.map +1 -0
- package/dist/agents/channels/discord/component-blocks.d.ts +108 -0
- package/dist/agents/channels/discord/component-blocks.d.ts.map +1 -0
- package/dist/agents/channels/discord/component-blocks.js +113 -0
- package/dist/agents/channels/discord/component-blocks.js.map +1 -0
- package/dist/agents/channels/discord/components.d.ts +175 -0
- package/dist/agents/channels/discord/components.d.ts.map +1 -0
- package/dist/agents/channels/discord/components.js +220 -0
- package/dist/agents/channels/discord/components.js.map +1 -0
- package/dist/agents/channels/discord/connection.d.ts +570 -0
- package/dist/agents/channels/discord/connection.d.ts.map +1 -0
- package/dist/agents/channels/discord/connection.js +1600 -0
- package/dist/agents/channels/discord/connection.js.map +1 -0
- package/dist/agents/channels/discord/directory-cache.d.ts +47 -0
- package/dist/agents/channels/discord/directory-cache.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-cache.js +131 -0
- package/dist/agents/channels/discord/directory-cache.js.map +1 -0
- package/dist/agents/channels/discord/directory-live.d.ts +61 -0
- package/dist/agents/channels/discord/directory-live.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-live.js +140 -0
- package/dist/agents/channels/discord/directory-live.js.map +1 -0
- package/dist/agents/channels/discord/draft-stream.d.ts +92 -0
- package/dist/agents/channels/discord/draft-stream.d.ts.map +1 -0
- package/dist/agents/channels/discord/draft-stream.js +213 -0
- package/dist/agents/channels/discord/draft-stream.js.map +1 -0
- package/dist/agents/channels/discord/format.d.ts +70 -0
- package/dist/agents/channels/discord/format.d.ts.map +1 -0
- package/dist/agents/channels/discord/format.js +303 -0
- package/dist/agents/channels/discord/format.js.map +1 -0
- package/dist/agents/channels/discord/guilds.d.ts +25 -0
- package/dist/agents/channels/discord/guilds.d.ts.map +1 -0
- package/dist/agents/channels/discord/guilds.js +46 -0
- package/dist/agents/channels/discord/guilds.js.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts +377 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.js +589 -0
- package/dist/agents/channels/discord/inbound-extras.js.map +1 -0
- package/dist/agents/channels/discord/index.d.ts +21 -0
- package/dist/agents/channels/discord/index.d.ts.map +1 -0
- package/dist/agents/channels/discord/index.js +21 -0
- package/dist/agents/channels/discord/index.js.map +1 -0
- package/dist/agents/channels/discord/media.d.ts +85 -0
- package/dist/agents/channels/discord/media.d.ts.map +1 -0
- package/dist/agents/channels/discord/media.js +242 -0
- package/dist/agents/channels/discord/media.js.map +1 -0
- package/dist/agents/channels/discord/modal-registry.d.ts +89 -0
- package/dist/agents/channels/discord/modal-registry.d.ts.map +1 -0
- package/dist/agents/channels/discord/modal-registry.js +104 -0
- package/dist/agents/channels/discord/modal-registry.js.map +1 -0
- package/dist/agents/channels/discord/modals.d.ts +100 -0
- package/dist/agents/channels/discord/modals.d.ts.map +1 -0
- package/dist/agents/channels/discord/modals.js +124 -0
- package/dist/agents/channels/discord/modals.js.map +1 -0
- package/dist/agents/channels/discord/module.d.ts +15 -0
- package/dist/agents/channels/discord/module.d.ts.map +1 -0
- package/dist/agents/channels/discord/module.js +22 -0
- package/dist/agents/channels/discord/module.js.map +1 -0
- package/dist/agents/channels/discord/permission-audit.d.ts +43 -0
- package/dist/agents/channels/discord/permission-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/permission-audit.js +192 -0
- package/dist/agents/channels/discord/permission-audit.js.map +1 -0
- package/dist/agents/channels/discord/plugin.d.ts +89 -0
- package/dist/agents/channels/discord/plugin.d.ts.map +1 -0
- package/dist/agents/channels/discord/plugin.js +372 -0
- package/dist/agents/channels/discord/plugin.js.map +1 -0
- package/dist/agents/channels/discord/probe.d.ts +115 -0
- package/dist/agents/channels/discord/probe.d.ts.map +1 -0
- package/dist/agents/channels/discord/probe.js +193 -0
- package/dist/agents/channels/discord/probe.js.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts +42 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.js +68 -0
- package/dist/agents/channels/discord/reasoning-lane.js.map +1 -0
- package/dist/agents/channels/discord/rest-actions.d.ts +346 -0
- package/dist/agents/channels/discord/rest-actions.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-actions.js +559 -0
- package/dist/agents/channels/discord/rest-actions.js.map +1 -0
- package/dist/agents/channels/discord/rest-components.d.ts +122 -0
- package/dist/agents/channels/discord/rest-components.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-components.js +243 -0
- package/dist/agents/channels/discord/rest-components.js.map +1 -0
- package/dist/agents/channels/discord/security-audit.d.ts +29 -0
- package/dist/agents/channels/discord/security-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-audit.js +94 -0
- package/dist/agents/channels/discord/security-audit.js.map +1 -0
- package/dist/agents/channels/discord/security-doctor.d.ts +43 -0
- package/dist/agents/channels/discord/security-doctor.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-doctor.js +83 -0
- package/dist/agents/channels/discord/security-doctor.js.map +1 -0
- package/dist/agents/channels/discord/status-issues.d.ts +37 -0
- package/dist/agents/channels/discord/status-issues.d.ts.map +1 -0
- package/dist/agents/channels/discord/status-issues.js +66 -0
- package/dist/agents/channels/discord/status-issues.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts +57 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js +98 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts +95 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js +208 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js.map +1 -0
- package/dist/agents/channels/discord/system-events.d.ts +31 -0
- package/dist/agents/channels/discord/system-events.d.ts.map +1 -0
- package/dist/agents/channels/discord/system-events.js +74 -0
- package/dist/agents/channels/discord/system-events.js.map +1 -0
- package/dist/agents/channels/general-callback.d.ts +12 -0
- package/dist/agents/channels/general-callback.d.ts.map +1 -1
- package/dist/agents/channels/general-callback.js +18 -0
- package/dist/agents/channels/general-callback.js.map +1 -1
- package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
- package/dist/agents/channels/inbound-pipeline.js +70 -10
- package/dist/agents/channels/inbound-pipeline.js.map +1 -1
- package/dist/agents/channels/sdk.d.ts +2 -0
- package/dist/agents/channels/sdk.d.ts.map +1 -1
- package/dist/agents/channels/sdk.js +2 -0
- package/dist/agents/channels/sdk.js.map +1 -1
- package/dist/agents/extensions/modules/index.d.ts.map +1 -1
- package/dist/agents/extensions/modules/index.js +5 -0
- package/dist/agents/extensions/modules/index.js.map +1 -1
- package/dist/agents/extensions/types.d.ts +7 -0
- package/dist/agents/extensions/types.d.ts.map +1 -1
- package/dist/agents/extensions/types.js.map +1 -1
- package/dist/agents/subagent-announce-delivery.d.ts +10 -0
- package/dist/agents/subagent-announce-delivery.d.ts.map +1 -1
- package/dist/agents/subagent-announce-delivery.js +1 -0
- package/dist/agents/subagent-announce-delivery.js.map +1 -1
- package/dist/agents/subagent-completion-bridge.d.ts.map +1 -1
- package/dist/agents/subagent-completion-bridge.js +81 -0
- package/dist/agents/subagent-completion-bridge.js.map +1 -1
- package/dist/agents/subagent-spawn.d.ts.map +1 -1
- package/dist/agents/subagent-spawn.js +57 -4
- package/dist/agents/subagent-spawn.js.map +1 -1
- package/dist/agents/tools/cron-tool.d.ts.map +1 -1
- package/dist/agents/tools/cron-tool.js +4 -1
- package/dist/agents/tools/cron-tool.js.map +1 -1
- package/dist/agents/tools/discord-action-tool.d.ts +224 -0
- package/dist/agents/tools/discord-action-tool.d.ts.map +1 -0
- package/dist/agents/tools/discord-action-tool.js +848 -0
- package/dist/agents/tools/discord-action-tool.js.map +1 -0
- package/dist/agents/tools/registry.d.ts.map +1 -1
- package/dist/agents/tools/registry.js +21 -0
- package/dist/agents/tools/registry.js.map +1 -1
- package/dist/agents/tools/sessions/index.d.ts +8 -0
- package/dist/agents/tools/sessions/index.d.ts.map +1 -1
- package/dist/agents/tools/sessions/index.js +15 -3
- package/dist/agents/tools/sessions/index.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/channels.d.ts +2 -0
- package/dist/cli/commands/channels.d.ts.map +1 -1
- package/dist/cli/commands/channels.js +58 -1
- package/dist/cli/commands/channels.js.map +1 -1
- package/dist/core/auth-bridge.d.ts +1 -0
- package/dist/core/auth-bridge.d.ts.map +1 -1
- package/dist/core/auth-bridge.js +46 -1
- package/dist/core/auth-bridge.js.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +18 -2
- package/dist/core/server.js.map +1 -1
- package/dist/cron/isolated-agent/run-executor.d.ts +11 -0
- package/dist/cron/isolated-agent/run-executor.d.ts.map +1 -1
- package/dist/cron/isolated-agent/run-executor.js +20 -4
- package/dist/cron/isolated-agent/run-executor.js.map +1 -1
- package/dist/cron/types.d.ts +8 -0
- package/dist/cron/types.d.ts.map +1 -1
- package/dist/system-prompt/assembler.d.ts.map +1 -1
- package/dist/system-prompt/assembler.js +4 -2
- package/dist/system-prompt/assembler.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord modal (form) building + submission decoding (Fix 3b).
|
|
3
|
+
*
|
|
4
|
+
* A modal is opened with `interaction.showModal(modal)` in response to a button
|
|
5
|
+
* press; on submit Discord delivers a `ModalSubmitInteraction` whose `fields`
|
|
6
|
+
* accessor yields each text input's value keyed by the field's custom_id. This
|
|
7
|
+
* module:
|
|
8
|
+
* - {@link buildDiscordModal} turns a registry {@link DiscordModalEntry} into a
|
|
9
|
+
* discord.js `ModalBuilder` (text inputs only — Discord's STABLE modals
|
|
10
|
+
* support only text inputs, which is correct for v1). It takes the discord.js
|
|
11
|
+
* builder constructors as an argument so a non-Discord boot never imports
|
|
12
|
+
* them and tests can inject lightweight fakes.
|
|
13
|
+
* - {@link extractModalFieldValues} reads the submitted values off a
|
|
14
|
+
* `ModalSubmitInteraction` defensively (pure — no discord.js).
|
|
15
|
+
* - {@link formatModalSubmissionText} renders a readable turn body from the
|
|
16
|
+
* entry's field labels + the submitted values (pure).
|
|
17
|
+
*
|
|
18
|
+
* The submission is routed as a NORMAL inbound message (`onMessage`) carrying the
|
|
19
|
+
* formatted text — a filled form is a typed turn, not a button tap — so the agent
|
|
20
|
+
* sees the labels + values exactly as a person would type them.
|
|
21
|
+
*/
|
|
22
|
+
import { type DiscordModalEntry, type DiscordModalField } from "./modal-registry.js";
|
|
23
|
+
/** The custom_id key the modal itself carries: `modal:<modalId>`. */
|
|
24
|
+
export declare const DISCORD_MODAL_CUSTOM_ID_PREFIX = "modal:";
|
|
25
|
+
/** Wrap a modal id into the modal's custom_id marker. */
|
|
26
|
+
export declare function buildDiscordModalCustomId(modalId: string): string;
|
|
27
|
+
/** True when a pressed-button / submitted-modal custom_id is a modal marker. */
|
|
28
|
+
export declare function isDiscordModalCustomId(value: string | undefined): boolean;
|
|
29
|
+
/** Strip the marker, returning the modal id (or "" when not a modal marker). */
|
|
30
|
+
export declare function decodeDiscordModalCustomId(value: string | undefined): string;
|
|
31
|
+
/**
|
|
32
|
+
* The discord.js builder constructors {@link buildDiscordModal} needs. Injected
|
|
33
|
+
* (rather than imported) so a non-Discord boot never loads discord.js and tests
|
|
34
|
+
* inject fakes. The real values are `discord.ModalBuilder`, `ActionRowBuilder`,
|
|
35
|
+
* and `TextInputBuilder`.
|
|
36
|
+
*/
|
|
37
|
+
export interface DiscordModalBuilderDeps {
|
|
38
|
+
ModalBuilder: new () => DiscordModalBuilderLike;
|
|
39
|
+
ActionRowBuilder: new () => DiscordModalRowBuilderLike;
|
|
40
|
+
TextInputBuilder: new () => DiscordTextInputBuilderLike;
|
|
41
|
+
}
|
|
42
|
+
interface DiscordModalBuilderLike {
|
|
43
|
+
setCustomId(id: string): this;
|
|
44
|
+
setTitle(title: string): this;
|
|
45
|
+
addComponents(...rows: unknown[]): this;
|
|
46
|
+
}
|
|
47
|
+
interface DiscordModalRowBuilderLike {
|
|
48
|
+
addComponents(...inputs: unknown[]): this;
|
|
49
|
+
}
|
|
50
|
+
interface DiscordTextInputBuilderLike {
|
|
51
|
+
setCustomId(id: string): this;
|
|
52
|
+
setLabel(label: string): this;
|
|
53
|
+
setStyle(style: number): this;
|
|
54
|
+
setRequired(required: boolean): this;
|
|
55
|
+
setPlaceholder(placeholder: string): this;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a discord.js `ModalBuilder` from a registry entry. Each field becomes a
|
|
59
|
+
* `TextInputBuilder` in its own `ActionRowBuilder` (Discord requires one input
|
|
60
|
+
* per row). Title + labels are capped to Discord's limits. `title` is the modal
|
|
61
|
+
* heading; pass it explicitly since the registry stores the form fields, not the
|
|
62
|
+
* heading.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildDiscordModal(deps: DiscordModalBuilderDeps, params: {
|
|
65
|
+
modalId: string;
|
|
66
|
+
title: string;
|
|
67
|
+
entry: DiscordModalEntry;
|
|
68
|
+
}): DiscordModalBuilderLike;
|
|
69
|
+
/** The minimal `ModalSubmitInteraction.fields` surface we read. */
|
|
70
|
+
interface DiscordModalSubmitFieldsLike {
|
|
71
|
+
/** discord.js: returns the submitted string value for a field custom_id. */
|
|
72
|
+
getTextInputValue?: (customId: string) => string;
|
|
73
|
+
/** Some shapes expose a raw field collection; read it as a fallback. */
|
|
74
|
+
fields?: Map<string, {
|
|
75
|
+
value?: string;
|
|
76
|
+
}> | Iterable<{
|
|
77
|
+
customId?: string;
|
|
78
|
+
value?: string;
|
|
79
|
+
}>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Pull the submitted `{ fieldId → value }` map off a modal-submit interaction,
|
|
83
|
+
* for the given entry's fields. Reads via `getTextInputValue` first (the stable
|
|
84
|
+
* discord.js accessor), falling back to a raw fields collection. Fully guarded —
|
|
85
|
+
* a missing value yields "".
|
|
86
|
+
*/
|
|
87
|
+
export declare function extractModalFieldValues(interaction: {
|
|
88
|
+
fields?: DiscordModalSubmitFieldsLike;
|
|
89
|
+
} | undefined, fields: DiscordModalField[]): Record<string, string>;
|
|
90
|
+
/**
|
|
91
|
+
* Render a readable turn body from a modal submission. Each filled field is a
|
|
92
|
+
* `Label: value` line; an empty field is shown as `Label: (empty)` so the agent
|
|
93
|
+
* sees the full form. A leading `[form]` marker tags the turn like `[button]`
|
|
94
|
+
* does for a tap, so the agent can recognize a form submission.
|
|
95
|
+
*/
|
|
96
|
+
export declare function formatModalSubmissionText(entry: {
|
|
97
|
+
fields: DiscordModalField[];
|
|
98
|
+
}, values: Record<string, string>): string;
|
|
99
|
+
export type { DiscordModalField };
|
|
100
|
+
//# sourceMappingURL=modals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modals.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/modals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAA4B,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAE/G,qEAAqE;AACrE,eAAO,MAAM,8BAA8B,WAAW,CAAC;AAEvD,yDAAyD;AACzD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,gFAAgF;AAChF,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEzE;AAED,gFAAgF;AAChF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAG5E;AAMD;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,YAAY,EAAE,UAAU,uBAAuB,CAAC;IAChD,gBAAgB,EAAE,UAAU,0BAA0B,CAAC;IACvD,gBAAgB,EAAE,UAAU,2BAA2B,CAAC;CACxD;AAED,UAAU,uBAAuB;IAChC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACxC;AACD,UAAU,0BAA0B;IACnC,aAAa,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC1C;AACD,UAAU,2BAA2B;IACpC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GAClE,uBAAuB,CAiBzB;AAED,mEAAmE;AACnE,UAAU,4BAA4B;IACrC,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACjD,wEAAwE;IACxE,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3F;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACtC,WAAW,EAAE;IAAE,MAAM,CAAC,EAAE,4BAA4B,CAAA;CAAE,GAAG,SAAS,EAClE,MAAM,EAAE,iBAAiB,EAAE,GACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BxB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACxC,KAAK,EAAE;IAAE,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAAE,EACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,MAAM,CAQR;AAED,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord modal (form) building + submission decoding (Fix 3b).
|
|
3
|
+
*
|
|
4
|
+
* A modal is opened with `interaction.showModal(modal)` in response to a button
|
|
5
|
+
* press; on submit Discord delivers a `ModalSubmitInteraction` whose `fields`
|
|
6
|
+
* accessor yields each text input's value keyed by the field's custom_id. This
|
|
7
|
+
* module:
|
|
8
|
+
* - {@link buildDiscordModal} turns a registry {@link DiscordModalEntry} into a
|
|
9
|
+
* discord.js `ModalBuilder` (text inputs only — Discord's STABLE modals
|
|
10
|
+
* support only text inputs, which is correct for v1). It takes the discord.js
|
|
11
|
+
* builder constructors as an argument so a non-Discord boot never imports
|
|
12
|
+
* them and tests can inject lightweight fakes.
|
|
13
|
+
* - {@link extractModalFieldValues} reads the submitted values off a
|
|
14
|
+
* `ModalSubmitInteraction` defensively (pure — no discord.js).
|
|
15
|
+
* - {@link formatModalSubmissionText} renders a readable turn body from the
|
|
16
|
+
* entry's field labels + the submitted values (pure).
|
|
17
|
+
*
|
|
18
|
+
* The submission is routed as a NORMAL inbound message (`onMessage`) carrying the
|
|
19
|
+
* formatted text — a filled form is a typed turn, not a button tap — so the agent
|
|
20
|
+
* sees the labels + values exactly as a person would type them.
|
|
21
|
+
*/
|
|
22
|
+
import { DISCORD_TEXT_INPUT_STYLE } from "./modal-registry.js";
|
|
23
|
+
/** The custom_id key the modal itself carries: `modal:<modalId>`. */
|
|
24
|
+
export const DISCORD_MODAL_CUSTOM_ID_PREFIX = "modal:";
|
|
25
|
+
/** Wrap a modal id into the modal's custom_id marker. */
|
|
26
|
+
export function buildDiscordModalCustomId(modalId) {
|
|
27
|
+
return `${DISCORD_MODAL_CUSTOM_ID_PREFIX}${modalId}`;
|
|
28
|
+
}
|
|
29
|
+
/** True when a pressed-button / submitted-modal custom_id is a modal marker. */
|
|
30
|
+
export function isDiscordModalCustomId(value) {
|
|
31
|
+
return typeof value === "string" && value.startsWith(DISCORD_MODAL_CUSTOM_ID_PREFIX);
|
|
32
|
+
}
|
|
33
|
+
/** Strip the marker, returning the modal id (or "" when not a modal marker). */
|
|
34
|
+
export function decodeDiscordModalCustomId(value) {
|
|
35
|
+
if (!isDiscordModalCustomId(value))
|
|
36
|
+
return "";
|
|
37
|
+
return value.slice(DISCORD_MODAL_CUSTOM_ID_PREFIX.length);
|
|
38
|
+
}
|
|
39
|
+
/** Discord caps a modal title at 45 chars + a text-input label at 45 chars. */
|
|
40
|
+
const DISCORD_MODAL_TITLE_MAX = 45;
|
|
41
|
+
const DISCORD_MODAL_LABEL_MAX = 45;
|
|
42
|
+
/**
|
|
43
|
+
* Build a discord.js `ModalBuilder` from a registry entry. Each field becomes a
|
|
44
|
+
* `TextInputBuilder` in its own `ActionRowBuilder` (Discord requires one input
|
|
45
|
+
* per row). Title + labels are capped to Discord's limits. `title` is the modal
|
|
46
|
+
* heading; pass it explicitly since the registry stores the form fields, not the
|
|
47
|
+
* heading.
|
|
48
|
+
*/
|
|
49
|
+
export function buildDiscordModal(deps, params) {
|
|
50
|
+
const modal = new deps.ModalBuilder()
|
|
51
|
+
.setCustomId(buildDiscordModalCustomId(params.modalId))
|
|
52
|
+
.setTitle((params.title || "Form").slice(0, DISCORD_MODAL_TITLE_MAX));
|
|
53
|
+
const rows = [];
|
|
54
|
+
for (const field of params.entry.fields) {
|
|
55
|
+
const input = new deps.TextInputBuilder()
|
|
56
|
+
.setCustomId(field.id)
|
|
57
|
+
.setLabel((field.label || field.id).slice(0, DISCORD_MODAL_LABEL_MAX))
|
|
58
|
+
.setStyle(field.style === "paragraph" ? DISCORD_TEXT_INPUT_STYLE.paragraph : DISCORD_TEXT_INPUT_STYLE.short)
|
|
59
|
+
.setRequired(field.required !== false);
|
|
60
|
+
if (field.placeholder)
|
|
61
|
+
input.setPlaceholder(field.placeholder);
|
|
62
|
+
const row = new deps.ActionRowBuilder().addComponents(input);
|
|
63
|
+
rows.push(row);
|
|
64
|
+
}
|
|
65
|
+
modal.addComponents(...rows);
|
|
66
|
+
return modal;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Pull the submitted `{ fieldId → value }` map off a modal-submit interaction,
|
|
70
|
+
* for the given entry's fields. Reads via `getTextInputValue` first (the stable
|
|
71
|
+
* discord.js accessor), falling back to a raw fields collection. Fully guarded —
|
|
72
|
+
* a missing value yields "".
|
|
73
|
+
*/
|
|
74
|
+
export function extractModalFieldValues(interaction, fields) {
|
|
75
|
+
const out = {};
|
|
76
|
+
const accessor = interaction?.fields;
|
|
77
|
+
// Build a fallback id→value map from a raw collection once, if present.
|
|
78
|
+
const fallback = new Map();
|
|
79
|
+
const rawFields = accessor?.fields;
|
|
80
|
+
if (rawFields) {
|
|
81
|
+
if (rawFields instanceof Map) {
|
|
82
|
+
for (const [id, v] of rawFields)
|
|
83
|
+
fallback.set(id, typeof v?.value === "string" ? v.value : "");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
for (const v of rawFields) {
|
|
87
|
+
if (typeof v?.customId === "string")
|
|
88
|
+
fallback.set(v.customId, typeof v.value === "string" ? v.value : "");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const field of fields) {
|
|
93
|
+
let value = "";
|
|
94
|
+
if (typeof accessor?.getTextInputValue === "function") {
|
|
95
|
+
try {
|
|
96
|
+
value = accessor.getTextInputValue(field.id) ?? "";
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
value = fallback.get(field.id) ?? "";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
value = fallback.get(field.id) ?? "";
|
|
104
|
+
}
|
|
105
|
+
out[field.id] = typeof value === "string" ? value : "";
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Render a readable turn body from a modal submission. Each filled field is a
|
|
111
|
+
* `Label: value` line; an empty field is shown as `Label: (empty)` so the agent
|
|
112
|
+
* sees the full form. A leading `[form]` marker tags the turn like `[button]`
|
|
113
|
+
* does for a tap, so the agent can recognize a form submission.
|
|
114
|
+
*/
|
|
115
|
+
export function formatModalSubmissionText(entry, values) {
|
|
116
|
+
const lines = ["[form]"];
|
|
117
|
+
for (const field of entry.fields) {
|
|
118
|
+
const raw = values[field.id];
|
|
119
|
+
const value = typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : "(empty)";
|
|
120
|
+
lines.push(`${field.label || field.id}: ${value}`);
|
|
121
|
+
}
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=modals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modals.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/modals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,wBAAwB,EAAkD,MAAM,qBAAqB,CAAC;AAE/G,qEAAqE;AACrE,MAAM,CAAC,MAAM,8BAA8B,GAAG,QAAQ,CAAC;AAEvD,yDAAyD;AACzD,MAAM,UAAU,yBAAyB,CAAC,OAAe;IACxD,OAAO,GAAG,8BAA8B,GAAG,OAAO,EAAE,CAAC;AACtD,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,sBAAsB,CAAC,KAAyB;IAC/D,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC,8BAA8B,CAAC,CAAC;AACtF,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,0BAA0B,CAAC,KAAyB;IACnE,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9C,OAAQ,KAAgB,CAAC,KAAK,CAAC,8BAA8B,CAAC,MAAM,CAAC,CAAC;AACvE,CAAC;AAED,+EAA+E;AAC/E,MAAM,uBAAuB,GAAG,EAAE,CAAC;AACnC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AA8BnC;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAChC,IAA6B,EAC7B,MAAoE;IAEpE,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,YAAY,EAAE;SACnC,WAAW,CAAC,yBAAyB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;SACtD,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC,CAAC;IACvE,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,gBAAgB,EAAE;aACvC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;aACrB,QAAQ,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC;aACrE,QAAQ,CAAC,KAAK,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,wBAAwB,CAAC,SAAS,CAAC,CAAC,CAAC,wBAAwB,CAAC,KAAK,CAAC;aAC3G,WAAW,CAAC,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC;QACxC,IAAI,KAAK,CAAC,WAAW;YAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC/D,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,CAAC;IAC7B,OAAO,KAAK,CAAC;AACd,CAAC;AAUD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACtC,WAAkE,EAClE,MAA2B;IAE3B,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAG,WAAW,EAAE,MAAM,CAAC;IACrC,wEAAwE;IACxE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,MAAM,SAAS,GAAG,QAAQ,EAAE,MAAM,CAAC;IACnC,IAAI,SAAS,EAAE,CAAC;QACf,IAAI,SAAS,YAAY,GAAG,EAAE,CAAC;YAC9B,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,SAAS;gBAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChG,CAAC;aAAM,CAAC;YACP,KAAK,MAAM,CAAC,IAAI,SAA4D,EAAE,CAAC;gBAC9E,IAAI,OAAO,CAAC,EAAE,QAAQ,KAAK,QAAQ;oBAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC3G,CAAC;QACF,CAAC;IACF,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,OAAO,QAAQ,EAAE,iBAAiB,KAAK,UAAU,EAAE,CAAC;YACvD,IAAI,CAAC;gBACJ,KAAK,GAAG,QAAQ,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YACpD,CAAC;YAAC,MAAM,CAAC;gBACR,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YACtC,CAAC;QACF,CAAC;aAAM,CAAC;YACP,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;QACtC,CAAC;QACD,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IACxD,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACxC,KAAsC,EACtC,MAA8B;IAE9B,MAAM,KAAK,GAAa,CAAC,QAAQ,CAAC,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACxF,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord extension module.
|
|
3
|
+
*
|
|
4
|
+
* Registers the Discord channel adapter through the seam. The loader gates it by
|
|
5
|
+
* the usual extension config (`extensions.disabled` / `entries`), and the
|
|
6
|
+
* adapter itself only starts when `channels.discord.enabled` is true AND a bot
|
|
7
|
+
* token resolves — so bundling this module is inert until the operator opts in.
|
|
8
|
+
*
|
|
9
|
+
* Unlike Slack's module, Discord registers NO gateway HTTP route: the Gateway
|
|
10
|
+
* (WebSocket) is the only inbound transport (no public URL needed, analogous to
|
|
11
|
+
* Slack Socket Mode / Telegram long-polling), so there is no events-mode webhook
|
|
12
|
+
* to wire. Discord mirror of `telegram/module.ts` (the polling-only shape).
|
|
13
|
+
*/
|
|
14
|
+
export declare const discordModule: import("../sdk.js").BrigadeModule;
|
|
15
|
+
//# sourceMappingURL=module.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,eAAO,MAAM,aAAa,mCAKxB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord extension module.
|
|
3
|
+
*
|
|
4
|
+
* Registers the Discord channel adapter through the seam. The loader gates it by
|
|
5
|
+
* the usual extension config (`extensions.disabled` / `entries`), and the
|
|
6
|
+
* adapter itself only starts when `channels.discord.enabled` is true AND a bot
|
|
7
|
+
* token resolves — so bundling this module is inert until the operator opts in.
|
|
8
|
+
*
|
|
9
|
+
* Unlike Slack's module, Discord registers NO gateway HTTP route: the Gateway
|
|
10
|
+
* (WebSocket) is the only inbound transport (no public URL needed, analogous to
|
|
11
|
+
* Slack Socket Mode / Telegram long-polling), so there is no events-mode webhook
|
|
12
|
+
* to wire. Discord mirror of `telegram/module.ts` (the polling-only shape).
|
|
13
|
+
*/
|
|
14
|
+
import { defineModule } from "../sdk.js";
|
|
15
|
+
import { createDiscordAdapter } from "./adapter.js";
|
|
16
|
+
export const discordModule = defineModule({
|
|
17
|
+
id: "discord",
|
|
18
|
+
register(b) {
|
|
19
|
+
b.channel(createDiscordAdapter());
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;IACzC,EAAE,EAAE,SAAS;IACb,QAAQ,CAAC,CAAC;QACT,CAAC,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IACnC,CAAC;CACD,CAAC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord channel-permission audit — the #2 Discord footgun after the MESSAGE
|
|
3
|
+
* CONTENT intent.
|
|
4
|
+
*
|
|
5
|
+
* A bot can be "connected" and still silently fail to post because its role
|
|
6
|
+
* lacks `View Channel` + `Send Messages` in a specific channel (or a channel
|
|
7
|
+
* permission OVERWRITE denies it). This module computes the bot's EFFECTIVE
|
|
8
|
+
* permissions per channel the same way Discord does — base @everyone + the
|
|
9
|
+
* bot's role permissions, then the channel's permission overwrites (deny then
|
|
10
|
+
* allow, applied @everyone → roles → member) — and reports which channels are
|
|
11
|
+
* missing the two required bits.
|
|
12
|
+
*
|
|
13
|
+
* Self-contained REST (no `discord.js`): `GET /channels/{id}` →
|
|
14
|
+
* `GET /guilds/{guildId}` (roles) → `GET /guilds/{guildId}/members/{botId}`
|
|
15
|
+
* (the bot's roles), plus a one-time `GET /users/@me` for the bot id. Injectable
|
|
16
|
+
* fetch (tests stub it); never throws — a per-channel failure surfaces as an
|
|
17
|
+
* `error` row, never an exception. Only NUMERIC ids are audited; a non-numeric
|
|
18
|
+
* key is reported unresolved so the operator fixes the config.
|
|
19
|
+
*/
|
|
20
|
+
/** Per-channel audit result. */
|
|
21
|
+
export interface DiscordChannelPermissionResult {
|
|
22
|
+
channelId: string;
|
|
23
|
+
/** True when the bot has every required bit (or Administrator). */
|
|
24
|
+
ok: boolean;
|
|
25
|
+
/** Required permission names the bot is MISSING (empty when ok). */
|
|
26
|
+
missingRequired: string[];
|
|
27
|
+
/** Best-effort error when the channel couldn't be evaluated. */
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Overall audit result. */
|
|
31
|
+
export interface DiscordPermissionAuditResult {
|
|
32
|
+
channels: DiscordChannelPermissionResult[];
|
|
33
|
+
/** Count of supplied ids that weren't numeric snowflakes (can't be audited). */
|
|
34
|
+
unresolvedChannels: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Audit the bot's `ViewChannel` + `SendMessages` in each supplied channel id.
|
|
38
|
+
* Non-numeric ids are counted under `unresolvedChannels` and skipped (Discord
|
|
39
|
+
* ids are numeric snowflakes — a name/slug key can't be resolved via REST). The
|
|
40
|
+
* bot user id is fetched once via `/users/@me`. Best-effort + never throws.
|
|
41
|
+
*/
|
|
42
|
+
export declare function auditDiscordChannelPermissions(token: string, channelIds: string[], fetchImpl?: typeof fetch): Promise<DiscordPermissionAuditResult>;
|
|
43
|
+
//# sourceMappingURL=permission-audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-audit.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/permission-audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAeH,gCAAgC;AAChC,MAAM,WAAW,8BAA8B;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,EAAE,EAAE,OAAO,CAAC;IACZ,oEAAoE;IACpE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,4BAA4B;AAC5B,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,EAAE,8BAA8B,EAAE,CAAC;IAC3C,gFAAgF;IAChF,kBAAkB,EAAE,MAAM,CAAC;CAC3B;AA+HD;;;;;GAKG;AACH,wBAAsB,8BAA8B,CACnD,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAAE,EACpB,SAAS,GAAE,OAAO,KAAa,GAC7B,OAAO,CAAC,4BAA4B,CAAC,CAuDvC"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord channel-permission audit — the #2 Discord footgun after the MESSAGE
|
|
3
|
+
* CONTENT intent.
|
|
4
|
+
*
|
|
5
|
+
* A bot can be "connected" and still silently fail to post because its role
|
|
6
|
+
* lacks `View Channel` + `Send Messages` in a specific channel (or a channel
|
|
7
|
+
* permission OVERWRITE denies it). This module computes the bot's EFFECTIVE
|
|
8
|
+
* permissions per channel the same way Discord does — base @everyone + the
|
|
9
|
+
* bot's role permissions, then the channel's permission overwrites (deny then
|
|
10
|
+
* allow, applied @everyone → roles → member) — and reports which channels are
|
|
11
|
+
* missing the two required bits.
|
|
12
|
+
*
|
|
13
|
+
* Self-contained REST (no `discord.js`): `GET /channels/{id}` →
|
|
14
|
+
* `GET /guilds/{guildId}` (roles) → `GET /guilds/{guildId}/members/{botId}`
|
|
15
|
+
* (the bot's roles), plus a one-time `GET /users/@me` for the bot id. Injectable
|
|
16
|
+
* fetch (tests stub it); never throws — a per-channel failure surfaces as an
|
|
17
|
+
* `error` row, never an exception. Only NUMERIC ids are audited; a non-numeric
|
|
18
|
+
* key is reported unresolved so the operator fixes the config.
|
|
19
|
+
*/
|
|
20
|
+
/** Discord permission bits (bigint). */
|
|
21
|
+
const PERM_ADMINISTRATOR = 1n << 3n;
|
|
22
|
+
const PERM_VIEW_CHANNEL = 1n << 10n;
|
|
23
|
+
const PERM_SEND_MESSAGES = 1n << 11n;
|
|
24
|
+
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
25
|
+
/** Required permissions a bot needs to operate in a channel. */
|
|
26
|
+
const REQUIRED = [
|
|
27
|
+
{ bit: PERM_VIEW_CHANNEL, name: "ViewChannel" },
|
|
28
|
+
{ bit: PERM_SEND_MESSAGES, name: "SendMessages" },
|
|
29
|
+
];
|
|
30
|
+
/** A numeric snowflake id (Discord ids are decimal strings). */
|
|
31
|
+
function isNumericId(value) {
|
|
32
|
+
return /^\d+$/.test(value.trim());
|
|
33
|
+
}
|
|
34
|
+
/** Apply an overwrite's deny then allow to a running permission bitfield. */
|
|
35
|
+
function applyOverwrite(perms, ow) {
|
|
36
|
+
let next = perms;
|
|
37
|
+
if (ow.deny) {
|
|
38
|
+
try {
|
|
39
|
+
next &= ~BigInt(ow.deny);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* malformed bitstring — ignore */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (ow.allow) {
|
|
46
|
+
try {
|
|
47
|
+
next |= BigInt(ow.allow);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* malformed bitstring — ignore */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
/** Authenticated GET → parsed JSON object, or throws with a compact message. */
|
|
56
|
+
async function getJson(url, token, fetchImpl) {
|
|
57
|
+
const res = await fetchImpl(url, {
|
|
58
|
+
method: "GET",
|
|
59
|
+
headers: { Authorization: `Bot ${token}`, "content-type": "application/json" },
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new Error(`HTTP ${res.status}`);
|
|
63
|
+
}
|
|
64
|
+
return (await res.json());
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Compute the bot's effective permission bitfield in one channel, mirroring
|
|
68
|
+
* Discord's resolution order: base (@everyone role + the bot's roles), then
|
|
69
|
+
* @everyone overwrite, then the bot's role overwrites, then the bot's member
|
|
70
|
+
* overwrite. Administrator short-circuits to "all". Returns the bitfield.
|
|
71
|
+
*/
|
|
72
|
+
function computeEffectivePermissions(params) {
|
|
73
|
+
const { botId, guildId, channel, guild, member } = params;
|
|
74
|
+
const rolesById = new Map();
|
|
75
|
+
for (const role of guild.roles ?? []) {
|
|
76
|
+
if (typeof role.id !== "string")
|
|
77
|
+
continue;
|
|
78
|
+
let bits = 0n;
|
|
79
|
+
try {
|
|
80
|
+
bits = role.permissions ? BigInt(role.permissions) : 0n;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
bits = 0n;
|
|
84
|
+
}
|
|
85
|
+
rolesById.set(role.id, bits);
|
|
86
|
+
}
|
|
87
|
+
// Base = @everyone (role id === guildId) + each of the bot's roles.
|
|
88
|
+
let perms = rolesById.get(guildId) ?? 0n;
|
|
89
|
+
const memberRoleIds = Array.isArray(member.roles) ? member.roles : [];
|
|
90
|
+
for (const roleId of memberRoleIds) {
|
|
91
|
+
perms |= rolesById.get(roleId) ?? 0n;
|
|
92
|
+
}
|
|
93
|
+
// Administrator → all permissions, overwrites ignored.
|
|
94
|
+
if ((perms & PERM_ADMINISTRATOR) === PERM_ADMINISTRATOR) {
|
|
95
|
+
return ~0n;
|
|
96
|
+
}
|
|
97
|
+
const overwrites = Array.isArray(channel.permission_overwrites) ? channel.permission_overwrites : [];
|
|
98
|
+
// @everyone overwrite.
|
|
99
|
+
for (const ow of overwrites) {
|
|
100
|
+
if (ow.id === guildId)
|
|
101
|
+
perms = applyOverwrite(perms, ow);
|
|
102
|
+
}
|
|
103
|
+
// The bot's role overwrites (accumulate allow/deny across them).
|
|
104
|
+
let roleAllow = 0n;
|
|
105
|
+
let roleDeny = 0n;
|
|
106
|
+
for (const ow of overwrites) {
|
|
107
|
+
if (ow.type === 0 && ow.id && memberRoleIds.includes(ow.id)) {
|
|
108
|
+
try {
|
|
109
|
+
if (ow.deny)
|
|
110
|
+
roleDeny |= BigInt(ow.deny);
|
|
111
|
+
if (ow.allow)
|
|
112
|
+
roleAllow |= BigInt(ow.allow);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* ignore malformed */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
perms &= ~roleDeny;
|
|
120
|
+
perms |= roleAllow;
|
|
121
|
+
// The bot's member overwrite (highest precedence).
|
|
122
|
+
for (const ow of overwrites) {
|
|
123
|
+
if (ow.id === botId)
|
|
124
|
+
perms = applyOverwrite(perms, ow);
|
|
125
|
+
}
|
|
126
|
+
return perms;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Audit the bot's `ViewChannel` + `SendMessages` in each supplied channel id.
|
|
130
|
+
* Non-numeric ids are counted under `unresolvedChannels` and skipped (Discord
|
|
131
|
+
* ids are numeric snowflakes — a name/slug key can't be resolved via REST). The
|
|
132
|
+
* bot user id is fetched once via `/users/@me`. Best-effort + never throws.
|
|
133
|
+
*/
|
|
134
|
+
export async function auditDiscordChannelPermissions(token, channelIds, fetchImpl = fetch) {
|
|
135
|
+
const clean = (token ?? "").trim();
|
|
136
|
+
const ids = (channelIds ?? []).map((c) => (c ?? "").trim()).filter(Boolean);
|
|
137
|
+
const numeric = ids.filter(isNumericId);
|
|
138
|
+
const unresolvedChannels = ids.length - numeric.length;
|
|
139
|
+
if (!clean || numeric.length === 0) {
|
|
140
|
+
return { channels: [], unresolvedChannels };
|
|
141
|
+
}
|
|
142
|
+
// Resolve the bot user id once (needed for the member overwrite + member fetch).
|
|
143
|
+
let botId = "";
|
|
144
|
+
try {
|
|
145
|
+
const me = await getJson(`${DISCORD_API_BASE}/users/@me`, clean, fetchImpl);
|
|
146
|
+
botId = typeof me?.id === "string" ? me.id : "";
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
botId = "";
|
|
150
|
+
}
|
|
151
|
+
if (!botId) {
|
|
152
|
+
return {
|
|
153
|
+
channels: numeric.map((channelId) => ({
|
|
154
|
+
channelId,
|
|
155
|
+
ok: false,
|
|
156
|
+
missingRequired: REQUIRED.map((r) => r.name),
|
|
157
|
+
error: "could not resolve bot identity (/users/@me failed)",
|
|
158
|
+
})),
|
|
159
|
+
unresolvedChannels,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const channels = [];
|
|
163
|
+
for (const channelId of numeric) {
|
|
164
|
+
try {
|
|
165
|
+
const channel = await getJson(`${DISCORD_API_BASE}/channels/${channelId}`, clean, fetchImpl);
|
|
166
|
+
const guildId = typeof channel.guild_id === "string" ? channel.guild_id : "";
|
|
167
|
+
if (!guildId) {
|
|
168
|
+
// A DM / group channel has no guild perms — treat as ok (the bot can
|
|
169
|
+
// always send to a DM it can fetch).
|
|
170
|
+
channels.push({ channelId, ok: true, missingRequired: [] });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const [guild, member] = await Promise.all([
|
|
174
|
+
getJson(`${DISCORD_API_BASE}/guilds/${guildId}`, clean, fetchImpl),
|
|
175
|
+
getJson(`${DISCORD_API_BASE}/guilds/${guildId}/members/${botId}`, clean, fetchImpl),
|
|
176
|
+
]);
|
|
177
|
+
const perms = computeEffectivePermissions({ botId, guildId, channel, guild, member });
|
|
178
|
+
const missingRequired = REQUIRED.filter((r) => (perms & r.bit) !== r.bit).map((r) => r.name);
|
|
179
|
+
channels.push({ channelId, ok: missingRequired.length === 0, missingRequired });
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
channels.push({
|
|
183
|
+
channelId,
|
|
184
|
+
ok: false,
|
|
185
|
+
missingRequired: REQUIRED.map((r) => r.name),
|
|
186
|
+
error: err instanceof Error ? err.message : String(err),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { channels, unresolvedChannels };
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=permission-audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-audit.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/permission-audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord `ChannelPlugin` — the multi-ACCOUNT contract surface.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `slack/plugin.ts`: wraps `createDiscordAdapter()` (the per-connection
|
|
5
|
+
* implementation) with the lifecycle adapters the `ChannelPluginManager`
|
|
6
|
+
* consumes, so an operator can run MORE THAN ONE Discord bot at once via:
|
|
7
|
+
*
|
|
8
|
+
* channels.discord = {
|
|
9
|
+
* enabled: true,
|
|
10
|
+
* accounts: [
|
|
11
|
+
* { id: "main", botToken: "…AAA" },
|
|
12
|
+
* { id: "labs", botToken: "…BBB" },
|
|
13
|
+
* ],
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* - `config.listAccountIds` / `resolveAccount` → multi-account discovery
|
|
17
|
+
* - `gateway.startAccount` / `stopAccount` → per-account bot lifecycle
|
|
18
|
+
* - `outbound.sendText` / `sendMedia` → routes by `target.accountId`
|
|
19
|
+
* - per-account approval-dispatcher registration → an exec-gate prompt raised
|
|
20
|
+
* by a turn on (discord, labs) replies on (discord, labs), not the default
|
|
21
|
+
*
|
|
22
|
+
* Per-account state lives in a `Map<accountId, AccountRuntime>` held in this
|
|
23
|
+
* closure — one Gateway connection per account, partitioned token resolution per
|
|
24
|
+
* `channels.discord.accounts[].botToken`. Inbound dispatch reuses the shared
|
|
25
|
+
* `runChannelInboundPipeline` so the multi-account path carries the identical
|
|
26
|
+
* ACL + debounce + abort + approval-reply + approval-callback surface as the
|
|
27
|
+
* legacy single-adapter manager.
|
|
28
|
+
*
|
|
29
|
+
* The legacy single-account `createDiscordAdapter` (started by the legacy
|
|
30
|
+
* `startChannels` manager) STEPS ASIDE when >1 account is configured — its
|
|
31
|
+
* `isConfigured` returns false for the default account in that case (mirrors
|
|
32
|
+
* Slack), so the two paths never double-start a bot.
|
|
33
|
+
*
|
|
34
|
+
* Discord has no events-mode HTTP route (the Gateway is the only inbound
|
|
35
|
+
* transport), so — unlike Slack's plugin — there is NO per-account webhook-sink
|
|
36
|
+
* registry to populate.
|
|
37
|
+
*/
|
|
38
|
+
import type { BrigadeConfig } from "../../../config/types.js";
|
|
39
|
+
import { type ChannelAdapter, type ChannelOutboundTarget, type ChannelPlugin, type StartChannelsArgs } from "../sdk.js";
|
|
40
|
+
import { type ResolvedDiscordAccount } from "./account-config.js";
|
|
41
|
+
import { type DiscordProbeResult } from "./probe.js";
|
|
42
|
+
import { type DiscordPermissionAuditResult } from "./permission-audit.js";
|
|
43
|
+
/** Dependencies the gateway hands the plugin to drive turns + replies. */
|
|
44
|
+
export interface DiscordPluginDeps {
|
|
45
|
+
/** Boot-time default agent for routing fallbacks. */
|
|
46
|
+
defaultAgentId: string;
|
|
47
|
+
/** Active gateway config — re-read fresh per inbound for live policy. */
|
|
48
|
+
loadConfig: () => BrigadeConfig;
|
|
49
|
+
/** Run one agent turn (the gateway's serialised turn executor). */
|
|
50
|
+
runTurn: StartChannelsArgs["runTurn"];
|
|
51
|
+
/**
|
|
52
|
+
* Optional adapter factory — tests inject a fake; production uses
|
|
53
|
+
* `createDiscordAdapter`. Receives the per-account scope.
|
|
54
|
+
*/
|
|
55
|
+
adapterFactory?: (args: {
|
|
56
|
+
accountId: string;
|
|
57
|
+
}) => ChannelAdapter;
|
|
58
|
+
}
|
|
59
|
+
/** Probe result + optional channel-permission audit (Phase 5 diagnostics). */
|
|
60
|
+
export type DiscordProbeWithAudit = DiscordProbeResult & {
|
|
61
|
+
/** Channel-permission audit for the configured guild channels, when run. */
|
|
62
|
+
permissionAudit?: DiscordPermissionAuditResult;
|
|
63
|
+
};
|
|
64
|
+
/** Operator-grade view of a per-account bot — exposed via attached helpers. */
|
|
65
|
+
export interface DiscordPluginRuntimeView {
|
|
66
|
+
/** Currently-running account ids. */
|
|
67
|
+
startedAccountIds(): string[];
|
|
68
|
+
/** Look up the per-account adapter (or undefined when the account isn't started). */
|
|
69
|
+
getAdapter(accountId: string): ChannelAdapter | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Run a `/users/@me` probe for an account (for status / doctor). Also runs the
|
|
72
|
+
* channel-permission audit over any configured guild channels (Phase 5).
|
|
73
|
+
*/
|
|
74
|
+
probeAccount(accountId: string, cfg: BrigadeConfig): Promise<DiscordProbeWithAudit>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Collect the numeric guild channel ids configured under
|
|
78
|
+
* `channels.discord.guilds.<guildId>.channels.<channelId>` so the permission
|
|
79
|
+
* audit knows which channels to check. Non-numeric keys are passed through too —
|
|
80
|
+
* the audit reports them as unresolved. Returns [] when none are configured.
|
|
81
|
+
*/
|
|
82
|
+
export declare function collectConfiguredDiscordChannelIds(cfg: BrigadeConfig): string[];
|
|
83
|
+
/** Plugin handle with the extra per-account introspection surface attached. */
|
|
84
|
+
export type DiscordPluginHandle = ChannelPlugin<ResolvedDiscordAccount> & DiscordPluginRuntimeView;
|
|
85
|
+
/** Construct the plugin instance, capturing per-account runtime state in closure. */
|
|
86
|
+
export declare function createDiscordPlugin(deps: DiscordPluginDeps): DiscordPluginHandle;
|
|
87
|
+
/** Outbound dispatch helper for callers reaching the plugin directly. */
|
|
88
|
+
export type DiscordOutboundTarget = ChannelOutboundTarget;
|
|
89
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAON,KAAK,cAAc,EAOnB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAKlB,KAAK,iBAAiB,EAEtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAMN,KAAK,sBAAsB,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAkC,KAAK,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAoB1G,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IACjC,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,UAAU,EAAE,MAAM,aAAa,CAAC;IAChC,mEAAmE;IACnE,OAAO,EAAE,iBAAiB,CAAC,SAAS,CAAC,CAAC;IACtC;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,cAAc,CAAC;CACjE;AAED,8EAA8E;AAC9E,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,GAAG;IACxD,4EAA4E;IAC5E,eAAe,CAAC,EAAE,4BAA4B,CAAC;CAC/C,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,wBAAwB;IACxC,qCAAqC;IACrC,iBAAiB,IAAI,MAAM,EAAE,CAAC;IAC9B,qFAAqF;IACrF,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;IAC1D;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACpF;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,EAAE,CAe/E;AAED,+EAA+E;AAC/E,MAAM,MAAM,mBAAmB,GAAG,aAAa,CAAC,sBAAsB,CAAC,GAAG,wBAAwB,CAAC;AAqBnG,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,CA4QhF;AAOD,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,GAAG,qBAAqB,CAAC"}
|