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/rest.rs

639 lines16,466 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//! REST: each route maps an HTTP request onto one operation.
2
3use serde_json::{Map, Value};
4
5use crate::operations::Op;
6
7pub struct Route {
8 pub method: &'static str,
9 /// Segments starting with `:` are parameters.
10 pub path: &'static str,
11 pub op: Op,
12 /// Query parameters the route reads, as `(name in the URL, input name)`.
13 pub query: &'static [(&'static str, &'static str)],
14}
15
16const fn route(
17 method: &'static str,
18 path: &'static str,
19 op: Op,
20 query: &'static [(&'static str, &'static str)],
21) -> Route {
22 Route {
23 method,
24 path,
25 op,
26 query,
27 }
28}
29
30pub const ROUTES: &[Route] = &[
Agents as a team: lifecycle, merge queue, billing and a new shell31 route("GET", "/user", Op::Whoami, &[]),
32 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
33 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
34 route("POST", "/repos", Op::CreateRepo, &[]),
35 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
36 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
37 route(
38 "GET",
39 "/repos/:owner/:name/settings",
40 Op::GetRepoSettings,
41 &[],
42 ),
43 route(
44 "PATCH",
45 "/repos/:owner/:name/settings",
46 Op::UpdateRepoSettings,
47 &[],
48 ),
49 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
API and MCP server in Rust; a public index at the API root50 route(
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request51 "POST",
52 "/repos/:owner/:name/pulls/:number/messages",
53 Op::MessageAgent,
54 &[],
55 ),
56 route(
57 "POST",
58 "/repos/:owner/:name/pulls/:number/messages/take",
59 Op::TakeMessages,
60 &[],
61 ),
62 route(
Docs worth reading, and kept that way63 "POST",
64 "/repos/:owner/:name/messages/:id/answer",
65 Op::AnswerMessage,
66 &[],
67 ),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains68 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
69 route(
70 "GET",
71 "/repos/:owner/:name/memory",
72 Op::Recall,
73 &[("q", "query"), ("limit", "limit")],
74 ),
Docs worth reading, and kept that way75 route(
API and MCP server in Rust; a public index at the API root76 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell77 "/repos/:owner/:name/events",
API and MCP server in Rust; a public index at the API root78 Op::ListEvents,
79 &[("before", "before")],
80 ),
Agents as a team: lifecycle, merge queue, billing and a new shell81 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
API and MCP server in Rust; a public index at the API root82 route(
83 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell84 "/repos/:owner/:name/issues",
API and MCP server in Rust; a public index at the API root85 Op::ListIssues,
86 &[("state", "state"), ("label", "label")],
87 ),
Agents as a team: lifecycle, merge queue, billing and a new shell88 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
API and MCP server in Rust; a public index at the API root89 route(
90 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell91 "/repos/:owner/:name/issues/:number",
API and MCP server in Rust; a public index at the API root92 Op::GetIssue,
93 &[],
94 ),
95 route(
96 "PATCH",
Agents as a team: lifecycle, merge queue, billing and a new shell97 "/repos/:owner/:name/issues/:number",
API and MCP server in Rust; a public index at the API root98 Op::UpdateIssue,
99 &[],
100 ),
101 route(
102 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell103 "/repos/:owner/:name/issues/:number/close",
API and MCP server in Rust; a public index at the API root104 Op::CloseIssue,
105 &[],
106 ),
107 route(
108 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell109 "/repos/:owner/:name/issues/:number/reopen",
API and MCP server in Rust; a public index at the API root110 Op::ReopenIssue,
111 &[],
112 ),
113 route(
114 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell115 "/repos/:owner/:name/issues/:number/assign",
116 Op::AssignIssue,
117 &[],
118 ),
Integrations: your own model provider, alerts that open issues, tickets agents read119 route(
120 "POST",
121 "/repos/:owner/:name/issues/import",
122 Op::ImportIssue,
123 &[],
124 ),
125 route(
126 "GET",
127 "/repos/:owner/:name/context",
128 Op::GetContext,
129 &[("reference", "reference")],
130 ),
131 route(
132 "GET",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API133 "/workspaces/:workspace/context/search",
134 Op::SearchContext,
135 &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
136 ),
137 route(
138 "GET",
139 "/workspaces/:workspace/context/:kind/:id",
140 Op::GetEntity,
141 &[],
142 ),
143 route(
144 "GET",
Integrations: your own model provider, alerts that open issues, tickets agents read145 "/workspaces/:workspace/integrations",
146 Op::ListIntegrations,
147 &[],
148 ),
149 route(
150 "POST",
151 "/workspaces/:workspace/integrations",
152 Op::ConnectIntegration,
153 &[],
154 ),
155 route(
Models per workspace: several providers, routed by kind of work156 "GET",
Webhooks: every event, to your own addresses, signed and retried157 "/repos/:owner/:name/hooks",
158 Op::ListWebhooks,
159 &[],
160 ),
161 route(
162 "POST",
163 "/repos/:owner/:name/hooks",
164 Op::CreateWebhook,
165 &[],
166 ),
167 route(
168 "PATCH",
169 "/repos/:owner/:name/hooks/:id",
170 Op::UpdateWebhook,
171 &[],
172 ),
173 route(
174 "DELETE",
175 "/repos/:owner/:name/hooks/:id",
176 Op::DeleteWebhook,
177 &[],
178 ),
179 route(
180 "POST",
181 "/repos/:owner/:name/hooks/:id/pings",
182 Op::PingWebhook,
183 &[],
184 ),
185 route(
186 "GET",
187 "/repos/:owner/:name/hooks/:id/deliveries",
188 Op::ListWebhookDeliveries,
189 &[],
190 ),
191 route(
192 "POST",
193 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
194 Op::RedeliverWebhook,
195 &[],
196 ),
197 route(
198 "GET",
199 "/workspaces/:workspace/hooks",
200 Op::ListWebhooks,
201 &[],
202 ),
203 route(
204 "POST",
205 "/workspaces/:workspace/hooks",
206 Op::CreateWebhook,
207 &[],
208 ),
209 route(
210 "PATCH",
211 "/workspaces/:workspace/hooks/:id",
212 Op::UpdateWebhook,
213 &[],
214 ),
215 route(
216 "DELETE",
217 "/workspaces/:workspace/hooks/:id",
218 Op::DeleteWebhook,
219 &[],
220 ),
221 route(
222 "POST",
223 "/workspaces/:workspace/hooks/:id/pings",
224 Op::PingWebhook,
225 &[],
226 ),
227 route(
228 "GET",
229 "/workspaces/:workspace/hooks/:id/deliveries",
230 Op::ListWebhookDeliveries,
231 &[],
232 ),
233 route(
234 "POST",
235 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
236 Op::RedeliverWebhook,
237 &[],
238 ),
239 route(
240 "GET",
Models per workspace: several providers, routed by kind of work241 "/workspaces/:workspace/model-routes",
242 Op::GetModelRoutes,
243 &[],
244 ),
245 route(
246 "PUT",
247 "/workspaces/:workspace/model-routes",
248 Op::SetModelRoutes,
249 &[],
250 ),
251 route(
Integrations: your own model provider, alerts that open issues, tickets agents read252 "DELETE",
253 "/workspaces/:workspace/integrations/:id",
254 Op::DisconnectIntegration,
255 &[],
256 ),
257 route(
258 "POST",
259 "/workspaces/:workspace/integrations/:id/test",
260 Op::TestIntegration,
261 &[],
262 ),
Automations: rules in .g1t/automations that act when something happens263 route(
264 "GET",
GitHub Actions on g1t, part two: running workflows265 "/repos/:owner/:name/actions/workflows",
266 Op::ListWorkflows,
267 &[],
268 ),
269 route(
270 "GET",
271 "/repos/:owner/:name/actions/workflows/:workflow/runs",
272 Op::ListWorkflowRuns,
273 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
274 ),
275 route(
276 "POST",
277 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
278 Op::DispatchWorkflow,
279 &[],
280 ),
281 route(
282 "PATCH",
283 "/repos/:owner/:name/actions/workflows/:workflow",
284 Op::UpdateWorkflow,
285 &[],
286 ),
287 route(
288 "PUT",
289 "/repos/:owner/:name/actions/workflows/:workflow/enable",
290 Op::UpdateWorkflow,
291 &[],
292 ),
293 route(
294 "PUT",
295 "/repos/:owner/:name/actions/workflows/:workflow/disable",
296 Op::UpdateWorkflow,
297 &[],
298 ),
299 route(
300 "GET",
301 "/repos/:owner/:name/actions/runs",
302 Op::ListWorkflowRuns,
303 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
304 ),
305 route(
306 "GET",
307 "/repos/:owner/:name/actions/runs/:id",
308 Op::GetWorkflowRun,
309 &[],
310 ),
311 route(
312 "POST",
313 "/repos/:owner/:name/actions/runs/:id/cancel",
314 Op::CancelWorkflowRun,
315 &[],
316 ),
317 route(
318 "POST",
319 "/repos/:owner/:name/actions/runs/:id/rerun",
320 Op::RerunWorkflowRun,
321 &[],
322 ),
323 route(
324 "POST",
325 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
326 Op::RerunWorkflowRun,
327 &[],
328 ),
329 route(
330 "GET",
331 "/repos/:owner/:name/actions/jobs/:job/logs",
332 Op::GetJobLogs,
333 &[("after", "after")],
334 ),
335 route(
336 "GET",
337 "/repos/:owner/:name/actions/secrets",
338 Op::ListActionsSecrets,
339 &[],
340 ),
341 route(
342 "PUT",
343 "/repos/:owner/:name/actions/secrets/:setting",
344 Op::SetActionsSecret,
345 &[],
346 ),
347 route(
348 "DELETE",
349 "/repos/:owner/:name/actions/secrets/:setting",
350 Op::DeleteActionsSecret,
351 &[],
352 ),
353 route(
354 "GET",
355 "/repos/:owner/:name/actions/variables",
356 Op::ListActionsVariables,
357 &[],
358 ),
359 route(
360 "POST",
361 "/repos/:owner/:name/actions/variables",
362 Op::SetActionsVariable,
363 &[],
364 ),
365 route(
366 "PATCH",
367 "/repos/:owner/:name/actions/variables/:setting",
368 Op::SetActionsVariable,
369 &[],
370 ),
371 route(
372 "DELETE",
373 "/repos/:owner/:name/actions/variables/:setting",
374 Op::DeleteActionsVariable,
375 &[],
376 ),
377 route(
378 "GET",
379 "/workspaces/:workspace/actions/secrets",
380 Op::ListActionsSecrets,
381 &[],
382 ),
383 route(
384 "PUT",
385 "/workspaces/:workspace/actions/secrets/:setting",
386 Op::SetActionsSecret,
387 &[],
388 ),
389 route(
390 "DELETE",
391 "/workspaces/:workspace/actions/secrets/:setting",
392 Op::DeleteActionsSecret,
393 &[],
394 ),
395 route(
396 "GET",
397 "/workspaces/:workspace/actions/variables",
398 Op::ListActionsVariables,
399 &[],
400 ),
401 route(
402 "POST",
403 "/workspaces/:workspace/actions/variables",
404 Op::SetActionsVariable,
405 &[],
406 ),
407 route(
408 "PATCH",
409 "/workspaces/:workspace/actions/variables/:setting",
410 Op::SetActionsVariable,
411 &[],
412 ),
413 route(
414 "DELETE",
415 "/workspaces/:workspace/actions/variables/:setting",
416 Op::DeleteActionsVariable,
417 &[],
418 ),
Agents as a team: lifecycle, merge queue, billing and a new shell419 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
420 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
421 route(
422 "POST",
423 "/repos/:owner/:name/plans/:plan/apply",
424 Op::ApplyPlan,
425 &[],
426 ),
427 route(
428 "POST",
429 "/repos/:owner/:name/issues/:number/comments",
API and MCP server in Rust; a public index at the API root430 Op::AddComment,
431 &[],
432 ),
433 route(
434 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell435 "/repos/:owner/:name/pulls",
API and MCP server in Rust; a public index at the API root436 Op::ListPullRequests,
437 &[("state", "state")],
438 ),
439 route(
440 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell441 "/repos/:owner/:name/pulls",
API and MCP server in Rust; a public index at the API root442 Op::CreatePullRequest,
443 &[],
444 ),
445 route(
446 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell447 "/repos/:owner/:name/pulls/:number",
API and MCP server in Rust; a public index at the API root448 Op::GetPullRequest,
449 &[],
450 ),
451 route(
452 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell453 "/repos/:owner/:name/pulls/:number/changes",
API and MCP server in Rust; a public index at the API root454 Op::GetPullRequestChanges,
455 &[],
456 ),
457 route(
Acceptance checks in sandboxes, line comments and review verdicts458 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell459 "/repos/:owner/:name/pulls/:number/reviews",
Acceptance checks in sandboxes, line comments and review verdicts460 Op::ReviewPullRequest,
461 &[],
462 ),
463 route(
API and MCP server in Rust; a public index at the API root464 "GET",
Agents as a team: lifecycle, merge queue, billing and a new shell465 "/repos/:owner/:name/pulls/:number/session",
API and MCP server in Rust; a public index at the API root466 Op::ReadSession,
467 &[("after", "after")],
468 ),
469 route(
470 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell471 "/repos/:owner/:name/pulls/:number/session",
API and MCP server in Rust; a public index at the API root472 Op::RecordSession,
473 &[],
474 ),
475 route(
476 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell477 "/repos/:owner/:name/pulls/:number/ready",
API and MCP server in Rust; a public index at the API root478 Op::MarkPullRequestReady,
479 &[],
480 ),
481 route(
482 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell483 "/repos/:owner/:name/pulls/:number/close",
API and MCP server in Rust; a public index at the API root484 Op::ClosePullRequest,
485 &[],
486 ),
487 route(
488 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell489 "/repos/:owner/:name/pulls/:number/merge",
API and MCP server in Rust; a public index at the API root490 Op::MergePullRequest,
491 &[],
492 ),
493];
494
495impl Route {
496 /// The names of the route's path parameters, in order.
497 pub fn params(&self) -> impl Iterator<Item = &'static str> {
498 self.path
499 .split('/')
500 .filter_map(|segment| segment.strip_prefix(':'))
501 }
502
503 /// The values of the path parameters, if `path` is this route's.
504 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
505 let mut values = Vec::new();
506 let mut actual = path.trim_end_matches('/').split('/');
507 for expected in self.path.split('/') {
508 let segment = actual.next()?;
509 match expected.strip_prefix(':') {
510 Some(name) if !segment.is_empty() => values.push((name, segment)),
511 Some(_) => return None,
512 None if expected == segment => {}
513 None => return None,
514 }
515 }
516 actual.next().is_none().then_some(values)
517 }
518}
519
520/// The route for a request, and the operation input it describes.
521///
522/// The input is the JSON body, overlaid with the query parameters the route
523/// reads and then with what the path names: `owner` and `name` become
524/// `repo`, and `number` becomes an integer.
525pub fn resolve(
526 method: &str,
527 path: &str,
528 query: &[(String, String)],
529 body: Value,
530) -> Option<(&'static Route, Value)> {
531 let (route, params) = ROUTES
532 .iter()
533 .filter(|route| route.method == method)
534 .find_map(|route| Some((route, route.matches(path)?)))?;
535
536 let mut input = match body {
537 Value::Object(fields) => fields,
538 _ => Map::new(),
539 };
540 for (name, key) in route.query {
541 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
542 input.insert((*key).to_owned(), Value::String(value.clone()));
543 }
544 }
545 let param = |wanted: &str| {
546 params
547 .iter()
548 .find(|(name, _)| *name == wanted)
549 .map(|(_, value)| *value)
550 };
551 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
552 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
553 }
GitHub Actions on g1t, part two: running workflows554 for key in ["plan", "id", "workspace", "delivery", "workflow", "job", "setting"] {
Docs worth reading, and kept that way555 if let Some(value) = param(key) {
556 input.insert(key.to_owned(), Value::String(value.to_owned()));
557 }
Agents as a team: lifecycle, merge queue, billing and a new shell558 }
GitHub Actions on g1t, part two: running workflows559 // GitHub says some things with the path alone.
560 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
561 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
562 }
563 if route.path.ends_with("/rerun-failed-jobs") {
564 input.insert("failed_only".to_owned(), Value::Bool(true));
565 }
API and MCP server in Rust; a public index at the API root566 if let Some(number) = param("number") {
567 // Not a number: zero, which no issue or pull request has.
568 input.insert(
569 "number".to_owned(),
570 number.parse::<u32>().unwrap_or(0).into(),
571 );
572 }
573 Some((route, Value::Object(input)))
574}
575
576#[cfg(test)]
577mod tests {
578 use serde_json::json;
579
580 use super::*;
581
582 #[test]
583 fn a_path_resolves_to_its_operation_and_input() {
584 let (route, input) = resolve(
585 "POST",
Agents as a team: lifecycle, merge queue, billing and a new shell586 "/repos/syntaqx/hello/pulls/14/merge",
API and MCP server in Rust; a public index at the API root587 &[],
588 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
589 )
590 .unwrap();
591 assert_eq!(route.op, Op::MergePullRequest);
592 // What the path names wins over the body.
593 assert_eq!(
594 input,
595 json!({ "keep_issue_open": true, "number": 14, "repo": "syntaqx/hello" })
596 );
597 }
598
599 #[test]
600 fn query_parameters_are_renamed() {
601 let query = [
602 ("q".to_owned(), "parser".to_owned()),
603 ("x".to_owned(), "y".to_owned()),
604 ];
Agents as a team: lifecycle, merge queue, billing and a new shell605 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
API and MCP server in Rust; a public index at the API root606 assert_eq!(route.op, Op::ListRepos);
607 assert_eq!(input, json!({ "query": "parser" }));
608 }
609
610 #[test]
611 fn method_and_shape_must_match() {
Agents as a team: lifecycle, merge queue, billing and a new shell612 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
613 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
614 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
615 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
API and MCP server in Rust; a public index at the API root616 }
617
618 #[test]
619 fn every_parameter_and_query_name_is_an_input() {
620 for route in ROUTES {
621 let properties = route.op.properties();
622 for (_, key) in route.query {
623 assert!(properties.contains_key(*key), "{}: {key}", route.path);
624 }
625 for name in route.params() {
626 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
627 || properties.contains_key(name);
628 assert!(covered, "{}: {name}", route.path);
629 }
630 }
631 }
632
633 #[test]
634 fn every_operation_has_a_route() {
635 for op in Op::ALL {
636 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
637 }
638 }
639}