Skip to content

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.sh
989 linesCodeBlameRaw

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.

Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1//! The public API: REST at api.g1t.sh and the MCP server at mcp.g1t.sh.
2//!
3//! One Worker, two hostnames. Both are thin adapters over the same
4//! operations (see [`operations::Op`]), which call the services that own
5//! the data. This Worker holds none.
6
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb977mod about;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.8mod run_artifacts;
Merge branch 'worktree-agent-aaf03bdceac799c89'9mod addresses;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily10mod alerts;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms11mod audit;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit12mod billing;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms13mod blobs;
Merge checks: statuses and check runs on every commit14mod checks;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r915mod customize;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9716mod deployments;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca17mod deploy_keys;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails18mod limits;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)19mod logs;
Merge branch 'mirroring' into artifacts-mode20mod mirrors;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms21mod mcp;
API: notifications over REST and MCP, with notifications scopes22mod notifications;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms23mod oauth;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R224mod oidc;
Merge packages: roles, Actions access, source label, soft delete, API25mod packages;
Merge main into Artifacts Phase 226mod folios;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar27mod people;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms28mod openapi;
API: pinned projects over REST and MCP29mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9730mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'31mod protection;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms32mod operations;
33mod renamed;
34#[cfg(test)]
35mod responses;
36mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge37mod rules;
Fast pages, required checks on the branch, self-hosted runners, honest incidents38mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar39mod security;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step40mod tools;
API and MCP for a workspace's personal access token rules, members' tokens and approvals41mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R242mod toolkit;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms43
Merge branch 'model-routing'44use g1t_contracts::billing::{FinishRunArgs, RunTokens};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms45use g1t_contracts::identity::{
46 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
47};
48use g1t_contracts::work::{
49 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs, ReportPlanArgs,
50 ReportQueueArgs, ReportReviewArgs,
51};
52use g1t_contracts::identity::AgentScope;
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)53use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms54use g1t_kit::wire;
55use serde_json::{Value, json};
56use worker::{Context, Env, Method, Request, Response, Result, event};
57
58use operations::Services;
59
60fn method_name(method: Method) -> &'static str {
61 match method {
62 Method::Get => "GET",
63 Method::Post => "POST",
64 Method::Patch => "PATCH",
65 Method::Put => "PUT",
66 Method::Delete => "DELETE",
67 Method::Options => "OPTIONS",
68 Method::Head => "HEAD",
69 _ => "OTHER",
70 }
71}
72
73/// A JSON response. Every body the API sends has its keys in `snake_case`;
74/// the contracts it passes through are `camelCase`, so they are converted
75/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
76/// the MCP protocol's own envelope keep the spelling their standards use.
77pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
78 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
79}
80
81/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
82/// workflow file, GitHub's contexts and event, and where to check out, all
83/// passed through as they are.
84const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'85 "spec", "workflow", "github", "event", "contexts", "checkout", "permissions",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms86];
87
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R288/// Puts the toolkit's variables in a job's spec: its runtime token, where
89/// the toolkit's services are, and where to ask for an OIDC token when the
90/// job may have one and this installation issues them. The `runtime` the
91/// actions service sent goes no further.
92fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
93 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
94 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
95 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
96 let vars = toolkit::runtime_variables(api, token, id_token);
97 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
98 variables.extend(vars);
99 }
100}
101
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)102/// What a person whose account has not confirmed its email address may
103/// call: who they are, their addresses, and confirming one with the code
104/// from the email. Nothing over MCP.
105fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
106 !on_mcp
107 && matches!(
108 (method, path.trim_end_matches('/')),
109 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
110 )
111}
112
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms113/// An error in the shape every endpoint uses.
114fn failure(failure: &Failure) -> Result<Response> {
115 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
116}
117
118fn fail(code: FailureCode, message: &str) -> Result<Response> {
119 failure(&Failure {
120 code,
121 message: message.to_owned(),
122 })
123}
124
125/// A request body as JSON. An empty or malformed body is no input.
126async fn json_body(request: &mut Request) -> Value {
127 request.json().await.unwrap_or(Value::Null)
128}
129
Merge branch 'worktree-agent-a1eb74e8a85291bbb' into claude/cool-newton-7nm8r9130/// Who a request's `Authorization: Bearer g1t_…` (or `token g1t_…`, as
131/// Octokit sends it) names. A missing token is an anonymous viewer; a
132/// wrong one is refused, so that a typo does not silently look signed out.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms133async fn authenticate(
134 request: &Request,
135 services: &Services,
136) -> Result<std::result::Result<Viewer, Response>> {
137 let header = request.headers().get("authorization")?.unwrap_or_default();
Merge branch 'worktree-agent-a1eb74e8a85291bbb' into claude/cool-newton-7nm8r9138 let Some(token) = limits::bearer(&header) else {
139 return Ok(Ok(None));
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms140 };
141 let viewer: Viewer = g1t_kit::call(
142 &services.identity,
143 "user_for_access_token",
144 &TokenArgs {
145 token: token.to_owned(),
146 },
147 )
148 .await?;
149 if viewer.is_some() {
150 return Ok(Ok(viewer));
151 }
152 let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?;
153 // Tells an MCP client where to sign in again.
154 response.headers_mut().set(
155 "www-authenticate",
Merge branch 'worktree-agent-aaf03bdceac799c89'156 &format!("{}, error=\"invalid_token\"", services.addresses.mcp_challenge()),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms157 )?;
158 Ok(Err(response))
159}
160
161/// Where everything is, for someone or something exploring the API.
Merge branch 'worktree-agent-aaf03bdceac799c89'162fn index(addresses: &addresses::Addresses) -> Value {
163 let api = &addresses.api;
164 let repo = format!("{api}/repos/{{owner}}/{{name}}");
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms165 json!({
166 "documentation_url": "https://docs.g1t.sh/reference/api/",
Merge branch 'worktree-agent-aaf03bdceac799c89'167 "openapi_url": format!("{api}/openapi.json"),
168 "mcp_url": addresses.mcp,
169 "current_user_url": format!("{api}/user"),
170 "workspaces_url": format!("{api}/workspaces"),
171 "repositories_url": format!("{api}/repos{{?q}}"),
172 "search_url": format!("{api}/search{{?q,type,page,per_page}}"),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms173 "repository_url": repo,
174 "repository_events_url": format!("{repo}/events{{?before}}"),
175 "labels_url": format!("{repo}/labels"),
176 "issues_url": format!("{repo}/issues{{?state,label}}"),
177 "issue_url": format!("{repo}/issues/{{number}}"),
178 "issue_comments_url": format!("{repo}/issues/{{number}}/comments"),
179 "pulls_url": format!("{repo}/pulls{{?state}}"),
180 "pull_url": format!("{repo}/pulls/{{number}}"),
181 "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"),
182 "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"),
183 "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"),
Merge branch 'worktree-agent-aaf03bdceac799c89'184 "device_code_url": format!("{api}/device/code"),
185 "device_token_url": format!("{api}/device/token"),
186 "oauth_metadata_url": format!("{api}/.well-known/oauth-authorization-server"),
187 "git_url": format!("{}/{{owner}}/{{name}}.git", addresses.site),
188 "integrations_url": format!("{api}/workspaces/{{workspace}}/integrations"),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms189 "context_url": format!("{repo}/context{{?reference}}"),
190 "import_issue_url": format!("{repo}/issues/import"),
Merge branch 'worktree-agent-aaf03bdceac799c89'191 "hooks_url": format!("{api}/hooks/{{integration}}"),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms192 })
193}
194
195// Signing in from a tool. Accounts are created, and passwords typed, only
196// in a browser; a tool gets its token by having a person approve a code.
197
198/// Passes a request from an outside system to its connection, as it came:
199/// its signature covers the exact bytes of the body.
200async fn receive_hook(request: &mut Request, services: &Services, id: &str) -> Result<Response> {
201 let headers: std::collections::HashMap<String, String> = request
202 .headers()
203 .entries()
204 .map(|(name, value)| (name.to_lowercase(), value))
205 .collect();
206 let body = request.text().await.unwrap_or_default();
207 // A push to GitHub with many commits makes a large payload.
208 let limit = if id == "github" { 10_000_000 } else { 1_000_000 };
209 if body.len() > limit {
210 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
211 }
212 // g1t's GitHub App has one webhook for every installation; it is
213 // checked against the app's own secret.
214 let (method, args) = if id == "github" {
215 ("github_receive", json!({ "headers": headers, "body": body }))
216 } else {
217 ("receive", json!({ "id": id, "headers": headers, "body": body }))
218 };
219 let received: g1t_contracts::integrations::Received =
220 g1t_kit::call(&services.integrations, method, &args).await?;
221 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
222}
223
224async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
225 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
226 let payload = request.text().await.unwrap_or_default();
227 if payload.len() > 1_000_000 || signature.is_empty() {
228 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
229 }
230 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
231 &env.service("BILLING")?,
232 "stripe_webhook",
233 &g1t_contracts::billing::StripeWebhookArgs { payload, signature },
234 )
235 .await?;
236 // A refusal is a 400, so Stripe shows it as failed; anything handled,
237 // or already handled, is a 200, so Stripe stops sending it.
238 Ok(match handled {
239 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
240 g1t_contracts::Outcome::Fail(failure) => {
241 reply(&json!({ "message": failure.message }))?.with_status(400)
242 }
243 })
244}
245
246async fn device_code(request: &mut Request, services: &Services) -> Result<Response> {
247 let body = json_body(request).await;
248 let started: DeviceStart = g1t_kit::call(
249 &services.identity,
250 "device_start",
251 &DeviceStartArgs {
252 client_name: body["client_name"].as_str().unwrap_or_default().to_owned(),
253 },
254 )
255 .await?;
256 reply(&json!({
257 "device_code": started.device_code,
258 "user_code": started.user_code,
Merge branch 'worktree-agent-aaf03bdceac799c89'259 "verification_uri": format!("{}/device", services.addresses.site),
260 "verification_uri_complete": format!("{}/device?code={}", services.addresses.site, started.user_code),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms261 "expires_in": started.expires_in,
262 "interval": started.interval,
263 }))
264}
265
266async fn device_token(request: &mut Request, services: &Services) -> Result<Response> {
267 let body = json_body(request).await;
268 let claim: DeviceClaim = g1t_kit::call(
269 &services.identity,
270 "device_claim",
271 &DeviceClaimArgs {
272 device_code: body["device_code"].as_str().unwrap_or_default().to_owned(),
273 },
274 )
275 .await?;
276 reply(&match claim {
277 DeviceClaim::Approved { token, user } => json!({
278 "status": "approved",
279 "token": token,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar280 "display_username": user.display_username.clone().filter(|display| display.eq_ignore_ascii_case(&user.username)).unwrap_or_else(|| user.username.clone()),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms281 "username": user.username,
282 "verified": user.verified,
283 }),
284 DeviceClaim::Pending => json!({ "status": "pending" }),
285 DeviceClaim::Denied => json!({ "status": "denied" }),
286 DeviceClaim::Expired => json!({ "status": "expired" }),
287 })
288}
289
Fast pages, required checks on the branch, self-hosted runners, honest incidents290/// A sandbox reporting on a run of an issue's commands, from before a pull
291/// request's checks were the workflows run on it.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms292/// The run's own token, in the body, is the credential: it was given to
293/// that sandbox and to nothing else.
294async fn report_checks(
295 request: &mut Request,
296 services: &Services,
297 run_id: &str,
298) -> Result<Response> {
299 let body = json_body(request).await;
300 let reported: Outcome<CheckRun> = g1t_kit::call(
301 &services.work,
302 "report_checks",
303 &ReportChecksArgs {
304 run_id: run_id.to_owned(),
305 token: body["token"].as_str().unwrap_or_default().to_owned(),
306 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
307 error: body["error"].as_str().map(str::to_owned),
308 skip: false,
309 },
310 )
311 .await?;
312 match reported {
313 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
314 Outcome::Fail(refused) => failure(&refused),
315 }
316}
317
318/// A sandbox reporting one tested state of a merge queue. As with checks,
319/// the entry's own token is the credential.
320async fn report_queue(
321 request: &mut Request,
322 services: &Services,
323 entry_id: &str,
324) -> Result<Response> {
325 let body = json_body(request).await;
326 let reported: Outcome<QueueState> = g1t_kit::call(
327 &services.work,
328 "report_queue",
329 &ReportQueueArgs {
330 entry_id: entry_id.to_owned(),
331 token: body["token"].as_str().unwrap_or_default().to_owned(),
332 combined_commit: body["combinedCommit"].as_str().map(str::to_owned),
333 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
334 error: body["error"].as_str().map(str::to_owned),
335 conflict_with: body["conflictWith"].as_u64().map(|n| n as u32),
336 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
337 },
338 )
339 .await?;
340 match reported {
341 Outcome::Ok(state) => reply(&json!({ "state": state })),
342 Outcome::Fail(refused) => failure(&refused),
343 }
344}
345
346/// A sandbox reporting whether a pull request merges cleanly. As with
347/// checks, the probe's own token is the credential.
348async fn report_mergecheck(
349 request: &mut Request,
350 services: &Services,
351 pull_id: &str,
352) -> Result<Response> {
353 let body = json_body(request).await;
354 let reported: Outcome<Mergeable> = g1t_kit::call(
355 &services.work,
356 "report_mergecheck",
357 &ReportMergecheckArgs {
358 pull_id: pull_id.to_owned(),
359 token: body["token"].as_str().unwrap_or_default().to_owned(),
360 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
361 error: body["error"].as_str().map(str::to_owned),
362 },
363 )
364 .await?;
365 match reported {
366 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
367 Outcome::Fail(refused) => failure(&refused),
368 }
369}
370
Merge branch 'worktree-agent-ac5b181a013e54348'371/// A backup's sandbox, passed on to the repos service, which holds the
372/// job (services/repos/src/backups.rs; the flow is in
373/// `g1t_contracts::backups`):
374///
375/// - `POST /backups/{job}/spec`: what to cut, and a read-only git credential
376/// - `PUT /backups/{job}/parts/{n}`: one part of the bundle, as bytes
377/// - `POST /backups/{job}/complete` with `{ refs, size, sha256, parts, fetched_bytes }`
378/// - `POST /backups/{job}/fail` with `{ error, fetched_bytes }`
379///
380/// Bodies are passed through as they are: snake_case already, and a
381/// bundle's refs are keyed by ref names, which must not be converted.
382async fn backup_job(request: &mut Request, services: &Services, method: &str, path: &str) -> Result<Response> {
383 use g1t_contracts::backups::TOKEN_HEADER;
384 let token = request.headers().get(TOKEN_HEADER)?.unwrap_or_default();
385 let rest = path.trim_start_matches("/backups/");
386 let (job, action) = rest.split_once('/').unwrap_or((rest, ""));
387 if job.is_empty() || token.is_empty() {
388 return fail(FailureCode::Unauthenticated, "A backup job's token is required.");
389 }
390 if method == "PUT" && action.starts_with("parts/") {
391 let bytes = request.bytes().await?;
392 if bytes.len() as u64 > g1t_contracts::backups::PART_BYTES {
393 return fail(FailureCode::Invalid, "A part holds 32 MiB at most.");
394 }
395 let headers = worker::Headers::new();
396 headers.set(TOKEN_HEADER, &token)?;
397 let mut init = worker::RequestInit::new();
398 init.with_method(Method::Put)
399 .with_headers(headers)
400 .with_body(Some(worker::js_sys::Uint8Array::from(bytes.as_slice()).into()));
401 let forwarded = Request::new_with_init(&format!("https://repos/backups/{job}/{action}"), &init)?;
402 let mut answered = services.repos.fetch_request(forwarded).await?;
403 return outcome_as_given(answered.json().await?);
404 }
405 let rpc = match (method, action) {
406 ("POST", "spec") => "backup_spec",
407 ("POST", "complete") => "backup_complete",
408 ("POST", "fail") => "backup_fail",
409 _ => return fail(FailureCode::NotFound, "No such endpoint."),
410 };
411 let mut body = json_body(request).await;
412 if !body.is_object() {
413 body = json!({});
414 }
415 body["job_id"] = json!(job);
416 body["token"] = json!(token);
417 let answered: Value = g1t_kit::call(&services.repos, rpc, &body).await?;
418 outcome_as_given(answered)
419}
420
421/// An `Outcome` from a service whose keys are already the API's: the value,
422/// or the failure in the shape every endpoint uses.
423fn outcome_as_given(answered: Value) -> Result<Response> {
424 match serde_json::from_value::<Outcome<Value>>(answered)? {
425 Outcome::Ok(value) => Response::from_json(&value),
426 Outcome::Fail(refused) => failure(&refused),
427 }
428}
429
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms430/// A sandbox reporting the review its agent wrote. As with checks, the
431/// run's own token is the credential.
432async fn report_review(
433 request: &mut Request,
434 services: &Services,
435 run_id: &str,
436) -> Result<Response> {
437 let body = json_body(request).await;
438 let reported: Outcome<bool> = g1t_kit::call(
439 &services.work,
440 "report_review",
441 &ReportReviewArgs {
442 run_id: run_id.to_owned(),
443 token: body["token"].as_str().unwrap_or_default().to_owned(),
444 verdict: serde_json::from_value(body["verdict"].clone()).unwrap_or(None),
445 body: body["body"].as_str().unwrap_or_default().to_owned(),
446 comments: serde_json::from_value(body["comments"].clone()).unwrap_or_default(),
447 model: body["model"].as_str().map(str::to_owned),
448 error: body["error"].as_str().map(str::to_owned),
449 },
450 )
451 .await?;
452 match reported {
453 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
454 Outcome::Fail(refused) => failure(&refused),
455 }
456}
457
458/// A sandbox reporting the plan its agent wrote. As with checks, the
459/// plan's own token is the credential.
460async fn report_plan(
461 request: &mut Request,
462 services: &Services,
463 plan_id: &str,
464) -> Result<Response> {
465 let body = json_body(request).await;
466 let reported: Outcome<bool> = g1t_kit::call(
467 &services.work,
468 "report_plan",
469 &ReportPlanArgs {
470 plan_id: plan_id.to_owned(),
471 token: body["token"].as_str().unwrap_or_default().to_owned(),
472 summary: body["summary"].as_str().unwrap_or_default().to_owned(),
473 issues: serde_json::from_value(body["issues"].clone()).unwrap_or_default(),
474 error: body["error"].as_str().map(str::to_owned),
475 },
476 )
477 .await?;
478 match reported {
479 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
480 Outcome::Fail(refused) => failure(&refused),
481 }
482}
483
484/// A sandbox reporting what its agent's run cost, so that the workspace
485/// it worked for is charged. As with checks, the run's own token is the
486/// credential.
487async fn report_usage(
488 request: &mut Request,
489 services: &Services,
490 run_id: &str,
491) -> Result<Response> {
492 let body = json_body(request).await;
493 let charged: Outcome<bool> = g1t_kit::call(
494 &services.billing,
495 "finish_run",
496 &FinishRunArgs {
497 run_id: run_id.to_owned(),
498 token: body["token"].as_str().unwrap_or_default().to_owned(),
499 cost_usd: body["cost_usd"].as_f64().unwrap_or_default(),
500 turns: body["turns"].as_u64().unwrap_or_default() as u32,
Merge branch 'model-routing'501 // What the harness counted; the agent rate is charged on no
502 // fewer, on a workspace's own model key too.
503 tokens: body.get("tokens").filter(|t| t.is_object()).map(|t| RunTokens {
504 input: t["input"].as_u64().unwrap_or_default(),
505 output: t["output"].as_u64().unwrap_or_default(),
506 cache_read: t["cache_read"].as_u64().unwrap_or_default(),
507 cache_write: t["cache_write"].as_u64().unwrap_or_default(),
508 }),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms509 },
510 )
511 .await?;
512 match charged {
513 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
514 Outcome::Fail(refused) => failure(&refused),
515 }
516}
517
518async fn respond(mut request: Request, env: &Env) -> Result<Response> {
519 let method = method_name(request.method());
520 if method == "OPTIONS" {
521 return Ok(Response::empty()?.with_status(204));
522 }
523 let url = request.url()?;
524 // Paths carry no version. An earlier form began with `/v1`, which is
525 // still accepted so that nothing already written against it breaks.
526 let path = match url.path().strip_prefix("/v1") {
527 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
528 _ => url.path().to_owned(),
529 };
530 let mut services = Services::new(env)?;
Merge branch 'worktree-agent-aaf03bdceac799c89'531 // MCP is a host of its own hosted, and may be a path on this one
532 // self-hosted (addresses.rs).
533 let on_mcp = services.addresses.mcp_path(&url).is_some();
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms534
535 // Stripe reporting to billing. Signed with the secret of the endpoint
536 // billing registered; the body goes through exactly as received, since
537 // the signature covers its bytes.
538 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
539 return receive_stripe(&mut request, env).await;
540 }
541
542 // Outside systems reporting to a connection. They sign what they send
543 // with the connection's own secret, which is not a g1t token, so this
544 // comes before anything that would read one.
545 if method == "POST" && !on_mcp
546 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
547 return receive_hook(&mut request, &services, id).await;
548 }
549
550 // A sandbox building a deployment, reporting with its build's token,
g1t.page runs Docker deployments, behind a switch that is off until Cloudflare's container APIs are checked against g1t's account (`CONTAINERS` in the deployments service). Each container app is a Worker of g1t's own in the same Workers for Platforms namespace, so the dispatcher, custom domains, previews and take-downs reach it by name as any app: its `G1tApp` Durable Object holds a Container application (default scheduling policy, one instance, the project's **Container size**, `lite` unless chosen), starts it on the first request with the project's secrets and variables and `PORT` as its environment, waits up to 90 seconds for it to answer on its port, proxies to it, and stops it five minutes after the last request; it charges each minute awake ahead and the bytes it sends, and reports them with a token of its own to `POST /deployments/apps/<app>/meter`, which the API forwards. A build's new `registry` step gets a registry login g1t mints push-only for 15 minutes, with its app's own repository named; g1t runs only an image by digest in that repository, checked with the registry, and never hands a build a login that can pull. Container time is metered at Cloudflare's Containers price plus the margin through new price book meters (vCPU charged as busy while awake, and egress at the most a GB costs anywhere, so g1t never sells below cost), noted for the spend limit as it runs and charged with the month's Deployments line; a paused workspace's container apps stop and start no more. Settings, Deployments gets Build as (Auto, Static site, Workers, Docker), Dockerfile and Container size with each size's price an hour, and says what the last build found with what it was built with ("Static site · Astro", "Container · Dockerfile"), now that the finish report's `framework` is stored. Deploy sandboxes get Docker, `BUILD_AS` and `DOCKERFILE`, and reach `registry.cloudflare.com`; `start_deployment_build` answers `build_as` and `dockerfile` for `g1t/deploy`, which takes them as inputs, as `g1t/detect` does. The Deployments guide has Docker deployments, their prices and how they work with the evidence for it, the limitations page says what is left, and Deploy g1t to Cloudflare lists each live check to make before switching it on.551 // which is not a g1t token, or a container app's Worker reporting how
552 // long its container ran, with its own token. The body goes through as
553 // it is: it can carry a Worker's bundled code.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms554 if method == "POST" && !on_mcp
g1t.page runs Docker deployments, behind a switch that is off until Cloudflare's container APIs are checked against g1t's account (`CONTAINERS` in the deployments service). Each container app is a Worker of g1t's own in the same Workers for Platforms namespace, so the dispatcher, custom domains, previews and take-downs reach it by name as any app: its `G1tApp` Durable Object holds a Container application (default scheduling policy, one instance, the project's **Container size**, `lite` unless chosen), starts it on the first request with the project's secrets and variables and `PORT` as its environment, waits up to 90 seconds for it to answer on its port, proxies to it, and stops it five minutes after the last request; it charges each minute awake ahead and the bytes it sends, and reports them with a token of its own to `POST /deployments/apps/<app>/meter`, which the API forwards. A build's new `registry` step gets a registry login g1t mints push-only for 15 minutes, with its app's own repository named; g1t runs only an image by digest in that repository, checked with the registry, and never hands a build a login that can pull. Container time is metered at Cloudflare's Containers price plus the margin through new price book meters (vCPU charged as busy while awake, and egress at the most a GB costs anywhere, so g1t never sells below cost), noted for the spend limit as it runs and charged with the month's Deployments line; a paused workspace's container apps stop and start no more. Settings, Deployments gets Build as (Auto, Static site, Workers, Docker), Dockerfile and Container size with each size's price an hour, and says what the last build found with what it was built with ("Static site · Astro", "Container · Dockerfile"), now that the finish report's `framework` is stored. Deploy sandboxes get Docker, `BUILD_AS` and `DOCKERFILE`, and reach `registry.cloudflare.com`; `start_deployment_build` answers `build_as` and `dockerfile` for `g1t/deploy`, which takes them as inputs, as `g1t/detect` does. The Deployments guide has Docker deployments, their prices and how they work with the evidence for it, the limitations page says what is left, and Deploy g1t to Cloudflare lists each live check to make before switching it on.555 && let Some(rest) = path
556 .strip_prefix("/deployments/jobs/")
557 .map(|rest| format!("jobs/{rest}"))
558 .or_else(|| path.strip_prefix("/deployments/apps/").filter(|rest| rest.ends_with("/meter")).map(|rest| format!("apps/{rest}")))
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms559 {
g1t.page runs Docker deployments, behind a switch that is off until Cloudflare's container APIs are checked against g1t's account (`CONTAINERS` in the deployments service). Each container app is a Worker of g1t's own in the same Workers for Platforms namespace, so the dispatcher, custom domains, previews and take-downs reach it by name as any app: its `G1tApp` Durable Object holds a Container application (default scheduling policy, one instance, the project's **Container size**, `lite` unless chosen), starts it on the first request with the project's secrets and variables and `PORT` as its environment, waits up to 90 seconds for it to answer on its port, proxies to it, and stops it five minutes after the last request; it charges each minute awake ahead and the bytes it sends, and reports them with a token of its own to `POST /deployments/apps/<app>/meter`, which the API forwards. A build's new `registry` step gets a registry login g1t mints push-only for 15 minutes, with its app's own repository named; g1t runs only an image by digest in that repository, checked with the registry, and never hands a build a login that can pull. Container time is metered at Cloudflare's Containers price plus the margin through new price book meters (vCPU charged as busy while awake, and egress at the most a GB costs anywhere, so g1t never sells below cost), noted for the spend limit as it runs and charged with the month's Deployments line; a paused workspace's container apps stop and start no more. Settings, Deployments gets Build as (Auto, Static site, Workers, Docker), Dockerfile and Container size with each size's price an hour, and says what the last build found with what it was built with ("Static site · Astro", "Container · Dockerfile"), now that the finish report's `framework` is stored. Deploy sandboxes get Docker, `BUILD_AS` and `DOCKERFILE`, and reach `registry.cloudflare.com`; `start_deployment_build` answers `build_as` and `dockerfile` for `g1t/deploy`, which takes them as inputs, as `g1t/detect` does. The Deployments guide has Docker deployments, their prices and how they work with the evidence for it, the limitations page says what is left, and Deploy g1t to Cloudflare lists each live check to make before switching it on.560 let target = format!("https://deployments/{rest}");
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms561 let body = request.bytes().await?;
562 let headers = worker::Headers::new();
563 headers.set("content-type", "application/json")?;
564 let mut init = worker::RequestInit::new();
565 init.with_method(Method::Post)
566 .with_headers(headers)
567 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
568 let mut answer = env
569 .service("DEPLOYMENTS")?
570 .fetch_request(Request::new_with_init(&target, &init)?)
571 .await?;
572 // A fresh response: a fetched one's headers cannot be changed, and
573 // every response gets the API's own on the way out.
574 let status = answer.status_code();
575 let bytes = answer.bytes().await?;
576 let bytes = match serde_json::from_slice::<Value>(&bytes) {
577 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
578 Err(_) => bytes,
579 };
580 return Ok(Response::from_bytes(bytes)?
581 .with_status(status)
582 .with_headers({
583 let headers = worker::Headers::new();
584 headers.set("content-type", "application/json")?;
585 headers
586 }));
587 }
588
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2589 // The services GitHub's toolkit calls from inside a job, with its
590 // runtime token, and the links they hand out (toolkit.rs).
591 if !on_mcp && method == "POST"
592 && let Some(rest) = path.strip_prefix("/twirp/")
593 {
594 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
595 let (service, rpc) = (service.to_owned(), rpc.to_owned());
596 return toolkit::twirp(request, env, &services, &service, &rpc).await;
597 }
598 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
599 let rest = rest.to_owned();
600 return toolkit::cache_v1(request, env, &services, method, &rest).await;
601 }
602 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
603 let token = token.to_owned();
604 return toolkit::blob(request, env, &services, method, &token).await;
605 }
606 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
607 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
608 return oidc::handle(&request, env, &services, &path).await;
609 }
610
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms611 // A sandbox's artifacts and cache, with its job's token, which is not a
612 // g1t token either.
613 if !on_mcp
614 && let Some(rest) = path.strip_prefix("/actions/jobs/")
Fast pages, required checks on the branch, self-hosted runners, honest incidents615 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms616 {
617 let rest = rest.to_owned();
618 return blobs::for_job(request, env, &services, method, &rest).await;
619 }
620
Fast pages, required checks on the branch, self-hosted runners, honest incidents621 // A self-hosted runner, with a registration token or its own
622 // credential, neither of which is a g1t access token.
623 if method == "POST"
624 && !on_mcp
625 && path.starts_with("/runners/")
626 && let Some(response) = runners::handle(&mut request, &services, &path).await?
627 {
628 return Ok(response);
629 }
630
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails631 // Per token, or per address without one (limits.rs).
632 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
633 return Ok(limited);
634 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms635 let viewer = match authenticate(&request, &services).await? {
636 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails637 Err(refused) => return Ok(limits::wrong_token(&request, env, on_mcp).await?.unwrap_or(refused)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms638 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)639 // A person who has not confirmed their email address: who they are,
640 // their addresses, and confirming one, nothing else (REST or MCP).
641 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
642 return fail(
643 FailureCode::Forbidden,
644 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
645 );
646 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms647 services.audit = audit::AuditContext::of(&request, on_mcp);
648 // An agent's token: what it may do comes with it, on the composite
649 // identity identity resolved it to.
650 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
651 services.scope = Some(acting.scope.clone());
652 } else if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
653 let header = request.headers().get("authorization")?.unwrap_or_default();
654 let token = header.split_once(' ').map(|(_, token)| token.trim()).unwrap_or_default();
655 let scope: Option<AgentScope> = g1t_kit::call(
656 &services.identity,
657 "agent_scope",
658 &TokenArgs {
659 token: token.to_owned(),
660 },
661 )
662 .await?;
663 // A scope is what lets an agent's token do anything at all.
664 let Some(scope) = scope else {
665 return fail(FailureCode::Unauthenticated, "Invalid access token.");
666 };
667 services.scope = Some(scope);
668 }
669 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
670 return Ok(response);
671 }
672 if on_mcp {
673 return mcp::handle(request, &services, &viewer).await;
674 }
675
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)676 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
677 if method == "GET" {
678 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
679 if let Some(wanted) = logs::wanted(&path, text) {
680 return logs::download(&services, &viewer, wanted).await;
681 }
682 }
683
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms684 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'685 ("GET", "") => return reply(&index(&services.addresses)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms686 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2687 // One of a run's artifacts downloaded by name (the run's artifacts
688 // are listed by the REST route in rest.rs).
689 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts/") => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms690 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2691 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'692 // A workflow job's token reaches its own repository only.
693 if viewer
694 .as_ref()
695 .and_then(|user| user.token.as_deref())
696 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
697 {
698 return fail(FailureCode::NotFound, "No such run.");
699 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2700 return blobs::download(env, &services, &viewer, owner, repo, run, name).await;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms701 }
702 }
703 ("POST", "/device/code") => return device_code(&mut request, &services).await,
704 ("POST", "/device/token") => return device_token(&mut request, &services).await,
705 // Where a pull request lives, for a tool that knows only its fork.
706 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
707 let located: Outcome<Value> = g1t_kit::call(
708 &services.work,
709 "locate_pull",
710 &json!({ "id": &path[7..], "viewer": viewer }),
711 )
712 .await?;
713 return match located {
714 Outcome::Ok(value) => reply(&value),
715 Outcome::Fail(refused) => failure(&refused),
716 };
717 }
718 ("POST", path) if path.starts_with("/mergechecks/") => {
719 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
720 return report_mergecheck(&mut request, &services, &pull_id).await;
721 }
Merge branch 'worktree-agent-ac5b181a013e54348'722 // A sandbox making a repository's nightly backup. The job's own
723 // token, in its header, is the credential.
724 (method, path) if path.starts_with("/backups/") => {
725 return backup_job(&mut request, &services, method, path).await;
726 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms727 ("POST", path) if path.starts_with("/queue/") => {
728 let entry_id = path.trim_start_matches("/queue/").to_owned();
729 return report_queue(&mut request, &services, &entry_id).await;
730 }
731 // A sandbox running a GitHub Actions job: fetching the job, and
732 // reporting how it goes. The job's own token is the credential.
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'733 // A sandbox downloading an action's archive that g1t keeps
734 // (actions' registry.rs). The job's own token is the credential.
735 ("POST", path) if path.starts_with("/actions/jobs/") && path.ends_with("/archive") => {
736 let job = path.trim_start_matches("/actions/jobs/").trim_end_matches("/archive").to_owned();
737 let body = json_body(&mut request).await;
738 let kept: Outcome<Value> = g1t_kit::call(
739 &services.actions,
740 "job_action_archive",
741 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
742 )
743 .await?;
744 let kept = match kept {
745 Outcome::Ok(kept) => kept,
746 Outcome::Fail(refused) => return failure(&refused),
747 };
748 let bucket = env.bucket("ACTIONS_CACHE")?;
749 let object = kept["object"].as_str().unwrap_or_default();
750 let Some(stored) = bucket.get(object).execute().await? else {
751 return fail(FailureCode::NotFound, "The archive is gone.");
752 };
753 let Some(body) = stored.body() else {
754 return fail(FailureCode::NotFound, "The archive is gone.");
755 };
756 let mut response = Response::from_body(body.response_body()?)?;
757 response.headers_mut().set("content-type", "application/gzip")?;
758 return Ok(response);
759 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms760 ("POST", path) if path.starts_with("/actions/jobs/") => {
761 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts762 // `/action`: where to fetch another repository's action from (on
763 // g1t, with a read token for a private one, or GitHub).
764 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
765 (Some(job), _) => (job.to_owned(), "job_spec"),
766 (_, Some(job)) => (job.to_owned(), "job_action"),
767 _ => (rest.to_owned(), "job_report"),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms768 };
769 let body = json_body(&mut request).await;
770 let answered: Outcome<Value> = g1t_kit::call(
771 &services.actions,
772 method,
773 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
774 )
775 .await?;
776 return match answered {
777 // A job's spec is the workflow and its contexts as GitHub
778 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2779 Outcome::Ok(value) => {
780 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
781 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
782 Response::from_json(&spec)
783 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms784 Outcome::Fail(refused) => failure(&refused),
785 };
786 }
787 // A sandbox reporting its agent run's steps, cost and end. As with
788 // checks, the run's own token, in the body, is the credential.
789 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/report") => {
790 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/report");
791 let mut body = json_body(&mut request).await;
792 if !body.is_object() {
793 body = json!({});
794 }
795 body["runId"] = json!(run_id);
796 let reported: Outcome<Value> = g1t_kit::call(&services.work, "report_run", &body).await?;
797 return match reported {
798 Outcome::Ok(status) => reply(&json!({ "status": status })),
799 Outcome::Fail(refused) => failure(&refused),
800 };
801 }
802 // What a run's agent learned, as memory candidates; the same token.
803 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/learned") => {
804 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/learned");
805 let body = json_body(&mut request).await;
806 let learned = json!({
807 "runId": run_id,
808 "token": body["token"].as_str().unwrap_or_default(),
809 "items": body["items"].as_array().cloned().unwrap_or_default(),
810 });
811 let captured: Outcome<Value> = g1t_kit::call(&services.work, "report_learned", &learned).await?;
812 return match captured {
813 Outcome::Ok(captured) => reply(&captured),
814 Outcome::Fail(refused) => failure(&refused),
815 };
816 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step817 // How sure a run's agent is of its change; the same token.
818 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/confidence") => {
819 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/confidence");
820 let body = json_body(&mut request).await;
821 let said = json!({
822 "runId": run_id,
823 "token": body["token"].as_str().unwrap_or_default(),
824 "confidence": body["confidence"].as_str().unwrap_or_default(),
825 "uncertainAbout": body["uncertain_about"]
826 .as_array()
827 .map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>())
828 .unwrap_or_default(),
829 });
830 let recorded: Outcome<Value> = g1t_kit::call(&services.work, "report_confidence", &said).await?;
831 return match recorded {
832 Outcome::Ok(recorded) => reply(&json!({ "recorded": recorded })),
833 Outcome::Fail(refused) => failure(&refused),
834 };
835 }
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms836 ("POST", path) if path.starts_with("/checks/") => {
837 let run_id = path.trim_start_matches("/checks/").to_owned();
838 return report_checks(&mut request, &services, &run_id).await;
839 }
840 ("POST", path) if path.starts_with("/runs/") && path.ends_with("/usage") => {
841 let run_id = path
842 .trim_start_matches("/runs/")
843 .trim_end_matches("/usage")
844 .to_owned();
845 return report_usage(&mut request, &services, &run_id).await;
846 }
847 ("POST", path) if path.starts_with("/plans/") => {
848 let plan_id = path.trim_start_matches("/plans/").to_owned();
849 return report_plan(&mut request, &services, &plan_id).await;
850 }
851 ("POST", path) if path.starts_with("/reviews/") => {
852 let run_id = path.trim_start_matches("/reviews/").to_owned();
853 return report_review(&mut request, &services, &run_id).await;
854 }
855 _ => {}
856 }
857
858 let query: Vec<(String, String)> = url
859 .query_pairs()
860 .map(|(name, value)| (name.into_owned(), value.into_owned()))
861 .collect();
862 let body = if method == "GET" {
863 Value::Null
864 } else {
865 snake_case_keys(json_body(&mut request).await)
866 };
867 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
868 return fail(FailureCode::NotFound, "No such endpoint.");
869 };
870 match audit::run(route.op, &services, &viewer, &input).await? {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2871 // A download is a redirect to its signed link, as GitHub's is.
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.872 Outcome::Ok(value) if route.op == operations::Op::Artifacts(run_artifacts::ArtifactsOp::DownloadArtifact) => {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2873 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
874 Some(url) => Response::redirect_with_status(url, 302),
875 None => reply(&value),
876 }
877 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts878 // A deleted comment has nothing to say, as GitHub's says nothing.
879 Outcome::Ok(_) if route.no_content() => Ok(Response::empty()?.with_status(204)),
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms880 Outcome::Ok(value) => reply(&value),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step881 // A token without the scope a call needs is told which one.
882 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
883 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
884 "error": {
885 "code": refused.code,
886 "message": refused.message,
887 "needed_scope": scope.as_str(),
888 }
889 }))?
890 .with_status(403)),
891 _ => failure(&refused),
892 },
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms893 }
894}
895
896/// Request bodies take the same keys as the MCP tools, `snake_case`, as
897/// responses use; the `camelCase` spelling is accepted too.
898fn snake_case_keys(body: Value) -> Value {
899 let Value::Object(fields) = body else {
900 return body;
901 };
902 let mut out = serde_json::Map::new();
903 for (key, value) in fields {
904 let mut snake = String::with_capacity(key.len() + 4);
905 for c in key.chars() {
906 if c.is_ascii_uppercase() {
907 snake.push('_');
908 snake.push(c.to_ascii_lowercase());
909 } else {
910 snake.push(c);
911 }
912 }
913 // A key given in both spellings keeps the snake_case one.
914 if snake != key && out.contains_key(&snake) {
915 continue;
916 }
917 out.insert(snake, value);
918 }
919 Value::Object(out)
920}
921
922#[cfg(test)]
923mod tests {
924 use super::snake_case_keys;
925 use serde_json::json;
926
927 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)928 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
929 assert!(super::pending_may("GET", "/user", false));
930 assert!(super::pending_may("GET", "/user/emails/", false));
931 assert!(super::pending_may("POST", "/user/emails/confirm", false));
932 assert!(!super::pending_may("POST", "/user/emails", false));
933 assert!(!super::pending_may("POST", "/workspaces", false));
934 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
935 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
936 assert!(!super::pending_may("POST", "/", true));
937 }
938
939 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2940 fn a_job_spec_gets_the_toolkits_variables() {
941 // As the actions service sends it, converted as the API does.
942 let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true } });
943 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
944 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
945 assert!(spec.get("runtime").is_none(), "the runner never sees it");
946 let vars = &spec["variables"];
947 assert_eq!(vars["GITHUB_SHA"], "abc");
948 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
949 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
950 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
951 // No OIDC key here: no OIDC variables, whatever the job may do.
952 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
953 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
954 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
955 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
956 }
957
958 #[test]
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms959 fn camel_case_keys_are_accepted() {
960 assert_eq!(
961 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
962 json!({ "count_agent_approvals": false, "title": "x" })
963 );
964 }
965
966 #[test]
967 fn snake_case_wins_when_both_are_given() {
968 assert_eq!(
969 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
970 json!({ "keep_issue_open": true })
971 );
972 }
973}
974
975// The API is called from browsers too: the reference's explorer, and apps
976// built on g1t. It carries no cookies, so any origin may call it.
977#[event(fetch)]
978async fn fetch(request: Request, env: Env, _ctx: Context) -> Result<Response> {
979 let mut response = respond(request, &env).await?;
980 let headers = response.headers_mut();
981 headers.set("access-control-allow-origin", "*")?;
982 headers.set(
983 "access-control-allow-headers",
984 "authorization, content-type",
985 )?;
986 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails987 headers.set("access-control-expose-headers", "www-authenticate, retry-after")?;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms988 Ok(response)
989}

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