Skip to content

FEATURE: Add content scanning and abuse controls - #37

Open
bmdavis419 wants to merge 11 commits into
review/hosted-07-jobsfrom
review/hosted-08-abuse
Open

bmdavis419 wants to merge 11 commits into
review/hosted-07-jobsfrom
review/hosted-08-abuse

Conversation

@bmdavis419

@bmdavis419 bmdavis419 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Add trust-based publishing, content scans, abuse reports, operator moderation, suspension, and request rate limits. Verified organizations hold newly published bytes until scanning completes; established organizations use scanning after publication. Owner views show pending and quarantined states, and signed previews continue to work for held sites and older private versions.

Publication and moderation lock current metadata before deciding visibility. Historical versions require their own clearance for anonymous access. Scan obligations survive queue outages, stale results cannot replace newer decisions, and partial URL submissions retain successful scans. Reports have bounded request bodies; moderation checks the displayed version and works under the restricted database role. Cached host mappings recheck current organization trust. Device polling uses compatible slowdown responses and respects retry delays. HTML parsing handles effective base URLs; incomplete inspections retain a suspicious floor. File requests are limited before metadata lookup, held-site tabs open during the click, and site publication locks its current trust decision.

Important files:

  • services/files/, services/sites/, and content-version-access.ts: publication holds, version-specific access, and signed owner previews.
  • services/scanner.ts, scan-jobs.ts, and html-links.ts: bounded inspection, durable recovery, verdict ownership, and HTML parsing, effective base URLs, and completeness limits.
  • services/admin.ts and routes/admin/+page.svelte: reports, version-aware moderation, and suspension controls.
  • migrations-pg/0007_scans.sql through 0009_scan_recovery.sql: moderation records and persisted scan obligations.

Validation: full root unit/shared/CLI/rune suites, route/Postgres integration tests, TypeScript/Effect/Svelte checks, formatting, and Worker build. Four broad review passes completed; the last pass had two accepted findings, both fixed and independently reviewed in a targeted closeout. Bot closeout covered 182 route/Postgres cases; two outdated verdict assertions were corrected and all 37 affected cases passed. HTML parser and MIME tests passed, including the bounded PE-signature follow-up; final targeted review was clean. Provider tests use mocks and local bindings; live Cloudflare URL scanning, cache purging, and deployed queue delivery remain separate verification steps.

Stack layer 8/11: depends on #36; followed by #38.

Final bot followup also recognizes every universal Mach-O header variant. All9 MIME tests passed; targeted independent review clean.

Note

Add content scanning, trust levels, rate limits, and admin abuse controls

  • Adds a scanner service that runs content scans with MIME sniffing, HTML link extraction, SHA-256 hashing, and Cloudflare URL reputation checks; scan verdicts (clean, suspicious, malicious) gate file publication through a publish-pending state in internals.ts, mutations.ts, and upload.ts
  • Introduces organization trust levels (new, verified, established, suspended) in trust-policy.ts and trust.ts that control publish eligibility, hourly publish quotas (30 verified / 300 established), and whether scans run before or after publication
  • Replaces AuthGuard with a RateLimits Effect service in rate-limits.ts backed by four Cloudflare Worker rate-limit bindings (upload, publish, auth, anonymous), keyed by organization ID or client IP; deletes auth-guard.ts
  • Adds admin moderation endpoints and UI in admin.ts and admin/+page.svelte for resolving abuse reports, marking file verdicts, blocking content hashes, and suspending or promoting organizations; adds the public /report abuse-report form in report/+server.ts
  • Behavioral Change: public-intent uploads and visibility changes now persist files as private with publish_pending until their scan completes; quarantined files are excluded from content queries and disabled in the dashboard UI; suspended organizations are rejected at authentication time with a 403; the DashboardFileSchema in packages/shared/src/index.ts now requires quarantined and publishPending boolean fields, and the JobSchema scan variant accepts an optional urlScan object — out-of-tree consumers of these schemas must be updated

Macroscope summarized 1d969eb.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 9 days. After that, they cost $0.25 per reviewed file.

Or wait 33 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 6 included reviews currently available. Your 49 included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: ad9faf91-e48f-4e51-b400-33c0ec4f11fe

📥 Commits

Reviewing files that changed from the base of the PR and between 0b19dc2 and 1d969eb.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (110)
  • README.md
  • apps/web/.dev.vars.example
  • apps/web/migrations-pg/0007_scans.sql
  • apps/web/migrations-pg/0008_abuse.sql
  • apps/web/migrations-pg/0009_scan_recovery.sql
  • apps/web/package.json
  • apps/web/src/app.d.ts
  • apps/web/src/env.d.ts
  • apps/web/src/lib/components/Dashboard.svelte
  • apps/web/src/lib/components/FileDetailView.svelte
  • apps/web/src/lib/components/files/FileCard.svelte
  • apps/web/src/lib/components/files/FileList.svelte
  • apps/web/src/lib/components/files/FileMenu.svelte
  • apps/web/src/lib/components/files/FileSidebar.svelte
  • apps/web/src/lib/components/files/VersionList.svelte
  • apps/web/src/lib/dashboard/file-list.svelte.test.ts
  • apps/web/src/lib/dashboard/parse.test.ts
  • apps/web/src/lib/dashboard/parse.ts
  • apps/web/src/lib/dashboard/selection.svelte.test.ts
  • apps/web/src/lib/server/auth-rate-limit-response.ts
  • apps/web/src/lib/server/config.test.ts
  • apps/web/src/lib/server/config.ts
  • apps/web/src/lib/server/content-host.ts
  • apps/web/src/lib/server/content-version-access.ts
  • apps/web/src/lib/server/edge.ts
  • apps/web/src/lib/server/file-content-link.ts
  • apps/web/src/lib/server/file-rows.ts
  • apps/web/src/lib/server/host-gate.test.ts
  • apps/web/src/lib/server/host-gate.ts
  • apps/web/src/lib/server/html-links.test.ts
  • apps/web/src/lib/server/html-links.ts
  • apps/web/src/lib/server/jobs/consumer.ts
  • apps/web/src/lib/server/layer.ts
  • apps/web/src/lib/server/mcp/server.ts
  • apps/web/src/lib/server/mime-sniff.test.ts
  • apps/web/src/lib/server/mime-sniff.ts
  • apps/web/src/lib/server/report-policy.ts
  • apps/web/src/lib/server/request-auth.ts
  • apps/web/src/lib/server/routes/abuse.test.ts
  • apps/web/src/lib/server/routes/device-polling.test.ts
  • apps/web/src/lib/server/routes/held-site-preview.pg.test.ts
  • apps/web/src/lib/server/routes/queues.test.ts
  • apps/web/src/lib/server/routes/routes.test.ts
  • apps/web/src/lib/server/routes/tenancy.test.ts
  • apps/web/src/lib/server/scan-inspection.test.ts
  • apps/web/src/lib/server/scan-inspection.ts
  • apps/web/src/lib/server/scan-jobs.ts
  • apps/web/src/lib/server/scan-policy.test.ts
  • apps/web/src/lib/server/scan-policy.ts
  • apps/web/src/lib/server/services/admin.pg.test.ts
  • apps/web/src/lib/server/services/admin.ts
  • apps/web/src/lib/server/services/auth-guard.test.ts
  • apps/web/src/lib/server/services/auth-guard.ts
  • apps/web/src/lib/server/services/auth.pg.test.ts
  • apps/web/src/lib/server/services/auth.ts
  • apps/web/src/lib/server/services/bindings.ts
  • apps/web/src/lib/server/services/cache-purge.ts
  • apps/web/src/lib/server/services/files/history-access.pg.test.ts
  • apps/web/src/lib/server/services/files/internals.ts
  • apps/web/src/lib/server/services/files/mutations.ts
  • apps/web/src/lib/server/services/files/publication.pg.test.ts
  • apps/web/src/lib/server/services/files/queries.ts
  • apps/web/src/lib/server/services/files/rename.pg.test.ts
  • apps/web/src/lib/server/services/files/upload.ts
  • apps/web/src/lib/server/services/grant-secrets.pg.test.ts
  • apps/web/src/lib/server/services/lifecycle.test.ts
  • apps/web/src/lib/server/services/lifecycle.ts
  • apps/web/src/lib/server/services/org.pg.test.ts
  • apps/web/src/lib/server/services/rate-limits.ts
  • apps/web/src/lib/server/services/scanner.pg.test.ts
  • apps/web/src/lib/server/services/scanner.ts
  • apps/web/src/lib/server/services/sites/publish.pg.test.ts
  • apps/web/src/lib/server/services/sites/quota.pg.test.ts
  • apps/web/src/lib/server/services/sites/read.ts
  • apps/web/src/lib/server/services/sites/sessions.ts
  • apps/web/src/lib/server/services/url-reputation.test.ts
  • apps/web/src/lib/server/services/url-reputation.ts
  • apps/web/src/lib/server/services/workos.test.ts
  • apps/web/src/lib/server/test/route-context.ts
  • apps/web/src/lib/server/test/scan.ts
  • apps/web/src/lib/server/trust-policy.test.ts
  • apps/web/src/lib/server/trust-policy.ts
  • apps/web/src/lib/server/trust.ts
  • apps/web/src/routes/+layout.server.ts
  • apps/web/src/routes/+layout.svelte
  • apps/web/src/routes/admin/+page.server.ts
  • apps/web/src/routes/admin/+page.svelte
  • apps/web/src/routes/api/admin/files/[id]/+server.ts
  • apps/web/src/routes/api/admin/hashes/+server.ts
  • apps/web/src/routes/api/admin/orgs/[id]/+server.ts
  • apps/web/src/routes/api/admin/overview/+server.ts
  • apps/web/src/routes/api/admin/reports/[id]/+server.ts
  • apps/web/src/routes/api/auth/device/+server.ts
  • apps/web/src/routes/api/auth/device/token/+server.ts
  • apps/web/src/routes/api/files/+server.ts
  • apps/web/src/routes/api/files/[id]/versions/+server.ts
  • apps/web/src/routes/api/internal/maintenance/+server.ts
  • apps/web/src/routes/api/sites/sessions/+server.ts
  • apps/web/src/routes/f/[id]/+server.ts
  • apps/web/src/routes/report/+server.ts
  • apps/web/src/routes/s/[id]/[...path]/+server.ts
  • apps/web/src/routes/t/[id]/[version]/grid.webp/+server.ts
  • apps/web/worker-configuration.d.ts
  • apps/web/wrangler.jsonc
  • docs/abuse.md
  • docs/plans/hosted-product.md
  • packages/cli/src/cli-smoke.test.ts
  • packages/cli/src/commands/auth.ts
  • packages/shared/src/index.ts
  • scripts/check-wrangler-drift.mjs

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

@bmdavis419
bmdavis419 added this pull request to stack #41 September 11, 2026 04:32
Comment thread apps/web/src/routes/f/[id]/+server.ts Outdated
Comment thread apps/web/src/lib/server/mime-sniff.ts Outdated
Comment thread apps/web/src/lib/server/services/admin.ts
Comment thread apps/web/src/lib/server/html-links.ts Outdated
yield* sql`
UPDATE files
SET public = ${visibility.public}, updated_at = ${updatedAt}
SET public = ${isPublicNow}, publish_pending = ${hold},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High files/mutations.ts:61

Cancelling a held publish in setVisibility does not prevent the in-flight scan from later setting public = true, so a file the user made private is exposed again. The scanner’s publish update must also require publish_pending to still be true (or otherwise invalidate the scan when this flag is cleared).

Also found in 1 other location(s)

apps/web/src/lib/server/services/scanner.ts:280

The publish update checks the version and quarantine state but not publish_pending. If a user cancels a pending publish by making the file private while its scan is running, setVisibility clears publish_pending; this stale scan then still executes this update and makes the file public. The user’s explicit privacy change is therefore undone and private content is exposed.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/services/files/mutations.ts around line 61:

Cancelling a held publish in `setVisibility` does not prevent the in-flight scan from later setting `public = true`, so a file the user made private is exposed again. The scanner’s publish update must also require `publish_pending` to still be true (or otherwise invalidate the scan when this flag is cleared).

Also found in 1 other location(s):
- apps/web/src/lib/server/services/scanner.ts:280 -- The publish update checks the version and quarantine state but not `publish_pending`. If a user cancels a pending publish by making the file private while its scan is running, `setVisibility` clears `publish_pending`; this stale scan then still executes this update and makes the file public. The user’s explicit privacy change is therefore undone and private content is exposed.

Comment thread apps/web/src/routes/api/auth/device/token/+server.ts
Comment thread apps/web/src/lib/server/services/files/mutations.ts Outdated
scan: received,
// The scanner records its own outcome (a verdict row, a re-sent
// poll); only storage trouble asks for a redelivery.
scan: (job) => scanner.runOne(job).pipe(Effect.as('done')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High jobs/consumer.ts:100

scan acknowledges the job as 'done' even when scanner.runOne returns Polling after jobs.trySend fails, so the only follow-up URL-scan poll is lost. Because trySend only logs the enqueue error and Lifecycle has no reconciliation sweep, held publishes remain pending indefinitely and already-public files are never finalized or quarantined. Preserve the scanner outcome (or propagate the enqueue failure) so the message is retried when the poll cannot be queued.

Also found in 3 other location(s)

apps/web/src/lib/server/services/files/mutations.ts:70

sendScanJob at line 70 uses jobs.trySend, which deliberately swallows a queue-send failure. This mutation leaves verified-org files with public = false and publish_pending = true, but the lifecycle only runs purge/index/site work and there is no scan reconciliation path. Therefore a transient queue outage permanently leaves a requested publish unavailable rather than retrying it.

apps/web/src/lib/server/services/scanner.ts:511

The initial URL-scan pass submits the URLs and then uses jobs.trySend for the only polling job. trySend swallows queue-send failures, while this service persists no pending poll state for maintenance to reconstruct. If that enqueue fails, the consumed job is acknowledged: a verified file remains held forever, and an already-public file is never quarantined even if the submitted URL scan later reports malicious.

apps/web/src/lib/server/services/sites/sessions.ts:413

Using jobs.trySend for the only scan job silently drops the job during a queue-send outage. A verified org's commit has already stored public = false, publish_pending = true, and there is no scanner lifecycle sweep or other reconciliation that re-enqueues held rows (the lifecycle only sweeps sites, indexing, and file purges). The site therefore remains permanently unavailable until manual intervention instead of eventually being scanned and published.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/jobs/consumer.ts around line 100:

`scan` acknowledges the job as `'done'` even when `scanner.runOne` returns `Polling` after `jobs.trySend` fails, so the only follow-up URL-scan poll is lost. Because `trySend` only logs the enqueue error and `Lifecycle` has no reconciliation sweep, held publishes remain pending indefinitely and already-public files are never finalized or quarantined. Preserve the scanner outcome (or propagate the enqueue failure) so the message is retried when the poll cannot be queued.

Also found in 3 other location(s):
- apps/web/src/lib/server/services/files/mutations.ts:70 -- `sendScanJob` at line 70 uses `jobs.trySend`, which deliberately swallows a queue-send failure. This mutation leaves verified-org files with `public = false` and `publish_pending = true`, but the lifecycle only runs purge/index/site work and there is no scan reconciliation path. Therefore a transient queue outage permanently leaves a requested publish unavailable rather than retrying it.
- apps/web/src/lib/server/services/scanner.ts:511 -- The initial URL-scan pass submits the URLs and then uses `jobs.trySend` for the only polling job. `trySend` swallows queue-send failures, while this service persists no pending poll state for maintenance to reconstruct. If that enqueue fails, the consumed job is acknowledged: a verified file remains held forever, and an already-public file is never quarantined even if the submitted URL scan later reports malicious.
- apps/web/src/lib/server/services/sites/sessions.ts:413 -- Using `jobs.trySend` for the only scan job silently drops the job during a queue-send outage. A verified org's commit has already stored `public = false, publish_pending = true`, and there is no scanner lifecycle sweep or other reconciliation that re-enqueues held rows (the lifecycle only sweeps sites, indexing, and file purges). The site therefore remains permanently unavailable until manual intervention instead of eventually being scanned and published.

);
const trust = recover(
'trust',
Effect.suspend(() => promoteEstablished(sql, new Date())),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High services/lifecycle.ts:101

lifecycle.trust promotes a long-lived free organization to established on the next maintenance tick after it upgrades to a paid plan, instead of waiting 14 days on the paid plan. promoteEstablished uses orgs.created_at for the cutoff, so it bypasses the intended verified-organization scan hold; base eligibility on the paid-plan transition time instead.

Also found in 2 other location(s)

apps/web/src/lib/server/trust.ts:59

promoteEstablished compares created_at with the 14-day cutoff, so an old free org that upgrades today is promoted to established on the next sweep instead of after 14 days on a paid plan. This bypasses the intended verified-org scan-before-publish period immediately after upgrading.

apps/web/src/routes/api/internal/maintenance/+server.ts:36

lifecycle.trust promotes organizations using promoteEstablished, whose eligibility is based on orgs.created_at rather than when the organization became paid. A long-lived free organization that upgrades to a paid plan is therefore promoted to established on the next maintenance tick instead of after the documented 14 days on a paid plan, bypassing the intended scan-before-publish trust period.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/services/lifecycle.ts around line 101:

`lifecycle.trust` promotes a long-lived free organization to `established` on the next maintenance tick after it upgrades to a paid plan, instead of waiting 14 days on the paid plan. `promoteEstablished` uses `orgs.created_at` for the cutoff, so it bypasses the intended verified-organization scan hold; base eligibility on the paid-plan transition time instead.

Also found in 2 other location(s):
- apps/web/src/lib/server/trust.ts:59 -- `promoteEstablished` compares `created_at` with the 14-day cutoff, so an old free org that upgrades today is promoted to `established` on the next sweep instead of after 14 days on a paid plan. This bypasses the intended verified-org scan-before-publish period immediately after upgrading.
- apps/web/src/routes/api/internal/maintenance/+server.ts:36 -- `lifecycle.trust` promotes organizations using `promoteEstablished`, whose eligibility is based on `orgs.created_at` rather than when the organization became paid. A long-lived free organization that upgrades to a paid plan is therefore promoted to `established` on the next maintenance tick instead of after the documented 14 days on a paid plan, bypassing the intended scan-before-publish trust period.

Comment thread apps/web/src/lib/server/services/admin.ts Outdated
Comment thread apps/web/src/lib/server/mime-sniff.ts Outdated
Comment thread apps/web/src/lib/components/Dashboard.svelte Outdated
Comment thread apps/web/src/lib/server/services/sites/sessions.ts Outdated
@bmdavis419

Copy link
Copy Markdown
Contributor Author

Addressed the confirmed follow-ups in this review update:

  • Limit file requests before metadata lookup, including nonexistent IDs.
  • Recognize BOM-prefixed HTML/SVG and avoid classifying ordinary MZ text as a PE executable.
  • Parse HTML with browser-compatible attribute/base handling and keep incomplete inspections held.
  • Open the destination tab during the click before awaiting a held site's signed link.
  • Recheck and lock organization trust inside site publication, including changes while waiting on file metadata.

Earlier reviewed fixes already cover canceling held publication, current-version mutation/scan selection, durable scan/poll recovery, and failure-safe suspension.

Intentional decisions:

  • Establishment uses organization age over 14 days plus a currently paid plan, as specified in the reviewed plan and abuse runbook. Updated stale inline comments that suggested paid-plan tenure.
  • Malicious retained versions conservatively quarantine the file. A version-aware operator clearance can restore the current version without clearing older malicious bytes.
  • Worker cache keys normalize irrelevant query strings, and content routes check current file/version access and organization trust before returning cached data. The reported query-variant cache bypass does not match this path; copies already downloaded to browsers are outside edge invalidation.

Validation results are recorded in the PR description. No deployment or live-provider claims are implied.

Comment thread apps/web/src/lib/server/mime-sniff.ts
@bmdavis419
bmdavis419 marked this pull request as ready for review September 11, 2026 08:23
@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown

Too many files changed for review (111 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

bmdavis419 and others added 11 commits September 11, 2026 01:47
Four rate limit bindings (RL_UPLOAD, RL_PUBLISH, RL_AUTH, RL_ANON) replace
the KV counters in auth-guard.ts. Uploads and site sessions are keyed by
org, device auth by client address, and anonymous content fetches past the
edge cache by IP. The wrangler drift check compares the bindings and their
limits across environments; route tests swap the bindings for fakes with a
per-name denial switch. The AUTH_GUARD namespace stays for the slug and
query embedding caches.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
trust-policy.ts decides what each level may do: new orgs stay private,
verified and established ones may publish. Signing in with a verified email
promotes new to verified; the maintenance tick promotes verified orgs on a
paid plan for 14 days to established. Uploads that land public (including
HTML, which is forced public), visibility changes, renames to .html, and
site sessions and commits all pass through requirePublishAllowed, which
answers 403 "Verify your email to share publicly" for a new org.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every version that becomes public gets a scan job. A verified org's publish
(visibility change, rename to .html, site commit) is held with
publish_pending until the scanner clears it; established orgs publish now
and are scanned after. The Scanner runs three checks and records a
scan_verdicts row for each: the object's sha256 against blocked_hashes,
the first bytes against the declared type (mime-sniff.ts), and the
outbound links in HTML through the Cloudflare URL Scanner (UrlReputation;
a Null answers clean when URLSCAN_API_KEY is unset, `fake:<verdict>`
selects a fake). Link scans are collected by re-sending the job with the
scan ids. Clean publishes a held row and purges the edge cache; malicious
quarantines the file, which content routes then 404, and writes a
notification; suspicious stays held for review.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`/report` on every content host takes a report (JSON or the one-form
page at `/report?f=<id>`) for a file that host serves, rate limited by
address; the address is stored hashed. The Admin service carries the kill
switch: suspendOrg sets trust to suspended, drops the slug cache so the
host answers 404 at once, and purges the host from the edge through the
Cloudflare API when CF_API_TOKEN and CF_ZONE_ID are set (logged when
not). API keys and sessions for a suspended org stop resolving with 401.
restoreOrg reverses it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ADMIN_USER_IDS names the WorkOS users who may operate the platform;
requireAdmin accepts only a browser session for one of them, never an API
key. /admin on the dashboard origin lists open reports, held and
quarantined files with their verdicts, dead-lettered jobs, and recent orgs
with trust, plan, and usage, with row actions for resolving a report,
marking a file clean or malicious, bumping trust, suspending and
restoring an org, and blocking a hash. Every action goes through
/api/admin/*, which the route tests exercise for non-admins and admins.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/abuse.md: the zone checklist (WAF managed rules, bot fight mode,
hotlink protection off, Netcraft feed, DMCA agent, abuse@ mailbox), the
trust levels and what each may do, the scan pipeline check by check and
what each verdict does to the row, the report endpoint, the kill switch
step by step including the manual purge when the zone API is not
configured, the rate limit bindings, and how to work /admin.

The admin review list also surfaces live files the scanner flagged after
publish until an operator rules on them, and admin verdict rows record
who made the call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bmdavis419
bmdavis419 force-pushed the review/hosted-08-abuse branch from 2e6e1bc to 1d969eb Compare September 11, 2026 08:50
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.

1 participant