Repository navigation
feat(moderation): agent accounts that vote on and purge spam - #262
Merged
Merged
Conversation
An AI agent can hold a narrow grant to read the moderation roster and cast spam or not-spam votes. Its votes are labeled agent-<slug>, stage for three days, and any staff vote overrules them. Proposal for review; nothing is implemented. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
The operator's policy: purge at once when an account posts spam, vote when a signup only looks like spam. Adds a separate purge scope with API-enforced preconditions, a public spam-purges record that blocks the same GitHub account from signing straight back in, and a fix so a purged account's votes no longer attach to a new account under the same slug. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
…scrub rule Three corrections found while building: the PrivateProfile schema requires an email, the purge log needs a resolved actor beside the stored string, and the scrubbed snapshot must pseudonymize the new slug fields. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
A vote dated before the person's createdAt was cast on an earlier account with the same slug. It no longer counts in the roster, the reversal rule, the sweep or prune-spam. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Schemas, evaluator kinds, sheet configs and contracts for agent grants and the spam purge log, plus the private store for token hashes. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
An agent authenticates with a cfpa_ token checked against a stored hash and is anonymous outside its scopes. Administrators create, mint and revoke grants. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Agent spam votes stage a member for the three-day sweep while the grant is active. Human votes are final. Staff can withdraw all of an agent's votes. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
POST /api/admin/members/:slug/purge with reason, flags, evidence and dryRun. Every precondition is enforced in the API, one commit per purge. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
A purged spammer's GitHub id is refused at sign-up and account linking until staff unblock it. The sweep and staff purges write the same record. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
…gn-in Roster badges and filter, the Agents panel with grants and the purge log, the week-long banner, and the messages a blocked sign-in sees. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
How an administrator creates an agent and mints a grant, and how to restore a member purged by mistake. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Votes after a purge of the slug, retired slugs, atomic checks, claims, revoke with withdraw, URL and evidence rules. From an independent review of the implementation. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Votes at or before a purge of the slug do not count for a different person. Renames onto a slug with moderation history, and renames of agent accounts, are refused. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
…ore lock State was applied after the write lock was released, so concurrent purges could pass the daily cap or race a staff vote. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
A GitHub user with a matching login was offered the agent account as a claim candidate. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
An agent created without a grant was an ordinary user. Revoking a compromised token can now withdraw its staged votes in the same commit. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Bare and defanged domains are refused in agent reasons and vote reasoning. A malformed evidence identifier is a 422, not a 500. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Concurrent lastUsedAt writes shared one temp path and could fail or drop a new hash. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Matches the adoption proposed on the data repo's empty branch (codeforphilly-data PR 21). The drift check passes once that merges. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
The revert stops on a conflict after an unblock, the clone must be pulled first, the purge commit is found by its Purge-Id, and Not spam must land before the next sweep. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
…ed purges Two assertions the plan's validation list named but no test made. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
Sixteen of seventeen validation boxes verified. The contract box closes when the release adopts the two sheets on the published branch. Co-Authored-By: Claude Fable 5.2 <[email protected]> Claude-Session: https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs
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.
What this does
Gives the site agent accounts: a bot signs in with a scoped bearer token and can stage spam votes on new members and purge new members who posted spam. Built for GrittyBot, Code for Philly's ops bot, under the policy Chris set on November 12, 2026: purge at once when an account posts spam, and stage a vote when a sign-up only looks like spam.
moderation:read,moderation:vote,moderation:purge. Thecfpa_…token is shown once and only its hash is stored, privately. Grants last 90 days and revoke immediately.POST /api/admin/members/:slug/purgewith a reason, flags, 1 to 5 evidence items anddryRun. Refused unless the target is an ordinary member, joined within 7 days, with no human vote, no agent not-spam vote, no Slack sign-in and nothing shared with another member. Cap: 5 a day. One commit per purge, with the evidence paths in trailers.spam-purgesrecord. The same GitHub id is refused at sign-up and account linking until staff unblock it. The daily sweep and a staff purge marked "This was spam" write the same record.49bb3cb).Verification
npm run type-check && npm run lint && npm testpasses: 727 API, 188 web, 120 shared tests at8f4ce58; later commits add two tests and docs. CI is green.curl.Not verified, and risks
publishedhas not adopted the two new sheets. That happens at release time, perdocs/operations/releases.md. An API built from this branch will not boot against a branch without them.empty,fixtureandlegacy-importhave them (codeforphilly-data PRs 21, 22, 23, merged).Rollout
published, deploy sandbox then production.grittybot) with a first grant of read and vote, and puts the token on the bot's host. Runbook section "Agents".Follow-ups
Listed in
plans/bot-spam-votes.md. The ones that deserve an issue: an evaluator's vote on an earlier account under the same slug is overwritten by a new vote;scripts/prune-spam.tslacks the purge cutoff; other routes still apply state after the write lock;purge-orphansskips grant secrets; a restored member keeps the "Slug purged before" badge.Opened by GrittyBot, Code for Philly's ops bot (AI). 🧡🤖
🤖 Generated with Claude Code
https://claude.ai/code/session_01S8dthiZS2vwh674eh2DDQs