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
11 changes: 9 additions & 2 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,18 @@ declare global {
* admin/bot contributor may have open ACROSS EVERY repo this install gates, combined. Purely an
* install-scoped aggregate over this same database (no cross-instance networking) -- catches an actor
* spreading low-volume spam/farming PRs across several gated repos in one self-hosted install, which no
* single repo's own contributorOpenPrCap/contributorOpenIssueCap can see. Unset/invalid (the default) = no
* cap, byte-identical to today. Checked IN ADDITION TO (not instead of) the existing per-repo caps, in the
* single repo's own contributorOpenPrCap/contributorOpenIssueCap can see. Unset/invalid falls back to a
* real default (20) rather than "no cap" (#4511) -- set to the literal string "off" for the old
* unconditional-no-cap behavior. Checked IN ADDITION TO (not instead of) the existing per-repo caps, in the
* same contributor_cap short-circuit (src/settings/agent-actions.ts). A positive integer string (e.g. "20");
* see src/settings/global-contributor-cap.ts for parsing. */
GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP?: string;
/** Same shape as {@link GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP}, but for a CONFIRMED official Gittensor miner
* specifically (#4511) -- a verified miner identity gets this cap instead of the human one, since a
* legitimate fleet spread across many repos in one install is expected to run more concurrent open items
* than a single human contributor. Unset/invalid falls back to a higher real default (50); "off" exempts
* confirmed miners from the install-wide cap entirely while humans stay capped. */
GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER?: string;
/** Install-wide default for the per-repo contributorCapCancelCi setting (#2462): "true"/"1"/"yes"/"on"
* (case-insensitive) enables cancelling in-flight CI runs on a contributor_cap close for every repo that
* hasn't explicitly configured its own value. Unset/blank/anything else = off (the existing behavior). A
Expand Down
52 changes: 39 additions & 13 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ import {
type PlannedAgentAction,
} from "../settings/agent-actions";
import { isAutoCloseExempt } from "../settings/auto-close-exempt";
import { resolveGlobalContributorOpenItemCap } from "../settings/global-contributor-cap";
import { resolveGlobalContributorOpenItemCap, resolveGlobalContributorOpenItemCapForMiner } from "../settings/global-contributor-cap";
import { detectMigrationCollisions, extractMigrationNumber, KNOWN_MIGRATION_DUPLICATES } from "../db/migration-collisions";
import { listMigrationFilenamesAtRef } from "../github/migration-tree";
import {
Expand Down Expand Up @@ -3054,12 +3054,27 @@ async function runAgentMaintenancePlanAndExecute(

// Install-wide contributor open-item cap (#2562, anti-abuse): IN ADDITION TO the per-repo cap above, not
// instead of it -- only evaluated when the per-repo cap didn't already match (short-circuit: no need for a
// second cross-repo DB read once this PR is already being closed). Off by default (resolveGlobalContributorOpenItemCap
// returns null when GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP is unset/invalid) ⇒ zero extra queries, zero behavior
// change for an install that hasn't opted in. Reuses the shared autoCloseExemptLogins list (#2463) so a
// maintainer-named login is exempt here exactly like the per-repo caps and review-nag cooldown.
if (contributorCapMatch === undefined && pr.authorLogin && !isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)) {
const globalCap = resolveGlobalContributorOpenItemCap(env);
// second cross-repo DB read once this PR is already being closed). Defaults to a real cap even when unset
// (#4511) -- a CONFIRMED official Gittensor miner gets its own, higher fleet-appropriate default instead of
// either "no cap" or the plain human default, since a legitimate fleet spread across many repos in one
// install is expected to run more concurrent open items than a single human contributor. Reuses the shared
// autoCloseExemptLogins list (#2463) so a maintainer-named login is exempt here exactly like the per-repo
// caps and review-nag cooldown.
const prGlobalCapForHuman = resolveGlobalContributorOpenItemCap(env);
const prGlobalCapForMiner = resolveGlobalContributorOpenItemCapForMiner(env);
if (
contributorCapMatch === undefined &&
pr.authorLogin &&
(prGlobalCapForHuman !== null || prGlobalCapForMiner !== null) &&
!isAutoCloseExempt(pr.authorLogin, settings.autoCloseExemptLogins)
) {
// Deferred until we know at least one of the two resolvers is actually active (#4511): the identity
// lookup below is cached but still a DB/network round trip, and both resolvers above are plain env reads.
const officialMiner = await getCachedOfficialMinerDetection(env, pr.authorLogin, {
targetKey: `${repoFullName}#${pr.number}`,
deliveryId,
});
const globalCap = officialMiner.status === "confirmed" ? prGlobalCapForMiner : prGlobalCapForHuman;
if (globalCap !== null) {
const globalOpenCount = await verifiedGlobalOpenItemCount(env, installationId, pr.authorLogin, {
repoFullName,
Expand Down Expand Up @@ -5406,15 +5421,19 @@ async function verifiedGlobalOpenItemCount(
*/
async function maybeCloseIssueOverContributorCap(
env: Env,
args: { installationId: number; repoFullName: string; issue: IssueRecord; settings: RepositorySettings },
args: { installationId: number; repoFullName: string; issue: IssueRecord; settings: RepositorySettings; deliveryId: string },
): Promise<void> {
const { installationId, repoFullName, issue, settings } = args;
const { installationId, repoFullName, issue, settings, deliveryId } = args;
const cap = settings.contributorOpenIssueCap;
const authorLogin = issue.authorLogin;
// Install-wide cap (#2562) is checked IN ADDITION TO the per-repo cap, so this function must still run when
// ONLY the global cap is configured (the per-repo cap stays optional/off, its usual default).
const globalCap = resolveGlobalContributorOpenItemCap(env);
if ((typeof cap !== "number" && globalCap === null) || !authorLogin) return;
// ONLY a global cap is configured (the per-repo cap stays optional/off, its usual default). Both global
// resolvers now default to a real number even when unset (#4511) -- a CONFIRMED official Gittensor miner
// gets its own fleet-appropriate cap instead of the human one, checked separately below once we know at
// least one of the two is active (both resolvers here are plain env reads; the identity check isn't).
const globalCapForHuman = resolveGlobalContributorOpenItemCap(env);
const globalCapForMiner = resolveGlobalContributorOpenItemCapForMiner(env);
if ((typeof cap !== "number" && globalCapForHuman === null && globalCapForMiner === null) || !authorLogin) return;

const repoOwner = repoOwnerLoginFromFullName(repoFullName);
const authorIsOwner = authorLogin.toLowerCase() === repoOwner.toLowerCase();
Expand All @@ -5429,7 +5448,13 @@ async function maybeCloseIssueOverContributorCap(
// Install-wide check first (#2562): reuses the shared autoCloseExemptLogins list, same as the PR path.
// verifiedGlobalOpenItemCount live-verifies every OTHER counted item before trusting it toward an
// irreversible close (#2562 gate-review follow-up), mirroring the per-repo cap's own sibling live-verify.
if (globalCap !== null && !isAutoCloseExempt(authorLogin, settings.autoCloseExemptLogins)) {
if ((globalCapForHuman !== null || globalCapForMiner !== null) && !isAutoCloseExempt(authorLogin, settings.autoCloseExemptLogins)) {
const officialMiner = await getCachedOfficialMinerDetection(env, authorLogin, {
targetKey: `${repoFullName}#${issue.number}`,
deliveryId,
});
const globalCap = officialMiner.status === "confirmed" ? globalCapForMiner : globalCapForHuman;
if (globalCap === null) return;
const globalOpenCount = await verifiedGlobalOpenItemCount(env, installationId, authorLogin, {
repoFullName,
number: issue.number,
Expand Down Expand Up @@ -6396,6 +6421,7 @@ async function processGitHubWebhook(
repoFullName: payload.repository.full_name,
issue,
settings: issueSettings,
deliveryId,
}).catch((error) => {
/* v8 ignore next -- best-effort: an issue-cap enforcement failure is logged, never surfaced to the webhook. */
console.error(
Expand Down
53 changes: 42 additions & 11 deletions src/settings/global-contributor-cap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,50 @@
// already tracks. Deliberately an env var (not a per-repo `.gittensory.yml`/DB field like the caps above) --
// this setting aggregates ACROSS repos, so it cannot be "this repo's" setting; it belongs to the install as a
// whole, mirroring how global_contributor_blacklist is a tenant-free singleton rather than a per-repo column.
// Off by default (unset/invalid ⇒ null ⇒ no cap): zero behavior change for a single-repo install or one that
// hasn't opted in.
//
// #4511 (AMS-readiness follow-up): "unset ⇒ null ⇒ no cap" was the ONLY defense against one identity farming
// PRs across every gated repo in an install, and it was off unless an operator proactively opted in AND
// remembered to pre-size it. That's backwards for a fleet-scale actor -- fail-safe means a sane cap exists by
// default, not that protection is silently absent until someone configures it. So: unset/malformed now falls
// back to a real default (DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP) rather than "no cap" -- this IS a behavior
// change for any install that never set the env var. An operator who genuinely wants no cap sets the env var
// to the literal string "off" (a load-bearing explicit opt-out, distinct from "unset"), mirroring the
// explicit-null-means-something idiom used elsewhere in this codebase (e.g. blacklistLabel).
const GLOBAL_ENV_KEY = "GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP";
const GLOBAL_MINER_ENV_KEY = "GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER";
const OFF_SENTINEL = "off";

/** Parse+validate the install-wide open-item cap from env. Same non-rounding shape as the per-repo caps'
* normalizeOpenItemCap (db/repositories.ts): a discrete count of open items, not a score, so a
* fractional/non-positive/non-numeric value is a malformed cap and is dropped to `null` (no cap) rather than
* coerced into a nonsensical threshold. Never throws. Unlike the per-repo cap, this install-wide cap is not clamped to the
* per-repo live-check budget because the install-wide verifier loads and verifies a larger row set. */
export function resolveGlobalContributorOpenItemCap(env: { GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP?: string | undefined }): number | null {
const raw = env[GLOBAL_ENV_KEY];
if (typeof raw !== "string" || raw.trim() === "") return null;
/** Default install-wide cap for a non-miner actor when {@link GLOBAL_ENV_KEY} is unset or malformed (#4511). */
export const DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP = 20;
/** Default install-wide cap for a CONFIRMED official Gittensor miner (#4511): higher than the human default
* because a legitimate fleet spread across many repos in one install is expected to run more concurrent open
* items than a single human contributor, without being unlimited. Applies ONLY once the author is verified
* via the same official-miner-detection path the rest of the codebase already trusts for this purpose
* (getCachedOfficialMinerDetection) -- an unverified/unconfirmed actor always gets the human default. */
export const DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER = 50;

function resolveCapEnv(raw: string | undefined, fallback: number): number | null {
if (typeof raw !== "string" || raw.trim() === "") return fallback;
if (raw.trim().toLowerCase() === OFF_SENTINEL) return null;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return null;
// A malformed value (fractional/non-positive/non-numeric) falls back to the SAME default an unset env var
// would use, not to "no cap" -- a typo in an operator's .env must never silently disable this defense.
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return fallback;
return parsed;
}

/** Resolve the install-wide open-item cap for an ordinary (non-miner) actor. `null` means explicitly disabled
* (env var set to `"off"`) -- everything else, including unset, resolves to a real number. Never throws.
* Unlike the per-repo cap, this install-wide cap is not clamped to the per-repo live-check budget because the
* install-wide verifier loads and verifies a larger row set. */
export function resolveGlobalContributorOpenItemCap(env: { GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP?: string | undefined }): number | null {
return resolveCapEnv(env[GLOBAL_ENV_KEY], DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
}

/** Resolve the install-wide open-item cap for a CONFIRMED official Gittensor miner (#4511) -- same shape and
* `"off"` escape hatch as {@link resolveGlobalContributorOpenItemCap}, but with a fleet-appropriate default.
* Callers must only use this once the actor's miner status is independently verified; this function does not
* itself check identity. */
export function resolveGlobalContributorOpenItemCapForMiner(env: { GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER?: string | undefined }): number | null {
return resolveCapEnv(env[GLOBAL_MINER_ENV_KEY], DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER);
}
54 changes: 42 additions & 12 deletions test/unit/global-contributor-cap.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { describe, expect, it } from "vitest";
import { resolveGlobalContributorOpenItemCap } from "../../src/settings/global-contributor-cap";
import {
DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP,
DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER,
resolveGlobalContributorOpenItemCap,
resolveGlobalContributorOpenItemCapForMiner,
} from "../../src/settings/global-contributor-cap";
import { listOpenItemsForAuthorAcrossInstall, upsertIssueFromGitHub, upsertPullRequestFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { createTestEnv } from "../helpers/d1";

describe("resolveGlobalContributorOpenItemCap (#2562)", () => {
it("is off by default when the env var is unset", () => {
expect(resolveGlobalContributorOpenItemCap({})).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: undefined })).toBeNull();
describe("resolveGlobalContributorOpenItemCap (#2562, #4511)", () => {
it("falls back to the real default when the env var is unset (no longer 'no cap')", () => {
expect(resolveGlobalContributorOpenItemCap({})).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: undefined })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
});

it("parses a valid positive-integer string", () => {
Expand All @@ -20,13 +25,38 @@ describe("resolveGlobalContributorOpenItemCap (#2562)", () => {
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "100" })).toBe(100);
});

it("drops a fractional/non-positive/non-numeric value to null (no cap), never coerced", () => {
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2.5" })).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "0" })).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "-3" })).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "not-a-number" })).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "" })).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: " " })).toBeNull();
it("falls back to the default (not null) on a fractional/non-positive/non-numeric value -- a typo must never silently disable this defense", () => {
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "2.5" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "0" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "-3" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "not-a-number" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: " " })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
});

it("the literal string 'off' (any case) is the explicit escape hatch back to no cap", () => {
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "off" })).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: "OFF" })).toBeNull();
expect(resolveGlobalContributorOpenItemCap({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP: " Off " })).toBeNull();
});
});

describe("resolveGlobalContributorOpenItemCapForMiner (#4511)", () => {
it("falls back to the higher miner default when unset", () => {
expect(resolveGlobalContributorOpenItemCapForMiner({})).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER);
expect(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER).toBeGreaterThan(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP);
});

it("parses a valid override independently of the human cap var", () => {
expect(resolveGlobalContributorOpenItemCapForMiner({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER: "75" })).toBe(75);
});

it("falls back to the miner default (not null) on a malformed value", () => {
expect(resolveGlobalContributorOpenItemCapForMiner({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER: "nope" })).toBe(DEFAULT_GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER);
});

it("'off' exempts confirmed miners from the install-wide cap entirely", () => {
expect(resolveGlobalContributorOpenItemCapForMiner({ GLOBAL_CONTRIBUTOR_OPEN_ITEM_CAP_MINER: "off" })).toBeNull();
});
});

Expand Down
Loading