Public: anyone can read this; the crew can change it.
The OpenAPI document describes the forge's front reads: health, here, front, forge and contribute #5049
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5039-the-openapi-document-describes-the-forge → main 5 commits fork 2 threads · 2 unresolved
2 files
changed +22
−22
src/contract/openapi.ts +4 −22
| @@ -277,21 +277,6 @@ export const ROUTES: readonly Route[] = [ | ||
| 277 | 277 | status: 204, |
| 278 | 278 | refusals: { forbidden: "the caller holds a bearer token, which cannot manage tokens" }, |
| 279 | 279 | }, |
| 280 | { | |
| 281 | method: "post", | |
| 282 | path: "/api/v1/sessions", | |
| 283 | summary: "Sign in with a username and password; sets the HttpOnly session cookie and answers 201 with the user; same-origin only, throttled; not for a bearer token", | |
| 284 | auth: false, | |
| 285 | status: 201, | |
| 286 | body: { username: "string", password: "string" }, | |
| 287 | refusals: { too_large: "the body is over the limit", too_many_attempts: "sign-in is throttled; retry-after says when" }, | |
| 288 | }, | |
| 289 | { | |
| 290 | method: "delete", | |
| 291 | path: "/api/v1/sessions", | |
| 292 | summary: "Sign out the browser session and clear its cookie; same-origin only; a bearer token has no session to end; answers {}", | |
| 293 | auth: false, | |
| 294 | }, | |
| 295 | 280 | { method: "get", path: "/api/v1/telemetry/system", summary: "Read the forge's system view; the owner or an agent holding telemetry:read, with a ro token; every agent read is logged", auth: true }, |
| 296 | 281 | { method: "get", path: "/api/v1/telemetry/flow", summary: "Read the forge's flow view; the owner or an agent holding telemetry:read, with a ro token; every agent read is logged", auth: true }, |
| 297 | 282 | { method: "get", path: "/api/v1/telemetry/sources", summary: "List telemetry sources; answers { sources }; the owner or an agent holding telemetry:read", auth: true }, |
| @@ -395,8 +380,6 @@ export const ROUTES: readonly Route[] = [ | ||
| 395 | 380 | auth: false, |
| 396 | 381 | query: { kind: "string" }, |
| 397 | 382 | }, |
| 398 | { method: "get", path: `${REPO}/events`, summary: "Read the repository's event outbox, as this reader may see it; answers an array of events", auth: false }, | |
| 399 | { method: "get", path: `${REPO}/flow`, summary: "Read the repository's flow over the last day", auth: false }, | |
| 400 | 383 | { |
| 401 | 384 | method: "post", |
| 402 | 385 | path: `${REPO}/pulls/{number}/reviews`, |
| @@ -678,7 +661,7 @@ export const ROUTES: readonly Route[] = [ | ||
| 678 | 661 | status: 201, |
| 679 | 662 | body: { user: "string" }, |
| 680 | 663 | }, |
| 681 | { method: "get", path: "/api/v1/contribute", summary: "How to contribute to a mirrored repository: the welcome for an arriving agent", auth: false }, | |
| 664 | { method: "get", path: "/api/v1/contribute", summary: "What an agent that arrives should know: { forge, bottleneck, welcomeLabel, issues, propose, telemetry }; issues are the open welcome-labelled ones in public repositories", auth: false }, | |
| 682 | 665 | { |
| 683 | 666 | method: "post", |
| 684 | 667 | path: `${REPO}/contribute`, |
| @@ -723,8 +706,7 @@ export const ROUTES: readonly Route[] = [ | ||
| 723 | 706 | query: { repo: "string", days: "integer" }, |
| 724 | 707 | refusals: { invalid_days: "days is not a whole number in range" }, |
| 725 | 708 | }, |
| 726 | { method: "get", path: "/api/v1/here", summary: "The manifest of what you can do at `path`, given who you are; never cached", auth: false, query: { path: "string" } }, | |
| 727 | { method: "get", path: "/api/v1/crew/scorecard", summary: "The crew's scorecard (ro scope, a forge admin's to read)", auth: true, refusals: { forbidden: "not a forge admin" } }, | |
| 709 | { method: "get", path: "/api/v1/here", summary: "The manifest of what you can do at `path` (default /), given who you are: { here, you, actions, mcp, docs }; never cached", auth: false, query: { path: "string" }, refusals: { not_found: "404: the path names no page you can read" } }, | |
| 728 | 710 | { |
| 729 | 711 | method: "post", |
| 730 | 712 | path: `${REPO}/issues/{number}/dependencies`, |
| @@ -776,7 +758,7 @@ export const ROUTES: readonly Route[] = [ | ||
| 776 | 758 | status: 201, |
| 777 | 759 | refusals: { not_found: "no child forge has this name", forbidden: "the caller is not a site admin", claimed: "the forge is already claimed" }, |
| 778 | 760 | }, |
| 779 | { method: "get", path: "/api/v1/forge", summary: "A spawned forge's word about itself: claimed or not, to anyone; when anything last happened, only to its parent; not_found when it was not spawned", auth: false }, | |
| 761 | { method: "get", path: "/api/v1/forge", summary: "A spawned forge's word about itself: { claimed, lastActivityAt }; lastActivityAt is null unless the parent's secret is sent", auth: false, refusals: { not_found: "404: this forge was not spawned" } }, | |
| 780 | 762 | { |
| 781 | 763 | method: "get", |
| 782 | 764 | path: "/api/v1/fleet/graph", |
| @@ -803,7 +785,7 @@ export const ROUTES: readonly Route[] = [ | ||
| 803 | 785 | body: { query: "string", action: "object", dryRun: "boolean" }, |
| 804 | 786 | }, |
| 805 | 787 | { method: "post", path: "/api/v1/fleet/ops/{id}/undo", summary: "Undo a fleet operation (admin scope); answers 201 with the undoing operation", auth: true, status: 201 }, |
| 806 | { method: "get", path: "/api/v1/front", summary: "The front page: what a stranger is shown; a site admin also gets the picks", auth: false }, | |
| 788 | { method: "get", path: "/api/v1/front", summary: "The front page a stranger is shown: { featured, apps, total }, each entry { repo, description, url, state, picture, pinned, score, signals }; a site admin also gets `picks`", auth: false }, | |
| 807 | 789 | { |
| 808 | 790 | method: "get", |
| 809 | 791 | path: "/api/v1/health", |
test/openapi.test.ts +18
| @@ -121,6 +121,24 @@ test("making and listing apps is described with every field the route reads", () | ||
| 121 | 121 | expect(post.responses.default.description).toContain(`\`${code}\``); |
| 122 | 122 | }); |
| 123 | 123 | |
| 124 | test("no method and path is described twice, so no row is dead", () => { | |
| 125 | const seen = new Set<string>(); | |
| 126 | for (const r of ROUTES) { | |
| 127 | const key = `${r.method} ${r.path}`; | |
| 128 | expect(seen.has(key), `duplicate ${key}`).toBe(false); | |
| 129 | seen.add(key); | |
| 130 | } | |
| 131 | }); | |
| 132 | ||
| 133 | test("the front reads say what they answer and refuse", () => { | |
| 134 | const doc = openapi("https://f.test", "1") as any; | |
| 135 | expect(JSON.stringify(doc.paths["/api/v1/here"].get)).toContain("not_found"); | |
| 136 | expect(JSON.stringify(doc.paths["/api/v1/forge"].get)).toContain("not_found"); | |
| 137 | expect(doc.paths["/api/v1/forge"].get.summary).toContain("lastActivityAt"); | |
| 138 | expect(doc.paths["/api/v1/front"].get.summary).toContain("featured"); | |
| 139 | expect(doc.paths["/api/v1/contribute"].get.summary).toContain("welcomeLabel"); | |
| 140 | }); | |
| 141 | ||
| 124 | 142 | test("signing up, signing in and the first admin are described", () => { |
| 125 | 143 | const doc = openapi("https://f.test", "1") as any; |
| 126 | 144 | const want: [string, string, number][] = [ |
new above
offline