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
892 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;
The API and MCP server coordinate work: start_session, update_session (an empty body is a heartbeat), end_session, get_session, list_sessions and record_session_entries at /repos/{owner}/{name}/sessions, as the new session tool's start, update, end, get, list and record; claim_issue, release_claim, hand_off_issue and get_work at /repos/{owner}/{name}/issues/{number}/claim, /release, /handoff and /work, and list_claims at /repos/{owner}/{name}/claims, as the issue tool's claim, release, handoff, work and list_claims; and update_issue takes blocked_by. Sessions need pull_requests scopes and claims and get_work issues scopes, reads are open to anyone who can read the repository, the reference has an example of each, and the OpenAPI copy and the CLI's table of operations are regenerated.17mod coordination;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9718mod 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.19mod telemetry;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca20mod deploy_keys;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails21mod limits;
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)22mod logs;
Fixes from the page sweep23mod mcp;
API: notifications over REST and MCP, with notifications scopes24mod notifications;
Fixes from the page sweep25mod oauth;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R226mod oidc;
Merge packages: roles, Actions access, source label, soft delete, API27mod packages;
Merge main into Artifacts Phase 228mod folios;
Merge main into stream A: coordination meets the merge queue's decisions and the CLI. get_work gives the issue's decision records newest first and its latest integration_failure decision as last_integration_failure, a merge queue failure noted in a pull request's session is noted in the live harness sessions on it or its issue too, the CLI's coordination commands use the generated operations now that the API describes them (its pending list is empty) and show get_work's definition of done, and the session, decision and coordination events sit together in the catalog, the webhook form and the docs.29mod follow;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar30mod people;
Fixes from the page sweep31mod openapi;
API: pinned projects over REST and MCP32mod pins;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9733mod projects;
Merge branch 'worktree-agent-a3abfcce648e87dca'34mod protection;
Fixes from the page sweep35mod operations;
36mod renamed;
37#[cfg(test)]
38mod responses;
39mod rest;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge40mod rules;
Fixes from the page sweep41mod runners;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar42mod security;
Fixes from the page sweep43mod tools;
Merge main into stream A: coordination meets the merge queue's decisions and the CLI. get_work gives the issue's decision records newest first and its latest integration_failure decision as last_integration_failure, a merge queue failure noted in a pull request's session is noted in the live harness sessions on it or its issue too, the CLI's coordination commands use the generated operations now that the API describes them (its pending list is empty) and show get_work's definition of done, and the session, decision and coordination events sit together in the catalog, the webhook form and the docs.44mod why;
API and MCP for a workspace's personal access token rules, members' tokens and approvals45mod token_policy;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R246mod toolkit;
Fixes from the page sweep47
48use g1t_contracts::identity::{
49 DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs,
50};
51use 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.52 CheckRun, Mergeable, QueueState, ReportChecksArgs, ReportMergecheckArgs,
53 ReportQueueArgs,
Fixes from the page sweep54};
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)55use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, User, Viewer};
Fixes from the page sweep56use g1t_kit::wire;
57use serde_json::{Value, json};
58use worker::{Context, Env, Method, Request, Response, Result, event};
59
60use operations::Services;
61
62fn method_name(method: Method) -> &'static str {
63 match method {
64 Method::Get => "GET",
65 Method::Post => "POST",
66 Method::Patch => "PATCH",
67 Method::Put => "PUT",
68 Method::Delete => "DELETE",
69 Method::Options => "OPTIONS",
70 Method::Head => "HEAD",
71 _ => "OTHER",
72 }
73}
74
75/// A JSON response. Every body the API sends has its keys in `snake_case`;
76/// the contracts it passes through are `camelCase`, so they are converted
77/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
78/// the MCP protocol's own envelope keep the spelling their standards use.
79pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
80 Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
81}
82
83/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
84/// workflow file, GitHub's contexts and event, and where to check out, all
85/// passed through as they are.
86const JOB_SPEC_AS_GIVEN: &[&str] = &[
Merge branch 'worktree-agent-a3abfcce648e87dca'87 "spec", "workflow", "github", "event", "contexts", "checkout", "permissions",
Fixes from the page sweep88];
89
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R290/// Puts the toolkit's variables in a job's spec: its runtime token, where
91/// the toolkit's services are, and where to ask for an OIDC token when the
92/// job may have one and this installation issues them. The `runtime` the
93/// actions service sent goes no further.
94fn with_runtime(spec: &mut Value, api: &str, oidc: bool) {
95 let Some(runtime) = spec.as_object_mut().and_then(|s| s.remove("runtime")) else { return };
96 let Some(token) = runtime["token"].as_str().filter(|t| !t.is_empty()) else { return };
97 let id_token = oidc && runtime["id_token"].as_bool() == Some(true);
98 let vars = toolkit::runtime_variables(api, token, id_token);
99 if let Some(variables) = spec.get_mut("variables").and_then(Value::as_object_mut) {
100 variables.extend(vars);
101 }
102}
103
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)104/// What a person whose account has not confirmed its email address may
105/// call: who they are, their addresses, and confirming one with the code
106/// from the email. Nothing over MCP.
107fn pending_may(method: &str, path: &str, on_mcp: bool) -> bool {
108 !on_mcp
109 && matches!(
110 (method, path.trim_end_matches('/')),
111 ("GET", "/user") | ("GET", "/user/emails") | ("POST", "/user/emails/confirm")
112 )
113}
114
Fixes from the page sweep115/// An error in the shape every endpoint uses.
116fn failure(failure: &Failure) -> Result<Response> {
117 Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
118}
119
120fn fail(code: FailureCode, message: &str) -> Result<Response> {
121 failure(&Failure {
122 code,
123 message: message.to_owned(),
124 })
125}
126
127/// A request body as JSON. An empty or malformed body is no input.
128async fn json_body(request: &mut Request) -> Value {
129 request.json().await.unwrap_or(Value::Null)
130}
131
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'132/// Who a request's `Authorization: Bearer g1t_…` (or `token g1t_…`, as
133/// Octokit sends it) names. A missing token is an anonymous viewer; a
134/// wrong one is refused, so that a typo does not silently look signed out.
Fixes from the page sweep135async fn authenticate(
136 request: &Request,
137 services: &Services,
138) -> Result<std::result::Result<Viewer, Response>> {
139 let header = request.headers().get("authorization")?.unwrap_or_default();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'140 let Some(token) = limits::bearer(&header) else {
141 return Ok(Ok(None));
Fixes from the page sweep142 };
143 let viewer: Viewer = g1t_kit::call(
144 &services.identity,
145 "user_for_access_token",
146 &TokenArgs {
147 token: token.to_owned(),
148 },
149 )
150 .await?;
151 if viewer.is_some() {
152 return Ok(Ok(viewer));
153 }
154 let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?;
155 // Tells an MCP client where to sign in again.
156 response.headers_mut().set(
157 "www-authenticate",
Merge branch 'worktree-agent-aaf03bdceac799c89'158 &format!("{}, error=\"invalid_token\"", services.addresses.mcp_challenge()),
Fixes from the page sweep159 )?;
160 Ok(Err(response))
161}
162
163/// Where everything is, for someone or something exploring the API.
Merge branch 'worktree-agent-aaf03bdceac799c89'164fn index(addresses: &addresses::Addresses) -> Value {
165 let api = &addresses.api;
166 let repo = format!("{api}/repos/{{owner}}/{{name}}");
Fixes from the page sweep167 json!({
168 "documentation_url": "https://docs.g1t.sh/reference/api/",
Merge branch 'worktree-agent-aaf03bdceac799c89'169 "openapi_url": format!("{api}/openapi.json"),
170 "mcp_url": addresses.mcp,
171 "current_user_url": format!("{api}/user"),
172 "workspaces_url": format!("{api}/workspaces"),
173 "repositories_url": format!("{api}/repos{{?q}}"),
174 "search_url": format!("{api}/search{{?q,type,page,per_page}}"),
Fixes from the page sweep175 "repository_url": repo,
176 "repository_events_url": format!("{repo}/events{{?before}}"),
177 "labels_url": format!("{repo}/labels"),
178 "issues_url": format!("{repo}/issues{{?state,label}}"),
179 "issue_url": format!("{repo}/issues/{{number}}"),
180 "issue_comments_url": format!("{repo}/issues/{{number}}/comments"),
181 "pulls_url": format!("{repo}/pulls{{?state}}"),
182 "pull_url": format!("{repo}/pulls/{{number}}"),
183 "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"),
184 "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"),
185 "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"),
Merge branch 'worktree-agent-aaf03bdceac799c89'186 "device_code_url": format!("{api}/device/code"),
187 "device_token_url": format!("{api}/device/token"),
188 "oauth_metadata_url": format!("{api}/.well-known/oauth-authorization-server"),
189 "git_url": format!("{}/{{owner}}/{{name}}.git", addresses.site),
190 "integrations_url": format!("{api}/workspaces/{{workspace}}/integrations"),
Fixes from the page sweep191 "context_url": format!("{repo}/context{{?reference}}"),
192 "import_issue_url": format!("{repo}/issues/import"),
Merge branch 'worktree-agent-aaf03bdceac799c89'193 "hooks_url": format!("{api}/hooks/{{integration}}"),
Fixes from the page sweep194 })
195}
196
197// Signing in from a tool. Accounts are created, and passwords typed, only
198// in a browser; a tool gets its token by having a person approve a code.
199
200/// Passes a request from an outside system to its connection, as it came:
201/// its signature covers the exact bytes of the body.
202async fn receive_hook(request: &mut Request, services: &Services, id: &str) -> Result<Response> {
203 let headers: std::collections::HashMap<String, String> = request
204 .headers()
205 .entries()
206 .map(|(name, value)| (name.to_lowercase(), value))
207 .collect();
208 let body = request.text().await.unwrap_or_default();
209 // A push to GitHub with many commits makes a large payload.
210 let limit = if id == "github" { 10_000_000 } else { 1_000_000 };
211 if body.len() > limit {
212 return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
213 }
214 // g1t's GitHub App has one webhook for every installation; it is
215 // checked against the app's own secret.
216 let (method, args) = if id == "github" {
217 ("github_receive", json!({ "headers": headers, "body": body }))
218 } else {
219 ("receive", json!({ "id": id, "headers": headers, "body": body }))
220 };
221 let received: g1t_contracts::integrations::Received =
222 g1t_kit::call(&services.integrations, method, &args).await?;
223 Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
224}
225
226async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
227 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
228 let payload = request.text().await.unwrap_or_default();
229 if payload.len() > 1_000_000 || signature.is_empty() {
230 return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
231 }
232 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
233 &env.service("BILLING")?,
234 "stripe_webhook",
235 &g1t_contracts::billing::StripeWebhookArgs { payload, signature },
236 )
237 .await?;
238 // A refusal is a 400, so Stripe shows it as failed; anything handled,
239 // or already handled, is a 200, so Stripe stops sending it.
240 Ok(match handled {
241 g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
242 g1t_contracts::Outcome::Fail(failure) => {
243 reply(&json!({ "message": failure.message }))?.with_status(400)
244 }
245 })
246}
247
248async fn device_code(request: &mut Request, services: &Services) -> Result<Response> {
249 let body = json_body(request).await;
250 let started: DeviceStart = g1t_kit::call(
251 &services.identity,
252 "device_start",
253 &DeviceStartArgs {
254 client_name: body["client_name"].as_str().unwrap_or_default().to_owned(),
255 },
256 )
257 .await?;
258 reply(&json!({
259 "device_code": started.device_code,
260 "user_code": started.user_code,
Merge branch 'worktree-agent-aaf03bdceac799c89'261 "verification_uri": format!("{}/device", services.addresses.site),
262 "verification_uri_complete": format!("{}/device?code={}", services.addresses.site, started.user_code),
Fixes from the page sweep263 "expires_in": started.expires_in,
264 "interval": started.interval,
265 }))
266}
267
268async fn device_token(request: &mut Request, services: &Services) -> Result<Response> {
269 let body = json_body(request).await;
270 let claim: DeviceClaim = g1t_kit::call(
271 &services.identity,
272 "device_claim",
273 &DeviceClaimArgs {
274 device_code: body["device_code"].as_str().unwrap_or_default().to_owned(),
275 },
276 )
277 .await?;
278 reply(&match claim {
279 DeviceClaim::Approved { token, user } => json!({
280 "status": "approved",
281 "token": token,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar282 "display_username": user.display_username.clone().filter(|display| display.eq_ignore_ascii_case(&user.username)).unwrap_or_else(|| user.username.clone()),
Fixes from the page sweep283 "username": user.username,
284 "verified": user.verified,
285 }),
286 DeviceClaim::Pending => json!({ "status": "pending" }),
287 DeviceClaim::Denied => json!({ "status": "denied" }),
288 DeviceClaim::Expired => json!({ "status": "expired" }),
289 })
290}
291
292/// A sandbox reporting on a run of an issue's commands, from before a pull
293/// request's checks were the workflows run on it.
294/// The run's own token, in the body, is the credential: it was given to
295/// that sandbox and to nothing else.
296async fn report_checks(
297 request: &mut Request,
298 services: &Services,
299 run_id: &str,
300) -> Result<Response> {
301 let body = json_body(request).await;
302 let reported: Outcome<CheckRun> = g1t_kit::call(
303 &services.work,
304 "report_checks",
305 &ReportChecksArgs {
306 run_id: run_id.to_owned(),
307 token: body["token"].as_str().unwrap_or_default().to_owned(),
308 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
309 error: body["error"].as_str().map(str::to_owned),
310 skip: false,
311 },
312 )
313 .await?;
314 match reported {
315 Outcome::Ok(run) => reply(&json!({ "status": run.status })),
316 Outcome::Fail(refused) => failure(&refused),
317 }
318}
319
320/// A sandbox reporting one tested state of a merge queue. As with checks,
321/// the entry's own token is the credential.
322async fn report_queue(
323 request: &mut Request,
324 services: &Services,
325 entry_id: &str,
326) -> Result<Response> {
327 let body = json_body(request).await;
328 let reported: Outcome<QueueState> = g1t_kit::call(
329 &services.work,
330 "report_queue",
331 &ReportQueueArgs {
332 entry_id: entry_id.to_owned(),
333 token: body["token"].as_str().unwrap_or_default().to_owned(),
334 combined_commit: body["combinedCommit"].as_str().map(str::to_owned),
335 results: serde_json::from_value(body["results"].clone()).unwrap_or_default(),
336 error: body["error"].as_str().map(str::to_owned),
337 conflict_with: body["conflictWith"].as_u64().map(|n| n as u32),
338 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
339 },
340 )
341 .await?;
342 match reported {
343 Outcome::Ok(state) => reply(&json!({ "state": state })),
344 Outcome::Fail(refused) => failure(&refused),
345 }
346}
347
348/// A sandbox reporting whether a pull request merges cleanly. As with
349/// checks, the probe's own token is the credential.
350async fn report_mergecheck(
351 request: &mut Request,
352 services: &Services,
353 pull_id: &str,
354) -> Result<Response> {
355 let body = json_body(request).await;
356 let reported: Outcome<Mergeable> = g1t_kit::call(
357 &services.work,
358 "report_mergecheck",
359 &ReportMergecheckArgs {
360 pull_id: pull_id.to_owned(),
361 token: body["token"].as_str().unwrap_or_default().to_owned(),
362 conflicts: serde_json::from_value(body["conflicts"].clone()).unwrap_or_default(),
363 error: body["error"].as_str().map(str::to_owned),
364 },
365 )
366 .await?;
367 match reported {
368 Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
369 Outcome::Fail(refused) => failure(&refused),
370 }
371}
372
Merge branch 'worktree-agent-ac5b181a013e54348'373/// A backup's sandbox, passed on to the repos service, which holds the
374/// job (services/repos/src/backups.rs; the flow is in
375/// `g1t_contracts::backups`):
376///
377/// - `POST /backups/{job}/spec`: what to cut, and a read-only git credential
378/// - `PUT /backups/{job}/parts/{n}`: one part of the bundle, as bytes
379/// - `POST /backups/{job}/complete` with `{ refs, size, sha256, parts, fetched_bytes }`
380/// - `POST /backups/{job}/fail` with `{ error, fetched_bytes }`
381///
382/// Bodies are passed through as they are: snake_case already, and a
383/// bundle's refs are keyed by ref names, which must not be converted.
384async fn backup_job(request: &mut Request, services: &Services, method: &str, path: &str) -> Result<Response> {
385 use g1t_contracts::backups::TOKEN_HEADER;
386 let token = request.headers().get(TOKEN_HEADER)?.unwrap_or_default();
387 let rest = path.trim_start_matches("/backups/");
388 let (job, action) = rest.split_once('/').unwrap_or((rest, ""));
389 if job.is_empty() || token.is_empty() {
390 return fail(FailureCode::Unauthenticated, "A backup job's token is required.");
391 }
392 if method == "PUT" && action.starts_with("parts/") {
393 let bytes = request.bytes().await?;
394 if bytes.len() as u64 > g1t_contracts::backups::PART_BYTES {
395 return fail(FailureCode::Invalid, "A part holds 32 MiB at most.");
396 }
397 let headers = worker::Headers::new();
398 headers.set(TOKEN_HEADER, &token)?;
399 let mut init = worker::RequestInit::new();
400 init.with_method(Method::Put)
401 .with_headers(headers)
402 .with_body(Some(worker::js_sys::Uint8Array::from(bytes.as_slice()).into()));
403 let forwarded = Request::new_with_init(&format!("https://repos/backups/{job}/{action}"), &init)?;
404 let mut answered = services.repos.fetch_request(forwarded).await?;
405 return outcome_as_given(answered.json().await?);
406 }
407 let rpc = match (method, action) {
408 ("POST", "spec") => "backup_spec",
409 ("POST", "complete") => "backup_complete",
410 ("POST", "fail") => "backup_fail",
411 _ => return fail(FailureCode::NotFound, "No such endpoint."),
412 };
413 let mut body = json_body(request).await;
414 if !body.is_object() {
415 body = json!({});
416 }
417 body["job_id"] = json!(job);
418 body["token"] = json!(token);
419 let answered: Value = g1t_kit::call(&services.repos, rpc, &body).await?;
420 outcome_as_given(answered)
421}
422
423/// An `Outcome` from a service whose keys are already the API's: the value,
424/// or the failure in the shape every endpoint uses.
425fn outcome_as_given(answered: Value) -> Result<Response> {
426 match serde_json::from_value::<Outcome<Value>>(answered)? {
427 Outcome::Ok(value) => Response::from_json(&value),
Fixes from the page sweep428 Outcome::Fail(refused) => failure(&refused),
429 }
430}
431
432async fn respond(mut request: Request, env: &Env) -> Result<Response> {
433 let method = method_name(request.method());
434 if method == "OPTIONS" {
435 return Ok(Response::empty()?.with_status(204));
436 }
437 let url = request.url()?;
438 // Paths carry no version. An earlier form began with `/v1`, which is
439 // still accepted so that nothing already written against it breaks.
440 let path = match url.path().strip_prefix("/v1") {
441 Some(rest) if rest.is_empty() || rest.starts_with('/') => rest.to_owned(),
442 _ => url.path().to_owned(),
443 };
444 let mut services = Services::new(env)?;
Merge branch 'worktree-agent-aaf03bdceac799c89'445 // MCP is a host of its own hosted, and may be a path on this one
446 // self-hosted (addresses.rs).
447 let on_mcp = services.addresses.mcp_path(&url).is_some();
Fixes from the page sweep448
449 // Stripe reporting to billing. Signed with the secret of the endpoint
450 // billing registered; the body goes through exactly as received, since
451 // the signature covers its bytes.
452 if method == "POST" && !on_mcp && path == "/stripe/webhook" {
453 return receive_stripe(&mut request, env).await;
454 }
455
456 // Outside systems reporting to a connection. They sign what they send
457 // with the connection's own secret, which is not a g1t token, so this
458 // comes before anything that would read one.
459 if method == "POST" && !on_mcp
460 && let Some(id) = path.strip_prefix("/hooks/").filter(|id| !id.is_empty() && !id.contains('/')) {
461 return receive_hook(&mut request, &services, id).await;
462 }
463
464 // 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.465 // which is not a g1t token, or a container app's Worker reporting how
466 // long its container ran, with its own token. The body goes through as
467 // it is: it can carry a Worker's bundled code.
Fixes from the page sweep468 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.469 && let Some(rest) = path
470 .strip_prefix("/deployments/jobs/")
471 .map(|rest| format!("jobs/{rest}"))
472 .or_else(|| path.strip_prefix("/deployments/apps/").filter(|rest| rest.ends_with("/meter")).map(|rest| format!("apps/{rest}")))
Fixes from the page sweep473 {
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.474 let target = format!("https://deployments/{rest}");
Fixes from the page sweep475 let body = request.bytes().await?;
476 let headers = worker::Headers::new();
477 headers.set("content-type", "application/json")?;
478 let mut init = worker::RequestInit::new();
479 init.with_method(Method::Post)
480 .with_headers(headers)
481 .with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
482 let mut answer = env
483 .service("DEPLOYMENTS")?
484 .fetch_request(Request::new_with_init(&target, &init)?)
485 .await?;
486 // A fresh response: a fetched one's headers cannot be changed, and
487 // every response gets the API's own on the way out.
488 let status = answer.status_code();
489 let bytes = answer.bytes().await?;
490 let bytes = match serde_json::from_slice::<Value>(&bytes) {
491 Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
492 Err(_) => bytes,
493 };
494 return Ok(Response::from_bytes(bytes)?
495 .with_status(status)
496 .with_headers({
497 let headers = worker::Headers::new();
498 headers.set("content-type", "application/json")?;
499 headers
500 }));
501 }
502
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2503 // The services GitHub's toolkit calls from inside a job, with its
504 // runtime token, and the links they hand out (toolkit.rs).
505 if !on_mcp && method == "POST"
506 && let Some(rest) = path.strip_prefix("/twirp/")
507 {
508 let (service, rpc) = rest.split_once('/').unwrap_or((rest, ""));
509 let (service, rpc) = (service.to_owned(), rpc.to_owned());
510 return toolkit::twirp(request, env, &services, &service, &rpc).await;
511 }
512 if !on_mcp && let Some(rest) = path.strip_prefix("/actions/toolkit/_apis/artifactcache/") {
513 let rest = rest.to_owned();
514 return toolkit::cache_v1(request, env, &services, method, &rest).await;
515 }
516 if !on_mcp && let Some(token) = path.strip_prefix("/actions/toolkit/blobs/") {
517 let token = token.to_owned();
518 return toolkit::blob(request, env, &services, method, &token).await;
519 }
520 // g1t as an OIDC issuer for workflow jobs (oidc.rs).
521 if !on_mcp && method == "GET" && path.starts_with("/actions/oidc/") {
522 return oidc::handle(&request, env, &services, &path).await;
523 }
524
Fixes from the page sweep525 // A sandbox's artifacts and cache, with its job's token, which is not a
526 // g1t token either.
527 if !on_mcp
528 && let Some(rest) = path.strip_prefix("/actions/jobs/")
529 && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
530 {
531 let rest = rest.to_owned();
532 return blobs::for_job(request, env, &services, method, &rest).await;
533 }
Merge branch 'main' into rm-agents534 // A self-hosted job's request to a brokered secret's host, passed to
535 // g1t to be filled in (broker.rs). The job's token is the credential.
536 if !on_mcp
537 && method == "POST"
538 && let Some(job) = broker::job_of(&path)
539 {
540 let job = job.to_owned();
541 return broker::relay(request, &services, &job).await;
542 }
Fixes from the page sweep543
544 // A self-hosted runner, with a registration token or its own
545 // credential, neither of which is a g1t access token.
546 if method == "POST"
547 && !on_mcp
548 && path.starts_with("/runners/")
549 && let Some(response) = runners::handle(&mut request, &services, &path).await?
550 {
551 return Ok(response);
552 }
553
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails554 // Per token, or per address without one (limits.rs).
555 if let Some(limited) = limits::limited(&request, env, method, &path, on_mcp).await? {
556 return Ok(limited);
557 }
Fixes from the page sweep558 let viewer = match authenticate(&request, &services).await? {
559 Ok(viewer) => viewer,
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails560 Err(refused) => return Ok(limits::wrong_token(&request, env, on_mcp).await?.unwrap_or(refused)),
Fixes from the page sweep561 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)562 // A person who has not confirmed their email address: who they are,
563 // their addresses, and confirming one, nothing else (REST or MCP).
564 if viewer.as_ref().is_some_and(User::awaits_confirmation) && !pending_may(method, &path, on_mcp) {
565 return fail(
566 FailureCode::Forbidden,
567 &g1t_contracts::accounts::confirm_email_first(&services.addresses.site),
568 );
569 }
Fixes from the page sweep570 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.571 // A run's token: what it may do comes with it, on the composite
572 // identity identity resolved it to. Any other token of g1t's own
573 // principal (the agent tokens g1t no longer issues) does nothing.
Fixes from the page sweep574 if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
575 services.scope = Some(acting.scope.clone());
576 } 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.577 return fail(FailureCode::Unauthenticated, "Invalid access token.");
Fixes from the page sweep578 }
579 if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? {
580 return Ok(response);
581 }
582 if on_mcp {
583 return mcp::handle(request, &services, &viewer).await;
584 }
585
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)586 // Workflow logs to download: a run's as a zip, a job's as text (logs.rs).
587 if method == "GET" {
588 let text = url.query_pairs().any(|(name, value)| name == "format" && value == "text");
589 if let Some(wanted) = logs::wanted(&path, text) {
590 return logs::download(&services, &viewer, wanted).await;
591 }
592 }
593
Fixes from the page sweep594 match (method, path.trim_end_matches('/')) {
Merge branch 'worktree-agent-aaf03bdceac799c89'595 ("GET", "") => return reply(&index(&services.addresses)),
Fixes from the page sweep596 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2597 // One of a run's artifacts downloaded by name (the run's artifacts
598 // are listed by the REST route in rest.rs).
599 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts/") => {
Fixes from the page sweep600 let parts: Vec<&str> = path.trim_start_matches("/repos/").split('/').collect();
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2601 if let [owner, repo, "actions", "runs", run, "artifacts", name] = parts.as_slice() {
Merge branch 'worktree-agent-a3abfcce648e87dca'602 // A workflow job's token reaches its own repository only.
603 if viewer
604 .as_ref()
605 .and_then(|user| user.token.as_deref())
606 .is_some_and(|token| !token.reaches(&format!("{owner}/{repo}")))
607 {
608 return fail(FailureCode::NotFound, "No such run.");
609 }
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2610 return blobs::download(env, &services, &viewer, owner, repo, run, name).await;
Fixes from the page sweep611 }
612 }
613 ("POST", "/device/code") => return device_code(&mut request, &services).await,
614 ("POST", "/device/token") => return device_token(&mut request, &services).await,
615 // Where a pull request lives, for a tool that knows only its fork.
616 ("GET", path) if path.starts_with("/pulls/") && !path[7..].contains('/') => {
617 let located: Outcome<Value> = g1t_kit::call(
618 &services.work,
619 "locate_pull",
620 &json!({ "id": &path[7..], "viewer": viewer }),
621 )
622 .await?;
623 return match located {
624 Outcome::Ok(value) => reply(&value),
625 Outcome::Fail(refused) => failure(&refused),
626 };
627 }
628 ("POST", path) if path.starts_with("/mergechecks/") => {
629 let pull_id = path.trim_start_matches("/mergechecks/").to_owned();
630 return report_mergecheck(&mut request, &services, &pull_id).await;
631 }
Merge branch 'worktree-agent-ac5b181a013e54348'632 // A sandbox making a repository's nightly backup. The job's own
633 // token, in its header, is the credential.
634 (method, path) if path.starts_with("/backups/") => {
635 return backup_job(&mut request, &services, method, path).await;
636 }
Fixes from the page sweep637 ("POST", path) if path.starts_with("/queue/") => {
638 let entry_id = path.trim_start_matches("/queue/").to_owned();
639 return report_queue(&mut request, &services, &entry_id).await;
640 }
641 // A sandbox running a GitHub Actions job: fetching the job, and
642 // reporting how it goes. The job's own token is the credential.
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'643 // A sandbox downloading an action's archive that g1t keeps
644 // (actions' registry.rs). The job's own token is the credential.
645 ("POST", path) if path.starts_with("/actions/jobs/") && path.ends_with("/archive") => {
646 let job = path.trim_start_matches("/actions/jobs/").trim_end_matches("/archive").to_owned();
647 let body = json_body(&mut request).await;
648 let kept: Outcome<Value> = g1t_kit::call(
649 &services.actions,
650 "job_action_archive",
651 &json!({ "job": job, "token": body["token"], "report": body["report"] }),
652 )
653 .await?;
654 let kept = match kept {
655 Outcome::Ok(kept) => kept,
656 Outcome::Fail(refused) => return failure(&refused),
657 };
658 let bucket = env.bucket("ACTIONS_CACHE")?;
659 let object = kept["object"].as_str().unwrap_or_default();
660 let Some(stored) = bucket.get(object).execute().await? else {
661 return fail(FailureCode::NotFound, "The archive is gone.");
662 };
663 let Some(body) = stored.body() else {
664 return fail(FailureCode::NotFound, "The archive is gone.");
665 };
666 let mut response = Response::from_body(body.response_body()?)?;
667 response.headers_mut().set("content-type", "application/gzip")?;
668 return Ok(response);
669 }
Fixes from the page sweep670 ("POST", path) if path.starts_with("/actions/jobs/") => {
671 let rest = path.trim_start_matches("/actions/jobs/");
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts672 // `/action`: where to fetch another repository's action from (on
673 // g1t, with a read token for a private one, or GitHub).
674 let (job, method) = match (rest.strip_suffix("/spec"), rest.strip_suffix("/action")) {
675 (Some(job), _) => (job.to_owned(), "job_spec"),
676 (_, Some(job)) => (job.to_owned(), "job_action"),
677 _ => (rest.to_owned(), "job_report"),
Fixes from the page sweep678 };
679 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.680 // `rotate`: a harness that takes a new token with the spec and
681 // lets go of the one it asked with (plan.rs `job_spec`).
682 let rotate = method == "job_spec" && body["rotate"].as_bool() == Some(true);
Merge branch 'main' into rm-agents683 // `broker`: a self-hosted harness that passes its job's
684 // requests to brokered hosts through g1t (broker.rs).
685 let broker = method == "job_spec" && body["broker"].as_bool() == Some(true);
Fixes from the page sweep686 let answered: Outcome<Value> = g1t_kit::call(
687 &services.actions,
688 method,
Merge branch 'main' into rm-agents689 &json!({ "job": job, "token": body["token"], "report": body["report"], "rotate": rotate, "broker": broker }),
Fixes from the page sweep690 )
691 .await?;
692 return match answered {
693 // A job's spec is the workflow and its contexts as GitHub
694 // has them; only g1t's own keys around them are converted.
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2695 Outcome::Ok(value) => {
696 let mut spec = wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN);
697 with_runtime(&mut spec, &services.addresses.api, oidc::configured(env));
698 Response::from_json(&spec)
699 }
Fixes from the page sweep700 Outcome::Fail(refused) => failure(&refused),
701 };
702 }
703 ("POST", path) if path.starts_with("/checks/") => {
704 let run_id = path.trim_start_matches("/checks/").to_owned();
705 return report_checks(&mut request, &services, &run_id).await;
706 }
707 _ => {}
708 }
709
710 let query: Vec<(String, String)> = url
711 .query_pairs()
712 .map(|(name, value)| (name.into_owned(), value.into_owned()))
713 .collect();
714 let body = if method == "GET" {
715 Value::Null
716 } else {
717 snake_case_keys(json_body(&mut request).await)
718 };
719 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
720 return fail(FailureCode::NotFound, "No such endpoint.");
721 };
722 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.723 // A download is a redirect to its signed link, as GitHub's is: an
724 // artifact's, and a repository's tarball and zipball.
725 Outcome::Ok(value) if route.op.redirects() => {
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2726 match value["url"].as_str().and_then(|url| worker::Url::parse(url).ok()) {
727 Some(url) => Response::redirect_with_status(url, 302),
728 None => reply(&value),
729 }
730 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts731 // A deleted comment has nothing to say, as GitHub's says nothing.
732 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.733 Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, route.op.as_given())),
Fixes from the page sweep734 // A token without the scope a call needs is told which one.
735 Outcome::Fail(refused) => match (refused.code, audit::missing_scope(route.op, &viewer, &input)) {
736 (FailureCode::Forbidden, Some(scope)) => Ok(reply(&json!({
737 "error": {
738 "code": refused.code,
739 "message": refused.message,
740 "needed_scope": scope.as_str(),
741 }
742 }))?
743 .with_status(403)),
744 _ => failure(&refused),
745 },
746 }
747}
748
749/// Request bodies take the same keys as the MCP tools, `snake_case`, as
750/// responses use; the `camelCase` spelling is accepted too.
751fn snake_case_keys(body: Value) -> Value {
752 let Value::Object(fields) = body else {
753 return body;
754 };
755 let mut out = serde_json::Map::new();
756 for (key, value) in fields {
757 let mut snake = String::with_capacity(key.len() + 4);
758 for c in key.chars() {
759 if c.is_ascii_uppercase() {
760 snake.push('_');
761 snake.push(c.to_ascii_lowercase());
762 } else {
763 snake.push(c);
764 }
765 }
766 // A key given in both spellings keeps the snake_case one.
767 if snake != key && out.contains_key(&snake) {
768 continue;
769 }
770 out.insert(snake, value);
771 }
772 Value::Object(out)
773}
774
775#[cfg(test)]
776mod tests {
777 use super::snake_case_keys;
778 use serde_json::json;
779
780 #[test]
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)781 fn an_unconfirmed_account_may_only_see_itself_and_confirm_its_address() {
782 assert!(super::pending_may("GET", "/user", false));
783 assert!(super::pending_may("GET", "/user/emails/", false));
784 assert!(super::pending_may("POST", "/user/emails/confirm", false));
785 assert!(!super::pending_may("POST", "/user/emails", false));
786 assert!(!super::pending_may("POST", "/workspaces", false));
787 assert!(!super::pending_may("GET", "/repos/acme/rocket", false));
788 assert!(!super::pending_may("POST", "/user/emails/confirm", true));
789 assert!(!super::pending_may("POST", "/", true));
790 }
791
792 #[test]
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2793 fn a_job_spec_gets_the_toolkits_variables() {
794 // 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.795 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 R2796 let mut spec = g1t_kit::wire::snake_case_keeping(sent.clone(), super::JOB_SPEC_AS_GIVEN);
797 super::with_runtime(&mut spec, "https://api.g1t.sh", true);
798 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.799 // The token that replaces the one the harness asked with.
800 assert_eq!(spec["next_token"], "n");
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2801 let vars = &spec["variables"];
802 assert_eq!(vars["GITHUB_SHA"], "abc");
803 assert_eq!(vars["ACTIONS_RUNTIME_TOKEN"], "h.p.s");
804 assert_eq!(vars["ACTIONS_CACHE_URL"], "https://api.g1t.sh/actions/toolkit/");
805 assert_eq!(vars["ACTIONS_ID_TOKEN_REQUEST_TOKEN"], "h.p.s");
806 // No OIDC key here: no OIDC variables, whatever the job may do.
807 let mut spec = g1t_kit::wire::snake_case_keeping(sent, super::JOB_SPEC_AS_GIVEN);
808 super::with_runtime(&mut spec, "https://api.g1t.sh", false);
809 assert!(spec["variables"].get("ACTIONS_ID_TOKEN_REQUEST_URL").is_none());
810 assert_eq!(spec["variables"]["ACTIONS_RESULTS_URL"], "https://api.g1t.sh/");
811 }
812
813 #[test]
Fixes from the page sweep814 fn camel_case_keys_are_accepted() {
815 assert_eq!(
816 snake_case_keys(json!({ "countAgentApprovals": false, "title": "x" })),
817 json!({ "count_agent_approvals": false, "title": "x" })
818 );
819 }
820
821 #[test]
822 fn snake_case_wins_when_both_are_given() {
823 assert_eq!(
824 snake_case_keys(json!({ "keep_issue_open": true, "keepIssueOpen": false })),
825 json!({ "keep_issue_open": true })
826 );
827 }
828}
829
830// The API is called from browsers too: the reference's explorer, and apps
831// 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.832/// The signal an OTLP path sends (`/otlp/v1/traces`, `logs` or `metrics`),
833/// if it is one.
834fn otlp_signal(path: &str) -> Option<&'static str> {
835 let path = path.strip_prefix("/v1").filter(|rest| rest.starts_with('/')).unwrap_or(path);
836 match path.trim_end_matches('/') {
837 "/otlp/v1/traces" => Some("traces"),
838 "/otlp/v1/logs" => Some("logs"),
839 "/otlp/v1/metrics" => Some("metrics"),
840 _ => None,
841 }
842}
843
844/// OpenTelemetry sent to the API's own address: a self-hosted install's
845/// only door, and a second one hosted. It goes to the telemetry service as
846/// it came, which checks the project's ingest token itself (not a g1t
847/// token), and its answer comes back as it is, CORS headers included.
848async fn forward_otlp(mut request: Request, env: &Env, signal: &str) -> Result<Response> {
849 let method = request.method();
850 let headers = worker::Headers::new();
851 for name in ["authorization", "x-g1t-token", "content-type", "content-encoding"] {
852 if let Some(value) = request.headers().get(name)? {
853 headers.set(name, &value)?;
854 }
855 }
856 let mut init = worker::RequestInit::new();
857 init.with_method(method.clone()).with_headers(headers);
858 if method == Method::Post {
859 let body = request.bytes().await?;
860 init.with_body(Some(worker::js_sys::Uint8Array::from(body.as_slice()).into()));
861 }
862 let target = format!("https://telemetry/v1/{signal}");
863 let mut answer = env.service("TELEMETRY")?.fetch_request(Request::new_with_init(&target, &init)?).await?;
864 // A fresh response, with the service's headers: a fetched one's cannot
865 // be changed, and nothing should be added to them.
866 let status = answer.status_code();
867 let headers = worker::Headers::new();
868 for (name, value) in answer.headers().entries() {
869 headers.set(&name, &value)?;
870 }
871 let bytes = answer.bytes().await?;
872 let response = if bytes.is_empty() { Response::empty()? } else { Response::from_bytes(bytes)? };
873 Ok(response.with_status(status).with_headers(headers))
874}
875
Fixes from the page sweep876#[event(fetch)]
877async 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.878 // OTLP passes through untouched, before any g1t token is read.
879 if let Some(signal) = otlp_signal(&request.path()) {
880 return forward_otlp(request, &env, signal).await;
881 }
Fixes from the page sweep882 let mut response = respond(request, &env).await?;
883 let headers = response.headers_mut();
884 headers.set("access-control-allow-origin", "*")?;
885 headers.set(
886 "access-control-allow-headers",
887 "authorization, content-type",
888 )?;
889 headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?;
Merge branch 'worktree-agent-a8752162fea25f63f' into spend-guardrails890 headers.set("access-control-expose-headers", "www-authenticate, retry-after")?;
Fixes from the page sweep891 Ok(response)
892}

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