Skip to content
960 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.

Integrations: your own model provider, alerts that open issues, tickets agents read1//! 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;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R28mod artifacts;
Merge branch 'worktree-agent-aaf03bdceac799c89'9mod addresses;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily10mod alerts;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API11mod audit;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit12mod billing;
A repository has its own sidebar, as settings do13mod blobs;
Merge checks: statuses and check runs on every commit14mod checks;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9715mod deployments;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca16mod deploy_keys;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails17mod limits;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)18mod logs;
Merge branch 'mirroring' into artifacts-mode19mod mirrors;
Integrations: your own model provider, alerts that open issues, tickets agents read20mod mcp;
API: notifications over REST and MCP, with notifications scopes21mod notifications;
Integrations: your own model provider, alerts that open issues, tickets agents read22mod oauth;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R223mod oidc;
Merge packages: roles, Actions access, source label, soft delete, API24mod packages;
The API answers for artifacts: the artifact MCP tool and /workspaces/{workspace}/artifacts list, search, read and edit a workspace's docs as the person sees them, make, move, trash, restore and purge them, show their versions and who can open them, and share them, each action offered only to tokens whose artifacts scope allows it, through a DOCS binding to the docs service, with the OpenAPI document and its examples.25mod folios;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar26mod people;
Integrations: your own model provider, alerts that open issues, tickets agents read27mod openapi;
API: pinned projects over REST and MCP28mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9729mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'30mod protection;
Integrations: your own model provider, alerts that open issues, tickets agents read31mod operations;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains32mod renamed;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API33#[cfg(test)]
34mod responses;
Integrations: your own model provider, alerts that open issues, tickets agents read35mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge36mod rules;
Fast pages, required checks on the branch, self-hosted runners, honest incidents37mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar38mod security;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step39mod tools;
API and MCP for a workspace's personal access token rules, members' tokens and approvals40mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R241mod toolkit;
Integrations: your own model provider, alerts that open issues, tickets agents read42
Merge branch 'model-routing'43use g1t_contracts::billing::{FinishRunArgs, RunTokens};
Integrations: your own model provider, alerts that open issues, tickets agents read44use g1t_contracts::identity::{
45 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
46};
47use g1t_contracts::work::{
Agents and memory, checks and conflicts, profiles, slug renames, custom domains48 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs, ReportPlanArgs,
49 ReportQueueArgs, ReportReviewArgs,
Integrations: your own model provider, alerts that open issues, tickets agents read50};
51use g1t_contracts::identity::AgentScope;
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)52use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API53use g1t_kit::wire;
Integrations: your own model provider, alerts that open issues, tickets agents read54use serde_json::{Value, json};
55use worker::{Context, Env, Method, Request, Response, Result, event};
56
57use operations::Services;
58
59fn method_name(method: Method) -> &'static str {
60 match method {
61 Method::Get => "GET",
62 Method::Post => "POST",
63 Method::Patch => "PATCH",
64 Method::Put => "PUT",
65 Method::Delete => "DELETE",
66 Method::Options => "OPTIONS",
67 Method::Head => "HEAD",
68 _ => "OTHER",
69 }
70}
71
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API72/// A JSON response. Every body the API sends has its keys in `snake_case`;
73/// the contracts it passes through are `camelCase`, so they are converted
74/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
75/// the MCP protocol's own envelope keep the spelling their standards use.
76pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
77 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
78}
79
80/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
81/// workflow file, GitHub's contexts and event, and where to check out, all
82/// passed through as they are.
83const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'84 "spec", "workflow", "github", "event", "contexts", "checkout", "permissions",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API85];
86
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R287/// Puts the toolkit's variables in a job's spec: its runtime token, where
88/// the toolkit's services are, and where to ask for an OIDC token when the
89/// job may have one and this installation issues them. The `runtime` the
90/// actions service sent goes no further.
91fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
92 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
93 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
94 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
95 let vars = toolkit::runtime_variables(api, token, id_token);
96 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
97 variables.extend(vars);
98 }
99}
100
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)101/// What a person whose account has not confirmed its email address may
102/// call: who they are, their addresses, and confirming one with the code
103/// from the email. Nothing over MCP.
104fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
105 !on_mcp
106 && matches!(
107 (method, path.trim_end_matches('/')),
108 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
109 )
110}
111
Integrations: your own model provider, alerts that open issues, tickets agents read112/// An error in the shape every endpoint uses.
113fn failure(failure: &Failure) -> Result<Response> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API114 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
Integrations: your own model provider, alerts that open issues, tickets agents read115}
116
117fn fail(code: FailureCode, message: &str) -> Result<Response> {
118 failure(&Failure {
119 code,
120 message: message.to_owned(),
121 })
122}
123
124/// A request body as JSON. An empty or malformed body is no input.
125async fn json_body(request: &mut Request) -> Value {
126 request.json().await.unwrap_or(Value::Null)
127}
128
129/// Who a request's `Authorization: Bearer g1t_…` names. A missing token is
130/// an anonymous viewer; a wrong one is refused, so that a typo does not
131/// silently look signed out.
132async fn authenticate(
133 request: &Request,
134 services: &Services,
135) -> Result<std::result::Result<Viewer, Response>> {
136 let header = request.headers().get("authorization")?.unwrap_or_default();
137 let token = match header.split_once(' ') {
138 Some((scheme, token)) if scheme.eq_ignore_ascii_case("bearer") && !token.is_empty() => {
139 token.trim()
140 }
141 _ => return Ok(Ok(None)),
142 };
143 let viewer: Viewer = g1t_kit::call(
144 &services.identity,
145 "user_for_access_token",
146 &TokenArgs {
147 token: token.to_owned(),
148 },
149 )
150 .await?;
151 if viewer.is_some() {
152 return Ok(Ok(viewer));
153 }
154 let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?;
155 // Tells an MCP client where to sign in again.
156 response.headers_mut().set(
157 "www-authenticate",
Merge branch 'worktree-agent-aaf03bdceac799c89'158 &format!("{}, error=\"invalid_token\"", services.addresses.mcp_challenge()),
Integrations: your own model provider, alerts that open issues, tickets agents read159 )?;
160 Ok(Err(response))
161}
162
163/// Where everything is, for someone or something exploring the API.
Merge branch 'worktree-agent-aaf03bdceac799c89'164fn index(addresses: &addresses::Addresses) -> Value {
165 let api = &addresses.api;
166 let repo = format!("{api}/repos/{{owner}}/{{name}}");
Integrations: your own model provider, alerts that open issues, tickets agents read167 json!({
Merge branch 'worktree-agent-ab2e39e11a6493412'168 "documentation_url": "https://docs.g1t.sh/reference/api/",
Merge branch 'worktree-agent-aaf03bdceac799c89'169 "openapi_url": format!("{api}/openapi.json"),
170 "mcp_url": addresses.mcp,
171 "current_user_url": format!("{api}/user"),
172 "workspaces_url": format!("{api}/workspaces"),
173 "repositories_url": format!("{api}/repos{{?q}}"),
174 "search_url": format!("{api}/search{{?q,type,page,per_page}}"),
Integrations: your own model provider, alerts that open issues, tickets agents read175 "repository_url": repo,
176 "repository_events_url": format!("{repo}/events{{?before}}"),
177 "labels_url": format!("{repo}/labels"),
178 "issues_url": format!("{repo}/issues{{?state,label}}"),
179 "issue_url": format!("{repo}/issues/{{number}}"),
180 "issue_comments_url": format!("{repo}/issues/{{number}}/comments"),
181 "pulls_url": format!("{repo}/pulls{{?state}}"),
182 "pull_url": format!("{repo}/pulls/{{number}}"),
183 "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"),
184 "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"),
185 "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"),
Merge branch 'worktree-agent-aaf03bdceac799c89'186 "device_code_url": format!("{api}/device/code"),
187 "device_token_url": format!("{api}/device/token"),
188 "oauth_metadata_url": format!("{api}/.well-known/oauth-authorization-server"),
189 "git_url": format!("{}/{{owner}}/{{name}}.git", addresses.site),
190 "integrations_url": format!("{api}/workspaces/{{workspace}}/integrations"),
Integrations: your own model provider, alerts that open issues, tickets agents read191 "context_url": format!("{repo}/context{{?reference}}"),
192 "import_issue_url": format!("{repo}/issues/import"),
Merge branch 'worktree-agent-aaf03bdceac799c89'193 "hooks_url": format!("{api}/hooks/{{integration}}"),
Integrations: your own model provider, alerts that open issues, tickets agents read194 })
195}
196
197// Signing in from a tool. Accounts are created, and passwords typed, only
198// in a browser; a tool gets its token by having a person approve a code.
199
200/// Passes a request from an outside system to its connection, as it came:
201/// its signature covers the exact bytes of the body.
202async fn receive_hook(request: &mut Request, services: &Services, id: &str) -> Result<Response> {
203 let headers: std::collections::HashMap<String, String> = request
204 .headers()
205 .entries()
206 .map(|(name, value)| (name.to_lowercase(), value))
207 .collect();
208 let body = request.text().await.unwrap_or_default();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look209 // A push to GitHub with many commits makes a large payload.
210 let limit = if id == "github" { 10_000_000 } else { 1_000_000 };
211 if body.len() > limit {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API212 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
Integrations: your own model provider, alerts that open issues, tickets agents read213 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look214 // g1t's GitHub App has one webhook for every installation; it is
215 // checked against the app's own secret.
216 let (method, args) = if id == "github" {
217 ("github_receive", json!({ "headers": headers, "body": body }))
218 } else {
219 ("receive", json!({ "id": id, "headers": headers, "body": body }))
220 };
221 let received: g1t_contracts::integrations::Received =
222 g1t_kit::call(&services.integrations, method, &args).await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API223 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
Integrations: your own model provider, alerts that open issues, tickets agents read224}
225
Stripe webhooks, enterprise invoices, and sudo for both226async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
227 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
228 let payload = request.text().await.unwrap_or_default();
229 if payload.len() > 1_000_000 || signature.is_empty() {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API230 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
Stripe webhooks, enterprise invoices, and sudo for both231 }
232 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
233 &env.service("BILLING")?,
234 "stripe_webhook",
235 &g1t_contracts::billing::StripeWebhookArgs { payload, signature },
236 )
237 .await?;
238 // A refusal is a 400, so Stripe shows it as failed; anything handled,
239 // or already handled, is a 200, so Stripe stops sending it.
240 Ok(match handled {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API241 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
Stripe webhooks, enterprise invoices, and sudo for both242 g1t_contracts::Outcome::Fail(failure) => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API243 reply(&json!({ "message": failure.message }))?.with_status(400)
Stripe webhooks, enterprise invoices, and sudo for both244 }
245 })
246}
247
Integrations: your own model provider, alerts that open issues, tickets agents read248async fn device_code(request: &mut Request, services: &Services) -> Result<Response> {
249 let body = json_body(request).await;
250 let started: DeviceStart = g1t_kit::call(
251 &services.identity,
252 "device_start",
253 &DeviceStartArgs {
254 client_name: body["client_name"].as_str().unwrap_or_default().to_owned(),
255 },
256 )
257 .await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API258 reply(&json!({
Integrations: your own model provider, alerts that open issues, tickets agents read259 "device_code": started.device_code,
260 "user_code": started.user_code,
Merge branch 'worktree-agent-aaf03bdceac799c89'261 "verification_uri": format!("{}/device", services.addresses.site),
262 "verification_uri_complete": format!("{}/device?code={}", services.addresses.site, started.user_code),
Integrations: your own model provider, alerts that open issues, tickets agents read263 "expires_in": started.expires_in,
264 "interval": started.interval,
265 }))
266}
267
268async fn device_token(request: &mut Request, services: &Services) -> Result<Response> {
269 let body = json_body(request).await;
270 let claim: DeviceClaim = g1t_kit::call(
271 &services.identity,
272 "device_claim",
273 &DeviceClaimArgs {
274 device_code: body["device_code"].as_str().unwrap_or_default().to_owned(),
275 },
276 )
277 .await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API278 reply(&match claim {
Integrations: your own model provider, alerts that open issues, tickets agents read279 DeviceClaim::Approved { token, user } => json!({
280 "status": "approved",
281 "token": token,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar282 "display_username": user.display_username.clone().filter(|display| display.eq_ignore_ascii_case(&user.username)).unwrap_or_else(|| user.username.clone()),
Integrations: your own model provider, alerts that open issues, tickets agents read283 "username": user.username,
284 "verified": user.verified,
285 }),
286 DeviceClaim::Pending => json!({ "status": "pending" }),
287 DeviceClaim::Denied => json!({ "status": "denied" }),
288 DeviceClaim::Expired => json!({ "status": "expired" }),
289 })
290}
291
Fast pages, required checks on the branch, self-hosted runners, honest incidents292/// A sandbox reporting on a run of an issue's commands, from before a pull
293/// request's checks were the workflows run on it.
Integrations: your own model provider, alerts that open issues, tickets agents read294/// The run's own token, in the body, is the credential: it was given to
295/// that sandbox and to nothing else.
296async fn report_checks(
297 request: &mut Request,
298 services: &Services,
299 run_id: &str,
300) -> Result<Response> {
301 let body = json_body(request).await;
302 let reported: Outcome<CheckRun> = g1t_kit::call(
303 &services.work,
304 "report_checks",
305 &ReportChecksArgs {
306 run_id: run_id.to_owned(),
307 token: body["token"].as_str().unwrap_or_default().to_owned(),
308 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
309 error: body["error"].as_str().map(str::to_owned),
310 skip: false,
311 },
312 )
313 .await?;
314 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API315 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
Integrations: your own model provider, alerts that open issues, tickets agents read316 Outcome::Fail(refused) => failure(&refused),
317 }
318}
319
320/// A sandbox reporting one tested state of a merge queue. As with checks,
321/// the entry's own token is the credential.
322async fn report_queue(
323 request: &mut Request,
324 services: &Services,
325 entry_id: &str,
326) -> Result<Response> {
327 let body = json_body(request).await;
328 let reported: Outcome<QueueState> = g1t_kit::call(
329 &services.work,
330 "report_queue",
331 &ReportQueueArgs {
332 entry_id: entry_id.to_owned(),
333 token: body["token"].as_str().unwrap_or_default().to_owned(),
334 combined_commit: body["combinedCommit"].as_str().map(str::to_owned),
335 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
336 error: body["error"].as_str().map(str::to_owned),
337 conflict_with: body["conflictWith"].as_u64().map(|n| n as u32),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains338 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
Integrations: your own model provider, alerts that open issues, tickets agents read339 },
340 )
341 .await?;
342 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API343 Outcome::Ok(state) => reply(&json!({ "state": state })),
Integrations: your own model provider, alerts that open issues, tickets agents read344 Outcome::Fail(refused) => failure(&refused),
345 }
346}
347
Agents and memory, checks and conflicts, profiles, slug renames, custom domains348/// A sandbox reporting whether a pull request merges cleanly. As with
349/// checks, the probe's own token is the credential.
350async fn report_mergecheck(
351 request: &mut Request,
352 services: &Services,
353 pull_id: &str,
354) -> Result<Response> {
355 let body = json_body(request).await;
356 let reported: Outcome<Mergeable> = g1t_kit::call(
357 &services.work,
358 "report_mergecheck",
359 &ReportMergecheckArgs {
360 pull_id: pull_id.to_owned(),
361 token: body["token"].as_str().unwrap_or_default().to_owned(),
362 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
363 error: body["error"].as_str().map(str::to_owned),
364 },
365 )
366 .await?;
367 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API368 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains369 Outcome::Fail(refused) => failure(&refused),
370 }
371}
372
Merge branch 'worktree-agent-ac5b181a013e54348'373/// A backup's sandbox, passed on to the repos service, which holds the
374/// job (services/repos/src/backups.rs; the flow is in
375/// `g1t_contracts::backups`):
376///
377/// - `POST /backups/{job}/spec`: what to cut, and a read-only git credential
378/// - `PUT /backups/{job}/parts/{n}`: one part of the bundle, as bytes
379/// - `POST /backups/{job}/complete` with `{ refs, size, sha256, parts, fetched_bytes }`
380/// - `POST /backups/{job}/fail` with `{ error, fetched_bytes }`
381///
382/// Bodies are passed through as they are: snake_case already, and a
383/// bundle's refs are keyed by ref names, which must not be converted.
384async fn backup_job(request: &mut Request, services: &Services, method: &str, path: &str) -> Result<Response> {
385 use g1t_contracts::backups::TOKEN_HEADER;
386 let token = request.headers().get(TOKEN_HEADER)?.unwrap_or_default();
387 let rest = path.trim_start_matches("/backups/");
388 let (job, action) = rest.split_once('/').unwrap_or((rest, ""));
389 if job.is_empty() || token.is_empty() {
390 return fail(FailureCode::Unauthenticated, "A backup job's token is required.");
391 }
392 if method == "PUT" && action.starts_with("parts/") {
393 let bytes = request.bytes().await?;
394 if bytes.len() as u64 > g1t_contracts::backups::PART_BYTES {
395 return fail(FailureCode::Invalid, "A part holds 32 MiB at most.");
396 }
397 let headers = worker::Headers::new();
398 headers.set(TOKEN_HEADER, &token)?;
399 let mut init = worker::RequestInit::new();
400 init.with_method(Method::Put)
401 .with_headers(headers)
402 .with_body(Some(worker::js_sys::Uint8Array::from(bytes.as_slice()).into()));
403 let forwarded = Request::new_with_init(&format!("https://repos/backups/{job}/{action}"), &init)?;
404 let mut answered = services.repos.fetch_request(forwarded).await?;
405 return outcome_as_given(answered.json().await?);
406 }
407 let rpc = match (method, action) {
408 ("POST", "spec") => "backup_spec",
409 ("POST", "complete") => "backup_complete",
410 ("POST", "fail") => "backup_fail",
411 _ => return fail(FailureCode::NotFound, "No such endpoint."),
412 };
413 let mut body = json_body(request).await;
414 if !body.is_object() {
415 body = json!({});
416 }
417 body["job_id"] = json!(job);
418 body["token"] = json!(token);
419 let answered: Value = g1t_kit::call(&services.repos, rpc, &body).await?;
420 outcome_as_given(answered)
421}
422
423/// An `Outcome` from a service whose keys are already the API's: the value,
424/// or the failure in the shape every endpoint uses.
425fn outcome_as_given(answered: Value) -> Result<Response> {
426 match serde_json::from_value::<Outcome<Value>>(answered)? {
427 Outcome::Ok(value) => Response::from_json(&value),
428 Outcome::Fail(refused) => failure(&refused),
429 }
430}
431
Integrations: your own model provider, alerts that open issues, tickets agents read432/// A sandbox reporting the review its agent wrote. As with checks, the
433/// run's own token is the credential.
434async fn report_review(
435 request: &mut Request,
436 services: &Services,
437 run_id: &str,
438) -> Result<Response> {
439 let body = json_body(request).await;
440 let reported: Outcome<bool> = g1t_kit::call(
441 &services.work,
442 "report_review",
443 &ReportReviewArgs {
444 run_id: run_id.to_owned(),
445 token: body["token"].as_str().unwrap_or_default().to_owned(),
446 verdict: serde_json::from_value(body["verdict"].clone()).unwrap_or(None),
447 body: body["body"].as_str().unwrap_or_default().to_owned(),
448 comments: serde_json::from_value(body["comments"].clone()).unwrap_or_default(),
449 model: body["model"].as_str().map(str::to_owned),
450 error: body["error"].as_str().map(str::to_owned),
451 },
452 )
453 .await?;
454 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API455 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
Integrations: your own model provider, alerts that open issues, tickets agents read456 Outcome::Fail(refused) => failure(&refused),
457 }
458}
459
460/// A sandbox reporting the plan its agent wrote. As with checks, the
461/// plan's own token is the credential.
462async fn report_plan(
463 request: &mut Request,
464 services: &Services,
465 plan_id: &str,
466) -> Result<Response> {
467 let body = json_body(request).await;
468 let reported: Outcome<bool> = g1t_kit::call(
469 &services.work,
470 "report_plan",
471 &ReportPlanArgs {
472 plan_id: plan_id.to_owned(),
473 token: body["token"].as_str().unwrap_or_default().to_owned(),
474 summary: body["summary"].as_str().unwrap_or_default().to_owned(),
475 issues: serde_json::from_value(body["issues"].clone()).unwrap_or_default(),
476 error: body["error"].as_str().map(str::to_owned),
477 },
478 )
479 .await?;
480 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API481 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
Integrations: your own model provider, alerts that open issues, tickets agents read482 Outcome::Fail(refused) => failure(&refused),
483 }
484}
485
486/// A sandbox reporting what its agent's run cost, so that the workspace
487/// it worked for is charged. As with checks, the run's own token is the
488/// credential.
489async fn report_usage(
490 request: &mut Request,
491 services: &Services,
492 run_id: &str,
493) -> Result<Response> {
494 let body = json_body(request).await;
495 let charged: Outcome<bool> = g1t_kit::call(
496 &services.billing,
497 "finish_run",
498 &FinishRunArgs {
499 run_id: run_id.to_owned(),
500 token: body["token"].as_str().unwrap_or_default().to_owned(),
501 cost_usd: body["cost_usd"].as_f64().unwrap_or_default(),
502 turns: body["turns"].as_u64().unwrap_or_default() as u32,
Merge branch 'model-routing'503 // What the harness counted; the agent rate is charged on no
504 // fewer, on a workspace's own model key too.
505 tokens: body.get("tokens").filter(|t| t.is_object()).map(|t| RunTokens {
506 input: t["input"].as_u64().unwrap_or_default(),
507 output: t["output"].as_u64().unwrap_or_default(),
508 cache_read: t["cache_read"].as_u64().unwrap_or_default(),
509 cache_write: t["cache_write"].as_u64().unwrap_or_default(),
510 }),
Integrations: your own model provider, alerts that open issues, tickets agents read511 },
512 )
513 .await?;
514 match charged {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API515 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
Integrations: your own model provider, alerts that open issues, tickets agents read516 Outcome::Fail(refused) => failure(&refused),
517 }
518}
519
520async fn respond(mut request: Request, env: &Env) -> Result<Response> {
521 let method = method_name(request.method());
522 if method == "OPTIONS" {
523 return Ok(Response::empty()?.with_status(204));
524 }
525 let url = request.url()?;
526 // Paths carry no version. An earlier form began with `/v1`, which is
527 // still accepted so that nothing already written against it breaks.
528 let path = match url.path().strip_prefix("/v1") {
529 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
530 _ => url.path().to_owned(),
531 };
532 let mut services = Services::new(env)?;
Merge branch 'worktree-agent-aaf03bdceac799c89'533 // MCP is a host of its own hosted, and may be a path on this one
534 // self-hosted (addresses.rs).
535 let on_mcp = services.addresses.mcp_path(&url).is_some();
Integrations: your own model provider, alerts that open issues, tickets agents read536
Stripe webhooks, enterprise invoices, and sudo for both537 // Stripe reporting to billing. Signed with the secret of the endpoint
538 // billing registered; the body goes through exactly as received, since
539 // the signature covers its bytes.
540 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
541 return receive_stripe(&mut request, env).await;
542 }
543
Integrations: your own model provider, alerts that open issues, tickets agents read544 // Outside systems reporting to a connection. They sign what they send
545 // with the connection's own secret, which is not a g1t token, so this
546 // comes before anything that would read one.
Polish: phones, copy boxes, the plan page, the landing page, a real glide547 if method == "POST" && !on_mcp
548 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
Integrations: your own model provider, alerts that open issues, tickets agents read549 return receive_hook(&mut request, &services, id).await;
550 }
551
Deployments: a preview for every pull request, production on g1t.page552 // A sandbox building a deployment, reporting with its build's token,
553 // which is not a g1t token. The body goes through as it is: it can
554 // carry a Worker's bundled code.
555 if method == "POST" && !on_mcp
556 && let Some(rest) = path.strip_prefix("/deployments/jobs/")
557 {
558 let target = format!("https://deployments/jobs/{rest}");
559 let body = request.bytes().await?;
560 let headers = worker::Headers::new();
561 headers.set("content-type", "application/json")?;
562 let mut init = worker::RequestInit::new();
563 init.with_method(Method::Post)
564 .with_headers(headers)
565 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
Deployments work end to end: fixes from the first live run566 let mut answer = env
Deployments: a preview for every pull request, production on g1t.page567 .service("DEPLOYMENTS")?
568 .fetch_request(Request::new_with_init(&target, &init)?)
Deployments work end to end: fixes from the first live run569 .await?;
570 // A fresh response: a fetched one's headers cannot be changed, and
571 // every response gets the API's own on the way out.
572 let status = answer.status_code();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API573 let bytes = answer.bytes().await?;
574 let bytes = match serde_json::from_slice::<Value>(&bytes) {
575 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
576 Err(_) => bytes,
577 };
578 return Ok(Response::from_bytes(bytes)?
Deployments work end to end: fixes from the first live run579 .with_status(status)
580 .with_headers({
581 let headers = worker::Headers::new();
582 headers.set("content-type", "application/json")?;
583 headers
584 }));
Deployments: a preview for every pull request, production on g1t.page585 }
586
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2587 // The services GitHub's toolkit calls from inside a job, with its
588 // runtime token, and the links they hand out (toolkit.rs).
589 if !on_mcp && method == "POST"
590 && let Some(rest) = path.strip_prefix("/twirp/")
591 {
592 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
593 let (service, rpc) = (service.to_owned(), rpc.to_owned());
594 return toolkit::twirp(request, env, &services, &service, &rpc).await;
595 }
596 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
597 let rest = rest.to_owned();
598 return toolkit::cache_v1(request, env, &services, method, &rest).await;
599 }
600 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
601 let token = token.to_owned();
602 return toolkit::blob(request, env, &services, method, &token).await;
603 }
604 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
605 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
606 return oidc::handle(&request, env, &services, &path).await;
607 }
608
A repository has its own sidebar, as settings do609 // A sandbox's artifacts and cache, with its job's token, which is not a
610 // g1t token either.
611 if !on_mcp
612 && let Some(rest) = path.strip_prefix("/actions/jobs/")
Fast pages, required checks on the branch, self-hosted runners, honest incidents613 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
A repository has its own sidebar, as settings do614 {
615 let rest = rest.to_owned();
616 return blobs::for_job(request, env, &services, method, &rest).await;
617 }
618
Fast pages, required checks on the branch, self-hosted runners, honest incidents619 // A self-hosted runner, with a registration token or its own
620 // credential, neither of which is a g1t access token.
621 if method == "POST"
622 && !on_mcp
623 && path.starts_with("/runners/")
624 && let Some(response) = runners::handle(&mut request, &services, &path).await?
625 {
626 return Ok(response);
627 }
628
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails629 // Per token, or per address without one (limits.rs).
630 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
631 return Ok(limited);
632 }
Integrations: your own model provider, alerts that open issues, tickets agents read633 let viewer = match authenticate(&request, &services).await? {
634 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails635 Err(refused) => return Ok(limits::wrong_token(&request, env, on_mcp).await?.unwrap_or(refused)),
Integrations: your own model provider, alerts that open issues, tickets agents read636 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)637 // A person who has not confirmed their email address: who they are,
638 // their addresses, and confirming one, nothing else (REST or MCP).
639 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
640 return fail(
641 FailureCode::Forbidden,
642 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
643 );
644 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API645 services.audit = audit::AuditContext::of(&request, on_mcp);
646 // An agent's token: what it may do comes with it, on the composite
647 // identity identity resolved it to.
648 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
649 services.scope = Some(acting.scope.clone());
650 } else if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
Integrations: your own model provider, alerts that open issues, tickets agents read651 let header = request.headers().get("authorization")?.unwrap_or_default();
652 let token = header.split_once(' ').map(|(_, token)| token.trim()).unwrap_or_default();
653 let scope: Option<AgentScope> = g1t_kit::call(
654 &services.identity,
655 "agent_scope",
656 &TokenArgs {
657 token: token.to_owned(),
658 },
659 )
660 .await?;
661 // A scope is what lets an agent's token do anything at all.
662 let Some(scope) = scope else {
663 return fail(FailureCode::Unauthenticated, "Invalid access token.");
664 };
665 services.scope = Some(scope);
666 }
667 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
668 return Ok(response);
669 }
670 if on_mcp {
671 return mcp::handle(request, &services, &viewer).await;
672 }
673
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)674 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
675 if method == "GET" {
676 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
677 if let Some(wanted) = logs::wanted(&path, text) {
678 return logs::download(&services, &viewer, wanted).await;
679 }
680 }
681
Integrations: your own model provider, alerts that open issues, tickets agents read682 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'683 ("GET", "") => return reply(&index(&services.addresses)),
Integrations: your own model provider, alerts that open issues, tickets agents read684 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2685 // One of a run's artifacts downloaded by name (the run's artifacts
686 // are listed by the REST route in rest.rs).
687 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts/") => {
A repository has its own sidebar, as settings do688 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2689 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'690 // A workflow job's token reaches its own repository only.
691 if viewer
692 .as_ref()
693 .and_then(|user| user.token.as_deref())
694 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
695 {
696 return fail(FailureCode::NotFound, "No such run.");
697 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2698 return blobs::download(env, &services, &viewer, owner, repo, run, name).await;
A repository has its own sidebar, as settings do699 }
700 }
Integrations: your own model provider, alerts that open issues, tickets agents read701 ("POST", "/device/code") => return device_code(&mut request, &services).await,
702 ("POST", "/device/token") => return device_token(&mut request, &services).await,
703 // Where a pull request lives, for a tool that knows only its fork.
704 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
705 let located: Outcome<Value> = g1t_kit::call(
706 &services.work,
707 "locate_pull",
708 &json!({ "id": &path[7..], "viewer": viewer }),
709 )
710 .await?;
711 return match located {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API712 Outcome::Ok(value) => reply(&value),
Integrations: your own model provider, alerts that open issues, tickets agents read713 Outcome::Fail(refused) => failure(&refused),
714 };
715 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains716 ("POST", path) if path.starts_with("/mergechecks/") => {
717 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
718 return report_mergecheck(&mut request, &services, &pull_id).await;
719 }
Merge branch 'worktree-agent-ac5b181a013e54348'720 // A sandbox making a repository's nightly backup. The job's own
721 // token, in its header, is the credential.
722 (method, path) if path.starts_with("/backups/") => {
723 return backup_job(&mut request, &services, method, path).await;
724 }
Integrations: your own model provider, alerts that open issues, tickets agents read725 ("POST", path) if path.starts_with("/queue/") => {
726 let entry_id = path.trim_start_matches("/queue/").to_owned();
727 return report_queue(&mut request, &services, &entry_id).await;
728 }
GitHub Actions on g1t, part two: running workflows729 // A sandbox running a GitHub Actions job: fetching the job, and
730 // reporting how it goes. The job's own token is the credential.
731 ("POST", path) if path.starts_with("/actions/jobs/") => {
732 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts733 // `/action`: where to fetch another repository's action from (on
734 // g1t, with a read token for a private one, or GitHub).
735 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
736 (Some(job), _) => (job.to_owned(), "job_spec"),
737 (_, Some(job)) => (job.to_owned(), "job_action"),
738 _ => (rest.to_owned(), "job_report"),
GitHub Actions on g1t, part two: running workflows739 };
740 let body = json_body(&mut request).await;
741 let answered: Outcome<Value> = g1t_kit::call(
742 &services.actions,
743 method,
744 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
745 )
746 .await?;
747 return match answered {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API748 // A job's spec is the workflow and its contexts as GitHub
749 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2750 Outcome::Ok(value) => {
751 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
752 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
753 Response::from_json(&spec)
754 }
GitHub Actions on g1t, part two: running workflows755 Outcome::Fail(refused) => failure(&refused),
756 };
757 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains758 // A sandbox reporting its agent run's steps, cost and end. As with
759 // checks, the run's own token, in the body, is the credential.
760 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/report") => {
761 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/report");
762 let mut body = json_body(&mut request).await;
763 if !body.is_object() {
764 body = json!({});
765 }
766 body["runId"] = json!(run_id);
767 let reported: Outcome<Value> = g1t_kit::call(&services.work, "report_run", &body).await?;
768 return match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API769 Outcome::Ok(status) => reply(&json!({ "status": status })),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains770 Outcome::Fail(refused) => failure(&refused),
771 };
772 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API773 // What a run's agent learned, as memory candidates; the same token.
774 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/learned") => {
775 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/learned");
776 let body = json_body(&mut request).await;
777 let learned = json!({
778 "runId": run_id,
779 "token": body["token"].as_str().unwrap_or_default(),
780 "items": body["items"].as_array().cloned().unwrap_or_default(),
781 });
782 let captured: Outcome<Value> = g1t_kit::call(&services.work, "report_learned", &learned).await?;
783 return match captured {
784 Outcome::Ok(captured) => reply(&captured),
785 Outcome::Fail(refused) => failure(&refused),
786 };
787 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step788 // How sure a run's agent is of its change; the same token.
789 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/confidence") => {
790 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/confidence");
791 let body = json_body(&mut request).await;
792 let said = json!({
793 "runId": run_id,
794 "token": body["token"].as_str().unwrap_or_default(),
795 "confidence": body["confidence"].as_str().unwrap_or_default(),
796 "uncertainAbout": body["uncertain_about"]
797 .as_array()
798 .map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>())
799 .unwrap_or_default(),
800 });
801 let recorded: Outcome<Value> = g1t_kit::call(&services.work, "report_confidence", &said).await?;
802 return match recorded {
803 Outcome::Ok(recorded) => reply(&json!({ "recorded": recorded })),
804 Outcome::Fail(refused) => failure(&refused),
805 };
806 }
Integrations: your own model provider, alerts that open issues, tickets agents read807 ("POST", path) if path.starts_with("/checks/") => {
808 let run_id = path.trim_start_matches("/checks/").to_owned();
809 return report_checks(&mut request, &services, &run_id).await;
810 }
811 ("POST", path) if path.starts_with("/runs/") && path.ends_with("/usage") => {
812 let run_id = path
813 .trim_start_matches("/runs/")
814 .trim_end_matches("/usage")
815 .to_owned();
816 return report_usage(&mut request, &services, &run_id).await;
817 }
818 ("POST", path) if path.starts_with("/plans/") => {
819 let plan_id = path.trim_start_matches("/plans/").to_owned();
820 return report_plan(&mut request, &services, &plan_id).await;
821 }
822 ("POST", path) if path.starts_with("/reviews/") => {
823 let run_id = path.trim_start_matches("/reviews/").to_owned();
824 return report_review(&mut request, &services, &run_id).await;
825 }
826 _ => {}
827 }
828
829 let query: Vec<(String, String)> = url
830 .query_pairs()
831 .map(|(name, value)| (name.into_owned(), value.into_owned()))
832 .collect();
833 let body = if method == "GET" {
834 Value::Null
835 } else {
836 snake_case_keys(json_body(&mut request).await)
837 };
838 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
839 return fail(FailureCode::NotFound, "No such endpoint.");
840 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API841 match audit::run(route.op, &services, &viewer, &input).await? {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2842 // A download is a redirect to its signed link, as GitHub's is.
843 Outcome::Ok(value) if route.op == operations::Op::Artifacts(artifacts::ArtifactsOp::DownloadArtifact) => {
844 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
845 Some(url) => Response::redirect_with_status(url, 302),
846 None => reply(&value),
847 }
848 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts849 // A deleted comment has nothing to say, as GitHub's says nothing.
850 Outcome::Ok(_) if route.no_content() => Ok(Response::empty()?.with_status(204)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API851 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 step852 // A token without the scope a call needs is told which one.
853 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
854 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
855 "error": {
856 "code": refused.code,
857 "message": refused.message,
858 "needed_scope": scope.as_str(),
859 }
860 }))?
861 .with_status(403)),
862 _ => failure(&refused),
863 },
Integrations: your own model provider, alerts that open issues, tickets agents read864 }
865}
866
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API867/// Request bodies take the same keys as the MCP tools, `snake_case`, as
868/// responses use; the `camelCase` spelling is accepted too.
Integrations: your own model provider, alerts that open issues, tickets agents read869fn snake_case_keys(body: Value) -> Value {
870 let Value::Object(fields) = body else {
871 return body;
872 };
873 let mut out = serde_json::Map::new();
874 for (key, value) in fields {
875 let mut snake = String::with_capacity(key.len() + 4);
876 for c in key.chars() {
877 if c.is_ascii_uppercase() {
878 snake.push('_');
879 snake.push(c.to_ascii_lowercase());
880 } else {
881 snake.push(c);
882 }
883 }
884 // A key given in both spellings keeps the snake_case one.
885 if snake != key && out.contains_key(&snake) {
886 continue;
887 }
888 out.insert(snake, value);
889 }
890 Value::Object(out)
891}
892
893#[cfg(test)]
894mod tests {
895 use super::snake_case_keys;
896 use serde_json::json;
897
898 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)899 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
900 assert!(super::pending_may("GET", "/user", false));
901 assert!(super::pending_may("GET", "/user/emails/", false));
902 assert!(super::pending_may("POST", "/user/emails/confirm", false));
903 assert!(!super::pending_may("POST", "/user/emails", false));
904 assert!(!super::pending_may("POST", "/workspaces", false));
905 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
906 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
907 assert!(!super::pending_may("POST", "/", true));
908 }
909
910 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2911 fn a_job_spec_gets_the_toolkits_variables() {
912 // As the actions service sends it, converted as the API does.
913 let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true } });
914 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
915 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
916 assert!(spec.get("runtime").is_none(), "the runner never sees it");
917 let vars = &spec["variables"];
918 assert_eq!(vars["GITHUB_SHA"], "abc");
919 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
920 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
921 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
922 // No OIDC key here: no OIDC variables, whatever the job may do.
923 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
924 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
925 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
926 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
927 }
928
929 #[test]
Integrations: your own model provider, alerts that open issues, tickets agents read930 fn camel_case_keys_are_accepted() {
931 assert_eq!(
932 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
933 json!({ "count_agent_approvals": false, "title": "x" })
934 );
935 }
936
937 #[test]
938 fn snake_case_wins_when_both_are_given() {
939 assert_eq!(
940 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
941 json!({ "keep_issue_open": true })
942 );
943 }
944}
945
946// The API is called from browsers too: the reference's explorer, and apps
947// built on g1t. It carries no cookies, so any origin may call it.
948#[event(fetch)]
949async fn fetch(request: Request, env: Env, _ctx: Context) -> Result<Response> {
950 let mut response = respond(request, &env).await?;
951 let headers = response.headers_mut();
952 headers.set("access-control-allow-origin", "*")?;
953 headers.set(
954 "access-control-allow-headers",
955 "authorization, content-type",
956 )?;
957 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails958 headers.set("access-control-expose-headers", "www-authenticate, retry-after")?;
Integrations: your own model provider, alerts that open issues, tickets agents read959 Ok(response)
960}

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