Skip to content

Commit eb90475

Browse files
committed
feat(settings): add configurable per-repo blacklistLabel (#1425)
Adds `blacklistLabel`, a per-repo configurable label (default "slop") for the contributor-blacklist disposition, wired with full config-as-code parity: DB column + migration, Drizzle/types, the focus-manifest (.gittensory.yml) resolver, OpenAPI, and the settings route. This is the management/config layer for the anti-abuse blacklist; the engine disposition that consumes it lands in a follow-up. The label is configurable so the disposition works regardless of the label a repo uses — nothing is hard-coded. Advances #1425.
1 parent a356441 commit eb90475

11 files changed

Lines changed: 54 additions & 3 deletions

File tree

apps/gittensory-ui/public/openapi.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8178,6 +8178,9 @@
81788178
"login"
81798179
]
81808180
}
8181+
},
8182+
"blacklistLabel": {
8183+
"type": "string"
81818184
}
81828185
},
81838186
"required": [
@@ -8200,6 +8203,7 @@
82008203
"slopAiAdvisory",
82018204
"autoLabelEnabled",
82028205
"gittensorLabel",
8206+
"blacklistLabel",
82038207
"createMissingLabel",
82048208
"publicSurface",
82058209
"includeMaintainerAuthors",
@@ -8803,6 +8807,9 @@
88038807
"advisory",
88048808
"block"
88058809
]
8810+
},
8811+
"blacklistLabel": {
8812+
"type": "string"
88068813
}
88078814
},
88088815
"required": [
@@ -8824,6 +8831,7 @@
88248831
"firstTimeContributorGrace",
88258832
"autoLabelEnabled",
88268833
"gittensorLabel",
8834+
"blacklistLabel",
88278835
"createMissingLabel",
88288836
"includeMaintainerAuthors",
88298837
"requireLinkedIssue",

docs/review-configuration.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ Everything a maintainer can toggle in the dashboard can be set as code under `se
154154
| Autonomy dial | `autonomy` | per-action-class level (`observe``auto`) | `{}` (= `observe`, deny-by-default) |
155155
| Auto-maintain policy | `autoMaintain` | `{ mergeMethod, requireApprovals }` | `squash` / `1` |
156156
| Command authorization | `commandAuthorization` | role policy | built-in default policy |
157+
| Contributor blacklist | `contributorBlacklist` | list of `{ login, reason?, evidence?, addedAt? }` (login required) | `[]` |
158+
| Blacklist label | `blacklistLabel` | string | `slop` |
159+
160+
The **contributor blacklist** is layered like every other setting (`.gittensory.yml`
161+
`settings.contributorBlacklist` > database) and is unioned with the shared/global list. Logins are
162+
public data, so entries carry only public-safe metadata (a `reason`, `evidence` URLs, an `addedAt`
163+
date) — never wallets, hotkeys, trust scores, or private values. `blacklistLabel` (default `slop`) is
164+
the label the engine applies to a blacklisted author's PR.
157165

158166
### Example `.gittensory.yml`
159167

@@ -196,6 +204,14 @@ settings:
196204
checkRunMode: enabled
197205
checkRunDetailLevel: standard
198206
badgeEnabled: true
207+
blacklistLabel: slop
208+
contributorBlacklist:
209+
- login: known-plagiarist
210+
reason: plagiarism
211+
evidence:
212+
- https://github.com/owner/repo/pull/1
213+
addedAt: "2026-06-26"
214+
- bad-farmer # bare login shorthand is also accepted
199215
```
200216
201217
---
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- #1425: per-repo configurable label for a blacklisted contributor's PR/issue. Default "slop" so the
2+
-- deterministic blacklist disposition works regardless of the label a repo uses.
3+
ALTER TABLE repository_settings ADD COLUMN blacklist_label TEXT NOT NULL DEFAULT 'slop';

src/api/routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ const repositorySettingsSchema = z.object({
618618
aiReviewModel: z.string().trim().min(1).max(120).nullable().optional(),
619619
autoLabelEnabled: z.boolean().default(true),
620620
gittensorLabel: z.string().trim().min(1).max(50).default("gittensor"),
621+
blacklistLabel: z.string().trim().min(1).max(50).default("slop"),
621622
createMissingLabel: z.boolean().default(true),
622623
publicSurface: z.enum(["off", "comment_and_label", "comment_only", "label_only"]).default("comment_and_label"),
623624
includeMaintainerAuthors: z.boolean().default(false),
@@ -666,6 +667,7 @@ const maintainerSettingsSchema = z
666667
slopAiAdvisory: z.boolean(),
667668
autoLabelEnabled: z.boolean(),
668669
gittensorLabel: z.string().trim().min(1).max(50),
670+
blacklistLabel: z.string().trim().min(1).max(50),
669671
createMissingLabel: z.boolean(),
670672
includeMaintainerAuthors: z.boolean(),
671673
requireLinkedIssue: z.boolean(),
@@ -3361,6 +3363,7 @@ export function createApp() {
33613363
aiReviewModel: parsed.data.aiReviewModel,
33623364
autoLabelEnabled: parsed.data.autoLabelEnabled,
33633365
gittensorLabel: parsed.data.gittensorLabel,
3366+
blacklistLabel: parsed.data.blacklistLabel,
33643367
createMissingLabel: parsed.data.createMissingLabel,
33653368
publicSurface: parsed.data.publicSurface,
33663369
includeMaintainerAuthors: parsed.data.includeMaintainerAuthors,

src/db/repositories.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
433433
aiReviewModel: null,
434434
autoLabelEnabled: true,
435435
gittensorLabel: "gittensor",
436+
blacklistLabel: "slop",
436437
createMissingLabel: true,
437438
publicSurface: "comment_and_label",
438439
includeMaintainerAuthors: false,
@@ -474,6 +475,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
474475
aiReviewModel: row.aiReviewModel ?? null,
475476
autoLabelEnabled: row.autoLabelEnabled,
476477
gittensorLabel: row.gittensorLabel,
478+
blacklistLabel: row.blacklistLabel,
477479
createMissingLabel: row.createMissingLabel,
478480
publicSurface: parsePublicSurface(row.publicSurface),
479481
includeMaintainerAuthors: row.includeMaintainerAuthors,
@@ -519,6 +521,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
519521
aiReviewModel: typeof settings.aiReviewModel === "string" && settings.aiReviewModel.trim() ? settings.aiReviewModel.trim() : null,
520522
autoLabelEnabled: settings.autoLabelEnabled ?? true,
521523
gittensorLabel: settings.gittensorLabel ?? "gittensor",
524+
blacklistLabel: settings.blacklistLabel ?? "slop",
522525
createMissingLabel: settings.createMissingLabel ?? true,
523526
publicSurface: settings.publicSurface ?? "comment_and_label",
524527
includeMaintainerAuthors: settings.includeMaintainerAuthors ?? false,
@@ -562,6 +565,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
562565
aiReviewModel: resolved.aiReviewModel,
563566
autoLabelEnabled: resolved.autoLabelEnabled,
564567
gittensorLabel: resolved.gittensorLabel,
568+
blacklistLabel: resolved.blacklistLabel,
565569
createMissingLabel: resolved.createMissingLabel,
566570
publicSurface: resolved.publicSurface,
567571
includeMaintainerAuthors: resolved.includeMaintainerAuthors,
@@ -606,6 +610,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
606610
aiReviewModel: resolved.aiReviewModel,
607611
autoLabelEnabled: resolved.autoLabelEnabled,
608612
gittensorLabel: resolved.gittensorLabel,
613+
blacklistLabel: resolved.blacklistLabel,
609614
createMissingLabel: resolved.createMissingLabel,
610615
publicSurface: resolved.publicSurface,
611616
includeMaintainerAuthors: resolved.includeMaintainerAuthors,

src/db/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export const repositorySettings = sqliteTable("repository_settings", {
6868
aiReviewModel: text("ai_review_model"),
6969
autoLabelEnabled: integer("auto_label_enabled", { mode: "boolean" }).notNull().default(true),
7070
gittensorLabel: text("gittensor_label").notNull().default("gittensor"),
71+
// Label applied to a blacklisted contributor's PR/issue (#1425); configurable so the disposition works
72+
// regardless of the label a repo uses.
73+
blacklistLabel: text("blacklist_label").notNull().default("slop"),
7174
createMissingLabel: integer("create_missing_label", { mode: "boolean" }).notNull().default(true),
7275
publicSurface: text("public_surface").notNull().default("comment_and_label"),
7376
includeMaintainerAuthors: integer("include_maintainer_authors", { mode: "boolean" }).notNull().default(false),

src/openapi/schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ export const RepositorySettingsSchema = z
603603
slopAiAdvisory: z.boolean(),
604604
autoLabelEnabled: z.boolean(),
605605
gittensorLabel: z.string(),
606+
blacklistLabel: z.string(),
606607
createMissingLabel: z.boolean(),
607608
publicSurface: z.enum(["off", "comment_and_label", "comment_only", "label_only"]),
608609
includeMaintainerAuthors: z.boolean(),
@@ -659,6 +660,7 @@ export const RepoSettingsPreviewSchema = z
659660
slopGateMinScore: z.number().nullable().optional(),
660661
autoLabelEnabled: z.boolean(),
661662
gittensorLabel: z.string(),
663+
blacklistLabel: z.string(),
662664
createMissingLabel: z.boolean(),
663665
includeMaintainerAuthors: z.boolean(),
664666
requireLinkedIssue: z.boolean(),

src/signals/focus-manifest.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export type FocusManifestSettings = Partial<
7575
| "agentPaused"
7676
| "agentDryRun"
7777
| "contributorBlacklist"
78+
| "blacklistLabel"
7879
>
7980
>;
8081

@@ -477,6 +478,8 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[])
477478
if (aiReviewModel !== null) out.aiReviewModel = aiReviewModel;
478479
const gittensorLabel = normalizeOptionalString(r.gittensorLabel, "settings.gittensorLabel", warnings);
479480
if (gittensorLabel !== null) out.gittensorLabel = gittensorLabel;
481+
const blacklistLabel = normalizeOptionalString(r.blacklistLabel, "settings.blacklistLabel", warnings);
482+
if (blacklistLabel !== null) out.blacklistLabel = blacklistLabel;
480483
const publicSurface = normalizeOptionalEnum(r.publicSurface, "settings.publicSurface", ["off", "comment_and_label", "comment_only", "label_only"] as const, warnings);
481484
if (publicSurface !== null) out.publicSurface = publicSurface;
482485
for (const key of ["aiReviewByok", "autoLabelEnabled", "createMissingLabel", "includeMaintainerAuthors", "requireLinkedIssue", "backfillEnabled", "privateTrustEnabled", "agentPaused", "agentDryRun"] as const) {

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,11 @@ export type RepositorySettings = {
559559
* DB) and unioned with the shared/global list at the point of use. Always populated by the DB layer
560560
* (default `[]`); optional so existing settings fixtures/callers need not be touched. */
561561
contributorBlacklist?: ContributorBlacklistEntry[] | undefined;
562+
/** The label applied to a blacklisted contributor's PR (#1425). Configurable per-repo (dashboard/DB +
563+
* `.gittensory.yml` `settings.blacklistLabel`); defaults to `"slop"` so the disposition works regardless of
564+
* the label a repo sets. Always populated by the DB layer (default `"slop"`); optional so existing settings
565+
* fixtures/callers need not be touched (mirrors the sibling `contributorBlacklist`). */
566+
blacklistLabel?: string | undefined;
562567
/** Agent-layer autonomy dial (#773): per-action-class level. Always populated by the DB layer (default
563568
* `{}` = deny-by-default = "observe" for every class); optional so existing settings fixtures/callers
564569
* need not be touched. The single source the action layer (#778) reads via `resolveAutonomy`. */

test/unit/focus-manifest.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -986,11 +986,13 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
986986
expect(ignored.autoMaintain).toEqual({ requireApprovals: 2, mergeMethod: "merge" });
987987
});
988988

989-
it("parses + resolves contributorBlacklist from the settings: block, overlaying the DB list (#1425)", () => {
990-
const manifest = parseFocusManifest({ settings: { contributorBlacklist: ["plagiarist1", { login: "farmer2", reason: "farming" }, { login: "-bad" }] } });
989+
it("parses + resolves contributorBlacklist + blacklistLabel from the settings: block, overlaying the DB (#1425)", () => {
990+
const manifest = parseFocusManifest({ settings: { contributorBlacklist: ["plagiarist1", { login: "farmer2", reason: "farming" }, { login: "-bad" }], blacklistLabel: "abuse" } });
991991
expect(manifest.settings.contributorBlacklist).toEqual([{ login: "plagiarist1" }, { login: "farmer2", reason: "farming" }]); // invalid login dropped
992+
expect(manifest.settings.blacklistLabel).toBe("abuse");
992993
const eff = resolveEffectiveSettings({ contributorBlacklist: [{ login: "db-only" }] } as unknown as RepositorySettings, manifest);
993994
expect(eff.contributorBlacklist?.map((e) => e.login)).toEqual(["plagiarist1", "farmer2"]); // yml overlays DB
995+
expect(eff.blacklistLabel).toBe("abuse"); // configurable label, not hardcoded
994996
// An empty/all-invalid block never blanks the DB-configured list (only set when a valid entry survives).
995997
const noOverride = resolveEffectiveSettings({ contributorBlacklist: [{ login: "keep-me" }] } as unknown as RepositorySettings, parseFocusManifest({ settings: { contributorBlacklist: [{ login: "" }] } }));
996998
expect(noOverride.contributorBlacklist?.map((e) => e.login)).toEqual(["keep-me"]);

0 commit comments

Comments
 (0)