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
15 changes: 10 additions & 5 deletions .loopover.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -668,13 +668,18 @@ settings:
# of this setting. Bool. Default: false.
closeOwnerAuthors: false

# Hard manual-review path guardrails are config-as-code only. Configured globs are ADDED to a fixed
# set of built-in invariant guardrails (config-as-code files, CI workflows/scripts, and core
# engine-decision paths — see DEFAULT_HARD_GUARDRAIL_GLOBS in src/review/guardrail-config.ts) that
# always apply and can never be disabled from here. Omit or use [] to rely on just those built-in
# invariants, with no repo-specific additions.
# Hard manual-review path guardrails are config-as-code only. Safe by default (#3943): whatever you list
# here is ADDED to a fixed, built-in invariant set (CI workflows/scripts, deploy config, config-as-code
# files, core engine-decision paths — see DEFAULT_HARD_GUARDRAIL_GLOBS in src/review/guardrail-config.ts)
# that an ordinary edit here can only ever widen, never shrink. Bool. Default: [].
hardGuardrailGlobs: []

# Opt-in escape hatch: set true to make hardGuardrailGlobs above REPLACE (not add to) the built-in
# invariant set, including using [] to disable path guardrails entirely. A deliberate, separately-visible
# decision — only set this if you specifically want full control over your own guardrail list instead of
# the safe-by-default add-only behavior. Bool. Default: false.
hardGuardrailGlobsOverridesInvariants: false

# Require a linked issue on every PR (dashboard equivalent of the toggle that
# can auto-promote gate.linkedIssue to block). Bool. Default: false.
requireLinkedIssue: false
Expand Down
4 changes: 4 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9635,6 +9635,10 @@
"updatedAt": {
"type": "string",
"nullable": true
},
"hardGuardrailGlobsOverridesInvariants": {
"type": "boolean",
"nullable": true
}
},
"required": [
Expand Down
9 changes: 7 additions & 2 deletions config/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,13 @@ Two `autonomy` classes govern every label the bot can apply, and they are **inde

All disposition labels are configurable under `settings.*Label`, and explicit `null` disables the
label without disabling the underlying merge/close/hold decision. Hard path guardrails are
config-as-code only: omitting `settings.hardGuardrailGlobs` or setting it to `[]` means no path
guardrails, and a concrete list replaces any lower-layer private global default.
config-as-code only and safe by default (#3943): `settings.hardGuardrailGlobs` is ADDED to a fixed,
built-in invariant set (`DEFAULT_HARD_GUARDRAIL_GLOBS` in `src/review/guardrail-config.ts`) — an
ordinary edit can only ever widen guardrail coverage, never shrink it. Set
`settings.hardGuardrailGlobsOverridesInvariants: true` to opt out of that safety net entirely: once
set, `hardGuardrailGlobs` REPLACES (never merges with) the built-in set or any lower-layer private
global default, including an explicit `[]` to disable path guardrails altogether — a deliberate,
separately-visible decision, not a side effect of trimming the list.

```yaml
# .loopover.yml (global default) — recommended one-shot baseline
Expand Down
15 changes: 10 additions & 5 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -682,13 +682,18 @@ settings:
# of this setting. Bool. Default: false.
closeOwnerAuthors: false

# Hard manual-review path guardrails are config-as-code only. Configured globs are ADDED to a fixed
# set of built-in invariant guardrails (config-as-code files, CI workflows/scripts, and core
# engine-decision paths — see DEFAULT_HARD_GUARDRAIL_GLOBS in src/review/guardrail-config.ts) that
# always apply and can never be disabled from here. Omit or use [] to rely on just those built-in
# invariants, with no repo-specific additions.
# Hard manual-review path guardrails are config-as-code only. Safe by default (#3943): whatever you list
# here is ADDED to a fixed, built-in invariant set (CI workflows/scripts, deploy config, config-as-code
# files, core engine-decision paths — see DEFAULT_HARD_GUARDRAIL_GLOBS in src/review/guardrail-config.ts)
# that an ordinary edit here can only ever widen, never shrink. Bool. Default: [].
hardGuardrailGlobs: []

# Opt-in escape hatch: set true to make hardGuardrailGlobs above REPLACE (not add to) the built-in
# invariant set, including using [] to disable path guardrails entirely. A deliberate, separately-visible
# decision — only set this if you specifically want full control over your own guardrail list instead of
# the safe-by-default add-only behavior. Bool. Default: false.
hardGuardrailGlobsOverridesInvariants: false

# Require a linked issue on every PR (dashboard equivalent of the toggle that
# can auto-promote gate.linkedIssue to block). Bool. Default: false.
requireLinkedIssue: false
Expand Down
8 changes: 6 additions & 2 deletions config/examples/global.gittensory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ settings:
autoCloseExemptLogins:
- your-admin-login

# Hard manual-review path guardrails are config-as-code only. Omit or use [] for no path
# guardrails; set a concrete list when you want otherwise-mergeable PRs held for a person.
# Hard manual-review path guardrails are config-as-code only and safe by default (#3943): this list is
# ADDED to a fixed, built-in invariant set (DEFAULT_HARD_GUARDRAIL_GLOBS in
# src/review/guardrail-config.ts), never allowed to shrink it. Set
# hardGuardrailGlobsOverridesInvariants: true below to let this list fully REPLACE the built-in set
# instead (including [] to disable path guardrails entirely) — a deliberate opt-in, not the default.
hardGuardrailGlobs: []
hardGuardrailGlobsOverridesInvariants: false

# Disposition label names are config-as-code, not engine identity. Set any value to your repo's
# preferred label name, or explicit null to disable that label while preserving the action/hold.
Expand Down
3 changes: 2 additions & 1 deletion packages/gittensory-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ export type FocusManifestSettings = Partial<
| "reviewNagMonitoredMentions"
| "autoCloseExemptLogins"
| "hardGuardrailGlobs"
| "hardGuardrailGlobsOverridesInvariants"
| "manualReviewLabel"
| "readyToMergeLabel"
| "changesRequestedLabel"
Expand Down Expand Up @@ -1856,7 +1857,7 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[],
}
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", "aiReviewAllAuthors", "closeOwnerAuthors", "autoLabelEnabled", "typeLabelsEnabled", "badgeEnabled", "publicQualityMetrics", "createMissingLabel", "includeMaintainerAuthors", "requireLinkedIssue", "backfillEnabled", "agentPaused", "agentDryRun"] as const) {
for (const key of ["aiReviewByok", "aiReviewAllAuthors", "closeOwnerAuthors", "autoLabelEnabled", "typeLabelsEnabled", "badgeEnabled", "publicQualityMetrics", "createMissingLabel", "includeMaintainerAuthors", "requireLinkedIssue", "backfillEnabled", "agentPaused", "agentDryRun", "hardGuardrailGlobsOverridesInvariants"] as const) {
const flag = normalizeOptionalBoolean(r[key], `settings.${key}`, warnings);
if (flag !== null) out[key] = flag;
}
Expand Down
25 changes: 19 additions & 6 deletions packages/gittensory-engine/src/review/guardrail-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,34 @@ export const ENGINE_DECISION_GUARDRAIL_GLOBS = [
"src/review/outcomes-wire.ts",
];

// Default, safe-by-default invariant set (restored by #3943 after the original pure-config-as-code design
// let a `.gittensory.yml` edit silently remove its own guardrail protection). Repo settings can only ADD to
// this set UNLESS the repo explicitly opts in via `hardGuardrailGlobsOverridesInvariants` (below).
export const DEFAULT_HARD_GUARDRAIL_GLOBS = [
...CONFIG_AS_CODE_GUARDRAIL_GLOBS,
...WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS,
...ENGINE_DECISION_GUARDRAIL_GLOBS,
];

/**
* Resolve hard-guardrail path globs from the already-effective repo settings. Built-in config-as-code,
* workflow/runtime, and engine decision guardrails are invariants; repo settings may only add globs.
* Resolve hard-guardrail path globs from the already-effective repo settings.
*
* Safe by default (#3943): `DEFAULT_HARD_GUARDRAIL_GLOBS` is an invariant floor, and a repo's configured
* `hardGuardrailGlobs` is ADDED to it (deduplicated), never allowed to shrink it — so an ordinary
* `.gittensory.yml` edit (even a careless or malicious one) can only ever widen guardrail protection.
*
* Full self-hoster control, opt-in (config-as-code mandate): a repo that explicitly sets
* `hardGuardrailGlobsOverridesInvariants: true` takes complete ownership of its guardrail list —
* `hardGuardrailGlobs` is then used EXACTLY as given (including an explicit `[]` to disable path guardrails
* entirely), REPLACING rather than adding to the built-in floor. This is deliberately a second, explicit
* field rather than reusing `hardGuardrailGlobs: []`'s presence/absence, so opting out of the safety net is
* always a conscious, separately-visible decision in the config file, not a side effect of trimming a list.
*/
export function resolveHardGuardrailGlobs(
settings: Pick<RepositorySettings, "hardGuardrailGlobs"> | null | undefined,
settings: Pick<RepositorySettings, "hardGuardrailGlobs" | "hardGuardrailGlobsOverridesInvariants"> | null | undefined,
): string[] {
const configured = settings?.hardGuardrailGlobs;
return Array.from(
new Set([...DEFAULT_HARD_GUARDRAIL_GLOBS, ...(Array.isArray(configured) ? configured : [])]),
);
const configuredList = Array.isArray(configured) ? configured : [];
if (settings?.hardGuardrailGlobsOverridesInvariants === true) return [...configuredList];
return Array.from(new Set([...DEFAULT_HARD_GUARDRAIL_GLOBS, ...configuredList]));
}
13 changes: 11 additions & 2 deletions packages/gittensory-engine/src/types/manifest-deps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,9 +442,18 @@ export type RepositorySettings = {
* `[]`); optional so existing settings fixtures/callers need not be touched. */
autoCloseExemptLogins?: string[] | undefined;
/** Hard manual-review guardrail globs. Config-as-code only: set in private/global or per-repo
* `.gittensory.yml` under `settings.hardGuardrailGlobs`. Absent means no path guardrails. Arrays are
* replacement overlays, so a repo can clear a global default with `[]`. */
* `.gittensory.yml` under `settings.hardGuardrailGlobs`. Safe by default (#3943): ADDED to the built-in
* invariant floor (`DEFAULT_HARD_GUARDRAIL_GLOBS` in src/review/guardrail-config.ts), never allowed to
* shrink it, unless {@link hardGuardrailGlobsOverridesInvariants} is explicitly `true` — see that field. */
hardGuardrailGlobs?: string[] | null | undefined;
/** Opt-in escape hatch (config-as-code mandate) from {@link hardGuardrailGlobs}'s safe-by-default
* add-only behavior: when explicitly `true`, `hardGuardrailGlobs` is used EXACTLY as configured —
* REPLACING the built-in invariant floor rather than adding to it, including an explicit `[]` to
* disable path guardrails entirely. Deliberately a separate, explicitly-named field (rather than
* overloading `hardGuardrailGlobs: []`'s meaning) so a repo choosing to drop the built-in safety net is
* always a conscious, separately-visible config decision. Default `false`/absent preserves #3943's
* protection: an ordinary `.gittensory.yml` edit can only ever widen guardrail coverage. */
hardGuardrailGlobsOverridesInvariants?: boolean | null | undefined;
/** Label applied when an otherwise-ready PR is held for manual review by a guardrail. Config-as-code only;
* `null` disables the label while keeping the hold. Distinct from `review_state_label`, so operators can
* apply one manual-review label without enabling ready/changes-requested disposition labels. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export type GatePolicyPack = "gittensor" | "oss-anti-slop";
export type RepositorySettings = {
repoFullName: string;
hardGuardrailGlobs?: string[] | null | undefined;
hardGuardrailGlobsOverridesInvariants?: boolean | null | undefined;
};

export type RecentMergedPullRequestRecord = {
Expand Down Expand Up @@ -331,6 +332,7 @@ export type FocusManifestReviewConfig = {

export type FocusManifestSettings = {
hardGuardrailGlobs?: string[] | null | undefined;
hardGuardrailGlobsOverridesInvariants?: boolean | null | undefined;
};

export type FocusManifest = {
Expand Down
1 change: 1 addition & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ export const RepositorySettingsSchema = z
reviewNagMonitoredMentions: z.array(z.string()).optional(),
autoCloseExemptLogins: z.array(z.string()).optional(),
hardGuardrailGlobs: z.array(z.string()).nullable().optional(),
hardGuardrailGlobsOverridesInvariants: z.boolean().nullable().optional(),
manualReviewLabel: z.string().nullable().optional(),
readyToMergeLabel: z.string().nullable().optional(),
changesRequestedLabel: z.string().nullable().optional(),
Expand Down
25 changes: 19 additions & 6 deletions src/review/guardrail-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,34 @@ export const ENGINE_DECISION_GUARDRAIL_GLOBS = [
"src/review/outcomes-wire.ts",
];

// Default, safe-by-default invariant set (restored by #3943 after the original pure-config-as-code design
// let a `.gittensory.yml` edit silently remove its own guardrail protection). Repo settings can only ADD to
// this set UNLESS the repo explicitly opts in via `hardGuardrailGlobsOverridesInvariants` (below).
export const DEFAULT_HARD_GUARDRAIL_GLOBS = [
...CONFIG_AS_CODE_GUARDRAIL_GLOBS,
...WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS,
...ENGINE_DECISION_GUARDRAIL_GLOBS,
];

/**
* Resolve hard-guardrail path globs from the already-effective repo settings. Built-in config-as-code,
* workflow/runtime, and engine decision guardrails are invariants; repo settings may only add globs.
* Resolve hard-guardrail path globs from the already-effective repo settings.
*
* Safe by default (#3943): `DEFAULT_HARD_GUARDRAIL_GLOBS` is an invariant floor, and a repo's configured
* `hardGuardrailGlobs` is ADDED to it (deduplicated), never allowed to shrink it — so an ordinary
* `.gittensory.yml` edit (even a careless or malicious one) can only ever widen guardrail protection.
*
* Full self-hoster control, opt-in (config-as-code mandate): a repo that explicitly sets
* `hardGuardrailGlobsOverridesInvariants: true` takes complete ownership of its guardrail list —
* `hardGuardrailGlobs` is then used EXACTLY as given (including an explicit `[]` to disable path guardrails
* entirely), REPLACING rather than adding to the built-in floor. This is deliberately a second, explicit
* field rather than reusing `hardGuardrailGlobs: []`'s presence/absence, so opting out of the safety net is
* always a conscious, separately-visible decision in the config file, not a side effect of trimming a list.
*/
export function resolveHardGuardrailGlobs(
settings: Pick<RepositorySettings, "hardGuardrailGlobs"> | null | undefined,
settings: Pick<RepositorySettings, "hardGuardrailGlobs" | "hardGuardrailGlobsOverridesInvariants"> | null | undefined,
): string[] {
const configured = settings?.hardGuardrailGlobs;
return Array.from(
new Set([...DEFAULT_HARD_GUARDRAIL_GLOBS, ...(Array.isArray(configured) ? configured : [])]),
);
const configuredList = Array.isArray(configured) ? configured : [];
if (settings?.hardGuardrailGlobsOverridesInvariants === true) return [...configuredList];
return Array.from(new Set([...DEFAULT_HARD_GUARDRAIL_GLOBS, ...configuredList]));
}
13 changes: 11 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,9 +1043,18 @@ export type RepositorySettings = {
* `[]`); optional so existing settings fixtures/callers need not be touched. */
autoCloseExemptLogins?: string[] | undefined;
/** Hard manual-review guardrail globs. Config-as-code only: set in private/global or per-repo
* `.gittensory.yml` under `settings.hardGuardrailGlobs`. Absent means no path guardrails. Arrays are
* replacement overlays, so a repo can clear a global default with `[]`. */
* `.gittensory.yml` under `settings.hardGuardrailGlobs`. Safe by default (#3943): ADDED to the built-in
* invariant floor (`DEFAULT_HARD_GUARDRAIL_GLOBS` in src/review/guardrail-config.ts), never allowed to
* shrink it, unless {@link hardGuardrailGlobsOverridesInvariants} is explicitly `true` — see that field. */
hardGuardrailGlobs?: string[] | null | undefined;
/** Opt-in escape hatch (config-as-code mandate) from {@link hardGuardrailGlobs}'s safe-by-default
* add-only behavior: when explicitly `true`, `hardGuardrailGlobs` is used EXACTLY as configured —
* REPLACING the built-in invariant floor rather than adding to it, including an explicit `[]` to
* disable path guardrails entirely. Deliberately a separate, explicitly-named field (rather than
* overloading `hardGuardrailGlobs: []`'s meaning) so a repo choosing to drop the built-in safety net is
* always a conscious, separately-visible config decision. Default `false`/absent preserves #3943's
* protection: an ordinary `.gittensory.yml` edit can only ever widen guardrail coverage. */
hardGuardrailGlobsOverridesInvariants?: boolean | null | undefined;
/** Label applied when an otherwise-ready PR is held for manual review by a guardrail. Config-as-code only;
* `null` disables the label while keeping the hold. Distinct from `review_state_label`, so operators can
* apply one manual-review label without enabling ready/changes-requested disposition labels. */
Expand Down
15 changes: 15 additions & 0 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ describe(".loopover.yml.example field-exhaustiveness (#1670)", () => {
reviewNagMonitoredMentions: "reviewNagMonitoredMentions:",
autoCloseExemptLogins: "autoCloseExemptLogins:",
hardGuardrailGlobs: "hardGuardrailGlobs:",
hardGuardrailGlobsOverridesInvariants: "hardGuardrailGlobsOverridesInvariants:",
manualReviewLabel: "manualReviewLabel:",
readyToMergeLabel: "readyToMergeLabel:",
changesRequestedLabel: "changesRequestedLabel:",
Expand Down Expand Up @@ -2320,6 +2321,20 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
expect(invalidArray.warnings.some((w) => /did not contain any valid path globs/.test(w))).toBe(true);
});

it("parses + resolves hardGuardrailGlobsOverridesInvariants as a plain boolean flag", () => {
const enabled = parseFocusManifest({ settings: { hardGuardrailGlobsOverridesInvariants: true } });
expect(enabled.settings.hardGuardrailGlobsOverridesInvariants).toBe(true);
const eff = resolveEffectiveSettings({ hardGuardrailGlobsOverridesInvariants: false } as unknown as RepositorySettings, enabled);
expect(eff.hardGuardrailGlobsOverridesInvariants).toBe(true);

const omitted = resolveEffectiveSettings({ hardGuardrailGlobsOverridesInvariants: true } as unknown as RepositorySettings, parseFocusManifest({}));
expect(omitted.hardGuardrailGlobsOverridesInvariants).toBe(true);

const malformed = parseFocusManifest({ settings: { hardGuardrailGlobsOverridesInvariants: "yes" as never } });
expect(malformed.settings.hardGuardrailGlobsOverridesInvariants).toBeUndefined();
expect(malformed.warnings.some((w) => /settings\.hardGuardrailGlobsOverridesInvariants/.test(w))).toBe(true);
});

it("#label-scoping: parses + resolves reviewNagMonitoredMentions from the settings: block, overlaying the DB", () => {
const manifest = parseFocusManifest({ settings: { reviewNagMonitoredMentions: ["JSONbored", "Some-Maintainer"] } });
expect(manifest.settings.reviewNagMonitoredMentions).toEqual(["JSONbored", "Some-Maintainer"]);
Expand Down
Loading
Loading