@loopover/engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/LICENSE +661 -0
- package/README.md +676 -0
- package/dist/advisory/gate-advisory.d.ts +151 -0
- package/dist/advisory/gate-advisory.js +512 -0
- package/dist/ai-policy-map.d.ts +97 -0
- package/dist/ai-policy-map.js +377 -0
- package/dist/ams-policy-spec.d.ts +56 -0
- package/dist/ams-policy-spec.js +165 -0
- package/dist/calibration-dashboard.d.ts +23 -0
- package/dist/calibration-dashboard.js +65 -0
- package/dist/calibration-trend.d.ts +40 -0
- package/dist/calibration-trend.js +60 -0
- package/dist/contributor-fit.d.ts +24 -0
- package/dist/contributor-fit.js +50 -0
- package/dist/discovery-index-contract.d.ts +74 -0
- package/dist/discovery-index-contract.js +184 -0
- package/dist/discovery-soft-claim.d.ts +35 -0
- package/dist/discovery-soft-claim.js +50 -0
- package/dist/duplicate-winner.d.ts +57 -0
- package/dist/duplicate-winner.js +98 -0
- package/dist/feasibility.d.ts +27 -0
- package/dist/feasibility.js +86 -0
- package/dist/finding-severity-calibration.d.ts +116 -0
- package/dist/finding-severity-calibration.js +457 -0
- package/dist/fleet-run-manifest.d.ts +41 -0
- package/dist/fleet-run-manifest.js +151 -0
- package/dist/focus-manifest/guidance.d.ts +21 -0
- package/dist/focus-manifest/guidance.js +222 -0
- package/dist/focus-manifest.d.ts +826 -0
- package/dist/focus-manifest.js +2730 -0
- package/dist/gate-verdict-calibration.d.ts +111 -0
- package/dist/gate-verdict-calibration.js +411 -0
- package/dist/github/constants.d.ts +1 -0
- package/dist/github/constants.js +1 -0
- package/dist/github/sanitize-public-comment.d.ts +1 -0
- package/dist/github/sanitize-public-comment.js +26 -0
- package/dist/goal-model.d.ts +7 -0
- package/dist/goal-model.js +87 -0
- package/dist/governor/action-mode.d.ts +55 -0
- package/dist/governor/action-mode.js +74 -0
- package/dist/governor/budget-cap.d.ts +49 -0
- package/dist/governor/budget-cap.js +34 -0
- package/dist/governor/chokepoint.d.ts +58 -0
- package/dist/governor/chokepoint.js +269 -0
- package/dist/governor/kill-switch.d.ts +38 -0
- package/dist/governor/kill-switch.js +62 -0
- package/dist/governor/rate-limit.d.ts +40 -0
- package/dist/governor/rate-limit.js +63 -0
- package/dist/governor/reputation-throttle.d.ts +50 -0
- package/dist/governor/reputation-throttle.js +94 -0
- package/dist/governor/run-halt.d.ts +39 -0
- package/dist/governor/run-halt.js +83 -0
- package/dist/governor/self-plagiarism.d.ts +47 -0
- package/dist/governor/self-plagiarism.js +200 -0
- package/dist/governor/write-rate-limit.d.ts +52 -0
- package/dist/governor/write-rate-limit.js +132 -0
- package/dist/governor-ledger.d.ts +25 -0
- package/dist/governor-ledger.js +91 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +120 -0
- package/dist/issue-plan-decomposition.d.ts +18 -0
- package/dist/issue-plan-decomposition.js +56 -0
- package/dist/issue-rag-query.d.ts +12 -0
- package/dist/issue-rag-query.js +35 -0
- package/dist/local-scorer.d.ts +34 -0
- package/dist/local-scorer.js +36 -0
- package/dist/metadata-best-min-score.d.ts +9 -0
- package/dist/metadata-best-min-score.js +9 -0
- package/dist/metadata-best-pick.d.ts +9 -0
- package/dist/metadata-best-pick.js +9 -0
- package/dist/metadata-min-score.d.ts +9 -0
- package/dist/metadata-min-score.js +13 -0
- package/dist/metadata-top-min-score.d.ts +9 -0
- package/dist/metadata-top-min-score.js +13 -0
- package/dist/metadata-top-pick.d.ts +9 -0
- package/dist/metadata-top-pick.js +13 -0
- package/dist/miner/acceptance-criteria.d.ts +43 -0
- package/dist/miner/acceptance-criteria.js +77 -0
- package/dist/miner/agent-sdk-driver.d.ts +39 -0
- package/dist/miner/agent-sdk-driver.js +162 -0
- package/dist/miner/attempt-log.d.ts +29 -0
- package/dist/miner/attempt-log.js +100 -0
- package/dist/miner/attempt-metering.d.ts +39 -0
- package/dist/miner/attempt-metering.js +67 -0
- package/dist/miner/cli-subprocess-driver.d.ts +68 -0
- package/dist/miner/cli-subprocess-driver.js +272 -0
- package/dist/miner/coding-agent-driver.d.ts +33 -0
- package/dist/miner/coding-agent-driver.js +36 -0
- package/dist/miner/coding-agent-invoke.d.ts +11 -0
- package/dist/miner/coding-agent-invoke.js +80 -0
- package/dist/miner/coding-agent-mode.d.ts +25 -0
- package/dist/miner/coding-agent-mode.js +43 -0
- package/dist/miner/driver-factory.d.ts +82 -0
- package/dist/miner/driver-factory.js +169 -0
- package/dist/miner/harness-submission-trigger.d.ts +37 -0
- package/dist/miner/harness-submission-trigger.js +46 -0
- package/dist/miner/iterate-loop.d.ts +97 -0
- package/dist/miner/iterate-loop.js +278 -0
- package/dist/miner/iterate-policy.d.ts +98 -0
- package/dist/miner/iterate-policy.js +98 -0
- package/dist/miner/lint-guard.d.ts +50 -0
- package/dist/miner/lint-guard.js +68 -0
- package/dist/miner/local-write-tools.d.ts +81 -0
- package/dist/miner/local-write-tools.js +113 -0
- package/dist/miner/loop-reentry-policy.d.ts +36 -0
- package/dist/miner/loop-reentry-policy.js +53 -0
- package/dist/miner/repo-map.d.ts +51 -0
- package/dist/miner/repo-map.js +268 -0
- package/dist/miner/self-review-adapter.d.ts +106 -0
- package/dist/miner/self-review-adapter.js +83 -0
- package/dist/miner/submission-gate.d.ts +40 -0
- package/dist/miner/submission-gate.js +89 -0
- package/dist/miner/worktree-allocator.d.ts +59 -0
- package/dist/miner/worktree-allocator.js +60 -0
- package/dist/miner/worktree-pool.d.ts +55 -0
- package/dist/miner/worktree-pool.js +65 -0
- package/dist/miner-goal-lane-fit.d.ts +18 -0
- package/dist/miner-goal-lane-fit.js +76 -0
- package/dist/miner-goal-spec.d.ts +139 -0
- package/dist/miner-goal-spec.js +281 -0
- package/dist/miner-prediction-metrics.d.ts +16 -0
- package/dist/miner-prediction-metrics.js +61 -0
- package/dist/miner-telemetry.d.ts +32 -0
- package/dist/miner-telemetry.js +87 -0
- package/dist/objective-anchor.d.ts +125 -0
- package/dist/objective-anchor.js +401 -0
- package/dist/opportunity-competition.d.ts +6 -0
- package/dist/opportunity-competition.js +29 -0
- package/dist/opportunity-freshness.d.ts +18 -0
- package/dist/opportunity-freshness.js +55 -0
- package/dist/opportunity-metadata.d.ts +53 -0
- package/dist/opportunity-metadata.js +204 -0
- package/dist/opportunity-ranker.d.ts +46 -0
- package/dist/opportunity-ranker.js +74 -0
- package/dist/pairwise-calibration.d.ts +49 -0
- package/dist/pairwise-calibration.js +106 -0
- package/dist/phase7-calibration-loop.d.ts +136 -0
- package/dist/phase7-calibration-loop.js +437 -0
- package/dist/plan-blocked.d.ts +6 -0
- package/dist/plan-blocked.js +24 -0
- package/dist/plan-completed.d.ts +5 -0
- package/dist/plan-completed.js +6 -0
- package/dist/plan-completion.d.ts +6 -0
- package/dist/plan-completion.js +7 -0
- package/dist/plan-empty.d.ts +5 -0
- package/dist/plan-empty.js +6 -0
- package/dist/plan-export.d.ts +27 -0
- package/dist/plan-export.js +86 -0
- package/dist/plan-failure.d.ts +5 -0
- package/dist/plan-failure.js +6 -0
- package/dist/plan-overall-status.d.ts +6 -0
- package/dist/plan-overall-status.js +25 -0
- package/dist/plan-pending.d.ts +5 -0
- package/dist/plan-pending.js +6 -0
- package/dist/plan-progress-complete.d.ts +7 -0
- package/dist/plan-progress-complete.js +10 -0
- package/dist/plan-ready.d.ts +6 -0
- package/dist/plan-ready.js +12 -0
- package/dist/plan-running.d.ts +5 -0
- package/dist/plan-running.js +6 -0
- package/dist/plan-skipped.d.ts +5 -0
- package/dist/plan-skipped.js +6 -0
- package/dist/plan-step-count.d.ts +5 -0
- package/dist/plan-step-count.js +6 -0
- package/dist/plan-step-stats.d.ts +5 -0
- package/dist/plan-step-stats.js +6 -0
- package/dist/plan-templates.d.ts +25 -0
- package/dist/plan-templates.js +107 -0
- package/dist/plan-terminated.d.ts +6 -0
- package/dist/plan-terminated.js +9 -0
- package/dist/portfolio/non-convergence.d.ts +37 -0
- package/dist/portfolio/non-convergence.js +57 -0
- package/dist/portfolio/queue.d.ts +30 -0
- package/dist/portfolio/queue.js +150 -0
- package/dist/predicted-gate.d.ts +103 -0
- package/dist/predicted-gate.js +218 -0
- package/dist/prompt-packet.d.ts +11 -0
- package/dist/prompt-packet.js +34 -0
- package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-best-min-score.js +9 -0
- package/dist/ranked-opportunity-best-pick.d.ts +8 -0
- package/dist/ranked-opportunity-best-pick.js +9 -0
- package/dist/ranked-opportunity-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-min-score.js +13 -0
- package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-top-min-score.js +13 -0
- package/dist/review/advisory-ai-routing-config.d.ts +8 -0
- package/dist/review/advisory-ai-routing-config.js +40 -0
- package/dist/review/check-names.d.ts +1 -0
- package/dist/review/check-names.js +1 -0
- package/dist/review/cla-check.d.ts +49 -0
- package/dist/review/cla-check.js +71 -0
- package/dist/review/diff-file-priority.d.ts +1 -0
- package/dist/review/diff-file-priority.js +12 -0
- package/dist/review/enrichment-analyzer-names.d.ts +3 -0
- package/dist/review/enrichment-analyzer-names.js +63 -0
- package/dist/review/guardrail-config.d.ts +10 -0
- package/dist/review/guardrail-config.js +62 -0
- package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
- package/dist/review/linked-issue-hard-rules-config.js +79 -0
- package/dist/review/linked-issue-label-propagation.d.ts +9 -0
- package/dist/review/linked-issue-label-propagation.js +114 -0
- package/dist/review/pre-merge-checks.d.ts +28 -0
- package/dist/review/pre-merge-checks.js +66 -0
- package/dist/review/review-thread-findings.d.ts +1 -0
- package/dist/review/review-thread-findings.js +1 -0
- package/dist/review/safe-url.d.ts +5 -0
- package/dist/review/safe-url.js +137 -0
- package/dist/review/screenshot-table-gate.d.ts +87 -0
- package/dist/review/screenshot-table-gate.js +340 -0
- package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
- package/dist/review/unlinked-issue-guardrail-config.js +44 -0
- package/dist/reviewer-consensus-calibration.d.ts +113 -0
- package/dist/reviewer-consensus-calibration.js +493 -0
- package/dist/reward-risk.d.ts +223 -0
- package/dist/reward-risk.js +618 -0
- package/dist/scoring/label-match.d.ts +4 -0
- package/dist/scoring/label-match.js +145 -0
- package/dist/scoring/model.d.ts +31 -0
- package/dist/scoring/model.js +157 -0
- package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
- package/dist/scoring/pending-pr-scenarios.js +145 -0
- package/dist/scoring/preview.d.ts +246 -0
- package/dist/scoring/preview.js +1108 -0
- package/dist/scoring/types.d.ts +131 -0
- package/dist/scoring/types.js +7 -0
- package/dist/settings/auto-close-exempt.d.ts +10 -0
- package/dist/settings/auto-close-exempt.js +58 -0
- package/dist/settings/autonomy.d.ts +33 -0
- package/dist/settings/autonomy.js +80 -0
- package/dist/settings/command-authorization.d.ts +49 -0
- package/dist/settings/command-authorization.js +222 -0
- package/dist/settings/contributor-blacklist.d.ts +17 -0
- package/dist/settings/contributor-blacklist.js +86 -0
- package/dist/settings/moderation-rules.d.ts +51 -0
- package/dist/settings/moderation-rules.js +104 -0
- package/dist/settings/pr-type-label.d.ts +70 -0
- package/dist/settings/pr-type-label.js +149 -0
- package/dist/signals/change-guardrail.d.ts +49 -0
- package/dist/signals/change-guardrail.js +164 -0
- package/dist/signals/check-summary.d.ts +9 -0
- package/dist/signals/check-summary.js +12 -0
- package/dist/signals/contributor-calibration.d.ts +23 -0
- package/dist/signals/contributor-calibration.js +47 -0
- package/dist/signals/duplicate-winner.d.ts +9 -0
- package/dist/signals/duplicate-winner.js +9 -0
- package/dist/signals/path-matchers.d.ts +36 -0
- package/dist/signals/path-matchers.js +371 -0
- package/dist/signals/predicted-gate-engine.d.ts +62 -0
- package/dist/signals/predicted-gate-engine.js +895 -0
- package/dist/signals/preflight-limits.d.ts +14 -0
- package/dist/signals/preflight-limits.js +14 -0
- package/dist/signals/slop.d.ts +56 -0
- package/dist/signals/slop.js +324 -0
- package/dist/signals/test-evidence.d.ts +25 -0
- package/dist/signals/test-evidence.js +108 -0
- package/dist/subprocess-env.d.ts +23 -0
- package/dist/subprocess-env.js +74 -0
- package/dist/track-record-summary.d.ts +92 -0
- package/dist/track-record-summary.js +301 -0
- package/dist/types/manifest-deps-types.d.ts +514 -0
- package/dist/types/manifest-deps-types.js +4 -0
- package/dist/types/predicted-gate-types.d.ts +338 -0
- package/dist/types/predicted-gate-types.js +3 -0
- package/dist/types/reward-risk-types.d.ts +319 -0
- package/dist/types/reward-risk-types.js +11 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +80 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export type RepoTimeDecayOverrides = {
|
|
6
|
+
gracePeriodHours?: number | null | undefined;
|
|
7
|
+
sigmoidMidpointDays?: number | null | undefined;
|
|
8
|
+
sigmoidSteepness?: number | null | undefined;
|
|
9
|
+
minMultiplier?: number | null | undefined;
|
|
10
|
+
};
|
|
11
|
+
export type RegistryRepoConfig = {
|
|
12
|
+
repo: string;
|
|
13
|
+
emissionShare: number;
|
|
14
|
+
issueDiscoveryShare: number;
|
|
15
|
+
labelMultipliers: Record<string, number>;
|
|
16
|
+
trustedLabelPipeline?: boolean | null;
|
|
17
|
+
maintainerCut: number;
|
|
18
|
+
defaultLabelMultiplier?: number | null;
|
|
19
|
+
fixedBaseScore?: number | null;
|
|
20
|
+
eligibilityMode?: string | null;
|
|
21
|
+
timeDecay?: RepoTimeDecayOverrides | null;
|
|
22
|
+
raw: Record<string, JsonValue>;
|
|
23
|
+
};
|
|
24
|
+
export type RepositoryRecord = {
|
|
25
|
+
fullName: string;
|
|
26
|
+
owner: string;
|
|
27
|
+
name: string;
|
|
28
|
+
installationId?: number | null | undefined;
|
|
29
|
+
isInstalled: boolean;
|
|
30
|
+
isRegistered: boolean;
|
|
31
|
+
isPrivate: boolean;
|
|
32
|
+
htmlUrl?: string | null | undefined;
|
|
33
|
+
defaultBranch?: string | null | undefined;
|
|
34
|
+
registryConfig?: RegistryRepoConfig | null | undefined;
|
|
35
|
+
};
|
|
36
|
+
export type PullRequestRecord = {
|
|
37
|
+
repoFullName: string;
|
|
38
|
+
number: number;
|
|
39
|
+
title: string;
|
|
40
|
+
state: string;
|
|
41
|
+
authorLogin?: string | null | undefined;
|
|
42
|
+
authorAssociation?: string | null | undefined;
|
|
43
|
+
headSha?: string | null | undefined;
|
|
44
|
+
headRef?: string | null | undefined;
|
|
45
|
+
baseRef?: string | null | undefined;
|
|
46
|
+
htmlUrl?: string | null | undefined;
|
|
47
|
+
mergedAt?: string | null | undefined;
|
|
48
|
+
isDraft?: boolean | null | undefined;
|
|
49
|
+
mergeableState?: string | null | undefined;
|
|
50
|
+
reviewDecision?: string | null | undefined;
|
|
51
|
+
body?: string | null | undefined;
|
|
52
|
+
createdAt?: string | null | undefined;
|
|
53
|
+
updatedAt?: string | null | undefined;
|
|
54
|
+
closedAt?: string | null | undefined;
|
|
55
|
+
linkedIssueClaimedAt?: string | null | undefined;
|
|
56
|
+
labels: string[];
|
|
57
|
+
linkedIssues: number[];
|
|
58
|
+
slopRisk?: number | null | undefined;
|
|
59
|
+
slopBand?: string | null | undefined;
|
|
60
|
+
mergeAttemptCount?: number | null | undefined;
|
|
61
|
+
mergeBlockedSha?: string | null | undefined;
|
|
62
|
+
mergeBlockedReason?: string | null | undefined;
|
|
63
|
+
approvedHeadSha?: string | null | undefined;
|
|
64
|
+
lastRegatedAt?: string | null | undefined;
|
|
65
|
+
lastPublishedSurfaceSha?: string | null | undefined;
|
|
66
|
+
changedFiles?: string[] | undefined;
|
|
67
|
+
};
|
|
68
|
+
export type PullRequestReviewRecord = {
|
|
69
|
+
id: string;
|
|
70
|
+
repoFullName: string;
|
|
71
|
+
pullNumber: number;
|
|
72
|
+
reviewerLogin?: string | null | undefined;
|
|
73
|
+
state: string;
|
|
74
|
+
authorAssociation?: string | null | undefined;
|
|
75
|
+
submittedAt?: string | null | undefined;
|
|
76
|
+
payload: Record<string, JsonValue>;
|
|
77
|
+
};
|
|
78
|
+
export type CheckSummaryRecord = {
|
|
79
|
+
id: string;
|
|
80
|
+
repoFullName: string;
|
|
81
|
+
pullNumber?: number | null | undefined;
|
|
82
|
+
headSha?: string | null | undefined;
|
|
83
|
+
name: string;
|
|
84
|
+
status: string;
|
|
85
|
+
conclusion?: string | null | undefined;
|
|
86
|
+
startedAt?: string | null | undefined;
|
|
87
|
+
completedAt?: string | null | undefined;
|
|
88
|
+
detailsUrl?: string | null | undefined;
|
|
89
|
+
payload: Record<string, JsonValue>;
|
|
90
|
+
};
|
|
91
|
+
export type ScoringModelSnapshotRecord = {
|
|
92
|
+
id: string;
|
|
93
|
+
sourceKind: "raw-github" | "api" | "fallback" | "test";
|
|
94
|
+
sourceUrl: string;
|
|
95
|
+
fetchedAt: string;
|
|
96
|
+
activeModel: "current_density_model" | "pending_saturation_model" | "exponential_saturation_model" | "unknown";
|
|
97
|
+
constants: Record<string, number>;
|
|
98
|
+
programmingLanguages: Record<string, JsonValue>;
|
|
99
|
+
registrySnapshotId?: string | null | undefined;
|
|
100
|
+
warnings: string[];
|
|
101
|
+
payload: Record<string, JsonValue>;
|
|
102
|
+
};
|
|
103
|
+
export type ScorePreviewRecord = {
|
|
104
|
+
id: string;
|
|
105
|
+
scoringModelSnapshotId: string;
|
|
106
|
+
repoFullName: string;
|
|
107
|
+
targetType: "planned_pr" | "pull_request" | "local_diff" | "variant";
|
|
108
|
+
targetKey: string;
|
|
109
|
+
contributorLogin?: string | null | undefined;
|
|
110
|
+
input: Record<string, JsonValue>;
|
|
111
|
+
result: Record<string, JsonValue>;
|
|
112
|
+
generatedAt: string;
|
|
113
|
+
};
|
|
114
|
+
export type ContributorEvidenceRecord = {
|
|
115
|
+
login: string;
|
|
116
|
+
payload: Record<string, JsonValue>;
|
|
117
|
+
generatedAt: string;
|
|
118
|
+
};
|
|
119
|
+
export type ContributorRole = "outside_contributor" | "repo_maintainer" | "org_member" | "collaborator" | "owner" | "unknown";
|
|
120
|
+
export type RoleContext = {
|
|
121
|
+
login: string;
|
|
122
|
+
repoFullName: string;
|
|
123
|
+
generatedAt: string;
|
|
124
|
+
role: ContributorRole;
|
|
125
|
+
maintainerLane: boolean;
|
|
126
|
+
normalContributorEvidenceAllowed: boolean;
|
|
127
|
+
source: "github_association" | "repo_owner_match" | "gittensor_api" | "cache" | "unknown";
|
|
128
|
+
association?: string | null | undefined;
|
|
129
|
+
reasons: string[];
|
|
130
|
+
guidance: string;
|
|
131
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Local mirror of the record/type shapes `scoring/preview.ts`, `scoring/model.ts`, and
|
|
2
|
+
// `scoring/pending-pr-scenarios.ts` need from the backend's `src/types.ts` and `src/signals/engine.ts`.
|
|
3
|
+
// The engine package cannot import across into `src/` (see the package's own tsconfig: `rootDir: "src"`,
|
|
4
|
+
// `types: []` — it stays isolated from the Cloudflare Worker/D1 ambient types the backend depends on), so
|
|
5
|
+
// these shapes are duplicated here rather than imported. `src/types.ts` and `src/signals/engine.ts` stay
|
|
6
|
+
// the canonical source; keep this file in sync with them by hand.
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Normalize a raw exempt-logins value (DB JSON or `.gittensory.yml`) into a validated, de-duplicated list of
|
|
2
|
+
* GitHub logins. Never throws: malformed entries are dropped with a warning. De-dup is case-insensitive (the
|
|
3
|
+
* FIRST occurrence's casing is kept). */
|
|
4
|
+
export declare function normalizeAutoCloseExemptLogins(input: unknown): {
|
|
5
|
+
logins: string[];
|
|
6
|
+
warnings: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Case-insensitive membership check against the resolved exempt-logins list. Absent/empty list ⇒ never exempt
|
|
9
|
+
* (the safe default — an unconfigured repo exempts no one beyond the standing owner/admin/bot rule). */
|
|
10
|
+
export declare function isAutoCloseExempt(login: string | null | undefined, exemptLogins: readonly string[] | undefined): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Shared repo-scoped exemption list (#2463) for gittensory's deterministic anti-abuse auto-close/throttle
|
|
2
|
+
// mechanisms — currently the review-nag cooldown; intended to be reused by the per-contributor open-item cap
|
|
3
|
+
// (#2270) once that lands, rather than each feature growing its own duplicate whitelist. A maintainer-named
|
|
4
|
+
// GitHub login here is NEVER throttled or closed by either mechanism, on top of the standing owner/admin/
|
|
5
|
+
// automation-bot exemption every such mechanism already honors. Config-driven and layered the same as other
|
|
6
|
+
// settings (`.gittensory.yml` > DB), never hard-coded for any repo. Mirrors contributor-blacklist.ts's shape
|
|
7
|
+
// (normalize → validated list + warnings), minus the reason/evidence metadata a ban carries that an exemption
|
|
8
|
+
// doesn't need.
|
|
9
|
+
// A trailing `[bot]` is a real, common GitHub App-actor login shape (e.g. `dependabot[bot]`, `sentry[bot]`) --
|
|
10
|
+
// this is exactly the kind of third-party automation identity a maintainer needs to exempt (a repo-specific bot
|
|
11
|
+
// integration the hardcoded, install-wide well-known-bot set in agent-actions.ts has no way to know about), so
|
|
12
|
+
// the base GitHub-login pattern (1-39 chars, alphanumeric/single-hyphens) is extended with an optional literal
|
|
13
|
+
// `[bot]` suffix rather than rejecting every bot-shaped login outright.
|
|
14
|
+
const GITHUB_LOGIN = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}(?:\[bot\])?$/;
|
|
15
|
+
const MAX_ENTRIES = 500;
|
|
16
|
+
/** Normalize a raw exempt-logins value (DB JSON or `.gittensory.yml`) into a validated, de-duplicated list of
|
|
17
|
+
* GitHub logins. Never throws: malformed entries are dropped with a warning. De-dup is case-insensitive (the
|
|
18
|
+
* FIRST occurrence's casing is kept). */
|
|
19
|
+
export function normalizeAutoCloseExemptLogins(input) {
|
|
20
|
+
const warnings = [];
|
|
21
|
+
if (input === undefined || input === null)
|
|
22
|
+
return { logins: [], warnings };
|
|
23
|
+
if (!Array.isArray(input)) {
|
|
24
|
+
warnings.push("autoCloseExemptLogins must be a list of GitHub logins; ignoring it.");
|
|
25
|
+
return { logins: [], warnings };
|
|
26
|
+
}
|
|
27
|
+
const logins = [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
for (const [index, raw] of input.entries()) {
|
|
30
|
+
if (logins.length >= MAX_ENTRIES) {
|
|
31
|
+
warnings.push(`autoCloseExemptLogins is capped at ${MAX_ENTRIES} entries; dropping the rest.`);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (typeof raw !== "string") {
|
|
35
|
+
warnings.push(`autoCloseExemptLogins[${index}] must be a string login; ignoring it.`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const login = raw.trim();
|
|
39
|
+
if (!GITHUB_LOGIN.test(login)) {
|
|
40
|
+
warnings.push(`autoCloseExemptLogins[${index}] is not a valid GitHub login; ignoring it.`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const key = login.toLowerCase();
|
|
44
|
+
if (seen.has(key))
|
|
45
|
+
continue; // first occurrence wins
|
|
46
|
+
seen.add(key);
|
|
47
|
+
logins.push(login);
|
|
48
|
+
}
|
|
49
|
+
return { logins, warnings };
|
|
50
|
+
}
|
|
51
|
+
/** Case-insensitive membership check against the resolved exempt-logins list. Absent/empty list ⇒ never exempt
|
|
52
|
+
* (the safe default — an unconfigured repo exempts no one beyond the standing owner/admin/bot rule). */
|
|
53
|
+
export function isAutoCloseExempt(login, exemptLogins) {
|
|
54
|
+
if (!login)
|
|
55
|
+
return false;
|
|
56
|
+
const lower = login.toLowerCase();
|
|
57
|
+
return (exemptLogins ?? []).some((entry) => entry.toLowerCase() === lower);
|
|
58
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentActionClass, AutoMaintainPolicy, AutonomyLevel, AutonomyPolicy } from "../types/manifest-deps-types.js";
|
|
2
|
+
export declare const AUTONOMY_LEVELS: readonly ["observe", "auto_with_approval", "auto"];
|
|
3
|
+
export declare const AGENT_ACTION_CLASSES: readonly ["review", "request_changes", "approve", "merge", "close", "label", "review_state_label", "update_branch", "assign"];
|
|
4
|
+
export declare const DEFAULT_AUTONOMY_LEVEL: AutonomyLevel;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the configured autonomy level for one action class on a repo. THE single gate the action layer
|
|
7
|
+
* (#778) consults before any write action. Deny-by-default: an unset (or malformed) action class is
|
|
8
|
+
* `observe` — gittensory observes but never acts. Pure.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveAutonomy(autonomy: AutonomyPolicy | null | undefined, actionClass: AgentActionClass): AutonomyLevel;
|
|
11
|
+
/** True when the level permits the agent to actually execute the action (directly or behind an approval). */
|
|
12
|
+
export declare function isActingAutonomyLevel(level: AutonomyLevel): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* True when a repo has opted into the agent layer at all — i.e. at least one action class has an acting
|
|
15
|
+
* autonomy level. The deny-by-default floor (every class `observe`) is NOT configured. The scheduled
|
|
16
|
+
* re-gate sweep (#777) uses this to skip repos that never asked the agent to act. Pure.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isAgentConfigured(autonomy: AutonomyPolicy | null | undefined): boolean;
|
|
19
|
+
/** True when the action must pass a human approval gate (#779) before it executes. */
|
|
20
|
+
export declare function autonomyRequiresApproval(level: AutonomyLevel): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Parse/validate an arbitrary value into an AutonomyPolicy: keep only known action classes mapped to known
|
|
23
|
+
* levels, drop everything else. Deny-by-default by omission. Used for the DB row, the API body, and the
|
|
24
|
+
* `.gittensory.yml` settings block. Pure.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeAutonomyPolicy(input: unknown): AutonomyPolicy;
|
|
27
|
+
export declare const AUTO_MERGE_METHODS: readonly ["merge", "squash", "rebase"];
|
|
28
|
+
export declare const DEFAULT_AUTO_MAINTAIN_POLICY: AutoMaintainPolicy;
|
|
29
|
+
/**
|
|
30
|
+
* Parse/validate an arbitrary value into an AutoMaintainPolicy, filling the conservative defaults for any
|
|
31
|
+
* missing/invalid field. `requireApprovals` is clamped to [0, 10]. Pure.
|
|
32
|
+
*/
|
|
33
|
+
export declare function normalizeAutoMaintainPolicy(input: unknown): AutoMaintainPolicy;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The graduated autonomy dial (#773), ordered least → most autonomous. Every later agent-layer phase reads
|
|
2
|
+
// this BEFORE acting. `observe` is the deny-by-default floor — gittensory watches but never takes an action.
|
|
3
|
+
// (#4620: `suggest`/`propose` removed -- both were 100% behaviorally identical to `observe`, see
|
|
4
|
+
// AutonomyLevel's own doc comment.)
|
|
5
|
+
export const AUTONOMY_LEVELS = ["observe", "auto_with_approval", "auto"];
|
|
6
|
+
// The write-action classes the maintainer auto-maintain layer (#778) can take on a PR. `review_state_label`
|
|
7
|
+
// (#label-scoping) is a separate class from `label`: it gates the planner's own disposition-communication
|
|
8
|
+
// labels (ready-to-merge / changes-requested / manual-review / migration-collision / pending-closure /
|
|
9
|
+
// new-account), independent of the anti-abuse enforcement labels (blacklist/contributor-cap/review-nag), which
|
|
10
|
+
// ride on `close` instead -- see agent-actions.ts. `assign` (#3182) is its own independent class, same shape:
|
|
11
|
+
// best-effort assignment of the PR's opening contributor, unrelated to merge/close/approve.
|
|
12
|
+
export const AGENT_ACTION_CLASSES = ["review", "request_changes", "approve", "merge", "close", "label", "review_state_label", "update_branch", "assign"];
|
|
13
|
+
// Deny-by-default: any action class with no explicit, valid level resolves to this.
|
|
14
|
+
export const DEFAULT_AUTONOMY_LEVEL = "observe";
|
|
15
|
+
const AUTONOMY_LEVEL_SET = new Set(AUTONOMY_LEVELS);
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the configured autonomy level for one action class on a repo. THE single gate the action layer
|
|
18
|
+
* (#778) consults before any write action. Deny-by-default: an unset (or malformed) action class is
|
|
19
|
+
* `observe` — gittensory observes but never acts. Pure.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveAutonomy(autonomy, actionClass) {
|
|
22
|
+
return autonomy?.[actionClass] ?? DEFAULT_AUTONOMY_LEVEL;
|
|
23
|
+
}
|
|
24
|
+
/** True when the level permits the agent to actually execute the action (directly or behind an approval). */
|
|
25
|
+
export function isActingAutonomyLevel(level) {
|
|
26
|
+
return level === "auto" || level === "auto_with_approval";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True when a repo has opted into the agent layer at all — i.e. at least one action class has an acting
|
|
30
|
+
* autonomy level. The deny-by-default floor (every class `observe`) is NOT configured. The scheduled
|
|
31
|
+
* re-gate sweep (#777) uses this to skip repos that never asked the agent to act. Pure.
|
|
32
|
+
*/
|
|
33
|
+
export function isAgentConfigured(autonomy) {
|
|
34
|
+
return AGENT_ACTION_CLASSES.some((actionClass) => isActingAutonomyLevel(resolveAutonomy(autonomy, actionClass)));
|
|
35
|
+
}
|
|
36
|
+
/** True when the action must pass a human approval gate (#779) before it executes. */
|
|
37
|
+
export function autonomyRequiresApproval(level) {
|
|
38
|
+
return level === "auto_with_approval";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse/validate an arbitrary value into an AutonomyPolicy: keep only known action classes mapped to known
|
|
42
|
+
* levels, drop everything else. Deny-by-default by omission. Used for the DB row, the API body, and the
|
|
43
|
+
* `.gittensory.yml` settings block. Pure.
|
|
44
|
+
*/
|
|
45
|
+
export function normalizeAutonomyPolicy(input) {
|
|
46
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
47
|
+
return {};
|
|
48
|
+
const record = input;
|
|
49
|
+
const policy = {};
|
|
50
|
+
for (const actionClass of AGENT_ACTION_CLASSES) {
|
|
51
|
+
const value = record[actionClass];
|
|
52
|
+
if (typeof value === "string" && AUTONOMY_LEVEL_SET.has(value)) {
|
|
53
|
+
policy[actionClass] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return policy;
|
|
57
|
+
}
|
|
58
|
+
// Auto-maintain policy (#774): how an action behaves once its autonomy level permits acting.
|
|
59
|
+
export const AUTO_MERGE_METHODS = ["merge", "squash", "rebase"];
|
|
60
|
+
const AUTO_MERGE_METHOD_SET = new Set(AUTO_MERGE_METHODS);
|
|
61
|
+
// Conservative defaults: squash (the tidiest history) + a single human approval before any auto-merge.
|
|
62
|
+
export const DEFAULT_AUTO_MAINTAIN_POLICY = { requireApprovals: 1, mergeMethod: "squash" };
|
|
63
|
+
// Approvals are clamped to a sane band so a malformed config can't disable the gate (negative) or stall it.
|
|
64
|
+
const MAX_REQUIRE_APPROVALS = 10;
|
|
65
|
+
/**
|
|
66
|
+
* Parse/validate an arbitrary value into an AutoMaintainPolicy, filling the conservative defaults for any
|
|
67
|
+
* missing/invalid field. `requireApprovals` is clamped to [0, 10]. Pure.
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeAutoMaintainPolicy(input) {
|
|
70
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
71
|
+
return { ...DEFAULT_AUTO_MAINTAIN_POLICY };
|
|
72
|
+
const record = input;
|
|
73
|
+
const rawApprovals = record.requireApprovals;
|
|
74
|
+
const requireApprovals = typeof rawApprovals === "number" && Number.isFinite(rawApprovals)
|
|
75
|
+
? Math.min(MAX_REQUIRE_APPROVALS, Math.max(0, Math.trunc(rawApprovals)))
|
|
76
|
+
: DEFAULT_AUTO_MAINTAIN_POLICY.requireApprovals;
|
|
77
|
+
const rawMethod = record.mergeMethod;
|
|
78
|
+
const mergeMethod = typeof rawMethod === "string" && AUTO_MERGE_METHOD_SET.has(rawMethod) ? rawMethod : DEFAULT_AUTO_MAINTAIN_POLICY.mergeMethod;
|
|
79
|
+
return { requireApprovals, mergeMethod };
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CommandAuthorizationRole, RepositoryCommandAuthorizationPolicy } from "../types/manifest-deps-types.js";
|
|
2
|
+
export declare const DEFAULT_COMMAND_AUTHORIZATION_POLICY: RepositoryCommandAuthorizationPolicy;
|
|
3
|
+
export type CommandAuthorizationDecision = {
|
|
4
|
+
authorized: boolean;
|
|
5
|
+
reason: string;
|
|
6
|
+
actorKind: "maintainer" | "author" | "none";
|
|
7
|
+
matchedRole: CommandAuthorizationRole | null;
|
|
8
|
+
allowedRoles: CommandAuthorizationRole[];
|
|
9
|
+
};
|
|
10
|
+
export declare function normalizeCommandAuthorizationPolicy(input: unknown): {
|
|
11
|
+
policy: RepositoryCommandAuthorizationPolicy;
|
|
12
|
+
warnings: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function commandAuthorizationAllowedRoles(policy: RepositoryCommandAuthorizationPolicy | null | undefined, commandName: string): CommandAuthorizationRole[];
|
|
15
|
+
export declare function commandAuthorizationNeedsMinerDetection(args: {
|
|
16
|
+
policy?: RepositoryCommandAuthorizationPolicy | null | undefined;
|
|
17
|
+
commandName: string;
|
|
18
|
+
commenterLogin?: string | null | undefined;
|
|
19
|
+
commenterAssociation?: string | null | undefined;
|
|
20
|
+
pullRequestAuthorLogin?: string | null | undefined;
|
|
21
|
+
}): boolean;
|
|
22
|
+
export declare function evaluateCommandAuthorization(args: {
|
|
23
|
+
policy?: RepositoryCommandAuthorizationPolicy | null | undefined;
|
|
24
|
+
commandName: string;
|
|
25
|
+
commenterLogin?: string | null | undefined;
|
|
26
|
+
commenterAssociation?: string | null | undefined;
|
|
27
|
+
pullRequestAuthorLogin?: string | null | undefined;
|
|
28
|
+
minerStatus?: "confirmed" | "not_found" | "unavailable" | undefined;
|
|
29
|
+
/** #5084: required (must be `"hold"`) for a bare `pr_author` match to actually authorize a command in
|
|
30
|
+
* {@link PR_AUTHOR_RATE_LIMITED_COMMANDS} (currently just `chat`) -- unset/`"off"` denies exactly as if
|
|
31
|
+
* `pr_author` weren't in the allowed-roles list at all, so a repo that hasn't turned on rate limiting
|
|
32
|
+
* never grants contributor chat access no matter what `chat`'s configured roles say. */
|
|
33
|
+
commandRateLimitPolicy?: "off" | "hold" | undefined;
|
|
34
|
+
/** #5092: ALSO required (must be `true`) for a bare `pr_author` match to authorize a command in
|
|
35
|
+
* {@link PR_AUTHOR_RATE_LIMITED_COMMANDS} -- the per-PR rate-limit counter (`repoFullName#issueNumber#command`)
|
|
36
|
+
* never resets or checks PR state, so without this a contributor could keep a fresh allowance forever by
|
|
37
|
+
* reopening/reusing a closed PR or spamming cheap draft PRs. Caller-computed (e.g. `pr.state === "open" &&
|
|
38
|
+
* !pr.isDraft`) so this function doesn't need to know GitHub's own state-string conventions. Unset/`false`
|
|
39
|
+
* denies exactly like a missing rate-limit policy -- maintainers/collaborators are unaffected regardless
|
|
40
|
+
* (this bounds the less-trusted pr_author tier, not already-trusted roles). */
|
|
41
|
+
pullRequestOpenAndNotDraft?: boolean | undefined;
|
|
42
|
+
}): CommandAuthorizationDecision;
|
|
43
|
+
export declare function summarizeCommandAuthorizationPolicy(policy: RepositoryCommandAuthorizationPolicy | null | undefined): {
|
|
44
|
+
defaultAllowed: CommandAuthorizationRole[];
|
|
45
|
+
commandOverrides: Array<{
|
|
46
|
+
command: string;
|
|
47
|
+
allowedRoles: CommandAuthorizationRole[];
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
export const DEFAULT_COMMAND_AUTHORIZATION_POLICY = {
|
|
2
|
+
default: ["maintainer", "collaborator", "confirmed_miner"],
|
|
3
|
+
commands: {
|
|
4
|
+
"queue-summary": ["maintainer", "collaborator"],
|
|
5
|
+
"confirmed-miners": ["maintainer", "collaborator"],
|
|
6
|
+
"review-now": ["maintainer", "collaborator"],
|
|
7
|
+
"needs-author": ["maintainer", "collaborator"],
|
|
8
|
+
"duplicate-clusters": ["maintainer", "collaborator"],
|
|
9
|
+
"burden-forecast": ["maintainer", "collaborator"],
|
|
10
|
+
"intake-health": ["maintainer", "collaborator"],
|
|
11
|
+
"outcome-patterns": ["maintainer", "collaborator"],
|
|
12
|
+
"noise-report": ["maintainer", "collaborator"],
|
|
13
|
+
"gate-override": ["maintainer", "collaborator"],
|
|
14
|
+
plan: ["maintainer", "collaborator"],
|
|
15
|
+
// #4595/#5084: chat is Ollama-only grounded LLM generation, a materially larger surface than ask's
|
|
16
|
+
// deterministic-only answer, so v1 started maintainer/collaborator-only. #5084 widens this to the PR's
|
|
17
|
+
// OWN author (never an arbitrary commenter on someone else's PR) -- but ONLY when commandRateLimitPolicy
|
|
18
|
+
// is "hold" for the repo, enforced in evaluateCommandAuthorization below, not just by operator convention.
|
|
19
|
+
// Explicit registration here (rather than falling through to `default`) also activates the
|
|
20
|
+
// MAINTAINER_ONLY_DEFAULT_COMMANDS clamp in normalizeCommandRoleList, so a self-hoster can't yml
|
|
21
|
+
// themselves into "any confirmed_miner" or similar widening beyond what's shipped here.
|
|
22
|
+
chat: ["maintainer", "collaborator", "pr_author"],
|
|
23
|
+
// #1960 PR control-surface verbs. "review" is deliberately widenable to confirmed_miner (same self-rerun
|
|
24
|
+
// precedent already applied to review-now, #824) — a confirmed miner may re-trigger review on their own PR.
|
|
25
|
+
// The rest (pause/resume/resolve/configuration/explain) are conservative maintainer/collaborator-only
|
|
26
|
+
// defaults out of the box; a maintainer who wants to widen them can do so via commandAuthorization overrides.
|
|
27
|
+
review: ["maintainer", "collaborator", "confirmed_miner"],
|
|
28
|
+
pause: ["maintainer", "collaborator"],
|
|
29
|
+
resume: ["maintainer", "collaborator"],
|
|
30
|
+
resolve: ["maintainer", "collaborator"],
|
|
31
|
+
configuration: ["maintainer", "collaborator"],
|
|
32
|
+
explain: ["maintainer", "collaborator"],
|
|
33
|
+
// #4195 (part of the #4189 E2E-test-generation epic): deliberately NARROWER than every command above --
|
|
34
|
+
// "maintainer" ONLY, excluding "collaborator" and "confirmed_miner". This command can write real content
|
|
35
|
+
// (a generated test) attributed to the PR; a repo could grant a contributor/miner collaborator-level
|
|
36
|
+
// push access, and that tier must not be able to invoke test generation for their own scored PR (the
|
|
37
|
+
// exact loophole a click-to-generate button would otherwise open). The existing
|
|
38
|
+
// `maintainer_command_requires_maintainer` guard below already denies the PR's own author when they
|
|
39
|
+
// don't independently hold the `maintainer` role, so no bespoke pr_author check is needed here.
|
|
40
|
+
"generate-tests": ["maintainer"],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const COMMAND_AUTHORIZATION_ROLES = new Set(["maintainer", "collaborator", "pr_author", "confirmed_miner"]);
|
|
44
|
+
// Roles that may remain configured on a maintainer-only command. The clamp drops only the spoofable
|
|
45
|
+
// plain `pr_author` role; `confirmed_miner` survives so a detected miner can self-trigger reruns (#824).
|
|
46
|
+
const MAINTAINER_COMMAND_AUTHORIZATION_ROLES = new Set(["maintainer", "collaborator", "confirmed_miner"]);
|
|
47
|
+
const MAINTAINER_ONLY_DEFAULT_COMMANDS = new Set(Object.keys(DEFAULT_COMMAND_AUTHORIZATION_POLICY.commands));
|
|
48
|
+
// #5084: commands where a `pr_author` match is only actually granted when commandRateLimitPolicy is "hold" for
|
|
49
|
+
// the repo -- checked in evaluateCommandAuthorization. Currently just `chat` (Ollama-only LLM generation);
|
|
50
|
+
// deliberately a narrow, explicit allowlist rather than inferring this from isAiCostBearingCommand, so widening
|
|
51
|
+
// it to another command later is a deliberate one-line addition, not an implicit side effect of an unrelated set.
|
|
52
|
+
const PR_AUTHOR_RATE_LIMITED_COMMANDS = new Set(["chat"]);
|
|
53
|
+
export function normalizeCommandAuthorizationPolicy(input) {
|
|
54
|
+
const warnings = [];
|
|
55
|
+
if (!isRecord(input)) {
|
|
56
|
+
if (input !== null && input !== undefined)
|
|
57
|
+
warnings.push("commandAuthorization must be an object; using secure defaults.");
|
|
58
|
+
return { policy: clonePolicy(DEFAULT_COMMAND_AUTHORIZATION_POLICY), warnings };
|
|
59
|
+
}
|
|
60
|
+
const defaultRoles = normalizeRoleList(input.default, DEFAULT_COMMAND_AUTHORIZATION_POLICY.default, "default", warnings);
|
|
61
|
+
const commands = { ...DEFAULT_COMMAND_AUTHORIZATION_POLICY.commands };
|
|
62
|
+
if (input.commands !== undefined) {
|
|
63
|
+
if (isRecord(input.commands)) {
|
|
64
|
+
for (const [command, roles] of Object.entries(input.commands)) {
|
|
65
|
+
const commandName = command.trim().toLowerCase();
|
|
66
|
+
if (!/^[a-z][a-z-]{0,63}$/.test(commandName)) {
|
|
67
|
+
warnings.push(`Ignored malformed command authorization key: ${command.slice(0, 64)}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
commands[commandName] = normalizeCommandRoleList(commandName, normalizeRoleList(roles, defaultRoles, commandName, warnings), warnings);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
warnings.push("commandAuthorization.commands must be an object; using command defaults.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { policy: { default: defaultRoles, commands }, warnings };
|
|
78
|
+
}
|
|
79
|
+
export function commandAuthorizationAllowedRoles(policy, commandName) {
|
|
80
|
+
const normalized = normalizeCommandAuthorizationPolicy(policy).policy;
|
|
81
|
+
// Policy command keys are stored normalized (trimmed + lowercased) by normalizeCommandAuthorizationPolicy,
|
|
82
|
+
// so the lookup MUST normalize the probe too. A raw mixed-case name (e.g. "Gate-Override") otherwise misses
|
|
83
|
+
// its restrictive override and silently falls back to the permissive default — under-stating the restriction.
|
|
84
|
+
const key = normalizeCommandName(commandName);
|
|
85
|
+
const commandRoles = Object.hasOwn(normalized.commands, key) ? normalized.commands[key] : undefined;
|
|
86
|
+
return dedupeRoles(commandRoles ?? normalized.default);
|
|
87
|
+
}
|
|
88
|
+
function normalizeCommandName(commandName) {
|
|
89
|
+
return commandName.trim().toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
export function commandAuthorizationNeedsMinerDetection(args) {
|
|
92
|
+
const allowedRoles = commandAuthorizationAllowedRoles(args.policy, args.commandName);
|
|
93
|
+
if (!allowedRoles.includes("confirmed_miner"))
|
|
94
|
+
return false;
|
|
95
|
+
if (!isSameLogin(args.commenterLogin, args.pullRequestAuthorLogin))
|
|
96
|
+
return false;
|
|
97
|
+
const rolesWithoutMiner = actorRoles({ ...args, minerStatus: undefined });
|
|
98
|
+
return !rolesWithoutMiner.some((role) => allowedRoles.includes(role));
|
|
99
|
+
}
|
|
100
|
+
export function evaluateCommandAuthorization(args) {
|
|
101
|
+
const allowedRoles = commandAuthorizationAllowedRoles(args.policy, args.commandName);
|
|
102
|
+
const roles = actorRoles(args);
|
|
103
|
+
const matchedRole = roles.find((role) => allowedRoles.includes(role)) ?? null;
|
|
104
|
+
const prAuthorGatedCommand = matchedRole === "pr_author" && PR_AUTHOR_RATE_LIMITED_COMMANDS.has(normalizeCommandName(args.commandName));
|
|
105
|
+
if (prAuthorGatedCommand && args.commandRateLimitPolicy !== "hold") {
|
|
106
|
+
return { authorized: false, reason: "pr_author_requires_rate_limiting", actorKind: "author", matchedRole: null, allowedRoles };
|
|
107
|
+
}
|
|
108
|
+
if (prAuthorGatedCommand && args.pullRequestOpenAndNotDraft !== true) {
|
|
109
|
+
return { authorized: false, reason: "pr_author_requires_open_pr", actorKind: "author", matchedRole: null, allowedRoles };
|
|
110
|
+
}
|
|
111
|
+
if (matchedRole) {
|
|
112
|
+
return {
|
|
113
|
+
authorized: true,
|
|
114
|
+
reason: authorizationReason(matchedRole),
|
|
115
|
+
actorKind: matchedRole === "maintainer" || matchedRole === "collaborator" ? "maintainer" : "author",
|
|
116
|
+
matchedRole,
|
|
117
|
+
allowedRoles,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const ownPrAuthor = isSameLogin(args.commenterLogin, args.pullRequestAuthorLogin);
|
|
121
|
+
if (ownPrAuthor && allowedRoles.includes("confirmed_miner")) {
|
|
122
|
+
return {
|
|
123
|
+
authorized: false,
|
|
124
|
+
reason: args.minerStatus === "unavailable" || !args.minerStatus ? "miner_detection_unavailable" : "pr_author_not_confirmed_miner",
|
|
125
|
+
actorKind: "author",
|
|
126
|
+
matchedRole: null,
|
|
127
|
+
allowedRoles,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (ownPrAuthor && MAINTAINER_ONLY_DEFAULT_COMMANDS.has(normalizeCommandName(args.commandName)) && allowedRoles.every((role) => role === "maintainer" || role === "collaborator")) {
|
|
131
|
+
return { authorized: false, reason: "maintainer_command_requires_maintainer", actorKind: "author", matchedRole: null, allowedRoles };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
authorized: false,
|
|
135
|
+
reason: ownPrAuthor ? "command_policy_denied" : "not_maintainer_or_pr_author",
|
|
136
|
+
actorKind: ownPrAuthor ? "author" : "none",
|
|
137
|
+
matchedRole: null,
|
|
138
|
+
allowedRoles,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export function summarizeCommandAuthorizationPolicy(policy) {
|
|
142
|
+
const normalized = normalizeCommandAuthorizationPolicy(policy).policy;
|
|
143
|
+
return {
|
|
144
|
+
defaultAllowed: normalized.default,
|
|
145
|
+
commandOverrides: Object.entries(normalized.commands)
|
|
146
|
+
.map(([command, allowedRoles]) => ({ command, allowedRoles }))
|
|
147
|
+
.sort((left, right) => left.command.localeCompare(right.command)),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function normalizeCommandRoleList(commandName, roles, warnings) {
|
|
151
|
+
if (!MAINTAINER_ONLY_DEFAULT_COMMANDS.has(commandName))
|
|
152
|
+
return roles;
|
|
153
|
+
// #5084: a role also survives the clamp if it's explicitly part of THIS command's own shipped default
|
|
154
|
+
// (chat's own default now includes pr_author) -- so a maintainer restating or narrowing a command's own
|
|
155
|
+
// default via yml never gets silently mangled, while every OTHER maintainer-only command whose own default
|
|
156
|
+
// excludes pr_author still can't have it added via override (this is a per-command union, not a blanket
|
|
157
|
+
// relaxation: the clamp still can't be conjured up on generate-tests/pause/etc.).
|
|
158
|
+
/* v8 ignore next -- defensive: MAINTAINER_ONLY_DEFAULT_COMMANDS is derived from these keys, so a maintainer-only command always resolves a default list. */
|
|
159
|
+
const commandOwnDefaultRoles = DEFAULT_COMMAND_AUTHORIZATION_POLICY.commands[commandName] ?? [];
|
|
160
|
+
const allowedClampRoles = new Set([...MAINTAINER_COMMAND_AUTHORIZATION_ROLES, ...commandOwnDefaultRoles]);
|
|
161
|
+
const maintainerRoles = roles.filter((role) => allowedClampRoles.has(role));
|
|
162
|
+
if (maintainerRoles.length === roles.length)
|
|
163
|
+
return roles;
|
|
164
|
+
warnings.push(`Ignored author command authorization roles for maintainer-only command: ${commandName}.`);
|
|
165
|
+
if (maintainerRoles.length > 0)
|
|
166
|
+
return dedupeRoles(maintainerRoles);
|
|
167
|
+
const defaultRoles = DEFAULT_COMMAND_AUTHORIZATION_POLICY.commands[commandName];
|
|
168
|
+
/* v8 ignore next -- defensive: MAINTAINER_ONLY_DEFAULT_COMMANDS is derived from these keys, so a maintainer-only command always resolves a default list. */
|
|
169
|
+
return [...(defaultRoles ?? ["maintainer", "collaborator"])];
|
|
170
|
+
}
|
|
171
|
+
function actorRoles(args) {
|
|
172
|
+
const roles = [];
|
|
173
|
+
if (args.commenterAssociation === "OWNER" || args.commenterAssociation === "MEMBER")
|
|
174
|
+
roles.push("maintainer");
|
|
175
|
+
if (args.commenterAssociation === "COLLABORATOR")
|
|
176
|
+
roles.push("collaborator");
|
|
177
|
+
if (isSameLogin(args.commenterLogin, args.pullRequestAuthorLogin)) {
|
|
178
|
+
roles.push("pr_author");
|
|
179
|
+
if (args.minerStatus === "confirmed")
|
|
180
|
+
roles.push("confirmed_miner");
|
|
181
|
+
}
|
|
182
|
+
return roles;
|
|
183
|
+
}
|
|
184
|
+
function normalizeRoleList(input, fallback, label, warnings) {
|
|
185
|
+
if (!Array.isArray(input)) {
|
|
186
|
+
if (input !== undefined)
|
|
187
|
+
warnings.push(`commandAuthorization.${label} must be an array of roles; using fallback roles.`);
|
|
188
|
+
return dedupeRoles(fallback);
|
|
189
|
+
}
|
|
190
|
+
const roles = input.filter((role) => {
|
|
191
|
+
const valid = typeof role === "string" && COMMAND_AUTHORIZATION_ROLES.has(role);
|
|
192
|
+
if (!valid)
|
|
193
|
+
warnings.push(`Ignored invalid command authorization role for ${label}.`);
|
|
194
|
+
return valid;
|
|
195
|
+
});
|
|
196
|
+
if (roles.length === 0) {
|
|
197
|
+
warnings.push(`commandAuthorization.${label} had no valid roles; using fallback roles.`);
|
|
198
|
+
return dedupeRoles(fallback);
|
|
199
|
+
}
|
|
200
|
+
return dedupeRoles(roles);
|
|
201
|
+
}
|
|
202
|
+
function dedupeRoles(roles) {
|
|
203
|
+
return [...new Set(roles)];
|
|
204
|
+
}
|
|
205
|
+
function clonePolicy(policy) {
|
|
206
|
+
return { default: [...policy.default], commands: Object.fromEntries(Object.entries(policy.commands).map(([command, roles]) => [command, [...roles]])) };
|
|
207
|
+
}
|
|
208
|
+
function authorizationReason(role) {
|
|
209
|
+
if (role === "maintainer")
|
|
210
|
+
return "maintainer_invocation";
|
|
211
|
+
if (role === "collaborator")
|
|
212
|
+
return "collaborator_invocation";
|
|
213
|
+
if (role === "confirmed_miner")
|
|
214
|
+
return "confirmed_miner_pr_author";
|
|
215
|
+
return "allowed_pr_author";
|
|
216
|
+
}
|
|
217
|
+
function isRecord(value) {
|
|
218
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
219
|
+
}
|
|
220
|
+
function isSameLogin(left, right) {
|
|
221
|
+
return Boolean(left && right && left.toLowerCase() === right.toLowerCase());
|
|
222
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ContributorBlacklistEntry } from "../types/manifest-deps-types.js";
|
|
2
|
+
/** Normalize a raw blacklist value (DB JSON or `.gittensory.yml`) into validated, de-duplicated entries. Never
|
|
3
|
+
* throws: malformed entries are dropped with a warning. De-dup is by case-insensitive login (the FIRST wins, so
|
|
4
|
+
* its richer metadata is kept). */
|
|
5
|
+
export declare function normalizeContributorBlacklist(input: unknown): {
|
|
6
|
+
entries: ContributorBlacklistEntry[];
|
|
7
|
+
warnings: string[];
|
|
8
|
+
};
|
|
9
|
+
/** The blacklist entry matching `login` (case-insensitive), or null. Tolerates an absent list (treated as empty)
|
|
10
|
+
* so callers can pass the optional `settings.contributorBlacklist` directly. */
|
|
11
|
+
export declare function findBlacklistEntry(login: string | null | undefined, entries: ContributorBlacklistEntry[] | undefined): ContributorBlacklistEntry | null;
|
|
12
|
+
/** True iff `login` is on the resolved blacklist. */
|
|
13
|
+
export declare function isAuthorBlacklisted(login: string | null | undefined, entries: ContributorBlacklistEntry[] | undefined): boolean;
|
|
14
|
+
/** Union multiple blacklist sources (e.g. the shared/global list + the per-repo list) by case-insensitive login.
|
|
15
|
+
* A login on ANY source is blocked; the FIRST source's entry wins on a duplicate so earlier (more authoritative)
|
|
16
|
+
* metadata is preserved. Already-normalized inputs in, de-duplicated entries out. */
|
|
17
|
+
export declare function mergeContributorBlacklists(...lists: ContributorBlacklistEntry[][]): ContributorBlacklistEntry[];
|