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

Keep g1t's own runs off the model proxy until it holds g1t's key1//! 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;
Keep g1t's own runs off the model proxy until it holds g1t's key20mod mcp;
API: notifications over REST and MCP, with notifications scopes21mod notifications;
Keep g1t's own runs off the model proxy until it holds g1t's key22mod 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;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar25mod people;
Keep g1t's own runs off the model proxy until it holds g1t's key26mod openapi;
API: pinned projects over REST and MCP27mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9728mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'29mod protection;
Keep g1t's own runs off the model proxy until it holds g1t's key30mod operations;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains31mod renamed;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API32#[cfg(test)]
33mod responses;
Keep g1t's own runs off the model proxy until it holds g1t's key34mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge35mod rules;
Fast pages, required checks on the branch, self-hosted runners, honest incidents36mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar37mod security;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step38mod tools;
API and MCP for a workspace's personal access token rules, members' tokens and approvals39mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R240mod toolkit;
Keep g1t's own runs off the model proxy until it holds g1t's key41
Merge branch 'model-routing'42use g1t_contracts::billing::{FinishRunArgs, RunTokens};
Keep g1t's own runs off the model proxy until it holds g1t's key43use g1t_contracts::identity::{
44 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
45};
46use g1t_contracts::work::{
Agents and memory, checks and conflicts, profiles, slug renames, custom domains47 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs, ReportPlanArgs,
48 ReportQueueArgs, ReportReviewArgs,
Keep g1t's own runs off the model proxy until it holds g1t's key49};
50use g1t_contracts::identity::AgentScope;
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)51use 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 API52use g1t_kit::wire;
Keep g1t's own runs off the model proxy until it holds g1t's key53use serde_json::{Value, json};
54use worker::{Context, Env, Method, Request, Response, Result, event};
55
56use operations::Services;
57
58fn method_name(method: Method) -> &'static str {
59 match method {
60 Method::Get => "GET",
61 Method::Post => "POST",
62 Method::Patch => "PATCH",
63 Method::Put => "PUT",
64 Method::Delete => "DELETE",
65 Method::Options => "OPTIONS",
66 Method::Head => "HEAD",
67 _ => "OTHER",
68 }
69}
70
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API71/// A JSON response. Every body the API sends has its keys in `snake_case`;
72/// the contracts it passes through are `camelCase`, so they are converted
73/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
74/// the MCP protocol's own envelope keep the spelling their standards use.
75pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
76 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
77}
78
79/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
80/// workflow file, GitHub's contexts and event, and where to check out, all
81/// passed through as they are.
82const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'83 "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 API84];
85
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R286/// Puts the toolkit's variables in a job's spec: its runtime token, where
87/// the toolkit's services are, and where to ask for an OIDC token when the
88/// job may have one and this installation issues them. The `runtime` the
89/// actions service sent goes no further.
90fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
91 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
92 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
93 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
94 let vars = toolkit::runtime_variables(api, token, id_token);
95 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
96 variables.extend(vars);
97 }
98}
99
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)100/// What a person whose account has not confirmed its email address may
101/// call: who they are, their addresses, and confirming one with the code
102/// from the email. Nothing over MCP.
103fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
104 !on_mcp
105 && matches!(
106 (method, path.trim_end_matches('/')),
107 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
108 )
109}
110
Keep g1t's own runs off the model proxy until it holds g1t's key111/// An error in the shape every endpoint uses.
112fn failure(failure: &Failure) -> Result<Response> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API113 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
Keep g1t's own runs off the model proxy until it holds g1t's key114}
115
116fn fail(code: FailureCode, message: &str) -> Result<Response> {
117 failure(&Failure {
118 code,
119 message: message.to_owned(),
120 })
121}
122
123/// A request body as JSON. An empty or malformed body is no input.
124async fn json_body(request: &mut Request) -> Value {
125 request.json().await.unwrap_or(Value::Null)
126}
127
128/// Who a request's `Authorization: Bearer g1t_…` names. A missing token is
129/// an anonymous viewer; a wrong one is refused, so that a typo does not
130/// silently look signed out.
131async fn authenticate(
132 request: &Request,
133 services: &Services,
134) -> Result<std::result::Result<Viewer, Response>> {
135 let header = request.headers().get("authorization")?.unwrap_or_default();
136 let token = match header.split_once(' ') {
137 Some((scheme, token)) if scheme.eq_ignore_ascii_case("bearer") && !token.is_empty() => {
138 token.trim()
139 }
140 _ => return Ok(Ok(None)),
141 };
142 let viewer: Viewer = g1t_kit::call(
143 &services.identity,
144 "user_for_access_token",
145 &TokenArgs {
146 token: token.to_owned(),
147 },
148 )
149 .await?;
150 if viewer.is_some() {
151 return Ok(Ok(viewer));
152 }
153 let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?;
154 // Tells an MCP client where to sign in again.
155 response.headers_mut().set(
156 "www-authenticate",
Merge branch 'worktree-agent-aaf03bdceac799c89'157 &format!("{}, error=\"invalid_token\"", services.addresses.mcp_challenge()),
Keep g1t's own runs off the model proxy until it holds g1t's key158 )?;
159 Ok(Err(response))
160}
161
162/// Where everything is, for someone or something exploring the API.
Merge branch 'worktree-agent-aaf03bdceac799c89'163fn index(addresses: &addresses::Addresses) -> Value {
164 let api = &addresses.api;
165 let repo = format!("{api}/repos/{{owner}}/{{name}}");
Keep g1t's own runs off the model proxy until it holds g1t's key166 json!({
Merge branch 'worktree-agent-ab2e39e11a6493412'167 "documentation_url": "https://docs.g1t.sh/reference/api/",
Merge branch 'worktree-agent-aaf03bdceac799c89'168 "openapi_url": format!("{api}/openapi.json"),
169 "mcp_url": addresses.mcp,
170 "current_user_url": format!("{api}/user"),
171 "workspaces_url": format!("{api}/workspaces"),
172 "repositories_url": format!("{api}/repos{{?q}}"),
173 "search_url": format!("{api}/search{{?q,type,page,per_page}}"),
Keep g1t's own runs off the model proxy until it holds g1t's key174 "repository_url": repo,
175 "repository_events_url": format!("{repo}/events{{?before}}"),
176 "labels_url": format!("{repo}/labels"),
177 "issues_url": format!("{repo}/issues{{?state,label}}"),
178 "issue_url": format!("{repo}/issues/{{number}}"),
179 "issue_comments_url": format!("{repo}/issues/{{number}}/comments"),
180 "pulls_url": format!("{repo}/pulls{{?state}}"),
181 "pull_url": format!("{repo}/pulls/{{number}}"),
182 "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"),
183 "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"),
184 "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"),
Merge branch 'worktree-agent-aaf03bdceac799c89'185 "device_code_url": format!("{api}/device/code"),
186 "device_token_url": format!("{api}/device/token"),
187 "oauth_metadata_url": format!("{api}/.well-known/oauth-authorization-server"),
188 "git_url": format!("{}/{{owner}}/{{name}}.git", addresses.site),
189 "integrations_url": format!("{api}/workspaces/{{workspace}}/integrations"),
Keep g1t's own runs off the model proxy until it holds g1t's key190 "context_url": format!("{repo}/context{{?reference}}"),
191 "import_issue_url": format!("{repo}/issues/import"),
Merge branch 'worktree-agent-aaf03bdceac799c89'192 "hooks_url": format!("{api}/hooks/{{integration}}"),
Keep g1t's own runs off the model proxy until it holds g1t's key193 })
194}
195
196// Signing in from a tool. Accounts are created, and passwords typed, only
197// in a browser; a tool gets its token by having a person approve a code.
198
199/// Passes a request from an outside system to its connection, as it came:
200/// its signature covers the exact bytes of the body.
201async fn receive_hook(request: &mut Request, services: &Services, id: &str) -> Result<Response> {
202 let headers: std::collections::HashMap<String, String> = request
203 .headers()
204 .entries()
205 .map(|(name, value)| (name.to_lowercase(), value))
206 .collect();
207 let body = request.text().await.unwrap_or_default();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look208 // A push to GitHub with many commits makes a large payload.
209 let limit = if id == "github" { 10_000_000 } else { 1_000_000 };
210 if body.len() > limit {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API211 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
Keep g1t's own runs off the model proxy until it holds g1t's key212 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look213 // g1t's GitHub App has one webhook for every installation; it is
214 // checked against the app's own secret.
215 let (method, args) = if id == "github" {
216 ("github_receive", json!({ "headers": headers, "body": body }))
217 } else {
218 ("receive", json!({ "id": id, "headers": headers, "body": body }))
219 };
220 let received: g1t_contracts::integrations::Received =
221 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 API222 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
Keep g1t's own runs off the model proxy until it holds g1t's key223}
224
Stripe webhooks, enterprise invoices, and sudo for both225async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
226 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
227 let payload = request.text().await.unwrap_or_default();
228 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 API229 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
Stripe webhooks, enterprise invoices, and sudo for both230 }
231 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
232 &env.service("BILLING")?,
233 "stripe_webhook",
234 &g1t_contracts::billing::StripeWebhookArgs { payload, signature },
235 )
236 .await?;
237 // A refusal is a 400, so Stripe shows it as failed; anything handled,
238 // or already handled, is a 200, so Stripe stops sending it.
239 Ok(match handled {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API240 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
Stripe webhooks, enterprise invoices, and sudo for both241 g1t_contracts::Outcome::Fail(failure) => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API242 reply(&json!({ "message": failure.message }))?.with_status(400)
Stripe webhooks, enterprise invoices, and sudo for both243 }
244 })
245}
246
Keep g1t's own runs off the model proxy until it holds g1t's key247async fn device_code(request: &mut Request, services: &Services) -> Result<Response> {
248 let body = json_body(request).await;
249 let started: DeviceStart = g1t_kit::call(
250 &services.identity,
251 "device_start",
252 &DeviceStartArgs {
253 client_name: body["client_name"].as_str().unwrap_or_default().to_owned(),
254 },
255 )
256 .await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API257 reply(&json!({
Keep g1t's own runs off the model proxy until it holds g1t's key258 "device_code": started.device_code,
259 "user_code": started.user_code,
Merge branch 'worktree-agent-aaf03bdceac799c89'260 "verification_uri": format!("{}/device", services.addresses.site),
261 "verification_uri_complete": format!("{}/device?code={}", services.addresses.site, started.user_code),
Keep g1t's own runs off the model proxy until it holds g1t's key262 "expires_in": started.expires_in,
263 "interval": started.interval,
264 }))
265}
266
267async fn device_token(request: &mut Request, services: &Services) -> Result<Response> {
268 let body = json_body(request).await;
269 let claim: DeviceClaim = g1t_kit::call(
270 &services.identity,
271 "device_claim",
272 &DeviceClaimArgs {
273 device_code: body["device_code"].as_str().unwrap_or_default().to_owned(),
274 },
275 )
276 .await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API277 reply(&match claim {
Keep g1t's own runs off the model proxy until it holds g1t's key278 DeviceClaim::Approved { token, user } => json!({
279 "status": "approved",
280 "token": token,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar281 "display_username": user.display_username.clone().filter(|display| display.eq_ignore_ascii_case(&user.username)).unwrap_or_else(|| user.username.clone()),
Keep g1t's own runs off the model proxy until it holds g1t's key282 "username": user.username,
283 "verified": user.verified,
284 }),
285 DeviceClaim::Pending => json!({ "status": "pending" }),
286 DeviceClaim::Denied => json!({ "status": "denied" }),
287 DeviceClaim::Expired => json!({ "status": "expired" }),
288 })
289}
290
Fast pages, required checks on the branch, self-hosted runners, honest incidents291/// A sandbox reporting on a run of an issue's commands, from before a pull
292/// request's checks were the workflows run on it.
Keep g1t's own runs off the model proxy until it holds g1t's key293/// The run's own token, in the body, is the credential: it was given to
294/// that sandbox and to nothing else.
295async fn report_checks(
296 request: &mut Request,
297 services: &Services,
298 run_id: &str,
299) -> Result<Response> {
300 let body = json_body(request).await;
301 let reported: Outcome<CheckRun> = g1t_kit::call(
302 &services.work,
303 "report_checks",
304 &ReportChecksArgs {
305 run_id: run_id.to_owned(),
306 token: body["token"].as_str().unwrap_or_default().to_owned(),
307 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
308 error: body["error"].as_str().map(str::to_owned),
309 skip: false,
310 },
311 )
312 .await?;
313 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API314 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
Keep g1t's own runs off the model proxy until it holds g1t's key315 Outcome::Fail(refused) => failure(&refused),
316 }
317}
318
319/// A sandbox reporting one tested state of a merge queue. As with checks,
320/// the entry's own token is the credential.
321async fn report_queue(
322 request: &mut Request,
323 services: &Services,
324 entry_id: &str,
325) -> Result<Response> {
326 let body = json_body(request).await;
327 let reported: Outcome<QueueState> = g1t_kit::call(
328 &services.work,
329 "report_queue",
330 &ReportQueueArgs {
331 entry_id: entry_id.to_owned(),
332 token: body["token"].as_str().unwrap_or_default().to_owned(),
333 combined_commit: body["combinedCommit"].as_str().map(str::to_owned),
334 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
335 error: body["error"].as_str().map(str::to_owned),
336 conflict_with: body["conflictWith"].as_u64().map(|n| n as u32),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains337 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
Keep g1t's own runs off the model proxy until it holds g1t's key338 },
339 )
340 .await?;
341 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API342 Outcome::Ok(state) => reply(&json!({ "state": state })),
Keep g1t's own runs off the model proxy until it holds g1t's key343 Outcome::Fail(refused) => failure(&refused),
344 }
345}
346
Agents and memory, checks and conflicts, profiles, slug renames, custom domains347/// A sandbox reporting whether a pull request merges cleanly. As with
348/// checks, the probe's own token is the credential.
349async fn report_mergecheck(
350 request: &mut Request,
351 services: &Services,
352 pull_id: &str,
353) -> Result<Response> {
354 let body = json_body(request).await;
355 let reported: Outcome<Mergeable> = g1t_kit::call(
356 &services.work,
357 "report_mergecheck",
358 &ReportMergecheckArgs {
359 pull_id: pull_id.to_owned(),
360 token: body["token"].as_str().unwrap_or_default().to_owned(),
361 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
362 error: body["error"].as_str().map(str::to_owned),
363 },
364 )
365 .await?;
366 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API367 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains368 Outcome::Fail(refused) => failure(&refused),
369 }
370}
371
Merge branch 'worktree-agent-ac5b181a013e54348'372/// A backup's sandbox, passed on to the repos service, which holds the
373/// job (services/repos/src/backups.rs; the flow is in
374/// `g1t_contracts::backups`):
375///
376/// - `POST /backups/{job}/spec`: what to cut, and a read-only git credential
377/// - `PUT /backups/{job}/parts/{n}`: one part of the bundle, as bytes
378/// - `POST /backups/{job}/complete` with `{ refs, size, sha256, parts, fetched_bytes }`
379/// - `POST /backups/{job}/fail` with `{ error, fetched_bytes }`
380///
381/// Bodies are passed through as they are: snake_case already, and a
382/// bundle's refs are keyed by ref names, which must not be converted.
383async fn backup_job(request: &mut Request, services: &Services, method: &str, path: &str) -> Result<Response> {
384 use g1t_contracts::backups::TOKEN_HEADER;
385 let token = request.headers().get(TOKEN_HEADER)?.unwrap_or_default();
386 let rest = path.trim_start_matches("/backups/");
387 let (job, action) = rest.split_once('/').unwrap_or((rest, ""));
388 if job.is_empty() || token.is_empty() {
389 return fail(FailureCode::Unauthenticated, "A backup job's token is required.");
390 }
391 if method == "PUT" && action.starts_with("parts/") {
392 let bytes = request.bytes().await?;
393 if bytes.len() as u64 > g1t_contracts::backups::PART_BYTES {
394 return fail(FailureCode::Invalid, "A part holds 32 MiB at most.");
395 }
396 let headers = worker::Headers::new();
397 headers.set(TOKEN_HEADER, &token)?;
398 let mut init = worker::RequestInit::new();
399 init.with_method(Method::Put)
400 .with_headers(headers)
401 .with_body(Some(worker::js_sys::Uint8Array::from(bytes.as_slice()).into()));
402 let forwarded = Request::new_with_init(&format!("https://repos/backups/{job}/{action}"), &init)?;
403 let mut answered = services.repos.fetch_request(forwarded).await?;
404 return outcome_as_given(answered.json().await?);
405 }
406 let rpc = match (method, action) {
407 ("POST", "spec") => "backup_spec",
408 ("POST", "complete") => "backup_complete",
409 ("POST", "fail") => "backup_fail",
410 _ => return fail(FailureCode::NotFound, "No such endpoint."),
411 };
412 let mut body = json_body(request).await;
413 if !body.is_object() {
414 body = json!({});
415 }
416 body["job_id"] = json!(job);
417 body["token"] = json!(token);
418 let answered: Value = g1t_kit::call(&services.repos, rpc, &body).await?;
419 outcome_as_given(answered)
420}
421
422/// An `Outcome` from a service whose keys are already the API's: the value,
423/// or the failure in the shape every endpoint uses.
424fn outcome_as_given(answered: Value) -> Result<Response> {
425 match serde_json::from_value::<Outcome<Value>>(answered)? {
426 Outcome::Ok(value) => Response::from_json(&value),
427 Outcome::Fail(refused) => failure(&refused),
428 }
429}
430
Keep g1t's own runs off the model proxy until it holds g1t's key431/// A sandbox reporting the review its agent wrote. As with checks, the
432/// run's own token is the credential.
433async fn report_review(
434 request: &mut Request,
435 services: &Services,
436 run_id: &str,
437) -> Result<Response> {
438 let body = json_body(request).await;
439 let reported: Outcome<bool> = g1t_kit::call(
440 &services.work,
441 "report_review",
442 &ReportReviewArgs {
443 run_id: run_id.to_owned(),
444 token: body["token"].as_str().unwrap_or_default().to_owned(),
445 verdict: serde_json::from_value(body["verdict"].clone()).unwrap_or(None),
446 body: body["body"].as_str().unwrap_or_default().to_owned(),
447 comments: serde_json::from_value(body["comments"].clone()).unwrap_or_default(),
448 model: body["model"].as_str().map(str::to_owned),
449 error: body["error"].as_str().map(str::to_owned),
450 },
451 )
452 .await?;
453 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API454 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
Keep g1t's own runs off the model proxy until it holds g1t's key455 Outcome::Fail(refused) => failure(&refused),
456 }
457}
458
459/// A sandbox reporting the plan its agent wrote. As with checks, the
460/// plan's own token is the credential.
461async fn report_plan(
462 request: &mut Request,
463 services: &Services,
464 plan_id: &str,
465) -> Result<Response> {
466 let body = json_body(request).await;
467 let reported: Outcome<bool> = g1t_kit::call(
468 &services.work,
469 "report_plan",
470 &ReportPlanArgs {
471 plan_id: plan_id.to_owned(),
472 token: body["token"].as_str().unwrap_or_default().to_owned(),
473 summary: body["summary"].as_str().unwrap_or_default().to_owned(),
474 issues: serde_json::from_value(body["issues"].clone()).unwrap_or_default(),
475 error: body["error"].as_str().map(str::to_owned),
476 },
477 )
478 .await?;
479 match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API480 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
Keep g1t's own runs off the model proxy until it holds g1t's key481 Outcome::Fail(refused) => failure(&refused),
482 }
483}
484
485/// A sandbox reporting what its agent's run cost, so that the workspace
486/// it worked for is charged. As with checks, the run's own token is the
487/// credential.
488async fn report_usage(
489 request: &mut Request,
490 services: &Services,
491 run_id: &str,
492) -> Result<Response> {
493 let body = json_body(request).await;
494 let charged: Outcome<bool> = g1t_kit::call(
495 &services.billing,
496 "finish_run",
497 &FinishRunArgs {
498 run_id: run_id.to_owned(),
499 token: body["token"].as_str().unwrap_or_default().to_owned(),
500 cost_usd: body["cost_usd"].as_f64().unwrap_or_default(),
501 turns: body["turns"].as_u64().unwrap_or_default() as u32,
Merge branch 'model-routing'502 // What the harness counted; the agent rate is charged on no
503 // fewer, on a workspace's own model key too.
504 tokens: body.get("tokens").filter(|t| t.is_object()).map(|t| RunTokens {
505 input: t["input"].as_u64().unwrap_or_default(),
506 output: t["output"].as_u64().unwrap_or_default(),
507 cache_read: t["cache_read"].as_u64().unwrap_or_default(),
508 cache_write: t["cache_write"].as_u64().unwrap_or_default(),
509 }),
Keep g1t's own runs off the model proxy until it holds g1t's key510 },
511 )
512 .await?;
513 match charged {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API514 Outcome::Ok(_) => reply(&json!({ "recorded": true })),
Keep g1t's own runs off the model proxy until it holds g1t's key515 Outcome::Fail(refused) => failure(&refused),
516 }
517}
518
519async fn respond(mut request: Request, env: &Env) -> Result<Response> {
520 let method = method_name(request.method());
521 if method == "OPTIONS" {
522 return Ok(Response::empty()?.with_status(204));
523 }
524 let url = request.url()?;
525 // Paths carry no version. An earlier form began with `/v1`, which is
526 // still accepted so that nothing already written against it breaks.
527 let path = match url.path().strip_prefix("/v1") {
528 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
529 _ => url.path().to_owned(),
530 };
531 let mut services = Services::new(env)?;
Merge branch 'worktree-agent-aaf03bdceac799c89'532 // MCP is a host of its own hosted, and may be a path on this one
533 // self-hosted (addresses.rs).
534 let on_mcp = services.addresses.mcp_path(&url).is_some();
Keep g1t's own runs off the model proxy until it holds g1t's key535
Stripe webhooks, enterprise invoices, and sudo for both536 // Stripe reporting to billing. Signed with the secret of the endpoint
537 // billing registered; the body goes through exactly as received, since
538 // the signature covers its bytes.
539 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
540 return receive_stripe(&mut request, env).await;
541 }
542
Keep g1t's own runs off the model proxy until it holds g1t's key543 // Outside systems reporting to a connection. They sign what they send
544 // with the connection's own secret, which is not a g1t token, so this
545 // comes before anything that would read one.
Polish: phones, copy boxes, the plan page, the landing page, a real glide546 if method == "POST" && !on_mcp
547 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
Keep g1t's own runs off the model proxy until it holds g1t's key548 return receive_hook(&mut request, &services, id).await;
549 }
550
Deployments: a preview for every pull request, production on g1t.page551 // A sandbox building a deployment, reporting with its build's token,
552 // which is not a g1t token. The body goes through as it is: it can
553 // carry a Worker's bundled code.
554 if method == "POST" && !on_mcp
555 && let Some(rest) = path.strip_prefix("/deployments/jobs/")
556 {
557 let target = format!("https://deployments/jobs/{rest}");
558 let body = request.bytes().await?;
559 let headers = worker::Headers::new();
560 headers.set("content-type", "application/json")?;
561 let mut init = worker::RequestInit::new();
562 init.with_method(Method::Post)
563 .with_headers(headers)
564 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
Deployments work end to end: fixes from the first live run565 let mut answer = env
Deployments: a preview for every pull request, production on g1t.page566 .service("DEPLOYMENTS")?
567 .fetch_request(Request::new_with_init(&target, &init)?)
Deployments work end to end: fixes from the first live run568 .await?;
569 // A fresh response: a fetched one's headers cannot be changed, and
570 // every response gets the API's own on the way out.
571 let status = answer.status_code();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API572 let bytes = answer.bytes().await?;
573 let bytes = match serde_json::from_slice::<Value>(&bytes) {
574 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
575 Err(_) => bytes,
576 };
577 return Ok(Response::from_bytes(bytes)?
Deployments work end to end: fixes from the first live run578 .with_status(status)
579 .with_headers({
580 let headers = worker::Headers::new();
581 headers.set("content-type", "application/json")?;
582 headers
583 }));
Deployments: a preview for every pull request, production on g1t.page584 }
585
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2586 // The services GitHub's toolkit calls from inside a job, with its
587 // runtime token, and the links they hand out (toolkit.rs).
588 if !on_mcp && method == "POST"
589 && let Some(rest) = path.strip_prefix("/twirp/")
590 {
591 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
592 let (service, rpc) = (service.to_owned(), rpc.to_owned());
593 return toolkit::twirp(request, env, &services, &service, &rpc).await;
594 }
595 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
596 let rest = rest.to_owned();
597 return toolkit::cache_v1(request, env, &services, method, &rest).await;
598 }
599 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
600 let token = token.to_owned();
601 return toolkit::blob(request, env, &services, method, &token).await;
602 }
603 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
604 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
605 return oidc::handle(&request, env, &services, &path).await;
606 }
607
A repository has its own sidebar, as settings do608 // A sandbox's artifacts and cache, with its job's token, which is not a
609 // g1t token either.
610 if !on_mcp
611 && let Some(rest) = path.strip_prefix("/actions/jobs/")
Fast pages, required checks on the branch, self-hosted runners, honest incidents612 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
A repository has its own sidebar, as settings do613 {
614 let rest = rest.to_owned();
615 return blobs::for_job(request, env, &services, method, &rest).await;
616 }
617
Fast pages, required checks on the branch, self-hosted runners, honest incidents618 // A self-hosted runner, with a registration token or its own
619 // credential, neither of which is a g1t access token.
620 if method == "POST"
621 && !on_mcp
622 && path.starts_with("/runners/")
623 && let Some(response) = runners::handle(&mut request, &services, &path).await?
624 {
625 return Ok(response);
626 }
627
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails628 // Per token, or per address without one (limits.rs).
629 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
630 return Ok(limited);
631 }
Keep g1t's own runs off the model proxy until it holds g1t's key632 let viewer = match authenticate(&request, &services).await? {
633 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails634 Err(refused) => return Ok(limits::wrong_token(&request, env, on_mcp).await?.unwrap_or(refused)),
Keep g1t's own runs off the model proxy until it holds g1t's key635 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)636 // A person who has not confirmed their email address: who they are,
637 // their addresses, and confirming one, nothing else (REST or MCP).
638 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
639 return fail(
640 FailureCode::Forbidden,
641 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
642 );
643 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API644 services.audit = audit::AuditContext::of(&request, on_mcp);
645 // An agent's token: what it may do comes with it, on the composite
646 // identity identity resolved it to.
647 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
648 services.scope = Some(acting.scope.clone());
649 } else if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
Keep g1t's own runs off the model proxy until it holds g1t's key650 let header = request.headers().get("authorization")?.unwrap_or_default();
651 let token = header.split_once(' ').map(|(_, token)| token.trim()).unwrap_or_default();
652 let scope: Option<AgentScope> = g1t_kit::call(
653 &services.identity,
654 "agent_scope",
655 &TokenArgs {
656 token: token.to_owned(),
657 },
658 )
659 .await?;
660 // A scope is what lets an agent's token do anything at all.
661 let Some(scope) = scope else {
662 return fail(FailureCode::Unauthenticated, "Invalid access token.");
663 };
664 services.scope = Some(scope);
665 }
666 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
667 return Ok(response);
668 }
669 if on_mcp {
670 return mcp::handle(request, &services, &viewer).await;
671 }
672
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)673 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
674 if method == "GET" {
675 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
676 if let Some(wanted) = logs::wanted(&path, text) {
677 return logs::download(&services, &viewer, wanted).await;
678 }
679 }
680
Keep g1t's own runs off the model proxy until it holds g1t's key681 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'682 ("GET", "") => return reply(&index(&services.addresses)),
Keep g1t's own runs off the model proxy until it holds g1t's key683 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2684 // One of a run's artifacts downloaded by name (the run's artifacts
685 // are listed by the REST route in rest.rs).
686 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts/") => {
A repository has its own sidebar, as settings do687 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2688 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'689 // A workflow job's token reaches its own repository only.
690 if viewer
691 .as_ref()
692 .and_then(|user| user.token.as_deref())
693 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
694 {
695 return fail(FailureCode::NotFound, "No such run.");
696 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2697 return blobs::download(env, &services, &viewer, owner, repo, run, name).await;
A repository has its own sidebar, as settings do698 }
699 }
Keep g1t's own runs off the model proxy until it holds g1t's key700 ("POST", "/device/code") => return device_code(&mut request, &services).await,
701 ("POST", "/device/token") => return device_token(&mut request, &services).await,
702 // Where a pull request lives, for a tool that knows only its fork.
703 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
704 let located: Outcome<Value> = g1t_kit::call(
705 &services.work,
706 "locate_pull",
707 &json!({ "id": &path[7..], "viewer": viewer }),
708 )
709 .await?;
710 return match located {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API711 Outcome::Ok(value) => reply(&value),
Keep g1t's own runs off the model proxy until it holds g1t's key712 Outcome::Fail(refused) => failure(&refused),
713 };
714 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains715 ("POST", path) if path.starts_with("/mergechecks/") => {
716 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
717 return report_mergecheck(&mut request, &services, &pull_id).await;
718 }
Merge branch 'worktree-agent-ac5b181a013e54348'719 // A sandbox making a repository's nightly backup. The job's own
720 // token, in its header, is the credential.
721 (method, path) if path.starts_with("/backups/") => {
722 return backup_job(&mut request, &services, method, path).await;
723 }
Keep g1t's own runs off the model proxy until it holds g1t's key724 ("POST", path) if path.starts_with("/queue/") => {
725 let entry_id = path.trim_start_matches("/queue/").to_owned();
726 return report_queue(&mut request, &services, &entry_id).await;
727 }
GitHub Actions on g1t, part two: running workflows728 // A sandbox running a GitHub Actions job: fetching the job, and
729 // reporting how it goes. The job's own token is the credential.
730 ("POST", path) if path.starts_with("/actions/jobs/") => {
731 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts732 // `/action`: where to fetch another repository's action from (on
733 // g1t, with a read token for a private one, or GitHub).
734 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
735 (Some(job), _) => (job.to_owned(), "job_spec"),
736 (_, Some(job)) => (job.to_owned(), "job_action"),
737 _ => (rest.to_owned(), "job_report"),
GitHub Actions on g1t, part two: running workflows738 };
739 let body = json_body(&mut request).await;
740 let answered: Outcome<Value> = g1t_kit::call(
741 &services.actions,
742 method,
743 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
744 )
745 .await?;
746 return match answered {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API747 // A job's spec is the workflow and its contexts as GitHub
748 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2749 Outcome::Ok(value) => {
750 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
751 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
752 Response::from_json(&spec)
753 }
GitHub Actions on g1t, part two: running workflows754 Outcome::Fail(refused) => failure(&refused),
755 };
756 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains757 // A sandbox reporting its agent run's steps, cost and end. As with
758 // checks, the run's own token, in the body, is the credential.
759 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/report") => {
760 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/report");
761 let mut body = json_body(&mut request).await;
762 if !body.is_object() {
763 body = json!({});
764 }
765 body["runId"] = json!(run_id);
766 let reported: Outcome<Value> = g1t_kit::call(&services.work, "report_run", &body).await?;
767 return match reported {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API768 Outcome::Ok(status) => reply(&json!({ "status": status })),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains769 Outcome::Fail(refused) => failure(&refused),
770 };
771 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API772 // What a run's agent learned, as memory candidates; the same token.
773 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/learned") => {
774 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/learned");
775 let body = json_body(&mut request).await;
776 let learned = json!({
777 "runId": run_id,
778 "token": body["token"].as_str().unwrap_or_default(),
779 "items": body["items"].as_array().cloned().unwrap_or_default(),
780 });
781 let captured: Outcome<Value> = g1t_kit::call(&services.work, "report_learned", &learned).await?;
782 return match captured {
783 Outcome::Ok(captured) => reply(&captured),
784 Outcome::Fail(refused) => failure(&refused),
785 };
786 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step787 // How sure a run's agent is of its change; the same token.
788 ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/confidence") => {
789 let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/confidence");
790 let body = json_body(&mut request).await;
791 let said = json!({
792 "runId": run_id,
793 "token": body["token"].as_str().unwrap_or_default(),
794 "confidence": body["confidence"].as_str().unwrap_or_default(),
795 "uncertainAbout": body["uncertain_about"]
796 .as_array()
797 .map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>())
798 .unwrap_or_default(),
799 });
800 let recorded: Outcome<Value> = g1t_kit::call(&services.work, "report_confidence", &said).await?;
801 return match recorded {
802 Outcome::Ok(recorded) => reply(&json!({ "recorded": recorded })),
803 Outcome::Fail(refused) => failure(&refused),
804 };
805 }
Keep g1t's own runs off the model proxy until it holds g1t's key806 ("POST", path) if path.starts_with("/checks/") => {
807 let run_id = path.trim_start_matches("/checks/").to_owned();
808 return report_checks(&mut request, &services, &run_id).await;
809 }
810 ("POST", path) if path.starts_with("/runs/") && path.ends_with("/usage") => {
811 let run_id = path
812 .trim_start_matches("/runs/")
813 .trim_end_matches("/usage")
814 .to_owned();
815 return report_usage(&mut request, &services, &run_id).await;
816 }
817 ("POST", path) if path.starts_with("/plans/") => {
818 let plan_id = path.trim_start_matches("/plans/").to_owned();
819 return report_plan(&mut request, &services, &plan_id).await;
820 }
821 ("POST", path) if path.starts_with("/reviews/") => {
822 let run_id = path.trim_start_matches("/reviews/").to_owned();
823 return report_review(&mut request, &services, &run_id).await;
824 }
825 _ => {}
826 }
827
828 let query: Vec<(String, String)> = url
829 .query_pairs()
830 .map(|(name, value)| (name.into_owned(), value.into_owned()))
831 .collect();
832 let body = if method == "GET" {
833 Value::Null
834 } else {
835 snake_case_keys(json_body(&mut request).await)
836 };
837 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
838 return fail(FailureCode::NotFound, "No such endpoint.");
839 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API840 match audit::run(route.op, &services, &viewer, &input).await? {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2841 // A download is a redirect to its signed link, as GitHub's is.
842 Outcome::Ok(value) if route.op == operations::Op::Artifacts(artifacts::ArtifactsOp::DownloadArtifact) => {
843 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
844 Some(url) => Response::redirect_with_status(url, 302),
845 None => reply(&value),
846 }
847 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts848 // A deleted comment has nothing to say, as GitHub's says nothing.
849 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 API850 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 step851 // A token without the scope a call needs is told which one.
852 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
853 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
854 "error": {
855 "code": refused.code,
856 "message": refused.message,
857 "needed_scope": scope.as_str(),
858 }
859 }))?
860 .with_status(403)),
861 _ => failure(&refused),
862 },
Keep g1t's own runs off the model proxy until it holds g1t's key863 }
864}
865
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API866/// Request bodies take the same keys as the MCP tools, `snake_case`, as
867/// responses use; the `camelCase` spelling is accepted too.
Keep g1t's own runs off the model proxy until it holds g1t's key868fn snake_case_keys(body: Value) -> Value {
869 let Value::Object(fields) = body else {
870 return body;
871 };
872 let mut out = serde_json::Map::new();
873 for (key, value) in fields {
874 let mut snake = String::with_capacity(key.len() + 4);
875 for c in key.chars() {
876 if c.is_ascii_uppercase() {
877 snake.push('_');
878 snake.push(c.to_ascii_lowercase());
879 } else {
880 snake.push(c);
881 }
882 }
883 // A key given in both spellings keeps the snake_case one.
884 if snake != key && out.contains_key(&snake) {
885 continue;
886 }
887 out.insert(snake, value);
888 }
889 Value::Object(out)
890}
891
892#[cfg(test)]
893mod tests {
894 use super::snake_case_keys;
895 use serde_json::json;
896
897 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)898 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
899 assert!(super::pending_may("GET", "/user", false));
900 assert!(super::pending_may("GET", "/user/emails/", false));
901 assert!(super::pending_may("POST", "/user/emails/confirm", false));
902 assert!(!super::pending_may("POST", "/user/emails", false));
903 assert!(!super::pending_may("POST", "/workspaces", false));
904 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
905 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
906 assert!(!super::pending_may("POST", "/", true));
907 }
908
909 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2910 fn a_job_spec_gets_the_toolkits_variables() {
911 // As the actions service sends it, converted as the API does.
912 let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true } });
913 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
914 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
915 assert!(spec.get("runtime").is_none(), "the runner never sees it");
916 let vars = &spec["variables"];
917 assert_eq!(vars["GITHUB_SHA"], "abc");
918 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
919 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
920 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
921 // No OIDC key here: no OIDC variables, whatever the job may do.
922 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
923 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
924 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
925 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
926 }
927
928 #[test]
Keep g1t's own runs off the model proxy until it holds g1t's key929 fn camel_case_keys_are_accepted() {
930 assert_eq!(
931 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
932 json!({ "count_agent_approvals": false, "title": "x" })
933 );
934 }
935
936 #[test]
937 fn snake_case_wins_when_both_are_given() {
938 assert_eq!(
939 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
940 json!({ "keep_issue_open": true })
941 );
942 }
943}
944
945// The API is called from browsers too: the reference's explorer, and apps
946// built on g1t. It carries no cookies, so any origin may call it.
947#[event(fetch)]
948async fn fetch(request: Request, env: Env, _ctx: Context) -> Result<Response> {
949 let mut response = respond(request, &env).await?;
950 let headers = response.headers_mut();
951 headers.set("access-control-allow-origin", "*")?;
952 headers.set(
953 "access-control-allow-headers",
954 "authorization, content-type",
955 )?;
956 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails957 headers.set("access-control-expose-headers", "www-authenticate, retry-after")?;
Keep g1t's own runs off the model proxy until it holds g1t's key958 Ok(response)
959}

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