flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/api/src/openapi.rs

825 lines33,527 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API and MCP server in Rust; a public index at the API root1//! The OpenAPI document, generated from the same list the routes are.
Merge branch 'worktree-agent-ab2e39e11a6493412'2//!
3//! The docs site builds its API reference from a copy of this document,
4//! `apps/docs/src/data/openapi.json`. A test keeps the copy current: run
5//! `G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi` to rewrite it.
API and MCP server in Rust; a public index at the API root6
7use serde_json::{Map, Value, json};
8
9use crate::operations::Op;
10use crate::rest::{ROUTES, Route};
11
Merge branch 'worktree-agent-ab2e39e11a6493412'12/// The sections of the API reference: a name, what it covers, and its
13/// operations in the order a reader meets them.
14const SECTIONS: &[(&str, &str, &[Op])] = &[
15 (
16 "Accounts",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17 "Signing in from a tool, who a token acts as, and your email addresses.",
18 &[Op::Whoami, Op::ListEmails, Op::AddEmail, Op::RemoveEmail, Op::UpdateEmailSettings],
Merge branch 'worktree-agent-ab2e39e11a6493412'19 ),
20 (
21 "Workspaces",
22 "A workspace owns repositories and is the first part of their address. People and agents work in workspaces.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23 &[Op::CreateWorkspace, Op::DeleteWorkspace],
24 ),
25 (
26 "Invites",
27 "While g1t is invite-only, every new account needs an invite. Your invites, and inviting people into a workspace by email.",
28 &[
29 Op::ListInvites,
30 Op::CreateInvite,
31 Op::RevokeInvite,
32 Op::ListWorkspaceInvites,
33 Op::InviteMember,
34 Op::RevokeWorkspaceInvite,
35 ],
Merge branch 'worktree-agent-ab2e39e11a6493412'36 ),
37 (
38 "Repositories",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look39 "A repository, how it handles pull requests, and its timeline: renaming, archiving, moving and deleting it.",
Merge branch 'worktree-agent-ab2e39e11a6493412'40 &[
41 Op::ListRepos,
42 Op::CreateRepo,
43 Op::GetRepo,
44 Op::UpdateRepo,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look45 Op::RenameRepo,
46 Op::RenameBranch,
47 Op::SetRepoVisibility,
48 Op::ArchiveRepo,
49 Op::UnarchiveRepo,
50 Op::TransferRepo,
51 Op::DeleteRepo,
52 Op::ListDeletedRepos,
53 Op::RestoreRepo,
54 Op::PurgeRepo,
Merge branch 'worktree-agent-ab2e39e11a6493412'55 Op::GetRepoSettings,
56 Op::UpdateRepoSettings,
57 Op::ListEvents,
58 ],
59 ),
60 (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look61 "Access",
62 "Who can do what in a repository: repository roles, people given a role on one repository (outside collaborators when they are not members), invitations, and a workspace's base permission.",
63 &[
64 Op::ListCollaborators,
65 Op::AddCollaborator,
66 Op::UpdateCollaborator,
67 Op::RemoveCollaborator,
68 Op::GetCollaboratorPermission,
69 Op::ListRepoInvitations,
70 Op::RevokeRepoInvitation,
71 Op::ListMyRepoInvitations,
72 Op::AcceptRepoInvitation,
73 Op::DeclineRepoInvitation,
74 Op::SetBasePermission,
75 Op::ListOutsideCollaborators,
76 ],
77 ),
78 (
Merge branch 'worktree-agent-ab2e39e11a6493412'79 "Issues",
80 "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.",
81 &[
82 Op::ListIssues,
83 Op::CreateIssue,
84 Op::GetIssue,
85 Op::UpdateIssue,
86 Op::CloseIssue,
87 Op::ReopenIssue,
88 Op::AssignIssue,
89 Op::AddComment,
90 Op::ListLabels,
91 ],
92 ),
93 (
94 "Plans",
95 "An outcome turned into the issues that would get there, with the order they must merge in.",
96 &[Op::PlanWork, Op::GetPlan, Op::ApplyPlan],
97 ),
98 (
99 "Pull requests",
100 "A proposed change in its own fork or on a branch. Several can be made for one issue; the one merged resolves it.",
101 &[
102 Op::ListPullRequests,
103 Op::CreatePullRequest,
104 Op::GetPullRequest,
105 Op::GetPullRequestChanges,
106 Op::MarkPullRequestReady,
107 Op::ReviewPullRequest,
108 Op::MergePullRequest,
109 Op::ClosePullRequest,
110 Op::GetMergeQueue,
111 Op::MessageAgent,
112 Op::AnswerMessage,
113 Op::TakeMessages,
114 ],
115 ),
116 (
117 "Sessions",
118 "The record of how a pull request was made: prompts, reasoning and the tools that ran.",
119 &[Op::ReadSession, Op::RecordSession],
120 ),
121 (
Agents and memory, checks and conflicts, profiles, slug renames, custom domains122 "Memory",
123 "What agents and people learned that the next agent should know, for one project or across a workspace. Members and g1t's agents only; never a secret.",
124 &[Op::Remember, Op::Recall],
125 ),
126 (
Search across all of g1t, Explore, and a command palette127 "Search",
128 "One search across all of g1t: repositories, code, issues, pull requests, people and workspaces. Public content for everyone, and private content in workspaces you belong to.",
129 &[Op::Search],
130 ),
131 (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API132 "Context",
133 "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.",
134 &[Op::SearchContext, Op::GetEntity],
135 ),
136 (
Merge branch 'worktree-agent-ab2e39e11a6493412'137 "Actions",
138 "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.",
139 &[
140 Op::ListWorkflows,
141 Op::ListWorkflowRuns,
142 Op::GetWorkflowRun,
143 Op::GetJobLogs,
144 Op::DispatchWorkflow,
145 Op::CancelWorkflowRun,
146 Op::RerunWorkflowRun,
147 Op::UpdateWorkflow,
148 ],
149 ),
150 (
151 "Secrets and variables",
152 "Values that workflows and deployments read, per repository or for a whole workspace, with a row per environment.",
153 &[
154 Op::ListActionsSecrets,
155 Op::SetActionsSecret,
156 Op::DeleteActionsSecret,
157 Op::ListActionsVariables,
158 Op::SetActionsVariable,
159 Op::DeleteActionsVariable,
160 ],
161 ),
162 (
163 "Webhooks",
164 "Signed HTTPS requests sent to your own address as things happen, for a repository or a whole workspace.",
165 &[
166 Op::ListWebhooks,
167 Op::CreateWebhook,
168 Op::UpdateWebhook,
169 Op::DeleteWebhook,
170 Op::PingWebhook,
171 Op::ListWebhookDeliveries,
172 Op::RedeliverWebhook,
173 ],
174 ),
175 (
176 "Integrations",
177 "A workspace's connections to outside systems: model providers, alert sources and issue trackers.",
178 &[
179 Op::ListIntegrations,
180 Op::ConnectIntegration,
181 Op::DisconnectIntegration,
182 Op::TestIntegration,
183 Op::GetModelRoutes,
184 Op::SetModelRoutes,
185 Op::GetContext,
186 Op::ImportIssue,
187 ],
188 ),
189];
190
API and MCP server in Rust; a public index at the API root191/// The section of the API reference an operation is listed under.
192fn tag(op: Op) -> &'static str {
Merge branch 'worktree-agent-ab2e39e11a6493412'193 SECTIONS
API and MCP server in Rust; a public index at the API root194 .iter()
Merge branch 'worktree-agent-ab2e39e11a6493412'195 .find(|(_, _, ops)| ops.contains(&op))
196 .map_or("Repositories", |(name, _, _)| name)
197}
198
199/// What an operation's page is called, as a short sentence.
200fn title(op: Op) -> &'static str {
201 match op {
202 Op::Whoami => "Get the current user",
203 Op::CreateWorkspace => "Create a workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204 Op::DeleteWorkspace => "Delete a workspace",
205 Op::ListEmails => "List your email addresses",
206 Op::AddEmail => "Add an email address",
207 Op::RemoveEmail => "Remove an email address",
208 Op::UpdateEmailSettings => "Change your email settings",
209 Op::ListInvites => "List your invites",
210 Op::CreateInvite => "Create an invite",
211 Op::RevokeInvite => "Revoke an invite",
212 Op::ListWorkspaceInvites => "List a workspace's invites",
213 Op::InviteMember => "Invite someone to a workspace",
214 Op::RevokeWorkspaceInvite => "Revoke a workspace's invite",
215 Op::TransferRepo => "Transfer a repository",
216 Op::RenameRepo => "Rename a repository",
217 Op::RenameBranch => "Rename a branch",
218 Op::ArchiveRepo => "Archive a repository",
219 Op::UnarchiveRepo => "Unarchive a repository",
220 Op::SetRepoVisibility => "Change a repository's visibility",
221 Op::DeleteRepo => "Delete a repository",
222 Op::ListDeletedRepos => "List recently deleted repositories",
223 Op::RestoreRepo => "Restore a deleted repository",
224 Op::PurgeRepo => "Purge a deleted repository",
Merge branch 'worktree-agent-ab2e39e11a6493412'225 Op::ListRepos => "List repositories",
226 Op::GetRepo => "Get a repository",
227 Op::CreateRepo => "Create a repository",
228 Op::UpdateRepo => "Update a repository",
229 Op::GetRepoSettings => "Get repository settings",
230 Op::UpdateRepoSettings => "Update repository settings",
231 Op::GetMergeQueue => "Get the merge queue",
232 Op::MessageAgent => "Message an agent",
233 Op::AnswerMessage => "Answer a message",
234 Op::TakeMessages => "Take new messages",
Agents and memory, checks and conflicts, profiles, slug renames, custom domains235 Op::Remember => "Remember something",
236 Op::Recall => "Recall memory",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API237 Op::SearchContext => "Search the context hub",
238 Op::GetEntity => "Get a catalog entry",
Search across all of g1t, Explore, and a command palette239 Op::Search => "Search g1t",
Merge branch 'worktree-agent-ab2e39e11a6493412'240 Op::ListIssues => "List issues",
241 Op::GetIssue => "Get an issue",
242 Op::CreateIssue => "Create an issue",
243 Op::UpdateIssue => "Update an issue",
244 Op::CloseIssue => "Close an issue",
245 Op::ReopenIssue => "Reopen an issue",
246 Op::AssignIssue => "Assign an issue to the g1t agent",
247 Op::PlanWork => "Plan work",
248 Op::GetPlan => "Get a plan",
249 Op::ApplyPlan => "Apply a plan",
250 Op::ListLabels => "List labels",
251 Op::AddComment => "Add a comment",
252 Op::ReviewPullRequest => "Review a pull request",
253 Op::ListPullRequests => "List pull requests",
254 Op::GetPullRequest => "Get a pull request",
255 Op::CreatePullRequest => "Create a pull request",
256 Op::RecordSession => "Record session entries",
257 Op::ReadSession => "Read a session",
258 Op::MarkPullRequestReady => "Mark a pull request ready",
259 Op::ClosePullRequest => "Close a pull request",
260 Op::GetPullRequestChanges => "Get a pull request's changes",
261 Op::MergePullRequest => "Merge a pull request",
262 Op::ListEvents => "List repository events",
263 Op::ListIntegrations => "List integrations",
264 Op::ConnectIntegration => "Connect an integration",
265 Op::DisconnectIntegration => "Disconnect an integration",
266 Op::TestIntegration => "Test an integration",
267 Op::GetContext => "Look up a ticket",
268 Op::ImportIssue => "Import an issue",
269 Op::GetModelRoutes => "Get model routes",
270 Op::SetModelRoutes => "Set model routes",
271 Op::ListWebhooks => "List webhooks",
272 Op::CreateWebhook => "Create a webhook",
273 Op::UpdateWebhook => "Update a webhook",
274 Op::DeleteWebhook => "Delete a webhook",
275 Op::PingWebhook => "Ping a webhook",
276 Op::ListWebhookDeliveries => "List webhook deliveries",
277 Op::RedeliverWebhook => "Redeliver a webhook delivery",
278 Op::ListWorkflows => "List workflows",
279 Op::ListWorkflowRuns => "List workflow runs",
280 Op::GetWorkflowRun => "Get a workflow run",
281 Op::GetJobLogs => "Get a job's log",
282 Op::DispatchWorkflow => "Run a workflow",
283 Op::CancelWorkflowRun => "Cancel a workflow run",
284 Op::RerunWorkflowRun => "Re-run a workflow run",
285 Op::UpdateWorkflow => "Turn a workflow on or off",
286 Op::ListActionsSecrets => "List secrets",
287 Op::SetActionsSecret => "Set a secret",
288 Op::DeleteActionsSecret => "Delete a secret",
289 Op::ListActionsVariables => "List variables",
290 Op::SetActionsVariable => "Set a variable",
291 Op::DeleteActionsVariable => "Delete a variable",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look292 Op::ListCollaborators => "List who has access",
293 Op::AddCollaborator => "Add a collaborator",
294 Op::UpdateCollaborator => "Change a collaborator's role",
295 Op::RemoveCollaborator => "Remove a collaborator",
296 Op::GetCollaboratorPermission => "Get someone's permission",
297 Op::ListRepoInvitations => "List a repository's invitations",
298 Op::RevokeRepoInvitation => "Revoke a repository invitation",
299 Op::ListMyRepoInvitations => "List your repository invitations",
300 Op::AcceptRepoInvitation => "Accept a repository invitation",
301 Op::DeclineRepoInvitation => "Decline a repository invitation",
302 Op::SetBasePermission => "Set the base permission",
303 Op::ListOutsideCollaborators => "List outside collaborators",
API and MCP server in Rust; a public index at the API root304 }
305}
306
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look307/// Why an operation can be refused with `402 payment_required`, if it
308/// can: the ones that start an agent, when the workspace has no credit,
309/// and the ones that make a repository private in a workspace, when a free
310/// workspace's private storage has no room for it.
311fn may_need_payment(op: Op) -> Option<&'static str> {
312 match op {
313 Op::AssignIssue | Op::PlanWork | Op::ApplyPlan => Some("The workspace has no agent credit."),
314 Op::UpdateRepo | Op::SetRepoVisibility | Op::TransferRepo => Some(
315 "A free workspace's private storage has no room for this private repository.",
316 ),
317 _ => None,
318 }
Merge branch 'worktree-agent-ab2e39e11a6493412'319}
320
321/// What the reference says beyond each operation's own description, keyed
322/// by operation id, written by hand from what the services return: `notes`
323/// (Markdown, added to the description) and example `params` (path),
324/// `query`, `request` (body) and `response`.
325const REFERENCE: &str = include_str!("reference.json");
326
327fn examples() -> Map<String, Value> {
328 match serde_json::from_str(REFERENCE) {
329 Ok(Value::Object(examples)) => examples,
330 _ => Map::new(),
API and MCP server in Rust; a public index at the API root331 }
332}
333
Agents as a team: lifecycle, merge queue, billing and a new shell334/// `/repos/:owner/:name` as OpenAPI writes it: `/repos/{owner}/{name}`.
API and MCP server in Rust; a public index at the API root335fn openapi_path(route: &Route) -> String {
336 route
337 .path
338 .split('/')
339 .map(|segment| match segment.strip_prefix(':') {
340 Some(name) => format!("{{{name}}}"),
341 None => segment.to_owned(),
342 })
343 .collect::<Vec<_>>()
344 .join("/")
345}
346
347fn error_response(description: &str) -> Value {
348 json!({
349 "description": description,
350 "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } },
351 })
352}
353
Merge branch 'worktree-agent-ab2e39e11a6493412'354/// A parameter in the path or the query, described by the operation's
355/// input schema where it has the same name.
356fn parameter(name: &str, place: &str, required: bool, schema: Option<&Value>) -> Value {
357 let mut schema = schema.cloned().unwrap_or_else(|| json!({ "type": "string" }));
358 let description = match name {
359 "owner" => Some(Value::from("The workspace that owns the repository.")),
360 "name" => Some(Value::from("The repository's name.")),
361 _ => schema.as_object_mut().and_then(|schema| schema.remove("description")),
362 };
363 let mut parameter = json!({
364 "name": name,
365 "in": place,
366 "required": required,
367 "schema": schema,
368 });
369 if let Some(description) = description {
370 parameter["description"] = description;
371 }
372 parameter
373}
374
375/// The operation id of a route. An operation reached at a workspace's
376/// address as well as a repository's is documented once for each, with its
377/// own id; GitHub's alternative addresses for one operation keep GitHub's
378/// names.
379fn operation_id(route: &Route) -> String {
380 let op = route.op;
381 let base = match (route.method, route.path.rsplit('/').next().unwrap_or_default()) {
382 ("PUT", "enable") => "enable_workflow".to_owned(),
383 ("PUT", "disable") => "disable_workflow".to_owned(),
384 ("POST", "rerun-failed-jobs") => "rerun_failed_jobs".to_owned(),
385 ("PATCH", ":setting") => "update_actions_variable".to_owned(),
386 ("GET", "runs") if route.path.contains("/workflows/:workflow/") => "list_runs_of_workflow".to_owned(),
387 _ => op.name().to_owned(),
388 };
389 if route.path.starts_with("/workspaces/") && ROUTES.iter().any(|other| other.op == op && other.path.starts_with("/repos/")) {
390 format!("{base}_for_workspace")
391 } else {
392 base
393 }
394}
395
396/// The summary of a route: its operation's title, or for one of GitHub's
397/// alternative addresses, what that address does.
398fn summary(route: &Route, id: &str) -> String {
399 let base = match id.trim_end_matches("_for_workspace") {
400 "enable_workflow" => "Turn a workflow on",
401 "disable_workflow" => "Turn a workflow off",
402 "rerun_failed_jobs" => "Re-run failed jobs",
403 "update_actions_variable" => "Update a variable",
404 "list_runs_of_workflow" => "List a workflow's runs",
405 _ => title(route.op),
406 };
407 if id.ends_with("_for_workspace") {
408 format!("{base} for a workspace")
409 } else {
410 base.to_owned()
411 }
412}
413
API and MCP server in Rust; a public index at the API root414fn operation(route: &Route) -> Value {
415 let op = route.op;
416 let path_params: Vec<&str> = route.params().collect();
417 // `owner` and `name` in the path stand for the operation's `repo` input.
418 let covered = |name: &str| name == "repo" || path_params.contains(&name);
Merge branch 'worktree-agent-ab2e39e11a6493412'419 let all_properties = op.properties();
420 let mut properties = all_properties.clone();
API and MCP server in Rust; a public index at the API root421 properties.retain(|name, _| !covered(name));
422 let required: Vec<String> = op
423 .required()
424 .into_iter()
425 .filter(|name| !covered(name))
426 .collect();
427
428 let mut parameters: Vec<Value> = path_params
429 .iter()
Merge branch 'worktree-agent-ab2e39e11a6493412'430 .map(|name| parameter(name, "path", true, all_properties.get(*name)))
API and MCP server in Rust; a public index at the API root431 .collect();
432 let mut body = Value::Null;
433 if route.method == "GET" {
434 for (name, key) in route.query {
Merge branch 'worktree-agent-ab2e39e11a6493412'435 parameters.push(parameter(
436 name,
437 "query",
438 required.iter().any(|required| required == key),
439 properties.get(*key),
440 ));
API and MCP server in Rust; a public index at the API root441 }
442 } else if !properties.is_empty() {
443 let mut schema = json!({ "type": "object", "properties": properties });
444 if !required.is_empty() {
445 schema["required"] = json!(required);
446 }
447 body = json!({
448 "required": !required.is_empty(),
449 "content": { "application/json": { "schema": schema } },
450 });
451 }
452
Merge branch 'worktree-agent-ab2e39e11a6493412'453 let id = operation_id(route);
454 let mut responses = Map::new();
455 responses.insert(
456 "200".into(),
457 json!({
458 "description": "Success.",
459 "content": { "application/json": { "schema": {} } },
460 }),
461 );
462 responses.insert(
463 "401".into(),
464 error_response("A token is required, or the one sent is not valid."),
465 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look466 if let Some(reason) = may_need_payment(op) {
467 responses.insert("402".into(), error_response(reason));
Merge branch 'worktree-agent-ab2e39e11a6493412'468 }
469 responses.insert("403".into(), error_response("Signed in, but not allowed to do this."));
Search across all of g1t, Explore, and a command palette470 if !matches!(op, Op::Whoami | Op::ListRepos | Op::Search) {
Merge branch 'worktree-agent-ab2e39e11a6493412'471 responses.insert("404".into(), error_response("It does not exist, or you cannot see it."));
472 }
473 if route.method != "GET" {
474 responses.insert(
475 "409".into(),
476 error_response("The request conflicts with the current state."),
477 );
478 }
479 if op != Op::Whoami {
480 responses.insert("422".into(), error_response("The input is not valid."));
481 }
482 // Public data can be read without a token; everything else needs one.
483 let security = if op.needs_user() {
484 json!([{ "token": [] }])
Webhooks: every event, to your own addresses, signed and retried485 } else {
Merge branch 'worktree-agent-ab2e39e11a6493412'486 json!([{ "token": [] }, {}])
Webhooks: every event, to your own addresses, signed and retried487 };
API and MCP server in Rust; a public index at the API root488 let mut described = json!({
Webhooks: every event, to your own addresses, signed and retried489 "operationId": id,
API and MCP server in Rust; a public index at the API root490 "tags": [tag(op)],
Merge branch 'worktree-agent-ab2e39e11a6493412'491 "summary": summary(route, &id),
API and MCP server in Rust; a public index at the API root492 "description": op.description(),
Merge branch 'worktree-agent-ab2e39e11a6493412'493 "x-mcp-tool": op.name(),
494 "security": security,
API and MCP server in Rust; a public index at the API root495 "parameters": parameters,
Merge branch 'worktree-agent-ab2e39e11a6493412'496 "responses": responses,
API and MCP server in Rust; a public index at the API root497 });
498 if !body.is_null() {
499 described["requestBody"] = body;
500 }
501 described
502}
503
504/// Entries for device sign-in, which is not an operation.
505fn onboarding() -> Map<String, Value> {
506 let paths = json!({
Agents as a team: lifecycle, merge queue, billing and a new shell507 "/device/code": {
API and MCP server in Rust; a public index at the API root508 "post": {
509 "operationId": "device_code",
510 "tags": ["Accounts"],
511 "summary": "Start signing in",
Agents as a team: lifecycle, merge queue, billing and a new shell512 "description": "Begins a device sign-in. Show the person `verification_uri_complete` and have them open it in a browser, where they sign in or register and approve the code. Then poll `/device/token`.",
API and MCP server in Rust; a public index at the API root513 "security": [],
514 "requestBody": {
515 "content": { "application/json": { "schema": {
516 "type": "object",
517 "properties": {
518 "client_name": {
519 "type": "string",
520 "description": "What is asking, shown to the person approving. For example, Claude Code.",
521 },
522 },
523 } } },
524 },
525 "responses": { "200": {
526 "description": "The codes for this sign-in.",
527 "content": { "application/json": { "schema": {
528 "type": "object",
529 "properties": {
Agents as a team: lifecycle, merge queue, billing and a new shell530 "device_code": { "type": "string", "description": "Secret. Send it to /device/token." },
API and MCP server in Rust; a public index at the API root531 "user_code": { "type": "string", "description": "Shown to the person, like WDJB-MJHT." },
532 "verification_uri": { "type": "string" },
533 "verification_uri_complete": {
534 "type": "string",
535 "description": "The link to give the person; it carries the code.",
536 },
537 "expires_in": { "type": "integer", "description": "Seconds until the codes expire." },
538 "interval": { "type": "integer", "description": "Seconds to wait between polls." },
539 },
540 } } },
541 } },
542 },
543 },
Agents as a team: lifecycle, merge queue, billing and a new shell544 "/device/token": {
API and MCP server in Rust; a public index at the API root545 "post": {
546 "operationId": "device_token",
547 "tags": ["Accounts"],
548 "summary": "Finish signing in",
549 "description": "Asks whether the person has approved. Poll no faster than the interval. The token is returned once.",
550 "security": [],
551 "requestBody": {
552 "required": true,
553 "content": { "application/json": { "schema": {
554 "type": "object",
555 "required": ["device_code"],
556 "properties": { "device_code": { "type": "string" } },
557 } } },
558 },
559 "responses": { "200": {
560 "description": "The state of the sign-in.",
561 "content": { "application/json": { "schema": {
562 "type": "object",
563 "required": ["status"],
564 "properties": {
565 "status": { "type": "string", "enum": ["pending", "approved", "denied", "expired"] },
566 "token": { "type": "string", "description": "Present when approved." },
567 "username": { "type": "string" },
568 "verified": {
569 "type": "boolean",
570 "description": "Whether the account's email is confirmed.",
571 },
572 },
573 } } },
574 } },
575 },
576 },
577 });
578 match paths {
579 Value::Object(paths) => paths,
580 _ => Map::new(),
581 }
582}
583
Merge branch 'worktree-agent-ab2e39e11a6493412'584
585/// Puts each operation's examples, where it has them, into its request
586/// and response. Path and query values go under `x-example-params` and
587/// `x-example-query`, which tools that build a request can use.
588fn attach_examples(paths: &mut Map<String, Value>) {
589 let examples = examples();
590 for methods in paths.values_mut() {
591 let Some(methods) = methods.as_object_mut() else { continue };
592 for operation in methods.values_mut() {
593 let id = operation["operationId"].as_str().unwrap_or_default().to_owned();
594 let tool = operation["x-mcp-tool"].as_str().unwrap_or_default().to_owned();
595 let Some(example) = examples.get(&id).or_else(|| examples.get(&tool)) else {
596 continue;
597 };
598 if let Some(notes) = example.get("notes").and_then(Value::as_str) {
599 let description = operation["description"].as_str().unwrap_or_default();
600 operation["description"] = json!(format!("{description}\n\n{notes}"));
601 }
602 if let Some(response) = example.get("response") {
603 let content = &mut operation["responses"]["200"]["content"]["application/json"];
604 if content.is_object() {
605 content["example"] = response.clone();
606 }
607 }
608 if let Some(request) = example.get("request") {
609 let content = &mut operation["requestBody"]["content"]["application/json"];
610 if content.is_object() {
611 content["example"] = request.clone();
612 }
613 }
614 for (key, extension) in [("params", "x-example-params"), ("query", "x-example-query")] {
615 if let Some(values) = example.get(key) {
616 operation[extension] = values.clone();
617 }
618 }
619 }
620 }
621}
622
API and MCP server in Rust; a public index at the API root623pub fn document() -> Value {
624 let mut paths = onboarding();
625 for route in ROUTES {
626 let entry = paths
627 .entry(openapi_path(route))
628 .or_insert_with(|| json!({}));
629 entry[route.method.to_lowercase()] = operation(route);
630 }
Merge branch 'worktree-agent-ab2e39e11a6493412'631 attach_examples(&mut paths);
632 let tags: Vec<Value> = SECTIONS
633 .iter()
634 .map(|(name, description, ops)| {
635 json!({
636 "name": name,
637 "description": description,
638 // The section's operations in reading order, by MCP tool name.
639 "x-tools": ops.iter().map(|op| op.name()).collect::<Vec<_>>(),
640 })
641 })
642 .collect();
643 let codes = ["unauthenticated", "payment_required", "forbidden", "not_found", "conflict", "invalid"];
API and MCP server in Rust; a public index at the API root644 json!({
645 "openapi": "3.1.0",
646 "info": {
647 "title": "g1t API",
648 "version": "1",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API649 "description": "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh. Every name in a request or response body is `snake_case`; names you chose, such as a workflow's inputs or a secret's name, are returned as you wrote them.",
API and MCP server in Rust; a public index at the API root650 "license": { "name": "MIT", "identifier": "MIT" },
651 },
652 "servers": [{ "url": "https://api.g1t.sh" }],
653 "security": [{ "token": [] }, {}],
Merge branch 'worktree-agent-ab2e39e11a6493412'654 "tags": tags,
API and MCP server in Rust; a public index at the API root655 "paths": paths,
656 "components": {
657 "securitySchemes": {
658 "token": {
659 "type": "http",
660 "scheme": "bearer",
661 "description": "An access token, `g1t_…`. Public data needs none.",
662 },
663 },
664 "schemas": {
665 "Error": {
666 "type": "object",
667 "required": ["error"],
668 "properties": {
669 "error": {
670 "type": "object",
671 "required": ["code", "message"],
672 "properties": {
Merge branch 'worktree-agent-ab2e39e11a6493412'673 "code": { "type": "string", "enum": codes },
API and MCP server in Rust; a public index at the API root674 "message": { "type": "string" },
675 },
676 },
677 },
678 },
679 },
680 },
681 })
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 #[test]
689 fn every_route_is_documented_once() {
690 let document = document();
691 let mut ids = Vec::new();
692 for (_, methods) in document["paths"].as_object().unwrap() {
693 for (_, operation) in methods.as_object().unwrap() {
694 ids.push(operation["operationId"].as_str().unwrap().to_owned());
695 }
696 }
697 for op in Op::ALL {
698 assert_eq!(
699 ids.iter().filter(|id| *id == op.name()).count(),
700 1,
701 "{}",
702 op.name()
703 );
704 }
Webhooks: every event, to your own addresses, signed and retried705 let mut unique = ids.clone();
706 unique.sort();
707 unique.dedup();
708 assert_eq!(unique.len(), ids.len(), "operation ids repeat");
API and MCP server in Rust; a public index at the API root709 }
710
711 #[test]
712 fn path_and_query_inputs_are_not_repeated_in_the_body() {
713 let document = document();
Agents as a team: lifecycle, merge queue, billing and a new shell714 let merge = &document["paths"]["/repos/{owner}/{name}/pulls/{number}/merge"]["post"];
API and MCP server in Rust; a public index at the API root715 let body = &merge["requestBody"]["content"]["application/json"]["schema"]["properties"];
716 assert!(body.get("keep_issue_open").is_some());
717 assert!(body.get("repo").is_none() && body.get("number").is_none());
Agents as a team: lifecycle, merge queue, billing and a new shell718 let list = &document["paths"]["/repos"]["get"];
API and MCP server in Rust; a public index at the API root719 assert_eq!(list["parameters"][0]["name"], "q");
720 assert!(list.get("requestBody").is_none());
721 }
722
723 #[test]
Merge branch 'worktree-agent-ab2e39e11a6493412'724 fn every_operation_is_in_one_section() {
725 for op in Op::ALL {
726 let sections = SECTIONS
727 .iter()
728 .filter(|(_, _, ops)| ops.contains(&op))
729 .count();
730 assert_eq!(sections, 1, "{}", op.name());
731 }
732 }
733
734 #[test]
API and MCP server in Rust; a public index at the API root735 fn titles_read_as_sentences() {
Merge branch 'worktree-agent-ab2e39e11a6493412'736 assert_eq!(title(Op::CreateIssue), "Create an issue");
API and MCP server in Rust; a public index at the API root737 assert_eq!(title(Op::Whoami), "Get the current user");
738 }
Merge branch 'worktree-agent-ab2e39e11a6493412'739
740 #[test]
741 fn every_operation_has_an_example_response() {
742 let examples = examples();
743 assert!(!examples.is_empty(), "reference.json does not parse");
744 let document = document();
745 let mut known = Vec::new();
746 for (path, methods) in document["paths"].as_object().unwrap() {
747 for (method, operation) in methods.as_object().unwrap() {
748 known.push(operation["operationId"].as_str().unwrap().to_owned());
749 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
750 assert!(!example.is_null(), "{method} {path} has no example response");
751 }
752 }
753 for id in examples.keys() {
754 assert!(known.contains(id), "reference.json names {id}, which is not an operation");
755 }
756 }
757
758 #[test]
759 fn example_requests_send_only_what_the_body_takes() {
760 let document = document();
761 for (path, methods) in document["paths"].as_object().unwrap() {
762 for (method, operation) in methods.as_object().unwrap() {
763 let content = &operation["requestBody"]["content"]["application/json"];
764 let Some(example) = content["example"].as_object() else { continue };
765 let properties = &content["schema"]["properties"];
766 for key in example.keys() {
767 assert!(!properties[key].is_null(), "{method} {path}: {key} is not in the body");
768 }
769 }
770 }
771 }
772
773 /// The docs site's copy of the document. Run with `G1T_WRITE_OPENAPI=1`
774 /// to rewrite it after changing an operation.
775 #[test]
776 fn the_docs_copy_is_current() {
777 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../docs/src/data/openapi.json");
778 let current = serde_json::to_string_pretty(&document()).unwrap() + "\n";
779 if std::env::var_os("G1T_WRITE_OPENAPI").is_some() {
780 std::fs::write(path, &current).unwrap();
781 return;
782 }
783 let copy = std::fs::read_to_string(path).unwrap_or_default().replace("\r\n", "\n");
784 assert!(
785 copy == current,
786 "apps/docs/src/data/openapi.json is out of date: run G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi"
787 );
788 }
API reference: no example reads as a real secret789
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API790 /// The reference shows responses as they are sent: `snake_case`.
791 #[test]
792 fn example_responses_are_snake_case() {
793 let document = document();
794 for (path, methods) in document["paths"].as_object().unwrap() {
795 for (method, operation) in methods.as_object().unwrap() {
796 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
797 let leaked = g1t_kit::wire::camel_case_keys(example);
798 assert!(leaked.is_empty(), "{method} {path} shows {leaked:?}");
799 }
800 }
801 }
802
API reference: no example reads as a real secret803 /// Examples never hold anything that reads as a real credential, which
804 /// secret scanners rightly flag in a public repository: they end in `…`
805 /// after the prefix, as `whsec_…` and `g1t_…` do.
806 #[test]
807 fn examples_hold_no_real_looking_secrets() {
808 let prefixes = ["whsec_", "g1t_", "sk_live_", "sk_test_", "ghp_", "github_pat_", "xoxb-", "AKIA"];
809 for (line, text) in REFERENCE.lines().enumerate() {
810 for prefix in prefixes {
811 let mut rest = text;
812 while let Some(at) = rest.find(prefix) {
813 let after = &rest[at + prefix.len()..];
814 let run = after.chars().take_while(|c| c.is_ascii_alphanumeric()).count();
815 assert!(
816 run < 12,
817 "reference.json line {}: `{prefix}` followed by {run} characters reads as a real secret; write `{prefix}…`",
818 line + 1
819 );
820 rest = after;
821 }
822 }
823 }
824 }
API and MCP server in Rust; a public index at the API root825}

This file's history is long; its oldest lines are credited to the oldest commit read.