Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
896 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.

sudo: discounts listed among what was given away1//! 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;
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.8mod activity;
9mod commits;
10mod contents;
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.11mod run_artifacts;
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'12mod caches;
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.13mod archives;
sudo: discounts listed among what was given away14mod addresses;
15mod alerts;
16mod audit;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit17mod billing;
sudo: discounts listed among what was given away18mod blobs;
Merge branch 'main' into rm-agents19mod broker;
Merge checks: statuses and check runs on every commit20mod checks;
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'21mod coordination;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9722mod deployments;
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.23mod telemetry;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca24mod deploy_keys;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails25mod limits;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)26mod logs;
sudo: discounts listed among what was given away27mod mcp;
API: notifications over REST and MCP, with notifications scopes28mod notifications;
sudo: discounts listed among what was given away29mod oauth;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R230mod oidc;
Merge packages: roles, Actions access, source label, soft delete, API31mod packages;
Merge main into Artifacts Phase 232mod folios;
Merge branch 'worktree-agent-ad531323a21f4c975'33mod follow;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar34mod people;
sudo: discounts listed among what was given away35mod openapi;
API: pinned projects over REST and MCP36mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9737mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'38mod protection;
sudo: discounts listed among what was given away39mod operations;
40mod renamed;
41#[cfg(test)]
42mod responses;
43mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge44mod rules;
sudo: discounts listed among what was given away45mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar46mod security;
sudo: discounts listed among what was given away47mod tools;
Merge branch 'worktree-agent-ab8033ab973f8d99a'48mod why;
API and MCP for a workspace's personal access token rules, members' tokens and approvals49mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R250mod toolkit;
sudo: discounts listed among what was given away51
52use g1t_contracts::identity::{
53 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
54};
55use g1t_contracts::work::{
The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused.56 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs,
57 ReportQueueArgs,
sudo: discounts listed among what was given away58};
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)59use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
sudo: discounts listed among what was given away60use g1t_kit::wire;
61use serde_json::{Value, json};
62use worker::{Context, Env, Method, Request, Response, Result, event};
63
64use operations::Services;
65
66fn method_name(method: Method) -> &'static str {
67 match method {
68 Method::Get => "GET",
69 Method::Post => "POST",
70 Method::Patch => "PATCH",
71 Method::Put => "PUT",
72 Method::Delete => "DELETE",
73 Method::Options => "OPTIONS",
74 Method::Head => "HEAD",
75 _ => "OTHER",
76 }
77}
78
79/// A JSON response. Every body the API sends has its keys in `snake_case`;
80/// the contracts it passes through are `camelCase`, so they are converted
81/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
82/// the MCP protocol's own envelope keep the spelling their standards use.
83pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
84 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
85}
86
87/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
88/// workflow file, GitHub's contexts and event, and where to check out, all
89/// passed through as they are.
90const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'91 "spec", "workflow", "github", "event", "contexts", "checkout", "permissions",
sudo: discounts listed among what was given away92];
93
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R294/// Puts the toolkit's variables in a job's spec: its runtime token, where
95/// the toolkit's services are, and where to ask for an OIDC token when the
96/// job may have one and this installation issues them. The `runtime` the
97/// actions service sent goes no further.
98fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
99 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
100 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
101 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
102 let vars = toolkit::runtime_variables(api, token, id_token);
103 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
104 variables.extend(vars);
105 }
106}
107
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)108/// What a person whose account has not confirmed its email address may
109/// call: who they are, their addresses, and confirming one with the code
110/// from the email. Nothing over MCP.
111fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
112 !on_mcp
113 && matches!(
114 (method, path.trim_end_matches('/')),
115 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
116 )
117}
118
sudo: discounts listed among what was given away119/// An error in the shape every endpoint uses.
120fn failure(failure: &Failure) -> Result<Response> {
121 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
122}
123
124fn fail(code: FailureCode, message: &str) -> Result<Response> {
125 failure(&Failure {
126 code,
127 message: message.to_owned(),
128 })
129}
130
131/// A request body as JSON. An empty or malformed body is no input.
132async fn json_body(request: &mut Request) -> Value {
133 request.json().await.unwrap_or(Value::Null)
134}
135
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'136/// Who a request's `Authorization: Bearer g1t_…` (or `token g1t_…`, as
137/// Octokit sends it) names. A missing token is an anonymous viewer; a
138/// wrong one is refused, so that a typo does not silently look signed out.
sudo: discounts listed among what was given away139async fn authenticate(
140 request: &Request,
141 services: &Services,
142) -> Result<std::result::Result<Viewer, Response>> {
143 let header = request.headers().get("authorization")?.unwrap_or_default();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'144 let Some(token) = limits::bearer(&header) else {
145 return Ok(Ok(None));
sudo: discounts listed among what was given away146 };
147 let viewer: Viewer = g1t_kit::call(
148 &services.identity,
149 "user_for_access_token",
150 &TokenArgs {
151 token: token.to_owned(),
152 },
153 )
154 .await?;
155 if viewer.is_some() {
156 return Ok(Ok(viewer));
157 }
158 let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?;
159 // Tells an MCP client where to sign in again.
160 response.headers_mut().set(
161 "www-authenticate",
162 &format!("{}, error=\"invalid_token\"", services.addresses.mcp_challenge()),
163 )?;
164 Ok(Err(response))
165}
166
167/// Where everything is, for someone or something exploring the API.
168fn index(addresses: &addresses::Addresses) -> Value {
169 let api = &addresses.api;
170 let repo = format!("{api}/repos/{{owner}}/{{name}}");
171 json!({
172 "documentation_url": "https://docs.g1t.sh/reference/api/",
173 "openapi_url": format!("{api}/openapi.json"),
174 "mcp_url": addresses.mcp,
175 "current_user_url": format!("{api}/user"),
176 "workspaces_url": format!("{api}/workspaces"),
177 "repositories_url": format!("{api}/repos{{?q}}"),
178 "search_url": format!("{api}/search{{?q,type,page,per_page}}"),
179 "repository_url": repo,
180 "repository_events_url": format!("{repo}/events{{?before}}"),
181 "labels_url": format!("{repo}/labels"),
182 "issues_url": format!("{repo}/issues{{?state,label}}"),
183 "issue_url": format!("{repo}/issues/{{number}}"),
184 "issue_comments_url": format!("{repo}/issues/{{number}}/comments"),
185 "pulls_url": format!("{repo}/pulls{{?state}}"),
186 "pull_url": format!("{repo}/pulls/{{number}}"),
187 "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"),
188 "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"),
189 "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"),
190 "device_code_url": format!("{api}/device/code"),
191 "device_token_url": format!("{api}/device/token"),
192 "oauth_metadata_url": format!("{api}/.well-known/oauth-authorization-server"),
193 "git_url": format!("{}/{{owner}}/{{name}}.git", addresses.site),
194 "integrations_url": format!("{api}/workspaces/{{workspace}}/integrations"),
195 "context_url": format!("{repo}/context{{?reference}}"),
196 "import_issue_url": format!("{repo}/issues/import"),
197 "hooks_url": format!("{api}/hooks/{{integration}}"),
198 })
199}
200
201// Signing in from a tool. Accounts are created, and passwords typed, only
202// in a browser; a tool gets its token by having a person approve a code.
203
204/// Passes a request from an outside system to its connection, as it came:
205/// its signature covers the exact bytes of the body.
206async fn receive_hook(request: &mut Request, services: &Services, id: &str) -> Result<Response> {
207 let headers: std::collections::HashMap<String, String> = request
208 .headers()
209 .entries()
210 .map(|(name, value)| (name.to_lowercase(), value))
211 .collect();
212 let body = request.text().await.unwrap_or_default();
213 // A push to GitHub with many commits makes a large payload.
214 let limit = if id == "github" { 10_000_000 } else { 1_000_000 };
215 if body.len() > limit {
216 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
217 }
218 // g1t's GitHub App has one webhook for every installation; it is
219 // checked against the app's own secret.
220 let (method, args) = if id == "github" {
221 ("github_receive", json!({ "headers": headers, "body": body }))
222 } else {
223 ("receive", json!({ "id": id, "headers": headers, "body": body }))
224 };
225 let received: g1t_contracts::integrations::Received =
226 g1t_kit::call(&services.integrations, method, &args).await?;
227 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
228}
229
230async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
231 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
232 let payload = request.text().await.unwrap_or_default();
233 if payload.len() > 1_000_000 || signature.is_empty() {
234 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
235 }
236 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
237 &env.service("BILLING")?,
238 "stripe_webhook",
239 &g1t_contracts::billing::StripeWebhookArgs { payload, signature },
240 )
241 .await?;
242 // A refusal is a 400, so Stripe shows it as failed; anything handled,
243 // or already handled, is a 200, so Stripe stops sending it.
244 Ok(match handled {
245 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
246 g1t_contracts::Outcome::Fail(failure) => {
247 reply(&json!({ "message": failure.message }))?.with_status(400)
248 }
249 })
250}
251
252async fn device_code(request: &mut Request, services: &Services) -> Result<Response> {
253 let body = json_body(request).await;
254 let started: DeviceStart = g1t_kit::call(
255 &services.identity,
256 "device_start",
257 &DeviceStartArgs {
258 client_name: body["client_name"].as_str().unwrap_or_default().to_owned(),
259 },
260 )
261 .await?;
262 reply(&json!({
263 "device_code": started.device_code,
264 "user_code": started.user_code,
265 "verification_uri": format!("{}/device", services.addresses.site),
266 "verification_uri_complete": format!("{}/device?code={}", services.addresses.site, started.user_code),
267 "expires_in": started.expires_in,
268 "interval": started.interval,
269 }))
270}
271
272async fn device_token(request: &mut Request, services: &Services) -> Result<Response> {
273 let body = json_body(request).await;
274 let claim: DeviceClaim = g1t_kit::call(
275 &services.identity,
276 "device_claim",
277 &DeviceClaimArgs {
278 device_code: body["device_code"].as_str().unwrap_or_default().to_owned(),
279 },
280 )
281 .await?;
282 reply(&match claim {
283 DeviceClaim::Approved { token, user } => json!({
284 "status": "approved",
285 "token": token,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar286 "display_username": user.display_username.clone().filter(|display| display.eq_ignore_ascii_case(&user.username)).unwrap_or_else(|| user.username.clone()),
sudo: discounts listed among what was given away287 "username": user.username,
288 "verified": user.verified,
289 }),
290 DeviceClaim::Pending => json!({ "status": "pending" }),
291 DeviceClaim::Denied => json!({ "status": "denied" }),
292 DeviceClaim::Expired => json!({ "status": "expired" }),
293 })
294}
295
296/// A sandbox reporting on a run of an issue's commands, from before a pull
297/// request's checks were the workflows run on it.
298/// The run's own token, in the body, is the credential: it was given to
299/// that sandbox and to nothing else.
300async fn report_checks(
301 request: &mut Request,
302 services: &Services,
303 run_id: &str,
304) -> Result<Response> {
305 let body = json_body(request).await;
306 let reported: Outcome<CheckRun> = g1t_kit::call(
307 &services.work,
308 "report_checks",
309 &ReportChecksArgs {
310 run_id: run_id.to_owned(),
311 token: body["token"].as_str().unwrap_or_default().to_owned(),
312 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
313 error: body["error"].as_str().map(str::to_owned),
314 skip: false,
315 },
316 )
317 .await?;
318 match reported {
319 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
320 Outcome::Fail(refused) => failure(&refused),
321 }
322}
323
324/// A sandbox reporting one tested state of a merge queue. As with checks,
325/// the entry's own token is the credential.
326async fn report_queue(
327 request: &mut Request,
328 services: &Services,
329 entry_id: &str,
330) -> Result<Response> {
331 let body = json_body(request).await;
332 let reported: Outcome<QueueState> = g1t_kit::call(
333 &services.work,
334 "report_queue",
335 &ReportQueueArgs {
336 entry_id: entry_id.to_owned(),
337 token: body["token"].as_str().unwrap_or_default().to_owned(),
338 combined_commit: body["combinedCommit"].as_str().map(str::to_owned),
339 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
340 error: body["error"].as_str().map(str::to_owned),
341 conflict_with: body["conflictWith"].as_u64().map(|n| n as u32),
342 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
343 },
344 )
345 .await?;
346 match reported {
347 Outcome::Ok(state) => reply(&json!({ "state": state })),
348 Outcome::Fail(refused) => failure(&refused),
349 }
350}
351
352/// A sandbox reporting whether a pull request merges cleanly. As with
353/// checks, the probe's own token is the credential.
354async fn report_mergecheck(
355 request: &mut Request,
356 services: &Services,
357 pull_id: &str,
358) -> Result<Response> {
359 let body = json_body(request).await;
360 let reported: Outcome<Mergeable> = g1t_kit::call(
361 &services.work,
362 "report_mergecheck",
363 &ReportMergecheckArgs {
364 pull_id: pull_id.to_owned(),
365 token: body["token"].as_str().unwrap_or_default().to_owned(),
366 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
367 error: body["error"].as_str().map(str::to_owned),
368 },
369 )
370 .await?;
371 match reported {
372 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
373 Outcome::Fail(refused) => failure(&refused),
374 }
375}
376
377/// A backup's sandbox, passed on to the repos service, which holds the
378/// job (services/repos/src/backups.rs; the flow is in
379/// `g1t_contracts::backups`):
380///
381/// - `POST /backups/{job}/spec`: what to cut, and a read-only git credential
382/// - `PUT /backups/{job}/parts/{n}`: one part of the bundle, as bytes
383/// - `POST /backups/{job}/complete` with `{ refs, size, sha256, parts, fetched_bytes }`
384/// - `POST /backups/{job}/fail` with `{ error, fetched_bytes }`
385///
386/// Bodies are passed through as they are: snake_case already, and a
387/// bundle's refs are keyed by ref names, which must not be converted.
388async fn backup_job(request: &mut Request, services: &Services, method: &str, path: &str) -> Result<Response> {
389 use g1t_contracts::backups::TOKEN_HEADER;
390 let token = request.headers().get(TOKEN_HEADER)?.unwrap_or_default();
391 let rest = path.trim_start_matches("/backups/");
392 let (job, action) = rest.split_once('/').unwrap_or((rest, ""));
393 if job.is_empty() || token.is_empty() {
394 return fail(FailureCode::Unauthenticated, "A backup job's token is required.");
395 }
396 if method == "PUT" && action.starts_with("parts/") {
397 let bytes = request.bytes().await?;
398 if bytes.len() as u64 > g1t_contracts::backups::PART_BYTES {
399 return fail(FailureCode::Invalid, "A part holds 32 MiB at most.");
400 }
401 let headers = worker::Headers::new();
402 headers.set(TOKEN_HEADER, &token)?;
403 let mut init = worker::RequestInit::new();
404 init.with_method(Method::Put)
405 .with_headers(headers)
406 .with_body(Some(worker::js_sys::Uint8Array::from(bytes.as_slice()).into()));
407 let forwarded = Request::new_with_init(&format!("https://repos/backups/{job}/{action}"), &init)?;
408 let mut answered = services.repos.fetch_request(forwarded).await?;
409 return outcome_as_given(answered.json().await?);
410 }
411 let rpc = match (method, action) {
412 ("POST", "spec") => "backup_spec",
413 ("POST", "complete") => "backup_complete",
414 ("POST", "fail") => "backup_fail",
415 _ => return fail(FailureCode::NotFound, "No such endpoint."),
416 };
417 let mut body = json_body(request).await;
418 if !body.is_object() {
419 body = json!({});
420 }
421 body["job_id"] = json!(job);
422 body["token"] = json!(token);
423 let answered: Value = g1t_kit::call(&services.repos, rpc, &body).await?;
424 outcome_as_given(answered)
425}
426
427/// An `Outcome` from a service whose keys are already the API's: the value,
428/// or the failure in the shape every endpoint uses.
429fn outcome_as_given(answered: Value) -> Result<Response> {
430 match serde_json::from_value::<Outcome<Value>>(answered)? {
431 Outcome::Ok(value) => Response::from_json(&value),
432 Outcome::Fail(refused) => failure(&refused),
433 }
434}
435
436async fn respond(mut request: Request, env: &Env) -> Result<Response> {
437 let method = method_name(request.method());
438 if method == "OPTIONS" {
439 return Ok(Response::empty()?.with_status(204));
440 }
441 let url = request.url()?;
442 // Paths carry no version. An earlier form began with `/v1`, which is
443 // still accepted so that nothing already written against it breaks.
444 let path = match url.path().strip_prefix("/v1") {
445 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
446 _ => url.path().to_owned(),
447 };
448 let mut services = Services::new(env)?;
449 // MCP is a host of its own hosted, and may be a path on this one
450 // self-hosted (addresses.rs).
451 let on_mcp = services.addresses.mcp_path(&url).is_some();
452
453 // Stripe reporting to billing. Signed with the secret of the endpoint
454 // billing registered; the body goes through exactly as received, since
455 // the signature covers its bytes.
456 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
457 return receive_stripe(&mut request, env).await;
458 }
459
460 // Outside systems reporting to a connection. They sign what they send
461 // with the connection's own secret, which is not a g1t token, so this
462 // comes before anything that would read one.
463 if method == "POST" && !on_mcp
464 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
465 return receive_hook(&mut request, &services, id).await;
466 }
467
468 // A sandbox building a deployment, reporting with its build's token,
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.469 // which is not a g1t token, or a container app's Worker reporting how
470 // long its container ran, with its own token. The body goes through as
471 // it is: it can carry a Worker's bundled code.
sudo: discounts listed among what was given away472 if method == "POST" && !on_mcp
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.473 && let Some(rest) = path
474 .strip_prefix("/deployments/jobs/")
475 .map(|rest| format!("jobs/{rest}"))
476 .or_else(|| path.strip_prefix("/deployments/apps/").filter(|rest| rest.ends_with("/meter")).map(|rest| format!("apps/{rest}")))
sudo: discounts listed among what was given away477 {
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.478 let target = format!("https://deployments/{rest}");
sudo: discounts listed among what was given away479 let body = request.bytes().await?;
480 let headers = worker::Headers::new();
481 headers.set("content-type", "application/json")?;
482 let mut init = worker::RequestInit::new();
483 init.with_method(Method::Post)
484 .with_headers(headers)
485 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
486 let mut answer = env
487 .service("DEPLOYMENTS")?
488 .fetch_request(Request::new_with_init(&target, &init)?)
489 .await?;
490 // A fresh response: a fetched one's headers cannot be changed, and
491 // every response gets the API's own on the way out.
492 let status = answer.status_code();
493 let bytes = answer.bytes().await?;
494 let bytes = match serde_json::from_slice::<Value>(&bytes) {
495 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
496 Err(_) => bytes,
497 };
498 return Ok(Response::from_bytes(bytes)?
499 .with_status(status)
500 .with_headers({
501 let headers = worker::Headers::new();
502 headers.set("content-type", "application/json")?;
503 headers
504 }));
505 }
506
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2507 // The services GitHub's toolkit calls from inside a job, with its
508 // runtime token, and the links they hand out (toolkit.rs).
509 if !on_mcp && method == "POST"
510 && let Some(rest) = path.strip_prefix("/twirp/")
511 {
512 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
513 let (service, rpc) = (service.to_owned(), rpc.to_owned());
514 return toolkit::twirp(request, env, &services, &service, &rpc).await;
515 }
516 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
517 let rest = rest.to_owned();
518 return toolkit::cache_v1(request, env, &services, method, &rest).await;
519 }
520 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
521 let token = token.to_owned();
522 return toolkit::blob(request, env, &services, method, &token).await;
523 }
524 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
525 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
526 return oidc::handle(&request, env, &services, &path).await;
527 }
528
sudo: discounts listed among what was given away529 // A sandbox's artifacts and cache, with its job's token, which is not a
530 // g1t token either.
531 if !on_mcp
532 && let Some(rest) = path.strip_prefix("/actions/jobs/")
533 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
534 {
535 let rest = rest.to_owned();
536 return blobs::for_job(request, env, &services, method, &rest).await;
537 }
Merge branch 'main' into rm-agents538 // A self-hosted job's request to a brokered secret's host, passed to
539 // g1t to be filled in (broker.rs). The job's token is the credential.
540 if !on_mcp
541 && method == "POST"
542 && let Some(job) = broker::job_of(&path)
543 {
544 let job = job.to_owned();
545 return broker::relay(request, &services, &job).await;
546 }
sudo: discounts listed among what was given away547
548 // A self-hosted runner, with a registration token or its own
549 // credential, neither of which is a g1t access token.
550 if method == "POST"
551 && !on_mcp
552 && path.starts_with("/runners/")
553 && let Some(response) = runners::handle(&mut request, &services, &path).await?
554 {
555 return Ok(response);
556 }
557
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails558 // Per token, or per address without one (limits.rs).
559 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
560 return Ok(limited);
561 }
sudo: discounts listed among what was given away562 let viewer = match authenticate(&request, &services).await? {
563 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails564 Err(refused) => return Ok(limits::wrong_token(&request, env, on_mcp).await?.unwrap_or(refused)),
sudo: discounts listed among what was given away565 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)566 // A person who has not confirmed their email address: who they are,
567 // their addresses, and confirming one, nothing else (REST or MCP).
568 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
569 return fail(
570 FailureCode::Forbidden,
571 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
572 );
573 }
sudo: discounts listed among what was given away574 services.audit = audit::AuditContext::of(&request, on_mcp);
The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused.575 // A run's token: what it may do comes with it, on the composite
576 // identity identity resolved it to. Any other token of g1t's own
577 // principal (the agent tokens g1t no longer issues) does nothing.
sudo: discounts listed among what was given away578 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
579 services.scope = Some(acting.scope.clone());
580 } else if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
The API and MCP server no longer offer g1t's own agents. g1t is what your coding agents work against, not an agent of its own, so the operations that drove hosted agents are gone: messaging an agent on a pull request (message_agent, take_messages, answer_message), assigning or delegating an issue to g1t (assign_issue, delegate), plans (plan_work, get_plan, apply_plan), the context hub (search_context, get_entity), model routes, agents' computers, AI Gateway requests, "Fix with g1t" on a security alert, and every Customize route (marketplaces, plugins, connectors). The callbacks the agent sandbox reported through (/agent-runs/*, /runs/:id/usage, /plans/:id, /reviews/:id) go with them, update_repo_settings stops accepting agent_review, max_revisions and hold_low_confidence, import_issue stops accepting assign, and an old agent token with no run behind it is refused.581 return fail(FailureCode::Unauthenticated, "Invalid access token.");
sudo: discounts listed among what was given away582 }
583 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
584 return Ok(response);
585 }
586 if on_mcp {
587 return mcp::handle(request, &services, &viewer).await;
588 }
589
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)590 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
591 if method == "GET" {
592 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
593 if let Some(wanted) = logs::wanted(&path, text) {
594 return logs::download(&services, &viewer, wanted).await;
595 }
596 }
597
sudo: discounts listed among what was given away598 match (method, path.trim_end_matches('/')) {
599 ("GET", "") => return reply(&index(&services.addresses)),
600 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2601 // One of a run's artifacts downloaded by name (the run's artifacts
602 // are listed by the REST route in rest.rs).
603 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts/") => {
sudo: discounts listed among what was given away604 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2605 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'606 // A workflow job's token reaches its own repository only.
607 if viewer
608 .as_ref()
609 .and_then(|user| user.token.as_deref())
610 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
611 {
612 return fail(FailureCode::NotFound, "No such run.");
613 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2614 return blobs::download(env, &services, &viewer, owner, repo, run, name).await;
sudo: discounts listed among what was given away615 }
616 }
617 ("POST", "/device/code") => return device_code(&mut request, &services).await,
618 ("POST", "/device/token") => return device_token(&mut request, &services).await,
619 // Where a pull request lives, for a tool that knows only its fork.
620 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
621 let located: Outcome<Value> = g1t_kit::call(
622 &services.work,
623 "locate_pull",
624 &json!({ "id": &path[7..], "viewer": viewer }),
625 )
626 .await?;
627 return match located {
628 Outcome::Ok(value) => reply(&value),
629 Outcome::Fail(refused) => failure(&refused),
630 };
631 }
632 ("POST", path) if path.starts_with("/mergechecks/") => {
633 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
634 return report_mergecheck(&mut request, &services, &pull_id).await;
635 }
636 // A sandbox making a repository's nightly backup. The job's own
637 // token, in its header, is the credential.
638 (method, path) if path.starts_with("/backups/") => {
639 return backup_job(&mut request, &services, method, path).await;
640 }
641 ("POST", path) if path.starts_with("/queue/") => {
642 let entry_id = path.trim_start_matches("/queue/").to_owned();
643 return report_queue(&mut request, &services, &entry_id).await;
644 }
645 // A sandbox running a GitHub Actions job: fetching the job, and
646 // reporting how it goes. The job's own token is the credential.
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'647 // A sandbox downloading an action's archive that g1t keeps
648 // (actions' registry.rs). The job's own token is the credential.
649 ("POST", path) if path.starts_with("/actions/jobs/") && path.ends_with("/archive") => {
650 let job = path.trim_start_matches("/actions/jobs/").trim_end_matches("/archive").to_owned();
651 let body = json_body(&mut request).await;
652 let kept: Outcome<Value> = g1t_kit::call(
653 &services.actions,
654 "job_action_archive",
655 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
656 )
657 .await?;
658 let kept = match kept {
659 Outcome::Ok(kept) => kept,
660 Outcome::Fail(refused) => return failure(&refused),
661 };
662 let bucket = env.bucket("ACTIONS_CACHE")?;
663 let object = kept["object"].as_str().unwrap_or_default();
664 let Some(stored) = bucket.get(object).execute().await? else {
665 return fail(FailureCode::NotFound, "The archive is gone.");
666 };
667 let Some(body) = stored.body() else {
668 return fail(FailureCode::NotFound, "The archive is gone.");
669 };
670 let mut response = Response::from_body(body.response_body()?)?;
671 response.headers_mut().set("content-type", "application/gzip")?;
672 return Ok(response);
673 }
sudo: discounts listed among what was given away674 ("POST", path) if path.starts_with("/actions/jobs/") => {
675 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts676 // `/action`: where to fetch another repository's action from (on
677 // g1t, with a read token for a private one, or GitHub).
678 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
679 (Some(job), _) => (job.to_owned(), "job_spec"),
680 (_, Some(job)) => (job.to_owned(), "job_action"),
681 _ => (rest.to_owned(), "job_report"),
sudo: discounts listed among what was given away682 };
683 let body = json_body(&mut request).await;
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.684 // `rotate`: a harness that takes a new token with the spec and
685 // lets go of the one it asked with (plan.rs `job_spec`).
686 let rotate = method == "job_spec" && body["rotate"].as_bool() == Some(true);
Merge branch 'main' into rm-agents687 // `broker`: a self-hosted harness that passes its job's
688 // requests to brokered hosts through g1t (broker.rs).
689 let broker = method == "job_spec" && body["broker"].as_bool() == Some(true);
sudo: discounts listed among what was given away690 let answered: Outcome<Value> = g1t_kit::call(
691 &services.actions,
692 method,
Merge branch 'main' into rm-agents693 &json!({ "job": job, "token": body["token"], "report": body["report"], "rotate": rotate, "broker": broker }),
sudo: discounts listed among what was given away694 )
695 .await?;
696 return match answered {
697 // A job's spec is the workflow and its contexts as GitHub
698 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2699 Outcome::Ok(value) => {
700 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
701 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
702 Response::from_json(&spec)
703 }
sudo: discounts listed among what was given away704 Outcome::Fail(refused) => failure(&refused),
705 };
706 }
707 ("POST", path) if path.starts_with("/checks/") => {
708 let run_id = path.trim_start_matches("/checks/").to_owned();
709 return report_checks(&mut request, &services, &run_id).await;
710 }
711 _ => {}
712 }
713
714 let query: Vec<(String, String)> = url
715 .query_pairs()
716 .map(|(name, value)| (name.into_owned(), value.into_owned()))
717 .collect();
718 let body = if method == "GET" {
719 Value::Null
720 } else {
721 snake_case_keys(json_body(&mut request).await)
722 };
723 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
724 return fail(FailureCode::NotFound, "No such endpoint.");
725 };
726 match audit::run(route.op, &services, &viewer, &input).await? {
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.727 // A download is a redirect to its signed link, as GitHub's is: an
728 // artifact's, and a repository's tarball and zipball.
729 Outcome::Ok(value) if route.op.redirects() => {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2730 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
731 Some(url) => Response::redirect_with_status(url, 302),
732 None => reply(&value),
733 }
734 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts735 // A deleted comment has nothing to say, as GitHub's says nothing.
736 Outcome::Ok(_) if route.no_content() => Ok(Response::empty()?.with_status(204)),
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.737 Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, route.op.as_given())),
sudo: discounts listed among what was given away738 // A token without the scope a call needs is told which one.
739 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
740 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
741 "error": {
742 "code": refused.code,
743 "message": refused.message,
744 "needed_scope": scope.as_str(),
745 }
746 }))?
747 .with_status(403)),
748 _ => failure(&refused),
749 },
750 }
751}
752
753/// Request bodies take the same keys as the MCP tools, `snake_case`, as
754/// responses use; the `camelCase` spelling is accepted too.
755fn snake_case_keys(body: Value) -> Value {
756 let Value::Object(fields) = body else {
757 return body;
758 };
759 let mut out = serde_json::Map::new();
760 for (key, value) in fields {
761 let mut snake = String::with_capacity(key.len() + 4);
762 for c in key.chars() {
763 if c.is_ascii_uppercase() {
764 snake.push('_');
765 snake.push(c.to_ascii_lowercase());
766 } else {
767 snake.push(c);
768 }
769 }
770 // A key given in both spellings keeps the snake_case one.
771 if snake != key && out.contains_key(&snake) {
772 continue;
773 }
774 out.insert(snake, value);
775 }
776 Value::Object(out)
777}
778
779#[cfg(test)]
780mod tests {
781 use super::snake_case_keys;
782 use serde_json::json;
783
784 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)785 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
786 assert!(super::pending_may("GET", "/user", false));
787 assert!(super::pending_may("GET", "/user/emails/", false));
788 assert!(super::pending_may("POST", "/user/emails/confirm", false));
789 assert!(!super::pending_may("POST", "/user/emails", false));
790 assert!(!super::pending_may("POST", "/workspaces", false));
791 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
792 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
793 assert!(!super::pending_may("POST", "/", true));
794 }
795
796 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2797 fn a_job_spec_gets_the_toolkits_variables() {
798 // As the actions service sends it, converted as the API does.
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.799 let sent = json!({ "variables": { "GITHUB_SHA": "abc" }, "runtime": { "token": "h.p.s", "idToken": true }, "nextToken": "n" });
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2800 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
801 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
802 assert!(spec.get("runtime").is_none(), "the runner never sees it");
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.803 // The token that replaces the one the harness asked with.
804 assert_eq!(spec["next_token"], "n");
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2805 let vars = &spec["variables"];
806 assert_eq!(vars["GITHUB_SHA"], "abc");
807 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
808 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
809 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
810 // No OIDC key here: no OIDC variables, whatever the job may do.
811 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
812 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
813 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
814 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
815 }
816
817 #[test]
sudo: discounts listed among what was given away818 fn camel_case_keys_are_accepted() {
819 assert_eq!(
820 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
821 json!({ "count_agent_approvals": false, "title": "x" })
822 );
823 }
824
825 #[test]
826 fn snake_case_wins_when_both_are_given() {
827 assert_eq!(
828 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
829 json!({ "keep_issue_open": true })
830 );
831 }
832}
833
834// The API is called from browsers too: the reference's explorer, and apps
835// built on g1t. It carries no cookies, so any origin may call it.
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.836/// The signal an OTLP path sends (`/otlp/v1/traces`, `logs` or `metrics`),
837/// if it is one.
838fn otlp_signal(path: &str) -> Option<&'static str> {
839 let path = path.strip_prefix("/v1").filter(|rest| rest.starts_with('/')).unwrap_or(path);
840 match path.trim_end_matches('/') {
841 "/otlp/v1/traces" => Some("traces"),
842 "/otlp/v1/logs" => Some("logs"),
843 "/otlp/v1/metrics" => Some("metrics"),
844 _ => None,
845 }
846}
847
848/// OpenTelemetry sent to the API's own address: a self-hosted install's
849/// only door, and a second one hosted. It goes to the telemetry service as
850/// it came, which checks the project's ingest token itself (not a g1t
851/// token), and its answer comes back as it is, CORS headers included.
852async fn forward_otlp(mut request: Request, env: &Env, signal: &str) -> Result<Response> {
853 let method = request.method();
854 let headers = worker::Headers::new();
855 for name in ["authorization", "x-g1t-token", "content-type", "content-encoding"] {
856 if let Some(value) = request.headers().get(name)? {
857 headers.set(name, &value)?;
858 }
859 }
860 let mut init = worker::RequestInit::new();
861 init.with_method(method.clone()).with_headers(headers);
862 if method == Method::Post {
863 let body = request.bytes().await?;
864 init.with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
865 }
866 let target = format!("https://telemetry/v1/{signal}");
867 let mut answer = env.service("TELEMETRY")?.fetch_request(Request::new_with_init(&target, &init)?).await?;
868 // A fresh response, with the service's headers: a fetched one's cannot
869 // be changed, and nothing should be added to them.
870 let status = answer.status_code();
871 let headers = worker::Headers::new();
872 for (name, value) in answer.headers().entries() {
873 headers.set(&name, &value)?;
874 }
875 let bytes = answer.bytes().await?;
876 let response = if bytes.is_empty() { Response::empty()? } else { Response::from_bytes(bytes)? };
877 Ok(response.with_status(status).with_headers(headers))
878}
879
sudo: discounts listed among what was given away880#[event(fetch)]
881async fn fetch(request: Request, env: Env, _ctx: Context) -> Result<Response> {
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.882 // OTLP passes through untouched, before any g1t token is read.
883 if let Some(signal) = otlp_signal(&request.path()) {
884 return forward_otlp(request, &env, signal).await;
885 }
sudo: discounts listed among what was given away886 let mut response = respond(request, &env).await?;
887 let headers = response.headers_mut();
888 headers.set("access-control-allow-origin", "*")?;
889 headers.set(
890 "access-control-allow-headers",
891 "authorization, content-type",
892 )?;
893 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails894 headers.set("access-control-expose-headers", "www-authenticate, retry-after")?;
sudo: discounts listed among what was given away895 Ok(response)
896}

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