docs/activity.md

Activity sharing

Clankie can share an existing game, delivered image, animation or finite demo through a read-only Discord Activity. The Activity renders PNG and bounded PCM; it has no authority to capture files, browse the machine, or control the source.

Use clankie share list and clankie share request JSON, or /share in either console. The same POST /v1/activity/shares contract reaches the local service with the operator bearer or the hosted service over the current encrypted paired device connection. ClankieApiClient.activityShares(request) exposes its typed request and response schemas. The CLI reference gives complete examples.

SourceRequest selectionBoundary
Current Pokémon or Minecraft playaction:"start", sourceId:"play"Existing authorized producer; no new player or capture grant
Delivered PNGaction:"image", conversationId, artifactIdExact delivered conversation/artifact and stored digest
Delivered PNG/GIF/MP4/WAV/MP3action:"start", sourceId:"artifact:CONVERSATION_ID:ARTIFACT_ID"Registered artifact ID; GIF/MP4/WAV/MP3 at most 32 MiB and 120 seconds

list returns active session metadata. switch names the current share ID and generation and chooses one registered source or exact conversation/artifact pair. stop names the current share ID and generation. The service assigns the tenant and current installation; a caller cannot pass a source URL, arbitrary path or capture permission. Shares last 30 minutes by default, at most two hours.

Where an official launch/stop adapter is configured, controls return a receipt with confirmed, refused or uncertain outcome, request identity and exact session. A confirmed launch may include its Discord invite. Receipt uncertainty does not authorize a retry: list active metadata and reconcile before deciding another action. A local stream without the adapter makes no claim that Discord launched.

Discord room turns refuse start, image and switch with activity_official_bot_required when no launch adapter is available. A self-hosted official bot with Activity admission configured launches room shares itself: start and image open the share in the voice channel the bot occupies in that room's server (another channel is refused), then the room's own Discord body posts the invite through its watch_start action, whose presence write records the launch for admission. Its receipt is confirmed, refused (the share is ended) or uncertain (the body may have acted; the share is kept and nothing is resent). stop ends the share and revokes the invite through watch_stop. Room share operations run one at a time across turns, so a stop waits for a pending launch and no older launch can republish after it. A stop also revokes every room launch reserved in that channel, so a body call that outlived the tool's timeout is refused when its presence write finally arrives: it posts no invite and registers no launch. A launch already inside its Discord effect is checked again at each invite, delete and message step and takes back anything that landed; a stop confirms only once that has finished, and otherwise reports uncertain. Local owner API/CLI requests can still create and switch delegated shares without an adapter.

Self-hosted official viewer

A self-hosted bot with an Activity application id and its OAuth2 client secret admits voice-channel viewers of its own /activity launch (ADR 0295). Store the secret in the console at /discord → Tokens → Activity client secret; it lives only in the credential broker as discord_activity_client_secret. Without it, admission fails closed with a 503 naming that step.

The activity server relays the SDK's code and instance ID to the service. The service exchanges the code, reads the Activity instance with the bot token, and admits only a participant of an instance in a channel where Clankie's bot posted the launch. The first admission while play is running opens that channel's play share. Grants last five minutes; the viewer renews before expiry, and each renewal re-reads the instance, so a viewer who leaves keeps media for at most about five minutes (VUH-2834).

Official hosted viewer

The locally bundled Embedded App SDK performs ready, authorize and authenticate. The browser exchanges its code and instance ID through POST /.proxy/activity/admit; the server determines the authenticated user's current Activity, guild, channel, tenant and installation. The returned read-only grant admits the media socket in its first message. The browser uses neither SDK guild/channel claims nor fragments to select a hosted destination. This follows Discord's supported Activity handshake.

The initial socket admission has a short, one-use deadline. Ongoing viewing is bounded by the share's expiry, with audience authority revalidated at most every 15 seconds; the initial deadline does not end an already admitted viewer. Permission revocation has an approximately 15-second window while verified audience evidence expires. Each hosted media stream is capped at 256 MiB. The gateway ends media on failed proof, revocation or installation replacement. Configuration, authentication and admission errors open no media connection. Official mode has no anonymous legacy fallback. The SDK uses only the viewer's ephemeral user OAuth token; bot, client-secret and media-control credentials stay server-side. Customers do not register an application, configure a tunnel or handle bot keys.

Local delegated viewer

Self-hosted local mode retains /#share=SHARE_ID&grant=GRANT. Read-only grants remain in the fragment; never substitute an operator or producer bearer. This is delegated access, not proof of Discord membership. Participant admission for voice-channel viewers is the self-hosted official viewer above.

Grants last at most five minutes and terminate existing admitted viewers at expiry. Source switch clears old image/text/audio, advances the generation and invalidates old grants for new joins. Current viewers follow the switch until their original grant expires. Stop, share expiry, producer loss and revocation are terminal; reconnect cannot restore ended media or open a different share. The scoped stream never feeds public legacy /frames.

Local verification and remaining live gate

pnpm --filter @clankie/discord-activity test:browser builds the SDK bundle and runs actual Chromium against loopback HTTP and WebSockets. It proves native PNG decoding, Web Audio construction, two-tenant separation, source switch, terminal stop/revocation, viewing beyond the initial admission deadline, configuration failure and refused/anonymous admission. The Discord RPC and admission provider boundaries use local fixtures; no Discord account, tunnel or external media provider is contacted.

Official application registration and deployment remain prerequisites. Recorded Discord iframe, launch, routing and playback checks remain pending; agents can perform the technical verification once that setup is authorized and deployed. Local browser evidence does not establish those live results. See the wire reference and ADR 0233.