Phase 3 — User & Auth System
Goal
The bot enforces approval before any agent gets to see user messages, and admin tools let an admin gate access via the MCP interface. After this phase, “an arbitrary Telegram user can talk to my bot” is no longer true — only approved users get through.
Scope / Deliverables
- On inbound update from unknown user: insert row
status='pending', reply with a fixed “approval pending” message, do not forward the event to the agent - On inbound update from
status='blocked'user: drop, audit-log the attempt - On inbound update from
status='active'user: normalized event forwarded to the agent (as MCP server-sent event or via a polling tool — see open question on event-to-agent mechanism in operations-and-strategy.md) - Admin notification: when a new user appears as pending, the admin user gets a Telegram message with their
userIdand a hint to callapprove_user - Tool:
approve_user(admin-only) - Tool:
set_role(admin-only; cannot demote the last admin) -
auth/middleware.ts— runs before every MCP tool call; enforces caller-as-admin where required; emitsaudit_logrows for denials - Migration:
audit_logtable
Automated Unit Testing
-
auth/middleware.test.ts—- admin-only tool called by non-admin → JSON-RPC
UNAUTHORIZEDerror and anaudit_logrow withaction='blocked_request' - admin-only tool called by admin → passes through
- non-admin tool called by
status='active'user → passes through - non-admin tool called by
status='blocked'user →UNAUTHORIZED+ audit row
- admin-only tool called by non-admin → JSON-RPC
-
users.flow.test.ts(webhook-level, with fake telegraf/outbound):- inbound from unknown user → DB row inserted with
status='pending', “approval pending” reply sent, agent receives nothing, admin gets a notification message containinguserId - inbound from
status='blocked'user → no reply, no agent forwarding, audit row written - inbound from
status='active'user → normalized event is forwarded to the agent stub
- inbound from unknown user → DB row inserted with
-
tools/approve_user.test.ts—- admin call on a pending user → status flips to
active,approved_atset to now-ish,approved_byset to admin’suserId, returns{userId, status: 'active', approvedAt} - non-admin caller →
UNAUTHORIZED(audit-logged) - unknown
userId→NOT_FOUND
- admin call on a pending user → status flips to
-
tools/set_role.test.ts—- admin promotes user →
role='admin' - admin demotes self while another admin exists → succeeds
- admin demotes the only remaining admin → refused with a clear error (e.g.
INVALID_INPUTor a dedicatedLAST_ADMINcode), no DB write - non-admin caller →
UNAUTHORIZED
- admin promotes user →
-
audit_log.test.ts— schema: every audit row hasactor_id,action,subject_id?,details JSONB,created_at; insert helpers reject malformed actions.
Integration scope (deferred): end-to-end Telegram → webhook → MCP → agent forwarding is exercised manually with the MCP Inspector against a personal bot.
Definition of Done
- New Telegram user → bot replies “pending”, DB row written, admin notified.
- Admin calls
approve_uservia MCP → user can now use the bot. - Non-admin attempt to call
approve_user→UNAUTHORIZED, audit logged. - Blocking a user via
set_role(or futureblock_user) stops their events from reaching the agent.