Skip to content

feat: Unified channel access control — verified-email identity, per-agent allow-list, access requests (web / Telegram / Slack) #311

Description

@vybe

Problem

Each channel today grows its own identity and access model:

The agent owner has no consistent way to answer: "who is allowed to talk to this agent?"

As a result, per-channel auth drifts apart, user memory (MEM-001) can't flow across channels, and there's no single enforcement point for access policy.

Design principle

Verified email is the single unit of identity across every channel. Each adapter is responsible only for translating its native identifier into a verified email. After that, every downstream concern (access control, memory, audit, rate limiting) runs off the same key.

The design reuses existing primitives wherever they already fit and introduces exactly one new table and one new adapter method.

Reuse map

Existing primitive Role in this design
agent_ownership Policy lives here — add require_email, open_access as an agent-settings mixin (architectural invariant #2)
agent_sharing(agent_name, shared_with_email) Per-agent allow-list — unchanged, becomes the source of truth for cross-channel access
telegram_chat_links Telegram identity binding — add verified_email, verified_at columns
email_verification_codes + db.create_verification / db.verify_code Telegram /login verification — no new verification code
public_chat_sessions.session_identifier Web identity binding — already holds email for verified users
public_user_memory(agent_name, user_email) Cross-channel memory (MEM-001) — already keyed on email
email_service Sends the 6-digit code — unchanged
OwnedAgentByName dependency Owner-gates the policy and allow-list editors — unchanged

What is actually new

One table:

access_requests(
  id TEXT PRIMARY KEY,
  agent_name TEXT NOT NULL,
  email TEXT NOT NULL,
  requested_at TEXT NOT NULL,
  status TEXT NOT NULL,          -- pending | approved | denied
  decided_by TEXT,               -- user id of approver
  decided_at TEXT,
  UNIQUE(agent_name, email)
);

Approval flow: status=approved → insert into agent_sharing → delete or archive the request.

One adapter method (ChannelAdapter base):

def resolve_verified_email(self, message: NormalizedMessage) -> Optional[str]:
    """Translate the channel-native identity to a verified email, or None."""
  • Telegram: returns telegram_chat_links.verified_email for the telegram_user_id; if absent, sends auth prompt and the router early-returns
  • Slack: calls users.info — Slack's workspace email is already verified, no 6-digit code needed
  • Web public links: reads public_chat_sessions.session_identifier when identifier_type='email'
  • WhatsApp (future): whatever the channel proves about identity

Telegram /login state machine (in the Telegram adapter only):

  1. New telegram_user_id sends a message → adapter replies with auth prompt
  2. /login user@example.comdb.create_verificationemail_service sends 6-digit code
  3. User replies with code → db.verify_code → adapter writes verified_email onto the telegram_chat_links row
  4. Subsequent messages resolve to the verified email automatically

Router-level enforcement (single gate)

In adapters/message_router.py, before execute_task:

email = adapter.resolve_verified_email(message)

if policy.require_email and not email:
    adapter.prompt_auth(message); return

if is_owner(email, agent) or is_admin(email) or email in agent_sharing(agent):
    proceed
elif policy.open_access:
    proceed
else:
    upsert access_requests(agent, email, status=pending)
    notify owner
    adapter.reply("Your access request is pending approval.")
    return

After success, source_user_email = email flows into execute_task, and memory injection (MEM-001 parity for all channels) uses public_user_memory(agent_name, email) directly.

Slack, WhatsApp, and any future channel inherit the whole flow by implementing only resolve_verified_email.

Net delta vs the first draft

  • ❌ No new channel_user_auth table → extend telegram_chat_links (2 columns)
  • ❌ No new agent_access_policy table → 2 bool columns on agent_ownership via settings mixin
  • ❌ No new allow_list concept → agent_sharing is the allow-list
  • ✅ One new table: access_requests
  • ✅ One new adapter method: resolve_verified_email
  • ✅ One router-level gate
  • ✅ MEM-001 becomes cross-channel without a schema change

Sub-tasks

  • Migrate agent_ownership with require_email, open_access (settings mixin)
  • Deprecate public_links.require_email → inherit from agent policy (question for feat: Public chat link email whitelist and access requests #252: keep per-link override or drop?)
  • Add access_requests table + owner inbox UI + approval endpoint (inserts into agent_sharing)
  • Extend telegram_chat_links with verified_email, verified_at
  • Telegram /login state machine in telegram_adapter.py
  • resolve_verified_email on ChannelAdapter base + implementations (Telegram, Slack, Web)
  • Router-level gate in message_router.py, replacing per-channel ad-hoc auth
  • Memory injection in router using resolved email (MEM-001 for Telegram/Slack)
  • Sharing-page UI becomes the single surface for "who can talk to this agent" (cross-channel)
  • feat: Public chat link email whitelist and access requests #252 becomes the web-surface slice of this work

Acceptance criteria

  • Enabling require_email + an agent_sharing entry on an agent gates chat identically on web, Telegram, and Slack
  • An approved access request inserts into agent_sharing and the user can chat immediately across any channel
  • The same email talking to the same agent via web and Telegram shares one public_user_memory record
  • Adding a new channel requires implementing only resolve_verified_email; access control and memory work without additional code
  • No new tables beyond access_requests

Architectural alignment

  • Invariant Feature/gemini runtime support #2 (settings mixin): access policy is an agent_settings mixin
  • Invariant Fix git pushing bug #9 (Channel Adapter ABC): the new method extends the existing ABC
  • Invariant security: implement safe tar extraction with symlink/hardlink validation #8 (auth pattern): gate runs inside the router; owner/admin checks reuse existing dependencies
  • Roles model (ROLE-001): end-user access control is orthogonal to platform roles — roles govern platform capabilities, access control governs agent end-user access. Admins still bypass. creator owning an agent still must /login to bind their Telegram identity — desirable, not a regression.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions