Background
The slash-command guardrail at tinyagentos/routes/chat.py:264-276 rejects /cmd messages in non-DM, non-A2A group channels that are not addressed to an agent (no @<agent> or @all mention). It returns HTTP 400 with a clear error.
This guard runs only on the HTTP post_message path. The WebSocket message branch (chat_ws handler) does not enforce it — so the same input is rejected over HTTP but accepted over WS.
This was flagged by CodeRabbit on PR #267 (review). It is pre-existing — the guard was added in PR #235 (commit `d28dcfa`, Chat Phase 2a) and has always been HTTP-only. PR #267 only added the A2A bypass clause inside the existing HTTP guard.
Why it matters
The safety behavior is transport-dependent. The guard is meant to prevent multi-agent fan-out from a single unaddressed slash command in a group channel; over WS that fan-out is currently still possible.
Proposed fix
- Extract the validation into a shared helper, e.g.:
```python
async def _validate_slash_target(content: str, channel: dict | None) -> str | None:
"""Return error message if slash command is unaddressed in a non-DM,
non-A2A group channel; otherwise None."""
```
- Call it from `post_message` (replacing the inline block).
- Call it from the WebSocket `message` branch in `chat_ws`. Either drop the message silently or send an error frame back over the socket — whichever matches the existing WS error-handling pattern in this file.
- Add a regression test in `tests/test_routes_chat_slash_guard.py` exercising the WS path.
Also: defer fix from PR #267 — release-actor attribution
While we're in this area, address the deferred CodeRabbit nit from PR #267:
`beads_bridge.py:287-293` — `task.released` event payload does not include `releaser_id`. `release_task` clears `claimed_by` before the event fires, so the bridge falls back to "agent" for the actor in `format_released`. Fix: have `task_store.release_task` include `releaser_id` in the event payload, then prefer it in the handler.
This is independent of the WS guard fix but small enough to bundle.
Acceptance
- WS and HTTP paths produce identical guardrail behavior for slash commands.
- `task.released` actor reflects the user/agent that released the task.
- Regression tests cover both.
Background
The slash-command guardrail at
tinyagentos/routes/chat.py:264-276rejects/cmdmessages in non-DM, non-A2A group channels that are not addressed to an agent (no@<agent>or@allmention). It returns HTTP 400 with a clear error.This guard runs only on the HTTP
post_messagepath. The WebSocketmessagebranch (chat_wshandler) does not enforce it — so the same input is rejected over HTTP but accepted over WS.This was flagged by CodeRabbit on PR #267 (review). It is pre-existing — the guard was added in PR #235 (commit `d28dcfa`, Chat Phase 2a) and has always been HTTP-only. PR #267 only added the A2A bypass clause inside the existing HTTP guard.
Why it matters
The safety behavior is transport-dependent. The guard is meant to prevent multi-agent fan-out from a single unaddressed slash command in a group channel; over WS that fan-out is currently still possible.
Proposed fix
```python
async def _validate_slash_target(content: str, channel: dict | None) -> str | None:
"""Return error message if slash command is unaddressed in a non-DM,
non-A2A group channel; otherwise None."""
```
Also: defer fix from PR #267 — release-actor attribution
While we're in this area, address the deferred CodeRabbit nit from PR #267:
This is independent of the WS guard fix but small enough to bundle.
Acceptance