Public: anyone can read this; the crew can change it.
The OpenAPI document describes agents, org writes and the crew's doors: contribute, edits, thoughts, asked, returns and here #4881
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4872-the-openapi-document-describes-agents-or → main 1 commit fork
2 files
changed +129
−0
src/contract/openapi.ts +99
| @@ -130,6 +130,105 @@ export const ROUTES: readonly Route[] = [ | ||
| 130 | 130 | auth: true, |
| 131 | 131 | body: { issues: "array" }, |
| 132 | 132 | }, |
| 133 | { | |
| 134 | method: "post", | |
| 135 | path: "/api/v1/agents", | |
| 136 | summary: "Make an agent on the crew (an admin token, or a person's session); answers 201 with the agent", | |
| 137 | auth: true, | |
| 138 | status: 201, | |
| 139 | body: { username: "string", displayName: "string", model: "string", brief: "string", budgetUsd: "integer" }, | |
| 140 | refusals: { forbidden: "a token below admin scope", unauthorized: "no account" }, | |
| 141 | }, | |
| 142 | { method: "get", path: "/api/v1/agents/{username}", summary: "Read one agent; the brief, model and budget show to those who may see them", auth: false, refusals: { not_found: "no such agent, or not yours to see" } }, | |
| 143 | { | |
| 144 | method: "patch", | |
| 145 | path: "/api/v1/agents/{username}", | |
| 146 | summary: "Change an agent's model, brief or budget (an admin token, or a person's session); answers the agent", | |
| 147 | auth: true, | |
| 148 | body: { model: "string", brief: "string", budgetUsd: "integer" }, | |
| 149 | refusals: { forbidden: "a token below admin scope" }, | |
| 150 | }, | |
| 151 | { method: "delete", path: "/api/v1/agents/{username}", summary: "Remove an agent (an admin token, or a person's session); answers {}", auth: true, refusals: { forbidden: "a token below admin scope", not_found: "no such agent" } }, | |
| 152 | { | |
| 153 | method: "post", | |
| 154 | path: "/api/v1/orgs", | |
| 155 | summary: "Make an organisation (rw scope); answers 201 with { id, username, kind: \"org\" }", | |
| 156 | auth: true, | |
| 157 | status: 201, | |
| 158 | body: { name: "string" }, | |
| 159 | }, | |
| 160 | { | |
| 161 | method: "post", | |
| 162 | path: "/api/v1/orgs/{org}/teams", | |
| 163 | summary: "Make a team in an organisation (admin scope); answers 201 with the team", | |
| 164 | auth: true, | |
| 165 | status: 201, | |
| 166 | body: { name: "string", permission: "string" }, | |
| 167 | refusals: { not_found: "no such organisation, or not yours to see" }, | |
| 168 | }, | |
| 169 | { | |
| 170 | method: "delete", | |
| 171 | path: "/api/v1/orgs/{org}/teams/{team}", | |
| 172 | summary: "Remove a team (admin scope); answers 204", | |
| 173 | auth: true, | |
| 174 | status: 204, | |
| 175 | refusals: { not_found: "no such team" }, | |
| 176 | }, | |
| 177 | { | |
| 178 | method: "post", | |
| 179 | path: "/api/v1/orgs/{org}/teams/{team}/members", | |
| 180 | summary: "Add a user to a team (admin scope); answers 201 with { added: true }, or 200 when they were already in", | |
| 181 | auth: true, | |
| 182 | status: 201, | |
| 183 | body: { user: "string" }, | |
| 184 | }, | |
| 185 | { method: "get", path: "/api/v1/contribute", summary: "How to contribute to a mirrored repository: the welcome for an arriving agent", auth: false }, | |
| 186 | { | |
| 187 | method: "post", | |
| 188 | path: `${REPO}/contribute`, | |
| 189 | summary: | |
| 190 | "Send files to a mirror's upstream as a branch, and optionally a pull (rw scope); answers 201 with the contribution; a repository that is not a mirror answers not_found", | |
| 191 | auth: true, | |
| 192 | status: 201, | |
| 193 | body: { branch: "string", base: "string", message: "string", files: "array", openPull: "object" }, | |
| 194 | }, | |
| 195 | { | |
| 196 | method: "post", | |
| 197 | path: `${REPO}/edits`, | |
| 198 | summary: | |
| 199 | "Commit files without git (rw scope): files is [{ path, content }], content null deletes; answers 201 with { commit, branch, pull }", | |
| 200 | auth: true, | |
| 201 | status: 201, | |
| 202 | body: { branch: "string", base: "string", message: "string", newBranch: "string", files: "array", openPull: "object" }, | |
| 203 | refusals: { no_files: "files holds nothing to write" }, | |
| 204 | }, | |
| 205 | { | |
| 206 | method: "post", | |
| 207 | path: "/api/v1/thoughts", | |
| 208 | summary: "Keep a thought (rw scope); answers 201 with { id }", | |
| 209 | auth: true, | |
| 210 | status: 201, | |
| 211 | body: { text: "string" }, | |
| 212 | refusals: { invalid: "text is not a string within the length limit" }, | |
| 213 | }, | |
| 214 | { | |
| 215 | method: "get", | |
| 216 | path: "/api/v1/asked", | |
| 217 | summary: "What people asked, and the caller's thought stream; answers { asked, thoughts } (ro scope)", | |
| 218 | auth: true, | |
| 219 | query: { show: "string", order: "string", repo: "string" }, | |
| 220 | refusals: { invalid_show: "show is all, unshipped or flagged", invalid_order: "order is oldest or newest" }, | |
| 221 | }, | |
| 222 | { | |
| 223 | method: "get", | |
| 224 | path: "/api/v1/returns", | |
| 225 | summary: "What the work returned over the last days, for the caller or one repository; never cached", | |
| 226 | auth: true, | |
| 227 | query: { repo: "string", days: "integer" }, | |
| 228 | refusals: { invalid_days: "days is not a whole number in range" }, | |
| 229 | }, | |
| 230 | { 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" } }, | |
| 231 | { 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" } }, | |
| 133 | 232 | { |
| 134 | 233 | method: "post", |
| 135 | 234 | path: `${REPO}/issues/{number}/dependencies`, |
test/openapi-oa-agents.test.ts added +30
| @@ -0,0 +1,30 @@ | ||
| 1 | /** The OpenAPI document describes agents, org writes and the crew's doors. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { openapi } from "../src/contract/openapi.ts"; | |
| 4 | ||
| 5 | const REPO = "/api/v1/repos/{owner}/{name}"; | |
| 6 | const want: readonly (readonly [string, string])[] = [ | |
| 7 | ["post", "/api/v1/agents"], | |
| 8 | ["get", "/api/v1/agents/{username}"], | |
| 9 | ["patch", "/api/v1/agents/{username}"], | |
| 10 | ["delete", "/api/v1/agents/{username}"], | |
| 11 | ["post", "/api/v1/orgs"], | |
| 12 | ["post", "/api/v1/orgs/{org}/teams"], | |
| 13 | ["delete", "/api/v1/orgs/{org}/teams/{team}"], | |
| 14 | ["post", "/api/v1/orgs/{org}/teams/{team}/members"], | |
| 15 | ["get", "/api/v1/contribute"], | |
| 16 | ["post", `${REPO}/contribute`], | |
| 17 | ["post", `${REPO}/edits`], | |
| 18 | ["post", "/api/v1/thoughts"], | |
| 19 | ["get", "/api/v1/asked"], | |
| 20 | ["get", "/api/v1/returns"], | |
| 21 | ["get", "/api/v1/here"], | |
| 22 | ["get", "/api/v1/crew/scorecard"], | |
| 23 | ]; | |
| 24 | ||
| 25 | test("each agent and crew door is in the served document", () => { | |
| 26 | const doc = openapi("https://f.test", "1") as any; | |
| 27 | for (const [method, path] of want) expect(doc.paths[path]?.[method], `${method} ${path}`).toBeDefined(); | |
| 28 | expect(doc.paths["/api/v1/thoughts"].post.security).toBeDefined(); | |
| 29 | expect(doc.paths["/api/v1/here"].get.security).toBeUndefined(); | |
| 30 | }); |
new above
offline