From 0ca3124414fda9ea86d7a9375b62d4c6f2c6d2d6 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Tue, 28 Jul 2026 05:12:32 -0700
Subject: [PATCH 1/4] feat(api): drive the route-spec ratchet to zero and
delete the baseline
The published document described 151 of the 244 operations createApp() actually
serves. It now describes all 244, and src/openapi/unspecced-routes-baseline.json
is gone -- there is no longer anywhere to record an exception.
The 93 newly-described routes include the entire ORB management surface
(/v1/orb/*, /v1/internal/orb/*), the fleet kill-switch and config-push, the
per-repo BYO-key routes, and the 32 internal job endpoints. Each carries a
stable operationId, real tags, and an auth level that DERIVES its security
stanza rather than having one bolted on afterwards by path prefix.
Three fixes the work forced:
- RouteAuth gained 'orb' and 'webhook'. The ORB ingress genuinely does not
authenticate the way the rest of the API does, and the old model published it
as needing no credential at all. It needs a different one: an ORB-issued
bearer for the relay and token endpoints, an HMAC signature header for the
webhook. Both now have their own security scheme.
- registerRouteSpec never emitted path parameters, so every templated segment it
registered was a Cloudflare 30046 schema-validation warning and a hole in any
generated client. Derived from the path now, because there is no case where a
path parameter is optional.
- test/unit/openapi.test.ts asserted /v1/internal/jobs/generate-signal-snapshots
/run must be ABSENT from the document as a removed route. It is live. The
assertion was requiring the document to stay wrong.
The ratchet's other direction earned its keep immediately: it rejected two job
routes I had assumed existed, because two of the internal jobs are
operator-triggered repairs with a /run form and no enqueue sibling.
---
apps/loopover-ui/public/openapi.json | 2970 ++++++++++++++++-
src/openapi/define-route.ts | 35 +-
.../internal-and-public-route-specs.ts | 357 ++
src/openapi/orb-and-control-route-specs.ts | 336 ++
src/openapi/spec.ts | 23 +
src/openapi/unspecced-routes-baseline.json | 92 -
test/unit/openapi.test.ts | 13 +-
test/unit/route-spec-ratchet.test.ts | 47 +-
8 files changed, 3713 insertions(+), 160 deletions(-)
create mode 100644 src/openapi/internal-and-public-route-specs.ts
create mode 100644 src/openapi/orb-and-control-route-specs.ts
delete mode 100644 src/openapi/unspecced-routes-baseline.json
diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json
index 47177df11..35ab8a208 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."
}
}
},
@@ -19023,6 +19034,53 @@
"LoopOverSessionCookie": []
}
]
+ },
+ "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"
+ }
+ ]
}
},
"/v1/repos/{owner}/{repo}/automation-state": {
@@ -21396,10 +21454,10 @@
},
"/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"
@@ -21412,15 +21470,20 @@
{
"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"
@@ -21433,18 +21496,20 @@
{
"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"
@@ -21457,18 +21522,20 @@
{
"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"
@@ -21481,18 +21548,20 @@
{
"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"
@@ -21505,15 +21574,20 @@
{
"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"
@@ -21526,15 +21600,20 @@
{
"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"
@@ -21547,15 +21626,20 @@
{
"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"
@@ -21568,6 +21652,11 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueFileUpstreamDriftIssuesJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
@@ -21594,10 +21683,10 @@
},
"/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"
@@ -21610,6 +21699,11 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueBuildContributorDecisionPacksJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
@@ -21636,10 +21730,10 @@
},
"/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"
@@ -21652,15 +21746,20 @@
{
"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"
@@ -21673,6 +21772,11 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "queueGenerateWeeklyValueReportJob",
+ "tags": [
+ "Internal",
+ "Jobs"
]
}
},
@@ -22975,6 +23079,2810 @@
}
]
}
+ },
+ "/v1/orb/token": {
+ "post": {
+ "operationId": "mintOrbInstanceToken",
+ "tags": [
+ "ORB"
+ ],
+ "summary": "Mint an ORB instance token",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/v1/internal/orb/instances": {
+ "get": {
+ "operationId": "listOrbInstances",
+ "tags": [
+ "ORB",
+ "Internal"
+ ],
+ "summary": "List registered ORB instances",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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"
+ }
+ }
+ }
+ },
+ "/loopover/shot": {
+ "get": {
+ "operationId": "getLoopoverShot",
+ "tags": [
+ "Meta"
+ ],
+ "summary": "Return the LoopOver social preview image",
+ "responses": {
+ "200": {
+ "description": "Preview image"
+ }
+ }
+ }
+ },
+ "/v1/public/subnet-interface": {
+ "get": {
+ "operationId": "getPublicSubnetInterface",
+ "tags": [
+ "Public"
+ ],
+ "summary": "Return the public subnet interface description",
+ "responses": {
+ "200": {
+ "description": "Subnet interface"
+ }
+ },
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/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": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ]
+ }
+ },
+ "/v1/ams/ingest": {
+ "post": {
+ "operationId": "ingestAmsTelemetry",
+ "tags": [
+ "AMS"
+ ],
+ "summary": "Ingest an AMS miner telemetry batch",
+ "security": [
+ {
+ "LoopOverBearer": []
+ },
+ {
+ "LoopOverSessionCookie": []
+ }
+ ],
+ "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"
+ }
+ }
+ }
}
},
"servers": [
diff --git a/src/openapi/define-route.ts b/src/openapi/define-route.ts
index 06464eab0..45e5c77e0 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,40 @@ 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;
if (auth === "internal") return [{ LoopOverBearer: [] }];
+ if (auth === "orb") return [{ OrbBearer: [] }];
+ if (auth === "webhook") return [{ OrbWebhookSignature: [] }];
return [{ LoopOverBearer: [] }, { LoopOverSessionCookie: [] }];
}
@@ -138,6 +162,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 000000000..6c5a60ade
--- /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 000000000..f0a8bcd7e
--- /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 8e80a61bd..a4637aea6 100644
--- a/src/openapi/spec.ts
+++ b/src/openapi/spec.ts
@@ -1,4 +1,6 @@
import { OpenApiGeneratorV3, OpenAPIRegistry } from "@asteasolutions/zod-to-openapi";
+import { registerOrbAndControlRouteSpecs } from "./orb-and-control-route-specs";
+import { registerInternalAndPublicRouteSpecs } from "./internal-and-public-route-specs";
import { z } from "zod";
import {
AdvisorySchema,
@@ -2091,6 +2093,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",
@@ -2124,6 +2133,20 @@ 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)) {
diff --git a/src/openapi/unspecced-routes-baseline.json b/src/openapi/unspecced-routes-baseline.json
deleted file mode 100644
index a5eef4523..000000000
--- 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/openapi.test.ts b/test/unit/openapi.test.ts
index e3632cb78..39ca4873b 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();
}
@@ -333,8 +335,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 f80302b0e..ddca09389 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);
});
});
From c38b252635cdd56b12f5f98c73503c04fc7374fe Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Tue, 28 Jul 2026 10:47:06 -0700
Subject: [PATCH 2/4] feat(api): give every operation a stable operationId and
real tags
The document emitted tags: [] on all 244 operations and an operationId on none,
which collapses every one of them into a single flat namespace for a generated
client or a doc explorer -- and makes every path edit a breaking rename, since
the id would otherwise be slugified from method+path.
126 literal registerPath calls carry both inline. The five families registered
from a table derive them in-loop from the same rules, because those tables exist
so that adding a sibling route is a one-line change and a second hand-written
entry per route would give that up.
All 244 now carry both, with no duplicate ids. That is the precondition for
deleting isProtectedPath/applySecurityMetadata: the parallel security model can
only go once every operation declares enough to derive its own stanza.
---
apps/loopover-ui/public/openapi.json | 598 ++++++++++++++++++++++++++-
src/openapi/spec.ts | 280 +++++++++++++
2 files changed, 865 insertions(+), 13 deletions(-)
diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json
index 35ab8a208..3aa887ad5 100644
--- a/apps/loopover-ui/public/openapi.json
+++ b/apps/loopover-ui/public/openapi.json
@@ -16602,7 +16602,11 @@
}
}
}
- }
+ },
+ "operationId": "getHealth",
+ "tags": [
+ "Meta"
+ ]
}
},
"/v1/mcp/compatibility": {
@@ -16619,7 +16623,11 @@
}
}
}
- }
+ },
+ "operationId": "getMcpCompatibility",
+ "tags": [
+ "MCP"
+ ]
}
},
"/v1/public/stats": {
@@ -16642,7 +16650,11 @@
"503": {
"description": "Public stats are temporarily unavailable"
}
- }
+ },
+ "operationId": "listPublicStats",
+ "tags": [
+ "Public"
+ ]
}
},
"/v1/public/github/repos/{owner}/{repo}/stats": {
@@ -16683,7 +16695,11 @@
"503": {
"description": "GitHub repository stats are unavailable"
}
- }
+ },
+ "operationId": "listPublicGithubReposByOwnerByRepoStats",
+ "tags": [
+ "Public"
+ ]
}
},
"/v1/public/repos/{owner}/{repo}/quality": {
@@ -16724,7 +16740,11 @@
"503": {
"description": "Public quality metrics are temporarily unavailable"
}
- }
+ },
+ "operationId": "getPublicReposByOwnerByRepoQuality",
+ "tags": [
+ "Public"
+ ]
}
},
"/v1/registry/snapshot": {
@@ -16749,6 +16769,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getRegistrySnapshot",
+ "tags": [
+ "Registry"
]
}
},
@@ -16774,6 +16798,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listRegistryChanges",
+ "tags": [
+ "Registry"
]
}
},
@@ -16799,6 +16827,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getScoringModel",
+ "tags": [
+ "Scoring"
]
}
},
@@ -16824,6 +16856,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getFindingTaxonomy",
+ "tags": [
+ "API"
]
}
},
@@ -16849,6 +16885,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listEnrichmentAnalyzers",
+ "tags": [
+ "API"
]
}
},
@@ -16874,6 +16914,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listUpstreamStatus",
+ "tags": [
+ "Registry"
]
}
},
@@ -16902,6 +16946,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getUpstreamRuleset",
+ "tags": [
+ "Registry"
]
}
},
@@ -16946,6 +16994,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getUpstreamDrift",
+ "tags": [
+ "Registry"
]
}
},
@@ -16974,6 +17026,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postScoringPreview",
+ "tags": [
+ "Scoring"
]
}
},
@@ -17002,6 +17058,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postScoringEligibilityPlan",
+ "tags": [
+ "Scoring"
]
}
},
@@ -17030,6 +17090,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postScoringExplainBreakdown",
+ "tags": [
+ "Scoring"
]
}
},
@@ -17055,6 +17119,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listSyncStatus",
+ "tags": [
+ "API"
]
}
},
@@ -17080,6 +17148,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReadiness",
+ "tags": [
+ "API"
]
}
},
@@ -17126,6 +17198,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listInstallations",
+ "tags": [
+ "API"
]
}
},
@@ -17164,6 +17240,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getInstallationsByIdHealth",
+ "tags": [
+ "API"
]
}
},
@@ -17202,6 +17282,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getInstallationsByIdRepair",
+ "tags": [
+ "API"
]
}
},
@@ -17240,6 +17324,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postInstallationsByIdRepairRefresh",
+ "tags": [
+ "API"
]
}
},
@@ -17380,6 +17468,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppNotificationModel",
+ "tags": [
+ "Control panel"
]
}
},
@@ -17408,6 +17500,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listRepos",
+ "tags": [
+ "API"
]
}
},
@@ -17454,6 +17550,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepo",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17497,6 +17597,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoIntelligence",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17543,6 +17647,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoIssueQuality",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17592,6 +17700,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoGateConfigEffective",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17629,6 +17741,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopEvaluateEscalation",
+ "tags": [
+ "Loop"
]
}
},
@@ -17666,6 +17782,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopResultsPayload",
+ "tags": [
+ "Loop"
]
}
},
@@ -17703,6 +17823,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopProgressSnapshot",
+ "tags": [
+ "Loop"
]
}
},
@@ -17740,6 +17864,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopIntakeIdea",
+ "tags": [
+ "Loop"
]
}
},
@@ -17777,6 +17905,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLoopPlanIdeaClaims",
+ "tags": [
+ "Loop"
]
}
},
@@ -17826,6 +17958,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoLiveGateThresholds",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17875,6 +18011,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoMaintainerNoise",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17924,6 +18064,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoAmsMinerCohort",
+ "tags": [
+ "Repositories"
]
}
},
@@ -17983,6 +18127,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoGatePrecision",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18042,6 +18190,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoOutcomeCalibration",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18091,6 +18243,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoActivationPreview",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18137,6 +18293,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoOutcomePatterns",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18180,6 +18340,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoRegistrationReadiness",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18223,6 +18387,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoGittensorConfigRecommendation",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18272,6 +18440,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoFocusManifest",
+ "tags": [
+ "Repositories"
]
},
"put": {
@@ -18322,6 +18494,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "putReposByOwnerByRepoFocusManifest",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18371,6 +18547,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoFocusManifestRefresh",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18518,6 +18698,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoAgentAuditFeed",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18643,6 +18827,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoPullsByNumberIncidentReports",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18754,6 +18942,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppIncidentReports",
+ "tags": [
+ "Control panel"
]
}
},
@@ -18785,6 +18977,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppSelfDogfoodRegistrationPack",
+ "tags": [
+ "Control panel"
]
}
},
@@ -18834,6 +19030,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoSelfDogfoodRegistrationPack",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18886,6 +19086,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoOnboardingPackPreview",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18938,6 +19142,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoContributorIssueDraftsGenerate",
+ "tags": [
+ "Repositories"
]
}
},
@@ -18990,6 +19198,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoIssuePlanDraftsGenerate",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19033,6 +19245,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoSettings",
+ "tags": [
+ "Repositories"
]
},
"put": {
@@ -19123,6 +19339,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoAutomationState",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19166,6 +19386,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoRepoDocsRefresh",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19212,6 +19436,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoSettingsPreview",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19263,6 +19491,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoPullsByNumberMaintainerPacket",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19314,6 +19546,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoPullsByNumberReviewability",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19385,6 +19621,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoPullsByNumberAiReviewFindings",
+ "tags": [
+ "Repositories"
]
}
},
@@ -19420,6 +19660,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginProfile",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19465,6 +19709,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginDecisionPack",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19500,6 +19748,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginOpenPrMonitor",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19546,6 +19798,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listContributorsByLoginPrOutcomes",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19581,6 +19837,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listContributorsByLoginNotifications",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19636,6 +19896,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postContributorsByLoginNotificationsRead",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19697,6 +19961,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getContributorsByLoginReposByOwnerByRepoDecision",
+ "tags": [
+ "Contributors"
]
}
},
@@ -19725,6 +19993,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postPreflightPr",
+ "tags": [
+ "Preflight"
]
}
},
@@ -19756,6 +20028,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postPreflightReviewRisk",
+ "tags": [
+ "Preflight"
]
}
},
@@ -19784,6 +20060,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postPreflightLocalDiff",
+ "tags": [
+ "Preflight"
]
}
},
@@ -19815,6 +20095,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLocalBranchAnalysis",
+ "tags": [
+ "Local"
]
}
},
@@ -19846,6 +20130,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentRuns",
+ "tags": [
+ "Agent automation"
]
},
"get": {
@@ -19909,6 +20197,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listAgentRuns",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -19947,6 +20239,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAgentRunsById",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -19988,6 +20284,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentPlanNextWork",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -20029,6 +20329,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentPreflightBranch",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -20070,6 +20374,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentPreparePrPacket",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -20111,6 +20419,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAgentExplainBlockers",
+ "tags": [
+ "Agent automation"
]
}
},
@@ -20139,6 +20451,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listBounties",
+ "tags": [
+ "Bounties"
]
}
},
@@ -20177,6 +20493,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getBountiesByIdAdvisory",
+ "tags": [
+ "Bounties"
]
}
},
@@ -20215,6 +20535,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getBountiesByIdLifecycle",
+ "tags": [
+ "Bounties"
]
}
},
@@ -20228,7 +20552,11 @@
"401": {
"description": "Invalid webhook signature"
}
- }
+ },
+ "operationId": "postGithubWebhook",
+ "tags": [
+ "Webhooks"
+ ]
}
},
"/v1/public/decision-ledger/verify": {
@@ -20249,6 +20577,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getPublicDecisionLedgerVerify",
+ "tags": [
+ "Public"
]
}
},
@@ -20283,6 +20615,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getPublicDecisionLedgerRowBySeq",
+ "tags": [
+ "Public"
]
}
},
@@ -20301,6 +20637,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getPublicDecisionLedgerAnchorKey",
+ "tags": [
+ "Public"
]
}
},
@@ -20350,6 +20690,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listPublicDecisionLedgerAnchors",
+ "tags": [
+ "Public"
]
}
},
@@ -20400,6 +20744,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getPublicDecisionRecordsByOwnerByRepoByPull",
+ "tags": [
+ "Public"
]
}
},
@@ -20439,6 +20787,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listPublicEvalScores",
+ "tags": [
+ "Public"
]
}
},
@@ -20460,6 +20812,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postOrbIngest",
+ "tags": [
+ "ORB"
]
}
},
@@ -20473,7 +20829,11 @@
"503": {
"description": "GitHub OAuth app secret is not configured"
}
- }
+ },
+ "operationId": "getAuthGithubStart",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/callback": {
@@ -20483,7 +20843,11 @@
"302": {
"description": "Completes GitHub web OAuth and redirects to the app"
}
- }
+ },
+ "operationId": "getAuthGithubCallback",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/device/start": {
@@ -20505,7 +20869,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthGithubDeviceStart",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/device/poll": {
@@ -20527,7 +20895,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthGithubDevicePoll",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/session": {
@@ -20549,7 +20921,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthGithubSession",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/logout": {
@@ -20571,7 +20947,11 @@
"429": {
"description": "Rate limited"
}
- }
+ },
+ "operationId": "postAuthLogout",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/session": {
@@ -20581,7 +20961,11 @@
"200": {
"description": "Current auth session, or signed_out when no app session is present"
}
- }
+ },
+ "operationId": "getAuthSession",
+ "tags": [
+ "Auth"
+ ]
}
},
"/v1/auth/github/token": {
@@ -20623,6 +21007,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAuthGithubToken",
+ "tags": [
+ "Auth"
]
}
},
@@ -20657,6 +21045,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppOverview",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20688,6 +21080,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppRoles",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20719,6 +21115,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppMinerDashboard",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20750,6 +21150,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppMaintainerDashboard",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20781,6 +21185,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppOperatorDashboard",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20812,6 +21220,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppCommands",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20843,6 +21255,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppCommandsUsefulness",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20874,6 +21290,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppDigest",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20905,6 +21325,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppAnalyticsDailyRollups",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20936,6 +21360,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppAnalyticsMcpCompatibility",
+ "tags": [
+ "Control panel"
]
}
},
@@ -20991,6 +21419,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppSelfhostQueueDeadByIdReplay",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21046,6 +21478,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteAppSelfhostQueueDeadById",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21083,6 +21519,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteAppSelfhostQueueDead",
+ "tags": [
+ "Control panel"
]
},
"get": {
@@ -21143,6 +21583,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppSelfhostQueueDead",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21223,6 +21667,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppAnalyticsWeeklyValueReport",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21318,6 +21766,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getAppSkippedPrAudit",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21355,6 +21807,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppCommandsPreview",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21402,6 +21858,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppCommandsFeedback",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21449,6 +21909,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postAppDigestSubscriptions",
+ "tags": [
+ "Control panel"
]
}
},
@@ -21678,6 +22142,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postInternalJobsBuildContributorEvidence",
+ "tags": [
+ "Internal"
]
}
},
@@ -21725,6 +22193,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postInternalJobsBuildBurdenForecasts",
+ "tags": [
+ "Internal"
]
}
},
@@ -21798,6 +22270,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postInternalJobsRepairDataFidelity",
+ "tags": [
+ "Internal"
]
}
},
@@ -21819,6 +22295,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postInternalBountiesImport",
+ "tags": [
+ "Internal"
]
}
},
@@ -21865,6 +22345,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listReposByOwnerByRepoAgentPendingActions",
+ "tags": [
+ "Repositories"
]
},
"post": {
@@ -21924,6 +22408,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoAgentPendingActions",
+ "tags": [
+ "Repositories"
]
}
},
@@ -21999,6 +22487,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoAgentPendingActionsByIdByDecision",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22036,6 +22528,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintPrText",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22073,6 +22569,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintSlopRisk",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22110,6 +22610,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintImprovementPotential",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22147,6 +22651,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintOpenPrPressure",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22184,6 +22692,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintBoundaryTests",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22221,6 +22733,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintTestEvidence",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22258,6 +22774,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postLintIssueSlop",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22295,6 +22815,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postValidateFocusManifest",
+ "tags": [
+ "Advisory checks"
]
}
},
@@ -22351,6 +22875,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getReposByOwnerByRepoSelftuneOverridesAudit",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22421,6 +22949,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteReposByOwnerByRepoSelftuneOverrides",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22482,6 +23014,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoValidateLinkedIssue",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22543,6 +23079,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postReposByOwnerByRepoCheckBeforeStart",
+ "tags": [
+ "Repositories"
]
}
},
@@ -22644,6 +23184,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postOpportunitiesFind",
+ "tags": [
+ "Discovery"
]
}
},
@@ -22723,6 +23267,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postIssueRagRetrieve",
+ "tags": [
+ "Discovery"
]
}
},
@@ -22764,6 +23312,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "listContributorsByLoginWatches",
+ "tags": [
+ "Contributors"
]
},
"post": {
@@ -22815,6 +23367,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postContributorsByLoginWatches",
+ "tags": [
+ "Contributors"
]
},
"delete": {
@@ -22866,6 +23422,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteContributorsByLoginWatches",
+ "tags": [
+ "Contributors"
]
}
},
@@ -22957,6 +23517,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "getInternalProviderCredentialsByProvider",
+ "tags": [
+ "Internal"
]
},
"post": {
@@ -23023,6 +23587,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "postInternalProviderCredentialsByProvider",
+ "tags": [
+ "Internal"
]
},
"delete": {
@@ -23077,6 +23645,10 @@
{
"LoopOverSessionCookie": []
}
+ ],
+ "operationId": "deleteInternalProviderCredentialsByProvider",
+ "tags": [
+ "Internal"
]
}
},
diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts
index a4637aea6..f21b25d8d 100644
--- a/src/openapi/spec.ts
+++ b/src/openapi/spec.ts
@@ -140,6 +140,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);
@@ -273,6 +296,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 } } },
@@ -281,6 +306,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 } } },
@@ -289,6 +316,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 } } },
@@ -299,6 +328,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: {
@@ -310,6 +341,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: {
@@ -325,6 +358,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 } } },
@@ -333,6 +368,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 } } },
@@ -341,6 +378,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 } } },
@@ -349,6 +388,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 } } },
@@ -357,6 +398,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 } } },
@@ -365,6 +408,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 } } },
@@ -373,6 +418,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 } } },
@@ -382,6 +429,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: {
@@ -401,6 +450,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 } } },
@@ -410,6 +461,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: {
@@ -423,6 +476,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: {
@@ -436,6 +491,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 } } },
@@ -444,6 +501,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 } } },
@@ -452,6 +511,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: {
@@ -470,6 +531,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: {
@@ -480,6 +543,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: {
@@ -490,6 +555,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: {
@@ -500,6 +567,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: {
@@ -540,6 +609,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() } } },
@@ -548,6 +619,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: {
@@ -558,6 +631,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: {
@@ -567,6 +642,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: {
@@ -577,6 +654,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: {
@@ -591,6 +670,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() }),
@@ -613,6 +694,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() }),
@@ -637,6 +720,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() }),
@@ -657,6 +742,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() }),
@@ -677,6 +764,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: {
@@ -695,6 +784,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: {
@@ -713,6 +804,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: {
@@ -731,6 +824,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: {
@@ -749,6 +844,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: {
@@ -767,6 +864,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: {
@@ -780,6 +879,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: {
@@ -793,6 +894,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: {
@@ -806,6 +909,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: {
@@ -819,6 +924,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: {
@@ -832,6 +939,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: {
@@ -845,6 +954,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: {
@@ -858,6 +969,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: {
@@ -871,6 +984,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: {
@@ -885,6 +1000,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: {
@@ -900,6 +1017,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: {
@@ -915,6 +1034,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() }),
@@ -933,6 +1054,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() }),
@@ -951,6 +1074,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: {
@@ -966,6 +1091,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: {
@@ -976,6 +1103,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: {
@@ -985,6 +1114,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: {
@@ -994,6 +1125,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: {
@@ -1004,6 +1137,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: {
@@ -1014,6 +1149,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: {
@@ -1025,6 +1162,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: {
@@ -1038,6 +1177,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() }),
@@ -1056,6 +1197,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: {
@@ -1072,6 +1215,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: {
@@ -1119,6 +1264,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() }),
@@ -1149,6 +1296,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: {
@@ -1180,6 +1329,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()) } } },
@@ -1189,6 +1340,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: {
@@ -1199,6 +1352,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: {
@@ -1210,6 +1365,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: {
@@ -1221,6 +1378,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: {
@@ -1232,6 +1391,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: {
@@ -1241,6 +1402,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: {
@@ -1254,6 +1417,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: {
@@ -1263,6 +1428,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: {
@@ -1273,6 +1440,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: {
@@ -1282,6 +1451,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: {
@@ -1291,6 +1462,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() }),
@@ -1311,6 +1484,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: {
@@ -1320,6 +1495,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: {
@@ -1333,6 +1510,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: {
@@ -1345,6 +1524,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() }),
@@ -1360,6 +1541,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: {
@@ -1372,6 +1555,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() }),
@@ -1394,6 +1579,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: {
@@ -1408,6 +1595,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() }),
@@ -1428,6 +1617,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() }),
@@ -1448,6 +1639,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: {
@@ -1458,6 +1651,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 } } },
@@ -1467,6 +1662,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 } } },
@@ -1477,6 +1674,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 } } },
@@ -1486,6 +1685,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 } } },
@@ -1496,6 +1697,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 } } },
@@ -1506,6 +1709,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({
@@ -1538,6 +1743,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: {
@@ -1548,6 +1755,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: {
@@ -1567,6 +1776,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: {
@@ -1592,6 +1803,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 } } },
@@ -1604,6 +1816,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() } } },
@@ -1612,6 +1826,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: {
@@ -1622,6 +1838,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: {
@@ -1632,6 +1850,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" },
@@ -1641,6 +1861,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)." },
@@ -1650,6 +1872,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: {
@@ -1661,6 +1885,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" },
@@ -1669,6 +1895,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: {
@@ -1678,6 +1906,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: {
@@ -1689,6 +1919,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: {
@@ -1699,6 +1931,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 }" },
@@ -1708,6 +1942,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" },
@@ -1717,6 +1953,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" },
@@ -1731,6 +1969,7 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path,
+ ...loopOperationMeta("post", path, "Auth"),
summary,
responses: {
200: { description: "Auth request completed" },
@@ -1744,6 +1983,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" },
@@ -1752,6 +1993,8 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path: "/v1/auth/github/token",
+ operationId: "postAuthGithubToken",
+ tags: ["Auth"],
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() }) } } },
@@ -1763,6 +2006,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()) } } },
@@ -1784,6 +2029,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()) } } },
@@ -1794,6 +2040,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({
@@ -1812,6 +2060,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({
@@ -1830,6 +2080,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()) } } },
@@ -1841,6 +2093,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({
@@ -1865,6 +2119,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({
@@ -1905,6 +2161,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({
@@ -1940,6 +2198,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 } } },
@@ -1956,6 +2216,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()) } } },
@@ -1970,6 +2231,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: {
@@ -1991,6 +2254,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"]) }),
@@ -2006,6 +2271,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: {
@@ -2017,6 +2284,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" },
@@ -2026,6 +2295,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" },
@@ -2035,6 +2306,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" },
@@ -2045,6 +2318,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" },
@@ -2055,6 +2330,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)" },
@@ -2076,6 +2353,7 @@ export function buildOpenApiSpec() {
registry.registerPath({
method: "post",
path,
+ ...loopOperationMeta("post", path, "Internal"),
summary,
responses: {
202: { description: "Internal job queued" },
@@ -2086,6 +2364,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" },
From c220fbdd0d9524bee633635467790526cd283063 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Tue, 28 Jul 2026 11:32:06 -0700
Subject: [PATCH 3/4] fix(api): actually derive each operation's security
stanza, and delete the parallel model
Review blockers on #9581, both correct: the document still emitted the identical
LoopOverBearer+SessionCookie pair on every operation regardless of its declared
auth -- a bearer requirement on the ORB ingress whose own comment says it takes
a shared-secret header, and on public routes that answer 200 anonymously.
Two causes, both fixed:
- applySecurityMetadata OVERWROTE whatever an operation declared. The seam was
producing differentiated stanzas and the post-pass clobbered all of them with
the generic pair. It now fills in only operations that declared nothing --
which required `public` to emit `[]` (OpenAPI's explicit "no credential")
rather than undefined, so a deliberately open route is distinguishable from a
legacy registerPath call that never said.
- The fill-in itself now derives from requiresApiToken, the predicate the app
gates on, extracted to src/auth/route-auth.ts (pure, no cloudflare: imports,
so the plain-Node spec builder can load it). isProtectedPath -- the second,
path-prefix model that had already drifted (it published the whole
/v1/public/decision-ledger/* family as bearer-gated) -- is deleted.
The published stanzas now split six ways instead of one: the generic pair (136),
bearer-only internal (63), legacy-undeclared (28), explicitly public (11), the
ORB instance bearer (5), and the webhook body signature (1).
Deriving from the real gate surfaced one route BOTH former models had wrong:
POST /v1/auth/github/token 403s a bearer-only caller (the handler checks
identity.kind !== "session"), yet was published as bearer-or-cookie by one model
and would be published as open by the other. It now declares
LoopOverSessionCookie alone.
The new parity meta-test asserts, against the real gate: no ungated route
advertises the ordinary credential, no gated route claims to be open, the auth
levels stay distinct rather than collapsing back to one stanza, and every
referenced scheme is declared. Its own first two drafts each misread an exempt
family as ungated -- /v1/internal/* has its own INTERNAL_JOB_TOKEN middleware,
and the session-only route above is gated in its handler -- which is recorded in
the test because those exemptions ARE the map of where auth is enforced other
than the token gate.
---
apps/loopover-ui/public/openapi.json | 429 ++--------------------
src/api/routes.ts | 49 +--
src/auth/route-auth.ts | 74 ++++
src/openapi/define-route.ts | 6 +-
src/openapi/spec.ts | 41 ++-
test/unit/define-route.test.ts | 8 +-
test/unit/openapi-security-parity.test.ts | 83 +++++
test/unit/openapi.test.ts | 5 +-
8 files changed, 229 insertions(+), 466 deletions(-)
create mode 100644 src/auth/route-auth.ts
create mode 100644 test/unit/openapi-security-parity.test.ts
diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json
index 3aa887ad5..1e1f1e91b 100644
--- a/apps/loopover-ui/public/openapi.json
+++ b/apps/loopover-ui/public/openapi.json
@@ -20570,14 +20570,6 @@
"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"
@@ -20608,14 +20600,6 @@
"description": "No ledger row at that seq (never appended -- distinct from a row with empty fields)"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "getPublicDecisionLedgerRowBySeq",
"tags": [
"Public"
@@ -20630,14 +20614,6 @@
"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"
@@ -20683,14 +20659,6 @@
"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"
@@ -20737,14 +20705,6 @@
"description": "No decision record persisted yet for this PR"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "getPublicDecisionRecordsByOwnerByRepoByPull",
"tags": [
"Public"
@@ -20780,14 +20740,6 @@
"description": "Public stats disabled (same flag as /v1/public/stats)"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "listPublicEvalScores",
"tags": [
"Public"
@@ -20805,14 +20757,6 @@
"description": "Malformed JSON or invalid payload shape"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "postOrbIngest",
"tags": [
"ORB"
@@ -21000,17 +20944,14 @@
"description": "Rate limited"
}
},
+ "operationId": "postAuthGithubToken",
+ "tags": [
+ "Auth"
+ ],
"security": [
- {
- "LoopOverBearer": []
- },
{
"LoopOverSessionCookie": []
}
- ],
- "operationId": "postAuthGithubToken",
- "tags": [
- "Auth"
]
}
},
@@ -21930,9 +21871,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueRefreshRegistryJob",
@@ -21956,9 +21894,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueBackfillRegisteredReposJob",
@@ -21982,9 +21917,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueBackfillRepoSegmentJob",
@@ -22008,9 +21940,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueBackfillPrDetailsJob",
@@ -22034,9 +21963,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueGenerateReviewRecapJob",
@@ -22060,9 +21986,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueRefreshScoringModelJob",
@@ -22086,9 +22009,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueRefreshUpstreamDriftJob",
@@ -22112,9 +22032,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueFileUpstreamDriftIssuesJob",
@@ -22135,14 +22052,6 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "postInternalJobsBuildContributorEvidence",
"tags": [
"Internal"
@@ -22163,9 +22072,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueBuildContributorDecisionPacksJob",
@@ -22186,14 +22092,6 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "postInternalJobsBuildBurdenForecasts",
"tags": [
"Internal"
@@ -22214,9 +22112,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueGenerateSignalSnapshotsJob",
@@ -22240,9 +22135,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"operationId": "queueGenerateWeeklyValueReportJob",
@@ -22263,14 +22155,6 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "postInternalJobsRepairDataFidelity",
"tags": [
"Internal"
@@ -22288,14 +22172,6 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "postInternalBountiesImport",
"tags": [
"Internal"
@@ -23510,14 +23386,6 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "getInternalProviderCredentialsByProvider",
"tags": [
"Internal"
@@ -23580,14 +23448,6 @@
"description": "TOKEN_ENCRYPTION_SECRET is not configured, so the credential cannot be stored encrypted"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "postInternalProviderCredentialsByProvider",
"tags": [
"Internal"
@@ -23638,14 +23498,6 @@
"description": "Invalid internal token"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ],
"operationId": "deleteInternalProviderCredentialsByProvider",
"tags": [
"Internal"
@@ -23661,10 +23513,7 @@
"summary": "Mint an ORB instance token",
"security": [
{
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
+ "OrbBearer": []
}
],
"responses": {
@@ -23689,10 +23538,7 @@
"summary": "Relay one ORB event to the hosted control plane",
"security": [
{
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
+ "OrbBearer": []
}
],
"responses": {
@@ -23717,10 +23563,7 @@
"summary": "Register a self-hosted ORB instance with the relay",
"security": [
{
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
+ "OrbBearer": []
}
],
"responses": {
@@ -23745,10 +23588,7 @@
"summary": "Pull queued relay work for an ORB instance",
"security": [
{
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
+ "OrbBearer": []
}
],
"responses": {
@@ -23770,10 +23610,7 @@
"summary": "Receive a signed ORB webhook",
"security": [
{
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
+ "OrbWebhookSignature": []
}
],
"responses": {
@@ -23804,14 +23641,7 @@
"description": "Missing or invalid OAuth code"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/internal/orb/instances": {
@@ -23825,9 +23655,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -23851,9 +23678,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -23880,9 +23704,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -23906,9 +23727,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -23935,9 +23753,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -23961,9 +23776,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -23990,9 +23802,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24487,9 +24296,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24529,9 +24335,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24574,9 +24377,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24761,9 +24561,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24803,9 +24600,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24848,9 +24642,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24941,9 +24732,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24967,9 +24755,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -24993,9 +24778,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25019,9 +24801,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25045,9 +24824,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25071,9 +24847,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25097,9 +24870,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25123,9 +24893,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25149,9 +24916,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25175,9 +24939,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25201,9 +24962,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25227,9 +24985,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25253,9 +25008,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25279,9 +25031,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25305,9 +25054,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25331,9 +25077,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25357,9 +25100,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25383,9 +25123,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25412,9 +25149,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25440,9 +25174,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25465,9 +25196,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25490,9 +25218,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25515,9 +25240,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25540,9 +25262,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25565,9 +25284,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25590,9 +25306,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25615,9 +25328,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25640,9 +25350,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25665,9 +25372,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25690,9 +25394,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25715,9 +25416,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25740,9 +25438,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25765,9 +25460,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"parameters": [
@@ -25803,9 +25495,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25831,9 +25520,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25859,9 +25545,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"responses": {
@@ -25888,9 +25571,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"parameters": [
@@ -25930,9 +25610,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"parameters": [
@@ -25977,9 +25654,6 @@
"security": [
{
"LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
}
],
"parameters": [
@@ -26024,7 +25698,8 @@
"200": {
"description": "The OpenAPI 3 document"
}
- }
+ },
+ "security": []
}
},
"/loopover/shot": {
@@ -26038,7 +25713,8 @@
"200": {
"description": "Preview image"
}
- }
+ },
+ "security": []
}
},
"/v1/public/subnet-interface": {
@@ -26053,14 +25729,7 @@
"description": "Subnet interface"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/public/repos/{owner}/{repo}/badge.json": {
@@ -26093,14 +25762,7 @@
"description": "Badge payload"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/public/repos/{owner}/{repo}/badge.svg": {
@@ -26133,14 +25795,7 @@
"description": "SVG badge"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/mcp/finding-taxonomy": {
@@ -26155,14 +25810,7 @@
"description": "Finding taxonomy"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/mcp/enrichment-analyzers": {
@@ -26177,14 +25825,7 @@
"description": "Analyzer descriptions"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/drafts": {
@@ -26205,14 +25846,7 @@
"description": "Draft submission is disabled"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/drafts/{id}": {
@@ -26240,14 +25874,7 @@
"description": "No such draft, or draft submission is disabled"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/drafts/auth/callback": {
@@ -26265,14 +25892,7 @@
"description": "Missing or invalid OAuth code"
}
},
- "security": [
- {
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
- }
- ]
+ "security": []
}
},
"/v1/ams/ingest": {
@@ -26284,10 +25904,7 @@
"summary": "Ingest an AMS miner telemetry batch",
"security": [
{
- "LoopOverBearer": []
- },
- {
- "LoopOverSessionCookie": []
+ "OrbBearer": []
}
],
"responses": {
diff --git a/src/api/routes.ts b/src/api/routes.ts
index bf3bc3858..e76869a5f 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 000000000..538675885
--- /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 45e5c77e0..8c8e0b350 100644
--- a/src/openapi/define-route.ts
+++ b/src/openapi/define-route.ts
@@ -76,7 +76,11 @@ function toSpecPath(path: string): string {
* 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: [] }];
diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts
index f21b25d8d..b22f1e86b 100644
--- a/src/openapi/spec.ts
+++ b/src/openapi/spec.ts
@@ -1,4 +1,5 @@
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";
@@ -1995,6 +1996,12 @@ export function buildOpenApiSpec() {
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() }) } } },
@@ -2397,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 ?? {}),
@@ -2430,18 +2447,26 @@ function applySecurityMetadata(document: GeneratedOpenApiDocument): GeneratedOpe
},
};
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/test/unit/define-route.test.ts b/test/unit/define-route.test.ts
index db8ccdb18..018a0d197 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 000000000..75b16ff37
--- /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 39ca4873b..8820d747a 100644
--- a/test/unit/openapi.test.ts
+++ b/test/unit/openapi.test.ts
@@ -187,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
From 72de26e42c5d200a24e530d6fd96caa76c2f89c3 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Tue, 28 Jul 2026 11:49:23 -0700
Subject: [PATCH 4/4] style: trim the blank line the isProtectedPath deletion
left at EOF
---
src/openapi/spec.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts
index b22f1e86b..25e479844 100644
--- a/src/openapi/spec.ts
+++ b/src/openapi/spec.ts
@@ -2469,4 +2469,3 @@ function applySecurityMetadata(document: GeneratedOpenApiDocument): GeneratedOpe
}
return document;
}
-