diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json
index 47177df11c..1e1f1e91b8 100644
--- a/apps/loopover-ui/public/openapi.json
+++ b/apps/loopover-ui/public/openapi.json
@@ -16573,6 +16573,17 @@
"in": "cookie",
"name": "loopover_session",
"description": "HttpOnly browser session cookie set by GitHub web OAuth."
+ },
+ "OrbBearer": {
+ "type": "http",
+ "scheme": "bearer",
+ "description": "ORB-issued instance token, minted by POST /v1/orb/token and presented by a self-hosted ORB instance on the relay endpoints. Not a LoopOver API token."
+ },
+ "OrbWebhookSignature": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "x-loopover-signature",
+ "description": "HMAC signature over the raw request body, verified against the instance's shared secret. The webhook carries no bearer token."
}
}
},
@@ -16591,7 +16602,11 @@
}
}
}
- }
+ },
+ "operationId": "getHealth",
+ "tags": [
+ "Meta"
+ ]
}
},
"/v1/mcp/compatibility": {
@@ -16608,7 +16623,11 @@
}
}
}
- }
+ },
+ "operationId": "getMcpCompatibility",
+ "tags": [
+ "MCP"
+ ]
}
},
"/v1/public/stats": {
@@ -16631,7 +16650,11 @@
"503": {
"description": "Public stats are temporarily unavailable"
}
- }
+ },
+ "operationId": "listPublicStats",
+ "tags": [
+ "Public"
+ ]
}
},
"/v1/public/github/repos/{owner}/{repo}/stats": {
@@ -16672,7 +16695,11 @@
"503": {
"description": "GitHub repository stats are unavailable"
}
- }
+ },
+ "operationId": "listPublicGithubReposByOwnerByRepoStats",
+ "tags": [
+ "Public"
+ ]
}
},
"/v1/public/repos/{owner}/{repo}/quality": {
@@ -16713,7 +16740,11 @@
"503": {
"description": "Public quality metrics are temporarily unavailable"
}
- }
+ },
+ "operationId": "getPublicReposByOwnerByRepoQuality",
+ "tags": [
+ "Public"
+ ]
}
},
"/v1/registry/snapshot": {
@@ -16738,6 +16769,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getRegistrySnapshot",
+ "tags": [
+ "Registry"
]
}
},
@@ -16763,6 +16798,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listRegistryChanges",
+ "tags": [
+ "Registry"
]
}
},
@@ -16788,6 +16827,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getScoringModel",
+ "tags": [
+ "Scoring"
]
}
},
@@ -16813,6 +16856,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getFindingTaxonomy",
+ "tags": [
+ "API"
]
}
},
@@ -16838,6 +16885,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listEnrichmentAnalyzers",
+ "tags": [
+ "API"
]
}
},
@@ -16863,6 +16914,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listUpstreamStatus",
+ "tags": [
+ "Registry"
]
}
},
@@ -16891,6 +16946,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getUpstreamRuleset",
+ "tags": [
+ "Registry"
]
}
},
@@ -16935,6 +16994,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getUpstreamDrift",
+ "tags": [
+ "Registry"
]
}
},
@@ -16963,6 +17026,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postScoringPreview",
+ "tags": [
+ "Scoring"
]
}
},
@@ -16991,6 +17058,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postScoringEligibilityPlan",
+ "tags": [
+ "Scoring"
]
}
},
@@ -17019,6 +17090,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postScoringExplainBreakdown",
+ "tags": [
+ "Scoring"
]
}
},
@@ -17044,6 +17119,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listSyncStatus",
+ "tags": [
+ "API"
]
}
},
@@ -17069,6 +17148,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReadiness",
+ "tags": [
+ "API"
]
}
},
@@ -17115,6 +17198,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listInstallations",
+ "tags": [
+ "API"
]
}
},
@@ -17153,6 +17240,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getInstallationsByIdHealth",
+ "tags": [
+ "API"
]
}
},
@@ -17191,6 +17282,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getInstallationsByIdRepair",
+ "tags": [
+ "API"
]
}
},
@@ -17229,6 +17324,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postInstallationsByIdRepairRefresh",
+ "tags": [
+ "API"
]
}
},
@@ -17369,6 +17468,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppNotificationModel",
+ "tags": [
+ "Control panel"
]
}
},
@@ -17397,6 +17500,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listRepos",
+ "tags": [
+ "API"
]
}
},
@@ -17443,6 +17550,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepo",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17486,6 +17597,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoIntelligence",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17532,6 +17647,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoIssueQuality",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17581,6 +17700,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoGateConfigEffective",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17618,6 +17741,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopEvaluateEscalation",
+ "tags": [
+ "Loop"
]
}
},
@@ -17655,6 +17782,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopResultsPayload",
+ "tags": [
+ "Loop"
]
}
},
@@ -17692,6 +17823,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopProgressSnapshot",
+ "tags": [
+ "Loop"
]
}
},
@@ -17729,6 +17864,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopIntakeIdea",
+ "tags": [
+ "Loop"
]
}
},
@@ -17766,6 +17905,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopPlanIdeaClaims",
+ "tags": [
+ "Loop"
]
}
},
@@ -17815,6 +17958,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoLiveGateThresholds",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17864,6 +18011,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoMaintainerNoise",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17913,6 +18064,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoAmsMinerCohort",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17972,6 +18127,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoGatePrecision",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18031,6 +18190,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoOutcomeCalibration",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18080,6 +18243,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoActivationPreview",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18126,6 +18293,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoOutcomePatterns",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18169,6 +18340,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoRegistrationReadiness",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18212,6 +18387,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoGittensorConfigRecommendation",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18261,6 +18440,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoFocusManifest",
+ "tags": [
+ "Repositories"
]
},
"put": {
@@ -18311,6 +18494,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "putReposByOwnerByRepoFocusManifest",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18360,6 +18547,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoFocusManifestRefresh",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18507,6 +18698,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoAgentAuditFeed",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18632,6 +18827,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoPullsByNumberIncidentReports",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18743,6 +18942,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppIncidentReports",
+ "tags": [
+ "Control panel"
]
}
},
@@ -18774,6 +18977,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppSelfDogfoodRegistrationPack",
+ "tags": [
+ "Control panel"
]
}
},
@@ -18823,6 +19030,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoSelfDogfoodRegistrationPack",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18875,6 +19086,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoOnboardingPackPreview",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18927,6 +19142,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoContributorIssueDraftsGenerate",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18979,6 +19198,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoIssuePlanDraftsGenerate",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19022,6 +19245,57 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoSettings",
+ "tags": [
+ "Repositories"
+ ]
+ },
+ "put": {
+ "operationId": "updateRepoSettings",
+ "tags": [
+ "Repositories"
+ ],
+ "summary": "Replace a repo's LoopOver settings",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Settings updated"
+ },
+ "400": {
+ "description": "Malformed settings"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
]
}
},
@@ -19065,6 +19339,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoAutomationState",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19108,6 +19386,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoRepoDocsRefresh",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19154,6 +19436,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoSettingsPreview",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19205,6 +19491,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoPullsByNumberMaintainerPacket",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19256,6 +19546,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoPullsByNumberReviewability",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19327,6 +19621,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoPullsByNumberAiReviewFindings",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19362,6 +19660,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginProfile",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19407,6 +19709,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginDecisionPack",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19442,6 +19748,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginOpenPrMonitor",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19488,6 +19798,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listContributorsByLoginPrOutcomes",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19523,6 +19837,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listContributorsByLoginNotifications",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19578,6 +19896,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postContributorsByLoginNotificationsRead",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19639,6 +19961,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginReposByOwnerByRepoDecision",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19667,6 +19993,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postPreflightPr",
+ "tags": [
+ "Preflight"
]
}
},
@@ -19698,6 +20028,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postPreflightReviewRisk",
+ "tags": [
+ "Preflight"
]
}
},
@@ -19726,6 +20060,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postPreflightLocalDiff",
+ "tags": [
+ "Preflight"
]
}
},
@@ -19757,6 +20095,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLocalBranchAnalysis",
+ "tags": [
+ "Local"
]
}
},
@@ -19788,6 +20130,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentRuns",
+ "tags": [
+ "Agent automation"
]
},
"get": {
@@ -19851,6 +20197,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listAgentRuns",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -19889,6 +20239,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAgentRunsById",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -19930,6 +20284,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentPlanNextWork",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -19971,6 +20329,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentPreflightBranch",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -20012,6 +20374,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentPreparePrPacket",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -20053,6 +20419,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentExplainBlockers",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -20081,6 +20451,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listBounties",
+ "tags": [
+ "Bounties"
]
}
},
@@ -20119,6 +20493,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getBountiesByIdAdvisory",
+ "tags": [
+ "Bounties"
]
}
},
@@ -20157,6 +20535,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getBountiesByIdLifecycle",
+ "tags": [
+ "Bounties"
]
}
},
@@ -20170,7 +20552,11 @@
"401": {
"description": "Invalid webhook signature"
}
- }
+ },
+ "operationId": "postGithubWebhook",
+ "tags": [
+ "Webhooks"
+ ]
}
},
"/v1/public/decision-ledger/verify": {
@@ -20184,13 +20570,9 @@
"description": "First break found: sequence_gap | predecessor_mismatch | row_hash_mismatch | missing_record | content_mismatch | short_tail (a record newer than the verified tip has no chain entry — the truncated-tail signature) | unchained_record (an INTERIOR record has no chain entry — the failed-append signature). Records younger than the 5-minute append grace window are not reported: the record insert and its chain append are two writes moments apart, and a verify landing between them is not evidence of tampering."
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "getPublicDecisionLedgerVerify",
+ "tags": [
+ "Public"
]
}
},
@@ -20218,13 +20600,9 @@
"description": "No ledger row at that seq (never appended -- distinct from a row with empty fields)"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "getPublicDecisionLedgerRowBySeq",
+ "tags": [
+ "Public"
]
}
},
@@ -20236,13 +20614,9 @@
"description": "{ keys: [{ keyId, publicKeySpki, notBefore, notAfter }], currentKeyId } — retired keys are retained so anchors signed under them stay verifiable; currentKeyId is null when unconfigured or the rotation state is ambiguous"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "getPublicDecisionLedgerAnchorKey",
+ "tags": [
+ "Public"
]
}
},
@@ -20285,13 +20659,9 @@
"description": "{ anchors: [{ id, seq, rowHash, keyId, backend, backendRef, status, error, createdAt }], nextBefore } — a failed attempt is returned identically to a successful one, never filtered out or reshaped"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "listPublicDecisionLedgerAnchors",
+ "tags": [
+ "Public"
]
}
},
@@ -20335,13 +20705,9 @@
"description": "No decision record persisted yet for this PR"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "getPublicDecisionRecordsByOwnerByRepoByPull",
+ "tags": [
+ "Public"
]
}
},
@@ -20374,13 +20740,9 @@
"description": "Public stats disabled (same flag as /v1/public/stats)"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "listPublicEvalScores",
+ "tags": [
+ "Public"
]
}
},
@@ -20395,13 +20757,9 @@
"description": "Malformed JSON or invalid payload shape"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "postOrbIngest",
+ "tags": [
+ "ORB"
]
}
},
@@ -20415,7 +20773,11 @@
"503": {
"description": "GitHub OAuth app secret is not configured"
}
- }
+ },
+ "operationId": "getAuthGithubStart",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/callback": {
@@ -20425,7 +20787,11 @@
"302": {
"description": "Completes GitHub web OAuth and redirects to the app"
}
- }
+ },
+ "operationId": "getAuthGithubCallback",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/device/start": {
@@ -20447,7 +20813,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthGithubDeviceStart",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/device/poll": {
@@ -20469,7 +20839,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthGithubDevicePoll",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/session": {
@@ -20491,7 +20865,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthGithubSession",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/logout": {
@@ -20513,7 +20891,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthLogout",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/session": {
@@ -20523,7 +20905,11 @@
"200": {
"description": "Current auth session, or signed_out when no app session is present"
}
- }
+ },
+ "operationId": "getAuthSession",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/token": {
@@ -20558,10 +20944,11 @@
"description": "Rate limited"
}
},
+ "operationId": "postAuthGithubToken",
+ "tags": [
+ "Auth"
+ ],
"security": [
- {
- "LoopOverBearer": []
- },
{
"LoopOverSessionCookie": []
}
@@ -20599,6 +20986,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppOverview",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20630,6 +21021,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppRoles",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20661,6 +21056,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppMinerDashboard",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20692,6 +21091,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppMaintainerDashboard",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20723,6 +21126,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppOperatorDashboard",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20754,6 +21161,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppCommands",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20785,6 +21196,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppCommandsUsefulness",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20816,6 +21231,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppDigest",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20847,6 +21266,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppAnalyticsDailyRollups",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20878,6 +21301,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppAnalyticsMcpCompatibility",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20933,6 +21360,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppSelfhostQueueDeadByIdReplay",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20988,6 +21419,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteAppSelfhostQueueDeadById",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21025,6 +21460,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteAppSelfhostQueueDead",
+ "tags": [
+ "Control panel"
]
},
"get": {
@@ -21085,6 +21524,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppSelfhostQueueDead",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21165,6 +21608,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppAnalyticsWeeklyValueReport",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21260,6 +21707,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppSkippedPrAudit",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21297,6 +21748,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppCommandsPreview",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21344,6 +21799,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppCommandsFeedback",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21391,15 +21850,19 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppDigestSubscriptions",
+ "tags": [
+ "Control panel"
]
}
},
"/v1/internal/jobs/refresh-registry": {
"post": {
- "summary": "Queue a registry refresh job",
+ "summary": "Queue a job to refresh the Gittensor registry snapshot",
"responses": {
"202": {
- "description": "Registry refresh queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21408,19 +21871,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueRefreshRegistryJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/backfill-registered-repos": {
"post": {
- "summary": "Queue a registered-repository backfill job",
+ "summary": "Queue a job to backfill registered repository records",
"responses": {
"202": {
- "description": "Registered repo backfill queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21429,22 +21894,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueBackfillRegisteredReposJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/backfill-repo-segment": {
"post": {
- "summary": "Queue a repository segment backfill job",
+ "summary": "Queue a job to backfill repository segment assignments",
"responses": {
"202": {
- "description": "Repository segment backfill queued"
- },
- "400": {
- "description": "Invalid segment request"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21453,22 +21917,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueBackfillRepoSegmentJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/backfill-pr-details": {
"post": {
- "summary": "Queue an open pull request detail backfill job",
+ "summary": "Queue a job to backfill pull-request detail rows",
"responses": {
"202": {
- "description": "Open PR detail backfill queued"
- },
- "400": {
- "description": "Invalid PR detail backfill request"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21477,22 +21940,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueBackfillPrDetailsJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/generate-review-recap": {
"post": {
- "summary": "Queue a maintainer review recap digest job",
+ "summary": "Queue a job to generate the maintainer review recap",
"responses": {
"202": {
- "description": "Maintainer review recap digest queued (#1963)"
- },
- "400": {
- "description": "Missing repoFullName"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21501,19 +21963,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueGenerateReviewRecapJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/refresh-scoring-model": {
"post": {
- "summary": "Queue a scoring model refresh job",
+ "summary": "Queue a job to refresh the active scoring model",
"responses": {
"202": {
- "description": "Internal job queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21522,19 +21986,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueRefreshScoringModelJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/refresh-upstream-drift": {
"post": {
- "summary": "Queue an upstream drift refresh job",
+ "summary": "Queue a job to recompute upstream ruleset drift",
"responses": {
"202": {
- "description": "Internal job queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21543,19 +22009,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueRefreshUpstreamDriftJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/file-upstream-drift-issues": {
"post": {
- "summary": "Queue a job that files upstream drift issues",
+ "summary": "Queue a job to file issues for open upstream drift",
"responses": {
"202": {
- "description": "Internal job queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21564,10 +22032,12 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueFileUpstreamDriftIssuesJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
@@ -21582,22 +22052,18 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "postInternalJobsBuildContributorEvidence",
+ "tags": [
+ "Internal"
]
}
},
"/v1/internal/jobs/build-contributor-decision-packs": {
"post": {
- "summary": "Queue a contributor decision pack build job",
+ "summary": "Queue a job to rebuild contributor decision packs",
"responses": {
"202": {
- "description": "Internal job queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21606,10 +22072,12 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueBuildContributorDecisionPacksJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
@@ -21624,22 +22092,18 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "postInternalJobsBuildBurdenForecasts",
+ "tags": [
+ "Internal"
]
}
},
"/v1/internal/jobs/generate-signal-snapshots": {
"post": {
- "summary": "Queue a signal snapshot generation job",
+ "summary": "Queue a job to generate signal snapshots",
"responses": {
"202": {
- "description": "Internal job queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21648,19 +22112,21 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueGenerateSignalSnapshotsJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
"/v1/internal/jobs/generate-weekly-value-report": {
"post": {
- "summary": "Queue a weekly value report job",
+ "summary": "Queue a job to generate the weekly value report",
"responses": {
"202": {
- "description": "Internal job queued"
+ "description": "Job queued"
},
"401": {
"description": "Invalid internal token"
@@ -21669,10 +22135,12 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueGenerateWeeklyValueReportJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
@@ -21687,13 +22155,9 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "postInternalJobsRepairDataFidelity",
+ "tags": [
+ "Internal"
]
}
},
@@ -21708,13 +22172,9 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "postInternalBountiesImport",
+ "tags": [
+ "Internal"
]
}
},
@@ -21761,6 +22221,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoAgentPendingActions",
+ "tags": [
+ "Repositories"
]
},
"post": {
@@ -21820,6 +22284,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoAgentPendingActions",
+ "tags": [
+ "Repositories"
]
}
},
@@ -21895,6 +22363,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoAgentPendingActionsByIdByDecision",
+ "tags": [
+ "Repositories"
]
}
},
@@ -21932,6 +22404,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintPrText",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -21969,6 +22445,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintSlopRisk",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22006,6 +22486,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintImprovementPotential",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22043,6 +22527,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintOpenPrPressure",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22080,6 +22568,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintBoundaryTests",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22117,6 +22609,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintTestEvidence",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22154,6 +22650,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintIssueSlop",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22191,6 +22691,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postValidateFocusManifest",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22247,6 +22751,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoSelftuneOverridesAudit",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22317,6 +22825,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteReposByOwnerByRepoSelftuneOverrides",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22378,6 +22890,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoValidateLinkedIssue",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22439,6 +22955,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoCheckBeforeStart",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22540,6 +23060,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postOpportunitiesFind",
+ "tags": [
+ "Discovery"
]
}
},
@@ -22619,6 +23143,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postIssueRagRetrieve",
+ "tags": [
+ "Discovery"
]
}
},
@@ -22660,6 +23188,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listContributorsByLoginWatches",
+ "tags": [
+ "Contributors"
]
},
"post": {
@@ -22711,6 +23243,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postContributorsByLoginWatches",
+ "tags": [
+ "Contributors"
]
},
"delete": {
@@ -22762,6 +23298,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteContributorsByLoginWatches",
+ "tags": [
+ "Contributors"
]
}
},
@@ -22846,13 +23386,9 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "getInternalProviderCredentialsByProvider",
+ "tags": [
+ "Internal"
]
},
"post": {
@@ -22912,13 +23448,9 @@
"description": "TOKEN_ENCRYPTION_SECRET is not configured, so the credential cannot be stored encrypted"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
+ "operationId": "postInternalProviderCredentialsByProvider",
+ "tags": [
+ "Internal"
]
},
"delete": {
@@ -22966,14 +23498,2579 @@
"description": "Invalid internal token"
}
},
+ "operationId": "deleteInternalProviderCredentialsByProvider",
+ "tags": [
+ "Internal"
+ ]
+ }
+ },
+ "/v1/orb/token": {
+ "post": {
+ "operationId": "mintOrbInstanceToken",
+ "tags": [
+ "ORB"
+ ],
+ "summary": "Mint an ORB instance token",
+ "security": [
+ {
+ "OrbBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Instance token issued"
+ },
+ "400": {
+ "description": "Malformed enrollment request"
+ },
+ "401": {
+ "description": "Missing or invalid ORB instance token"
+ }
+ }
+ }
+ },
+ "/v1/orb/relay": {
+ "post": {
+ "operationId": "relayOrbEvent",
+ "tags": [
+ "ORB"
+ ],
+ "summary": "Relay one ORB event to the hosted control plane",
+ "security": [
+ {
+ "OrbBearer": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Event accepted for processing"
+ },
+ "400": {
+ "description": "Malformed relay payload"
+ },
+ "401": {
+ "description": "Missing or invalid ORB instance token"
+ }
+ }
+ }
+ },
+ "/v1/orb/relay/register": {
+ "post": {
+ "operationId": "registerOrbRelayInstance",
+ "tags": [
+ "ORB"
+ ],
+ "summary": "Register a self-hosted ORB instance with the relay",
+ "security": [
+ {
+ "OrbBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Instance registered"
+ },
+ "400": {
+ "description": "Malformed registration"
+ },
+ "401": {
+ "description": "Missing or invalid ORB instance token"
+ }
+ }
+ }
+ },
+ "/v1/orb/relay/pull": {
+ "post": {
+ "operationId": "pullOrbRelayWork",
+ "tags": [
+ "ORB"
+ ],
+ "summary": "Pull queued relay work for an ORB instance",
+ "security": [
+ {
+ "OrbBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Queued work, possibly empty"
+ },
+ "401": {
+ "description": "Missing or invalid ORB instance token"
+ }
+ }
+ }
+ },
+ "/v1/orb/webhook": {
+ "post": {
+ "operationId": "receiveOrbWebhook",
+ "tags": [
+ "ORB"
+ ],
+ "summary": "Receive a signed ORB webhook",
+ "security": [
+ {
+ "OrbWebhookSignature": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Webhook accepted"
+ },
+ "400": {
+ "description": "Malformed webhook body"
+ },
+ "401": {
+ "description": "Signature verification failed"
+ }
+ }
+ }
+ },
+ "/v1/orb/oauth/callback": {
+ "get": {
+ "operationId": "completeOrbOauth",
+ "tags": [
+ "ORB"
+ ],
+ "summary": "Complete the ORB GitHub OAuth flow",
+ "responses": {
+ "302": {
+ "description": "Redirect back to the ORB instance"
+ },
+ "400": {
+ "description": "Missing or invalid OAuth code"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/internal/orb/instances": {
+ "get": {
+ "operationId": "listOrbInstances",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "List registered ORB instances",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Registered instances"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/orb/instances/register": {
+ "post": {
+ "operationId": "registerOrbInstanceInternal",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "Register an ORB instance from the control plane",
"security": [
{
"LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Instance registered"
+ },
+ "400": {
+ "description": "Malformed registration"
},
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/orb/installations": {
+ "get": {
+ "operationId": "listOrbInstallations",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "List GitHub App installations known to the ORB control plane",
+ "security": [
{
- "LoopOverSessionCookie": []
+ "LoopOverBearer": []
}
- ]
+ ],
+ "responses": {
+ "200": {
+ "description": "Installations"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/orb/installations/register": {
+ "post": {
+ "operationId": "registerOrbInstallation",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "Register a GitHub App installation with the ORB control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Installation registered"
+ },
+ "400": {
+ "description": "Malformed registration"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/orb/installations/backfill": {
+ "post": {
+ "operationId": "backfillOrbInstallations",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "Backfill ORB installation records from GitHub",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Backfill queued"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/orb/enrollments": {
+ "post": {
+ "operationId": "createOrbEnrollment",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "Create an ORB instance enrollment",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Enrollment created"
+ },
+ "400": {
+ "description": "Malformed enrollment"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/orb/enrollments/{enrollId}/revoke": {
+ "post": {
+ "operationId": "revokeOrbEnrollment",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "Revoke an ORB instance enrollment",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Enrollment revoked"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ },
+ "404": {
+ "description": "No such enrollment"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "enrollId",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/app/installations": {
+ "get": {
+ "operationId": "listAppInstallations",
+ "tags": [
+ "Control panel"
+ ],
+ "summary": "List the GitHub App installations the caller can administer",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Installations"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ }
+ }
+ },
+ "/v1/app/installations/{id}/health": {
+ "get": {
+ "operationId": "getAppInstallationHealth",
+ "tags": [
+ "Control panel"
+ ],
+ "summary": "Return one installation's health summary",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Installation health"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ },
+ "404": {
+ "description": "No such installation"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "id",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/app/installations/{id}/repair": {
+ "get": {
+ "operationId": "getAppInstallationRepair",
+ "tags": [
+ "Control panel"
+ ],
+ "summary": "Return the repair plan for an unhealthy installation",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Repair plan"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ },
+ "404": {
+ "description": "No such installation"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "id",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/app/installations/{id}/repair/refresh": {
+ "post": {
+ "operationId": "refreshAppInstallationRepair",
+ "tags": [
+ "Control panel"
+ ],
+ "summary": "Recompute an installation's repair plan",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Repair plan recomputed"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ },
+ "404": {
+ "description": "No such installation"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "id",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/app/installations/{id}/agent/bulk-settings": {
+ "put": {
+ "operationId": "bulkUpdateInstallationAgentSettings",
+ "tags": [
+ "Control panel",
+ "Agent automation"
+ ],
+ "summary": "Apply agent settings across every repo in an installation",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Settings applied"
+ },
+ "400": {
+ "description": "Malformed settings"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ },
+ "404": {
+ "description": "No such installation"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "id",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/app/kill-switch": {
+ "get": {
+ "operationId": "getFleetKillSwitch",
+ "tags": [
+ "Control panel",
+ "Operations"
+ ],
+ "summary": "Read the fleet-wide agent kill switch",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Kill-switch state"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ }
+ },
+ "post": {
+ "operationId": "setFleetKillSwitch",
+ "tags": [
+ "Control panel",
+ "Operations"
+ ],
+ "summary": "Set the fleet-wide agent kill switch",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Kill switch updated"
+ },
+ "400": {
+ "description": "Malformed request"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ }
+ }
+ },
+ "/v1/app/fleet/config-push": {
+ "post": {
+ "operationId": "pushFleetConfig",
+ "tags": [
+ "Control panel",
+ "Operations"
+ ],
+ "summary": "Push a configuration change across the fleet",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Config push queued"
+ },
+ "400": {
+ "description": "Malformed push"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ }
+ }
+ },
+ "/v1/app/miner-dashboard/refresh": {
+ "post": {
+ "operationId": "refreshMinerDashboard",
+ "tags": [
+ "Control panel"
+ ],
+ "summary": "Recompute the miner dashboard aggregates",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Refresh queued"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ }
+ }
+ },
+ "/v1/repos/{owner}/{repo}/ai-key": {
+ "get": {
+ "operationId": "getAiKey",
+ "tags": [
+ "Repositories",
+ "Bring your own key"
+ ],
+ "summary": "Report whether a AI provider key is configured for this repo (never the key itself)",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key presence and metadata"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ },
+ "404": {
+ "description": "Repo not registered"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "setAiKey",
+ "tags": [
+ "Repositories",
+ "Bring your own key"
+ ],
+ "summary": "Store a AI provider key for this repo",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key stored"
+ },
+ "400": {
+ "description": "Malformed key"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "deleteAiKey",
+ "tags": [
+ "Repositories",
+ "Bring your own key"
+ ],
+ "summary": "Remove this repo's AI provider key",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key removed"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/internal/repos/{owner}/{repo}/ai-key": {
+ "get": {
+ "operationId": "getAiKeyInternal",
+ "tags": [
+ "Bring your own key",
+ "Internal"
+ ],
+ "summary": "Read this repo's AI provider key state from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key presence and metadata"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "setAiKeyInternal",
+ "tags": [
+ "Bring your own key",
+ "Internal"
+ ],
+ "summary": "Store this repo's AI provider key from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key stored"
+ },
+ "400": {
+ "description": "Malformed key"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "deleteAiKeyInternal",
+ "tags": [
+ "Bring your own key",
+ "Internal"
+ ],
+ "summary": "Remove this repo's AI provider key from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key removed"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/repos/{owner}/{repo}/linear-key": {
+ "get": {
+ "operationId": "getLinearKey",
+ "tags": [
+ "Repositories",
+ "Bring your own key"
+ ],
+ "summary": "Report whether a Linear key is configured for this repo (never the key itself)",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key presence and metadata"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ },
+ "404": {
+ "description": "Repo not registered"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "setLinearKey",
+ "tags": [
+ "Repositories",
+ "Bring your own key"
+ ],
+ "summary": "Store a Linear key for this repo",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key stored"
+ },
+ "400": {
+ "description": "Malformed key"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "deleteLinearKey",
+ "tags": [
+ "Repositories",
+ "Bring your own key"
+ ],
+ "summary": "Remove this repo's Linear key",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key removed"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/internal/repos/{owner}/{repo}/linear-key": {
+ "get": {
+ "operationId": "getLinearKeyInternal",
+ "tags": [
+ "Bring your own key",
+ "Internal"
+ ],
+ "summary": "Read this repo's Linear key state from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key presence and metadata"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "post": {
+ "operationId": "setLinearKeyInternal",
+ "tags": [
+ "Bring your own key",
+ "Internal"
+ ],
+ "summary": "Store this repo's Linear key from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key stored"
+ },
+ "400": {
+ "description": "Malformed key"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ },
+ "delete": {
+ "operationId": "deleteLinearKeyInternal",
+ "tags": [
+ "Bring your own key",
+ "Internal"
+ ],
+ "summary": "Remove this repo's Linear key from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Key removed"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/repos/{owner}/{repo}/ai-review": {
+ "put": {
+ "operationId": "updateRepoAiReviewMode",
+ "tags": [
+ "Repositories"
+ ],
+ "summary": "Set a repo's AI-review mode",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "AI-review mode updated"
+ },
+ "400": {
+ "description": "Malformed mode"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "403": {
+ "description": "Insufficient control-panel role"
+ }
+ },
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ]
+ }
+ },
+ "/v1/internal/jobs/refresh-registry/run": {
+ "post": {
+ "operationId": "runRefreshRegistryJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to refresh the Gittensor registry snapshot inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/refresh-scoring-model/run": {
+ "post": {
+ "operationId": "runRefreshScoringModelJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to refresh the active scoring model inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/refresh-upstream-drift/run": {
+ "post": {
+ "operationId": "runRefreshUpstreamDriftJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to recompute upstream ruleset drift inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/file-upstream-drift-issues/run": {
+ "post": {
+ "operationId": "runFileUpstreamDriftIssuesJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to file issues for open upstream drift inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/build-contributor-decision-packs/run": {
+ "post": {
+ "operationId": "runBuildContributorDecisionPacksJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to rebuild contributor decision packs inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/refresh-contributor-activity": {
+ "post": {
+ "operationId": "queueRefreshContributorActivityJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Queue a job to refresh cached contributor activity",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Job queued"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/refresh-contributor-activity/run": {
+ "post": {
+ "operationId": "runRefreshContributorActivityJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to refresh cached contributor activity inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/generate-signal-snapshots/run": {
+ "post": {
+ "operationId": "runGenerateSignalSnapshotsJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to generate signal snapshots inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/generate-weekly-value-report/run": {
+ "post": {
+ "operationId": "runGenerateWeeklyValueReportJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to generate the weekly value report inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/generate-review-recap/run": {
+ "post": {
+ "operationId": "runGenerateReviewRecapJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to generate the maintainer review recap inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/backfill-registered-repos/run": {
+ "post": {
+ "operationId": "runBackfillRegisteredReposJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to backfill registered repository records inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/backfill-repo-segment/run": {
+ "post": {
+ "operationId": "runBackfillRepoSegmentJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to backfill repository segment assignments inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/backfill-pr-details/run": {
+ "post": {
+ "operationId": "runBackfillPrDetailsJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to backfill pull-request detail rows inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/backfill-contributor-gate-history/run": {
+ "post": {
+ "operationId": "runBackfillContributorGateHistoryJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to backfill contributor gate history",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/refresh-installation-health/run": {
+ "post": {
+ "operationId": "runRefreshInstallationHealthJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to refresh GitHub App installation health",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/rollup-product-usage": {
+ "post": {
+ "operationId": "queueRollupProductUsageJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Queue a job to roll up product usage counters",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Job queued"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/rollup-product-usage/run": {
+ "post": {
+ "operationId": "runRollupProductUsageJob",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to roll up product usage counters inline, bypassing the queue",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/rag-index": {
+ "post": {
+ "operationId": "RunRagIndex",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to index repository content for retrieval",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "400": {
+ "description": "Malformed job request"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/jobs/regate-pr": {
+ "post": {
+ "operationId": "RegatePullRequest",
+ "tags": [
+ "Internal",
+ "Jobs"
+ ],
+ "summary": "Run the job to re-run the gate for one pull request",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Job ran inline and returned its result"
+ },
+ "400": {
+ "description": "Malformed job request"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/status": {
+ "get": {
+ "operationId": "getInternalStatus",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return the control plane's own status",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/decision": {
+ "get": {
+ "operationId": "getInternalDecision",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return one recorded gate decision",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/parity": {
+ "get": {
+ "operationId": "getInternalParity",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return native-vs-live gate parity measurements",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/predicted-agreement": {
+ "get": {
+ "operationId": "getInternalPredictedAgreement",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return predicted-gate agreement measurements",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/calibration": {
+ "get": {
+ "operationId": "getInternalCalibration",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return gate calibration measurements",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/calibration-trend": {
+ "get": {
+ "operationId": "getInternalCalibrationTrend",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return the calibration trend over time",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/calibration/knobs": {
+ "get": {
+ "operationId": "getInternalCalibrationKnobs",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return the tunable calibration knobs and their live values",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/calibration/satisfaction-floor": {
+ "get": {
+ "operationId": "getInternalSatisfactionFloor",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return the current satisfaction floor",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/audit-labels": {
+ "get": {
+ "operationId": "listInternalAuditLabels",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "List adjudication labels for the audit corpus",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/fairness/contributors": {
+ "get": {
+ "operationId": "listInternalFairnessContributors",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "List contributor fairness measurements",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/fleet/analytics": {
+ "get": {
+ "operationId": "getInternalFleetAnalytics",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return fleet-wide analytics",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/ops/stats": {
+ "get": {
+ "operationId": "getInternalOpsStats",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return operational statistics",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/retention/preview": {
+ "get": {
+ "operationId": "getInternalRetentionPreview",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Preview what a retention sweep would delete",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Measurement payload"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/fairness/contributors/{login}": {
+ "get": {
+ "operationId": "getInternalFairnessContributor",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Return one contributor's fairness measurements",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "login",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Fairness measurements"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ },
+ "404": {
+ "description": "No such contributor"
+ }
+ }
+ }
+ },
+ "/v1/internal/audit-labels/adjudicate": {
+ "post": {
+ "operationId": "adjudicateAuditLabel",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Record an adjudication for one audit-corpus item",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Adjudication recorded"
+ },
+ "400": {
+ "description": "Malformed adjudication"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/calibration/loosen-satisfaction-floor": {
+ "post": {
+ "operationId": "loosenSatisfactionFloor",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Loosen the satisfaction floor by one calibrated step",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Floor updated"
+ },
+ "400": {
+ "description": "Malformed request"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/queue-intelligence": {
+ "post": {
+ "operationId": "recordQueueIntelligence",
+ "tags": [
+ "Internal"
+ ],
+ "summary": "Record a queue-intelligence observation",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Observation recorded"
+ },
+ "400": {
+ "description": "Malformed observation"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/repos/{owner}/{repo}/contribution-policy": {
+ "get": {
+ "operationId": "getRepoContributionPolicyInternal",
+ "tags": [
+ "Internal",
+ "Repositories"
+ ],
+ "summary": "Read a repo's contribution policy from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Contribution policy"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ },
+ "post": {
+ "operationId": "setRepoContributionPolicyInternal",
+ "tags": [
+ "Internal",
+ "Repositories"
+ ],
+ "summary": "Write a repo's contribution policy from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Policy written"
+ },
+ "400": {
+ "description": "Malformed policy"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/v1/internal/repos/{owner}/{repo}/settings": {
+ "post": {
+ "operationId": "setRepoSettingsInternal",
+ "tags": [
+ "Internal",
+ "Repositories"
+ ],
+ "summary": "Write a repo's settings from the control plane",
+ "security": [
+ {
+ "LoopOverBearer": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Settings written"
+ },
+ "400": {
+ "description": "Malformed settings"
+ },
+ "401": {
+ "description": "Invalid internal token"
+ }
+ }
+ }
+ },
+ "/openapi.json": {
+ "get": {
+ "operationId": "getOpenApiDocument",
+ "tags": [
+ "Meta"
+ ],
+ "summary": "Return this OpenAPI document",
+ "responses": {
+ "200": {
+ "description": "The OpenAPI 3 document"
+ }
+ },
+ "security": []
+ }
+ },
+ "/loopover/shot": {
+ "get": {
+ "operationId": "getLoopoverShot",
+ "tags": [
+ "Meta"
+ ],
+ "summary": "Return the LoopOver social preview image",
+ "responses": {
+ "200": {
+ "description": "Preview image"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/public/subnet-interface": {
+ "get": {
+ "operationId": "getPublicSubnetInterface",
+ "tags": [
+ "Public"
+ ],
+ "summary": "Return the public subnet interface description",
+ "responses": {
+ "200": {
+ "description": "Subnet interface"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/public/repos/{owner}/{repo}/badge.json": {
+ "get": {
+ "operationId": "getRepoBadgeJson",
+ "tags": [
+ "Public"
+ ],
+ "summary": "Return a shields.io-compatible badge payload for a repo",
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Badge payload"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/public/repos/{owner}/{repo}/badge.svg": {
+ "get": {
+ "operationId": "getRepoBadgeSvg",
+ "tags": [
+ "Public"
+ ],
+ "summary": "Return a rendered SVG badge for a repo",
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "SVG badge"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/mcp/finding-taxonomy": {
+ "get": {
+ "operationId": "getMcpFindingTaxonomy",
+ "tags": [
+ "MCP"
+ ],
+ "summary": "Return the review finding taxonomy the MCP surfaces use",
+ "responses": {
+ "200": {
+ "description": "Finding taxonomy"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/mcp/enrichment-analyzers": {
+ "get": {
+ "operationId": "getMcpEnrichmentAnalyzers",
+ "tags": [
+ "MCP"
+ ],
+ "summary": "Return the review-enrichment analyzers the MCP surfaces expose",
+ "responses": {
+ "200": {
+ "description": "Analyzer descriptions"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/drafts": {
+ "post": {
+ "operationId": "createReviewDraft",
+ "tags": [
+ "Drafts"
+ ],
+ "summary": "Submit a review draft",
+ "responses": {
+ "200": {
+ "description": "Draft accepted"
+ },
+ "400": {
+ "description": "Malformed draft"
+ },
+ "404": {
+ "description": "Draft submission is disabled"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/drafts/{id}": {
+ "get": {
+ "operationId": "getReviewDraft",
+ "tags": [
+ "Drafts"
+ ],
+ "summary": "Read one submitted review draft",
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "id",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Draft"
+ },
+ "404": {
+ "description": "No such draft, or draft submission is disabled"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/drafts/auth/callback": {
+ "get": {
+ "operationId": "completeDraftOauth",
+ "tags": [
+ "Drafts"
+ ],
+ "summary": "Complete the draft-submission GitHub OAuth flow",
+ "responses": {
+ "302": {
+ "description": "Redirect back to the draft"
+ },
+ "400": {
+ "description": "Missing or invalid OAuth code"
+ }
+ },
+ "security": []
+ }
+ },
+ "/v1/ams/ingest": {
+ "post": {
+ "operationId": "ingestAmsTelemetry",
+ "tags": [
+ "AMS"
+ ],
+ "summary": "Ingest an AMS miner telemetry batch",
+ "security": [
+ {
+ "OrbBearer": []
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Batch accepted"
+ },
+ "400": {
+ "description": "Malformed batch"
+ },
+ "401": {
+ "description": "Missing or invalid ingest credential"
+ }
+ }
+ }
+ },
+ "/v1/contributors/{login}/ams-notifications": {
+ "post": {
+ "operationId": "sendAmsNotification",
+ "tags": [
+ "Contributors",
+ "AMS"
+ ],
+ "summary": "Deliver an AMS notification to a contributor",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "login",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "202": {
+ "description": "Notification queued"
+ },
+ "400": {
+ "description": "Malformed notification"
+ },
+ "401": {
+ "description": "Missing or invalid token"
+ }
+ }
+ }
+ },
+ "/v1/local/remediation-plan": {
+ "post": {
+ "operationId": "buildLocalRemediationPlan",
+ "tags": [
+ "Local"
+ ],
+ "summary": "Build a remediation plan from local branch metadata",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Remediation plan"
+ },
+ "400": {
+ "description": "Malformed branch metadata"
+ },
+ "401": {
+ "description": "Missing or invalid token"
+ }
+ }
+ }
+ },
+ "/v1/loop/request-apr-transfer": {
+ "post": {
+ "operationId": "requestAprTransfer",
+ "tags": [
+ "Loop"
+ ],
+ "summary": "Request an APR transfer for a rented loop",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Transfer requested"
+ },
+ "400": {
+ "description": "Malformed request"
+ },
+ "401": {
+ "description": "Missing or invalid token"
+ }
+ }
+ }
+ },
+ "/v1/repos/{owner}/{repo}/pulls/{number}/chat-qa": {
+ "post": {
+ "operationId": "askPullRequestChatQuestion",
+ "tags": [
+ "Repositories",
+ "Review"
+ ],
+ "summary": "Ask a question about one pull request's review",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "parameters": [
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "owner",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "repo",
+ "in": "path"
+ },
+ {
+ "schema": {
+ "type": "string"
+ },
+ "required": true,
+ "name": "number",
+ "in": "path"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Answer"
+ },
+ "400": {
+ "description": "Malformed question"
+ },
+ "401": {
+ "description": "Not signed in"
+ },
+ "404": {
+ "description": "No such pull request"
+ }
+ }
}
}
},
diff --git a/src/api/routes.ts b/src/api/routes.ts
index bf3bc3858d..e76869a5f1 100644
--- a/src/api/routes.ts
+++ b/src/api/routes.ts
@@ -1,4 +1,5 @@
import { Hono, type Context } from "hono";
+import { requiresApiToken } from "../auth/route-auth";
import { createWorkerPostHogErrorMiddleware } from "./worker-posthog";
import { z } from "zod";
import { parsePositiveInt } from "../utils/json";
@@ -6870,54 +6871,6 @@ async function isAuthorizedIngest(configuredToken: string | undefined, presented
return timingSafeEqual(presentedToken, configuredToken);
}
-function requiresApiToken(path: string): boolean {
- if (path === "/health") return false;
- if (path === "/v1/mcp/compatibility") return false;
- if (path === "/v1/mcp/finding-taxonomy") return false;
- if (path === "/v1/mcp/enrichment-analyzers") return false;
- if (/^\/v1\/public\/github\/repos\/[^/]+\/[^/]+\/stats$/.test(path)) return false;
- if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/badge\.(svg|json)$/.test(path)) return false;
- if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/quality$/.test(path)) return false;
- if (path === "/v1/public/subnet-interface") return false;
- if (path === "/v1/public/stats") return false;
- // #9120: this route's own doc comment always claimed "safe unauthenticated" but was missing from this exact
- // list, so it 401'd in prod — verified live: subnet-interface/stats/quality answered 200, this one 401'd.
- if (path === "/v1/public/decision-ledger/verify") return false;
- // #9266: the eval-scores transport is unauthenticated by the same design as every /v1/public/* sibling
- // above -- committed to a corpus checksum, independently re-derivable, nothing gated behind a token.
- if (path === "/v1/public/eval-scores") return false;
- // #9269: the single-row read, added in the SAME PR as its route so the two can never drift the way #9120's
- // sibling did. Regex (not a literal) because of the :seq path parameter.
- if (/^\/v1\/public\/decision-ledger\/row\/[^/]+$/.test(path)) return false;
- // #9270: the published anchor-signing public keys, added in the SAME PR as its route so the two cannot
- // drift the way #9120's sibling did.
- if (path === "/v1/public/decision-ledger/anchor-key") return false;
- // #9271: the public anchor-attempt listing, added in the SAME PR as its route.
- if (path === "/v1/public/decision-ledger/anchors") return false;
- // #9123: the new public decision-record read route — same unauthenticated posture as its ledger-verify
- // sibling immediately above, added in the SAME PR so the two can never drift apart the way #9120 did. The
- // pull segment matches any non-slash text (not just digits): an invalid pull number is the ROUTE's 400 to
- // return, not the auth gate's 401 — mirrors every other dynamic-segment exemption below (owner/repo use the
- // same unvalidated [^/]+).
- if (/^\/v1\/public\/decision-records\/[^/]+\/[^/]+\/[^/]+$/.test(path)) return false;
- if (path === "/openapi.json") return false;
- if (path === "/mcp") return false;
- // Public OAuth draft-submission flow (LOOPOVER_REVIEW_DRAFT): the submission entry points are unauthenticated
- // by design. The handlers themselves 404 when the flag is off, so this exemption is inert flag-OFF.
- if (path === "/v1/drafts" || path.startsWith("/v1/drafts/")) return false;
- if (path.startsWith("/v1/auth/")) return false;
- if (path === "/v1/github/webhook") return false;
- if (path === "/v1/orb/webhook") return false;
- if (path === "/v1/orb/relay") return false;
- if (path === "/v1/orb/oauth/callback") return false;
- if (path === "/v1/orb/token") return false;
- if (path === "/v1/orb/relay/register") return false;
- if (path === "/v1/orb/relay/pull") return false;
- if (path === "/v1/orb/ingest") return false;
- if (path === "/v1/ams/ingest") return false;
- if (path.startsWith("/v1/internal/")) return false;
- return path.startsWith("/v1/");
-}
// Unauthenticated, cookie-free, aggregate-only public GET endpoints (health check, homepage stats counter,
// per-repo public stats badge) -- open to any origin via a separate, credential-free CORS branch above.
diff --git a/src/auth/route-auth.ts b/src/auth/route-auth.ts
new file mode 100644
index 0000000000..538675885e
--- /dev/null
+++ b/src/auth/route-auth.ts
@@ -0,0 +1,74 @@
+// Which routes require a credential, as ONE model (#9531).
+//
+// Extracted from src/api/routes.ts so the OpenAPI builder can read the same predicate the app gates
+// on. It cannot import routes.ts: that module reaches `cloudflare:` imports, and the spec is
+// generated by a plain Node script (scripts/write-ui-openapi.ts) whose loader cannot resolve them.
+//
+// Before this, src/openapi/spec.ts carried `isProtectedPath` -- a second, path-prefix approximation
+// that treated every `/v1/*` route as protected bar a short literal list. It had already drifted:
+// the whole `/v1/public/decision-ledger/*` family answers 200 to an anonymous caller and was
+// published as needing a bearer. A pure module both sides import is what makes that class of drift
+// impossible rather than merely fixed.
+//
+// PURE by construction: string predicates only, no env, no I/O, no Workers types. Keep it that way
+// -- the moment this file needs a runtime binding, the spec builder stops working again.
+
+/**
+ * The ONE model of which routes are credential-gated (#9531).
+ *
+ * Exported so src/openapi/spec.ts derives its security stanzas from this rather than from a second,
+ * path-prefix approximation of it. `isProtectedPath` was that approximation, and it had already
+ * drifted: it treated every `/v1/*` path as protected except a short literal list, which disagreed
+ * with this function on the whole `/v1/public/decision-ledger/*` family among others -- so the
+ * published document advertised a bearer requirement on routes that answer 200 to an anonymous
+ * caller. Deriving from the enforcer means the document cannot say something the middleware does
+ * not do.
+ */
+export function requiresApiToken(path: string): boolean {
+ if (path === "/health") return false;
+ if (path === "/v1/mcp/compatibility") return false;
+ if (path === "/v1/mcp/finding-taxonomy") return false;
+ if (path === "/v1/mcp/enrichment-analyzers") return false;
+ if (/^\/v1\/public\/github\/repos\/[^/]+\/[^/]+\/stats$/.test(path)) return false;
+ if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/badge\.(svg|json)$/.test(path)) return false;
+ if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/quality$/.test(path)) return false;
+ if (path === "/v1/public/subnet-interface") return false;
+ if (path === "/v1/public/stats") return false;
+ // #9120: this route's own doc comment always claimed "safe unauthenticated" but was missing from this exact
+ // list, so it 401'd in prod — verified live: subnet-interface/stats/quality answered 200, this one 401'd.
+ if (path === "/v1/public/decision-ledger/verify") return false;
+ // #9266: the eval-scores transport is unauthenticated by the same design as every /v1/public/* sibling
+ // above -- committed to a corpus checksum, independently re-derivable, nothing gated behind a token.
+ if (path === "/v1/public/eval-scores") return false;
+ // #9269: the single-row read, added in the SAME PR as its route so the two can never drift the way #9120's
+ // sibling did. Regex (not a literal) because of the :seq path parameter.
+ if (/^\/v1\/public\/decision-ledger\/row\/[^/]+$/.test(path)) return false;
+ // #9270: the published anchor-signing public keys, added in the SAME PR as its route so the two cannot
+ // drift the way #9120's sibling did.
+ if (path === "/v1/public/decision-ledger/anchor-key") return false;
+ // #9271: the public anchor-attempt listing, added in the SAME PR as its route.
+ if (path === "/v1/public/decision-ledger/anchors") return false;
+ // #9123: the new public decision-record read route — same unauthenticated posture as its ledger-verify
+ // sibling immediately above, added in the SAME PR so the two can never drift apart the way #9120 did. The
+ // pull segment matches any non-slash text (not just digits): an invalid pull number is the ROUTE's 400 to
+ // return, not the auth gate's 401 — mirrors every other dynamic-segment exemption below (owner/repo use the
+ // same unvalidated [^/]+).
+ if (/^\/v1\/public\/decision-records\/[^/]+\/[^/]+\/[^/]+$/.test(path)) return false;
+ if (path === "/openapi.json") return false;
+ if (path === "/mcp") return false;
+ // Public OAuth draft-submission flow (LOOPOVER_REVIEW_DRAFT): the submission entry points are unauthenticated
+ // by design. The handlers themselves 404 when the flag is off, so this exemption is inert flag-OFF.
+ if (path === "/v1/drafts" || path.startsWith("/v1/drafts/")) return false;
+ if (path.startsWith("/v1/auth/")) return false;
+ if (path === "/v1/github/webhook") return false;
+ if (path === "/v1/orb/webhook") return false;
+ if (path === "/v1/orb/relay") return false;
+ if (path === "/v1/orb/oauth/callback") return false;
+ if (path === "/v1/orb/token") return false;
+ if (path === "/v1/orb/relay/register") return false;
+ if (path === "/v1/orb/relay/pull") return false;
+ if (path === "/v1/orb/ingest") return false;
+ if (path === "/v1/ams/ingest") return false;
+ if (path.startsWith("/v1/internal/")) return false;
+ return path.startsWith("/v1/");
+}
diff --git a/src/openapi/define-route.ts b/src/openapi/define-route.ts
index 06464eab08..8c8e0b350e 100644
--- a/src/openapi/define-route.ts
+++ b/src/openapi/define-route.ts
@@ -21,7 +21,7 @@ import { z } from "zod";
* declaration the runtime gate enforces -- replacing `isProtectedPath()`, a second, path-prefix
* model of the same policy that had already drifted out of agreement with it.
*/
-export type RouteAuth = "public" | "token" | "session" | "internal";
+export type RouteAuth = "public" | "token" | "session" | "internal" | "orb" | "webhook";
export type RouteMethod = "get" | "post" | "put" | "patch" | "delete";
@@ -46,16 +46,44 @@ export type DefineRouteOptions
;
};
+/**
+ * Every `:param` in the path, as an OpenAPI `in: path` parameter schema.
+ *
+ * Emitted automatically rather than declared per route: a templated segment with no matching
+ * parameter is a schema-validation warning (Cloudflare 30046) and leaves a generated client with a
+ * URL it cannot fill, and there is no case where a path parameter is optional -- so the correct
+ * declaration is fully derivable from the path itself and nothing is gained by asking for it twice.
+ */
+function pathParameters(path: string): { params: z.ZodObject } | undefined {
+ const names = [...path.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1]!);
+ if (names.length === 0) return undefined;
+ return { params: z.object(Object.fromEntries(names.map((name) => [name, z.string()]))) };
+}
+
/** Hono writes `:param`; OpenAPI writes `{param}`. */
function toSpecPath(path: string): string {
return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
}
-/** `public` routes carry no security stanza; everything else accepts either credential the API
- * actually supports. Internal routes are bearer-only -- there is no cookie path to them. */
+/**
+ * The security stanza a declared auth level emits.
+ *
+ * `public` carries none. `internal` is bearer-only -- there is no cookie path to it. `orb` and
+ * `webhook` (#9531) exist because the ORB ingress genuinely does not authenticate the way the rest
+ * of the API does, and collapsing them into `public` would publish a document that says these
+ * routes need no credential at all. They need a DIFFERENT one: an ORB-issued bearer for the relay
+ * and token endpoints, an HMAC signature header for the webhook. `requiresApiToken()` exempts both
+ * from the LoopOver bearer check, which is what made them look public to the old path-prefix model.
+ */
function securityFor(auth: RouteAuth): RouteConfig["security"] {
- if (auth === "public") return undefined;
+ // `[]`, not undefined: an empty security array is OpenAPI's explicit "this operation needs no
+ // credential", where an ABSENT one means "not stated". The distinction is load-bearing here --
+ // applySecurityMetadata fills in the legacy `registerPath` calls that never declared anything, and
+ // it can only tell those apart from a deliberately public route if public says so out loud.
+ if (auth === "public") return [];
if (auth === "internal") return [{ LoopOverBearer: [] }];
+ if (auth === "orb") return [{ OrbBearer: [] }];
+ if (auth === "webhook") return [{ OrbWebhookSignature: [] }];
return [{ LoopOverBearer: [] }, { LoopOverSessionCookie: [] }];
}
@@ -138,6 +166,11 @@ export function registerRouteSpec(registry: OpenAPIRegistry, options: RouteSpecO
registry.registerPath({
method: options.method,
path: toSpecPath(options.path),
+ request: {
+ ...(pathParameters(options.path) ?? {}),
+ ...(options.request?.body ? { body: { content: { "application/json": { schema: options.request.body } } } } : {}),
+ ...(options.request?.query ? { query: options.request.query } : {}),
+ },
operationId: options.operationId,
tags: options.tags,
summary: options.summary,
diff --git a/src/openapi/internal-and-public-route-specs.ts b/src/openapi/internal-and-public-route-specs.ts
new file mode 100644
index 0000000000..6c5a60ade4
--- /dev/null
+++ b/src/openapi/internal-and-public-route-specs.ts
@@ -0,0 +1,357 @@
+// Spec entries for the remaining internal, public, and miscellaneous routes (#9531, batch 2).
+//
+// Completes the ratchet: with these the published document describes every route `createApp()`
+// serves, and src/openapi/unspecced-routes-baseline.json is deleted.
+//
+// The `/run` suffix pattern is the biggest family here and worth naming: nineteen internal job
+// routes exist in two forms -- a bare POST that ENQUEUES the job onto the durable queue, and a
+// `/run` sibling that executes it inline and returns the result. Both are real, both are called
+// (the queue path in production, the inline path by the ops runbook when a queue is wedged), and
+// the two answer with different status codes. Registering them from one table keeps that pairing
+// visible instead of leaving nineteen near-identical stanzas to drift apart by hand.
+import type { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
+import { registerRouteSpec, type RouteAuth, type RouteMethod } from "./define-route";
+
+type SpecEntry = {
+ method: RouteMethod;
+ path: string;
+ operationId: string;
+ tags: [string, ...string[]];
+ summary: string;
+ auth: RouteAuth;
+ responses: Record;
+};
+
+const INTERNAL_AUTH = { 401: { description: "Invalid internal token" } };
+const QUEUED = { 202: { description: "Job queued" }, ...INTERNAL_AUTH };
+const RAN = { 200: { description: "Job ran inline and returned its result" }, ...INTERNAL_AUTH };
+
+/** `[path segment, operationId stem, human summary]` for the jobs that have BOTH forms. */
+const JOB_PAIRS: ReadonlyArray = [
+ ["refresh-registry", "RefreshRegistry", "refresh the Gittensor registry snapshot"],
+ ["refresh-scoring-model", "RefreshScoringModel", "refresh the active scoring model"],
+ ["refresh-upstream-drift", "RefreshUpstreamDrift", "recompute upstream ruleset drift"],
+ ["file-upstream-drift-issues", "FileUpstreamDriftIssues", "file issues for open upstream drift"],
+ ["build-contributor-decision-packs", "BuildContributorDecisionPacks", "rebuild contributor decision packs"],
+ ["refresh-contributor-activity", "RefreshContributorActivity", "refresh cached contributor activity"],
+ ["generate-signal-snapshots", "GenerateSignalSnapshots", "generate signal snapshots"],
+ ["generate-weekly-value-report", "GenerateWeeklyValueReport", "generate the weekly value report"],
+ ["generate-review-recap", "GenerateReviewRecap", "generate the maintainer review recap"],
+ ["backfill-registered-repos", "BackfillRegisteredRepos", "backfill registered repository records"],
+ ["backfill-repo-segment", "BackfillRepoSegment", "backfill repository segment assignments"],
+ ["backfill-pr-details", "BackfillPrDetails", "backfill pull-request detail rows"],
+ ["rollup-product-usage", "RollupProductUsage", "roll up product usage counters"],
+];
+
+/** Jobs that exist only in the bare (enqueue-or-run) form. */
+const SINGLE_JOBS: ReadonlyArray = [
+ ["rag-index", "RunRagIndex", "index repository content for retrieval"],
+ ["regate-pr", "RegatePullRequest", "re-run the gate for one pull request"],
+];
+
+/**
+ * Jobs that exist ONLY in the `/run` form, with no enqueue sibling.
+ *
+ * Not an oversight in the routes -- both are operator-triggered repairs with no scheduled trigger,
+ * so there is nothing to enqueue them from. Listed separately because assuming the pair was
+ * symmetric published two operations no route served, which is precisely what the ratchet's
+ * "never publishes an operation for a route the app does not serve" direction caught.
+ */
+const RUN_ONLY_JOBS: ReadonlyArray = [
+ ["backfill-contributor-gate-history", "BackfillContributorGateHistory", "backfill contributor gate history"],
+ ["refresh-installation-health", "RefreshInstallationHealth", "refresh GitHub App installation health"],
+];
+
+function jobRoutes(): SpecEntry[] {
+ const entries: SpecEntry[] = [];
+ for (const [segment, stem, summary] of JOB_PAIRS) {
+ entries.push({
+ method: "post",
+ path: `/v1/internal/jobs/${segment}`,
+ operationId: `queue${stem}Job`,
+ tags: ["Internal", "Jobs"],
+ summary: `Queue a job to ${summary}`,
+ auth: "internal",
+ responses: QUEUED,
+ });
+ entries.push({
+ method: "post",
+ path: `/v1/internal/jobs/${segment}/run`,
+ operationId: `run${stem}Job`,
+ tags: ["Internal", "Jobs"],
+ summary: `Run the job to ${summary} inline, bypassing the queue`,
+ auth: "internal",
+ responses: RAN,
+ });
+ }
+ for (const [segment, stem, summary] of RUN_ONLY_JOBS) {
+ entries.push({
+ method: "post",
+ path: `/v1/internal/jobs/${segment}/run`,
+ operationId: `run${stem}Job`,
+ tags: ["Internal", "Jobs"],
+ summary: `Run the job to ${summary}`,
+ auth: "internal",
+ responses: RAN,
+ });
+ }
+ for (const [segment, operationId, summary] of SINGLE_JOBS) {
+ entries.push({
+ method: "post",
+ path: `/v1/internal/jobs/${segment}`,
+ operationId,
+ tags: ["Internal", "Jobs"],
+ summary: `Run the job to ${summary}`,
+ auth: "internal",
+ responses: { ...RAN, 400: { description: "Malformed job request" } },
+ });
+ }
+ return entries;
+}
+
+const INTERNAL_READS: SpecEntry[] = [
+ ["/v1/internal/status", "getInternalStatus", "Return the control plane's own status"],
+ ["/v1/internal/decision", "getInternalDecision", "Return one recorded gate decision"],
+ ["/v1/internal/parity", "getInternalParity", "Return native-vs-live gate parity measurements"],
+ ["/v1/internal/predicted-agreement", "getInternalPredictedAgreement", "Return predicted-gate agreement measurements"],
+ ["/v1/internal/calibration", "getInternalCalibration", "Return gate calibration measurements"],
+ ["/v1/internal/calibration-trend", "getInternalCalibrationTrend", "Return the calibration trend over time"],
+ ["/v1/internal/calibration/knobs", "getInternalCalibrationKnobs", "Return the tunable calibration knobs and their live values"],
+ ["/v1/internal/calibration/satisfaction-floor", "getInternalSatisfactionFloor", "Return the current satisfaction floor"],
+ ["/v1/internal/audit-labels", "listInternalAuditLabels", "List adjudication labels for the audit corpus"],
+ ["/v1/internal/fairness/contributors", "listInternalFairnessContributors", "List contributor fairness measurements"],
+ ["/v1/internal/fleet/analytics", "getInternalFleetAnalytics", "Return fleet-wide analytics"],
+ ["/v1/internal/ops/stats", "getInternalOpsStats", "Return operational statistics"],
+ ["/v1/internal/retention/preview", "getInternalRetentionPreview", "Preview what a retention sweep would delete"],
+].map(([path, operationId, summary]) => ({
+ method: "get" as const,
+ path: path!,
+ operationId: operationId!,
+ tags: ["Internal"] as [string, ...string[]],
+ summary: summary!,
+ auth: "internal" as const,
+ responses: { 200: { description: "Measurement payload" }, ...INTERNAL_AUTH },
+}));
+
+const INTERNAL_OTHER: SpecEntry[] = [
+ {
+ method: "get",
+ path: "/v1/internal/fairness/contributors/:login",
+ operationId: "getInternalFairnessContributor",
+ tags: ["Internal"],
+ summary: "Return one contributor's fairness measurements",
+ auth: "internal",
+ responses: { 200: { description: "Fairness measurements" }, 404: { description: "No such contributor" }, ...INTERNAL_AUTH },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/audit-labels/adjudicate",
+ operationId: "adjudicateAuditLabel",
+ tags: ["Internal"],
+ summary: "Record an adjudication for one audit-corpus item",
+ auth: "internal",
+ responses: { 200: { description: "Adjudication recorded" }, 400: { description: "Malformed adjudication" }, ...INTERNAL_AUTH },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/calibration/loosen-satisfaction-floor",
+ operationId: "loosenSatisfactionFloor",
+ tags: ["Internal"],
+ summary: "Loosen the satisfaction floor by one calibrated step",
+ auth: "internal",
+ responses: { 200: { description: "Floor updated" }, 400: { description: "Malformed request" }, ...INTERNAL_AUTH },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/queue-intelligence",
+ operationId: "recordQueueIntelligence",
+ tags: ["Internal"],
+ summary: "Record a queue-intelligence observation",
+ auth: "internal",
+ responses: { 202: { description: "Observation recorded" }, 400: { description: "Malformed observation" }, ...INTERNAL_AUTH },
+ },
+ {
+ method: "get",
+ path: "/v1/internal/repos/:owner/:repo/contribution-policy",
+ operationId: "getRepoContributionPolicyInternal",
+ tags: ["Internal", "Repositories"],
+ summary: "Read a repo's contribution policy from the control plane",
+ auth: "internal",
+ responses: { 200: { description: "Contribution policy" }, ...INTERNAL_AUTH },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/repos/:owner/:repo/contribution-policy",
+ operationId: "setRepoContributionPolicyInternal",
+ tags: ["Internal", "Repositories"],
+ summary: "Write a repo's contribution policy from the control plane",
+ auth: "internal",
+ responses: { 200: { description: "Policy written" }, 400: { description: "Malformed policy" }, ...INTERNAL_AUTH },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/repos/:owner/:repo/settings",
+ operationId: "setRepoSettingsInternal",
+ tags: ["Internal", "Repositories"],
+ summary: "Write a repo's settings from the control plane",
+ auth: "internal",
+ responses: { 200: { description: "Settings written" }, 400: { description: "Malformed settings" }, ...INTERNAL_AUTH },
+ },
+];
+
+const PUBLIC_ROUTES: SpecEntry[] = [
+ {
+ method: "get",
+ path: "/openapi.json",
+ operationId: "getOpenApiDocument",
+ tags: ["Meta"],
+ summary: "Return this OpenAPI document",
+ auth: "public",
+ responses: { 200: { description: "The OpenAPI 3 document" } },
+ },
+ {
+ method: "get",
+ path: "/loopover/shot",
+ operationId: "getLoopoverShot",
+ tags: ["Meta"],
+ summary: "Return the LoopOver social preview image",
+ auth: "public",
+ responses: { 200: { description: "Preview image" } },
+ },
+ {
+ method: "get",
+ path: "/v1/public/subnet-interface",
+ operationId: "getPublicSubnetInterface",
+ tags: ["Public"],
+ summary: "Return the public subnet interface description",
+ auth: "public",
+ responses: { 200: { description: "Subnet interface" } },
+ },
+ {
+ method: "get",
+ path: "/v1/public/repos/:owner/:repo/badge.json",
+ operationId: "getRepoBadgeJson",
+ tags: ["Public"],
+ summary: "Return a shields.io-compatible badge payload for a repo",
+ auth: "public",
+ responses: { 200: { description: "Badge payload" } },
+ },
+ {
+ method: "get",
+ path: "/v1/public/repos/:owner/:repo/badge.svg",
+ operationId: "getRepoBadgeSvg",
+ tags: ["Public"],
+ summary: "Return a rendered SVG badge for a repo",
+ auth: "public",
+ responses: { 200: { description: "SVG badge" } },
+ },
+ {
+ method: "get",
+ path: "/v1/mcp/finding-taxonomy",
+ operationId: "getMcpFindingTaxonomy",
+ tags: ["MCP"],
+ summary: "Return the review finding taxonomy the MCP surfaces use",
+ auth: "public",
+ responses: { 200: { description: "Finding taxonomy" } },
+ },
+ {
+ method: "get",
+ path: "/v1/mcp/enrichment-analyzers",
+ operationId: "getMcpEnrichmentAnalyzers",
+ tags: ["MCP"],
+ summary: "Return the review-enrichment analyzers the MCP surfaces expose",
+ auth: "public",
+ responses: { 200: { description: "Analyzer descriptions" } },
+ },
+];
+
+const MISC_ROUTES: SpecEntry[] = [
+ {
+ method: "post",
+ path: "/v1/drafts",
+ operationId: "createReviewDraft",
+ tags: ["Drafts"],
+ // Unauthenticated by design (the OAuth draft-submission flow); the handlers 404 when the
+ // LOOPOVER_REVIEW_DRAFT flag is off, so the exemption is inert flag-OFF.
+ auth: "public",
+ summary: "Submit a review draft",
+ responses: { 200: { description: "Draft accepted" }, 400: { description: "Malformed draft" }, 404: { description: "Draft submission is disabled" } },
+ },
+ {
+ method: "get",
+ path: "/v1/drafts/:id",
+ operationId: "getReviewDraft",
+ tags: ["Drafts"],
+ auth: "public",
+ summary: "Read one submitted review draft",
+ responses: { 200: { description: "Draft" }, 404: { description: "No such draft, or draft submission is disabled" } },
+ },
+ {
+ method: "get",
+ path: "/v1/drafts/auth/callback",
+ operationId: "completeDraftOauth",
+ tags: ["Drafts"],
+ auth: "public",
+ summary: "Complete the draft-submission GitHub OAuth flow",
+ responses: { 302: { description: "Redirect back to the draft" }, 400: { description: "Missing or invalid OAuth code" } },
+ },
+ {
+ method: "post",
+ path: "/v1/ams/ingest",
+ operationId: "ingestAmsTelemetry",
+ tags: ["AMS"],
+ // Its own shared-secret header, like the ORB ingress -- not a LoopOver bearer.
+ auth: "orb",
+ summary: "Ingest an AMS miner telemetry batch",
+ responses: { 202: { description: "Batch accepted" }, 400: { description: "Malformed batch" }, 401: { description: "Missing or invalid ingest credential" } },
+ },
+ {
+ method: "post",
+ path: "/v1/contributors/:login/ams-notifications",
+ operationId: "sendAmsNotification",
+ tags: ["Contributors", "AMS"],
+ summary: "Deliver an AMS notification to a contributor",
+ auth: "token",
+ responses: { 202: { description: "Notification queued" }, 400: { description: "Malformed notification" }, 401: { description: "Missing or invalid token" } },
+ },
+ {
+ method: "post",
+ path: "/v1/local/remediation-plan",
+ operationId: "buildLocalRemediationPlan",
+ tags: ["Local"],
+ summary: "Build a remediation plan from local branch metadata",
+ auth: "token",
+ responses: { 200: { description: "Remediation plan" }, 400: { description: "Malformed branch metadata" }, 401: { description: "Missing or invalid token" } },
+ },
+ {
+ method: "post",
+ path: "/v1/loop/request-apr-transfer",
+ operationId: "requestAprTransfer",
+ tags: ["Loop"],
+ summary: "Request an APR transfer for a rented loop",
+ auth: "token",
+ responses: { 200: { description: "Transfer requested" }, 400: { description: "Malformed request" }, 401: { description: "Missing or invalid token" } },
+ },
+ {
+ method: "post",
+ path: "/v1/repos/:owner/:repo/pulls/:number/chat-qa",
+ operationId: "askPullRequestChatQuestion",
+ tags: ["Repositories", "Review"],
+ summary: "Ask a question about one pull request's review",
+ auth: "session",
+ responses: { 200: { description: "Answer" }, 400: { description: "Malformed question" }, 401: { description: "Not signed in" }, 404: { description: "No such pull request" } },
+ },
+];
+
+export function registerInternalAndPublicRouteSpecs(registry: OpenAPIRegistry): void {
+ for (const entry of INTERNAL_AND_PUBLIC_ROUTE_SPECS) registerRouteSpec(registry, entry);
+}
+
+/** Exported for the auth-parity meta-test. */
+export const INTERNAL_AND_PUBLIC_ROUTE_SPECS: readonly SpecEntry[] = [
+ ...jobRoutes(),
+ ...INTERNAL_READS,
+ ...INTERNAL_OTHER,
+ ...PUBLIC_ROUTES,
+ ...MISC_ROUTES,
+];
diff --git a/src/openapi/orb-and-control-route-specs.ts b/src/openapi/orb-and-control-route-specs.ts
new file mode 100644
index 0000000000..f0a8bcd7e9
--- /dev/null
+++ b/src/openapi/orb-and-control-route-specs.ts
@@ -0,0 +1,336 @@
+// Spec entries for the ORB ingress, the control-panel app surface, and the per-repo key/settings
+// routes (#9531, batch 1 of the ratchet).
+//
+// These 24 routes served real traffic while the published document said nothing about them -- the
+// whole ORB management surface among them, which is the precondition for #9522's management tools.
+//
+// SPEC-ONLY, deliberately. `registerRouteSpec` contributes a correct operation without moving the
+// handler through `defineRoute`'s validating wrapper. Moving handlers is the other half of #9531 and
+// happens per family; separating the two means the document stops lying about what the app serves
+// now, rather than after every handler has been rewritten. Each entry still carries the things the
+// old hand-registered operations never did: a stable operationId, real tags, and an auth level that
+// derives its security stanza rather than having one bolted on by path prefix.
+import type { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
+import { registerRouteSpec, type RouteAuth, type RouteMethod } from "./define-route";
+
+type SpecEntry = {
+ method: RouteMethod;
+ path: string;
+ operationId: string;
+ tags: [string, ...string[]];
+ summary: string;
+ auth: RouteAuth;
+ responses: Record;
+};
+
+/** Shared by every ORB-authenticated endpoint. */
+const ORB_AUTH_RESPONSES = { 401: { description: "Missing or invalid ORB instance token" } };
+const INTERNAL_AUTH_RESPONSES = { 401: { description: "Invalid internal token" } };
+const SESSION_AUTH_RESPONSES = { 401: { description: "Not signed in" }, 403: { description: "Insufficient control-panel role" } };
+
+const ORB_ROUTES: SpecEntry[] = [
+ {
+ method: "post",
+ path: "/v1/orb/token",
+ operationId: "mintOrbInstanceToken",
+ tags: ["ORB"],
+ summary: "Mint an ORB instance token",
+ // The minting endpoint is reached WITH the enrollment secret, not with a token it has yet to
+ // issue -- so `orb`, not `public`: a credential is required, just not a LoopOver bearer.
+ auth: "orb",
+ responses: { 200: { description: "Instance token issued" }, 400: { description: "Malformed enrollment request" }, ...ORB_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/orb/relay",
+ operationId: "relayOrbEvent",
+ tags: ["ORB"],
+ summary: "Relay one ORB event to the hosted control plane",
+ auth: "orb",
+ responses: { 202: { description: "Event accepted for processing" }, 400: { description: "Malformed relay payload" }, ...ORB_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/orb/relay/register",
+ operationId: "registerOrbRelayInstance",
+ tags: ["ORB"],
+ summary: "Register a self-hosted ORB instance with the relay",
+ auth: "orb",
+ responses: { 200: { description: "Instance registered" }, 400: { description: "Malformed registration" }, ...ORB_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/orb/relay/pull",
+ operationId: "pullOrbRelayWork",
+ tags: ["ORB"],
+ summary: "Pull queued relay work for an ORB instance",
+ auth: "orb",
+ responses: { 200: { description: "Queued work, possibly empty" }, ...ORB_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/orb/webhook",
+ operationId: "receiveOrbWebhook",
+ tags: ["ORB"],
+ // Signature-verified, not bearer-authenticated -- the one route whose credential is a header
+ // over the raw body.
+ auth: "webhook",
+ summary: "Receive a signed ORB webhook",
+ responses: { 202: { description: "Webhook accepted" }, 400: { description: "Malformed webhook body" }, 401: { description: "Signature verification failed" } },
+ },
+ {
+ method: "get",
+ path: "/v1/orb/oauth/callback",
+ operationId: "completeOrbOauth",
+ tags: ["ORB"],
+ // Genuinely public: the browser arrives here from GitHub with a one-time code and no credential
+ // of its own; the code IS the proof.
+ auth: "public",
+ summary: "Complete the ORB GitHub OAuth flow",
+ responses: { 302: { description: "Redirect back to the ORB instance" }, 400: { description: "Missing or invalid OAuth code" } },
+ },
+];
+
+const INTERNAL_ORB_ROUTES: SpecEntry[] = [
+ {
+ method: "get",
+ path: "/v1/internal/orb/instances",
+ operationId: "listOrbInstances",
+ tags: ["ORB", "Internal"],
+ summary: "List registered ORB instances",
+ auth: "internal",
+ responses: { 200: { description: "Registered instances" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/orb/instances/register",
+ operationId: "registerOrbInstanceInternal",
+ tags: ["ORB", "Internal"],
+ summary: "Register an ORB instance from the control plane",
+ auth: "internal",
+ responses: { 200: { description: "Instance registered" }, 400: { description: "Malformed registration" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "get",
+ path: "/v1/internal/orb/installations",
+ operationId: "listOrbInstallations",
+ tags: ["ORB", "Internal"],
+ summary: "List GitHub App installations known to the ORB control plane",
+ auth: "internal",
+ responses: { 200: { description: "Installations" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/orb/installations/register",
+ operationId: "registerOrbInstallation",
+ tags: ["ORB", "Internal"],
+ summary: "Register a GitHub App installation with the ORB control plane",
+ auth: "internal",
+ responses: { 200: { description: "Installation registered" }, 400: { description: "Malformed registration" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/orb/installations/backfill",
+ operationId: "backfillOrbInstallations",
+ tags: ["ORB", "Internal"],
+ summary: "Backfill ORB installation records from GitHub",
+ auth: "internal",
+ responses: { 202: { description: "Backfill queued" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/orb/enrollments",
+ operationId: "createOrbEnrollment",
+ tags: ["ORB", "Internal"],
+ summary: "Create an ORB instance enrollment",
+ auth: "internal",
+ responses: { 200: { description: "Enrollment created" }, 400: { description: "Malformed enrollment" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/internal/orb/enrollments/:enrollId/revoke",
+ operationId: "revokeOrbEnrollment",
+ tags: ["ORB", "Internal"],
+ summary: "Revoke an ORB instance enrollment",
+ auth: "internal",
+ responses: { 200: { description: "Enrollment revoked" }, 404: { description: "No such enrollment" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+];
+
+const APP_ROUTES: SpecEntry[] = [
+ {
+ method: "get",
+ path: "/v1/app/installations",
+ operationId: "listAppInstallations",
+ tags: ["Control panel"],
+ summary: "List the GitHub App installations the caller can administer",
+ auth: "session",
+ responses: { 200: { description: "Installations" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "get",
+ path: "/v1/app/installations/:id/health",
+ operationId: "getAppInstallationHealth",
+ tags: ["Control panel"],
+ summary: "Return one installation's health summary",
+ auth: "session",
+ responses: { 200: { description: "Installation health" }, 404: { description: "No such installation" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "get",
+ path: "/v1/app/installations/:id/repair",
+ operationId: "getAppInstallationRepair",
+ tags: ["Control panel"],
+ summary: "Return the repair plan for an unhealthy installation",
+ auth: "session",
+ responses: { 200: { description: "Repair plan" }, 404: { description: "No such installation" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/app/installations/:id/repair/refresh",
+ operationId: "refreshAppInstallationRepair",
+ tags: ["Control panel"],
+ summary: "Recompute an installation's repair plan",
+ auth: "session",
+ responses: { 200: { description: "Repair plan recomputed" }, 404: { description: "No such installation" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "put",
+ path: "/v1/app/installations/:id/agent/bulk-settings",
+ operationId: "bulkUpdateInstallationAgentSettings",
+ tags: ["Control panel", "Agent automation"],
+ summary: "Apply agent settings across every repo in an installation",
+ auth: "session",
+ responses: { 200: { description: "Settings applied" }, 400: { description: "Malformed settings" }, 404: { description: "No such installation" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "get",
+ path: "/v1/app/kill-switch",
+ operationId: "getFleetKillSwitch",
+ tags: ["Control panel", "Operations"],
+ summary: "Read the fleet-wide agent kill switch",
+ auth: "session",
+ responses: { 200: { description: "Kill-switch state" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/app/kill-switch",
+ operationId: "setFleetKillSwitch",
+ tags: ["Control panel", "Operations"],
+ summary: "Set the fleet-wide agent kill switch",
+ auth: "session",
+ responses: { 200: { description: "Kill switch updated" }, 400: { description: "Malformed request" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/app/fleet/config-push",
+ operationId: "pushFleetConfig",
+ tags: ["Control panel", "Operations"],
+ summary: "Push a configuration change across the fleet",
+ auth: "session",
+ responses: { 202: { description: "Config push queued" }, 400: { description: "Malformed push" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: "/v1/app/miner-dashboard/refresh",
+ operationId: "refreshMinerDashboard",
+ tags: ["Control panel"],
+ summary: "Recompute the miner dashboard aggregates",
+ auth: "session",
+ responses: { 202: { description: "Refresh queued" }, ...SESSION_AUTH_RESPONSES },
+ },
+];
+
+/** The per-repo BYO-key and settings routes. Each key route exists twice -- once maintainer-facing
+ * under /v1/repos, once internal -- and the pair is specced together so they cannot drift. */
+const REPO_ROUTES: SpecEntry[] = [
+ ...(["ai-key", "linear-key"] as const).flatMap((kind): SpecEntry[] => {
+ const label = kind === "ai-key" ? "AI provider" : "Linear";
+ const Kind = kind === "ai-key" ? "Ai" : "Linear";
+ return [
+ {
+ method: "get",
+ path: `/v1/repos/:owner/:repo/${kind}`,
+ operationId: `get${Kind}Key`,
+ tags: ["Repositories", "Bring your own key"],
+ summary: `Report whether a ${label} key is configured for this repo (never the key itself)`,
+ auth: "session",
+ responses: { 200: { description: "Key presence and metadata" }, 404: { description: "Repo not registered" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: `/v1/repos/:owner/:repo/${kind}`,
+ operationId: `set${Kind}Key`,
+ tags: ["Repositories", "Bring your own key"],
+ summary: `Store a ${label} key for this repo`,
+ auth: "session",
+ responses: { 200: { description: "Key stored" }, 400: { description: "Malformed key" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "delete",
+ path: `/v1/repos/:owner/:repo/${kind}`,
+ operationId: `delete${Kind}Key`,
+ tags: ["Repositories", "Bring your own key"],
+ summary: `Remove this repo's ${label} key`,
+ auth: "session",
+ responses: { 200: { description: "Key removed" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "get",
+ path: `/v1/internal/repos/:owner/:repo/${kind}`,
+ operationId: `get${Kind}KeyInternal`,
+ tags: ["Bring your own key", "Internal"],
+ summary: `Read this repo's ${label} key state from the control plane`,
+ auth: "internal",
+ responses: { 200: { description: "Key presence and metadata" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "post",
+ path: `/v1/internal/repos/:owner/:repo/${kind}`,
+ operationId: `set${Kind}KeyInternal`,
+ tags: ["Bring your own key", "Internal"],
+ summary: `Store this repo's ${label} key from the control plane`,
+ auth: "internal",
+ responses: { 200: { description: "Key stored" }, 400: { description: "Malformed key" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ {
+ method: "delete",
+ path: `/v1/internal/repos/:owner/:repo/${kind}`,
+ operationId: `delete${Kind}KeyInternal`,
+ tags: ["Bring your own key", "Internal"],
+ summary: `Remove this repo's ${label} key from the control plane`,
+ auth: "internal",
+ responses: { 200: { description: "Key removed" }, ...INTERNAL_AUTH_RESPONSES },
+ },
+ ];
+ }),
+ {
+ method: "put",
+ path: "/v1/repos/:owner/:repo/settings",
+ operationId: "updateRepoSettings",
+ tags: ["Repositories"],
+ summary: "Replace a repo's LoopOver settings",
+ auth: "session",
+ responses: { 200: { description: "Settings updated" }, 400: { description: "Malformed settings" }, ...SESSION_AUTH_RESPONSES },
+ },
+ {
+ method: "put",
+ path: "/v1/repos/:owner/:repo/ai-review",
+ operationId: "updateRepoAiReviewMode",
+ tags: ["Repositories"],
+ summary: "Set a repo's AI-review mode",
+ auth: "session",
+ responses: { 200: { description: "AI-review mode updated" }, 400: { description: "Malformed mode" }, ...SESSION_AUTH_RESPONSES },
+ },
+];
+
+/** Register every entry in this batch. Called from buildOpenApiSpec. */
+export function registerOrbAndControlRouteSpecs(registry: OpenAPIRegistry): void {
+ for (const entry of [...ORB_ROUTES, ...INTERNAL_ORB_ROUTES, ...APP_ROUTES, ...REPO_ROUTES]) {
+ registerRouteSpec(registry, entry);
+ }
+}
+
+/** Exported for the meta-test that asserts every entry's declared auth matches the middleware that
+ * actually gates it. */
+export const ORB_AND_CONTROL_ROUTE_SPECS: readonly SpecEntry[] = [...ORB_ROUTES, ...INTERNAL_ORB_ROUTES, ...APP_ROUTES, ...REPO_ROUTES];
diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts
index 8e80a61bd2..25e4798440 100644
--- a/src/openapi/spec.ts
+++ b/src/openapi/spec.ts
@@ -1,4 +1,7 @@
import { OpenApiGeneratorV3, OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
+import { requiresApiToken } from "../auth/route-auth";
+import { registerOrbAndControlRouteSpecs } from "./orb-and-control-route-specs";
+import { registerInternalAndPublicRouteSpecs } from "./internal-and-public-route-specs";
import { z } from "zod";
import {
AdvisorySchema,
@@ -138,6 +141,29 @@ import {
WorkboardItemSchema,
} from "./schemas";
+/**
+ * The operationId and tag for a route registered from a loop (#9531).
+ *
+ * The literal `registerPath` calls above carry both inline; the handful registered from a table
+ * cannot, so they derive them here from the same rules. Derived rather than hand-listed because
+ * these tables exist precisely so that adding a sibling route is a one-line change -- requiring a
+ * second hand-written entry per route would give that up.
+ */
+function loopOperationMeta(method: string, path: string, tag: string): { operationId: string; tags: [string, ...string[]] } {
+ const stem = path
+ .split("/")
+ .filter((segment) => segment && segment !== "v1")
+ .map((segment) =>
+ segment
+ .split(/[-.]/)
+ .filter(Boolean)
+ .map((word) => `${word[0]!.toUpperCase()}${word.slice(1)}`)
+ .join(""),
+ )
+ .join("");
+ return { operationId: `${method}${stem}`, tags: [tag] };
+}
+
export function buildOpenApiSpec() {
const registry = new OpenAPIRegistry();
registry.register("Health", HealthSchema);
@@ -271,6 +297,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/health",
+ operationId: "getHealth",
+ tags: ["Meta"],
summary: "Service liveness probe",
responses: {
200: { description: "Service health", content: { "application/json": { schema: HealthSchema } } },
@@ -279,6 +307,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/mcp/compatibility",
+ operationId: "getMcpCompatibility",
+ tags: ["MCP"],
summary: "Public-safe API and MCP client compatibility metadata",
responses: {
200: { description: "Public-safe API and MCP compatibility metadata", content: { "application/json": { schema: McpCompatibilitySchema } } },
@@ -287,6 +317,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/stats",
+ operationId: "listPublicStats",
+ tags: ["Public"],
summary: "Public homepage aggregate stats",
responses: {
200: { description: "Public-safe homepage stats: lifetime PRs handled/merged/closed, gate + slop blocks, and reversal-grounded accuracy. Aggregate counts only.", content: { "application/json": { schema: PublicStatsSchema } } },
@@ -297,6 +329,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/github/repos/{owner}/{repo}/stats",
+ operationId: "listPublicGithubReposByOwnerByRepoStats",
+ tags: ["Public"],
summary: "Public GitHub stars and forks for an allowlisted repository",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -308,6 +342,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/repos/{owner}/{repo}/quality",
+ operationId: "getPublicReposByOwnerByRepoQuality",
+ tags: ["Public"],
summary: "Public repository quality summary for an opted-in repository",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -323,6 +359,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/registry/snapshot",
+ operationId: "getRegistrySnapshot",
+ tags: ["Registry"],
summary: "Latest Gittensor registry snapshot",
responses: {
200: { description: "Latest Gittensor registry snapshot", content: { "application/json": { schema: RegistrySnapshotSchema } } },
@@ -331,6 +369,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/registry/changes",
+ operationId: "listRegistryChanges",
+ tags: ["Registry"],
summary: "Diff between the two latest registry snapshots",
responses: {
200: { description: "Diff between latest registry snapshots", content: { "application/json": { schema: RegistryChangeReportSchema } } },
@@ -339,6 +379,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/scoring/model",
+ operationId: "getScoringModel",
+ tags: ["Scoring"],
summary: "Latest scoring model snapshot",
responses: {
200: { description: "Latest private scoring model snapshot", content: { "application/json": { schema: ScoringModelSnapshotSchema } } },
@@ -347,6 +389,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/finding-taxonomy",
+ operationId: "getFindingTaxonomy",
+ tags: ["API"],
summary: "Canonical AI-review finding taxonomy",
responses: {
200: { description: "Finding categories and the severity ladder", content: { "application/json": { schema: FindingTaxonomyDocumentSchema } } },
@@ -355,6 +399,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/enrichment-analyzers",
+ operationId: "listEnrichmentAnalyzers",
+ tags: ["API"],
summary: "REES enrichment analyzer taxonomy",
responses: {
200: { description: "Default profile and the registered enrichment analyzers", content: { "application/json": { schema: EnrichmentAnalyzersTaxonomyDocumentSchema } } },
@@ -363,6 +409,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/upstream/status",
+ operationId: "listUpstreamStatus",
+ tags: ["Registry"],
summary: "Upstream Gittensor source and ruleset drift status",
responses: {
200: { description: "Upstream Gittensor source/ruleset drift status", content: { "application/json": { schema: UpstreamStatusSchema } } },
@@ -371,6 +419,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/upstream/ruleset",
+ operationId: "getUpstreamRuleset",
+ tags: ["Registry"],
summary: "Latest normalized upstream Gittensor ruleset snapshot",
responses: {
200: { description: "Latest normalized upstream Gittensor ruleset snapshot", content: { "application/json": { schema: UpstreamRulesetSnapshotSchema } } },
@@ -380,6 +430,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/upstream/drift",
+ operationId: "getUpstreamDrift",
+ tags: ["Registry"],
summary: "Open and historical upstream drift reports",
responses: {
200: {
@@ -399,6 +451,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/scoring/preview",
+ operationId: "postScoringPreview",
+ tags: ["Scoring"],
summary: "Generate a scoring preview artifact for a candidate contribution",
responses: {
200: { description: "Private scoring preview artifact", content: { "application/json": { schema: ScorePreviewSchema } } },
@@ -408,6 +462,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/scoring/eligibility-plan",
+ operationId: "postScoringEligibilityPlan",
+ tags: ["Scoring"],
summary: "Derive a contributor eligibility plan from a scoring preview — REST mirror of loopover_get_eligibility_plan (#9301)",
responses: {
200: {
@@ -421,6 +477,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/scoring/explain-breakdown",
+ operationId: "postScoringExplainBreakdown",
+ tags: ["Scoring"],
summary: "Explain a score breakdown from a scoring preview — REST mirror of loopover_explain_score_breakdown (#9301)",
responses: {
200: {
@@ -434,6 +492,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/sync/status",
+ operationId: "listSyncStatus",
+ tags: ["API"],
summary: "Repository and installation sync status",
responses: {
200: { description: "Repository and installation sync status", content: { "application/json": { schema: SyncStatusSchema } } },
@@ -442,6 +502,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/readiness",
+ operationId: "listReadiness",
+ tags: ["API"],
summary: "Operational readiness summary for the hosted API",
responses: {
200: { description: "Operational readiness summary for hosted API, signal fidelity, and public-review preparation", content: { "application/json": { schema: ReadinessSchema } } },
@@ -450,6 +512,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/installations",
+ operationId: "listInstallations",
+ tags: ["API"],
summary: "List GitHub App installations and their health",
responses: {
200: {
@@ -468,6 +532,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/installations/{id}/health",
+ operationId: "getInstallationsByIdHealth",
+ tags: ["API"],
summary: "GitHub App installation health detail",
request: { params: z.object({ id: z.string() }) },
responses: {
@@ -478,6 +544,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/installations/{id}/repair",
+ operationId: "getInstallationsByIdRepair",
+ tags: ["API"],
summary: "GitHub App installation repair diagnostics",
request: { params: z.object({ id: z.string() }) },
responses: {
@@ -488,6 +556,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/installations/{id}/repair/refresh",
+ operationId: "postInstallationsByIdRepairRefresh",
+ tags: ["API"],
summary: "Recompute GitHub App installation repair diagnostics",
request: { params: z.object({ id: z.string() }) },
responses: {
@@ -498,6 +568,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/app/notification-model",
+ operationId: "getAppNotificationModel",
+ tags: ["Control panel"],
summary: "Opt-in notification model and PWA-readiness metadata",
responses: {
200: {
@@ -538,6 +610,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos",
+ operationId: "listRepos",
+ tags: ["API"],
summary: "List known repositories",
responses: {
200: { description: "Known repositories", content: { "application/json": { schema: RepositorySchema.array() } } },
@@ -546,6 +620,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}",
+ operationId: "getReposByOwnerByRepo",
+ tags: ["Repositories"],
summary: "Repository detail",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -556,6 +632,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/intelligence",
+ operationId: "getReposByOwnerByRepoIntelligence",
+ tags: ["Repositories"],
summary: "Canonical repository intelligence bundle",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -565,6 +643,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/issue-quality",
+ operationId: "getReposByOwnerByRepoIssueQuality",
+ tags: ["Repositories"],
summary: "Repository issue quality report",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -575,6 +655,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/gate-config/effective",
+ operationId: "getReposByOwnerByRepoGateConfigEffective",
+ tags: ["Repositories"],
summary: "Current effective self-tuned gate config for a repo (#6247)",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -589,6 +671,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/selftune/overrides/audit",
+ operationId: "getReposByOwnerByRepoSelftuneOverridesAudit",
+ tags: ["Repositories"],
summary: "Self-tune gate override audit trail for a repo (#9303)",
request: {
params: z.object({ owner: z.string(), repo: z.string() }),
@@ -611,6 +695,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "delete",
path: "/v1/repos/{owner}/{repo}/selftune/overrides",
+ operationId: "deleteReposByOwnerByRepoSelftuneOverrides",
+ tags: ["Repositories"],
summary: "Clear the live self-tune gate override for a repo (#9303)",
request: {
params: z.object({ owner: z.string(), repo: z.string() }),
@@ -635,6 +721,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/validate-linked-issue",
+ operationId: "postReposByOwnerByRepoValidateLinkedIssue",
+ tags: ["Repositories"],
summary: "Validate a linked issue for a planned change — REST mirror of loopover_validate_linked_issue (#9304)",
request: {
params: z.object({ owner: z.string(), repo: z.string() }),
@@ -655,6 +743,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/check-before-start",
+ operationId: "postReposByOwnerByRepoCheckBeforeStart",
+ tags: ["Repositories"],
summary: "Pre-work claim/duplicate check before starting an issue — REST mirror of loopover_check_before_start (#9304)",
request: {
params: z.object({ owner: z.string(), repo: z.string() }),
@@ -675,6 +765,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/loop/evaluate-escalation",
+ operationId: "postLoopEvaluateEscalation",
+ tags: ["Loop"],
summary: "Evaluate whether a loop outcome should escalate — REST mirror of loopover_evaluate_escalation (#9309)",
request: {
body: {
@@ -693,6 +785,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/loop/results-payload",
+ operationId: "postLoopResultsPayload",
+ tags: ["Loop"],
summary: "Compose a loop results-delivery payload — REST mirror of loopover_build_results_payload (#9309)",
request: {
body: {
@@ -711,6 +805,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/loop/progress-snapshot",
+ operationId: "postLoopProgressSnapshot",
+ tags: ["Loop"],
summary: "Compose a running-loop progress snapshot — REST mirror of loopover_build_progress_snapshot (#9309)",
request: {
body: {
@@ -729,6 +825,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/loop/intake-idea",
+ operationId: "postLoopIntakeIdea",
+ tags: ["Loop"],
summary: "Validate an idea submission and assemble its task-graph — REST mirror of loopover_intake_idea (#9309)",
request: {
body: {
@@ -747,6 +845,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/loop/plan-idea-claims",
+ operationId: "postLoopPlanIdeaClaims",
+ tags: ["Loop"],
summary: "Disposition an idea's task-graph into a claim plan — REST mirror of loopover_plan_idea_claims (#9309)",
request: {
body: {
@@ -765,6 +865,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/lint/pr-text",
+ operationId: "postLintPrText",
+ tags: ["Advisory checks"],
summary: "Lint a PR's commit messages + body — REST mirror of loopover_lint_pr_text (#9308)",
request: { body: { content: { "application/json": { schema: LintPrTextRequestSchema } } } },
responses: {
@@ -778,6 +880,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/lint/slop-risk",
+ operationId: "postLintSlopRisk",
+ tags: ["Advisory checks"],
summary: "Assess a changeset's slop risk — REST mirror of loopover_check_slop_risk (#9308)",
request: { body: { content: { "application/json": { schema: CheckSlopRiskRequestSchema } } } },
responses: {
@@ -791,6 +895,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/lint/improvement-potential",
+ operationId: "postLintImprovementPotential",
+ tags: ["Advisory checks"],
summary: "Score a changeset's structural improvement potential — REST mirror of loopover_check_improvement_potential (#9308)",
request: { body: { content: { "application/json": { schema: CheckImprovementPotentialRequestSchema } } } },
responses: {
@@ -804,6 +910,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/lint/open-pr-pressure",
+ operationId: "postLintOpenPrPressure",
+ tags: ["Advisory checks"],
summary: "Simulate open-PR queue pressure for a repo — REST mirror of loopover_simulate_open_pr_pressure (#9308)",
request: { body: { content: { "application/json": { schema: SimulateOpenPrPressureRequestSchema } } } },
responses: {
@@ -817,6 +925,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/lint/boundary-tests",
+ operationId: "postLintBoundaryTests",
+ tags: ["Advisory checks"],
summary: "Suggest boundary tests for a changeset — REST mirror of loopover_suggest_boundary_tests (#9308)",
request: { body: { content: { "application/json": { schema: SuggestBoundaryTestsRequestSchema } } } },
responses: {
@@ -830,6 +940,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/lint/test-evidence",
+ operationId: "postLintTestEvidence",
+ tags: ["Advisory checks"],
summary: "Classify a changeset's test evidence — REST mirror of loopover_check_test_evidence (#9308)",
request: { body: { content: { "application/json": { schema: CheckTestEvidenceRequestSchema } } } },
responses: {
@@ -843,6 +955,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/lint/issue-slop",
+ operationId: "postLintIssueSlop",
+ tags: ["Advisory checks"],
summary: "Assess an issue's slop risk from its title/body — REST mirror of loopover_check_issue_slop (#9308)",
request: { body: { content: { "application/json": { schema: CheckIssueSlopRequestSchema } } } },
responses: {
@@ -856,6 +970,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/validate/focus-manifest",
+ operationId: "postValidateFocusManifest",
+ tags: ["Advisory checks"],
summary: "Validate a .loopover focus-manifest config — REST mirror of loopover_validate_config (#9308)",
request: { body: { content: { "application/json": { schema: ValidateFocusManifestRequestSchema } } } },
responses: {
@@ -869,6 +985,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/live-gate-thresholds",
+ operationId: "listReposByOwnerByRepoLiveGateThresholds",
+ tags: ["Repositories"],
summary: "Live self-tuned gate thresholds for AMS probe (#6486)",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -883,6 +1001,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/maintainer-noise",
+ operationId: "getReposByOwnerByRepoMaintainerNoise",
+ tags: ["Repositories"],
summary: "Maintainer queue-noise triage report for a repository (#9302)",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -898,6 +1018,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/ams-miner-cohort",
+ operationId: "getReposByOwnerByRepoAmsMinerCohort",
+ tags: ["Repositories"],
summary: "AMS-vs-human contributor-mix cohort comparison for a repository (#9302)",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -913,6 +1035,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/gate-precision",
+ operationId: "getReposByOwnerByRepoGatePrecision",
+ tags: ["Repositories"],
summary: "Per-gate-type false-positive precision measurement for a repository (#9302)",
request: {
params: z.object({ owner: z.string(), repo: z.string() }),
@@ -931,6 +1055,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/outcome-calibration",
+ operationId: "getReposByOwnerByRepoOutcomeCalibration",
+ tags: ["Repositories"],
summary: "Slop-band and recommendation outcome calibration for a repository (#9302)",
request: {
params: z.object({ owner: z.string(), repo: z.string() }),
@@ -949,6 +1075,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/activation-preview",
+ operationId: "getReposByOwnerByRepoActivationPreview",
+ tags: ["Repositories"],
summary: "Deterministic maintainer activation preview for a repository (#9302)",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -964,6 +1092,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/outcome-patterns",
+ operationId: "listReposByOwnerByRepoOutcomePatterns",
+ tags: ["Repositories"],
summary: "Accepted and rejected pull request outcome patterns for a repository",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -974,6 +1104,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/registration-readiness",
+ operationId: "listReposByOwnerByRepoRegistrationReadiness",
+ tags: ["Repositories"],
summary: "Gittensor registration readiness signal for repository owners",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -983,6 +1115,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/gittensor-config-recommendation",
+ operationId: "getReposByOwnerByRepoGittensorConfigRecommendation",
+ tags: ["Repositories"],
summary: "Recommended Gittensor configuration for a repository",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -992,6 +1126,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/focus-manifest",
+ operationId: "getReposByOwnerByRepoFocusManifest",
+ tags: ["Repositories"],
summary: "Repository focus manifest and compiled policy",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1002,6 +1138,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/focus-manifest/refresh",
+ operationId: "postReposByOwnerByRepoFocusManifestRefresh",
+ tags: ["Repositories"],
summary: "Refresh the persisted focus manifest from the repository file",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1012,6 +1150,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "put",
path: "/v1/repos/{owner}/{repo}/focus-manifest",
+ operationId: "putReposByOwnerByRepoFocusManifest",
+ tags: ["Repositories"],
summary: "Persist an API-backed focus manifest for a repository",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1023,6 +1163,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/agent/pending-actions",
+ operationId: "listReposByOwnerByRepoAgentPendingActions",
+ tags: ["Repositories"],
summary: "Maintainer-scoped agent approval queue of pending staged actions",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1036,6 +1178,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/agent/pending-actions",
+ operationId: "postReposByOwnerByRepoAgentPendingActions",
+ tags: ["Repositories"],
summary: "Stage an agent action into the approval queue for maintainer review",
request: {
params: z.object({ owner: z.string(), repo: z.string() }),
@@ -1054,6 +1198,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/agent/pending-actions/{id}/{decision}",
+ operationId: "postReposByOwnerByRepoAgentPendingActionsByIdByDecision",
+ tags: ["Repositories"],
summary: "Accept (execute) or reject a staged agent action in the approval queue",
request: { params: z.object({ owner: z.string(), repo: z.string(), id: z.string(), decision: z.enum(["accept", "reject"]) }) },
responses: {
@@ -1070,6 +1216,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/agent/audit-feed",
+ operationId: "getReposByOwnerByRepoAgentAuditFeed",
+ tags: ["Repositories"],
summary: "Maintainer-scoped agent audit feed of executed actions and approval decisions",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1117,6 +1265,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/pulls/{number}/incident-reports",
+ operationId: "postReposByOwnerByRepoPullsByNumberIncidentReports",
+ tags: ["Repositories"],
summary: "Record a post-merge incident report for a pull request",
request: {
params: z.object({ owner: z.string(), repo: z.string(), number: z.string() }),
@@ -1147,6 +1297,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/app/incident-reports",
+ operationId: "postAppIncidentReports",
+ tags: ["Control panel"],
summary: "Record a post-merge incident report from the operator side",
request: {
body: {
@@ -1178,6 +1330,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/app/self-dogfood/registration-pack",
+ operationId: "getAppSelfDogfoodRegistrationPack",
+ tags: ["Control panel"],
summary: "Self-dogfood registration pack for the LoopOver repository",
responses: {
200: { description: "Private self-dogfood registration pack for the LoopOver repo", content: { "application/json": { schema: z.record(z.string(), z.unknown()) } } },
@@ -1187,6 +1341,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/self-dogfood-registration-pack",
+ operationId: "getReposByOwnerByRepoSelfDogfoodRegistrationPack",
+ tags: ["Repositories"],
summary: "Self-dogfood registration pack when the repository matches the configured target",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1197,6 +1353,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/onboarding-pack/preview",
+ operationId: "getReposByOwnerByRepoOnboardingPackPreview",
+ tags: ["Repositories"],
summary: "Preview the onboarding pack for an accepted repository",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1208,6 +1366,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/contributor-issue-drafts/generate",
+ operationId: "postReposByOwnerByRepoContributorIssueDraftsGenerate",
+ tags: ["Repositories"],
summary: "Generate maintainer-reviewed contributor issue drafts",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1219,6 +1379,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/issue-plan-drafts/generate",
+ operationId: "postReposByOwnerByRepoIssuePlanDraftsGenerate",
+ tags: ["Repositories"],
summary: "AI-plan repo issue drafts from a maintainer goal",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1230,6 +1392,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/settings",
+ operationId: "listReposByOwnerByRepoSettings",
+ tags: ["Repositories"],
summary: "Repository automation settings",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1239,6 +1403,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/automation-state",
+ operationId: "getReposByOwnerByRepoAutomationState",
+ tags: ["Repositories"],
summary: "Derived agent automation state for a repository",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1252,6 +1418,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/repo-docs/refresh",
+ operationId: "postReposByOwnerByRepoRepoDocsRefresh",
+ tags: ["Repositories"],
summary: "Open (or find the already-open) AGENTS.md/CLAUDE.md generation pull request",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1261,6 +1429,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/repos/{owner}/{repo}/settings-preview",
+ operationId: "postReposByOwnerByRepoSettingsPreview",
+ tags: ["Repositories"],
summary: "Dry-run the public surface decision for a sample pull request",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
@@ -1271,6 +1441,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/pulls/{number}/maintainer-packet",
+ operationId: "getReposByOwnerByRepoPullsByNumberMaintainerPacket",
+ tags: ["Repositories"],
summary: "Maintainer review packet for a pull request",
request: { params: z.object({ owner: z.string(), repo: z.string(), number: z.string() }) },
responses: {
@@ -1280,6 +1452,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/pulls/{number}/reviewability",
+ operationId: "getReposByOwnerByRepoPullsByNumberReviewability",
+ tags: ["Repositories"],
summary: "Pull request reviewability score and maintainer action",
request: { params: z.object({ owner: z.string(), repo: z.string(), number: z.string() }) },
responses: {
@@ -1289,6 +1463,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/pulls/{number}/ai-review-findings",
+ operationId: "listReposByOwnerByRepoPullsByNumberAiReviewFindings",
+ tags: ["Repositories"],
summary: "A PR author's own structured, published AI-review findings",
request: {
params: z.object({ owner: z.string(), repo: z.string(), number: z.string() }),
@@ -1309,6 +1485,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/contributors/{login}/profile",
+ operationId: "getContributorsByLoginProfile",
+ tags: ["Contributors"],
summary: "Contributor evidence profile",
request: { params: z.object({ login: z.string() }) },
responses: {
@@ -1318,6 +1496,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/contributors/{login}/decision-pack",
+ operationId: "getContributorsByLoginDecisionPack",
+ tags: ["Contributors"],
summary: "Canonical contributor decision pack",
request: { params: z.object({ login: z.string() }) },
responses: {
@@ -1331,6 +1511,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/contributors/{login}/open-pr-monitor",
+ operationId: "getContributorsByLoginOpenPrMonitor",
+ tags: ["Contributors"],
summary: "Contributor open-PR monitor with classifications and next-step packets",
request: { params: z.object({ login: z.string() }) },
responses: {
@@ -1343,6 +1525,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/contributors/{login}/pr-outcomes",
+ operationId: "listContributorsByLoginPrOutcomes",
+ tags: ["Contributors"],
summary: "Contributor post-merge PR outcome history",
request: {
params: z.object({ login: z.string() }),
@@ -1358,6 +1542,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/contributors/{login}/notifications",
+ operationId: "listContributorsByLoginNotifications",
+ tags: ["Contributors"],
summary: "Contributor badge notification feed",
request: { params: z.object({ login: z.string() }) },
responses: {
@@ -1370,6 +1556,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/contributors/{login}/notifications/read",
+ operationId: "postContributorsByLoginNotificationsRead",
+ tags: ["Contributors"],
summary: "Mark contributor notifications read",
request: {
params: z.object({ login: z.string() }),
@@ -1392,6 +1580,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/contributors/{login}/watches",
+ operationId: "listContributorsByLoginWatches",
+ tags: ["Contributors"],
summary: "List contributor issue-watch subscriptions — REST mirror of loopover_watch_issues action=list (#9306)",
request: { params: z.object({ login: z.string() }) },
responses: {
@@ -1406,6 +1596,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/contributors/{login}/watches",
+ operationId: "postContributorsByLoginWatches",
+ tags: ["Contributors"],
summary: "Subscribe to a repo's new issues — REST mirror of loopover_watch_issues action=watch (#9306)",
request: {
params: z.object({ login: z.string() }),
@@ -1426,6 +1618,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "delete",
path: "/v1/contributors/{login}/watches",
+ operationId: "deleteContributorsByLoginWatches",
+ tags: ["Contributors"],
summary: "Unsubscribe from a repo's issue watches — REST mirror of loopover_watch_issues action=unwatch (#9306)",
request: {
params: z.object({ login: z.string() }),
@@ -1446,6 +1640,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/contributors/{login}/repos/{owner}/{repo}/decision",
+ operationId: "getContributorsByLoginReposByOwnerByRepoDecision",
+ tags: ["Contributors"],
summary: "Repository-specific contributor decision",
request: { params: z.object({ login: z.string(), owner: z.string(), repo: z.string() }) },
responses: {
@@ -1456,6 +1652,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/preflight/pr",
+ operationId: "postPreflightPr",
+ tags: ["Preflight"],
summary: "Run submission preflight for a pull request",
responses: {
200: { description: "Submission preflight result", content: { "application/json": { schema: PreflightResultSchema } } },
@@ -1465,6 +1663,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/preflight/review-risk",
+ operationId: "postPreflightReviewRisk",
+ tags: ["Preflight"],
summary: "Explain review risk for a planned pull request",
responses: {
200: { description: "Review-risk explanation with preflight, role context, and recommendation", content: { "application/json": { schema: ReviewRiskExplanationSchema } } },
@@ -1475,6 +1675,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/preflight/local-diff",
+ operationId: "postPreflightLocalDiff",
+ tags: ["Preflight"],
summary: "Run preflight against a local diff",
responses: {
200: { description: "Local diff preflight result", content: { "application/json": { schema: LocalDiffPreflightResultSchema } } },
@@ -1484,6 +1686,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/local/branch-analysis",
+ operationId: "postLocalBranchAnalysis",
+ tags: ["Local"],
summary: "Analyze a local branch for MCP clients",
responses: {
200: { description: "Private local branch analysis for MCP clients", content: { "application/json": { schema: LocalBranchAnalysisSchema } } },
@@ -1494,6 +1698,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/agent/runs",
+ operationId: "postAgentRuns",
+ tags: ["Agent automation"],
summary: "Queue an agent run",
responses: {
202: { description: "Copilot-only agent run queued", content: { "application/json": { schema: AgentRunBundleSchema } } },
@@ -1504,6 +1710,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/agent/runs",
+ operationId: "listAgentRuns",
+ tags: ["Agent automation"],
summary: "List persisted agent runs for an actor",
request: {
query: z.object({
@@ -1536,6 +1744,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/agent/runs/{id}",
+ operationId: "getAgentRunsById",
+ tags: ["Agent automation"],
summary: "Persisted agent run bundle",
request: { params: z.object({ id: z.string() }) },
responses: {
@@ -1546,6 +1756,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/opportunities/find",
+ operationId: "postOpportunitiesFind",
+ tags: ["Discovery"],
summary: "Find cross-repo contribution opportunities (#9310)",
request: {
body: {
@@ -1565,6 +1777,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/issue-rag/retrieve",
+ operationId: "postIssueRagRetrieve",
+ tags: ["Discovery"],
summary: "Retrieve issue-centric RAG context for the miner analyze phase (#9310)",
request: {
body: {
@@ -1590,6 +1804,7 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path,
+ ...loopOperationMeta("post", path, "Agent automation"),
summary,
responses: {
200: { description: "Agent run completed with deterministic ranked actions", content: { "application/json": { schema: AgentRunBundleSchema } } },
@@ -1602,6 +1817,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/bounties",
+ operationId: "listBounties",
+ tags: ["Bounties"],
summary: "List known bounty records",
responses: {
200: { description: "Known bounty records", content: { "application/json": { schema: BountySchema.array() } } },
@@ -1610,6 +1827,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/bounties/{id}/advisory",
+ operationId: "getBountiesByIdAdvisory",
+ tags: ["Bounties"],
summary: "Bounty lifecycle advisory",
request: { params: z.object({ id: z.string() }) },
responses: {
@@ -1620,6 +1839,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/bounties/{id}/lifecycle",
+ operationId: "getBountiesByIdLifecycle",
+ tags: ["Bounties"],
summary: "Bounty lifecycle transition history",
request: { params: z.object({ id: z.string() }) },
responses: {
@@ -1630,6 +1851,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/github/webhook",
+ operationId: "postGithubWebhook",
+ tags: ["Webhooks"],
summary: "Receive a GitHub webhook delivery",
responses: {
202: { description: "Webhook queued" },
@@ -1639,6 +1862,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/decision-ledger/verify",
+ operationId: "getPublicDecisionLedgerVerify",
+ tags: ["Public"],
summary: "Verify a window of the hash-chained decision ledger (resumable via afterSeq)",
responses: {
200: { description: "Window verified clean; nextAfterSeq is the resume cursor (null at the tip). Every response also carries tipSeq/tipHash/totalCount for third-party checkpointing, and prunedRecords — the count of rows whose record preimage was legitimately pruned by the published retention window (chain checks still hold for them; only the content re-check is impossible, and the committed digest stays published)." },
@@ -1648,6 +1873,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/decision-ledger/row/{seq}",
+ operationId: "getPublicDecisionLedgerRowBySeq",
+ tags: ["Public"],
summary: "Fetch one decision-ledger row by seq, so an external anchor can be bound back to the live chain",
request: { params: z.object({ seq: z.string() }) },
responses: {
@@ -1659,6 +1886,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/decision-ledger/anchor-key",
+ operationId: "getPublicDecisionLedgerAnchorKey",
+ tags: ["Public"],
summary: "Published anchor-signing public keys with their full rotation history, for verifying an externally-published ledger anchor",
responses: {
200: { description: "{ keys: [{ keyId, publicKeySpki, notBefore, notAfter }], currentKeyId } — retired keys are retained so anchors signed under them stay verifiable; currentKeyId is null when unconfigured or the rotation state is ambiguous" },
@@ -1667,6 +1896,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/decision-ledger/anchors",
+ operationId: "listPublicDecisionLedgerAnchors",
+ tags: ["Public"],
summary: "Every external anchoring attempt, success and failure, paginated newest-first — anchoring's own health as a public fact",
request: { query: z.object({ backend: z.enum(["rekor", "git", "ots"]).optional(), before: z.string().optional(), limit: z.string().optional() }) },
responses: {
@@ -1676,6 +1907,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/decision-records/{owner}/{repo}/{pull}",
+ operationId: "getPublicDecisionRecordsByOwnerByRepoByPull",
+ tags: ["Public"],
summary: "Fetch the latest published decision record for a PR, verbatim, plus its content digest",
request: { params: z.object({ owner: z.string(), repo: z.string(), pull: z.string() }) },
responses: {
@@ -1687,6 +1920,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/public/eval-scores",
+ operationId: "listPublicEvalScores",
+ tags: ["Public"],
summary: "Fetch EvalScoreRecords (#9215) -- the objective-eval-provider transport, digest-committed and independently re-derivable",
request: { query: z.object({ subject: z.string().optional(), since: z.string().optional() }) },
responses: {
@@ -1697,6 +1932,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/orb/ingest",
+ operationId: "postOrbIngest",
+ tags: ["ORB"],
summary: "Ingest a batch of Orb events",
responses: {
200: { description: "Batch accepted; returns { accepted: number }" },
@@ -1706,6 +1943,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/auth/github/start",
+ operationId: "getAuthGithubStart",
+ tags: ["Auth"],
summary: "Start GitHub web OAuth",
responses: {
302: { description: "Redirects to GitHub web OAuth" },
@@ -1715,6 +1954,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/auth/github/callback",
+ operationId: "getAuthGithubCallback",
+ tags: ["Auth"],
summary: "Complete GitHub web OAuth and redirect to the app",
responses: {
302: { description: "Completes GitHub web OAuth and redirects to the app" },
@@ -1729,6 +1970,7 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path,
+ ...loopOperationMeta("post", path, "Auth"),
summary,
responses: {
200: { description: "Auth request completed" },
@@ -1742,6 +1984,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/auth/session",
+ operationId: "getAuthSession",
+ tags: ["Auth"],
summary: "Current authentication session",
responses: {
200: { description: "Current auth session, or signed_out when no app session is present" },
@@ -1750,6 +1994,14 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/auth/github/token",
+ operationId: "postAuthGithubToken",
+ tags: ["Auth"],
+ // #9531: SESSION ONLY, declared rather than inferred. Both former models got this wrong in
+ // different directions -- isProtectedPath published the generic bearer+cookie pair (a bearer
+ // alone gets a 403 from this handler: it checks `identity.kind !== "session"`), and the real
+ // token gate exempts all of `/v1/auth/*`, which would publish it as needing nothing. It is
+ // gated, in the handler, on a browser session specifically.
+ security: [{ LoopOverSessionCookie: [] }],
summary: "Fetch the current session's live GitHub token (for AMS git operations)",
responses: {
200: { description: "The session's GitHub token", content: { "application/json": { schema: z.object({ token: z.string() }) } } },
@@ -1761,6 +2013,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/app/overview",
+ operationId: "getAppOverview",
+ tags: ["Control panel"],
summary: "Live app overview assembled from backend data",
responses: {
200: { description: "Live app overview assembled from backend data", content: { "application/json": { schema: z.record(z.string(), z.unknown()) } } },
@@ -1782,6 +2036,7 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path,
+ ...loopOperationMeta("get", path, "Control panel"),
summary,
responses: {
200: { description: "Live app API response", content: { "application/json": { schema: z.record(z.string(), z.unknown()) } } },
@@ -1792,6 +2047,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/app/selfhost/queue/dead/{id}/replay",
+ operationId: "postAppSelfhostQueueDeadByIdReplay",
+ tags: ["Control panel"],
summary: "Replay a dead-letter queue job",
request: {
params: z.object({
@@ -1810,6 +2067,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "delete",
path: "/v1/app/selfhost/queue/dead/{id}",
+ operationId: "deleteAppSelfhostQueueDeadById",
+ tags: ["Control panel"],
summary: "Delete a dead-letter queue job",
request: {
params: z.object({
@@ -1828,6 +2087,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "delete",
path: "/v1/app/selfhost/queue/dead",
+ operationId: "deleteAppSelfhostQueueDead",
+ tags: ["Control panel"],
summary: "Purge all dead-letter queue jobs",
responses: {
200: { description: "Dead-letter jobs purged", content: { "application/json": { schema: z.record(z.string(), z.unknown()) } } },
@@ -1839,6 +2100,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/app/selfhost/queue/dead",
+ operationId: "getAppSelfhostQueueDead",
+ tags: ["Control panel"],
summary: "List dead-letter queue jobs",
request: {
query: z.object({
@@ -1863,6 +2126,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/app/analytics/weekly-value-report",
+ operationId: "getAppAnalyticsWeeklyValueReport",
+ tags: ["Control panel"],
summary: "Weekly value report",
request: {
query: z.object({
@@ -1903,6 +2168,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/app/skipped-pr-audit",
+ operationId: "getAppSkippedPrAudit",
+ tags: ["Control panel"],
summary: "Audit of pull requests the review agent skipped",
request: {
query: z.object({
@@ -1938,6 +2205,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/app/commands/preview",
+ operationId: "postAppCommandsPreview",
+ tags: ["Control panel"],
summary: "Dry-run a sanitized @loopover command response",
responses: {
200: { description: "Maintainer dry-run preview of a sanitized @loopover command response (no GitHub mutation)", content: { "application/json": { schema: CommandPreviewResponseSchema } } },
@@ -1954,6 +2223,7 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path,
+ ...loopOperationMeta("post", path, "Control panel"),
summary,
responses: {
200: { description: "Live app mutation or preview response", content: { "application/json": { schema: z.record(z.string(), z.unknown()) } } },
@@ -1968,6 +2238,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "get",
path: "/v1/internal/provider-credentials/{provider}",
+ operationId: "getInternalProviderCredentialsByProvider",
+ tags: ["Internal"],
summary: "Read the secret-free status of a stored instance subscription credential",
request: { params: z.object({ provider: z.enum(["claude-code", "codex"]) }) },
responses: {
@@ -1989,6 +2261,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/internal/provider-credentials/{provider}",
+ operationId: "postInternalProviderCredentialsByProvider",
+ tags: ["Internal"],
summary: "Store or replace an instance subscription credential, encrypted at rest",
request: {
params: z.object({ provider: z.enum(["claude-code", "codex"]) }),
@@ -2004,6 +2278,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "delete",
path: "/v1/internal/provider-credentials/{provider}",
+ operationId: "deleteInternalProviderCredentialsByProvider",
+ tags: ["Internal"],
summary: "Clear a stored instance subscription credential, falling back to the secret file or boot env",
request: { params: z.object({ provider: z.enum(["claude-code", "codex"]) }) },
responses: {
@@ -2015,6 +2291,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/internal/jobs/refresh-registry",
+ operationId: "postInternalJobsRefreshRegistry",
+ tags: ["Internal"],
summary: "Queue a registry refresh job",
responses: {
202: { description: "Registry refresh queued" },
@@ -2024,6 +2302,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/internal/jobs/backfill-registered-repos",
+ operationId: "postInternalJobsBackfillRegisteredRepos",
+ tags: ["Internal"],
summary: "Queue a registered-repository backfill job",
responses: {
202: { description: "Registered repo backfill queued" },
@@ -2033,6 +2313,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/internal/jobs/backfill-repo-segment",
+ operationId: "postInternalJobsBackfillRepoSegment",
+ tags: ["Internal"],
summary: "Queue a repository segment backfill job",
responses: {
202: { description: "Repository segment backfill queued" },
@@ -2043,6 +2325,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/internal/jobs/backfill-pr-details",
+ operationId: "postInternalJobsBackfillPrDetails",
+ tags: ["Internal"],
summary: "Queue an open pull request detail backfill job",
responses: {
202: { description: "Open PR detail backfill queued" },
@@ -2053,6 +2337,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/internal/jobs/generate-review-recap",
+ operationId: "postInternalJobsGenerateReviewRecap",
+ tags: ["Internal"],
summary: "Queue a maintainer review recap digest job",
responses: {
202: { description: "Maintainer review recap digest queued (#1963)" },
@@ -2074,6 +2360,7 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path,
+ ...loopOperationMeta("post", path, "Internal"),
summary,
responses: {
202: { description: "Internal job queued" },
@@ -2084,6 +2371,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/internal/bounties/import",
+ operationId: "postInternalBountiesImport",
+ tags: ["Internal"],
summary: "Import a bounty snapshot",
responses: {
200: { description: "Bounty snapshot imported" },
@@ -2091,6 +2380,13 @@ export function buildOpenApiSpec() {
},
});
+ // #9531: the ORB ingress, the control-panel app surface, and the per-repo key/settings routes.
+ // Registered from their own module rather than inline here because each entry declares an auth
+ // level that DERIVES its security stanza, instead of having one bolted on afterwards by
+ // applySecurityMetadata's path-prefix guesswork.
+ registerOrbAndControlRouteSpecs(registry);
+ registerInternalAndPublicRouteSpecs(registry);
+
const generator = new OpenApiGeneratorV3(registry.definitions);
const document = generator.generateDocument({
openapi: "3.0.3",
@@ -2108,6 +2404,16 @@ type GeneratedOperation = NonNullable
security?: Array>;
};
+/**
+ * Attach the security schemes, and the per-operation stanza derived from the REAL gate (#9531).
+ *
+ * This used to consult `isProtectedPath`, a second path-prefix model of the same policy that had
+ * already drifted out of agreement with the middleware -- it called every `/v1/*` route protected
+ * bar a short literal list, so the document advertised a bearer requirement on the entire
+ * `/v1/public/decision-ledger/*` family, all of which answer 200 unauthenticated. Now it asks
+ * `requiresApiToken`, the function the app itself gates on, so the document cannot claim something
+ * the runtime does not enforce.
+ */
function applySecurityMetadata(document: GeneratedOpenApiDocument): GeneratedOpenApiDocument {
document.components = {
...(document.components ?? {}),
@@ -2124,21 +2430,42 @@ function applySecurityMetadata(document: GeneratedOpenApiDocument): GeneratedOpe
name: "loopover_session",
description: "HttpOnly browser session cookie set by GitHub web OAuth.",
},
+ // #9531: the ORB ingress does not authenticate the way the rest of the API does, and the old
+ // path-prefix security model published it as needing no credential at all. It needs a
+ // different one.
+ OrbBearer: {
+ type: "http",
+ scheme: "bearer",
+ description: "ORB-issued instance token, minted by POST /v1/orb/token and presented by a self-hosted ORB instance on the relay endpoints. Not a LoopOver API token.",
+ },
+ OrbWebhookSignature: {
+ type: "apiKey",
+ in: "header",
+ name: "x-loopover-signature",
+ description: "HMAC signature over the raw request body, verified against the instance's shared secret. The webhook carries no bearer token.",
+ },
},
};
for (const [path, pathItem] of Object.entries(document.paths)) {
- if (!pathItem || !isProtectedPath(path)) continue;
+ if (!pathItem) continue;
+ // The document writes `{param}`; the gate matches the concrete path a caller sends. Substituting
+ // a placeholder segment keeps the two comparable -- every exemption in requiresApiToken that
+ // covers a dynamic route is a regex over non-slash segments, which any placeholder satisfies.
+ const gated = requiresApiToken(path.replace(/\{[^}]+\}/g, "_"));
for (const method of ["get", "post", "put", "patch", "delete"] as const) {
const operation = pathItem[method] as GeneratedOperation | undefined;
- if (operation) operation.security = [{ LoopOverBearer: [] }, { LoopOverSessionCookie: [] }];
+ if (!operation) continue;
+ // NEVER overwrite a declared stanza. Routes registered through defineRoute already carry the
+ // scheme their own `auth` implies -- OrbBearer for the AMS ingress, a signature header for the
+ // webhook, bearer-only for `/v1/internal/*`, `[]` for public -- and clobbering that with the
+ // generic pair is precisely the bug this function used to have: every auth level in the
+ // published document compiled to the same requirement, so it advertised a LoopOver bearer on
+ // the ORB ingress route whose own comment says it takes a shared-secret header instead.
+ if (operation.security !== undefined) continue;
+ // Only the legacy registerPath calls reach here -- they declare no auth, so the gate is the
+ // only thing that knows.
+ if (gated) operation.security = [{ LoopOverBearer: [] }, { LoopOverSessionCookie: [] }];
}
}
return document;
}
-
-function isProtectedPath(path: string): boolean {
- if (path === "/health" || path === "/openapi.json" || path === "/mcp" || path === "/v1/mcp/compatibility" || path === "/v1/public/stats" || path === "/v1/public/github/repos/{owner}/{repo}/stats" || path === "/v1/public/repos/{owner}/{repo}/quality") return false;
- if (path.startsWith("/v1/auth/")) return path === "/v1/auth/github/token";
- if (path === "/v1/github/webhook") return false;
- return path.startsWith("/v1/");
-}
diff --git a/src/openapi/unspecced-routes-baseline.json b/src/openapi/unspecced-routes-baseline.json
deleted file mode 100644
index a5eef45230..0000000000
--- a/src/openapi/unspecced-routes-baseline.json
+++ /dev/null
@@ -1,92 +0,0 @@
-[
- "DELETE /v1/internal/repos/{owner}/{repo}/ai-key",
- "DELETE /v1/internal/repos/{owner}/{repo}/linear-key",
- "DELETE /v1/repos/{owner}/{repo}/ai-key",
- "DELETE /v1/repos/{owner}/{repo}/linear-key",
- "GET /loopover/shot",
- "GET /openapi.json",
- "GET /v1/app/installations",
- "GET /v1/app/installations/{id}/health",
- "GET /v1/app/installations/{id}/repair",
- "GET /v1/app/kill-switch",
- "GET /v1/drafts/auth/callback",
- "GET /v1/drafts/{id}",
- "GET /v1/internal/audit-labels",
- "GET /v1/internal/calibration",
- "GET /v1/internal/calibration-trend",
- "GET /v1/internal/calibration/knobs",
- "GET /v1/internal/calibration/satisfaction-floor",
- "GET /v1/internal/decision",
- "GET /v1/internal/fairness/contributors",
- "GET /v1/internal/fairness/contributors/{login}",
- "GET /v1/internal/fleet/analytics",
- "GET /v1/internal/ops/stats",
- "GET /v1/internal/orb/installations",
- "GET /v1/internal/orb/instances",
- "GET /v1/internal/parity",
- "GET /v1/internal/predicted-agreement",
- "GET /v1/internal/repos/{owner}/{repo}/ai-key",
- "GET /v1/internal/repos/{owner}/{repo}/contribution-policy",
- "GET /v1/internal/repos/{owner}/{repo}/linear-key",
- "GET /v1/internal/retention/preview",
- "GET /v1/internal/status",
- "GET /v1/mcp/enrichment-analyzers",
- "GET /v1/mcp/finding-taxonomy",
- "GET /v1/orb/oauth/callback",
- "GET /v1/public/repos/{owner}/{repo}/badge.json",
- "GET /v1/public/repos/{owner}/{repo}/badge.svg",
- "GET /v1/public/subnet-interface",
- "GET /v1/repos/{owner}/{repo}/ai-key",
- "GET /v1/repos/{owner}/{repo}/linear-key",
- "POST /v1/ams/ingest",
- "POST /v1/app/fleet/config-push",
- "POST /v1/app/installations/{id}/repair/refresh",
- "POST /v1/app/kill-switch",
- "POST /v1/app/miner-dashboard/refresh",
- "POST /v1/contributors/{login}/ams-notifications",
- "POST /v1/drafts",
- "POST /v1/internal/audit-labels/adjudicate",
- "POST /v1/internal/calibration/loosen-satisfaction-floor",
- "POST /v1/internal/jobs/backfill-contributor-gate-history/run",
- "POST /v1/internal/jobs/backfill-pr-details/run",
- "POST /v1/internal/jobs/backfill-registered-repos/run",
- "POST /v1/internal/jobs/backfill-repo-segment/run",
- "POST /v1/internal/jobs/build-contributor-decision-packs/run",
- "POST /v1/internal/jobs/file-upstream-drift-issues/run",
- "POST /v1/internal/jobs/generate-review-recap/run",
- "POST /v1/internal/jobs/generate-signal-snapshots/run",
- "POST /v1/internal/jobs/generate-weekly-value-report/run",
- "POST /v1/internal/jobs/rag-index",
- "POST /v1/internal/jobs/refresh-contributor-activity",
- "POST /v1/internal/jobs/refresh-contributor-activity/run",
- "POST /v1/internal/jobs/refresh-installation-health/run",
- "POST /v1/internal/jobs/refresh-registry/run",
- "POST /v1/internal/jobs/refresh-scoring-model/run",
- "POST /v1/internal/jobs/refresh-upstream-drift/run",
- "POST /v1/internal/jobs/regate-pr",
- "POST /v1/internal/jobs/rollup-product-usage",
- "POST /v1/internal/jobs/rollup-product-usage/run",
- "POST /v1/internal/orb/enrollments",
- "POST /v1/internal/orb/enrollments/{enrollId}/revoke",
- "POST /v1/internal/orb/installations/backfill",
- "POST /v1/internal/orb/installations/register",
- "POST /v1/internal/orb/instances/register",
- "POST /v1/internal/queue-intelligence",
- "POST /v1/internal/repos/{owner}/{repo}/ai-key",
- "POST /v1/internal/repos/{owner}/{repo}/contribution-policy",
- "POST /v1/internal/repos/{owner}/{repo}/linear-key",
- "POST /v1/internal/repos/{owner}/{repo}/settings",
- "POST /v1/local/remediation-plan",
- "POST /v1/loop/request-apr-transfer",
- "POST /v1/orb/relay",
- "POST /v1/orb/relay/pull",
- "POST /v1/orb/relay/register",
- "POST /v1/orb/token",
- "POST /v1/orb/webhook",
- "POST /v1/repos/{owner}/{repo}/ai-key",
- "POST /v1/repos/{owner}/{repo}/linear-key",
- "POST /v1/repos/{owner}/{repo}/pulls/{number}/chat-qa",
- "PUT /v1/app/installations/{id}/agent/bulk-settings",
- "PUT /v1/repos/{owner}/{repo}/ai-review",
- "PUT /v1/repos/{owner}/{repo}/settings"
-]
diff --git a/test/unit/define-route.test.ts b/test/unit/define-route.test.ts
index db8ccdb182..018a0d1974 100644
--- a/test/unit/define-route.test.ts
+++ b/test/unit/define-route.test.ts
@@ -149,7 +149,7 @@ describe("defineRoute", () => {
expect((await app.request("/v1/thing?limit=nope")).status).toBe(400);
});
- it("omits a security stanza for public routes and requires credentials otherwise", () => {
+ it("declares an empty security stanza for public routes and requires credentials otherwise", () => {
const { app, registry } = build();
for (const [auth, path, id] of [
["public", "/v1/open", "openOp"],
@@ -160,7 +160,11 @@ describe("defineRoute", () => {
defineRoute(app, registry, { method: "get", path, operationId: id, tags: ["t"], summary: "s", auth, responses: { 200: { description: "ok" } } }, (c) => c.json({}));
}
const paths = generate(registry).paths ?? {};
- expect(paths["/v1/open"]?.get?.security).toBeUndefined();
+ // `[]`, not undefined (#9531): an empty array is OpenAPI's explicit "no credential required",
+ // where an absent one means "not stated". applySecurityMetadata fills in the legacy registerPath
+ // calls that never declared anything, and it can only leave a deliberately public route alone if
+ // that route says so out loud.
+ expect(paths["/v1/open"]?.get?.security).toEqual([]);
// Internal routes are bearer-only: no browser session ever reaches them.
expect(paths["/v1/internal/thing"]?.get?.security).toEqual([{ LoopOverBearer: [] }]);
for (const path of ["/v1/tokened", "/v1/sessioned"]) {
diff --git a/test/unit/openapi-security-parity.test.ts b/test/unit/openapi-security-parity.test.ts
new file mode 100644
index 0000000000..75b16ff376
--- /dev/null
+++ b/test/unit/openapi-security-parity.test.ts
@@ -0,0 +1,83 @@
+// The published security stanzas must describe what the app actually enforces (#9531).
+//
+// This replaces `isProtectedPath`, which was a SECOND model of the same policy living in the spec
+// builder -- a path-prefix approximation that treated every `/v1/*` route as protected bar a short
+// literal list. It had already drifted from the real gate: the whole `/v1/public/decision-ledger/*`
+// family answers 200 to an anonymous caller and was published as requiring a bearer.
+//
+// There is now one model (src/auth/route-auth.ts) that both the middleware and the document read.
+// These tests guard the two ways that can still go wrong: a route whose declared stanza contradicts
+// the gate, and the gate quietly changing which routes are open.
+import { describe, expect, it } from "vitest";
+import { buildOpenApiSpec } from "../../src/openapi/spec";
+import { requiresApiToken } from "../../src/auth/route-auth";
+
+type Operation = { security?: Array> };
+
+function operations(): Array<{ path: string; method: string; operation: Operation }> {
+ const spec = buildOpenApiSpec();
+ const rows: Array<{ path: string; method: string; operation: Operation }> = [];
+ for (const [path, item] of Object.entries(spec.paths)) {
+ for (const method of ["get", "post", "put", "patch", "delete"] as const) {
+ const operation = (item as Record)[method] as Operation | undefined;
+ if (operation) rows.push({ path, method, operation });
+ }
+ }
+ return rows;
+}
+
+/** The document writes `{param}`; the gate matches a concrete path. */
+const concrete = (path: string): string => path.replace(/\{[^}]+\}/g, "_");
+
+describe("OpenAPI security parity with the real gate (#9531)", () => {
+ // `requiresApiToken` is ONE gate, not the only one, and reading it as "needs no credential at all"
+ // is the mistake this test originally made -- twice. Four route families are exempt from it
+ // precisely because they carry their own credential:
+ //
+ // - `/v1/internal/*`, gated by its own INTERNAL_JOB_TOKEN middleware (`app.use("/v1/internal/*")`);
+ // - the ORB ingress, presenting an ORB-issued bearer;
+ // - the ORB webhook, verified by a body signature;
+ // - `POST /v1/auth/github/token`, gated IN THE HANDLER on a browser session (it 403s a
+ // bearer-only caller), which is why it declares LoopOverSessionCookie alone.
+ //
+ // Naming the actual scheme is more accurate than silence in every one of those cases, so what
+ // this hunts is narrower: a route with no gate of any kind still advertising a credential.
+ const SELF_GATED_SCHEMES = ["OrbBearer", "OrbWebhookSignature", "LoopOverSessionCookie"];
+ const selfGated = (path: string, operation: Operation): boolean =>
+ path.startsWith("/v1/internal/") ||
+ // A session-cookie-ONLY stanza means handler-gated; the generic bearer+cookie PAIR is what the
+ // legacy fill-in emits, and that is the thing worth catching on an ungated route.
+ (operation.security?.length === 1 && SELF_GATED_SCHEMES.some((scheme) => scheme in operation.security![0]!));
+
+ it("never advertises a credential requirement on a route no gate protects", () => {
+ const contradictions = operations()
+ .filter(({ path, operation }) => !requiresApiToken(concrete(path)) && !selfGated(path, operation) && (operation.security?.length ?? 0) > 0)
+ .map(({ path, method }) => `${method.toUpperCase()} ${path}`);
+ expect(contradictions).toEqual([]);
+ });
+
+ it("never publishes a gated route as needing no credential", () => {
+ const silent = operations()
+ .filter(({ path, operation }) => requiresApiToken(concrete(path)) && operation.security !== undefined && operation.security.length === 0)
+ .map(({ path, method }) => `${method.toUpperCase()} ${path}`);
+ expect(silent).toEqual([]);
+ });
+
+ it("distinguishes the auth levels rather than collapsing them to one stanza", () => {
+ // The defect this replaced: every operation emitted the same LoopOverBearer+SessionCookie pair,
+ // so the document could not tell a caller which routes need nothing, which need an internal
+ // token, and which need the ORB's own credential.
+ const shapes = new Set(operations().map(({ operation }) => JSON.stringify(operation.security ?? null)));
+ expect(shapes.size).toBeGreaterThan(3);
+ expect(shapes).toContain(JSON.stringify([]));
+ expect(shapes).toContain(JSON.stringify([{ OrbBearer: [] }]));
+ expect(shapes).toContain(JSON.stringify([{ LoopOverBearer: [] }]));
+ });
+
+ it("declares every security scheme it references", () => {
+ const spec = buildOpenApiSpec();
+ const declared = new Set(Object.keys(spec.components?.securitySchemes ?? {}));
+ const referenced = new Set(operations().flatMap(({ operation }) => (operation.security ?? []).flatMap((entry) => Object.keys(entry))));
+ expect([...referenced].filter((name) => !declared.has(name))).toEqual([]);
+ });
+});
diff --git a/test/unit/openapi.test.ts b/test/unit/openapi.test.ts
index e3632cb783..8820d747a1 100644
--- a/test/unit/openapi.test.ts
+++ b/test/unit/openapi.test.ts
@@ -122,9 +122,11 @@ describe("OpenAPI contract", () => {
"/v1/repos/{owner}/{repo}/maintainer-lane",
"/v1/repos/{owner}/{repo}/pulls/{number}/review-intelligence",
"/v1/repos/{owner}/{repo}/pulls/{number}/scoring-preview",
- "/v1/internal/jobs/generate-signal-snapshots/run",
"/v1/auth/extension/session",
"/v1/extension/pull-context",
+ // #9531 removed "/v1/internal/jobs/generate-signal-snapshots/run" from this list: the route
+ // is LIVE (src/api/routes.ts registers it), so asserting the document must not describe it
+ // was asserting the document must stay wrong. It is specced now, like every other route.
]) {
expect(spec.paths[removedPath]).toBeUndefined();
}
@@ -185,7 +187,10 @@ describe("OpenAPI contract", () => {
expect(spec.paths["/v1/app/overview"]?.get?.security).toEqual([{ LoopOverBearer: [] }, { LoopOverSessionCookie: [] }]);
expect(spec.paths["/v1/auth/session"]?.get?.security).toBeUndefined();
expect(spec.paths["/v1/auth/logout"]?.post?.security).toBeUndefined();
- expect(spec.paths["/v1/auth/github/token"]?.post?.security).toEqual([{ LoopOverBearer: [] }, { LoopOverSessionCookie: [] }]);
+ // #9531: session-only, not the generic pair. This handler returns 403 to a bearer-only caller
+ // (`identity.kind !== "session"`), so advertising LoopOverBearer as sufficient was a published
+ // lie -- one neither of the two former security models got right.
+ expect(spec.paths["/v1/auth/github/token"]?.post?.security).toEqual([{ LoopOverSessionCookie: [] }]);
});
// #9303: selftune-override routes were live but undocumented; assert both paths, their HTTP methods, and the
@@ -333,8 +338,13 @@ describe("OpenAPI contract", () => {
expect(propKeys(response)).toEqual(Object.keys(outputShape).sort());
}
- // request-apr-transfer is explicitly out of scope for this batch and must stay undocumented here.
- expect(spec.paths["/v1/loop/request-apr-transfer"]).toBeUndefined();
+ // request-apr-transfer was out of scope for #9309's batch. #9531 documents every live route, so
+ // it is described now -- but deliberately WITHOUT the tool-parity request/response schemas the
+ // family above carries, because it has no MCP tool to be at parity with. Asserting that
+ // distinction is what this line is for now that "absent entirely" is no longer true of anything.
+ const aprTransfer = spec.paths["/v1/loop/request-apr-transfer"]?.post;
+ expect(aprTransfer?.operationId).toBe("requestAprTransfer");
+ expect(aprTransfer?.requestBody).toBeUndefined();
});
// #9308: the eight /v1/lint/* + /v1/validate/focus-manifest advisory-check routes are each backed by an MCP
diff --git a/test/unit/route-spec-ratchet.test.ts b/test/unit/route-spec-ratchet.test.ts
index f80302b0ef..ddca09389e 100644
--- a/test/unit/route-spec-ratchet.test.ts
+++ b/test/unit/route-spec-ratchet.test.ts
@@ -7,10 +7,10 @@
// 90 live routes had no spec entry, including every ORB management surface (/v1/orb/*,
// /v1/internal/orb/*, fleet config-push, kill-switch, the DLQ admin quartet).
//
-// The baseline is a RATCHET, not an allowlist: it may only shrink. A route removed from it can
-// never silently come back, and a NEW unspecced route fails immediately rather than joining a
-// growing pile. #9531 drives it to zero, at which point the file is deleted and this test keeps
-// enforcing the invariant with an empty set.
+// The baseline WAS a ratchet that could only shrink. #9531 drove it to zero and deleted the file;
+// what remains is the invariant it was protecting, now enforced absolutely: every live route has an
+// operation, and every operation has a live route. There is no longer anywhere to record an
+// exception, which is the point -- a new unspecced route fails immediately.
import { describe, expect, it } from "vitest";
import { createApp } from "../../src/api/routes";
import { buildOpenApiSpec } from "../../src/openapi/spec";
@@ -21,9 +21,6 @@ import {
listSpecRouteKeys,
normalizeRoutePath,
} from "../../src/openapi/route-inventory";
-import baseline from "../../src/openapi/unspecced-routes-baseline.json" with { type: "json" };
-
-const KNOWN_UNSPECCED = new Set(baseline as string[]);
describe("route inventory helpers", () => {
it("rewrites Hono path params into OpenAPI form", () => {
@@ -62,41 +59,29 @@ describe("route inventory helpers", () => {
describe("route↔spec ratchet", () => {
const diff = diffRoutesAgainstSpec(listLiveRouteKeys(createApp() as never), listSpecRouteKeys(buildOpenApiSpec() as never));
- it("describes every live route in the OpenAPI document, except the shrinking known-gap baseline", () => {
- const newlyUnspecced = diff.missingFromSpec.filter((key) => !KNOWN_UNSPECCED.has(key));
- expect(
- newlyUnspecced,
- `These routes exist in createApp() but have no OpenAPI operation. Register them through the spec ` +
- `(see src/openapi/spec.ts) rather than adding them to src/openapi/unspecced-routes-baseline.json -- ` +
- `that file may only shrink (#9531).`,
- ).toEqual([]);
- });
-
- it("keeps the baseline honest: an entry that is now specced must be removed from it", () => {
- // Without this, the baseline would quietly retain entries for routes someone specced along the
- // way, and the "may only shrink" promise would be unverifiable -- the file would stop
- // describing the real remaining work.
- const stale = [...KNOWN_UNSPECCED].filter((key) => !diff.missingFromSpec.includes(key));
+ it("describes every live route in the OpenAPI document", () => {
expect(
- stale,
- `These routes ARE now described in the OpenAPI document but are still listed in ` +
- `src/openapi/unspecced-routes-baseline.json. Delete them from that file.`,
+ diff.missingFromSpec,
+ `These routes exist in createApp() but have no OpenAPI operation. Register them through the ` +
+ `seam (src/openapi/define-route.ts) -- there is no baseline to add them to (#9531).`,
).toEqual([]);
});
it("never publishes an operation for a route the app does not serve", () => {
// Strictly worse than a missing entry: a generated client compiles a call that 404s at runtime.
- // No baseline for this one -- it is zero today and must stay zero.
+ // This direction never had a baseline and caught two invented job routes during #9531 -- an
+ // assumption that every internal job had both an enqueue and a `/run` form, which two of them
+ // do not.
expect(
diff.missingFromApp,
"These operations are in the OpenAPI document but no live route serves them.",
).toEqual([]);
});
- it("has a baseline that only contains real, currently-unspecced routes", () => {
- expect(KNOWN_UNSPECCED.size).toBeGreaterThan(0);
- for (const key of KNOWN_UNSPECCED) {
- expect(key, `baseline entry is not a METHOD /path key`).toMatch(/^(GET|POST|PUT|PATCH|DELETE) \//);
- }
+ it("describes a real, non-trivial surface -- so neither direction can pass by describing nothing", () => {
+ // Both assertions above are satisfied by two empty lists. This is what makes them mean
+ // something.
+ expect(listLiveRouteKeys(createApp() as never).length).toBeGreaterThan(200);
+ expect(listSpecRouteKeys(buildOpenApiSpec() as never).length).toBeGreaterThan(200);
});
});