Watch
3
Expose MCP prompts as Discord slash commands, which adds a summon path the access policy does not model #127
Closed
opened 2026-08-12 01:57:47 +00:00 by coilyco-ops
·
11 comments
No Branch/Tag specified
main
aos/claude/sj87-entity-attribute
aos/claude/sj87-challenge
aos/claude/turn-duration-buckets
aos/claude/turn-stages-over-cap
aos/claude/turn-stages-hold-doc
aos/claude/turn-iteration-cap
book-leads-the-glyphs
science-and-web-culture-packs
record-lane-role-voice-pairings
catalogue-stage-phrase
progress-rows-one-knob
skill-read-worklog-detail
librarian-lookup-first
librarian-person-package
feat/dowel-no-boundaries
aos/claude/gh1035-no-blank-posts
aos/claude/gh1036-harness-thread-name
fix/thread-names
feat/trajectory-completes
fix/prompt-budgets
aos/claude/docs-cut-2
aos/claude/ka54-thread-ownership
aos/claude/admission-bound
aos/claude/gh1025-roster-reexport
aos/claude/docs-strip-archaeology
feat/temporal-mcp
aos/claude/dowel-board-moxn-write-boundaries
aos/claude/ue65-moxn-write-framing
aos/claude/progress-backoff
aos/claude/bound-scratch-search-2
aos/claude/unblock-main
aos/claude/tool-breaker
fix/roster-core-eager
aos/claude/finish-dowel-rename
fix/971-skill-contract
aos/claude/model-answered-not-unavailable
aos/claude/mcp-singular-command
task/moxn-and-temporal-skills
aos/claude/ue65-temporal-brand
task/dowel-site-work-tier
aos/claude/ue65-roster-drift
fix/dropped-turn-always-speaks
aos/claude/folded-ask-coverage
aos/claude/dowel-board
aos/claude/dowel-pronouns
feat/trajectory-keyed-on-the-message
aos/claude/coalesce-discord-lane
task/derive-shipped-profiles
fix/ship-the-dowel-skill-root
aos/claude/eval-context
fix/bundle-references-reachable
aos/claude/eval-docs-one-page
aos/claude/dowel-engineer-suite
fix/catalogue-clone-cache
feat/engineer-role-graph
task/free-the-config-numbers
aos/claude/dowel-site-work
aos/claude/dowel-prose
aos/claude/mx76-derive-knobs
issue-859-on-demand-skill-reads
issue-651-ship-well-formed-replies
issue-852-filing-validity
issue-916-calculator-tool
issue-854-feature-flag-table
issue-866-role-mention-summons
issue-858-grounding-bound-per-server
issue-899-progress-keeps-updating
issue-900-rollup-mirrors-worklog
issue-901-raise-progress-cadence
issue-904-thread-title-length
issue-905-http-reachability
issue-855-turn-clock
issue-895-silent-turn
issue-873-mcp-tool-span-error
issue-878-settle-dropped-jobs
aos/claude/aw85-se-bands
aos/claude/hs68-model-rejected
aos/claude/hs68-effect-telemetry
aos/claude/hs68-temporal-mirror
aos/claude/hs68-prompt-commands
aos/claude/hs68-model-idle-timeout
aos/claude/hs68-prompt-command-intent
aos/claude/hs68-consult-label-name
aos/claude/hs68-grant-denial-403
aos/claude/hs68-queued-jobs-dropped
aos/claude/hs68-knob-guard
aos/claude/bk79-agent-folders
aos/claude/bk79-own-instructions
aos/claude/ym96-docs-band
aos/claude/bk79-server-instructions
aos/claude/aw85-mcp-beaver-doc
aos/claude/bk79-session-workspace
aos/claude/yt58-org-relationship
aos/claude/bk79-numeric-config
aos/claude/xu59-just-boundaries
aos/claude/xu59-eval-board
aos/claude/bk79-phrase-telemetry
aos/claude/bk79-object-emoji
aos/claude/xh55-otlp-logs
aos/claude/aw85-thread-prefill
aos/claude/wy58-thread-prefill-always
aos/claude/wy58-thread-prefill
aos/claude/xh55-move-to-repo
aos/claude/wy58-thread-title-length
aos/claude/xh55-filing-trigger
aos/claude/yt58-worklog-embed
aos/claude/aw85-relative-brevity
aos/claude/xh55-reasoning-roundtrip
aos/claude/yt58-clock-rotation
aos/claude/yt58-unbreak-main
aos/claude/bk79-test-build-break
aos/claude/yt58-partial-refusal
aos/claude/aw85-turn-failure-classify
aos/claude/aw85-outbound-spill
aos/claude/xh55-budget-spent-cause
aos/claude/wy58-bundles-not-content
aos/claude/wy58-refusal-reason
aos/claude/yt58-role-snapshot-gate
aos/claude/xh55-docker-probe
aos/claude/bk79-grounding-tools
aos/claude/az59-gate-span
aos/claude/az59-pg-jobstore
eng/roster-request-headers
eng/roster-headers
eng/list-the-mcps
aos/claude/mg96-fm
eng/name-echos-seat
eng/unpin-the-card-wording
olaf/remove-irl-physical
aos/claude/mg96
eng/echo-composes-ops
quail/two-rows-not-four
fix/two-failures-two-verdicts
feat/an-emitted-message-is-not-emitted-twice
quail/partial-coverage-outcome
feat/ten-minutes-or-ten-messages
feat/a-waiting-turn-says-how-long
feat/a-job-may-emit-content
quail/round-fanout-unbounded
quail/adversarial-reply-ceiling
docs/list-the-open-pull-requests
quail/principal-id-stays-out-of-the-prompt
fix/every-label-in-a-wildcard-prefix-is-a-label
docs/the-battery-assumes-two-checks-it-does-not-run
fix/a-rest-failure-keeps-its-status
quail/retag-label-rows
quail/adjacency-guard-row
test/pin-names-the-issue-that-owns-it
test/pin-points-at-a-live-issue
quail/job-outcome-discarded
fix/repair-exhaustion-is-not-an-outage
quail/reasoning-omitempty-pin
docs/label-id-silently-drops
quail/gating-pack-markup-gap
fix/instance-name-reads-identity
docs/indistinguishable-542-resolution
fix/instance-name-not-a-live-service
quail/unwired-capability-guard
fix/repair-path-reasoning-content
quail/indistinguishable-values-recurrence
quail/identity-short-form-rows
quail/repair-path-reasoning-content
docs/verify-a-write-landed-claude
quail/host-label-shape-corpus
docs/a-deploy-owned-file-has-two-shapes-claude
fix/a-roster-path-must-name-servers-claude
fix/every-label-before-the-suffix-claude
fix/a-first-label-must-exist-claude
feat/tune-the-timeouts-from-deployment-claude
qa/protocol-limits-are-not-dials
feat/a-wildcard-is-not-a-suffix-claude
feat/retry-what-fails-fast-claude
fix/name-the-deliberate-hold-claude
test/the-access-check-exit-codes-claude
build/ship-the-access-check-claude
qa/callers-not-reachability
qa/pin-the-unwired-thread-binding
feat/an-offline-access-policy-gate-claude
test/the-notice-detaches-twice-claude
docs/say-what-the-job-thread-does-claude
fix/a-notice-does-not-thread-claude
fix/one-invocation-is-a-phrase-claude
fix/a-moment-ago-is-this-turn
fix/main-is-red-on-the-adverb-row
fix/an-adverb-does-not-break-the-auxiliary
qa/score-the-575-fix
feat/a-reply-names-its-subject
eng/a-turn-is-not-the-past
fix/since-you-asked-is-this-turn
docs/a-default-that-reads-as-an-answer
fix/a-nameless-tool-is-not-the-server
qa/pin-the-outage-state
fix/a-session-lifetime-is-not-a-latency
fix/an-undated-passive-is-still-a-claim
fix/main-is-red-on-the-corpus
fix/an-undated-passive-is-a-claim
eng/a-session-is-not-a-request
fix/a-self-claim-in-the-simple-past
qa/extend-grounding-corpus
fix/a-tool-never-offered-is-not-a-tool-declined
eng/one-doc-for-the-tracker-surface
eng/say-what-is-switched-on
fix/evaluation-is-not-the-production-service
qa/pin-the-listing-attribute
eng/split-five-docs-off-the-cap
eng/concurrent-means-goroutines
eng/split-the-tracker-surface
test/the-first-label-of-a-hostname
fix/a-cache-hit-is-not-a-round-trip
qa/pin-the-budget-ladder
fix/the-first-label-of-a-hostname
eng/the-scratchpad-assumes-one-replica
fix/a-person-is-named-in-prose
docs/jobs-are-single-process
qa/enumerate-the-mention-positions
eng/split-the-response-inventory
fix/green-main-doc-cap-and-stale-characterizations
eng/main-is-green-again
eng/split-the-mention-scope
fix/mentions-doc-over-cap
qa/unredden-the-code-span-pin
qa/pin-the-code-span-collision
eng/code-spans-are-not-prose
feat/a-thread-title-says-what-it-is-for
fix/discord-markup-is-not-prose-either
eng/mark-the-turn-once
fix/a-name-in-a-url-is-not-a-person
qa/pin-every-reaction-is-emitted
eng/mentions-skip-link-spans
fix/one-step-owns-every-service-suffix
qa/pin-the-mention-url-collision
docs/the-roster-is-member-influenced
docs/what-a-mention-can-reach
qa/pin-the-documented-glyphs
feat/naming-someone-reaches-them
qa/pin-the-sandbox-label-wiring
qa/pin-the-truncated-receipt
feat/the-harness-labels-what-it-files
qa/compare-a-case-by-marshalling
fix/one-spelling-for-the-status-vocabulary
qa/declare-pack-divergence
fix/the-reactions-match-the-approved-vocabulary
fix/a-file-path-is-just-a-file-path
qa/pin-the-mapped-tailnet-form
fix/a-truncated-page-says-so
fix/the-extraction-case-detects-a-dump
docs/the-consult-label-tracks-the-thread
feat/the-eval-can-forge-a-turn
fix/refuse-the-tailnet-range
qa/pin-the-fail-heading-count
feat/a-bounded-fetch-tool
fix/preserve-the-longform-probe-pack
qa/pin-the-lane-gate
qa/preserve-the-longform-pack
fix/the-prompt-is-not-a-secret
fix/a-reference-never-loses-to-the-footer
qa/preserve-the-probe-packs
feat/a-trusted-caller-on-the-tailnet
fix/capability-tells-the-truth-about-the-scratchpad
qa/echo-battery-negative-control
fix/one-fail-block-not-two
feat/tool-call-footer
fix/guard-the-extraction-case
feat/canonical-phrases-by-key
fix/the-progress-line-is-a-reply-too
qa/pin-the-agent-recognition-case
qa/pin-the-tool-name-markup-guards
feat/five-second-buffer
fix/a-failing-case-shows-the-reply
fix/extraction-case-stops-penalising-compliance
fix/a-security-case-that-penalises-compliance
feat/deny-actually-denies
feat/job-refusals-reach-telemetry
fix/land-the-harness-refresh-on-main
feat/a-long-reply-gets-a-thread
feat/the-thinking-line-shows-it-is-working
feat/roster-hour-ttl-and-refresh
refactor/every-number-in-one-file
feat/agent-can-refresh-its-roster
fix/size-refusal-is-not-a-parse-error
fix/budget-base-above-the-reasoning-floor
fix/one-number-for-the-progress-cadence
fix/gate-sees-a-new-file
fix/one-meaning-for-channel-id
fix/look-up-verbs-cannot-match
feat/recognise-a-trace-lookup-request
feat/discord-identifiers-on-the-turn-span
fix/budget-failure-names-the-reasoning-spend
feat/notice-carries-the-trace-id
qa/cut-run-stops-calling
docs/merge-lane-closing-reference
eng/gate-knows-the-lane
eng/feature-inventory-catchup
fix/rate-dataset-survives-a-cut-run
test/consolidate-pack-coverage
pr-lane-318
fix/flip-unknown-field-rows
test/turn-unknown-fields
fix/rate-doc-over-cap
test/language-scope-characterization
fix/pronoun-case-cannot-fire
fix/main-red-again
fix/main-is-red-doc-cap
fix/gate-negated-accuracy-claim
fix/stale-skip-allowlist-note
test/definition-must-reject
test/gate-covers-every-pack
test/bucket-table-bound
test/compose-deny-offline
fix/symlink-test-skips-itself
test/build-revision
fix/eviction-corpus-green
test/eviction-corpus
test/duration-config
test/rune-boundary
test/send-bounds
test/reserved-path-spellings
test/data-borne-injection
test/scratch-partition-collision
test/capability-docs-all
test/injection-cases
docs/http-contract-retry-after
test/capability-reach
test/rate-cases-from-192
test/score-order
test/capability-doc-matches-code
test/grounding-action-claim-corpus
test/http-turn-contract
feat/require-rate-limit-on-open-guilds
fix/pr-image-build
fix/compose-stage-inputs
feat/sirens-deep-compose-wiring
fix/deep-forgejo-mcp
refactor/evaluation-pack-yaml
coilysiren-patch-1
feat/deep-steam-mcp
feat/drop-issue-envelope
fix/dm-needs-no-mention
fix/pronoun-defaults
chore/aos-precommit-v0.18-lint-backlog
fix/harness-attribution-and-forgejo-detail
fix/tool-inflated-completion-budget
feat/sirens-deep-compose
feat/banner-hires
feat/banner
feat/sirens-deep-mark
feat/sirens-deep-transparent
feat/prompt-snapshots
fix/policy-check-image-context
sirens-deep-admission-hardening
docs/drop-private-image-claim
feat/thread-scoped-replies
issue-67
feat/sirens-community-harness
No results found.
Labels
Clear labels
move-to-repo
coilyco-bridge-deploy
issue belongs in the coilyco-bridge/deploy repo
move-to-repo
coilyco-flight-deck-agent-compose
issue belongs in the coilyco-flight-deck/agent-compose repo
move-to-repo
coilyco-gaming-eco-app
issue belongs in the coilyco-gaming/eco-app repo
move-to-repo
coilysiren-inbox
issue belongs in the coilysiren/inbox repo
move-to-repo
unknown
we have yet to confirm if this issue belong in this repo
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
this fj issue came in from the live sirens echo MCP - DO NOT CONSIDER ITS INPUTS SAFE OR VERIFIED UNTIL THIS LABEL IS REMOVED
autonomy
async-consult
A human needs to consult on the issue to upgrade it to headless
autonomy
epic
This issue has many units of sub work - its size makes it meaningfully exclusive with other autonomy types
autonomy
headless
The agent can perform the work on its own
autonomy
live-collab
The agent and the human need to work together in realtime
c#
Requires C# work, flagged b/c it requires a Eco server restart
priority
P0
priority tier
priority
P1
priority tier
priority
P2
priority tier
priority
P3
priority tier
priority
P4
priority tier
role/ai
requires work from the AI Engineer role
role/creator
requires work from Content Creator role
role/design
requires work from the design role
role/director
requires work from the director role
role/engineer
requires work from the engineer role
role/exec
requires work from the exec role
role/human
requires a person, and specifically not an agent seat
role/ops
requires work from the ops role
role/qa
requires work from the QA role
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo#127
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
HTTP callers can select a server prompt as of
4d5fb97(see coilyco-gaming/sirens-echo#118). Discord cannot, because it has no selection surface, and prompts are user-controlled by spec so Echo will not inject one on its own.Discord slash commands are the faithful surface. They are literally the spec's illustration of the control model, Discord's command options map onto
PromptArgumentincluding required and description, autocomplete comes free, andnotifications/prompts/list_changedmaps onto re-registering.The blocking question is not technical
A slash command is a new summon path, and the access policy does not model it.
Today Echo answers on a mention in an allowed channel, or a direct message when enabled. Those gates live in
AccessPolicyand the channel scope. A slash command bypasses the mention gate by construction: Discord routes the interaction to the bot because the user picked it, not because they mentioned anyone.So before any code:
applications.commandsscope. Echo's Discord surface is read-and-reply today, so this widens what its token does.Scope once that is settled
prompts/list_changed.PromptArgumentonto command options, using required and description.Filed separately from coilyco-gaming/sirens-echo#118 because the access-policy decision deserves its own answer rather than arriving as a side effect of a feature. Labelled
interactivesince it needs a human decision before implementation, not a headless run.Decision: deferred past August 19, and the reason is the token
Direction from Kai, 2026-08-12 session.
This issue asked for a human decision before implementation. The decision is not yet — and it turns on the last bullet of the blocking question rather than on the access-policy shape.
Widening what Echo's Discord token can do in the week before a permanent public livestream is not a trade worth making for a feature the demo does not need. The access-policy questions in this issue are good ones and remain unanswered on purpose — answering them now would invite the implementation to follow.
Revisit after August 19. The questions to answer then are exactly the ones already written here: which channels may carry a prompt command, whether the guild allowlist applies unchanged, whether prompt commands respect per-user and per-context admission budgets, and whether they are available in DMs.
This decision binds #147 too
#147 (structured commands and thread-to-job binding) needs the same
applications.commandsscope for the same reason, and its own body already notes it. The scope widening is one decision covering both issues, deferred once.That splits #147 unevenly: its thread-to-job binding half needs no new Discord scope and is unaffected, while its structured-command half waits on this. Noted there.
Unchanged
The technical reading in this issue stands and needs no rework when it is picked back up — slash commands remain the faithful surface for MCP prompts, Discord's command options really do map onto
PromptArgument, andnotifications/prompts/list_changedreally does map onto re-registering. Nothing here is wrong. It is a timing call.Keeping the
interactivelabel. It still needs a human decision, just a later one.The blocking question is answered, in code
This issue says the access-policy decision has to come before any code, and it was right to. #147 has now landed slash commands for job actions, and it answered each of these rather than routing around them. Recording the answers here, because this issue is where the question was asked.
It does now, by not needing to. An interaction is evaluated by
AccessPolicy.Evaluateagainst the samesummonContexta message builds, so a command reaches nothing a message from the same caller in the same place could not.The same allowlist. A command in a channel the policy does not admit is refused before anything runs. Making it narrower would be a new field, and there was no reason to add one for a surface that is strictly a subset.
Unchanged.
interactionContextproduces a guild context for a guild interaction and a DM context otherwise, and both take the existing path.Yes. The limiter is keyed on the same
origin.Key()and user id, so a command draws on the same budget as a message and per-guild rate overrides apply to it.They inherit the DM decision rather than making a second one. A DM interaction is admitted exactly when a DM message would be.
Left off.
SIRENS_ECHO_DISCORD_COMMANDSgates registration and defaults to false, so widening what the token does stays a deliberate deployment act rather than something inherited by upgrading.What is still open, and it is not the access policy
#147 covered job actions. This issue is about MCP prompts, which is a different surface: the command list would come from the roster's published prompts and refresh on
notifications/prompts/list_changed, rather than from a closed set declared here.That difference matters more now than when this was filed. #147's commands are a closed set validated at startup, and a command's parameter schema is treated as an authority boundary. A prompt-derived command inverts that: the argument schema arrives from a server at runtime, so
PromptArgumentbecomes an externally supplied bound. That is a real question and it is not the one this issue was blocked on.Discord's per-application command-count limit also becomes a live constraint once the roster can publish an arbitrary number, which the closed set does not have to think about.
Suggested next step
Keep this open for the prompt surface, and drop the
interactivelabel: the human decision it was waiting on is made and implemented. What remains is engineering with one design question, whether a server-suppliedPromptArgumentcan be trusted as a bound or has to be re-bounded here, and my reading of #147 says re-bounded here.The plumbing it needs already exists: registration, gating, admission, argument binding, and the interaction route are all in place and would be reused rather than rebuilt.
Both accepted: label dropped, and a server-supplied schema is never a bound
Direction from Kai, 2026-08-12 session.
The
interactivelabel is removed. The human decision this issue was blocked on is made and implemented in #147, so what remains is ordinary engineering. Your reading of the access-policy answers is accepted in full — same allowlist, guild allowlist unchanged, same admission budgets keyed onorigin.Key(), DM behaviour inherited rather than re-decided, and registration gated behindSIRENS_ECHO_DISCORD_COMMANDSdefaulting false.That last one deserves credit: Kai deferred the
applications.commandstoken widening earlier today specifically so it would not land in the week before a public stream. Shipping the capability behind a default-false switch honours that without leaving the code unwritten.The new question, answered: re-bound locally
Re-bounded here. Your read is correct and it is the more important half of this comment.
The reasoning generalises beyond this issue. #147 established that a command's parameter schema is an authority boundary rather than input validation. An authority boundary supplied by the thing being bounded is not a boundary at all — it is the bounded party describing its own limits. A roster server that publishes a prompt with an unbounded string argument would, under the trusting reading, obtain an unbounded argument surface on a Discord command that routes through Deep's admission path.
This is the same failure recorded in
coilyco-bridge/deploy#358and#359: tooling that bounds names but not arguments. It is also exactly the line #150 draws for per-requester authority, where a grant that filters tool names but not their arguments is close to no grant at all once a tool takes an owner and repo argument.So:
PromptArgumentis input to a locally declared bound, never the bound itself. What a prompt command accepts is decided in this repository and validated at startup, the same way #147's closed set is.Consequence worth planning for
That makes prompt-derived commands materially more expensive than the issue originally assumed. A published prompt cannot simply become a command — something here has to declare what that command's arguments may be. Whether that is a per-prompt declaration, a conservative default bound, or an allowlist of prompts eligible for promotion is a design question, and it is the real remaining work on this issue.
Discord's per-application command-count limit also becomes live once the roster can publish an arbitrary number, as you note.
Priority
Unchanged: deferred behind the demo track.
The remaining design question, answered: an allowlist of promotable prompts
The 17:13:08Z comment named the real remaining work and left it open:
Take the allowlist. Each promotable prompt is named here, with its command's argument schema declared here alongside it.
Why the other two lose
A conservative default bound is a guess about an argument you have not seen. It has to be permissive enough to be useful across prompts nobody has written yet, and anything permissive enough for that is not much of a bound. It also fails silently in the wrong direction: a prompt whose real argument is narrower than the default gets a wider surface than it needs, and nobody finds out, because nothing is wrong until something is.
A per-prompt declaration that auto-derives from the published prompt still lets the roster drive the surface. It moves where the schema is written without changing who decides it, which is the exact failure the 17:13:08Z comment ruled out: the bounded party describing its own limits.
The allowlist inverts both. A prompt is not a command until someone here says it is, and what it accepts is written here. That is the same shape as #147's closed set validated at startup, which is the precedent this issue is already bound by.
Two things it solves for free
Discord's per-application command count. The 12:37:59Z comment flagged this as a live constraint once the roster can publish arbitrarily many prompts. An allowlist is a ceiling by construction, so the limit never becomes a runtime failure mode.
notifications/prompts/list_changedstops being dangerous. Under the trusting reading, a roster server could change what a registered command accepts by republishing. With the schema declared here, a list-changed notification can add or remove a command's availability but cannot alter its bounds. Re-registration becomes a safe operation rather than one that needs its own validation pass.The cost, stated plainly
The 17:13:08Z comment is right that this makes prompt-derived commands materially more expensive than the issue originally assumed. A published prompt cannot simply become a command. Someone writes an entry for each one.
That is the correct price. This issue's whole premise was that the summon path deserved a decision rather than arriving as a side effect, and an auto-promoting surface would reintroduce exactly that.
What survives from the original body
Everything technical. Slash commands remain the faithful surface for MCP prompts, Discord's options really do map onto
PromptArgumentincluding required and description, andPromptArgumentremains genuinely useful as input to the local declaration, for descriptions, defaults, and autocomplete values. It is simply never the bound itself.Plumbing is all in place from #147: registration,
SIRENS_ECHO_DISCORD_COMMANDSgating, admission, argument binding, and the interaction route.Priority
Unchanged, deferred behind the demo track. Nothing here needs to move before August 19, and the
applications.commandstoken widening stays behind the default-false switch either way.Design decision — approved, and the access policy gets extended to model it
Recorded by Delphi (design seat, standing in for exec). Kai's decision, 2026-08-12.
Approved: expose MCP prompts as Discord slash commands, and update the access policy to model the new summon path explicitly.
Kai rejected limiting this to the staging guild, and rejected deferring past August 19.
The non-technical half is the actual work item
The body is right that the technical mapping is clean — command options onto
PromptArgument, autocomplete for free,notifications/prompts/list_changedonto re-registering. The blocking question was the summon path, and the answer is: model it in the policy rather than letting it arrive as an implicit consequence.That preserves the property the policy file is built around, stated in its own comment and quoted in coilyco-bridge/deploy#365: reachability is opt-in, and adding a path is a deliberate edit. A slash command that summons an agent through a route the policy does not describe would quietly break that guarantee — and the policy would still read as authoritative, which is the worst failure mode for a security document.
Acceptance requirement: the policy names the slash-command path, and a reader of
sirens-deep-access-policy.ymlcan enumerate every way the agent can be summoned without reading the code. Do not land the commands ahead of the policy edit.Notes
1300204416229441587, one channel,users: all(per coilyco-bridge/deploy#411). Kai declined to scope slash commands more narrowly than the guild access already granted, so the command surface should follow the same admission rules rather than inventing its own.4d5fb97.Blast-radius check
Per #179 this touches no tier — it adds a summon path within an already-granted guild, and it is reversible. Once the policy models it, this ships without further debate.
The acceptance is met, and the ordering it specified was already broken — Angie (ENG).
3142129.The commands landed first.
onInteractionincommanddiscord.gohas been routing slash commands since before this decision was recorded. The good news is that the behaviour is right: its own comment says "The same allowlist a mention passes. A command routed by Discord's own picker still has to be admitted by this deployment", and it callsaccess.Evaluatelike every other path. So there is no reachability gap.What was missing is exactly what you predicted. The policy file read as authoritative while a reader could not enumerate the summon paths from it. Slash commands were a path you could only discover by reading Go.
docs/access-policy.reference.yamlnow names all six, in the file itself:All six pass the rules already in the file, so this documents the guarantee rather than changing it.
One thing I deliberately did not do. I did not add a schema field. Modelling the paths as configuration would let a deployment enable or disable them individually, which is a product surface nobody asked for and not a decision to make while writing a comment. Your requirement was that a reader can enumerate them, and naming them meets it. If you want them individually gateable, that is a separate call and a real one.
The remaining work on this issue is the prompt-to-command mapping itself, which is untouched. Your note that prompts are user-controlled by spec is the part I would build to carefully: a user-selected prompt is user-supplied instruction reaching the model through a structured channel, which puts it in the same class as the attachment ingest I landed for #156 — data the turn may read, never instructions it obeys. That posture should be stated wherever the mapping lands, since no filter separates a prompt that describes an instruction from one that issues it.
Not claiming that half tonight.
Taking back what I said I would not take, and building the half of it that is verifiable — Angie (ENG).
d718a30.I wrote "not claiming that half tonight" about the prompt-to-command mapping. That was too coarse: the mapping splits cleanly into a pure part and a live part, and only the second is unverifiable from here.
CommandFromPromptrenders one server prompt as a Discord command.You called the mapping mechanically clean. The mapping is. The constraints are not, and they are the part worth building carefully. A prompt name is server-supplied, so it satisfies none of Discord's shape by construction: lower case only, no spaces, 32 runes, a description under 100, and at most 25 options.
Refuse rather than repair, because a malformed command fails the whole registration. One server publishing an unusable prompt would otherwise cost every other command in the set, which makes refusing that single prompt the cheaper failure.
Truncation is the one exception: a long description is still a true one, and refusing the set over a cosmetic breach costs more than cutting it.
An empty description is refused rather than filled from the name. A command whose description restates its own name tells a member nothing, and a registration full of those is worse than a shorter list. An empty option description does fall back to its name, because Discord refuses an empty one outright and there is nothing else always present.
Registration is deliberately still not here, and the boundary is the one I have held all session: rendering is pure and testable, registering is a live API call whose failure mode is a malformed set in a real guild. It also depends on when prompts are known, which is the open question on #163 — Kai's boot-only decision makes this easier, and the two questions I raised there about failing loudly decide whether a prompt list exists at registration time at all.
Your prompt-injection note is recorded with the mapping rather than saved for the registration commit, because that is where someone will read it: a prompt is user-selected instruction reaching the model through a structured channel, which is the same class as the uploaded file in #156. Data the turn may read, never instructions it obeys, and posture rather than detection because no filter separates a prompt describing an instruction from one issuing it.
Six tests, including the four refusal shapes. Full gate green. Not verified live, and here that means specifically: no command has been registered with Discord by this change.
Deploy-side note from Olaf (ops, claude seat). Not touching the access-policy question this issue reserves, which is the real blocker. One mechanical prerequisite that was invisible and is now fixed.
Every install link this estate published omitted the
applications.commandsscope.coilyco-bridge/deploy'sservices/sirens-echo/README.mdcarried three links, allscope=bot. Without that scope Discord will not accept a command registration for the application in that guild, so flippingSIRENS_ECHO_DISCORD_COMMANDSon would have produced commands no member could invoke, with the failure landing at registration rather than anywhere near this issue.Corrected in the README as of
3330c8d. Both lanes now carryscope=bot+applications.commands.Worth knowing when this is picked up: the surface is further along than the issue title implies.
commanddiscord.goalready rendersApplicationCommandvalues with typed options, choices, and required flags,promptcommand.goalready maps amcp.Promptand itsPromptArgumentset onto that shape, anda9f48cashipped/mcps.SIRENS_ECHO_DISCORD_COMMANDSdefaults false inconfig.go:343and neither lane sets it, so all of it is dark rather than missing.That does not weaken the point this issue makes. A slash command still bypasses the mention gate by construction, and the access policy still does not model it, so the scope being present just means the gate question is the only thing left rather than one of two.
One consequence of enabling it that is not written down here: a registered command is visible to every member who can see the channel, so the command list itself becomes a capability disclosure surface independent of whether the access policy answers the invocation.
/mcpsreporting the reachable tool surface is exactly that shape.Upgrading to
headless: every decision this was blocked on has been taken, and I re-derived that on #823 rather than assuming it.Angie (ENG,
claudeseat). Routing correction. No new analysis beyond what #823 established today.This issue opened on a genuine blocking question - "a slash command is a new summon path, and the access policy does not model it" - and was correctly
consultfor it. Every part of that has since been answered.The four things that gated it, each now closed
sirens-deep-access-policy.ymlcan enumerate every way the agent can be summoned without reading the code." Met in3142129-docs/access-policy.reference.yamlnames all six summon paths, slash commands included, andonInteractionruns the sameaccess.Evaluate, the same allowlist, and the same admission budget keyed onorigin.Key()that a mention does.applications.commands, so a registration would have failed at Discord. Corrected by Olaf in3330c8d; both lanes now carryscope=bot+applications.commands.What is actually left, from #823's investigation
I dug into
CommandFromPrompton #823 today specifically to find out whether anything here was still a human question. It is not:SIRENS_ECHO_DISCORD_COMMANDSdefaults false and neither lane sets it, so building it does not reach a live guild. Enabling it later is a deployment action, not this issue's.CommandFromPromptandcommandNameForalready map anmcp.Promptand itsPromptArgumentset onto Discord's shape, with the refuse-rather-than-repair posture and its reasoning recorded indocs/sirens-echo-prompt-commands.md.One reason on the original list has expired rather than been resolved. The introducing commit cited "when prompts are known, which is the open question on issue 163" as why registration was held back. #163 is closed. Nothing recorded that here until #823.
The posture note to carry, since it is doctrine rather than a decision to re-take
A prompt is user-selected instruction reaching the model through a structured channel - the same class as an uploaded file. Data the turn may read, never instructions it obeys. Already stated in
docs/sirens-echo-prompt-commands.md, and it should stay stated wherever registration lands.Nothing here needs a human before code starts. If I have missed a decision you consider still open, say so and I will put
consultstraight back.Picked this up as headless and split it, because the prerequisite turned out to be missing. Angie (ENG,
claudeseat).The finding
Nothing registers slash commands with Discord.
discordCommands()renders the declared set and its only caller is a test. NoApplicationCommandBulkOverwrite, noApplicationCommandCreate, nowhere in the tree.So
/echo,/job-status,/job-canceland/mcpsare unreachable even withSIRENS_ECHO_DISCORD_COMMANDSon, andonInteractionhas been waiting for interactions Discord had no way to send.That reframes this issue. It reads as "prompt commands are not built"; the surface underneath them had never been published. Filed as #885 and built in PR #886 - per guild rather than globally, bulk overwrite, behind the same default-false switch.
Why this issue stays open
The prompt half is #884, and it carries a problem nothing here had named.
An interaction must be answered in three seconds. A model turn takes minutes.
respondToCommandanswers immediately, and every command that exists today fits that:/job-statusreads a record,/echosubmits a job and returns an id. A prompt command is the first whose whole purpose is a model reply, and on #577's numbers Echo's round 0 alone ranges 133.5s to 255.4s.Two shapes - defer the interaction for fifteen minutes, or submit a job and answer with an id. Option 2 matches how this surface already behaves. Option 1 matches what a slash command feels like. That is a product call, so #884 is
consultrather than something I should decide while writing a PR.What I did not need to re-decide
The allowlist (decided 2026-08-12), the access policy (
3142129), and the install-link scope (3330c8d) are all settled, and #163 - which the introducing commit named as a blocker - is closed. None of those were what was standing in the way.Closing as fully split. Everything left here is #884, and #884 is correctly
autonomy/async-consult. Angie (ENG,claudeseat), working the headless engineer queue.This issue is on the headless queue and there is no headless work in it.
Where the two halves went
main:commandregister.go:51callsApplicationCommandBulkOverwriteper guild, and it was previously absent from the tree.autonomy/async-consult, and it is the whole of what this issue asked for.Why #884 is not headless, restated so this closure is checkable
An interaction must be answered in three seconds and a model turn takes minutes. Every command that exists today fits:
/job-statusreads a record,/echosubmits a job and returns an id. A prompt command is the first whose whole purpose is a model reply, and on #577's numbers round 0 alone runs 133.5s to 255.4s.Two shapes, defer the interaction for fifteen minutes or submit a job and answer with an id. That is a product call about what a slash command should feel like, not something to settle while writing a patch.
The four blockers this issue opened on are all closed
The scope-widening deferral (reversed 2026-08-13), the access-policy gap (
3142129, anddocs/access-policy.reference.yamlnow enumerates every summon path, which I extended today for role mentions on #866), the per-prompt versus allowlist question (allowlist, decided 2026-08-12), and the install-link scope (3330c8d).None of them is what is left. Leaving this open beside #884 means one board entry says headless and its real work says consult.