@spinabot/brigade 1.6.1 → 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/channels/discord/account-config.d.ts +60 -0
- package/dist/agents/channels/discord/account-config.d.ts.map +1 -1
- package/dist/agents/channels/discord/account-config.js +89 -0
- package/dist/agents/channels/discord/account-config.js.map +1 -1
- package/dist/agents/channels/discord/adapter.d.ts +24 -1
- package/dist/agents/channels/discord/adapter.d.ts.map +1 -1
- package/dist/agents/channels/discord/adapter.js +208 -41
- package/dist/agents/channels/discord/adapter.js.map +1 -1
- 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 +78 -0
- package/dist/agents/channels/discord/components.d.ts.map +1 -1
- package/dist/agents/channels/discord/components.js +89 -0
- package/dist/agents/channels/discord/components.js.map +1 -1
- package/dist/agents/channels/discord/connection.d.ts +195 -12
- package/dist/agents/channels/discord/connection.d.ts.map +1 -1
- package/dist/agents/channels/discord/connection.js +852 -38
- package/dist/agents/channels/discord/connection.js.map +1 -1
- 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/format.d.ts +15 -0
- package/dist/agents/channels/discord/format.d.ts.map +1 -1
- package/dist/agents/channels/discord/format.js +56 -0
- package/dist/agents/channels/discord/format.js.map +1 -1
- 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 +166 -9
- package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -1
- package/dist/agents/channels/discord/inbound-extras.js +246 -8
- package/dist/agents/channels/discord/inbound-extras.js.map +1 -1
- package/dist/agents/channels/discord/index.d.ts +10 -3
- package/dist/agents/channels/discord/index.d.ts.map +1 -1
- package/dist/agents/channels/discord/index.js +10 -3
- package/dist/agents/channels/discord/index.js.map +1 -1
- 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/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 +18 -2
- package/dist/agents/channels/discord/plugin.d.ts.map +1 -1
- package/dist/agents/channels/discord/plugin.js +73 -4
- package/dist/agents/channels/discord/plugin.js.map +1 -1
- package/dist/agents/channels/discord/probe.d.ts +23 -1
- package/dist/agents/channels/discord/probe.d.ts.map +1 -1
- package/dist/agents/channels/discord/probe.js +40 -5
- package/dist/agents/channels/discord/probe.js.map +1 -1
- 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 +5 -3
- package/dist/agents/channels/inbound-pipeline.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/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/package.json +1 -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":"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"}
|
|
@@ -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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"}
|
|
@@ -39,6 +39,7 @@ import type { BrigadeConfig } from "../../../config/types.js";
|
|
|
39
39
|
import { type ChannelAdapter, type ChannelOutboundTarget, type ChannelPlugin, type StartChannelsArgs } from "../sdk.js";
|
|
40
40
|
import { type ResolvedDiscordAccount } from "./account-config.js";
|
|
41
41
|
import { type DiscordProbeResult } from "./probe.js";
|
|
42
|
+
import { type DiscordPermissionAuditResult } from "./permission-audit.js";
|
|
42
43
|
/** Dependencies the gateway hands the plugin to drive turns + replies. */
|
|
43
44
|
export interface DiscordPluginDeps {
|
|
44
45
|
/** Boot-time default agent for routing fallbacks. */
|
|
@@ -55,15 +56,30 @@ export interface DiscordPluginDeps {
|
|
|
55
56
|
accountId: string;
|
|
56
57
|
}) => ChannelAdapter;
|
|
57
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
|
+
};
|
|
58
64
|
/** Operator-grade view of a per-account bot — exposed via attached helpers. */
|
|
59
65
|
export interface DiscordPluginRuntimeView {
|
|
60
66
|
/** Currently-running account ids. */
|
|
61
67
|
startedAccountIds(): string[];
|
|
62
68
|
/** Look up the per-account adapter (or undefined when the account isn't started). */
|
|
63
69
|
getAdapter(accountId: string): ChannelAdapter | undefined;
|
|
64
|
-
/**
|
|
65
|
-
|
|
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>;
|
|
66
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[];
|
|
67
83
|
/** Plugin handle with the extra per-account introspection surface attached. */
|
|
68
84
|
export type DiscordPluginHandle = ChannelPlugin<ResolvedDiscordAccount> & DiscordPluginRuntimeView;
|
|
69
85
|
/** Construct the plugin instance, capturing per-account runtime state in closure. */
|
|
@@ -1 +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;
|
|
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"}
|
|
@@ -43,6 +43,9 @@ import { buildBundledCommands, createInboundPipelineContext, createSubsystemLogg
|
|
|
43
43
|
import { listDiscordAccountIds, resolveDiscordAccount, resolveDiscordBotToken, DISCORD_CHANNEL_ID, DISCORD_DEFAULT_ACCOUNT_ID, } from "./account-config.js";
|
|
44
44
|
import { createDiscordAdapter, DISCORD_CAPABILITIES } from "./adapter.js";
|
|
45
45
|
import { probeDiscord } from "./probe.js";
|
|
46
|
+
import { auditDiscordChannelPermissions } from "./permission-audit.js";
|
|
47
|
+
import { collectDiscordSecurityAuditFindings } from "./security-audit.js";
|
|
48
|
+
import { collectDiscordStatusIssues } from "./status-issues.js";
|
|
46
49
|
const log = createSubsystemLogger("channels/discord/plugin");
|
|
47
50
|
// Single source of truth for the channel's user-facing metadata lives in the
|
|
48
51
|
// import-light `bundled-channel-metas` module (re-exported via the SDK barrel),
|
|
@@ -50,6 +53,32 @@ const log = createSubsystemLogger("channels/discord/plugin");
|
|
|
50
53
|
// `DISCORD_CHANNEL_META.id` is the same canonical `"discord"` string as
|
|
51
54
|
// `DISCORD_CHANNEL_ID`.
|
|
52
55
|
const DISCORD_META = DISCORD_CHANNEL_META;
|
|
56
|
+
/**
|
|
57
|
+
* Collect the numeric guild channel ids configured under
|
|
58
|
+
* `channels.discord.guilds.<guildId>.channels.<channelId>` so the permission
|
|
59
|
+
* audit knows which channels to check. Non-numeric keys are passed through too —
|
|
60
|
+
* the audit reports them as unresolved. Returns [] when none are configured.
|
|
61
|
+
*/
|
|
62
|
+
export function collectConfiguredDiscordChannelIds(cfg) {
|
|
63
|
+
const slot = cfg.channels?.[DISCORD_CHANNEL_ID];
|
|
64
|
+
const guilds = slot && typeof slot === "object" ? slot.guilds : undefined;
|
|
65
|
+
if (!guilds || typeof guilds !== "object")
|
|
66
|
+
return [];
|
|
67
|
+
const ids = new Set();
|
|
68
|
+
for (const guildValue of Object.values(guilds)) {
|
|
69
|
+
if (!guildValue || typeof guildValue !== "object")
|
|
70
|
+
continue;
|
|
71
|
+
const channels = guildValue.channels;
|
|
72
|
+
if (!channels || typeof channels !== "object")
|
|
73
|
+
continue;
|
|
74
|
+
for (const channelKey of Object.keys(channels)) {
|
|
75
|
+
const key = channelKey.trim();
|
|
76
|
+
if (key)
|
|
77
|
+
ids.add(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [...ids];
|
|
81
|
+
}
|
|
53
82
|
/** Build the per-account approval capability — the native component prompt + approver gate. */
|
|
54
83
|
function buildApprovalCapability(adapter, accountId) {
|
|
55
84
|
return {
|
|
@@ -200,13 +229,28 @@ export function createDiscordPlugin(deps) {
|
|
|
200
229
|
probeAccount: async (accountId, cfg) => {
|
|
201
230
|
const token = resolveDiscordBotToken(cfg, accountId);
|
|
202
231
|
const result = await probeDiscord({ token });
|
|
232
|
+
// Channel-permission audit over any configured guild channels (Phase 5).
|
|
233
|
+
// Best-effort: skipped when the token / probe failed (nothing to check
|
|
234
|
+
// against), or when no channels are configured.
|
|
235
|
+
let permissionAudit;
|
|
236
|
+
const channelIds = collectConfiguredDiscordChannelIds(cfg);
|
|
237
|
+
if (result.ok && token && channelIds.length > 0) {
|
|
238
|
+
try {
|
|
239
|
+
permissionAudit = await auditDiscordChannelPermissions(token, channelIds);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
/* never fail the probe on the audit */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
203
245
|
// Surface the started adapter's liveness signal alongside the /users/@me
|
|
204
246
|
// reachability check (observability only — never changes `ok`).
|
|
205
247
|
const live = accountRuntimes.get(accountId)?.adapter;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
248
|
+
const lastEventAt = live && typeof live.lastEventAt === "function" ? live.lastEventAt() : undefined;
|
|
249
|
+
return {
|
|
250
|
+
...result,
|
|
251
|
+
...(lastEventAt !== undefined ? { lastEventAt } : {}),
|
|
252
|
+
...(permissionAudit ? { permissionAudit } : {}),
|
|
253
|
+
};
|
|
210
254
|
},
|
|
211
255
|
config: {
|
|
212
256
|
listAccountIds: (cfg) => listDiscordAccountIds(cfg),
|
|
@@ -294,6 +338,31 @@ export function createDiscordPlugin(deps) {
|
|
|
294
338
|
{ path: "channels.discord.accounts.*.botToken", description: "Discord bot token (per account)" },
|
|
295
339
|
],
|
|
296
340
|
},
|
|
341
|
+
// Supplementary security audit (Phase 5): warn on name-based (mutable)
|
|
342
|
+
// allow-list entries. Consumed by `brigade doctor` via the central
|
|
343
|
+
// `channel-security-registry.ts` collector.
|
|
344
|
+
security: {
|
|
345
|
+
collectAuditFindings: (ctx) => collectDiscordSecurityAuditFindings({ cfg: ctx.sourceConfig, accountId: ctx.accountId }),
|
|
346
|
+
},
|
|
347
|
+
// Structured status rollup (Phase 5): intent + permission issues. The
|
|
348
|
+
// central status surface stashes the probe/audit on each account snapshot
|
|
349
|
+
// (under `probe` / `audit`); this adapts those into Discord status issues.
|
|
350
|
+
status: {
|
|
351
|
+
collectStatusIssues: (accounts) => collectDiscordStatusIssues(accounts.map((snap) => {
|
|
352
|
+
const s = snap;
|
|
353
|
+
return {
|
|
354
|
+
accountId: typeof s.accountId === "string" ? s.accountId : "",
|
|
355
|
+
...(s.probe ? { probe: s.probe } : {}),
|
|
356
|
+
// The audit may ride on the probe (probeAccount attaches it) or be
|
|
357
|
+
// stashed directly on the snapshot.
|
|
358
|
+
...(s.probe?.permissionAudit
|
|
359
|
+
? { audit: s.probe.permissionAudit }
|
|
360
|
+
: s.audit
|
|
361
|
+
? { audit: s.audit }
|
|
362
|
+
: {}),
|
|
363
|
+
};
|
|
364
|
+
})),
|
|
365
|
+
},
|
|
297
366
|
};
|
|
298
367
|
}
|
|
299
368
|
/** Default adapter factory — threads the per-account scope. */
|