Authenticated remote Discord ingress
POST /v1/discord/ingress accepts a connection's sealed, fleet-permitted text
turn or connection-owned voice callback. It is not an operator bearer API. The public gateway forwards this route's
own encrypted envelope; neither message text nor the answer is plaintext there.
The managed body's broker owns a P-256 ingress key, registered through the signed
/fleet/v1/body/discord-key call at boot. Failed registration retries without
preventing the rest of the body from starting. Unmanaged bodies do not enable
this hosted ingress automatically.
The contracts are exported from @clankie/protocol/discord-ingress and the Node
cryptographic client from @clankie/protocol/discord-ingress-crypto.
- The trusted connection creates a strict
DiscordIngressEvent: tenant and installation, delivery/message/channel/actor ids, addressed kind, verified owner flag, bounded content/images, original timestamp and at most five-minute expiry. An end user's message cannot choose identity or authority. prepareDiscordIngress(event, registeredPublicKey)creates an ephemeral P-256 response recipient and nonce. Its digest is SHA-256/base64url of UTF-8JSON.stringify(["clankie-discord-ingress-v1", parsedEvent, ephemeralPublicKey, nonce]). Binding the recipient prevents a gateway from re-encrypting known text under its own key to steal a private answer.- The connection gets a 60-second fleet permit for that digest, tenant and
installation. Its Ed25519 header/claims have distinct
clankie-discordtype,clankie-bodyaudience, and the body's existing fleet trust roots. prepared.seal(permit)creates the HTTP envelope. P-256 ECDH and HKDF-SHA256 derive an AES-256-GCM key. The salt is the 16-byte nonce; info is the domain, tenant and installation joined by newlines. Each ciphertext is IV(12), data, tag(16), base64url. AAD appends request/response and the exact permit. Callprepared.destroy()when done.- The body verifies before admission, durably records the delivery, starts the existing Discord captain flow, and returns a sealed result. Poll with a fresh permit/envelope for the same event until reply/silent/failed; pending does not start another turn. The verified owner gets the existing machine-authority session separation. Server ownership, audience privacy and bounded room skills remain subject to ADR 0251; buffered context and a wake trigger confer no machine authority.
On a crash after admission, an uncertain pending turn becomes interrupted.
It is not automatically repeated: repeating a shell side effect is worse than
asking the customer to retry explicitly. A different event under an existing
delivery id is a conflict. Input errors expose only codes. Admission/result
files are private service state, not telemetry; they contain no Discord token.
The hosted edge, OAuth install UI, tenant routing and wake/allowance policy live in the private operations repository. The official shared bot token never goes into this service or a tenant's credential broker.
kind: "message" is unaddressed guild chat admitted by the owner's wake
trigger (discord.wakeTrigger: mention, name or any; VUH-1765; addressed
is the earlier spelling of mention, still accepted). Any
event except voice may carry context: at most 20 buffered channel messages
before it, oldest first, which the body passes to the captain as the turn's
context messages. Context is never a trigger. The model sees each context
message as one JSON record with its connection-read author id, so no body can
pass as another speaker's line. A turn that holds machine tools acts only on the
trigger, never on an earlier context message
(ADR 0251, VUH-2591). Default remote text still
requires explicit addressing; channels opt in to buffering separately
(discord.ambientChannelIds).
Bring-your-own bot and user-session text bridges preserve the persona's reply
policy and live-conversation window when wakeTrigger is unset. An explicit
mention or name setting admits only its selected triggers, including after a
reply or inbox restart; ordinary follow-ups do not become new model turns.
Direct replies, admitted DMs and commands remain addressed events. The hosted
edge's encrypted ambient buffer and budget ledger live in clankie-ops.
Self-hosted official bot
A self-hosted machine signed in with its Clankie account can receive the same
envelopes (VUH-1766). With discord.officialBotEnabled, the service keeps a
P-256 ingress key in its credential broker and registers it at
POST /fleet/v1/self-hosted/discord/register with the account bearer. The
fleet answers with the route id the permits name and its public Ed25519 permit
keys. The edge then delivers through the public gateway to the machine's
account-derived host. Until registration succeeds, the route answers 503, and
while the official bot is off it answers 404. GET/POST /v1/discord/official
reports and changes this in the running service, without a restart
(CLI and route contract).
The edge's owner flag grants nothing on this route: the machine's own Discord
settings decide authority, exactly as for a bring-your-own bot. Contracts are in
@clankie/protocol/official-discord.
If the fleet report is unavailable, the status retains the local enabled and
running facts, omits official, and returns fleetError plus a plain-language
next step. This does not establish which server is connected. A refused on
leaves the settings unchanged; off still stops the local ingress even when
the account report cannot be read. Clients refresh through the same route.
If account removal fails, unregister_failed:* and next keep that incomplete
disconnect explicit. A refresh also reports it while this installation remains
registered with its local switch off; an explicit off retries removal.
Voice callbacks use kind: "voice" and one strict voice.action: briefing,
handoff, or self_tool. Briefing and the three existing voice self-tools use
the service's existing voice routes; they grant no general captain or operator
bearer. Handoffs require voice_event with the same guild, channel and actor as
the sealed event and the bot transport. The body supplies the hosted identity,
verified owner proof and live source fence before entering the existing captain
flow. Approval prompts remain on the authenticated operator surface.
Callbacks and replies use the same recipient-bound encryption, durable admission,
retry and restart behavior as text. Call audio and provider credentials belong
to the trusted connection; this callback contract never transports a shared bot
token. state: "voice" carries the bounded operation result inside the sealed
reply. Hosting, charging and rollout records remain in the private operations
repository.
