feat(sharing): notify requester on channel access-request approval (#951) - #977
Merged
Conversation
) After #311 introduced the unified channel access-control flow, a user who DMs an agent on Telegram/Slack/WhatsApp without prior sharing is told "I'll let you know once the agent owner responds" — but the decide endpoint never actually told them. Result: requesters were left guessing whether they'd been admitted. Wire up the proactive-message primitive (#321) to close the loop. Backend: - `ProactiveMessageService.send_access_grant_notification(agent, email, channel, text)` — one-shot path that calls `_deliver_via_channel` directly and audits the outcome. Bypasses the `allow_proactive` opt-in (the user explicitly initiated the request; this is the response) and the per-recipient rate limit (one-shot, not a campaign). Failures and missing bindings are caught, audit-logged, and returned as `DeliveryResult(success=False, ...)` — never raised. - `routers/sharing.py` decide endpoint: when `decision.approve` AND `existing.channel in {telegram, slack, whatsapp}`, fires the notification as `asyncio.create_task` so a missing binding or transport hiccup can't block the HTTP response. Rejections stay silent (deliberate — owners may not want to confirm the agent exists). Web channel handled by the existing `agent_shared` WebSocket dashboard event. Notification text mirrors the tone of the original pending-approval reply so the user recognises it as the promised follow-up: "✅ Access to {agent_name} approved by the agent owner. You can now message the agent here." Docs: - `architecture.md` decide-endpoint row + Access Control Flow note call out the post-approval notification and where the audit lands. - `feature-flows/unified-channel-access-control.md` documents the approve flow's new step plus a manual test path for each of Telegram, Slack, WhatsApp, web (negative), and missing-binding (negative). Tests: - 4 new unit cases in `tests/test_access_grant_notification_unit.py`: delivery success → audit success; `RecipientNotFoundError` → audited as `recipient_not_found`, returned not raised; unexpected exception → audited + swallowed; opt-in bypass verified. Verified live: approving a telegram-channel request on an agent with no Telegram binding returns HTTP 200, the approval succeeds, and `/api/audit-log?event_type=proactive_message` shows `success=false / error=recipient_not_found / channel=telegram / message_preview="✅ Access to trinity-system approved..."`. Related to #951 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test was using `del sys.modules[mod]` to force a fresh import of proactive_message_service after stubbing its deps. CI's `tests/lint_sys_modules.py` baseline gate rejects bare deletions in new test files — the codebase convention is the snapshot/restore autouse fixture pattern from `tests/unit/test_telegram_webhook_backfill.py`. Switched to `_STUBBED_MODULE_NAMES` + `_restore_sys_modules` autouse fixture, then `monkeypatch.delitem(..., raising=False)` for the forced reimport. Same behaviour, no global leakage into sibling tests, lint baseline stays at 237. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
approved these changes
Jun 1, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
LGTM — verified send_access_grant_notification signatures match _deliver_via_channel and _audit_send. Auth boundary checked: decide endpoint is owner-only (OwnedAgentByName), recipient email+channel sourced from the stored access_requests row (not request body — no redirect vector), and the opt-in/rate-limit bypass is justified (one-shot response to a user-initiated, owner-approved request). Fire-and-forget wiring keeps a missing binding from blocking approval; all failure paths audited not raised. Docs updated on both surfaces. CI green, 4 new tests. Validated via /validate-pr.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After #311 introduced unified channel access control, a user who DMs an agent on Telegram/Slack/WhatsApp without prior sharing is told "I'll let you know once the agent owner responds" — but the decide endpoint never actually told them. Requesters were left guessing whether they'd been admitted.
This wires up the existing proactive-message primitive (#321) to close the loop.
Backend
ProactiveMessageService.send_access_grant_notification(agent, email, channel, text)— one-shot path that calls_deliver_via_channeldirectly and audits the outcome. Bypasses theallow_proactiveopt-in (the user explicitly initiated the request; this is the response) and the per-recipient rate limit (one-shot, not a campaign). Failures and missing bindings are caught, audit-logged, and returned asDeliveryResult(success=False, ...)— never raised.routers/sharing.pydecide endpoint: whendecision.approveANDexisting.channel in {telegram, slack, whatsapp}, fires the notification asasyncio.create_taskso a missing binding or transport hiccup can't block the HTTP response. Rejections stay silent (deliberate — owners may not want to confirm the agent exists). Web channel handled by the existingagent_sharedWebSocket dashboard event.Notification text mirrors the tone of the original pending-approval reply:
Docs
architecture.mddecide-endpoint row + Access Control Flow note call out the post-approval notification and where the audit lands.feature-flows/unified-channel-access-control.mddocuments the approve flow's new step plus a manual test path for each of Telegram, Slack, WhatsApp, web (negative), and missing-binding (negative).Tests
4 new unit cases in
tests/test_access_grant_notification_unit.py:RecipientNotFoundError→ audited asrecipient_not_found, returned not raisedallow_proactivebypass verifiedRelated to #951
Test plan
.venv/bin/pytest tests/test_access_grant_notification_unit.py tests/test_proactive_audit_unit.py tests/test_sharing_null_email_unit.py— 13 passed.channel='telegram'for an agent without any Telegram binding, approved viaPOST /api/agents/.../access-requests/.../decide:GET /api/audit-log?event_type=proactive_messageshows new entry withdetails.channel='telegram',details.success=false,details.error="recipient_not_found: No Telegram bot configured for agent 'trinity-system'",details.message_preview="✅ Access to trinity-system approved by the agent owner. You can now message the agent here."🤖 Generated with Claude Code