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

Fixes from the page sweep1//! The public API: REST at api.g1t.sh and the MCP server at mcp.g1t.sh.
2//!
3//! One Worker, two hostnames. Both are thin adapters over the same
4//! operations (see [`operations::Op`]), which call the services that own
5//! the data. This Worker holds none.
6
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb977mod about;
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.8mod run_artifacts;
Merge 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.9mod archives;
Merge branch 'worktree-agent-aaf03bdceac799c89'10mod addresses;
Fixes from the page sweep11mod alerts;
12mod audit;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit13mod billing;
Fixes from the page sweep14mod blobs;
Merge branch 'main' into rm-agents15mod broker;
Merge checks: statuses and check runs on every commit16mod checks;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9717mod 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.18mod telemetry;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca19mod deploy_keys;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails20mod limits;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)21mod logs;
Fixes from the page sweep22mod mcp;
API: notifications over REST and MCP, with notifications scopes23mod notifications;
Fixes from the page sweep24mod oauth;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R225mod oidc;
Merge packages: roles, Actions access, source label, soft delete, API26mod packages;
Merge main into Artifacts Phase 227mod folios;
Merge branch 'worktree-agent-ad531323a21f4c975'28mod follow;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar29mod people;
Fixes from the page sweep30mod openapi;
API: pinned projects over REST and MCP31mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9732mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'33mod protection;
Fixes from the page sweep34mod operations;
35mod renamed;
36#[cfg(test)]
37mod responses;
38mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge39mod rules;
Fixes from the page sweep40mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar41mod security;
Fixes from the page sweep42mod tools;
Merge branch 'worktree-agent-ab8033ab973f8d99a'43mod why;
API and MCP for a workspace's personal access token rules, members' tokens and approvals44mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R245mod toolkit;
Fixes from the page sweep46
47use g1t_contracts::identity::{
48 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
49};
50use 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.51 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs,
52 ReportQueueArgs,
Fixes from the page sweep53};
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)54use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
Fixes from the page sweep55use g1t_kit::wire;
56use serde_json::{Value, json};
57use worker::{Context, Env, Method, Request, Response, Result, event};
58
59use operations::Services;
60
61fn method_name(method: Method) -> &'static str {
62 match method {
63 Method::Get => "GET",
64 Method::Post => "POST",
65 Method::Patch => "PATCH",
66 Method::Put => "PUT",
67 Method::Delete => "DELETE",
68 Method::Options => "OPTIONS",
69 Method::Head => "HEAD",
70 _ => "OTHER",
71 }
72}
73
74/// A JSON response. Every body the API sends has its keys in `snake_case`;
75/// the contracts it passes through are `camelCase`, so they are converted
76/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
77/// the MCP protocol's own envelope keep the spelling their standards use.
78pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
79 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
80}
81
82/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
83/// workflow file, GitHub's contexts and event, and where to check out, all
84/// passed through as they are.
85const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'86 "spec", "workflow", "github", "event", "contexts", "checkout", "permissions",
Fixes from the page sweep87];
88
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R289/// Puts the toolkit's variables in a job's spec: its runtime token, where
90/// the toolkit's services are, and where to ask for an OIDC token when the
91/// job may have one and this installation issues them. The `runtime` the
92/// actions service sent goes no further.
93fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
94 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
95 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
96 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
97 let vars = toolkit::runtime_variables(api, token, id_token);
98 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
99 variables.extend(vars);
100 }
101}
102
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)103/// What a person whose account has not confirmed its email address may
104/// call: who they are, their addresses, and confirming one with the code
105/// from the email. Nothing over MCP.
106fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
107 !on_mcp
108 && matches!(
109 (method, path.trim_end_matches('/')),
110 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
111 )
112}
113
Fixes from the page sweep114/// An error in the shape every endpoint uses.
115fn failure(failure: &Failure) -> Result<Response> {
116 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
117}
118
119fn fail(code: FailureCode, message: &str) -> Result<Response> {
120 failure(&Failure {
121 code,
122 message: message.to_owned(),
123 })
124}
125
126/// A request body as JSON. An empty or malformed body is no input.
127async fn json_body(request: &mut Request) -> Value {
128 request.json().await.unwrap_or(Value::Null)
129}
130
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'131/// Who a request's `Authorization: Bearer g1t_…` (or `token g1t_…`, as
132/// Octokit sends it) names. A missing token is an anonymous viewer; a
133/// wrong one is refused, so that a typo does not silently look signed out.
Fixes from the page sweep134async fn authenticate(
135 request: &Request,
136 services: &Services,
137) -> Result<std::result::Result<Viewer, Response>> {
138 let header = request.headers().get("authorization")?.unwrap_or_default();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'139 let Some(token) = limits::bearer(&header) else {
140 return Ok(Ok(None));
Fixes from the page sweep141 };
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()),
Fixes from the page sweep158 )?;
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}}");
Fixes from the page sweep166 json!({
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}}"),
Fixes from the page sweep174 "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"),
Fixes from the page sweep190 "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}}"),
Fixes from the page sweep193 })
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();
208 // 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 {
211 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
212 }
213 // 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?;
222 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
223}
224
225async 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() {
229 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
230 }
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 {
240 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
241 g1t_contracts::Outcome::Fail(failure) => {
242 reply(&json!({ "message": failure.message }))?.with_status(400)
243 }
244 })
245}
246
247async 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?;
257 reply(&json!({
258 "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),
Fixes from the page sweep262 "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?;
277 reply(&match claim {
278 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()),
Fixes from the page sweep282 "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
291/// 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.
293/// 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 {
314 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
315 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),
337 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
338 },
339 )
340 .await?;
341 match reported {
342 Outcome::Ok(state) => reply(&json!({ "state": state })),
343 Outcome::Fail(refused) => failure(&refused),
344 }
345}
346
347/// 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 {
367 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
368 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),
Fixes from the page sweep427 Outcome::Fail(refused) => failure(&refused),
428 }
429}
430
431async fn respond(mut request: Request, env: &Env) -> Result<Response> {
432 let method = method_name(request.method());
433 if method == "OPTIONS" {
434 return Ok(Response::empty()?.with_status(204));
435 }
436 let url = request.url()?;
437 // Paths carry no version. An earlier form began with `/v1`, which is
438 // still accepted so that nothing already written against it breaks.
439 let path = match url.path().strip_prefix("/v1") {
440 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
441 _ => url.path().to_owned(),
442 };
443 let mut services = Services::new(env)?;
Merge branch 'worktree-agent-aaf03bdceac799c89'444 // MCP is a host of its own hosted, and may be a path on this one
445 // self-hosted (addresses.rs).
446 let on_mcp = services.addresses.mcp_path(&url).is_some();
Fixes from the page sweep447
448 // Stripe reporting to billing. Signed with the secret of the endpoint
449 // billing registered; the body goes through exactly as received, since
450 // the signature covers its bytes.
451 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
452 return receive_stripe(&mut request, env).await;
453 }
454
455 // Outside systems reporting to a connection. They sign what they send
456 // with the connection's own secret, which is not a g1t token, so this
457 // comes before anything that would read one.
458 if method == "POST" && !on_mcp
459 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
460 return receive_hook(&mut request, &services, id).await;
461 }
462
463 // 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.464 // which is not a g1t token, or a container app's Worker reporting how
465 // long its container ran, with its own token. The body goes through as
466 // it is: it can carry a Worker's bundled code.
Fixes from the page sweep467 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.468 && let Some(rest) = path
469 .strip_prefix("/deployments/jobs/")
470 .map(|rest| format!("jobs/{rest}"))
471 .or_else(|| path.strip_prefix("/deployments/apps/").filter(|rest| rest.ends_with("/meter")).map(|rest| format!("apps/{rest}")))
Fixes from the page sweep472 {
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 target = format!("https://deployments/{rest}");
Fixes from the page sweep474 let body = request.bytes().await?;
475 let headers = worker::Headers::new();
476 headers.set("content-type", "application/json")?;
477 let mut init = worker::RequestInit::new();
478 init.with_method(Method::Post)
479 .with_headers(headers)
480 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
481 let mut answer = env
482 .service("DEPLOYMENTS")?
483 .fetch_request(Request::new_with_init(&target, &init)?)
484 .await?;
485 // A fresh response: a fetched one's headers cannot be changed, and
486 // every response gets the API's own on the way out.
487 let status = answer.status_code();
488 let bytes = answer.bytes().await?;
489 let bytes = match serde_json::from_slice::<Value>(&bytes) {
490 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
491 Err(_) => bytes,
492 };
493 return Ok(Response::from_bytes(bytes)?
494 .with_status(status)
495 .with_headers({
496 let headers = worker::Headers::new();
497 headers.set("content-type", "application/json")?;
498 headers
499 }));
500 }
501
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2502 // The services GitHub's toolkit calls from inside a job, with its
503 // runtime token, and the links they hand out (toolkit.rs).
504 if !on_mcp && method == "POST"
505 && let Some(rest) = path.strip_prefix("/twirp/")
506 {
507 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
508 let (service, rpc) = (service.to_owned(), rpc.to_owned());
509 return toolkit::twirp(request, env, &services, &service, &rpc).await;
510 }
511 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
512 let rest = rest.to_owned();
513 return toolkit::cache_v1(request, env, &services, method, &rest).await;
514 }
515 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
516 let token = token.to_owned();
517 return toolkit::blob(request, env, &services, method, &token).await;
518 }
519 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
520 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
521 return oidc::handle(&request, env, &services, &path).await;
522 }
523
Fixes from the page sweep524 // A sandbox's artifacts and cache, with its job's token, which is not a
525 // g1t token either.
526 if !on_mcp
527 && let Some(rest) = path.strip_prefix("/actions/jobs/")
528 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
529 {
530 let rest = rest.to_owned();
531 return blobs::for_job(request, env, &services, method, &rest).await;
532 }
Merge branch 'main' into rm-agents533 // A self-hosted job's request to a brokered secret's host, passed to
534 // g1t to be filled in (broker.rs). The job's token is the credential.
535 if !on_mcp
536 && method == "POST"
537 && let Some(job) = broker::job_of(&path)
538 {
539 let job = job.to_owned();
540 return broker::relay(request, &services, &job).await;
541 }
Fixes from the page sweep542
543 // A self-hosted runner, with a registration token or its own
544 // credential, neither of which is a g1t access token.
545 if method == "POST"
546 && !on_mcp
547 && path.starts_with("/runners/")
548 && let Some(response) = runners::handle(&mut request, &services, &path).await?
549 {
550 return Ok(response);
551 }
552
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails553 // Per token, or per address without one (limits.rs).
554 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
555 return Ok(limited);
556 }
Fixes from the page sweep557 let viewer = match authenticate(&request, &services).await? {
558 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails559 Err(refused) => return Ok(limits::wrong_token(&request, env, on_mcp).await?.unwrap_or(refused)),
Fixes from the page sweep560 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)561 // A person who has not confirmed their email address: who they are,
562 // their addresses, and confirming one, nothing else (REST or MCP).
563 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
564 return fail(
565 FailureCode::Forbidden,
566 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
567 );
568 }
Fixes from the page sweep569 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.570 // A run's token: what it may do comes with it, on the composite
571 // identity identity resolved it to. Any other token of g1t's own
572 // principal (the agent tokens g1t no longer issues) does nothing.
Fixes from the page sweep573 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
574 services.scope = Some(acting.scope.clone());
575 } 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.576 return fail(FailureCode::Unauthenticated, "Invalid access token.");
Fixes from the page sweep577 }
578 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
579 return Ok(response);
580 }
581 if on_mcp {
582 return mcp::handle(request, &services, &viewer).await;
583 }
584
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)585 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
586 if method == "GET" {
587 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
588 if let Some(wanted) = logs::wanted(&path, text) {
589 return logs::download(&services, &viewer, wanted).await;
590 }
591 }
592
Fixes from the page sweep593 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'594 ("GET", "") => return reply(&index(&services.addresses)),
Fixes from the page sweep595 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2596 // One of a run's artifacts downloaded by name (the run's artifacts
597 // are listed by the REST route in rest.rs).
598 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts/") => {
Fixes from the page sweep599 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2600 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'601 // A workflow job's token reaches its own repository only.
602 if viewer
603 .as_ref()
604 .and_then(|user| user.token.as_deref())
605 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
606 {
607 return fail(FailureCode::NotFound, "No such run.");
608 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2609 return blobs::download(env, &services, &viewer, owner, repo, run, name).await;
Fixes from the page sweep610 }
611 }
612 ("POST", "/device/code") => return device_code(&mut request, &services).await,
613 ("POST", "/device/token") => return device_token(&mut request, &services).await,
614 // Where a pull request lives, for a tool that knows only its fork.
615 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
616 let located: Outcome<Value> = g1t_kit::call(
617 &services.work,
618 "locate_pull",
619 &json!({ "id": &path[7..], "viewer": viewer }),
620 )
621 .await?;
622 return match located {
623 Outcome::Ok(value) => reply(&value),
624 Outcome::Fail(refused) => failure(&refused),
625 };
626 }
627 ("POST", path) if path.starts_with("/mergechecks/") => {
628 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
629 return report_mergecheck(&mut request, &services, &pull_id).await;
630 }
Merge branch 'worktree-agent-ac5b181a013e54348'631 // A sandbox making a repository's nightly backup. The job's own
632 // token, in its header, is the credential.
633 (method, path) if path.starts_with("/backups/") => {
634 return backup_job(&mut request, &services, method, path).await;
635 }
Fixes from the page sweep636 ("POST", path) if path.starts_with("/queue/") => {
637 let entry_id = path.trim_start_matches("/queue/").to_owned();
638 return report_queue(&mut request, &services, &entry_id).await;
639 }
640 // A sandbox running a GitHub Actions job: fetching the job, and
641 // reporting how it goes. The job's own token is the credential.
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'642 // A sandbox downloading an action's archive that g1t keeps
643 // (actions' registry.rs). The job's own token is the credential.
644 ("POST", path) if path.starts_with("/actions/jobs/") && path.ends_with("/archive") => {
645 let job = path.trim_start_matches("/actions/jobs/").trim_end_matches("/archive").to_owned();
646 let body = json_body(&mut request).await;
647 let kept: Outcome<Value> = g1t_kit::call(
648 &services.actions,
649 "job_action_archive",
650 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
651 )
652 .await?;
653 let kept = match kept {
654 Outcome::Ok(kept) => kept,
655 Outcome::Fail(refused) => return failure(&refused),
656 };
657 let bucket = env.bucket("ACTIONS_CACHE")?;
658 let object = kept["object"].as_str().unwrap_or_default();
659 let Some(stored) = bucket.get(object).execute().await? else {
660 return fail(FailureCode::NotFound, "The archive is gone.");
661 };
662 let Some(body) = stored.body() else {
663 return fail(FailureCode::NotFound, "The archive is gone.");
664 };
665 let mut response = Response::from_body(body.response_body()?)?;
666 response.headers_mut().set("content-type", "application/gzip")?;
667 return Ok(response);
668 }
Fixes from the page sweep669 ("POST", path) if path.starts_with("/actions/jobs/") => {
670 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts671 // `/action`: where to fetch another repository's action from (on
672 // g1t, with a read token for a private one, or GitHub).
673 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
674 (Some(job), _) => (job.to_owned(), "job_spec"),
675 (_, Some(job)) => (job.to_owned(), "job_action"),
676 _ => (rest.to_owned(), "job_report"),
Fixes from the page sweep677 };
678 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.679 // `rotate`: a harness that takes a new token with the spec and
680 // lets go of the one it asked with (plan.rs `job_spec`).
681 let rotate = method == "job_spec" && body["rotate"].as_bool() == Some(true);
Merge branch 'main' into rm-agents682 // `broker`: a self-hosted harness that passes its job's
683 // requests to brokered hosts through g1t (broker.rs).
684 let broker = method == "job_spec" && body["broker"].as_bool() == Some(true);
Fixes from the page sweep685 let answered: Outcome<Value> = g1t_kit::call(
686 &services.actions,
687 method,
Merge branch 'main' into rm-agents688 &json!({ "job": job, "token": body["token"], "report": body["report"], "rotate": rotate, "broker": broker }),
Fixes from the page sweep689 )
690 .await?;
691 return match answered {
692 // A job's spec is the workflow and its contexts as GitHub
693 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2694 Outcome::Ok(value) => {
695 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
696 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
697 Response::from_json(&spec)
698 }
Fixes from the page sweep699 Outcome::Fail(refused) => failure(&refused),
700 };
701 }
702 ("POST", path) if path.starts_with("/checks/") => {
703 let run_id = path.trim_start_matches("/checks/").to_owned();
704 return report_checks(&mut request, &services, &run_id).await;
705 }
706 _ => {}
707 }
708
709 let query: Vec<(String, String)> = url
710 .query_pairs()
711 .map(|(name, value)| (name.into_owned(), value.into_owned()))
712 .collect();
713 let body = if method == "GET" {
714 Value::Null
715 } else {
716 snake_case_keys(json_body(&mut request).await)
717 };
718 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
719 return fail(FailureCode::NotFound, "No such endpoint.");
720 };
721 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.722 // A download is a redirect to its signed link, as GitHub's is: an
723 // artifact's, and a repository's tarball and zipball.
724 Outcome::Ok(value) if route.op.redirects() => {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2725 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
726 Some(url) => Response::redirect_with_status(url, 302),
727 None => reply(&value),
728 }
729 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts730 // A deleted comment has nothing to say, as GitHub's says nothing.
731 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.732 Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, route.op.as_given())),
Fixes from the page sweep733 // A token without the scope a call needs is told which one.
734 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
735 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
736 "error": {
737 "code": refused.code,
738 "message": refused.message,
739 "needed_scope": scope.as_str(),
740 }
741 }))?
742 .with_status(403)),
743 _ => failure(&refused),
744 },
745 }
746}
747
748/// Request bodies take the same keys as the MCP tools, `snake_case`, as
749/// responses use; the `camelCase` spelling is accepted too.
750fn snake_case_keys(body: Value) -> Value {
751 let Value::Object(fields) = body else {
752 return body;
753 };
754 let mut out = serde_json::Map::new();
755 for (key, value) in fields {
756 let mut snake = String::with_capacity(key.len() + 4);
757 for c in key.chars() {
758 if c.is_ascii_uppercase() {
759 snake.push('_');
760 snake.push(c.to_ascii_lowercase());
761 } else {
762 snake.push(c);
763 }
764 }
765 // A key given in both spellings keeps the snake_case one.
766 if snake != key && out.contains_key(&snake) {
767 continue;
768 }
769 out.insert(snake, value);
770 }
771 Value::Object(out)
772}
773
774#[cfg(test)]
775mod tests {
776 use super::snake_case_keys;
777 use serde_json::json;
778
779 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)780 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
781 assert!(super::pending_may("GET", "/user", false));
782 assert!(super::pending_may("GET", "/user/emails/", false));
783 assert!(super::pending_may("POST", "/user/emails/confirm", false));
784 assert!(!super::pending_may("POST", "/user/emails", false));
785 assert!(!super::pending_may("POST", "/workspaces", false));
786 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
787 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
788 assert!(!super::pending_may("POST", "/", true));
789 }
790
791 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2792 fn a_job_spec_gets_the_toolkits_variables() {
793 // 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.794 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 R2795 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
796 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
797 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.798 // The token that replaces the one the harness asked with.
799 assert_eq!(spec["next_token"], "n");
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2800 let vars = &spec["variables"];
801 assert_eq!(vars["GITHUB_SHA"], "abc");
802 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
803 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
804 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
805 // No OIDC key here: no OIDC variables, whatever the job may do.
806 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
807 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
808 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
809 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
810 }
811
812 #[test]
Fixes from the page sweep813 fn camel_case_keys_are_accepted() {
814 assert_eq!(
815 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
816 json!({ "count_agent_approvals": false, "title": "x" })
817 );
818 }
819
820 #[test]
821 fn snake_case_wins_when_both_are_given() {
822 assert_eq!(
823 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
824 json!({ "keep_issue_open": true })
825 );
826 }
827}
828
829// The API is called from browsers too: the reference's explorer, and apps
830// 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.831/// The signal an OTLP path sends (`/otlp/v1/traces`, `logs` or `metrics`),
832/// if it is one.
833fn otlp_signal(path: &str) -> Option<&'static str> {
834 let path = path.strip_prefix("/v1").filter(|rest| rest.starts_with('/')).unwrap_or(path);
835 match path.trim_end_matches('/') {
836 "/otlp/v1/traces" => Some("traces"),
837 "/otlp/v1/logs" => Some("logs"),
838 "/otlp/v1/metrics" => Some("metrics"),
839 _ => None,
840 }
841}
842
843/// OpenTelemetry sent to the API's own address: a self-hosted install's
844/// only door, and a second one hosted. It goes to the telemetry service as
845/// it came, which checks the project's ingest token itself (not a g1t
846/// token), and its answer comes back as it is, CORS headers included.
847async fn forward_otlp(mut request: Request, env: &Env, signal: &str) -> Result<Response> {
848 let method = request.method();
849 let headers = worker::Headers::new();
850 for name in ["authorization", "x-g1t-token", "content-type", "content-encoding"] {
851 if let Some(value) = request.headers().get(name)? {
852 headers.set(name, &value)?;
853 }
854 }
855 let mut init = worker::RequestInit::new();
856 init.with_method(method.clone()).with_headers(headers);
857 if method == Method::Post {
858 let body = request.bytes().await?;
859 init.with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
860 }
861 let target = format!("https://telemetry/v1/{signal}");
862 let mut answer = env.service("TELEMETRY")?.fetch_request(Request::new_with_init(&target, &init)?).await?;
863 // A fresh response, with the service's headers: a fetched one's cannot
864 // be changed, and nothing should be added to them.
865 let status = answer.status_code();
866 let headers = worker::Headers::new();
867 for (name, value) in answer.headers().entries() {
868 headers.set(&name, &value)?;
869 }
870 let bytes = answer.bytes().await?;
871 let response = if bytes.is_empty() { Response::empty()? } else { Response::from_bytes(bytes)? };
872 Ok(response.with_status(status).with_headers(headers))
873}
874
Fixes from the page sweep875#[event(fetch)]
876async 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.877 // OTLP passes through untouched, before any g1t token is read.
878 if let Some(signal) = otlp_signal(&request.path()) {
879 return forward_otlp(request, &env, signal).await;
880 }
Fixes from the page sweep881 let mut response = respond(request, &env).await?;
882 let headers = response.headers_mut();
883 headers.set("access-control-allow-origin", "*")?;
884 headers.set(
885 "access-control-allow-headers",
886 "authorization, content-type",
887 )?;
888 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails889 headers.set("access-control-expose-headers", "www-authenticate, retry-after")?;
Fixes from the page sweep890 Ok(response)
891}

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