Refuse a Slack trigger claiming a channel another
Epic 5336 (event-driven triggers) residual, named by coder 5367 in its security-gate question (Q 4718) and approved there as a follow-up.
## Defect
Slack mentions are routed fleet-wide by channel id: `apps/api/app/trigger_intake.py:451-463` (`_slack_trigger_for`) takes the FIRST `slack` trigger by id whose `source_config.channel_ids` contains the channel, across ALL orgs, paused/failed included. Save-time validation (`apps/api/app/trigger_config.py:77-87`, `SlackConfig`) only checks the id's shape. So an admin of org B can save org A's channel id; if B's trigger has the lower id it takes every mention in that channel. An unlinked sender's text is dropped (`_slack_sender_refusal`, trigger_intake.py:554), but a Slack user linked to members of both orgs files into org B, and a paused B trigger swallows A's mentions as `skipped` "trigger paused" — A's trigger never sees them.
Observed: read of develop 39377a9b2 (the code paths above); no reproduction on dev (dev console not reachable from the finish node).
## Fix (no new surface)
In the save path (`apps/api/app/routes/triggers.py` `write_trigger_create` :657 and `write_trigger_update` :728 — the advisor verbs in `routes/agent_triggers.py` delegate there, so they are covered too), refuse with a readable 409 any Slack `channel_ids` entry already held by a `slack` trigger of a DIFFERENT org: "Channel C… is already used by a trigger in another organization." Same-org overlap stays allowed (first-by-id within the org is the documented rule). Read the existing triggers across orgs in a detached/fleet session as `_slack_trigger_for` does. Optionally harden the match the same way (ignore a trigger whose org differs from the channel's first claimer) only if it is a one-clause change.
Out of scope: do NOT add Slack API reads or new Slack scopes, do NOT change the Socket Mode transport, matching order within an org, the unmatched log, the sender gate, or any other trigger source. No migration unless a partial unique index is genuinely simpler than the query (it is not expected to be).
Done means:
- A test: org A saves channel C1; org B creating (and separately updating to) a Slack trigger with C1 gets 409 with the readable message, and nothing is written. → run `apps/api/tests/test_trigger_slack.py` (or a new test beside it) and cite the passing output.
- A test: two triggers in the SAME org may share C1 (existing `test_the_first_trigger_by_id_takes_the_channel` still passes).
- A test: the advisor `task_trigger_create` path gets the same 409.
- On dev, as an org admin of a second org, the Triggers editor shows the readable error when saving a channel id another org uses (if dev is unreachable from your node, ASK and name the remaining oracle).
- `./dc check full` green; PR merged to develop; Jaah Deploy of the merge succeeds.
Outcome
Cross-org Slack channel claims now get a readable 409, in the console and through the advisor verbs. Tests pass; the full gate is red only on a selftest that also fails on develop. PR #4761 merged and deployed (4350485b7). Open: the dev editor check, which the operator skipped.
Artifacts
Activity
- Ready
- Runner started
- Awaiting response
- Awaiting response
- Runner resumed
- Runner ended (done)