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
55 changes: 55 additions & 0 deletions packages/loopover-engine/src/settings/global-contributor-cap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Install-wide contributor open-item cap (#2562, anti-abuse): a self-hosted install that gates multiple repos
// shares ONE database, but the per-repo contributorOpenPrCap/contributorOpenIssueCap (repository-settings.ts)
// only ever counts open items on the SAME repo -- an actor spreading low-volume spam/farming PRs across several
// gated repos in that install never trips any single repo's cap. This is cross-REPO-within-one-install only (no
// federation, no cross-instance privacy design): a same-database aggregate against every repo this install
// already tracks. Deliberately an env var (not a per-repo `.loopover.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.
//
// #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";

/** 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);
// 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);
}
62 changes: 5 additions & 57 deletions src/settings/auto-close-exempt.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,5 @@
// Shared repo-scoped exemption list (#2463) for gittensory's deterministic anti-abuse auto-close/throttle
// mechanisms — currently the review-nag cooldown; intended to be reused by the per-contributor open-item cap
// (#2270) once that lands, rather than each feature growing its own duplicate whitelist. A maintainer-named
// GitHub login here is NEVER throttled or closed by either mechanism, on top of the standing owner/admin/
// automation-bot exemption every such mechanism already honors. Config-driven and layered the same as other
// settings (`.loopover.yml` > DB), never hard-coded for any repo. Mirrors contributor-blacklist.ts's shape
// (normalize → validated list + warnings), minus the reason/evidence metadata a ban carries that an exemption
// doesn't need.
// A trailing `[bot]` is a real, common GitHub App-actor login shape (e.g. `dependabot[bot]`, `sentry[bot]`) --
// this is exactly the kind of third-party automation identity a maintainer needs to exempt (a repo-specific bot
// integration the hardcoded, install-wide well-known-bot set in agent-actions.ts has no way to know about), so
// the base GitHub-login pattern (1-39 chars, alphanumeric/single-hyphens) is extended with an optional literal
// `[bot]` suffix rather than rejecting every bot-shaped login outright.
const GITHUB_LOGIN = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}(?:\[bot\])?$/;
const MAX_ENTRIES = 500;

/** Normalize a raw exempt-logins value (DB JSON or `.loopover.yml`) into a validated, de-duplicated list of
* GitHub logins. Never throws: malformed entries are dropped with a warning. De-dup is case-insensitive (the
* FIRST occurrence's casing is kept). */
export function normalizeAutoCloseExemptLogins(input: unknown): { logins: string[]; warnings: string[] } {
const warnings: string[] = [];
if (input === undefined || input === null) return { logins: [], warnings };
if (!Array.isArray(input)) {
warnings.push("autoCloseExemptLogins must be a list of GitHub logins; ignoring it.");
return { logins: [], warnings };
}
const logins: string[] = [];
const seen = new Set<string>();
for (const [index, raw] of input.entries()) {
if (logins.length >= MAX_ENTRIES) {
warnings.push(`autoCloseExemptLogins is capped at ${MAX_ENTRIES} entries; dropping the rest.`);
break;
}
if (typeof raw !== "string") {
warnings.push(`autoCloseExemptLogins[${index}] must be a string login; ignoring it.`);
continue;
}
const login = raw.trim();
if (!GITHUB_LOGIN.test(login)) {
warnings.push(`autoCloseExemptLogins[${index}] is not a valid GitHub login; ignoring it.`);
continue;
}
const key = login.toLowerCase();
if (seen.has(key)) continue; // first occurrence wins
seen.add(key);
logins.push(login);
}
return { logins, warnings };
}

/** Case-insensitive membership check against the resolved exempt-logins list. Absent/empty list ⇒ never exempt
* (the safe default — an unconfigured repo exempts no one beyond the standing owner/admin/bot rule). */
export function isAutoCloseExempt(login: string | null | undefined, exemptLogins: readonly string[] | undefined): boolean {
if (!login) return false;
const lower = login.toLowerCase();
return (exemptLogins ?? []).some((entry) => entry.toLowerCase() === lower);
}
// auto-close-exempt, converged onto @loopover/engine (#4879). This src/ file was a hand-maintained twin of the
// engine copy; it is now a thin re-export shim so the single implementation lives at
// packages/loopover-engine/src/settings/auto-close-exempt.ts (imported via relative source path, not the published
// package, to match this repo's existing engine-consumption convention — see src/signals/check-summary.ts).
export * from "../../packages/loopover-engine/src/settings/auto-close-exempt";
59 changes: 4 additions & 55 deletions src/settings/global-contributor-cap.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,4 @@
// Install-wide contributor open-item cap (#2562, anti-abuse): a self-hosted install that gates multiple repos
// shares ONE database, but the per-repo contributorOpenPrCap/contributorOpenIssueCap (repository-settings.ts)
// only ever counts open items on the SAME repo -- an actor spreading low-volume spam/farming PRs across several
// gated repos in that install never trips any single repo's cap. This is cross-REPO-within-one-install only (no
// federation, no cross-instance privacy design): a same-database aggregate against every repo this install
// already tracks. Deliberately an env var (not a per-repo `.loopover.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.
//
// #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";

/** 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);
// 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);
}
// global-contributor-cap, extracted to @loopover/engine (#4879). Thin re-export shim; the implementation lives at
// packages/loopover-engine/src/settings/global-contributor-cap.ts (imported via relative source path, not the
// published package, to match this repo's existing engine-consumption convention — see src/signals/check-summary.ts).
export * from "../../packages/loopover-engine/src/settings/global-contributor-cap";
Loading