Imported from BlackBeltTechnology/pi-agent-dashboard (
packages/chat-gateway/src/adapters/AGENTS.md). Install upstream withnpx skills add BlackBeltTechnology/pi-agent-dashboard --skill adapters. Copyright stays with the author.
DOX — packages/chat-gateway/src/adapters
Files in this directory. One row per source file. See change: add-chat-gateway.
| File | Purpose |
|---|---|
base.ts |
VENDORED from @gamalan/pi-gateway@1.10.1 (MIT — see ../../NOTICE) with ADDITIVE changes only: InteractiveResponse gained an optional userId, and the channel-provisioning surface was added (ChannelOverwrite, ProvisionChannelInput, and the OPTIONAL provisionChannel/setChannelOverwrites/renameChannel). The provisioning methods are optional by design — a platform without channel management is unaffected, and the provisioner fails closed (platform_cannot_provision) rather than appearing healthy. provisionChannel MUST carry overwrites in the create call and MUST NOT create-then-patch; setChannelOverwrites MUST be awaited (a caller may not report success before the platform reflects the new access). The platform-adapter contract: PlatformAdapter (initialize/start/stop/sendMessage/editMessage/deleteMessage/setTyping/getStatus/sendInteractive/cleanupInteractive + the three provisioning methods), BaseAdapter with its generic text fallback, and the InteractivePrompt/InteractiveResponse/PlatformMessage/AdapterCallbacks/PlatformConfig shapes. userId is REQUIRED for L1/L4 authorization of a click (a group-channel member who is not allowlisted can see the buttons, so rendering is not a grant); missing ⇒ treated as unknown ⇒ refused. Pure — no imports, no side effects. Re-vendor the rest; only these are intentionally added. |
discord-payload.ts |
PURE prompt↔Discord mapping, deliberately free of any discord.js import so it is unit-testable with no client and no network. toDiscordControl (select→string-select, confirm→Yes/No buttons, input/editor→modal, notify→message, unknown→message, >25 options degrades to message rather than silently truncating), the length-safe round-trippable customIdFor/parseCustomId codec (inline p1|… under Discord's 100-char budget, else a bounded p2|token memo), DISCORD_MESSAGE_LIMIT, and chunkForDiscord (splits on the last newline/space; hard-splits an over-long token; never truncates; join("") === input). Also owns the CHANNEL-PROVISIONING payload: channelCreatePayload/channelOverwrites PREPEND the platform-default-role deny (@everyone's id IS the guild id, so the deny travels in the create call with no lookup) and there is deliberately NO create-without-overwrites shape, so no path can open a world-readable window; channelNameFor slugifies and caps at Discord's 100 chars. VIEW_CHANNEL is a literal (1n << 10n) to keep this module free of discord.js. And the delegation RULE (assignersForRole over MemberSummary/RoleSummary): the owner always qualifies, anyone else needs Manage Roles AND a highest-role position STRICTLY ABOVE the target, because Discord refuses a manager assigning at or above their own level — naming them would promise a delegation the platform rejects. RoleAssigners = assigners |
discord.ts |
The discord.js transport implementing the vendored contract — REPLACES upstream's discord.ts (which used the ambient global WebSocket, derived the bot id from the token prefix, and re-armed a reconnect timer from onclose after stop()). Transport-only by design (task 2.2): no child_process/spawn, no --mode rpc, no createServer/node:http/node:net — it never learns about sessions or servers. stop() sets a stopped flag BEFORE disconnecting so nothing reconnects. sendInteractive/handleInteraction defers before work that may exceed Discord's 3s ack window (C5/F5) so the client never sees "interaction failed"; every emitted InteractiveResponse carries interaction.user.id for edge authorization. initialize() creates the client + logs in (the server entry MUST call it before start()); allowedChannels drops non-opted-in guild channels at the adapter; thread messages carry metadata.parentChannelId. Channel provisioning goes through the injectable DiscordChannelOps port (create/update/guildIdFor) so a test records the FULL call sequence with no connection; the default impl wraps guild.channels.create/channel.edit and maps the dependency-free overwrite data onto discord.js's resolvable shape. Role delegation is a SECOND injectable port, DiscordMemberOps (assignersForRoles), BATCHED so R roles cost ONE members.fetch() + ONE roles.fetch() rather than R of each. Its real impl reads the role list only when it holds Manage Roles and returns unavailable naming the missing permission otherwise, and names the Server Members intent when members.fetch() fails (the privileged-intent cause an operator can act on) — never an empty list. toPlatformMessage (module-level, extracted to keep handleMessage within the complexity budget) sets bot/webhook/roleIds so the EDGE can refuse non-humans and resolve role→tier, not just this adapter's own drop. |
fake.ts |
Socket-less FAKE platform for the L3 harness (PI_CHAT_GATEWAY_FAKE=1 lists delegation, =nolist reports it unavailable), ENV-GUARDED like PI_BROWSER_RELAY_FAKE so no normal install can construct it — the docker harness carries no Discord credential, and five of the seven L3 team-controls rows need a platform to talk TO. Driven over the FILESYSTEM, deliberately not a wire surface: a spec APPENDS to inbound.jsonl to act as a chat user and READS outbound.jsonl for what the layer posted (mirror frames, elision markers, refusal replies), so there is no test-only plugin message an install could reach. createFakeAdapterFromEnv is the only entry point; initialize() truncates both scratch files so a spec starts from empty and its assertions are exact rather than "somewhere in history". The dir override treats an EMPTY PI_CHAT_GATEWAY_FAKE_DIR as unset (override.length > 0, not ??): docker compose passes the empty string through for an unset var, so ?? yielded dir: "" and the fixture died in mkdir ''. Delegation reuses the REAL assignersForRole rule INCLUDING a non-owner who qualifies — a stub answering by a different rule would make F1/F2 prove nothing about the shipped rule — and nolist answers EVERY asked role with unavailable naming the missing permission, never a missing key (the caller reads a missing key as an empty roster) and never an empty assigners list. |
__tests__/recording-adapter.ts |
The in-memory L3 fake: a full PlatformAdapter that records ordered sent/edited/deleted/typing/interactive/cleaned calls, with emitMessage/emitInteractiveResponse drivers and reset(). Keeps the F/X rows deterministic in CI with no live Discord. Also the CHANNEL-PROVISIONING fixture (10i.1): provisionCalls records every create/set-overwrites/rename IN ORDER with timestamps, provisionCallsOfKind filters them (the create-then-patch assertion), and failProvision/failOverwrites/overwriteDelayMs+releaseHeldOverwrites inject the fault and slowness the X1/X3/X4 rows need. |