What happens
TelegramDispatcher.deliver_spawn_approval posts the spawn-approval prompt without consulting the channels governance ceiling. An operator profile that denies the telegram channel after the transport connected therefore does not stop a spawn-approval prompt, carrying an agent-authored task preview, from being posted into that conversation.
Why it matters
The prompt cannot be answered once it is posted. telegram/transport_dispatch.py drops an inbound callback press on a governance-denied channel, and only an explicit reject (a:<rid>:<nonce>:0) is exempt from that drop. So an Approve press on a denied channel resolves nothing, the deny-by-default wait runs to APPROVAL_TIMEOUT_S, and deliver_spawn_approval returns False.
That False is not a fall-through. The delivery seam reads it as the user's real decision, so the host gate refuses the spawn on it instead of re-offering the prompt on the still-permitted Slack DM or dashboard surface. The user sees a spawn denied by a prompt they were never able to see, after a full timeout.
Where
src/kiro_crew/telegram/transport_dispatch.py, deliver_spawn_approval. The method consults the local rosters and _spawn_prompt_destination_permitted, neither of which speaks for the governance profile. channel_inbound_permitted is already imported in that module and used on the inbound path and the callback path, but not on this delivery path.
The startup gate does not cover this: it only stops a transport from connecting. A profile that denies the channel afterwards leaves the transport up with its rosters intact, which is the gap _channel_inbound_permitted_sync exists to close on the other paths.
Suggested fix
Consult channel_inbound_permitted("telegram") at the top of deliver_spawn_approval, before the nonce is armed, and return None on a deny so the gate falls through to Slack/dashboard. The check has to sit before the arm rather than beside the roster check: the roster check is deliberately synchronous with no suspension point before the send it guards, and an await between them would reopen that window.
Notes
Found while addressing a review finding of exactly this shape against the Discord port of the same seam in #13454, where the check was added. Kept out of that PR so it does not widen a converging review round. The two dispatchers share the seam but not the code, so fixing one does not fix the other.
What happens
TelegramDispatcher.deliver_spawn_approvalposts the spawn-approval prompt without consulting thechannelsgovernance ceiling. An operator profile that denies thetelegramchannel after the transport connected therefore does not stop a spawn-approval prompt, carrying an agent-authored task preview, from being posted into that conversation.Why it matters
The prompt cannot be answered once it is posted.
telegram/transport_dispatch.pydrops an inbound callback press on a governance-denied channel, and only an explicit reject (a:<rid>:<nonce>:0) is exempt from that drop. So an Approve press on a denied channel resolves nothing, the deny-by-default wait runs toAPPROVAL_TIMEOUT_S, anddeliver_spawn_approvalreturnsFalse.That
Falseis not a fall-through. The delivery seam reads it as the user's real decision, so the host gate refuses the spawn on it instead of re-offering the prompt on the still-permitted Slack DM or dashboard surface. The user sees a spawn denied by a prompt they were never able to see, after a full timeout.Where
src/kiro_crew/telegram/transport_dispatch.py,deliver_spawn_approval. The method consults the local rosters and_spawn_prompt_destination_permitted, neither of which speaks for the governance profile.channel_inbound_permittedis already imported in that module and used on the inbound path and the callback path, but not on this delivery path.The startup gate does not cover this: it only stops a transport from connecting. A profile that denies the channel afterwards leaves the transport up with its rosters intact, which is the gap
_channel_inbound_permitted_syncexists to close on the other paths.Suggested fix
Consult
channel_inbound_permitted("telegram")at the top ofdeliver_spawn_approval, before the nonce is armed, and returnNoneon a deny so the gate falls through to Slack/dashboard. The check has to sit before the arm rather than beside the roster check: the roster check is deliberately synchronous with no suspension point before the send it guards, and anawaitbetween them would reopen that window.Notes
Found while addressing a review finding of exactly this shape against the Discord port of the same seam in #13454, where the check was added. Kept out of that PR so it does not widen a converging review round. The two dispatchers share the seam but not the code, so fixing one does not fix the other.