Skip to content

Add bad-actor abuse controls: suspension, outbound-email pause, compute quotas, delivery insights - #911

Merged
kody-bot merged 6 commits into
mainfrom
cursor/bad-actor-controls-b47e
Jul 24, 2026
Merged

kody-bot merged 6 commits into
mainfrom
cursor/bad-actor-controls-b47e

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 24, 2026 •

Copy link
Copy Markdown
Owner

Why

One bad actor can poison shared platform identity: every user sends mail from one platform domain through one Cloudflare Email Sending account, and every sandbox fetch leaves through the same Worker egress. Until now the platform had prevention (invite gate, verification, plan quotas) but no reactive controls — no account kill switch, no complaint-driven email pause, and no quota on execute/fetch volume despite metering both.

What

  1. Automatic outbound-email pause (packages/worker/src/email/outbound-abuse.ts): the delivery queue evaluates provider delivery events; one spam complaint or 5+ bounces per UTC day sets users.email_outbound_paused_at, blocks further sends, and notifies admin accounts via the transactional sender. Only persisted (deduped-by-provider_event_id) events drive the pause; idempotent (only transitions NULL); cleared by the audited resume_email_outbound admin action.
  2. Platform suspension (users.suspended_at + packages/worker/src/app/account-suspension.ts): admin-set kill switch enforced fail-closed at browser session resolution, MCP bearer auth (403 account_suspended), inbound email (bounded account-suspension rejection), and outbound send. Audited suspend_user / unsuspend_user admin actions with a Moderation panel on /admin/users (self-suspension blocked).
  3. Compute quotas: new execute_calls_per_day and outbound_fetches_per_day entitlements, consumed atomically at the top of the MCP execute tool and executeGatewayFetch (before bundling/secret expansion, so over-limit calls cost nothing). When a gateway caller carries no email (OpenAPI provider requests, package runtime), the gateway reverse-resolves the account from the stable userId so the caller's real plan binds. Both surfaces were already metered; this closes the metering → enforcement loop.
  4. Email delivery health insight: /admin/insights gains a platform-wide per-day chart of provider delivery outcomes (delivered/deferred/bounced/failed/rejected/complained) so shared-domain reputation trouble is visible before providers act on it.

Migration 0091-user-abuse-controls.sql adds both columns (renumbered from 0090 after rebasing on the webhook-endpoints work); ledger updated. Docs updated: security.md (new Abuse controls section), authentication.md, entitlements.md, email-primitives.md.

Testing

npm run validate fully green on the rebased branch (format, lint, typecheck, 1274+ unit tests across 390 files, Playwright E2E, MCP E2E, primitives + migrations checks). New coverage: complaint/bounce pause thresholds, phantom-complaint and idempotency guards, paused/suspended send + inbound rejection, MCP 403 for suspended accounts, admin suspend/unsuspend/resume actions + audit events, fetch-gateway quota consumption/denial including the no-email reverse-resolution path, insights delivery-day builder.

CodeRabbit and Bugbot review findings addressed: real-plan binding for email-less gateway calls, persisted-event-only abuse pause, userId-scoping assertions in test stubs, admin notification cap removed, exact bounce threshold documented.

System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ 9c877620 · Head: 541bfff8

Classification: extends — no new primitives; this PR adds columns, resources, and fail-closed gates to existing ones.

Primitives touched

Primitive Group Impact
d1-app-db storage extends — users.suspended_at, users.email_outbound_paused_at (0091)
email assistant extends — suspension + pause gates on send/receive; abuse monitor
email-delivery-queue assistant extends — evaluates persisted bounce/complaint events into the pause
entitlements platform extends — execute_calls_per_day, outbound_fetches_per_day resources
mcp-server surfaces extends — execute tool consumes daily quota; admin metadata exposes new fields
mcp-oauth surfaces extends — 403 account_suspended gate after email verification
capabilities-execute runtime composes — threads email into fetch gateway props
app-ui surfaces extends — admin Moderation panel + delivery-health chart; suspended sessions treated as signed out
rbac platform composes — new audited admin actions behind update:user:any
openapi-bindings assistant composes — gateway prop shape updated; account reverse-resolved for quota
package-apps runtime composes — gateway prop shape updated at call site

System map

Provider delivery events flow through the queue into the abuse monitor, which pauses senders in D1; suspension gates fan out from the users table to session, MCP, and email chokepoints.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	deliveryQueue["email-delivery-queue<br/>Email delivery events queue"]:::extended
	email["email<br/>Email"]:::extended
	d1AppDb["d1-app-db<br/>D1 app database"]:::extended
	mcpOauth["mcp-oauth<br/>MCP OAuth gateway"]:::extended
	mcpServer["mcp-server<br/>MCP endpoint"]:::extended
	entitlements["entitlements<br/>Plans and quotas"]:::extended
	appUi["app-ui<br/>Browser app (Remix 3)"]:::extended
	capabilitiesExecute["capabilities-execute<br/>Capabilities execute runtime"]:::touched
	deliveryQueue -->|"persisted bounce/complaint → applyOutboundEmailAbusePause"| email
	email -->|"users.email_outbound_paused_at write; send gate"| d1AppDb
	mcpOauth -->|"isAccountSuspended → 403 account_suspended"| d1AppDb
	mcpServer -->|"execute_calls_per_day consumeDailyEntitlement"| entitlements
	capabilitiesExecute -->|"outbound_fetches_per_day via props.email or stable-id reverse-resolution"| entitlements
	entitlements -->|"entitlement_daily_counters upsert"| d1AppDb
	appUi -->|"suspend_user / unsuspend_user / resume_email_outbound POST /admin/users.json"| d1AppDb
	appUi -->|"delivery-outcomes chart from email_delivery_events"| d1AppDb
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

-- 0091-user-abuse-controls.sql
ALTER TABLE users ADD COLUMN suspended_at TEXT;
ALTER TABLE users ADD COLUMN email_outbound_paused_at TEXT;
Entitlement resource free pro partner max
execute_calls_per_day 500 5,000 10,000 500,000
outbound_fetches_per_day 2,000 20,000 40,000 2,000,000

Invariants

Per-user isolation unchanged: every new read/write is scoped by userId / stable_user_id. Suspension gates fail closed at all chokepoints, mirroring the existing deleting_at / email-verification patterns. The admin insights delivery chart is a platform-wide admin-only aggregation of outcome counts with no user identifiers.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added admin controls to suspend accounts, restore access, and resume paused outbound email.
    • Added email delivery health reporting for the past 28 days in Admin Insights.
    • Added daily limits for execute calls and outbound fetches, with usage visibility.
  • Bug Fixes
    • Suspended accounts are consistently blocked from sessions, MCP requests, and email sending or receiving.
    • Outbound email automatically pauses after spam complaints or repeated bounces.
  • Documentation
    • Updated security, entitlement, authentication, and email guidance for the new controls and limits.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds platform account suspension, automatic outbound-email abuse pauses, daily compute and egress quotas, admin moderation controls, email delivery analytics, supporting schema updates, tests, and documentation.

Changes

Abuse controls

Layer / File(s) Summary
Account suspension and moderation
packages/worker/src/app/*, packages/worker/src/mcp-auth.ts, packages/worker/src/email/inbound.ts, packages/worker/client/routes/admin-users.tsx, packages/worker/migrations/*
Adds suspension storage, fail-closed session/MCP/email checks, admin suspension and resume actions, and moderation UI state.
Outbound email abuse pause
packages/worker/src/email/outbound-abuse.ts, packages/worker/src/email/delivery-queue.ts, packages/worker/src/email/outbound.ts, packages/worker/src/email/*test.ts
Pauses outbound email after complaint or bounce thresholds, applies the pause idempotently, notifies admins, and blocks sending while paused.

Compute quotas

Layer / File(s) Summary
Entitlement resources and enforcement
packages/worker/src/entitlements/*, packages/worker/src/mcp/tools/execute.ts, packages/worker/src/mcp/fetch-gateway.ts
Adds daily execute and outbound-fetch limits, consumption, plan lookup, usage reporting, and quota tests.
Caller context propagation
packages/worker/src/mcp/*, packages/worker/src/package-runtime/*, packages/worker/src/execute-maintenance.ts
Passes caller email and database context through gateway and executor paths.

Delivery insights

Layer / File(s) Summary
Email delivery reporting
packages/worker/src/app/admin-insights-data.ts, packages/worker/src/app/loader-data.ts, packages/worker/client/routes/admin-insights.tsx, packages/worker/src/app/*test.ts
Aggregates daily Cloudflare delivery outcomes and renders the email delivery health chart.

Documentation and migration ledger

Layer / File(s) Summary
Documentation and migration validation
docs/contributing/*, docs/use/email-primitives.md, tools/*
Documents suspension, email pauses, compute quotas, and updates migration ledger expectations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main additions: abuse controls, outbound-email pause, compute quotas, and delivery insights.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/bad-actor-controls-b47e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 24, 2026 16:22
@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-911.kody-a99.workers.dev

Worker: kody-pr-911
D1: kody-pr-911-db
KV: kody-pr-911-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/contributing/architecture/entitlements.md`:
- Around line 99-106: Update the outbound-fetch quota flow around getUserPlan
and executeGatewayFetch so a userId with missing or blank
FetchGatewayProps.email never resolves directly to the max plan. Resolve the
caller email from the stable userId before plan lookup, or reject the request
when it cannot be resolved, while preserving quota enforcement for contexts
without a userId.

In `@docs/use/email-primitives.md`:
- Around line 119-123: Update the delivery-events description in the email
primitives documentation to state that five or more bounced sends within a UTC
day trigger the automatic outbound-sending pause, while preserving the existing
spam-complaint trigger and pause behavior.

In `@packages/worker/src/app/admin-insights-data.ts`:
- Around line 147-157: The email delivery aggregation in the admin insights
query must not read cross-tenant events without an ownership boundary. Update
the query and surrounding insights flow to constrain results by the authorized
userId, including that value in any cache key, or explicitly route this metric
through an approved platform-wide aggregation boundary that does not expose
tenant-level outcomes directly.

In `@packages/worker/src/email/outbound-abuse.ts`:
- Around line 134-141: Remove the LIMIT 10 clause from the admin query used by
the outbound abuse notification flow so admins are not capped at ten recipients.
Preserve the existing admin filtering, ordering, and result mapping in the
admins lookup.

In `@packages/worker/src/mcp-auth.workers.test.ts`:
- Around line 165-170: Update the suspended-account query mock branch in
mcp-auth.workers.test.ts to validate the bound email/stableUserId against the
configured test user, matching the adjacent profile and verification branches,
and return null for non-matching values. Preserve returning options.suspendedAt
only for the expected bound identity so the mock detects missing or widened
query scoping in isAccountSuspended.

In `@packages/worker/src/mcp/fetch-gateway.node.test.ts`:
- Around line 24-40: Update the APP_DB D1 stub in the test environment so bind()
captures its arguments and run() validates the expected userId and relevant
entitlement upsert query contract before returning success. Ensure the stub
fails when userId is omitted or belongs to another account, while preserving
first() behavior.

In `@packages/worker/src/mcp/fetch-gateway.ts`:
- Around line 31-37: Update the gateway quota plan resolution around the email
field and its associated logic to work independently of optional email: derive
the account plan from userId, or ensure the verified caller email is propagated
by every gateway caller, including operation-request.ts authenticated calls.
Preserve the existing email-based behavior when available and ensure
authenticated users with email: null receive their account-specific quota
instead of the max fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc82be90-70dd-41fb-a723-c84c7ad7e415

📥 Commits

Reviewing files that changed from the base of the PR and between 958206d and 6189e1a.

📒 Files selected for processing (50)
  • docs/contributing/architecture/authentication.md
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/security.md
  • docs/use/email-primitives.md
  • packages/worker/client/routes/admin-insights.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/migrations/0090-user-abuse-controls.sql
  • packages/worker/src/app/account-suspension.ts
  • packages/worker/src/app/admin-insights-data.node.test.ts
  • packages/worker/src/app/admin-insights-data.ts
  • packages/worker/src/app/admin-user-usage-data.ts
  • packages/worker/src/app/admin-users-data.ts
  • packages/worker/src/app/authenticated-user.ts
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/request-auth-cache.ts
  • packages/worker/src/app/session-info.ts
  • packages/worker/src/community/community-flow-test-schema.ts
  • packages/worker/src/db.ts
  • packages/worker/src/email/delivery-queue.node.test.ts
  • packages/worker/src/email/delivery-queue.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/outbound-abuse.ts
  • packages/worker/src/email/outbound-abuse.workers.test.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/email/service.ts
  • packages/worker/src/entitlements/plans.ts
  • packages/worker/src/entitlements/service.ts
  • packages/worker/src/entitlements/test-schema.ts
  • packages/worker/src/execute-maintenance.node.test.ts
  • packages/worker/src/execute-maintenance.ts
  • packages/worker/src/mcp-auth.ts
  • packages/worker/src/mcp-auth.workers.test.ts
  • packages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.ts
  • packages/worker/src/mcp/capabilities/admin/admin-shared.ts
  • packages/worker/src/mcp/capabilities/admin/admin-user-usage.ts
  • packages/worker/src/mcp/capabilities/openapi-provider/operation-request.ts
  • packages/worker/src/mcp/fetch-gateway.node.test.ts
  • packages/worker/src/mcp/fetch-gateway.ts
  • packages/worker/src/mcp/fetch-gateway.workers.test.ts
  • packages/worker/src/mcp/run-kody-registry.ts
  • packages/worker/src/mcp/tools/execute.node.test.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/package-registry/test-schema.ts
  • packages/worker/src/package-runtime/package-app.ts
  • tools/check-migrations.node.test.ts
  • tools/migration-ledger.json

Comment thread docs/contributing/architecture/entitlements.md
Comment thread docs/use/email-primitives.md Outdated
Comment on lines +147 to +157
db
.prepare(
// Provider ('cloudflare-email') events only: outbound
// delivery outcomes, excluding inbound routing rejections.
`SELECT substr(created_at, 1, 10) AS day, event_type, COUNT(*) AS n
FROM email_delivery_events
WHERE provider = 'cloudflare-email' AND created_at >= ?
GROUP BY day, event_type`,
)
.bind(dayCutoff)
.all<EmailDeliveryDayRow>(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a '0090-user-abuse-controls.sql' packages/worker/migrations
rg -n -i -C 4 'email_delivery_events|user_id|loadAdminInsightsData|admin/insights' \
  packages/worker/migrations packages/worker/src packages/worker/client \
  -g '*.sql' -g '*.ts' -g '*.tsx'

Repository: kentcdodds/kody

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the admin-insights source and schema-related files without producing excessive output.
printf '--- relevant files ---\n'
fd -a 'admin-insights|0090-user-abuse|email_delivery|insights' packages/worker packages/worker/src packages/worker/client 2>/dev/null | sed -n '1,120p'

printf '\n--- admin-insights-data outline ---\n'
ast-grep outline packages/worker/src/app/admin-insights-data.ts --view compact 2>/dev/null | sed -n '1,220p' || rg -n "queryAdminInsights|loadAdminInsightsData|emailDeliveryByDay|email_delivery_events" packages/worker/src/app/admin-insights-data.ts -C 6

printf '\n--- targeted admin-insights-data snippets ---\n'
sed -n '1,220p' packages/worker/src/app/admin-insights-data.ts

printf '\n--- targeted schema migration snippets ---\n'
rg -n -C 5 "CREATE TABLE email_delivery_events|email_delivery_events" packages/worker/migrations -g '*.sql' | sed -n '1,220p'

printf '\n--- targeted TypeScript usage snippets ---\n'
rg -n -C 4 "emailDeliveryByDay|queryAdminInsights|loadAdminInsightsData|email_delivery_events|userId" packages/worker/src/app packages/worker/src/client packages/worker/client -g '*.ts' -g '*.tsx' | sed -n '1,260p'

Repository: kentcdodds/kody

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant files ---'
fd -a 'admin-insights|0090-user-abuse|email_delivery|insights' packages/worker packages/worker/src packages/worker/client 2>/dev/null | sed -n '1,160p'

printf '%s\n' '--- admin-insights-data outline ---'
ast-grep outline packages/worker/src/app/admin-insights-data.ts --view compact 2>/dev/null | sed -n '1,220p' || true

printf '%s\n' '--- targeted admin-insights-data snippets ---'
sed -n '1,220p' packages/worker/src/app/admin-insights-data.ts

printf '%s\n' '--- targeted schema migration snippets ---'
rg -n -C 5 "CREATE TABLE email_delivery_events|email_delivery_events" packages/worker/migrations -g '*.sql' | sed -n '1,220p'

printf '%s\n' '--- targeted TypeScript usage snippets ---'
rg -n -C 4 "emailDeliveryByDay|queryAdminInsights|loadAdminInsightsData|email_delivery_events|userId" packages/worker/src/app packages/worker/src/client packages/worker/client -g '*.ts' -g '*.tsx' | sed -n '1,280p'

Repository: kentcdodds/kody

Length of output: 40020


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- admin-insights-data rest ---'
sed -n '220,340p' packages/worker/src/app/admin-insights-data.ts

printf '%s\n' '--- admin-insights data test ---'
sed -n '1,240p' packages/worker/src/app/admin-insights-data.node.test.ts

printf '%s\n' '--- admin-insights handler ---'
sed -n '1,220p' packages/worker/src/app/handlers/admin-insights.ts

printf '%s\n' '--- loader-data admin types ---'
sed -n '1,220p' packages/worker/src/app/loader-data.ts

printf '%s\n' '--- routes admin-insights ---'
sed -n '1,260p' packages/worker/client/routes/admin-insights.tsx

printf '%s\n' '--- migrations full email_delivery_events table defs ---'
sed -n '176,195p' packages/worker/migrations/0030-email-primitives.sql
sed -n '40,96p' packages/worker/migrations/0061-email-delivery-lifecycle.sql
sed -n '215,311p' packages/worker/migrations/0031-unified-email-receipt.sql

Repository: kentcdodds/kody

Length of output: 28504


Keep insights aggregation scoped or explicitly platform-owned.

email_delivery_events.user_id is owned account data, so this aggregation should not read every tenant’s delivery outcomes without a userId predicate. Either scope this by an authorized userId/include it in the cache key for per-account insights, or define an explicit platform-wide aggregation boundary that does not expose cross-user tenant delivery events directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/admin-insights-data.ts` around lines 147 - 157, The
email delivery aggregation in the admin insights query must not read
cross-tenant events without an ownership boundary. Update the query and
surrounding insights flow to constrain results by the authorized userId,
including that value in any cache key, or explicitly route this metric through
an approved platform-wide aggregation boundary that does not expose tenant-level
outcomes directly.

Source: Coding guidelines

Comment thread packages/worker/src/mcp-auth.workers.test.ts
Comment thread packages/worker/src/mcp/fetch-gateway.node.test.ts
Comment thread packages/worker/src/mcp/fetch-gateway.ts
Comment thread packages/worker/src/email/outbound-abuse.ts
Comment thread packages/worker/src/email/outbound-abuse.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/worker/src/mcp-auth.workers.test.ts (1)

165-176: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle stable-user-only suspension lookups.

Production supports WHERE stable_user_id = ? when no email is available, but this mock always treats boundParams[0] as an email and reads the stable ID from index 1. Valid stable-user-only lookups therefore return null; custom accountByStableId identities are also compared against the defaults.

Mirror the query shape and expected identity used by the adjacent verification mocks.

As per coding guidelines, every user-owned read path must remain scoped by userId; this mock should validate the identity predicate actually used.

Proposed fix
 if (normalized.includes('select suspended_at from users')) {
-	const email =
-		typeof boundParams[0] === 'string' ? boundParams[0] : null
-	if (email !== defaultEmail) return null
-	if (normalized.includes('stable_user_id')) {
-		const stableUserId =
-			typeof boundParams[1] === 'string' ? boundParams[1] : null
-		if (stableUserId !== defaultStableUserId) return null
-	}
+	const expectedEmail =
+		options.accountByStableId?.email ?? defaultEmail
+	const expectedStableUserId =
+		options.accountByStableId?.stable_user_id ?? defaultStableUserId
+	const hasEmail = normalized.includes('email = ?')
+	const hasStableUserId = normalized.includes('stable_user_id = ?')
+	if (hasEmail) {
+		const email =
+			typeof boundParams[0] === 'string' ? boundParams[0] : null
+		if (email !== expectedEmail) return null
+	}
+	if (hasStableUserId) {
+		const index = hasEmail ? 1 : 0
+		const stableUserId =
+			typeof boundParams[index] === 'string' ? boundParams[index] : null
+		if (stableUserId !== expectedStableUserId) return null
+	}
 	return { suspended_at: options.suspendedAt ?? null }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/mcp-auth.workers.test.ts` around lines 165 - 176, Update
the mock branch handling isAccountSuspended queries in the query handler to
distinguish email-based lookups from stable_user_id-only lookups: validate
boundParams[0] against defaultEmail only when the query includes the email
predicate, otherwise validate it against the expected stable user ID. Reuse the
custom accountByStableId identity used by adjacent verification mocks so
user-owned reads remain scoped to the predicate actually present.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/worker/src/mcp-auth.workers.test.ts`:
- Around line 165-176: Update the mock branch handling isAccountSuspended
queries in the query handler to distinguish email-based lookups from
stable_user_id-only lookups: validate boundParams[0] against defaultEmail only
when the query includes the email predicate, otherwise validate it against the
expected stable user ID. Reuse the custom accountByStableId identity used by
adjacent verification mocks so user-owned reads remain scoped to the predicate
actually present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 17e9fad2-5ae9-4306-a78f-0bde2092b243

📥 Commits

Reviewing files that changed from the base of the PR and between 6189e1a and 16a0eaa.

📒 Files selected for processing (8)
  • docs/contributing/architecture/entitlements.md
  • docs/use/email-primitives.md
  • packages/worker/src/app/admin-insights-data.ts
  • packages/worker/src/email/outbound-abuse.ts
  • packages/worker/src/mcp-auth.workers.test.ts
  • packages/worker/src/mcp/fetch-gateway.node.test.ts
  • packages/worker/src/mcp/fetch-gateway.ts
  • packages/worker/src/mcp/fetch-gateway.workers.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/worker/src/mcp/fetch-gateway.node.test.ts
  • docs/use/email-primitives.md
  • docs/contributing/architecture/entitlements.md
  • packages/worker/src/mcp/fetch-gateway.workers.test.ts
  • packages/worker/src/mcp/fetch-gateway.ts
  • packages/worker/src/app/admin-insights-data.ts
  • packages/worker/src/email/outbound-abuse.ts

cursoragent and others added 6 commits July 24, 2026 17:33
… insights

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
…test scoping, doc threshold

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
…points

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor
cursor Bot force-pushed the cursor/bad-actor-controls-b47e branch from eb46df5 to 541bfff Compare July 24, 2026 17:42

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 541bfff. Configure here.

email: callerContext.user.email,
resource: 'execute_calls_per_day',
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta execute bypasses daily quota

Medium Severity

This PR adds execute_calls_per_day enforcement only on the public MCP execute tool, but the meta execute capability still calls runModuleWithRegistry with no consumeDailyEntitlement. Package and in-sandbox runs that use that capability (including many nested runs from a single tool call) can exceed the documented daily execute limit.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 541bfff. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/check-migrations.node.test.ts (1)

375-384: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use an unused migration prefix for this fixture.

The test writes 0091-future.sql, but packages/worker/migrations/ and tools/migration-ledger.json already have 0091-user-abuse-controls.sql, so this lands a duplicate prefix before the assertion. Use an unused prefix such as 0092-future.sql consistently, or another prefix greater than 91.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/check-migrations.node.test.ts` around lines 375 - 384, Update the
migration fixture in the test around the migrationPath, ledger.migrations entry,
and related filename references to use an unused prefix greater than 91, such as
0092, consistently throughout the setup and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/email/outbound-abuse.ts`:
- Around line 61-88: Update the complaint and bounce handling in the
delivery-status switch to persist and consult a resume watermark or per-event
application state, distinguishing events recorded before an account was resumed
from new events. Prevent historical replay events from pausing or notifying
again, while preserving crash recovery for a newly recorded complaint or bounce
that was persisted before pausing.
- Around line 158-164: Update the admins query in the outbound-abuse
notification flow to scope recipients through the paused user’s authorized
ownership/tenant relationship and bind the relevant userId parameter. Remove the
platform-wide admin lookup while preserving the existing email/username result
shape and notification behavior.
- Around line 193-196: Update the notification HTML construction in the outbound
abuse email flow to HTML-escape user-controlled content, including the username
embedded in text, before wrapping paragraphs in <p> elements. Apply the existing
escaping utility if available, and keep the surrounding document structure and
paragraph formatting unchanged.

---

Outside diff comments:
In `@tools/check-migrations.node.test.ts`:
- Around line 375-384: Update the migration fixture in the test around the
migrationPath, ledger.migrations entry, and related filename references to use
an unused prefix greater than 91, such as 0092, consistently throughout the
setup and assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 32b48204-68bc-424b-af3e-df80a66f6d34

📥 Commits

Reviewing files that changed from the base of the PR and between 16a0eaa and 541bfff.

📒 Files selected for processing (50)
  • docs/contributing/architecture/authentication.md
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/security.md
  • docs/use/email-primitives.md
  • packages/worker/client/routes/admin-insights.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/migrations/0091-user-abuse-controls.sql
  • packages/worker/src/app/account-suspension.ts
  • packages/worker/src/app/admin-insights-data.node.test.ts
  • packages/worker/src/app/admin-insights-data.ts
  • packages/worker/src/app/admin-user-usage-data.ts
  • packages/worker/src/app/admin-users-data.ts
  • packages/worker/src/app/authenticated-user.ts
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/request-auth-cache.ts
  • packages/worker/src/app/session-info.ts
  • packages/worker/src/community/community-flow-test-schema.ts
  • packages/worker/src/db.ts
  • packages/worker/src/email/delivery-queue.node.test.ts
  • packages/worker/src/email/delivery-queue.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/src/email/outbound-abuse.ts
  • packages/worker/src/email/outbound-abuse.workers.test.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/email/service.ts
  • packages/worker/src/entitlements/plans.ts
  • packages/worker/src/entitlements/service.ts
  • packages/worker/src/entitlements/test-schema.ts
  • packages/worker/src/execute-maintenance.node.test.ts
  • packages/worker/src/execute-maintenance.ts
  • packages/worker/src/mcp-auth.ts
  • packages/worker/src/mcp-auth.workers.test.ts
  • packages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.ts
  • packages/worker/src/mcp/capabilities/admin/admin-shared.ts
  • packages/worker/src/mcp/capabilities/admin/admin-user-usage.ts
  • packages/worker/src/mcp/capabilities/openapi-provider/operation-request.ts
  • packages/worker/src/mcp/fetch-gateway.node.test.ts
  • packages/worker/src/mcp/fetch-gateway.ts
  • packages/worker/src/mcp/fetch-gateway.workers.test.ts
  • packages/worker/src/mcp/run-kody-registry.ts
  • packages/worker/src/mcp/tools/execute.node.test.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/package-registry/test-schema.ts
  • packages/worker/src/package-runtime/package-app.ts
  • tools/check-migrations.node.test.ts
  • tools/migration-ledger.json
🚧 Files skipped from review as they are similar to previous changes (41)
  • packages/worker/src/package-registry/test-schema.ts
  • packages/worker/src/email/service.ts
  • packages/worker/src/app/session-info.ts
  • packages/worker/src/execute-maintenance.ts
  • packages/worker/src/app/authenticated-user.ts
  • packages/worker/src/execute-maintenance.node.test.ts
  • packages/worker/src/entitlements/test-schema.ts
  • packages/worker/src/mcp/capabilities/admin/admin-user-usage.ts
  • packages/worker/src/db.ts
  • packages/worker/src/mcp-auth.ts
  • packages/worker/src/community/community-flow-test-schema.ts
  • packages/worker/src/app/admin-user-usage-data.ts
  • docs/contributing/architecture/authentication.md
  • packages/worker/src/email/delivery-queue.node.test.ts
  • packages/worker/src/email/outbound-abuse.workers.test.ts
  • packages/worker/src/mcp/capabilities/openapi-provider/operation-request.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/outbound.workers.test.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/client/routes/admin-insights.tsx
  • docs/use/email-primitives.md
  • packages/worker/src/email/inbound.workers.test.ts
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/src/mcp/fetch-gateway.node.test.ts
  • docs/contributing/architecture/entitlements.md
  • packages/worker/src/app/admin-insights-data.node.test.ts
  • packages/worker/src/mcp/fetch-gateway.workers.test.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/app/request-auth-cache.ts
  • packages/worker/src/mcp/tools/execute.node.test.ts
  • packages/worker/src/package-runtime/package-app.ts
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/mcp/fetch-gateway.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.ts
  • packages/worker/src/email/delivery-queue.ts
  • packages/worker/src/app/account-suspension.ts
  • packages/worker/src/mcp-auth.workers.test.ts
  • packages/worker/src/entitlements/plans.ts
  • packages/worker/src/app/admin-users-data.ts

Comment on lines +61 to +88
switch (input.deliveryStatus) {
case 'complained': {
// A freshly recorded complaint always pauses. A replayed or
// conflicting-duplicate complaint signal only pauses when a
// real persisted complaint event backs it (crash recovery
// between recording and pausing), never on its own.
if (!input.eventRecorded) {
const complaintsToday = await countProviderDeliveryEventsToday({
db: input.env.APP_DB,
userId: input.userId,
eventType: 'complained',
now,
})
if (complaintsToday < 1) return { paused: false }
}
break
}
case 'bounced': {
const bouncesToday = await countProviderDeliveryEventsToday({
db: input.env.APP_DB,
userId: input.userId,
eventType: 'bounced',
now,
})
if (bouncesToday < outboundEmailBouncePauseThresholdPerDay) {
return { paused: false }
}
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Prevent historical replay events from re-pausing a resumed account.

Clearing email_outbound_paused_at removes the only idempotency marker. A replayed complaint finds the existing complaint count, and a replayed bounce finds the existing threshold count, then both pause and notify again. Persist a resume watermark or per-event application state so pre-resume events cannot reapply the pause while retaining crash recovery.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/email/outbound-abuse.ts` around lines 61 - 88, Update the
complaint and bounce handling in the delivery-status switch to persist and
consult a resume watermark or per-event application state, distinguishing events
recorded before an account was resumed from new events. Prevent historical
replay events from pausing or notifying again, while preserving crash recovery
for a newly recorded complaint or bounce that was persisted before pausing.

Comment on lines +158 to +164
const admins = await input.env.APP_DB.prepare(
`SELECT u.email, u.username FROM users u
INNER JOIN user_roles ur ON ur.user_id = u.id
INNER JOIN roles r ON r.id = ur.role_id
WHERE r.name = 'admin'
ORDER BY u.id ASC`,
).all<{ email: string; username: string }>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Scope the admin-recipient read to the owning user context.

This query reads every admin’s email and username without a userId-scoped ownership/tenant relationship, then shares the paused user’s event with all of them. Introduce and bind an authorized ownership relationship for the notifying scope rather than performing a platform-wide cross-user lookup. As per coding guidelines, every Kody multi-user read/write path must be scoped by userId; cross-user sharing is a bug.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/email/outbound-abuse.ts` around lines 158 - 164, Update
the admins query in the outbound-abuse notification flow to scope recipients
through the paused user’s authorized ownership/tenant relationship and bind the
relevant userId parameter. Remove the platform-wide admin lookup while
preserving the existing email/username result shape and notification behavior.

Source: Coding guidelines

Comment on lines +193 to +196
html: `<!doctype html><html lang="en"><body>${text
.split('\n\n')
.map((paragraph) => `<p>${paragraph}</p>`)
.join('')}</body></html>`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Escape user-controlled values before generating notification HTML.

username is inserted into text and then rendered as HTML without escaping. A crafted username can inject markup into emails sent to administrators. HTML-escape dynamic values when constructing the HTML body.

Proposed fix
+const escapeHtml = (value: string) =>
+	value.replace(/[&<>"']/g, (character) =>
+		({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&`#39`;' })[
+			character
+		]!,
+	)
+
 // ...
- html: `<!doctype html><html lang="en"><body>${text
+ html: `<!doctype html><html lang="en"><body>${escapeHtml(text)
    .split('\n\n')
    .map((paragraph) => `<p>${paragraph}</p>`)
    .join('')}</body></html>`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
html: `<!doctype html><html lang="en"><body>${text
.split('\n\n')
.map((paragraph) => `<p>${paragraph}</p>`)
.join('')}</body></html>`,
const escapeHtml = (value: string) =>
value.replace(/[&<>"']/g, (character) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&`#39`;' })[
character
]!,
)
// ...
html: `<!doctype html><html lang="en"><body>${escapeHtml(text)
.split('\n\n')
.map((paragraph) => `<p>${paragraph}</p>`)
.join('')}</body></html>`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/email/outbound-abuse.ts` around lines 193 - 196, Update
the notification HTML construction in the outbound abuse email flow to
HTML-escape user-controlled content, including the username embedded in text,
before wrapping paragraphs in <p> elements. Apply the existing escaping utility
if available, and keep the surrounding document structure and paragraph
formatting unchanged.

@kody-bot
kody-bot merged commit 49893e2 into main Jul 24, 2026
7 of 8 checks passed
@kody-bot
kody-bot deleted the cursor/bad-actor-controls-b47e branch July 24, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants