Skip to content

feat(selfhost): add a modular moderation-rules engine with a cross-repo violation ledger - #2746

Merged
JSONbored merged 6 commits into
mainfrom
fix/selfhost-moderation-rules-engine
Jul 3, 2026
Merged

feat(selfhost): add a modular moderation-rules engine with a cross-repo violation ledger#2746
JSONbored merged 6 commits into
mainfrom
fix/selfhost-moderation-rules-engine

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • Adds a single, config-driven moderation layer over the three existing per-PR anti-abuse mechanisms (contributor cap, blacklist, review-nag): every time one of them fires against a non-exempt contributor, it counts toward that login's install-wide violation tally. At >=1 lifetime violation the contributor gets a configurable "warning" label; at the configured ban threshold (default 5) they get a "banned" label and, when enabled, are auto-added to the existing global contributor blacklist (the same enforcement an already-banned login gets).
  • global_moderation_config: a new singleton table holding the whole-layer on/off switch (default off), which of the three rules participate, label text (defaults mod:warning/mod:banned), the ban threshold, an optional violation-decay window (permanent by default, matching the existing blacklist's permanent-ban philosophy), and whether a ban auto-enforces.
  • Per-repo overrides in repository_settings (moderationGateMode, moderationRules, moderationWarningLabel, moderationBannedLabel), wired through the full settings pipeline in this same PR: migration, Drizzle schema, RepositorySettings, .gittensory.yml parsing, OpenAPI.
  • The violation ledger reuses the existing audit_events table (already install-wide by construction -- no repo/installation column at all -- the same shape review-nag's own cooldown counter already relies on) rather than adding a new table.
  • A single convergence point in the PR/issue action executors: every one of the three anti-abuse mechanisms already tags its close action with a matching closeKind, so escalation hooks in there instead of duplicating wiring at each mechanism's several call sites in queue/processors.ts.
  • Fully generic: no JSONbored/metagraphed/gittensory-repo-specific values are hardcoded into the engine. A self-hoster's own deployment config chooses everything; the whole layer defaults OFF until an operator opts in.

Scope

  • Conventional Commit title.
  • One cohesive feature (new engine + its full settings-pipeline wiring) -- not mixed with unrelated changes.
  • Follows CONTRIBUTING.md.
  • No linked issue -- a self-contained infra feature built from direct investigation/discussion, not tracked against a pre-filed issue.

Validation

  • git diff --check
  • npm run actionlint
  • npm run db:migrations:check / npm run db:schema-drift:check
  • npm run typecheck / npm run ui:typecheck
  • npm run test:coverage -- 100% line + branch coverage on every changed line across all 8 touched src/** files (verified via direct lcov cross-reference against the diff hunks, not just the aggregate report)
  • npm run ui:openapi:check / npm run ui:openapi:settings-parity
  • npm audit --audit-level=moderate
  • New behavior has unit tests for: the pure escalation/gate/rule-resolution logic (20 tests), the DB round-trip for the global config + violation ledger + per-repo overrides (15 tests), .gittensory.yml layering, and 14 executor-level integration scenarios including the exact "4 violations -> warning only, 5th -> banned + auto-blacklist" escalation path, decay-window scoping, per-repo gate/rule/label overrides, dry-run/denied-close no-ops, and double-add prevention.

Safety

  • No secrets/wallet/hotkey/trust-score/reward content anywhere.
  • The auto-ban path reuses the EXISTING global-blacklist write path (upsertGlobalContributorBlacklist) rather than adding a new enforcement mechanism, and is itself gated behind an explicit opt-in (autoBlacklistOnBan, plus the whole layer's own off-by-default master switch).
  • N/A: no auth/CORS/session change, no UI change (backend + generated OpenAPI artifact only).

Notes

  • Backend-only; no UI Evidence needed.
  • This PR is larger than usual (10 source files) because the feature is genuinely one cohesive unit -- migration + schema + settings-pipeline wiring + executor hook are all interdependent and none of them is independently useful/mergeable on its own.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
gittensory-ui 14356e3 Commit Preview URL

Branch Preview URL
Jul 03 2026, 07:35 PM

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.18%. Comparing base (68bda61) to head (14356e3).
⚠️ Report is 11 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2746      +/-   ##
==========================================
+ Coverage   96.13%   96.18%   +0.04%     
==========================================
  Files         248      249       +1     
  Lines       27569    27689     +120     
  Branches    10012    10065      +53     
==========================================
+ Hits        26503    26632     +129     
+ Misses        443      434       -9     
  Partials      623      623              
Files with missing lines Coverage Δ
src/db/repositories.ts 96.43% <100.00%> (+0.10%) ⬆️
src/db/schema.ts 69.46% <ø> (ø)
src/openapi/schemas.ts 100.00% <ø> (ø)
src/queue/processors.ts 92.64% <ø> (ø)
src/services/agent-action-executor.ts 96.07% <100.00%> (+0.57%) ⬆️
src/settings/moderation-rules.ts 100.00% <100.00%> (ø)
src/signals/focus-manifest.ts 99.38% <100.00%> (+<0.01%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@loopover-orb loopover-orb Bot added the gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. label Jul 3, 2026
@loopover-orb

loopover-orb Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-03 18:40:27 UTC

16 files · 1 AI reviewer · 1 blocker · readiness 93/100 · CI green · dirty

⏸️ Suggested Action - Manual Review

  • AI reviewers agree on a likely critical defect: src/services/agent-action-executor.ts:342 records a fresh moderation violation for every completed tracked close without first checking whether the same actor/eventType/targetKey was already recorded, so webhook replay or a queue retry after the close mutation can double-count the same PR/issue and falsely escalate the actor
  • make the ledger insert idempotent per actor + event type + target key, or count distinct targets instead. — Resolve the flagged defect, or override if the AI reviewers are mistaken, then re-run the gate.

Review summary
The PR wires a configurable moderation layer through repository settings, config-as-code parsing, schema/migrations, OpenAPI, and the action executor, with sensible default-off behavior and focused coverage for the happy paths. The main enforcement hook is in the right convergence point, but the ledger write is not idempotent, so replayed or retried close executions can inflate a contributor's count and trigger escalation without a new violation. The decay-window normalization also declares a bound that the enforcement path does not actually apply.

Blockers

  • src/services/agent-action-executor.ts:342 records a fresh moderation violation for every completed tracked close without first checking whether the same actor/eventType/targetKey was already recorded, so webhook replay or a queue retry after the close mutation can double-count the same PR/issue and falsely escalate the actor; make the ledger insert idempotent per actor + event type + target key, or count distinct targets instead.
  • src/services/agent-action-executor.ts:350 and src/db/repositories.ts:2410 accept violationDecayDays through normalizeOpenItemCap without enforcing MAX_MODERATION_VIOLATION_DECAY_DAYS, so a large positive configured value can produce an invalid Date and throw from toISOString() on the live close path; clamp or reject above the moderation-specific max before persisting/using it.
Nits — 5 non-blocking
  • nit: migrations/0104_repository_moderation_settings.sql:2 says global_moderation_config is migration 0101, but this PR adds it as 0103.
  • nit: src/db/repositories.ts:2380 says malformed JSON fails open to DEFAULT_GLOBAL_MODERATION_CONFIG, but malformed rules_json currently normalizes to an empty rules list while preserving the rest of the row.
  • nit: src/settings/moderation-rules.ts:119 documents non-positive thresholds as a runtime degradation path, but the DB read/upsert normalizes thresholds before enforcement, so that branch is mostly defensive and should be called out as such or kept only in unit coverage for the pure helper.
  • In src/services/agent-action-executor.ts:342, add an idempotency helper alongside the existing audit-event duplicate checks and cover a repeated execution of the same close plan against the same target.
  • In src/db/repositories.ts:2410, add a moderation-specific normalizer that enforces MAX_MODERATION_VIOLATION_DECAY_DAYS and test the upper-bound behavior.

Concerns raised — review before merging

  • src/services/agent-action-executor.ts:342 records a fresh moderation violation for every completed tracked close without first checking whether the same actor/eventType/targetKey was already recorded, so webhook replay or a queue retry after the close mutation can double-count the same PR/issue and falsely escalate the actor; make the ledger insert idempotent per actor + event type + target key, or count distinct targets instead.
Signal Result Evidence
Code review ❌ 1 blocker 1 reviewer
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (no linked issue context).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 60 registered-repo PR(s), 51 merged, 463 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 60 PR(s), 463 issue(s).
Gate result ❌ Blocking Repo-configured hard blocker found.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 60 PR(s), 463 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Explain no-issue PR.
  • No action.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

JSONbored added 6 commits July 3, 2026 12:29
…po violation ledger

Adds a single, config-driven moderation layer over the three existing
per-PR anti-abuse mechanisms (contributor cap, blacklist, review-nag):
every time one of them fires against a non-exempt contributor, it now
counts toward that login's install-wide violation tally. At >=1
lifetime violation the contributor gets a configurable "warning"
label; at the configured ban threshold (default 5) they get a
"banned" label and, when enabled, are auto-added to the existing
global contributor blacklist.

- global_moderation_config: a new singleton table holding the
  whole-layer on/off switch, which of the three rules participate,
  label text, ban threshold, an optional violation-decay window
  (permanent by default), and whether a ban auto-enforces.
- Per-repo overrides in repository_settings (moderationGateMode,
  moderationRules, moderationWarningLabel, moderationBannedLabel),
  wired through the full settings pipeline: migration, Drizzle
  schema, RepositorySettings, .gittensory.yml parsing, OpenAPI.
- The violation ledger reuses the existing audit_events table
  (install-wide by construction, same shape review-nag's own
  cooldown counter already uses) rather than a new table.
- A single convergence point in the PR/issue action executors: every
  anti-abuse close already tags itself with a closeKind (blacklist /
  contributor_cap / review_nag), so escalation hooks in there instead
  of duplicating wiring at each mechanism's own several call sites.
- Fully generic: no repo-specific values are hardcoded into the
  engine. A self-hoster's own deployment config chooses everything;
  the whole layer defaults OFF until an operator opts in.
….gittensory.yml override

normalizeModerationRules degrades BOTH a genuinely empty yml list and
a malformed one (non-array, or every entry invalid) to the same empty
array, but only the former is an intentional "opt every rule out for
this repo" -- the latter is bad config that should leave the DB value
untouched, not silently disable moderation for the repo. Distinguish
them by the raw input's own shape (a literal empty array) rather than
the normalized result alone.
…-effective rules

maybeEscalateModeration counted every rule type ever recorded
(Object.values(MODERATION_VIOLATION_EVENT_TYPE)) toward the ban
threshold, regardless of which rules the global/per-repo config
currently has enabled. A rule an operator has excluded still
influenced the ban decision as long as a violation of that kind was
recorded at some point (globally, or on a repo that still counts it).

Scope the count to effectiveRules (already computed for the record-time
gate just above) instead, so excluding a rule is an ongoing policy
stance about what a contributor's standing is judged on, consistently
applied at both record time and count time.
…amp the decay window

Two gate-flagged defects in the moderation-rules engine:

- recordModerationViolation recorded a fresh violation on every
  completed tracked close with no idempotency check, so a webhook
  redelivery or queue retry re-executing an already-recorded close
  could double-count it and falsely push a contributor toward the ban
  threshold. Now idempotent per (actor, eventType, targetKey); a
  duplicate returns false and the caller skips the rest of escalation.

- violationDecayDays was normalized with the generic, unbounded
  normalizeOpenItemCap, but it feeds Date arithmetic on the live close
  path (Date.now() - days * 86400000).toISOString(); an unbounded
  value could overflow into an Invalid Date and throw a RangeError,
  crashing the close. Clamped to MAX_MODERATION_VIOLATION_DECAY_DAYS
  on both read and write, mirroring reviewNagCooldownDays' own
  clamping shape for the same family of day-count settings.

Also fixes two non-blocking doc-comment nits the same review pass
raised (a stale migration-number cross-reference, and an
over-broad description of what "fails open" on malformed JSON).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.

Development

Successfully merging this pull request may close these issues.

1 participant