Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8178,6 +8178,9 @@
"login"
]
}
},
"blacklistLabel": {
"type": "string"
}
},
"required": [
Expand All @@ -8200,6 +8203,7 @@
"slopAiAdvisory",
"autoLabelEnabled",
"gittensorLabel",
"blacklistLabel",
"createMissingLabel",
"publicSurface",
"includeMaintainerAuthors",
Expand Down Expand Up @@ -8803,6 +8807,9 @@
"advisory",
"block"
]
},
"blacklistLabel": {
"type": "string"
}
},
"required": [
Expand All @@ -8824,6 +8831,7 @@
"firstTimeContributorGrace",
"autoLabelEnabled",
"gittensorLabel",
"blacklistLabel",
"createMissingLabel",
"includeMaintainerAuthors",
"requireLinkedIssue",
Expand Down
16 changes: 16 additions & 0 deletions docs/review-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ Everything a maintainer can toggle in the dashboard can be set as code under `se
| Autonomy dial | `autonomy` | per-action-class level (`observe`…`auto`) | `{}` (= `observe`, deny-by-default) |
| Auto-maintain policy | `autoMaintain` | `{ mergeMethod, requireApprovals }` | `squash` / `1` |
| Command authorization | `commandAuthorization` | role policy | built-in default policy |
| Contributor blacklist | `contributorBlacklist` | list of `{ login, reason?, evidence?, addedAt? }` (login required) | `[]` |
| Blacklist label | `blacklistLabel` | string | `slop` |

The **contributor blacklist** is layered like every other setting (`.gittensory.yml`
`settings.contributorBlacklist` > database) and is unioned with the shared/global list. Logins are
public data, so entries carry only public-safe metadata (a `reason`, `evidence` URLs, an `addedAt`
date) — never wallets, hotkeys, trust scores, or private values. `blacklistLabel` (default `slop`) is
the label the engine applies to a blacklisted author's PR.

### Example `.gittensory.yml`

Expand Down Expand Up @@ -196,6 +204,14 @@ settings:
checkRunMode: enabled
checkRunDetailLevel: standard
badgeEnabled: true
blacklistLabel: slop
contributorBlacklist:
- login: known-plagiarist
reason: plagiarism
evidence:
- https://github.com/owner/repo/pull/1
addedAt: "2026-06-26"
- bad-farmer # bare login shorthand is also accepted
```

---
Expand Down
3 changes: 3 additions & 0 deletions migrations/0073_blacklist_label.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- #1425: per-repo configurable label for a blacklisted contributor's PR/issue. Default "slop" so the
-- deterministic blacklist disposition works regardless of the label a repo uses.
ALTER TABLE repository_settings ADD COLUMN blacklist_label TEXT NOT NULL DEFAULT 'slop';
3 changes: 3 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ const repositorySettingsSchema = z.object({
aiReviewModel: z.string().trim().min(1).max(120).nullable().optional(),
autoLabelEnabled: z.boolean().default(true),
gittensorLabel: z.string().trim().min(1).max(50).default("gittensor"),
blacklistLabel: z.string().trim().min(1).max(50).default("slop"),
createMissingLabel: z.boolean().default(true),
publicSurface: z.enum(["off", "comment_and_label", "comment_only", "label_only"]).default("comment_and_label"),
includeMaintainerAuthors: z.boolean().default(false),
Expand Down Expand Up @@ -666,6 +667,7 @@ const maintainerSettingsSchema = z
slopAiAdvisory: z.boolean(),
autoLabelEnabled: z.boolean(),
gittensorLabel: z.string().trim().min(1).max(50),
blacklistLabel: z.string().trim().min(1).max(50),
createMissingLabel: z.boolean(),
includeMaintainerAuthors: z.boolean(),
requireLinkedIssue: z.boolean(),
Expand Down Expand Up @@ -3361,6 +3363,7 @@ export function createApp() {
aiReviewModel: parsed.data.aiReviewModel,
autoLabelEnabled: parsed.data.autoLabelEnabled,
gittensorLabel: parsed.data.gittensorLabel,
blacklistLabel: parsed.data.blacklistLabel,
createMissingLabel: parsed.data.createMissingLabel,
publicSurface: parsed.data.publicSurface,
includeMaintainerAuthors: parsed.data.includeMaintainerAuthors,
Expand Down
5 changes: 5 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
aiReviewModel: null,
autoLabelEnabled: true,
gittensorLabel: "gittensor",
blacklistLabel: "slop",
createMissingLabel: true,
publicSurface: "comment_and_label",
includeMaintainerAuthors: false,
Expand Down Expand Up @@ -474,6 +475,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
aiReviewModel: row.aiReviewModel ?? null,
autoLabelEnabled: row.autoLabelEnabled,
gittensorLabel: row.gittensorLabel,
blacklistLabel: row.blacklistLabel,
createMissingLabel: row.createMissingLabel,
publicSurface: parsePublicSurface(row.publicSurface),
includeMaintainerAuthors: row.includeMaintainerAuthors,
Expand Down Expand Up @@ -519,6 +521,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewModel: typeof settings.aiReviewModel === "string" && settings.aiReviewModel.trim() ? settings.aiReviewModel.trim() : null,
autoLabelEnabled: settings.autoLabelEnabled ?? true,
gittensorLabel: settings.gittensorLabel ?? "gittensor",
blacklistLabel: settings.blacklistLabel ?? "slop",
createMissingLabel: settings.createMissingLabel ?? true,
publicSurface: settings.publicSurface ?? "comment_and_label",
includeMaintainerAuthors: settings.includeMaintainerAuthors ?? false,
Expand Down Expand Up @@ -562,6 +565,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewModel: resolved.aiReviewModel,
autoLabelEnabled: resolved.autoLabelEnabled,
gittensorLabel: resolved.gittensorLabel,
blacklistLabel: resolved.blacklistLabel,
createMissingLabel: resolved.createMissingLabel,
publicSurface: resolved.publicSurface,
includeMaintainerAuthors: resolved.includeMaintainerAuthors,
Expand Down Expand Up @@ -606,6 +610,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewModel: resolved.aiReviewModel,
autoLabelEnabled: resolved.autoLabelEnabled,
gittensorLabel: resolved.gittensorLabel,
blacklistLabel: resolved.blacklistLabel,
createMissingLabel: resolved.createMissingLabel,
publicSurface: resolved.publicSurface,
includeMaintainerAuthors: resolved.includeMaintainerAuthors,
Expand Down
3 changes: 3 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export const repositorySettings = sqliteTable("repository_settings", {
aiReviewModel: text("ai_review_model"),
autoLabelEnabled: integer("auto_label_enabled", { mode: "boolean" }).notNull().default(true),
gittensorLabel: text("gittensor_label").notNull().default("gittensor"),
// Label applied to a blacklisted contributor's PR/issue (#1425); configurable so the disposition works
// regardless of the label a repo uses.
blacklistLabel: text("blacklist_label").notNull().default("slop"),
createMissingLabel: integer("create_missing_label", { mode: "boolean" }).notNull().default(true),
publicSurface: text("public_surface").notNull().default("comment_and_label"),
includeMaintainerAuthors: integer("include_maintainer_authors", { mode: "boolean" }).notNull().default(false),
Expand Down
2 changes: 2 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ export const RepositorySettingsSchema = z
slopAiAdvisory: z.boolean(),
autoLabelEnabled: z.boolean(),
gittensorLabel: z.string(),
blacklistLabel: z.string(),
createMissingLabel: z.boolean(),
publicSurface: z.enum(["off", "comment_and_label", "comment_only", "label_only"]),
includeMaintainerAuthors: z.boolean(),
Expand Down Expand Up @@ -659,6 +660,7 @@ export const RepoSettingsPreviewSchema = z
slopGateMinScore: z.number().nullable().optional(),
autoLabelEnabled: z.boolean(),
gittensorLabel: z.string(),
blacklistLabel: z.string(),
createMissingLabel: z.boolean(),
includeMaintainerAuthors: z.boolean(),
requireLinkedIssue: z.boolean(),
Expand Down
3 changes: 3 additions & 0 deletions src/signals/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type FocusManifestSettings = Partial<
| "agentPaused"
| "agentDryRun"
| "contributorBlacklist"
| "blacklistLabel"
>
>;

Expand Down Expand Up @@ -477,6 +478,8 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[])
if (aiReviewModel !== null) out.aiReviewModel = aiReviewModel;
const gittensorLabel = normalizeOptionalString(r.gittensorLabel, "settings.gittensorLabel", warnings);
if (gittensorLabel !== null) out.gittensorLabel = gittensorLabel;
const blacklistLabel = normalizeOptionalString(r.blacklistLabel, "settings.blacklistLabel", warnings);
if (blacklistLabel !== null) out.blacklistLabel = blacklistLabel;
const publicSurface = normalizeOptionalEnum(r.publicSurface, "settings.publicSurface", ["off", "comment_and_label", "comment_only", "label_only"] as const, warnings);
if (publicSurface !== null) out.publicSurface = publicSurface;
for (const key of ["aiReviewByok", "autoLabelEnabled", "createMissingLabel", "includeMaintainerAuthors", "requireLinkedIssue", "backfillEnabled", "privateTrustEnabled", "agentPaused", "agentDryRun"] as const) {
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,11 @@ export type RepositorySettings = {
* DB) and unioned with the shared/global list at the point of use. Always populated by the DB layer
* (default `[]`); optional so existing settings fixtures/callers need not be touched. */
contributorBlacklist?: ContributorBlacklistEntry[] | undefined;
/** The label applied to a blacklisted contributor's PR (#1425). Configurable per-repo (dashboard/DB +
* `.gittensory.yml` `settings.blacklistLabel`); defaults to `"slop"` so the disposition works regardless of
* the label a repo sets. Always populated by the DB layer (default `"slop"`); optional so existing settings
* fixtures/callers need not be touched (mirrors the sibling `contributorBlacklist`). */
blacklistLabel?: string | undefined;
/** Agent-layer autonomy dial (#773): per-action-class level. Always populated by the DB layer (default
* `{}` = deny-by-default = "observe" for every class); optional so existing settings fixtures/callers
* need not be touched. The single source the action layer (#778) reads via `resolveAutonomy`. */
Expand Down
6 changes: 4 additions & 2 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,11 +986,13 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
expect(ignored.autoMaintain).toEqual({ requireApprovals: 2, mergeMethod: "merge" });
});

it("parses + resolves contributorBlacklist from the settings: block, overlaying the DB list (#1425)", () => {
const manifest = parseFocusManifest({ settings: { contributorBlacklist: ["plagiarist1", { login: "farmer2", reason: "farming" }, { login: "-bad" }] } });
it("parses + resolves contributorBlacklist + blacklistLabel from the settings: block, overlaying the DB (#1425)", () => {
const manifest = parseFocusManifest({ settings: { contributorBlacklist: ["plagiarist1", { login: "farmer2", reason: "farming" }, { login: "-bad" }], blacklistLabel: "abuse" } });
expect(manifest.settings.contributorBlacklist).toEqual([{ login: "plagiarist1" }, { login: "farmer2", reason: "farming" }]); // invalid login dropped
expect(manifest.settings.blacklistLabel).toBe("abuse");
const eff = resolveEffectiveSettings({ contributorBlacklist: [{ login: "db-only" }] } as unknown as RepositorySettings, manifest);
expect(eff.contributorBlacklist?.map((e) => e.login)).toEqual(["plagiarist1", "farmer2"]); // yml overlays DB
expect(eff.blacklistLabel).toBe("abuse"); // configurable label, not hardcoded
// An empty/all-invalid block never blanks the DB-configured list (only set when a valid entry survives).
const noOverride = resolveEffectiveSettings({ contributorBlacklist: [{ login: "keep-me" }] } as unknown as RepositorySettings, parseFocusManifest({ settings: { contributorBlacklist: [{ login: "" }] } }));
expect(noOverride.contributorBlacklist?.map((e) => e.login)).toEqual(["keep-me"]);
Expand Down
3 changes: 2 additions & 1 deletion test/unit/routes-ai-byok.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("maintainer AI-review config route", () => {
it("sets mode/byok/provider/model and preserves unrelated settings", async () => {
const app = createApp();
const env = createTestEnv({ TOKEN_ENCRYPTION_SECRET: SECRET });
await upsertRepositorySettings(env, { repoFullName: REPO, gateCheckMode: "enabled", gittensorLabel: "custom-label" });
await upsertRepositorySettings(env, { repoFullName: REPO, gateCheckMode: "enabled", gittensorLabel: "custom-label", blacklistLabel: "abuse" });
const res = await app.request(
`/v1/repos/${REPO}/ai-review`,
{ method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ mode: "block", byok: true, provider: "anthropic", model: "claude-3-5-sonnet-latest" }) },
Expand All @@ -44,6 +44,7 @@ describe("maintainer AI-review config route", () => {
expect(settings.aiReviewMode).toBe("block");
expect(settings.gateCheckMode).toBe("enabled"); // preserved
expect(settings.gittensorLabel).toBe("custom-label"); // preserved
expect(settings.blacklistLabel).toBe("abuse"); // #1425 round-trips through the DB
});

it("accepts a config without provider/model (stored as null)", async () => {
Expand Down
Loading