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.

Two limits, real invoices, trust that grows by itself, sales signals1//! 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;
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;
Two limits, real invoices, trust that grows by itself, sales signals13mod 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;
Two limits, real invoices, trust that grows by itself, sales signals20mod mcp;
API: notifications over REST and MCP, with notifications scopes21mod notifications;
Two limits, real invoices, trust that grows by itself, sales signals22mod 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;
Merge main into Artifacts Phase 225mod folios;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar26mod people;
Two limits, real invoices, trust that grows by itself, sales signals27mod openapi;
API: pinned projects over REST and MCP28mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9729mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'30mod protection;
Two limits, real invoices, trust that grows by itself, sales signals31mod 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;
Two limits, real invoices, trust that grows by itself, sales signals35mod 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;
Two limits, real invoices, trust that grows by itself, sales signals42
Merge branch 'model-routing'43use g1t_contracts::billing::{FinishRunArgs, RunTokens};
Two limits, real invoices, trust that grows by itself, sales signals44use 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,
Two limits, real invoices, trust that grows by itself, sales signals50};
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;
Two limits, real invoices, trust that grows by itself, sales signals54use 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
Two limits, real invoices, trust that grows by itself, sales signals112/// 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()))
Two limits, real invoices, trust that grows by itself, sales signals115}
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()),
Two limits, real invoices, trust that grows by itself, sales signals159 )?;
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}}");
Two limits, real invoices, trust that grows by itself, sales signals167 json!({
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}}"),
Two limits, real invoices, trust that grows by itself, sales signals175 "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"),
Two limits, real invoices, trust that grows by itself, sales signals191 "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}}"),
Two limits, real invoices, trust that grows by itself, sales signals194 })
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));
Two limits, real invoices, trust that grows by itself, sales signals213 }
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))
Two limits, real invoices, trust that grows by itself, sales signals224}
225
226async 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));
Two limits, real invoices, trust that grows by itself, sales signals231 }
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 }))?,
Two limits, real invoices, trust that grows by itself, sales signals242 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)
Two limits, real invoices, trust that grows by itself, sales signals244 }
245 })
246}
247
248async 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!({
Two limits, real invoices, trust that grows by itself, sales signals259 "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),
Two limits, real invoices, trust that grows by itself, sales signals263 "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 {
Two limits, real invoices, trust that grows by itself, sales signals279 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()),
Two limits, real invoices, trust that grows by itself, sales signals283 "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.
Two limits, real invoices, trust that grows by itself, sales signals294/// 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 })),
Two limits, real invoices, trust that grows by itself, sales signals316 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(),
Two limits, real invoices, trust that grows by itself, sales signals339 },
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 })),
Two limits, real invoices, trust that grows by itself, sales signals344 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
Two limits, real invoices, trust that grows by itself, sales signals432/// 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 })),
Two limits, real invoices, trust that grows by itself, sales signals456 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 })),
Two limits, real invoices, trust that grows by itself, sales signals482 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 }),
Two limits, real invoices, trust that grows by itself, sales signals511 },
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 })),
Two limits, real invoices, trust that grows by itself, sales signals516 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();
Two limits, real invoices, trust that grows by itself, sales signals536
537 // 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
544 // 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.
547 if method == "POST" && !on_mcp
548 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
549 return receive_hook(&mut request, &services, id).await;
550 }
551
552 // 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()));
566 let mut answer = env
567 .service("DEPLOYMENTS")?
568 .fetch_request(Request::new_with_init(&target, &init)?)
569 .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)?
Two limits, real invoices, trust that grows by itself, sales signals579 .with_status(status)
580 .with_headers({
581 let headers = worker::Headers::new();
582 headers.set("content-type", "application/json")?;
583 headers
584 }));
585 }
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
Two limits, real invoices, trust that grows by itself, sales signals609 // 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"))
Two limits, real invoices, trust that grows by itself, sales signals614 {
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 }
Two limits, real invoices, trust that grows by itself, sales signals633 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)),
Two limits, real invoices, trust that grows by itself, sales signals636 };
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) {
Two limits, real invoices, trust that grows by itself, sales signals651 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
Two limits, real invoices, trust that grows by itself, sales signals682 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'683 ("GET", "") => return reply(&index(&services.addresses)),
Two limits, real invoices, trust that grows by itself, sales signals684 ("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/") => {
Two limits, real invoices, trust that grows by itself, sales signals688 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;
Two limits, real invoices, trust that grows by itself, sales signals699 }
700 }
701 ("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),
Two limits, real invoices, trust that grows by itself, sales signals713 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 }
Two limits, real invoices, trust that grows by itself, sales signals725 ("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 }
729 // 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"),
Two limits, real invoices, trust that grows by itself, sales signals739 };
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 }
Two limits, real invoices, trust that grows by itself, sales signals755 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 }
Two limits, real invoices, trust that grows by itself, sales signals807 ("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.
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.843 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 R2844 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 },
Two limits, real invoices, trust that grows by itself, sales signals864 }
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.
Two limits, real invoices, trust that grows by itself, sales signals869fn 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]
Two limits, real invoices, trust that grows by itself, sales signals930 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")?;
Two limits, real invoices, trust that grows by itself, sales signals959 Ok(response)
960}

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