flagon-io/g1t

public

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

Commit

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily

- Artifacts: every interaction metered by type with the billable mapping as data, hour-long credentials, PR forks retired a week after close, streamed pushes with size limits, retries and a breaker, a Git storage status part, refs-version caches, coalesced mergeability. - Speed: read methods no longer look like writes, so public pages cache and replicas serve; the pull request page reads in one batch; the overview streams. - Security: likely test values never block or count as critical; dismiss and reopen with reasons and an activity log; security updates opened by g1t itself as pull requests; large pushes scanned after they land. - A g1t system identity for platform work. - Costs: Cloudflare's bill ingested daily and reconciled with our meters and revenue; a versioned price book with proposals; margin alerts; free workspaces never billed for scans or embeddings. - Truth audit: about 95 claims corrected across pricing, policies, docs and llms.txt; hosted models only for listed workspaces while payments are in test mode; push-to-create is private; model tokens revoked when a run ends; update_workspace.

syntaqxcommitted Parentdc7a94fBrowse files
192 files+1167−4210/192 viewed
+2−0
10071007 "base64 0.22.1",
10081008 "ed25519-dalek",
10091009 "g1t-actions",
1010+ "g1t-scan",
10101011 "hex",
10111012 "serde",
10121013 "serde_json",
10621063 "getrandom 0.2.17",
10631064 "serde",
10641065 "serde_json",
1066+ "serde_yaml",
10651067 "worker",
10661068 ]
10671069
+41−17
2222 - **Secure and healthy.** Push protection, history scanning, dependency
2323 upkeep that an agent lands, and an audit log on every workspace.
2424 - **Open and fair.** MIT licensed and self-hostable (an early Docker Compose
25− version of the core forge). The forge is free; compute is what it costs
25+ version of the core forge, in `deploy/self-host`). The forge is free; compute is what it costs
2626 plus 20%, never per seat.
2727
2828 g1t is made by Flagon, Inc. It is also an entry in Cloudflare's **Build the
6969 verdicts, from people and from agents.
7070 - Overlap: each pull request shows which others in progress change the
7171 same files, while the work is still going on.
72−- Catch-up: when `main` has moved under a pull request, a g1t agent merges
73− it in and resolves any conflict.
72+- Catch-up: when `main` has moved under a pull request, g1t merges it in,
73+ and a g1t agent resolves any conflict.
7474 - Reviews written by a g1t agent, on request: line comments, a summary and
7575 a verdict.
76−- Importing a public repository from GitHub or any git host.
76+- Importing a public repository from any git host by its address, and
77+ public or private repositories through g1t's GitHub App, imported once,
78+ mirrored, or pushed back to GitHub.
7779 - Merging: lands a pull request on `main`, closes its issue naming the pull
7880 request that resolved it, and closes the others for that issue as
79− superseded. Refused when the pull request is behind, so no commit is lost.
81+ superseded. When `main` has moved, the pull request is brought up to date
82+ first, or refused where the repository requires that, so no commit is
83+ lost.
8084 - g1t agents: g1t's own agents working on an issue in sandboxes on
8185 Cloudflare Containers, seeing each pull request through checks, an
8286 agent's review, revisions and catch-up.
101105 - An event bus: every state change is published, logged and delivered to
102106 subscribers.
103107
104−Not built yet: a code-search index and the Soon pages in each project's
105−menu. Git over SSH waits on inbound TCP on port 22, which on Cloudflare
108+Not built yet: what the Soon pages in each project's menu describe. Git
109+over SSH waits on inbound TCP on port 22, which on Cloudflare
106110 means Workers inbound TCP, a beta g1t has applied for and is waiting on.
107111 Use HTTPS until then. See the build order in the plan.
108112
131135 | `services/repos` | Repository registry, contents, forks, diffs, landing, git over HTTPS. Rust. |
132136 | `services/work` | Issues, pull requests, reviews, check runs and sessions. Rust. |
133137 | `services/events` | The event bus and its log. Rust. |
134−| `services/runner` | Starts sandboxes: for g1t agents, workflow jobs and the merge queue. |
135−| `services/og` | Social cards at `og.g1t.sh`: a PNG per page, showing only what anyone may see. |
138+| `services/search` | Site-wide search and Explore. Rust. |
139+| `services/billing` | Usage, the price book, limits, invoices and payments. Rust. |
140+| `services/actions` | GitHub Actions workflows, runs, caches and self-hosted runners. Rust. |
141+| `services/security` | Push protection findings, history scanning and dependency upkeep. Rust. |
142+| `services/integrations` | Model providers, alerts, trackers and the GitHub App. Rust. |
143+| `services/webhooks` | Webhook deliveries. Rust. |
144+| `services/runner` | Starts sandboxes: for g1t agents, workflow jobs and the merge queue. TypeScript. |
145+| `services/projects` | Projects and the dependencies between them. TypeScript. |
146+| `services/deployments` | Builds, previews and production on `g1t.page`. TypeScript. |
147+| `services/pages` | Serves every app deployed on `g1t.page`, and custom domains. TypeScript. |
148+| `services/models` | The model proxy at `models.g1t.sh`. TypeScript. |
149+| `services/context` | The context hub: catalog, search and scorecards. TypeScript. |
150+| `services/og` | Social cards at `og.g1t.sh`: a PNG per page, showing only what anyone may see. TypeScript. |
151+| `apps/status` | `status.g1t.sh`. TypeScript. |
152+| `apps/sudo` | g1t's own staff console. |
136153 | `crates/runner` | The program inside a sandbox: runs an agent, a workflow job or a merge queue build, and reports back. Rust. |
137154 | `crates/contracts` | Types and service interfaces for the Rust services. |
138155 | `crates/kit` | Plumbing shared by Rust services on Workers. |
156+| `crates/actions` | Reads workflows and evaluates their expressions. Rust. |
157+| `crates/scan` | Secret and lockfile scanning, shared by services. Rust. |
158+| `crates/secrets` | Secrets at rest and signatures. Rust. |
139159 | `crates/sshd` | Git over SSH, bridged to Artifacts. Not deployed yet. |
140160 | `packages/contracts` | The same interfaces for TypeScript callers. |
141161 | `packages/theme` | Design tokens and the logo, shared by the site and the docs. |
162+| `deploy` | `stack.jsonc`, every deployable part and its resources; `self-host`, the Docker Compose version. |
142163
143−Each service is its own Worker with its own database. They call each other
144−through service bindings and react to each other through events. Everything
145−that is not a web UI is written in Rust, except the small Worker that
146−starts sandboxes, which uses a TypeScript-only Cloudflare library.
164+Each service is its own Worker, and each one that keeps data has its own
165+database. They call each other through service bindings and react to each
166+other through events. The core services (accounts, repositories, work,
167+events, billing, Actions, security and the API) are written in Rust; the
168+rest are the web apps and the Workers marked TypeScript above.
147169
148170 ## Run your own
149171
150172 You need a Cloudflare account on the Workers Paid plan (Artifacts requires
151−it), Node 22 or newer, Rust with the `wasm32-unknown-unknown` target, and
173+it), Node 22.22 or newer (`engines` in `package.json`; g1t is built on
174+Node 24), Rust with the `wasm32-unknown-unknown` target, and
152175 Docker to build the sandbox image.
153176
154177 ```sh
158181
159182 Then, once:
160183
161−1. Create each service's D1 database and the event queues with
162− `npx wrangler d1 create <name>` and `npx wrangler queues create <name>`
163− (the names are in each `wrangler.jsonc`).
184+1. Create the resources each part needs: D1 databases, queues, KV
185+ namespaces, R2 buckets and the Artifacts namespace (`npx wrangler d1
186+ create <name>`, `npx wrangler queues create <name>`, and so on), and set
187+ each part's secrets. `deploy/stack.jsonc` lists them all.
164188 2. Put your own `account_id`, database ids and hostnames in each
165189 `wrangler.jsonc`.
166190 3. For [Deployments](https://docs.g1t.sh/guides/deployments/), which needs
+363−0
1+//! Security alerts as the API gives them: a secret found in a repository,
2+//! or a dependency with a known vulnerability, in one flat `snake_case`
3+//! shape with `kind` saying which.
4+//!
5+//! The security service keeps them as `SecretFinding` and `Vulnerability`
6+//! (see `g1t_contracts::security`); this is the public form of both.
7+
8+use g1t_contracts::security::{
9+ AlertChange, AlertState, DismissReason, SecretFinding, SecretStatus, SecurityUpdate, UpdateState,
10+ Vulnerability,
11+};
12+use serde::{Deserialize, Serialize};
13+
14+/// Which kind of alert.
15+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16+#[serde(rename_all = "snake_case")]
17+pub enum AlertKind {
18+ Secret,
19+ Dependency,
20+}
21+
22+impl AlertKind {
23+ pub const ALL: [AlertKind; 2] = [AlertKind::Secret, AlertKind::Dependency];
24+
25+ pub fn as_str(self) -> &'static str {
26+ match self {
27+ AlertKind::Secret => "secret",
28+ AlertKind::Dependency => "dependency",
29+ }
30+ }
31+
32+ pub fn parse(text: &str) -> Option<AlertKind> {
33+ AlertKind::ALL.into_iter().find(|kind| kind.as_str() == text)
34+ }
35+
36+ /// The kind an alert's id names: `sec_…` or `vul_…`.
37+ pub fn of_id(id: &str) -> Option<AlertKind> {
38+ if id.starts_with("sec_") {
39+ Some(AlertKind::Secret)
40+ } else if id.starts_with("vul_") {
41+ Some(AlertKind::Dependency)
42+ } else {
43+ None
44+ }
45+ }
46+
47+ /// Whether `reason` can dismiss an alert of this kind.
48+ pub fn takes(self, reason: DismissReason) -> bool {
49+ reason.for_secrets() == (self == AlertKind::Secret)
50+ }
51+
52+ /// The reasons that dismiss an alert of this kind, as words.
53+ pub fn reasons(self) -> Vec<&'static str> {
54+ DismissReason::ALL
55+ .into_iter()
56+ .filter(|reason| self.takes(*reason))
57+ .map(DismissReason::as_str)
58+ .collect()
59+ }
60+}
61+
62+/// One alert.
63+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
64+pub struct SecurityAlert {
65+ pub kind: AlertKind,
66+ /// `sec_…` for a secret, `vul_…` for a dependency.
67+ pub id: String,
68+ pub state: AlertState,
69+ #[serde(flatten)]
70+ pub detail: AlertDetail,
71+ /// RFC 3339.
72+ pub found_at: String,
73+ /// Why it was dismissed (or, for a secret, revoked); null while open.
74+ pub dismissed_reason: Option<DismissReason>,
75+ pub dismissed_comment: Option<String>,
76+ /// Who dismissed it.
77+ pub dismissed_by: Option<String>,
78+ /// RFC 3339.
79+ pub dismissed_at: Option<String>,
80+}
81+
82+/// What only one kind of alert has.
83+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
84+#[serde(untagged)]
85+pub enum AlertDetail {
86+ Secret(SecretDetail),
87+ Dependency(DependencyDetail),
88+}
89+
90+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91+pub struct SecretDetail {
92+ /// `aws_access_key`, `github_token`, …
93+ pub secret_type: String,
94+ /// "an AWS access key".
95+ pub label: String,
96+ pub path: String,
97+ pub line: u32,
98+ pub commit: String,
99+ /// Enough of it to recognise; the secret itself is never kept.
100+ pub preview: String,
101+ /// open, blocked, allowed or resolved.
102+ pub status: SecretStatus,
103+ /// `push` or `history`.
104+ pub source: String,
105+ /// Why the value looks made for tests or documentation, when it does.
106+ pub test_value: Option<String>,
107+ /// Who pushed it, for a push.
108+ pub found_by: Option<String>,
109+}
110+
111+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
112+pub struct DependencyDetail {
113+ pub ecosystem: String,
114+ pub package: String,
115+ pub version: String,
116+ /// The lockfile that resolves it.
117+ pub manifest: String,
118+ pub advisory: String,
119+ pub osv_id: String,
120+ pub summary: String,
121+ pub severity: String,
122+ /// Null when no patched version is available.
123+ pub fixed_version: Option<String>,
124+ pub fixed_at: Option<String>,
125+ /// The pull request g1t opens to upgrade it, once started.
126+ pub update: Option<AlertUpdate>,
127+}
128+
129+/// The security update for a dependency's package.
130+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131+pub struct AlertUpdate {
132+ pub state: UpdateState,
133+ pub target: String,
134+ pub branch: Option<String>,
135+ pub pull: Option<u32>,
136+ pub issue: Option<u32>,
137+ pub error: Option<String>,
138+ pub updated_at: String,
139+}
140+
141+impl From<SecurityUpdate> for AlertUpdate {
142+ fn from(update: SecurityUpdate) -> Self {
143+ AlertUpdate {
144+ state: update.state,
145+ target: update.target,
146+ branch: update.branch,
147+ pull: update.pull,
148+ issue: update.issue,
149+ error: update.error,
150+ updated_at: update.updated_at,
151+ }
152+ }
153+}
154+
155+impl From<SecretFinding> for SecurityAlert {
156+ fn from(secret: SecretFinding) -> Self {
157+ let decided = secret.state != AlertState::Open;
158+ SecurityAlert {
159+ kind: AlertKind::Secret,
160+ id: secret.id,
161+ state: secret.state,
162+ detail: AlertDetail::Secret(SecretDetail {
163+ secret_type: secret.kind,
164+ label: secret.label,
165+ path: secret.path,
166+ line: secret.line,
167+ commit: secret.commit,
168+ preview: secret.preview,
169+ status: secret.status,
170+ source: secret.source,
171+ test_value: secret.test_value,
172+ found_by: secret.found_by,
173+ }),
174+ found_at: secret.found_at,
175+ dismissed_reason: secret.dismissed_reason.filter(|_| decided),
176+ dismissed_comment: secret.reason.filter(|_| decided),
177+ dismissed_by: secret.decided_by.filter(|_| decided),
178+ dismissed_at: secret.decided_at.filter(|_| decided),
179+ }
180+ }
181+}
182+
183+impl From<Vulnerability> for SecurityAlert {
184+ fn from(vulnerability: Vulnerability) -> Self {
185+ let dismissed = vulnerability.state == AlertState::Dismissed;
186+ SecurityAlert {
187+ kind: AlertKind::Dependency,
188+ id: vulnerability.id,
189+ state: vulnerability.state,
190+ detail: AlertDetail::Dependency(DependencyDetail {
191+ ecosystem: vulnerability.ecosystem,
192+ package: vulnerability.package,
193+ version: vulnerability.version,
194+ manifest: vulnerability.manifest,
195+ advisory: vulnerability.advisory,
196+ osv_id: vulnerability.osv_id,
197+ summary: vulnerability.summary,
198+ severity: vulnerability.severity,
199+ fixed_version: vulnerability.fixed_version,
200+ fixed_at: vulnerability.fixed_at,
201+ update: vulnerability.update.map(AlertUpdate::from),
202+ }),
203+ found_at: vulnerability.found_at,
204+ dismissed_reason: vulnerability.dismissed_reason.filter(|_| dismissed),
205+ dismissed_comment: vulnerability.dismissed_comment.filter(|_| dismissed),
206+ dismissed_by: vulnerability.dismissed_by.filter(|_| dismissed),
207+ dismissed_at: vulnerability.dismissed_at.filter(|_| dismissed),
208+ }
209+ }
210+}
211+
212+impl SecurityAlert {
213+ /// The alert `dismiss` or `reopen` changed, if it names one.
214+ pub fn from_change(change: AlertChange) -> Option<SecurityAlert> {
215+ change
216+ .secret
217+ .map(SecurityAlert::from)
218+ .or_else(|| change.vulnerability.map(SecurityAlert::from))
219+ }
220+}
221+
222+/// Secrets, then dependencies, filtered by state and kind when given.
223+pub fn list(
224+ secrets: Vec<SecretFinding>,
225+ vulnerabilities: Vec<Vulnerability>,
226+ state: Option<AlertState>,
227+ kind: Option<AlertKind>,
228+) -> Vec<SecurityAlert> {
229+ let secrets = secrets.into_iter().map(SecurityAlert::from);
230+ let dependencies = vulnerabilities.into_iter().map(SecurityAlert::from);
231+ secrets
232+ .chain(dependencies)
233+ .filter(|alert| state.is_none_or(|state| alert.state == state))
234+ .filter(|alert| kind.is_none_or(|kind| alert.kind == kind))
235+ .collect()
236+}
237+
238+#[cfg(test)]
239+mod tests {
240+ use super::*;
241+ use serde_json::{Value, json};
242+
243+ fn secret(state: AlertState) -> SecretFinding {
244+ serde_json::from_value(json!({
245+ "id": "sec_1",
246+ "repoId": "rep_1",
247+ "kind": "aws_access_key",
248+ "label": "an AWS access key",
249+ "path": "config/dev.env",
250+ "line": 3,
251+ "commit": "9f2c1e0",
252+ "preview": "AKIA…MPLE",
253+ "status": if state == AlertState::Open { "open" } else { "allowed" },
254+ "source": "history",
255+ "foundBy": null,
256+ "foundAt": "2026-10-01T12:00:00Z",
257+ "decidedBy": "ada",
258+ "reason": "Only in the test fixtures.",
259+ "decidedAt": "2026-10-02T09:00:00Z",
260+ "dismissedReason": "used_in_tests",
261+ "testValue": "a documented example key",
262+ "state": state,
263+ }))
264+ .unwrap()
265+ }
266+
267+ fn vulnerability() -> Vulnerability {
268+ serde_json::from_value(json!({
269+ "id": "vul_1",
270+ "repoId": "rep_1",
271+ "ecosystem": "npm",
272+ "package": "lodash",
273+ "version": "4.17.20",
274+ "manifest": "package-lock.json",
275+ "advisory": "GHSA-35jh-r3h4-6jhm",
276+ "osvId": "GHSA-35jh-r3h4-6jhm",
277+ "summary": "Command injection in lodash",
278+ "severity": "high",
279+ "fixedVersion": null,
280+ "status": "open",
281+ "issue": null,
282+ "foundAt": "2026-10-01T12:00:00Z",
283+ "fixedAt": null,
284+ "state": "open",
285+ "update": { "state": "open", "target": "4.17.21", "branch": "g1t/security/lodash-4.17.21", "pull": 12, "issue": null, "error": null, "updatedAt": "2026-10-01T12:05:00Z" },
286+ }))
287+ .unwrap()
288+ }
289+
290+ fn keys(value: &Value, out: &mut Vec<String>) {
291+ match value {
292+ Value::Object(fields) => {
293+ for (key, value) in fields {
294+ out.push(key.clone());
295+ keys(value, out);
296+ }
297+ }
298+ Value::Array(items) => items.iter().for_each(|item| keys(item, out)),
299+ _ => {}
300+ }
301+ }
302+
303+ #[test]
304+ fn an_alert_is_snake_case_in_one_flat_shape() {
305+ let alerts = list(vec![secret(AlertState::Dismissed)], vec![vulnerability()], None, None);
306+ let sent = serde_json::to_value(&alerts).unwrap();
307+ assert!(g1t_kit::wire::camel_case_keys(&sent).is_empty(), "{sent}");
308+ let mut names = Vec::new();
309+ keys(&sent, &mut names);
310+ for name in names {
311+ assert!(name.chars().all(|c| c.is_ascii_lowercase() || c == '_'), "{name}");
312+ }
313+ let (secret, dependency) = (&sent[0], &sent[1]);
314+ assert_eq!(secret["kind"], "secret");
315+ assert_eq!(secret["secret_type"], "aws_access_key");
316+ assert_eq!(secret["dismissed_reason"], "used_in_tests");
317+ assert_eq!(secret["dismissed_comment"], "Only in the test fixtures.");
318+ assert_eq!(secret["dismissed_by"], "ada");
319+ assert!(secret.get("package").is_none());
320+ assert_eq!(dependency["kind"], "dependency");
321+ // No patched version is a null, not a missing field.
322+ assert!(dependency["fixed_version"].is_null() && dependency.get("fixed_version").is_some());
323+ assert_eq!(dependency["update"]["updated_at"], "2026-10-01T12:05:00Z");
324+ assert!(dependency["dismissed_reason"].is_null());
325+ assert!(dependency.get("secret_type").is_none());
326+ // And it reads back as it was.
327+ let again: Vec<SecurityAlert> = serde_json::from_value(sent).unwrap();
328+ assert_eq!(again, alerts);
329+ }
330+
331+ #[test]
332+ fn an_open_secret_shows_no_decision() {
333+ let alert = SecurityAlert::from(secret(AlertState::Open));
334+ assert_eq!(alert.dismissed_reason, None);
335+ assert_eq!(alert.dismissed_by, None);
336+ assert_eq!(alert.dismissed_comment, None);
337+ }
338+
339+ #[test]
340+ fn alerts_filter_by_state_and_kind() {
341+ let all = || (vec![secret(AlertState::Dismissed)], vec![vulnerability()]);
342+ let (s, v) = all();
343+ assert_eq!(list(s, v, Some(AlertState::Open), None).len(), 1);
344+ let (s, v) = all();
345+ let secrets = list(s, v, None, Some(AlertKind::Secret));
346+ assert_eq!(secrets.len(), 1);
347+ assert_eq!(secrets[0].kind, AlertKind::Secret);
348+ let (s, v) = all();
349+ assert!(list(s, v, Some(AlertState::Fixed), None).is_empty());
350+ }
351+
352+ #[test]
353+ fn each_kind_takes_its_own_reasons() {
354+ assert_eq!(AlertKind::Secret.reasons(), ["false_positive", "used_in_tests", "revoked", "wont_fix"]);
355+ assert_eq!(
356+ AlertKind::Dependency.reasons(),
357+ ["fix_started", "no_bandwidth", "tolerable_risk", "inaccurate", "not_used"]
358+ );
359+ assert_eq!(AlertKind::of_id("sec_9"), Some(AlertKind::Secret));
360+ assert_eq!(AlertKind::of_id("vul_9"), Some(AlertKind::Dependency));
361+ assert_eq!(AlertKind::of_id("x"), None);
362+ }
363+}
+1−0
44 //! operations (see [`operations::Op`]), which call the services that own
55 //! the data. This Worker holds none.
66
7+mod alerts;
78 mod audit;
89 mod blobs;
910 mod mcp;
+10−1
2121 (
2222 "Workspaces",
2323 "A workspace owns repositories and is the first part of their address. People and agents work in workspaces.",
24− &[Op::CreateWorkspace, Op::DeleteWorkspace],
24+ &[Op::CreateWorkspace, Op::UpdateWorkspace, Op::DeleteWorkspace],
2525 ),
2626 (
2727 "Invites",
7878 ],
7979 ),
8080 (
81+ "Security",
82+ "Secrets found in what is pushed and in a repository's history, and dependencies with known vulnerabilities: listing the alerts, and dismissing or reopening them.",
83+ &[Op::ListSecurityAlerts, Op::DismissSecurityAlert, Op::ReopenSecurityAlert],
84+ ),
85+ (
8186 "Issues",
8287 "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.",
8388 &[
220225 Op::Whoami => "Get the current user",
221226 Op::CreateWorkspace => "Create a workspace",
222227 Op::DeleteWorkspace => "Delete a workspace",
228+ Op::UpdateWorkspace => "Update a workspace",
223229 Op::ListEmails => "List your email addresses",
224230 Op::AddEmail => "Add an email address",
225231 Op::RemoveEmail => "Remove an email address",
330336 Op::DeclineRepoInvitation => "Decline a repository invitation",
331337 Op::SetBasePermission => "Set the base permission",
332338 Op::ListOutsideCollaborators => "List outside collaborators",
339+ Op::ListSecurityAlerts => "List security alerts",
340+ Op::DismissSecurityAlert => "Dismiss a security alert",
341+ Op::ReopenSecurityAlert => "Reopen a security alert",
333342 }
334343 }
335344
+287−2
1111 };
1212 use g1t_contracts::identity::AgentScope;
1313 use g1t_contracts::events::{Event, ListArgs as ListEventsArgs};
14−use g1t_contracts::identity::CreateWorkspaceArgs;
14+use g1t_contracts::identity::{CreateWorkspaceArgs, UpdateWorkspaceArgs, Workspace};
1515 use g1t_contracts::repos::{CreateArgs, GetArgs, ListArgs as ListReposArgs, Repo, RepoPath};
16+use g1t_contracts::security::{
17+ AlertChange, AlertState, DismissArgs, DismissReason, OverviewArgs as SecurityOverviewArgs, ReopenArgs,
18+ SecurityOverview,
19+};
20+
21+use crate::alerts::{AlertKind, SecurityAlert};
1622 use g1t_contracts::work::*;
1723 use g1t_contracts::{FailureCode, Outcome, Viewer};
1824 use serde::Serialize;
3541 pub context: Fetcher,
3642 /// Search across all of g1t.
3743 pub search: Fetcher,
44+ /// Secret and dependency alerts.
45+ pub security: Fetcher,
3846 /// Where the request came in, for its audit entries.
3947 pub audit: crate::audit::AuditContext,
4048 /// Set for a request made with an agent's token: all it may do.
5563 actions: env.service("ACTIONS")?,
5664 context: env.service("CONTEXT")?,
5765 search: env.service("SEARCH")?,
66+ security: env.service("SECURITY")?,
5867 scope: None,
5968 audit: crate::audit::AuditContext::default(),
6069 })
6675 Whoami,
6776 CreateWorkspace,
6877 DeleteWorkspace,
78+ UpdateWorkspace,
6979 ListEmails,
7080 AddEmail,
7181 RemoveEmail,
176186 DeclineRepoInvitation,
177187 SetBasePermission,
178188 ListOutsideCollaborators,
189+ ListSecurityAlerts,
190+ DismissSecurityAlert,
191+ ReopenSecurityAlert,
179192 }
180193
181194 fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> {
397410 })
398411 }
399412
413+fn alert_id_schema() -> Value {
414+ json!({
415+ "type": "string",
416+ "description": "The alert's id, from list_security_alerts: sec_… for a secret, vul_… for a dependency.",
417+ })
418+}
419+
400420 impl Op {
401− pub const ALL: [Op; 113] = [
421+ pub const ALL: [Op; 117] = [
402422 Op::Whoami,
403423 Op::CreateWorkspace,
404424 Op::DeleteWorkspace,
425+ Op::UpdateWorkspace,
405426 Op::ListEmails,
406427 Op::AddEmail,
407428 Op::RemoveEmail,
512533 Op::DeclineRepoInvitation,
513534 Op::SetBasePermission,
514535 Op::ListOutsideCollaborators,
536+ Op::ListSecurityAlerts,
537+ Op::DismissSecurityAlert,
538+ Op::ReopenSecurityAlert,
515539 ];
516540
517541 pub fn by_name(name: &str) -> Option<Op> {
524548 Op::Whoami => "whoami",
525549 Op::CreateWorkspace => "create_workspace",
526550 Op::DeleteWorkspace => "delete_workspace",
551+ Op::UpdateWorkspace => "update_workspace",
527552 Op::ListEmails => "list_emails",
528553 Op::AddEmail => "add_email",
529554 Op::RemoveEmail => "remove_email",
634659 Op::DeclineRepoInvitation => "decline_repo_invitation",
635660 Op::SetBasePermission => "set_base_permission",
636661 Op::ListOutsideCollaborators => "list_outside_collaborators",
662+ Op::ListSecurityAlerts => "list_security_alerts",
663+ Op::DismissSecurityAlert => "dismiss_security_alert",
664+ Op::ReopenSecurityAlert => "reopen_security_alert",
637665 }
638666 }
639667
676704 Op::DeleteWorkspace => {
677705 "Delete a workspace. Owners only, signed in as a person, and confirm must be the workspace's slug. It must hold no repositories (move them with transfer_repo first) and no projects, and billing must be able to settle it: no unpaid invoice, no prepaid credit left, and no usage this month still being metered; what it owes is charged to its card at once. Its members, access tokens, webhooks, integrations and workspace secrets are removed; its statements, invoices and audit log are kept. The slug is never given to another workspace; the person whose username it is may create it again."
678706 }
707+ Op::UpdateWorkspace => {
708+ "Change a workspace's display name and description, and what every member gets on each of its repositories (base_permission: none, read, write or admin). Only the fields given are changed; give at least one. An empty name falls back to the slug, which this never changes (that is a rename, on Settings); an empty description clears it. Owners only, signed in as a person. Returns the workspace as it is now."
709+ }
679710 Op::ListRepos => "Repositories you can see, optionally filtered by a search query.",
680711 Op::GetRepo => "One repository's details.",
681712 Op::UpdateRepo => {
944975 Op::ListOutsideCollaborators => {
945976 "The people with a role on some of a workspace's repositories who are not its members, each with the repositories they can reach and their role on each. Owners only."
946977 }
978+ Op::ListSecurityAlerts => {
979+ "A repository's security alerts: secrets found in what was pushed or in its history (`kind` `secret`), and dependencies with a known vulnerability (`kind` `dependency`), secrets first. Each has a `state`: `open`, `dismissed` (someone said why it can stay) or `fixed` (a secret revoked, a dependency no longer vulnerable). Filter with `state` and `kind`; both are left out for all. A secret is never returned, only a `preview`. Needs the Write role on the repository; anyone else is told it does not exist, whether or not the repository is public."
980+ }
981+ Op::DismissSecurityAlert => {
982+ "Dismiss an alert with a reason and an optional comment. A secret takes false_positive, used_in_tests, revoked or wont_fix; a dependency takes fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used. A dismissed secret is let through push protection from then on, unless the reason is `revoked`, which marks it fixed, so dismissing a secret needs the Admin role on the repository; a dependency needs Write. Returns the alert as it is now. Reopen it with reopen_security_alert."
983+ }
984+ Op::ReopenSecurityAlert => {
985+ "Open a dismissed alert again. A reopened secret stops pushes that carry it again. The same roles as dismissing: Admin for a secret, Write for a dependency. Returns the alert as it is now."
986+ }
947987 }
948988 }
949989
10471087 }),
10481088 &["workspace", "confirm"],
10491089 ),
1090+ Op::UpdateWorkspace => object(
1091+ json!({
1092+ "workspace": workspace_schema(),
1093+ "name": {
1094+ "type": "string",
1095+ "description": "Its display name, at most 80 characters; longer is cut. Empty: its slug.",
1096+ },
1097+ "description": {
1098+ "type": "string",
1099+ "description": "One line saying what it is for, at most 160 characters; longer is cut. Empty clears it.",
1100+ },
1101+ "base_permission": {
1102+ "type": "string",
1103+ "enum": g1t_contracts::access::BasePermission::ALL.map(|base| base.as_str()),
1104+ "description": "What every member gets on each repository: none, read, write or admin. Needs the access:admin scope as well.",
1105+ },
1106+ }),
1107+ &["workspace"],
1108+ ),
10501109 Op::TransferRepo => object(
10511110 json!({
10521111 "repo": repo_schema(),
17841843 &["workspace", "base_permission"],
17851844 ),
17861845 Op::ListOutsideCollaborators => object(json!({ "workspace": workspace_schema() }), &["workspace"]),
1846+ Op::ListSecurityAlerts => object(
1847+ json!({
1848+ "repo": repo_schema(),
1849+ "state": {
1850+ "type": "string",
1851+ "enum": ([AlertState::Open, AlertState::Dismissed, AlertState::Fixed].map(AlertState::as_str)),
1852+ "description": "Only alerts in this state. Left out for all.",
1853+ },
1854+ "kind": {
1855+ "type": "string",
1856+ "enum": AlertKind::ALL.map(AlertKind::as_str),
1857+ "description": "Only secrets, or only vulnerable dependencies. Left out for both.",
1858+ },
1859+ }),
1860+ &["repo"],
1861+ ),
1862+ Op::DismissSecurityAlert => object(
1863+ json!({
1864+ "repo": repo_schema(),
1865+ "id": alert_id_schema(),
1866+ "reason": {
1867+ "type": "string",
1868+ "enum": DismissReason::ALL.map(DismissReason::as_str),
1869+ "description": "Why it can stay. For a secret: false_positive, used_in_tests, revoked (it was rotated: the alert is fixed) or wont_fix. For a dependency: fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used.",
1870+ },
1871+ "comment": { "type": "string", "description": "More about why, for whoever reads the alert next." },
1872+ }),
1873+ &["repo", "id", "reason"],
1874+ ),
1875+ Op::ReopenSecurityAlert => object(json!({ "repo": repo_schema(), "id": alert_id_schema() }), &["repo", "id"]),
17871876 }
17881877 }
17891878
18201909 Op::Whoami
18211910 | Op::CreateWorkspace
18221911 | Op::DeleteWorkspace
1912+ | Op::UpdateWorkspace
18231913 | Op::ListEmails
18241914 | Op::AddEmail
18251915 | Op::RemoveEmail
21012191 )
21022192 .await
21032193 }
2194+ Op::UpdateWorkspace => {
2195+ let base = match input.get("base_permission").filter(|value| !value.is_null()) {
2196+ None => None,
2197+ Some(value) => match value.as_str().and_then(BasePermission::parse) {
2198+ Some(base) => Some(base),
2199+ None => return failed(FailureCode::Invalid, "base_permission is none, read, write or admin."),
2200+ },
2201+ };
2202+ let (name, description) = (optional_text(input, "name"), optional_text(input, "description"));
2203+ if base.is_none() && name.is_none() && description.is_none() {
2204+ return failed(FailureCode::Invalid, "Give name, description or base_permission to change.");
2205+ }
2206+ let found = || async {
2207+ g1t_kit::call::<_, Option<Workspace>>(identity, "get_workspace", &json!({ "slug": workspace() })).await
2208+ };
2209+ if name.is_some() || description.is_some() {
2210+ // Identity sets both: what was not given stays as it is.
2211+ let Some(current) = found().await? else {
2212+ return failed(FailureCode::NotFound, "Workspace not found.");
2213+ };
2214+ let updated: Outcome<Workspace> = call(
2215+ identity,
2216+ "update_workspace",
2217+ &UpdateWorkspaceArgs {
2218+ actor: actor(),
2219+ slug: workspace(),
2220+ name: name.unwrap_or(current.name),
2221+ description: description.unwrap_or(current.description.unwrap_or_default()),
2222+ },
2223+ )
2224+ .await?;
2225+ if let Outcome::Fail(failure) = updated {
2226+ return Ok(Outcome::Fail(failure));
2227+ }
2228+ }
2229+ if let Some(base) = base {
2230+ let set: Outcome<BasePermission> = call(
2231+ identity,
2232+ "set_base_permission",
2233+ &SetBasePermissionArgs {
2234+ actor: actor(),
2235+ slug: workspace(),
2236+ base_permission: base,
2237+ surface: Some(services.audit.surface),
2238+ },
2239+ )
2240+ .await?;
2241+ if let Outcome::Fail(failure) = set {
2242+ return Ok(Outcome::Fail(failure));
2243+ }
2244+ }
2245+ match found().await? {
2246+ Some(workspace) => ok(&workspace),
2247+ None => failed(FailureCode::NotFound, "Workspace not found."),
2248+ }
2249+ }
21042250 Op::TransferRepo => {
21052251 pass(
21062252 repos,
31783324 )
31793325 .await
31803326 }
3327+ // Security alerts: the security service decides who may see and
3328+ // change them; the API gives them one public shape.
3329+ Op::ListSecurityAlerts => {
3330+ let filters = match alert_filters(input) {
3331+ Ok(filters) => filters,
3332+ Err(message) => return failed(FailureCode::Invalid, &message),
3333+ };
3334+ let overview: Outcome<SecurityOverview> = call(
3335+ &services.security,
3336+ "overview",
3337+ &SecurityOverviewArgs { repo, viewer: viewer.clone() },
3338+ )
3339+ .await?;
3340+ match overview {
3341+ Outcome::Ok(overview) => ok(&crate::alerts::list(
3342+ overview.secrets,
3343+ overview.vulnerabilities,
3344+ filters.0,
3345+ filters.1,
3346+ )),
3347+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
3348+ }
3349+ }
3350+ Op::DismissSecurityAlert => {
3351+ let id = text(input, "id");
3352+ let reason = match dismiss_reason(input, &id) {
3353+ Ok(reason) => reason,
3354+ Err(message) => return failed(FailureCode::Invalid, &message),
3355+ };
3356+ let comment = text(input, "comment").trim().to_owned();
3357+ let changed: Outcome<AlertChange> = call(
3358+ &services.security,
3359+ "dismiss",
3360+ &DismissArgs { actor: actor(), repo, id, reason, comment },
3361+ )
3362+ .await?;
3363+ changed_alert(changed)
3364+ }
3365+ Op::ReopenSecurityAlert => {
3366+ let changed: Outcome<AlertChange> = call(
3367+ &services.security,
3368+ "reopen",
3369+ &ReopenArgs { actor: actor(), repo, id: text(input, "id") },
3370+ )
3371+ .await?;
3372+ changed_alert(changed)
3373+ }
31813374 }
31823375 }
31833376 }
31843377
3378+/// `state` and `kind`, as list_security_alerts reads them.
3379+fn alert_filters(input: &Value) -> std::result::Result<(Option<AlertState>, Option<AlertKind>), String> {
3380+ let state = match optional_text(input, "state") {
3381+ None => None,
3382+ Some(state) => Some(
3383+ AlertState::parse(&state.to_lowercase())
3384+ .ok_or_else(|| format!("state is open, dismissed or fixed, not {state}."))?,
3385+ ),
3386+ };
3387+ let kind = match optional_text(input, "kind") {
3388+ None => None,
3389+ Some(kind) => Some(
3390+ AlertKind::parse(&kind.to_lowercase())
3391+ .ok_or_else(|| format!("kind is secret or dependency, not {kind}."))?,
3392+ ),
3393+ };
3394+ Ok((state, kind))
3395+}
3396+
3397+/// The reason dismiss_security_alert was given, checked against the kind
3398+/// of alert its id names.
3399+fn dismiss_reason(input: &Value, id: &str) -> std::result::Result<DismissReason, String> {
3400+ let all = || DismissReason::ALL.map(DismissReason::as_str).join(", ");
3401+ let given = text(input, "reason");
3402+ let Some(reason) = DismissReason::parse(given.trim()) else {
3403+ return Err(if given.is_empty() {
3404+ format!("Give a reason: one of {}.", all())
3405+ } else {
3406+ format!("{given} is not a reason. Give one of {}.", all())
3407+ });
3408+ };
3409+ match AlertKind::of_id(id) {
3410+ Some(kind) if !kind.takes(reason) => Err(format!(
3411+ "A {} alert is dismissed with {}, not {}.",
3412+ kind.as_str(),
3413+ kind.reasons().join(", "),
3414+ reason.as_str()
3415+ )),
3416+ _ => Ok(reason),
3417+ }
3418+}
3419+
3420+/// The alert dismiss or reopen changed, in its public shape.
3421+fn changed_alert(changed: Outcome<AlertChange>) -> Result<Outcome<Value>> {
3422+ match changed {
3423+ Outcome::Ok(change) => match SecurityAlert::from_change(change) {
3424+ Some(alert) => ok(&alert),
3425+ None => failed(FailureCode::NotFound, "No such alert."),
3426+ },
3427+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
3428+ }
3429+}
3430+
31853431 const ROLE_NEEDED: &str = "Give a role: read, triage, write, maintain or admin.";
31863432
31873433 /// The role named by `role`.
33133559 assert!(op.needs_user(), "{}", op.name());
33143560 }
33153561 }
3562+
3563+ /// An unknown reason, or one for the other kind of alert, is refused
3564+ /// before the security service is asked.
3565+ #[test]
3566+ fn dismiss_reasons_are_checked_against_the_alert() {
3567+ let reason = |reason: &str, id: &str| dismiss_reason(&json!({ "reason": reason }), id);
3568+ assert_eq!(reason("used_in_tests", "sec_1"), Ok(DismissReason::UsedInTests));
3569+ assert_eq!(reason("tolerable_risk", "vul_1"), Ok(DismissReason::TolerableRisk));
3570+ assert!(reason("because", "sec_1").unwrap_err().contains("not a reason"));
3571+ assert!(reason("", "sec_1").unwrap_err().starts_with("Give a reason"));
3572+ assert!(reason("not_used", "sec_1").unwrap_err().contains("false_positive"));
3573+ assert!(reason("revoked", "vul_1").unwrap_err().contains("fix_started"));
3574+ assert_eq!(
3575+ Op::DismissSecurityAlert.input()["properties"]["reason"]["enum"].as_array().unwrap().len(),
3576+ DismissReason::ALL.len()
3577+ );
3578+ }
3579+
3580+ #[test]
3581+ fn alert_filters_are_read_as_words() {
3582+ assert_eq!(alert_filters(&json!({})), Ok((None, None)));
3583+ assert_eq!(
3584+ alert_filters(&json!({ "state": "Dismissed", "kind": "secret" })),
3585+ Ok((Some(AlertState::Dismissed), Some(AlertKind::Secret)))
3586+ );
3587+ assert!(alert_filters(&json!({ "state": "closed" })).is_err());
3588+ assert!(alert_filters(&json!({ "kind": "vulnerability" })).is_err());
3589+ }
3590+
3591+ /// An agent's token reads alerts at most; it never dismisses or
3592+ /// reopens one, whatever its scope lists.
3593+ #[test]
3594+ fn agents_never_dismiss_alerts() {
3595+ use g1t_contracts::credentials::NEVER;
3596+ for op in [Op::DismissSecurityAlert, Op::ReopenSecurityAlert] {
3597+ assert!(NEVER.contains(&op.name()), "{}", op.name());
3598+ }
3599+ assert!(!NEVER.contains(&Op::ListSecurityAlerts.name()));
3600+ }
33163601 }
+152−3
6262 "member_count": 1,
6363 "base_permission": "write"
6464 },
65− "notes": "`base_permission` is what every member gets on each of its repositories: `write` until an owner changes it with `PATCH /workspaces/{workspace}`. See [Access and roles](/guides/access-and-roles/)."
65+ "notes": "`base_permission` is what every member gets on each of its repositories: `write` until an owner changes it with `PATCH /workspaces/{workspace}` or `PUT /workspaces/{workspace}/base_permission`. See [Access and roles](/guides/access-and-roles/)."
6666 },
67+ "update_workspace": {
68+ "params": {
69+ "workspace": "acme-labs"
70+ },
71+ "request": {
72+ "name": "Acme Labs",
73+ "description": "Rockets, and the software that flies them."
74+ },
75+ "response": {
76+ "id": "wsp_01m43teqa9em6bje0bhvdj4jkb",
77+ "slug": "acme-labs",
78+ "name": "Acme Labs",
79+ "description": "Rockets, and the software that flies them.",
80+ "created_at": "2026-10-04T16:02:51.337Z",
81+ "member_count": 3,
82+ "base_permission": "write"
83+ },
84+ "notes": "Only the fields given change. `name` is the display name; the slug, the first part of the workspace's addresses, stays as it is. `base_permission` needs the `access:admin` scope as well as `workspace:admin`; [`set_base_permission`](/reference/api/access/set-base-permission/) sets it alone. Refused with `403` for anyone but an owner signed in as a person. Recorded in the [audit log](/guides/audit-log/). See [Workspaces](/guides/workspaces/)."
85+ },
6786 "delete_workspace": {
6887 "request": {
6988 "confirm": "acme-labs"
732751 "updated_by": "syntaqx",
733752 "updated_at": "2026-10-04T16:20:37.508Z"
734753 },
735− "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](#list_check_names) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/g1t-agents/#how-sure-the-agent-is)."
754+ "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](/reference/api/repositories/list-check-names/) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/g1t-agents/#how-sure-the-agent-is)."
736755 },
737756 "list_check_names": {
738757 "response": [
16251644 }
16261645 ]
16271646 },
1628− "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](#update_repo_settings)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request the g1t agent is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have the g1t agent do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](#get_workflow_run) and [`get_job_logs`](#get_job_logs)."
1647+ "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](/reference/api/repositories/update-repo-settings/)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request the g1t agent is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have the g1t agent do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](/reference/api/actions/get-workflow-run/) and [`get_job_logs`](/reference/api/actions/get-job-logs/)."
16291648 },
16301649 "get_pull_request_changes": {
16311650 "response": {
40544073 }
40554074 ],
40564075 "notes": "By username, each with the repositories they have a role on. Refused with `403` for anyone but an owner. See [Access and roles](/guides/access-and-roles/)."
4076+ },
4077+ "list_security_alerts": {
4078+ "params": {
4079+ "owner": "flagon-io",
4080+ "name": "hello"
4081+ },
4082+ "query": {
4083+ "state": "open"
4084+ },
4085+ "response": [
4086+ {
4087+ "kind": "secret",
4088+ "id": "sec_01kp4a7b2c3d4e5f6g7h8j9k0m",
4089+ "state": "dismissed",
4090+ "secret_type": "aws_access_key",
4091+ "label": "an AWS access key",
4092+ "path": "test/fixtures/aws.env",
4093+ "line": 3,
4094+ "commit": "9f2c1e04b7d3a8e6f5c2b1a0d9e8f7c6b5a4d3e2",
4095+ "preview": "AKIA…MPLE",
4096+ "status": "allowed",
4097+ "source": "history",
4098+ "test_value": "the documented example key",
4099+ "found_by": null,
4100+ "found_at": "2026-10-01T12:00:00.000Z",
4101+ "dismissed_reason": "used_in_tests",
4102+ "dismissed_comment": "Only in the test fixtures.",
4103+ "dismissed_by": "syntaqx",
4104+ "dismissed_at": "2026-10-02T09:15:00.000Z"
4105+ },
4106+ {
4107+ "kind": "dependency",
4108+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n",
4109+ "state": "open",
4110+ "ecosystem": "npm",
4111+ "package": "lodash",
4112+ "version": "4.17.20",
4113+ "manifest": "package-lock.json",
4114+ "advisory": "GHSA-35jh-r3h4-6jhm",
4115+ "osv_id": "GHSA-35jh-r3h4-6jhm",
4116+ "summary": "Command Injection in lodash",
4117+ "severity": "high",
4118+ "fixed_version": "4.17.21",
4119+ "fixed_at": null,
4120+ "update": {
4121+ "state": "open",
4122+ "target": "4.17.21",
4123+ "branch": "g1t/security/lodash-4.17.21",
4124+ "pull": 42,
4125+ "issue": null,
4126+ "error": null,
4127+ "updated_at": "2026-10-01T12:20:00.000Z"
4128+ },
4129+ "found_at": "2026-10-01T12:00:00.000Z",
4130+ "dismissed_reason": null,
4131+ "dismissed_comment": null,
4132+ "dismissed_by": null,
4133+ "dismissed_at": null
4134+ }
4135+ ],
4136+ "notes": "Secrets come first, then dependencies. Fields only one kind has are left out of the other: a secret has `secret_type`, `label`, `path`, `line`, `commit`, `preview`, `status` (`open`, `blocked`, `allowed` or `resolved`), `source` (`push` or `history`), `test_value` and `found_by`; a dependency has `ecosystem`, `package`, `version`, `manifest`, `advisory`, `osv_id`, `summary`, `severity`, `fixed_version` (null when no patched version is available), `fixed_at` and `update`, the pull request g1t opens to upgrade it. `dismissed_reason`, `dismissed_comment`, `dismissed_by` and `dismissed_at` are null while an alert is open; a secret fixed by being revoked keeps them. `422` for a `state` or `kind` it does not know, and `404` for anyone without the Write role. See [Security](/guides/security/)."
4137+ },
4138+ "dismiss_security_alert": {
4139+ "params": {
4140+ "owner": "flagon-io",
4141+ "name": "hello",
4142+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n"
4143+ },
4144+ "request": {
4145+ "reason": "tolerable_risk",
4146+ "comment": "Only the build uses it, on trusted input."
4147+ },
4148+ "response": {
4149+ "kind": "dependency",
4150+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n",
4151+ "state": "dismissed",
4152+ "ecosystem": "npm",
4153+ "package": "lodash",
4154+ "version": "4.17.20",
4155+ "manifest": "package-lock.json",
4156+ "advisory": "GHSA-35jh-r3h4-6jhm",
4157+ "osv_id": "GHSA-35jh-r3h4-6jhm",
4158+ "summary": "Command Injection in lodash",
4159+ "severity": "high",
4160+ "fixed_version": "4.17.21",
4161+ "fixed_at": null,
4162+ "update": {
4163+ "state": "open",
4164+ "target": "4.17.21",
4165+ "branch": "g1t/security/lodash-4.17.21",
4166+ "pull": 42,
4167+ "issue": null,
4168+ "error": null,
4169+ "updated_at": "2026-10-01T12:20:00.000Z"
4170+ },
4171+ "found_at": "2026-10-01T12:00:00.000Z",
4172+ "dismissed_reason": "tolerable_risk",
4173+ "dismissed_comment": "Only the build uses it, on trusted input.",
4174+ "dismissed_by": "syntaqx",
4175+ "dismissed_at": "2026-10-06T10:00:00.000Z"
4176+ },
4177+ "notes": "| Kind | Reasons |\n| --- | --- |\n| `secret` | `false_positive`, `used_in_tests`, `revoked`, `wont_fix` |\n| `dependency` | `fix_started`, `no_bandwidth`, `tolerable_risk`, `inaccurate`, `not_used` |\n\nA reason for the other kind of alert, or one not in the table, is refused with `422`. A secret dismissed as `revoked` becomes `fixed`; with any other reason it becomes `dismissed` and pushes that carry it go through. Dismissing a secret needs the Admin role on the repository; a dependency needs the Write role. A token needs `repo:admin` for either, and a g1t agent’s token never dismisses alerts."
4178+ },
4179+ "reopen_security_alert": {
4180+ "params": {
4181+ "owner": "flagon-io",
4182+ "name": "hello",
4183+ "id": "sec_01kp4a7b2c3d4e5f6g7h8j9k0m"
4184+ },
4185+ "response": {
4186+ "kind": "secret",
4187+ "id": "sec_01kp4a7b2c3d4e5f6g7h8j9k0m",
4188+ "state": "open",
4189+ "secret_type": "aws_access_key",
4190+ "label": "an AWS access key",
4191+ "path": "test/fixtures/aws.env",
4192+ "line": 3,
4193+ "commit": "9f2c1e04b7d3a8e6f5c2b1a0d9e8f7c6b5a4d3e2",
4194+ "preview": "AKIA…MPLE",
4195+ "status": "open",
4196+ "source": "history",
4197+ "test_value": "the documented example key",
4198+ "found_by": null,
4199+ "found_at": "2026-10-01T12:00:00.000Z",
4200+ "dismissed_reason": null,
4201+ "dismissed_comment": null,
4202+ "dismissed_by": null,
4203+ "dismissed_at": null
4204+ },
4205+ "notes": "The alert is `open` again, and a reopened secret stops pushes that carry it. The same roles as dismissing: Admin for a secret, Write for a dependency."
40574206 }
40584207 }
+6−1
7979 return through::<access::RepoInvitation>(op, as_is);
8080 }
8181 Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is),
82+ // Built by the API itself, in `snake_case`.
83+ Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
84+ Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
85+ return through::<crate::alerts::SecurityAlert>(op, as_is);
86+ }
8287 _ => {}
8388 }
8489 let sent = as_services_send(example);
8590 match op {
86− Op::CreateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
91+ Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
8792 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
8893 Op::Search => through::<search::SearchResults>(op, sent),
8994 Op::GetRepo
+11−1
5858 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
5959 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
6060 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
61− route("PATCH", "/workspaces/:workspace", Op::SetBasePermission, &[]),
61+ route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
62+ route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
6263 route(
6364 "GET",
6465 "/workspaces/:workspace/outside_collaborators",
6566 Op::ListOutsideCollaborators,
6667 &[],
6768 ),
69+ // Security alerts: secrets and vulnerable dependencies.
70+ route(
71+ "GET",
72+ "/repos/:owner/:name/security/alerts",
73+ Op::ListSecurityAlerts,
74+ &[("state", "state"), ("kind", "kind")],
75+ ),
76+ route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
77+ route("POST", "/repos/:owner/:name/security/alerts/:id/reopen", Op::ReopenSecurityAlert, &[]),
6878 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
6979 route(
7080 "GET",
+7−2
5757 Tool {
5858 name: "repository",
5959 title: "Repositories",
60− description: "Repositories: find, read and create them, and change their settings. Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.",
60+ description: "Repositories: find, read and create them, change their settings, and see and dismiss their security alerts (secrets and vulnerable dependencies). Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.",
6161 default_action: None,
6262 actions: &[
6363 a("list", Op::ListRepos, "Repositories you can see"),
7979 a("list_deleted", Op::ListDeletedRepos, "A workspace's deleted repositories"),
8080 a("restore", Op::RestoreRepo, "Restore a deleted one"),
8181 a("purge", Op::PurgeRepo, "Remove a deleted one for good"),
82+ a("security_alerts", Op::ListSecurityAlerts, "Secret and dependency alerts, filtered by state"),
83+ a("dismiss_alert", Op::DismissSecurityAlert, "Dismiss an alert with a reason"),
84+ a("reopen_alert", Op::ReopenSecurityAlert, "Reopen a dismissed alert"),
8285 ],
8386 },
8487 Tool {
224227 Tool {
225228 name: "workspace",
226229 title: "Workspaces",
227− description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create or delete one, invite members, and connect integrations and model providers.",
230+ description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, and connect integrations and model providers.",
228231 default_action: None,
229232 actions: &[
230233 a("create", Op::CreateWorkspace, "Create a workspace"),
231234 a("delete", Op::DeleteWorkspace, "Delete an empty workspace"),
235+ a("update", Op::UpdateWorkspace, "Change its name, description or base permission"),
232236 a("list_invites", Op::ListWorkspaceInvites, "Its invites"),
233237 a("invite_member", Op::InviteMember, "Invite an email address"),
234238 a("revoke_invite", Op::RevokeWorkspaceInvite, "Revoke a pending invite"),
267271 matches!(
268272 op,
269273 Op::DeleteWorkspace
274+ | Op::UpdateWorkspace
270275 | Op::DeleteRepo
271276 | Op::PurgeRepo
272277 | Op::TransferRepo
+3−1
2727 { "binding": "DEPLOYMENTS", "service": "g1t-deployments" },
2828 // The context hub: search_context and get_entity.
2929 { "binding": "CONTEXT", "service": "g1t-context" },
30− { "binding": "SEARCH", "service": "g1t-search" }
30+ { "binding": "SEARCH", "service": "g1t-search" },
31+ // Secret and dependency alerts: list_security_alerts and dismissing them.
32+ { "binding": "SECURITY", "service": "g1t-security" }
3133 ],
3234 // GitHub Actions artifacts, in chunks, with KV's own expiry (and cache
3335 // entries saved before the cache moved to R2, until they expire).
+50−35
3737 ### Repositories up to 1 GB, files up to 32 MB, no LFS
3838
3939 A repository can hold up to 1 GB and a single file up to 32 MB. Git LFS is
40−not supported.
40+not supported. A push that would cross either is declined before it is
41+stored, and git prints why. See [Size limits](/guides/git/#size-limits).
4142
4243 - **Why.** These are the limits of Cloudflare Artifacts, where every
43− repository is stored. g1t does not check them before a push reaches the
44− store yet, so a push that crosses them fails late, and git's message may
45− not say why.
44+ repository is stored.
4645 - **Instead.** Keep large binaries out of the repository: in a release
4746 bucket, a package registry or object storage, fetched at build time.
48−- **Status.** Checking both limits before the push, with a message git
49− shows you, is planned. Large file storage is planned. Raising the limits
50− themselves depends on Cloudflare.
47+- **Status.** Large file storage is planned. Raising the limits themselves
48+ depends on Cloudflare.
5149
5250 ### Pushes up to 100 MB each
5351
9896 - **Status.** Not scheduled for your own scripts. A hook in the store,
9997 which would let g1t enforce more before refs move, depends on Cloudflare.
10098
101−### Push protection skips very large pushes
99+### Very large pushes are scanned after they land, not before
102100
103−Very large pushes, by size or by number of commits, are not yet fully
104−scanned for secrets. Most pushes are scanned in full; we are raising the
105−limit by streaming the scan instead of reading the whole push at once.
101+A very large push is too large for push protection to read before it is
102+stored, so it is let through, and g1t scans every commit it added
103+afterwards, in the background. A secret found that way is an open alert
104+rather than a refused push, and the workspace's owners are emailed when one
105+looks real. Most pushes are scanned before they land.
106106
107107 - **Why.** Scanning reads the whole push inside a Worker, which has 128 MB
108− for everything it is doing at once. Past that size, scanning could fail
109− the push outright.
110−- **Instead.** Push large histories in steps (see above), so each push is
111− scanned. Secrets already in history are listed under
108+ for everything it is doing at once. Past a certain size, scanning could fail
109+ the push outright, and refusing such pushes would block importing real
110+ repositories.
111+- **Instead.** To have a large history checked before it lands, push it in
112+ steps (see above), so each push is scanned first. Secrets already in
113+ history are listed under
112114 [Secrets in history](/guides/security/#secrets-in-history).
113115 - **Status.** Planned: scanning while the push streams, at any size.
114116
140142 in your workspace; pushing creates it.
141143 - **Status.** Not scheduled.
142144
143−### Pull request forks are kept after they close
145+### Pull request forks are removed a week after they close
144146
145−A pull request's fork stays after the pull request merges or closes.
147+Seven days after a pull request merges or closes, its fork's git data is
148+removed. The pull request's changes stay readable: its head is kept in the
149+repository as `refs/pull/<pull request id>/head`. Pushing to the fork, or
150+reopening the pull request, makes the fork again from there.
146151
147152 - **Why.** Cloudflare has not documented whether a fork shares stored
148− objects with its source or copies them, and the answer decides how and
149− when forks should be cleaned up.
150−- **Instead.** Nothing you need to do.
151−- **Status.** Deleting forks some time after their pull request closes is
152− planned. Clear fork storage rules depend on Cloudflare.
153+ objects with its source or copies them, so forks are not kept longer
154+ than they are useful.
155+- **Instead.** Nothing you need to do. To keep working on a closed pull
156+ request's change, fetch `refs/pull/<pull request id>/head` and push it
157+ to a branch.
158+- **Status.** Clear fork storage rules depend on Cloudflare.
153159
154160 ### No conflict resolution in the browser
155161
163169
164170 ### No Docker in g1t's sandboxes
165171
166−On g1t's own machines, Docker container actions, `services:` containers and
167−`container:` do not run, and a step cannot run `docker build`.
172+On g1t's own machines, a job's `container:` image is not used (its steps
173+run on g1t's runner image instead), and a step cannot run `docker build`.
174+Docker container actions (`uses: docker://…`, or an action that runs as a
175+Docker image) and `services:` containers, such as a database, do not run
176+on any runner yet, self-hosted ones included.
168177
169178 - **Why.** Jobs run in Cloudflare Containers, which offer no supported way
170179 to run Docker or another image builder inside a container.
171−- **Instead.** Run those jobs on a [self-hosted runner](/guides/self-hosted-runners/).
172− A runner in Docker mode runs each job in its `container:` image. To build
173− images, register a runner with `--no-docker` on a machine that has Docker,
174− and its steps can call `docker build` and `docker push`. Self-hosted time
175− costs nothing.
176−- **Status.** Image builds on g1t's machines depend on Cloudflare.
180+- **Instead.** Run `container:` jobs and image builds on a
181+ [self-hosted runner](/guides/self-hosted-runners/). A runner in Docker
182+ mode runs each job in its `container:` image. To build images, register a
183+ runner with `--no-docker` on a machine that has Docker, and its steps can
184+ call `docker build` and `docker push`. Self-hosted time costs nothing. For
185+ a database, start it from a `run:` step on a self-hosted runner.
186+- **Status.** Docker container actions and `services:` are planned. Image
187+ builds on g1t's machines depend on Cloudflare.
177188
178189 ### Linux only on g1t's machines
179190
190201 | --- | --- |
191202 | Largest machine | 4 vCPUs, 12 GiB of memory, 20 GB of disk (`g1t-4core`). No GPUs. |
192203 | One job on g1t's machines | 60 minutes. On a self-hosted runner, 24 hours. |
193−| One cache entry | 2 GB, compressed. A larger one is not saved. |
194−| A repository's caches | 10 GB together. Past it, the entries restored longest ago are removed. |
204+| One cache entry | 2 GiB, compressed. A larger one is not saved. |
205+| A repository's caches | 10 GiB together. Past it, the entries restored longest ago are removed. |
195206 | One artifact | 60 MB, kept for 14 days |
196207
197208 The machine sizes are Cloudflare Containers' instance sizes. For more, use a
233244 - **Why.** Each of these is a resource g1t has to create and bill per
234245 project, and that is not built yet.
235246 - **Instead.** Check that a binding exists before using it. The deployment
236− lists each one it left out.
247+ lists each binding it left out, and warns when its cron triggers will
248+ not run.
237249 - **Status.** Planned. See [Workers projects](/guides/deployments/#workers-projects).
238250
239251 ### Build and size limits
275287
276288 ### Payments are in test mode
277289
278−While payments are in test mode, no real card is charged, and g1t's hosted
279−models are open only to g1t's own workspaces.
290+While payments are in test mode, no real card is charged, so a card check
291+proves nothing. g1t's hosted models are open only to a few invited
292+workspaces, g1t's own among them. Every other workspace, trial or not,
293+runs its agents on its own model provider; without one, assigning an agent
294+is refused with a message that says so.
280295
281296 - **Instead.** Connect your own [model provider](/guides/models/). Your
282297 agents then run on your keys, and the provider bills you directly.
+15−12
77
88 We're the small team at Flagon, Inc. building g1t, a git platform where
99 people and coding agents work in the same issues, pull requests and merge
10−queue. Every part of it runs on you: about twenty Workers, a D1 database per
11−service, Artifacts for every repository and every pull request, Containers
12−for agents and CI, R2, KV, Queues, and Cloudflare for SaaS for our customers'
13−domains. We have no servers.
10+queue. Every part of it runs on you: about twenty Workers, a D1 database for
11+each service that keeps data, Artifacts for every repository and every pull
12+request's fork, Containers for agents and CI, R2, KV, Queues, and Cloudflare
13+for SaaS for our customers' domains. We have no servers.
1414
1515 This is a thank-you, and a list of what would help us most next.
1616
3232 every page we render, blame, mergeability and search, without a git client
3333 anywhere.
3434
35−The rest of the platform held up too. Rust compiled to WebAssembly runs our
36−services. D1's read replication is free and good. Containers gave us
35+The rest of the platform held up too. Rust compiled to WebAssembly runs most of
36+our services, and TypeScript the rest. D1's read replication is free and good. Containers gave us
3737 sandboxes in three sizes. With a cached credential and ref listing, a
3838 `git fetch` with nothing new answers in under half a second, and most of
3939 our pages answer in under 250 ms. We went from an empty repository to a
5656 **What a fork stores.** Forks are the natural primitive for a pull request,
5757 and agents open pull requests by the thousand. We can't find whether a fork
5858 shares objects with its source or copies them. If it copies, an agent-heavy
59−account reaches the 1 TB account limit in days, and at that point every push
59+account reaches the 1 TB account limit in days (it can be raised on
60+request, but only by asking), and at that point every push
6061 in the account fails, for every customer at once. We keep forks for now,
6162 and are measuring it ourselves.
6263
6566 git's wire protocol to our own storage from inside a Worker, buffering packs
6667 in an isolate with 128 MB to share. With no hook before refs move, branch
6768 protection and secret scanning only hold for pushes through our proxy, which
68−parses every pack in WebAssembly before forwarding it. We wrote a second
69+parses each pack in WebAssembly before forwarding it, up to the size an
70+isolate can hold. We wrote a second
6971 implementation of git's pack format to get there.
7072
7173 **Ref-change events and the cost of a credential.** Push events need one
8284 measure each Worker by hand.
8385
8486 **D1 sessions across service bindings.** Read replicas need a bookmark to
85−give read-your-writes. Our site calls seven services, each with its own
86−database, so we built a header protocol to carry bookmarks through service
87+give read-your-writes. Our site reads from seven services, each with its
88+own database, so we built a header protocol to carry bookmarks through service
8789 bindings into a cookie and back.
8890
8991 **Containers that build images and keep disks.** We found no supported way
99101 ## What we built in the meantime
100102
101103 A per-workspace operation counter that is our best guess at your invoice. A
102−fork sweep we can switch on once we know what forks cost. A smart HTTP
104+smart HTTP
103105 client inside a Worker for landing, catch-up, mirrors and imports. Our own
104106 push policy in front of Artifacts. A versioned ref cache with a test that
105107 guards it. Two layers of credential caching. A bookmark protocol for D1.
107109 `Server-Timing` header on every response so we see the next regression.
108110 Image builds on a laptop.
109111
110−All of it works. Most of it is code we'd happily delete.
112+All of it works. Most of it is code we'd happily delete. Next is a fork
113+sweep, to switch on once we know what forks cost.
111114
112115 ## What we're asking for
113116
+7−5
33 description: Why a pull request on g1t gets its own fork, when a branch is the better choice, and what each costs.
44 ---
55
6−On most forges, a pull request comes from a branch of the repository. g1t
7−has those too. But an agent's pull request comes from a **fork**: a separate
6+A pull request can come from a branch of the repository. But an agent's
7+pull request comes from a **fork**: a separate
88 repository that starts as a copy of yours. This page explains why, what it
99 costs, and when to use which.
1010
3838 listed to every client on every fetch, and each needing cleanup once the
3939 work is merged or closed.
4040
41−Forks add nothing to the repository. A closed pull request is a fork that is
42−never looked at again. The repository's own refs stay the handful that
43−describe the project.
41+Forks add nothing to the repository's branches. A week after a pull request
42+merges or closes, its fork is removed, and only its head is kept in the
43+repository, as `refs/pull/<pull request id>/head`, which clones and fetches
44+do not download. The repository's branches stay the handful that describe
45+the project.
4446
4547 ### Each pull request has its own capacity
4648
+13−9
184184 request's head commit, or, in a repository that merges through
185185 [the merge queue](/guides/merge-queue/), adds it to the queue.
186186
187−A pull request can only merge if it contains everything already on `main`.
188−If something else landed first, merging is refused and the pull request is
189−**behind**. Its page says so before you try.
187+`main` only moves forward to a commit that contains everything already on
188+it. If something else landed first, the pull request is **behind**, and its
189+page says so. Merging it then brings it up to date first and lands it once
190+that is done, unless the repository requires pull requests to be up to date
191+before they merge; then merging is refused until it has caught up.
190192
191−**Catch up with main** fixes that. When the pull request and `main` changed
193+**Catch up with main** brings it up to date. When the pull request and `main` changed
192194 different files, g1t merges `main` in itself and pushes the merge in a few
193195 seconds. When they changed some of the same files, a g1t agent merges `main`
194196 into the pull request in a sandbox: if the merge is clean, it is pushed as it
231233 [What g1t can't do yet](/about/limitations/)):
232234
233235 - **Milestones.**
234−- **g1t agents for everyone.** g1t can put its own agents on an issue, each
235− in a sandbox. A workspace that connects its own model provider can use
236− them today on the [g1t plan](/guides/usage-and-billing/#the-g1t-plan)
237− or the one-time $5 trial after a card check.
238− See [the trial](/guides/usage-and-billing/#the-trial).
236+- **g1t's hosted models for everyone.** g1t can put its own agents on an
237+ issue, each in a sandbox, on the
238+ [g1t plan](/guides/usage-and-billing/#the-g1t-plan) or the one-time $5
239+ [trial](/guides/usage-and-billing/#the-trial) after a card check. Agents
240+ run on the workspace's own model provider, or on g1t's hosted models
241+ while its trial has credit; hosted models open to every workspace once
242+ payments go live. See [model providers](/guides/models/).
+16−15
219219
220220 ## Through the API
221221
222−Every route is in the [API reference](/reference/api/). Each has an MCP
223−tool of the same name.
222+Every route is in the [API reference](/reference/api/). Each is also an
223+action of an [MCP tool](/reference/mcp/): `access` for a repository's
224+people and the base permission, `account` for invitations to you.
224225
225−| Route | MCP tool | What it does | Who |
226+| Route | MCP tool and action | What it does | Who |
226227 | --- | --- | --- | --- |
227−| `GET /repos/{owner}/{name}/collaborators` | `list_collaborators` | Everyone with access to a repository, their role and where it comes from. | Write |
228−| `GET /repos/{owner}/{name}/collaborators/{username}/permission` | `get_collaborator_permission` | One person's role on a repository and what it lets them do. | Write, or about yourself |
229−| `POST /repos/{owner}/{name}/collaborators` | `add_collaborator` | Give someone a role. Body: `invitee` (a username or an email address) and `role`. Answers with `result`: `granted` or `invited`. | Admin |
230−| `PATCH /repos/{owner}/{name}/collaborators/{username}` | `update_collaborator` | Change someone's role, or a pending invitation's. Body: `role`. | Admin |
231−| `DELETE /repos/{owner}/{name}/collaborators/{username}` | `remove_collaborator` | Take away the role given to someone on the repository. | Admin, or yourself |
232−| `GET /repos/{owner}/{name}/invitations` | `list_repo_invitations` | A repository's pending invitations. | Admin |
233−| `DELETE /repos/{owner}/{name}/invitations/{id}` | `revoke_repo_invitation` | Withdraw a pending invitation. | Admin |
234−| `GET /user/repository_invitations` | `list_my_repo_invitations` | The invitations waiting for you. | You |
235−| `PATCH /user/repository_invitations/{id}` | `accept_repo_invitation` | Accept one. | You |
236−| `DELETE /user/repository_invitations/{id}` | `decline_repo_invitation` | Decline one. | You |
237−| `PATCH /workspaces/{workspace}` | `set_base_permission` | Set the base permission. Body: `base_permission`: `none`, `read`, `write` or `admin`. | Owners |
238−| `GET /workspaces/{workspace}/outside_collaborators` | `list_outside_collaborators` | A workspace's outside collaborators and the repositories each can reach. | Owners |
228+| `GET /repos/{owner}/{name}/collaborators` | `access` `list_collaborators` | Everyone with access to a repository, their role and where it comes from. | Write |
229+| `GET /repos/{owner}/{name}/collaborators/{username}/permission` | `access` `get_permission` | One person's role on a repository and what it lets them do. | Write, or about yourself |
230+| `POST /repos/{owner}/{name}/collaborators` | `access` `add_collaborator` | Give someone a role. Body: `invitee` (a username or an email address) and `role`. Answers with `result`: `granted` or `invited`. | Admin |
231+| `PATCH /repos/{owner}/{name}/collaborators/{username}` | `access` `update_collaborator` | Change someone's role, or a pending invitation's. Body: `role`. | Admin |
232+| `DELETE /repos/{owner}/{name}/collaborators/{username}` | `access` `remove_collaborator` | Take away the role given to someone on the repository. | Admin, or yourself |
233+| `GET /repos/{owner}/{name}/invitations` | `access` `list_invitations` | A repository's pending invitations. | Admin |
234+| `DELETE /repos/{owner}/{name}/invitations/{id}` | `access` `revoke_invitation` | Withdraw a pending invitation. | Admin |
235+| `GET /user/repository_invitations` | `account` `list_repository_invitations` | The invitations waiting for you. | You |
236+| `PATCH /user/repository_invitations/{id}` | `account` `accept_repository_invitation` | Accept one. | You |
237+| `DELETE /user/repository_invitations/{id}` | `account` `decline_repository_invitation` | Decline one. | You |
238+| `PUT /workspaces/{workspace}/base_permission` | `access` `set_base_permission` | Set the base permission. Body: `base_permission`: `none`, `read`, `write` or `admin`. `PATCH /workspaces/{workspace}` (`workspace` `update`) takes `base_permission` too, with the `access:admin` scope. | Owners |
239+| `GET /workspaces/{workspace}/outside_collaborators` | `access` `list_outside_collaborators` | A workspace's outside collaborators and the repositories each can reach. | Owners |
239240
240241 Changing who has access, answering an invitation and setting the base
241242 permission are for people, signed in or with a personal access token.
+27−14
4444 | `secrets.*`, `vars.*`, `secrets.GITHUB_TOKEN` | The same. `secrets.G1T_TOKEN` is the workspace's own token for the run; `GITHUB_TOKEN` is its alias. |
4545 | `environment:` on a job | The job reads each key's row for that environment, as GitHub's environment secrets work. |
4646 | `actions/upload-artifact`, `actions/download-artifact` | Kept with the run for 14 days, passed between its jobs, and downloadable from the run's page. Up to 60 MB each. |
47−| `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository, found by `key` or the newest under a `restore-keys` prefix. `path` takes globs and `!` exclusions. Up to 2 GB each; see [the cache](#the-cache). |
47+| `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository, found by `key` or the newest under a `restore-keys` prefix. `path` takes globs and `!` exclusions. Up to 2 GiB each; see [the cache](#the-cache). |
4848
4949 The **Actions** page of a workflow says, under *How this runs on g1t*,
5050 anything in it that runs differently.
5555 job with `runs-on: windows-latest` or `macos-latest` fails, and says so.
5656 [Self-hosted runners](/guides/self-hosted-runners/) of any OS run them:
5757 `runs-on: [self-hosted, windows]`.
58−- **Docker** container actions, `services:` containers and `container:`.
58+- **Docker** container actions, `services:` containers and `container:` on
59+ g1t's machines. A job's `container:` is ignored there and its steps run on
60+ g1t's image; a [self-hosted runner](/guides/self-hosted-runners/#what-a-job-gets)
61+ that runs jobs in Docker uses it.
5962 - **Reusable workflows from other repositories** (`uses: owner/repo/.github/workflows/x.yml@v1`); ones in the same repository work.
6063 - **The toolkit's own cache.** Actions that cache through GitHub's service
6164 themselves, such as `actions/setup-node` with `cache: npm`, run without
7376 Jobs run in a fresh sandbox each: Debian with Node 24, Python 3, Go, Rust,
7477 `build-essential`, `git`, `curl`, `jq` and passwordless `sudo`, in GitHub's
7578 layout (`/home/runner/work`, `RUNNER_TEMP`, `RUNNER_TOOL_CACHE`).
76−`runner.os` is `Linux`. `ubuntu-latest`, `ubuntu-24.04`, `self-hosted` and
77−other Linux labels all run here. Setup actions such as
79+`runner.os` is `Linux`. `ubuntu-latest`, `ubuntu-24.04` and other Linux
80+labels all run here. A job whose `runs-on` names `self-hosted` waits for one
81+of your [self-hosted runners](/guides/self-hosted-runners/) instead. Setup actions such as
7882 `actions/setup-node` and `actions/setup-python` install other versions as
7983 they do on GitHub.
8084
102106 Builds that compile, such as Rust or a large TypeScript project, finish
103107 several times faster on one.
104108
105−A job runs for at most 60 minutes, whatever its `timeout-minutes`, and
106−for less if the workspace's plan caps runs lower (a new workspace's first
107−month, or the trial). A job stopped at its time cap fails saying so.
109+A job on g1t's machines runs for at most 60 minutes, whatever its
110+`timeout-minutes`; one on a self-hosted runner can run for up to 24 hours.
111+A job stopped at its time cap fails saying so.
108112
109113 ### What a job can reach
110114
139143
140144 | | |
141145 | --- | --- |
142−| One entry | Up to 2 GB, compressed. A larger one is not saved, and the job goes on. |
143−| A repository's entries | Up to 10 GB together. Saving past it removes the entries restored longest ago. |
146+| One entry | Up to 2 GiB, compressed. A larger one is not saved, and the job goes on. |
147+| A repository's entries | Up to 10 GiB together. Saving past it removes the entries restored longest ago. |
144148 | How long | Until it has not been restored for 7 days, and at most 28 days after it was saved. |
145149 | Keys | Written once: saving under a key that exists does nothing. A restore finds its `key` exactly, else the newest entry whose key starts with one of its `restore-keys`. |
146150 | `path` | Files and folders; globs, `**` included; `~/` is the home folder; a line starting with `!` leaves matching paths out. |
233237 on `pull_request`, on `push` to the default branch, and on `merge_group`.
234238 3. Change it on the pull request if the steps are not how your project
235239 builds, and merge it.
236−4. Once it has run, `CI` is offered under **Required status checks**.
240+4. Once it has run, `CI` is offered under **Require status checks to pass
241+ before merging**.
237242 Require it, so that nothing merges into the default branch unless it
238243 passes.
239244
269274
270275 Jobs run in g1t's sandboxes, so they need the
271276 [g1t plan](/guides/usage-and-billing/#the-g1t-plan) or
272−[the trial](/guides/usage-and-billing/#the-trial). On a public repository,
277+[the trial](/guides/usage-and-billing/#the-trial); jobs on
278+[self-hosted runners](/guides/self-hosted-runners/#billing) need neither.
279+On a public repository,
273280 [g1t's open-source pool](/guides/usage-and-billing/#the-open-source-pool)
274281 runs them too, after a card check, until the month's pool is spent.
275282
279286 [sandbox time](/guides/usage-and-billing/#sandbox-time), from the first
280287 second. A job billing refuses does not start: it is recorded as failed
281288 with "Not started:" and the reason, such as "Workflows run in g1t's
282−sandboxes, which need a paid workspace", and what to do about it. The
289+sandboxes, which cost real money, so they need the g1t plan ($20 a month)
290+or a card check", and what to do about it. The
283291 Actions page tells people with Write on a repository whose workspace
284292 cannot run jobs before the first run.
285293
298306 | `cancel` | `POST /repos/{owner}/{repo}/actions/runs/{id}/cancel` |
299307 | `rerun` | `POST …/runs/{id}/rerun`, or `…/rerun-failed-jobs` |
300308 | `update` | `PUT …/workflows/{workflow}/enable` and `…/disable` |
301−| `list_actions_secrets`, `set_actions_secret`, `delete_actions_secret` | `GET`, `PUT` and `DELETE /repos/{owner}/{repo}/actions/secrets/{name}` |
302−| `list_actions_variables`, `set_actions_variable`, `delete_actions_variable` | `GET` and `POST /repos/{owner}/{repo}/actions/variables`, `PATCH` and `DELETE …/variables/{name}` |
303309
310+Secrets and variables have a tool of their own, `secret`:
311+
312+| `secret` action | Route |
313+| --- | --- |
314+| `list_secrets`, `set_secret`, `delete_secret` | `GET /repos/{owner}/{repo}/actions/secrets`, `PUT` and `DELETE …/secrets/{name}` |
315+| `list_variables`, `set_variable`, `delete_variable` | `GET` and `POST /repos/{owner}/{repo}/actions/variables`, `PATCH` and `DELETE …/variables/{name}` |
316+
304317 Workspace secrets and variables are under
305318 `/workspaces/{workspace}/actions/secrets` and `…/variables`. The fields
306319 g1t adds (environments, who reads a row, linked repositories) are in
+1−1
2929 | `workspace.base_permission_changed` | An owner changed what members get on every repository. |
3030
3131 Through the API and the MCP server, the call itself is recorded under its
32−tool's name too, such as `delete_repo`. See
32+operation's name too, such as `delete_repo`. See
3333 [managing a repository](/guides/managing-repositories/).
3434
3535 Refusals are recorded too, with the rule that refused them. Entries are
+4−4
332332
333333 | Scope | What it lets a token do |
334334 | --- | --- |
335−| `repo:read` | See repositories, their settings, labels and timelines, and search |
335+| `repo:read` | See repositories, their settings, labels, timelines and security alerts, and search |
336336 | `repo:write` | Create repositories, rename branches and change how pull requests merge |
337−| `repo:admin` | Rename, archive, transfer, delete or change who can see a repository |
337+| `repo:admin` | Rename, archive, transfer, delete or change who can see a repository, and dismiss security alerts |
338338 | `code:read` | Clone and fetch private repositories with git |
339339 | `code:write` | Push commits with git |
340340 | `issues:read` | Read issues, comments and plans |
394394 | Preset | Scopes |
395395 | --- | --- |
396396 | Read only | Every `read` scope. Changes nothing. |
397−| Agent | Every `read` scope, and `code:write`, `issues:write`, `pull_requests:write`, `agents:run` and `memory:write`. Reads everything, works on issues and pull requests, pushes code and runs g1t agents. No admin scope. |
397+| Agent | Every `read` scope except `runners:read`, and `code:write`, `issues:write`, `pull_requests:write`, `agents:run` and `memory:write`. Reads everything, works on issues and pull requests, pushes code and runs g1t agents. No admin scope. |
398398 | CI | `repo:read`, `code:read`, `code:write`, `workflows:read` and `workflows:write`. Clones and pushes code, and runs workflows. |
399399 | Full access | Everything you can do, including deleting repositories and changing who has access. Marked **Dangerous**. |
400400
469469 everything you can.
470470
471471 An application that asks for no scopes in particular gets the
472−[Agent preset](#presets): every `read` scope, and `code:write`,
472+[Agent preset](#presets): every `read` scope except `runners:read`, and `code:write`,
473473 `issues:write`, `pull_requests:write`, `agents:run` and `memory:write`.
474474 It never gets an admin scope unless it asks for one and you leave it
475475 ticked.
+2−2
4545
4646 | Choose | For an agent |
4747 | --- | --- |
48−| Scopes | The **Agent** preset: every `read` scope, and `code:write`, `issues:write`, `pull_requests:write`, `agents:run` and `memory:write`. Untick `agents:run` if it should not start g1t's agents, which spends the workspace's money. |
48+| Scopes | The **Agent** preset: every `read` scope except `runners:read`, and `code:write`, `issues:write`, `pull_requests:write`, `agents:run` and `memory:write`. Untick `agents:run` if it should not start g1t's agents, which spends the workspace's money. |
4949 | Expires | The shortest that fits the work, such as 30 days. |
5050
5151 A token reaches every workspace and repository you can. To keep an agent
151151 each, compare them, and read each one's check results from `get`. It can
152152 leave findings on specific lines with `issue` and `comment`, give a verdict
153153 with `pull_request` and `review`, and, if its account is a member of the
154−workspace, `merge` the best one. It cannot review a pull request it opened.
154+workspace, `merge` the best one. It cannot approve or request changes on a pull request it opened.
155155
156156 ## Filing issues from another system
157157
+11−8
4343 ## Turn on deployments
4444
4545 1. **Start the g1t plan for the workspace**, if it is not on. An owner
46− opens **Settings → Billing**, `g1t.sh/<workspace>/-/billing`, and
47− starts it under **Plans**. Back on Billing, the plan says **On** with
48− its renewal date. The trial does not pay for deployments.
46+ opens **Settings → Billing and plans**, `g1t.sh/<workspace>/-/billing`,
47+ and chooses **Start the g1t plan** on the **g1t** card. Back on
48+ Billing, the card says **On the g1t plan**. The trial does not pay for
49+ deployments.
4950 2. **Turn on deployments for a project.** Someone with the Admin role on
5051 its repository opens the project's
5152 **Settings → Deployments**, `g1t.sh/<workspace>/<project>/settings/deployments`,
9596
9697 - Your Worker's `fetch` handler runs as written, and its static assets
9798 are served under the binding name your config gives them. Cron triggers
98− in the config are not scheduled.
99+ (`triggers.crons`) are not scheduled, so a `scheduled` handler never
100+ runs; the deployment says so in its warnings.
99101 - `vars` are deployed as plain-text bindings (or JSON, for objects). Rows
100102 of the project's [secrets and variables](/guides/secrets-and-variables/)
101103 available to Deployments are bound too, and replace a `var` of the same
398400 network is restricted to the project's allowed domains, the package
399401 registries, GitHub and Cloudflare's API, as
400402 [workflow jobs'](/guides/actions/#what-a-job-can-reach) are, and it
401− stops at 30 minutes, or sooner if the workspace's plan caps runs lower.
403+ stops at 30 minutes.
402404 When it stops, what it cost is settled against what was reserved.
403405 3. The sandbox builds, then lists its files. g1t opens an upload with
404406 Cloudflare for exactly those files and hands the sandbox a key that can
419421
420422 | You see | Do |
421423 | --- | --- |
422−| "Deployments is a paid feature, and it is not on" | An owner starts the g1t plan under Billing. |
423−| "Not started" or "Deployments need a paid workspace" | The build was refused before it started. Follow the link in the message: start the plan, or raise the spend limit, on Billing. |
424+| "Deployments come with the g1t plan … and `<workspace>` does not have it" | An owner starts the g1t plan under Billing. |
425+| A deployment **Skipped**, saying it reached its spend limit or that new compute is paused | The build was refused before it started. Follow the link in the message: raise the spend limit, prepay, or answer the spike, on Billing. |
424426 | "Stopped: unusual CPU use" | The build looked like it was mining. Contact support if it was a real build; see [abuse and mining](/guides/guardrails/#abuse-and-mining). |
425427 | "403" from a host in the build log | The build's network is restricted. Add the host to the project's allowed domains under **Settings → Guardrails**. |
426428 | "found nothing to serve" | Add a `build` script, an `index.html`, or a Workers config; or set **Output directory**. |
427429 | "the output directory `x` does not exist after the build" | The build wrote elsewhere: check its log, then fix **Output directory**. |
428430 | "`d1_databases` is not provisioned on g1t.page yet" | The app deployed without that binding. See [Workers projects](#workers-projects). |
431+| "`triggers.crons` (1 schedule) is not set up on g1t.page yet" | The app deployed, but its `scheduled` handler never runs. See [Workers projects](#workers-projects). |
429432 | A preview page says "This preview is not up" | It came down (see [When apps come down](#when-apps-come-down)). Push, or choose **Redeploy**. |
430433 | "Custom domains are being switched on" | Custom domains are not on for g1t.page yet. Domains you add are kept, and set up by themselves once they are. |
431434 | A domain stays at **Waiting for DNS** | Check the record against the one listed, remove other `A`/`AAAA` records for the same name, then choose **Check now**. |
432435 | A domain says "This domain is not set up" | It points at g1t, but no project has added it. Add it under **Settings → Domains**. |
433−| "The build did not finish in 45 minutes" | Builds are stopped after 45 minutes. Make the build faster, or build less for previews with **Build command**. |
436+| "The build did not finish in 45 minutes" | The build stopped reporting: a build stops at 30 minutes, and one not heard from after 45 is failed. Make the build faster, or build less for previews with **Build command**. |
434437
435438 ## Running your own g1t
436439
+29−18
360360
361361 ## Other things g1t agents do
362362
363−- **Review.** On a pull request that is ready, **Review by a g1t agent**
363+- **Review.** On a pull request that is ready, **Request review from g1t agent**
364364 has an agent read the change and post comments on lines, a summary and a
365365 verdict.
366366 - **Catch up.** When `main` has moved under a pull request, **Catch up with
368368 itself in seconds, with no agent; otherwise an agent merges it in a
369369 sandbox and resolves any conflict
370370 ([catching up](/guides/pull-requests/#catching-up)).
371+- **Finish a security update.** g1t raises a vulnerable dependency to its
372+ fixed version itself, with no agent. When raising the version is not
373+ enough (the bump fails, or the pull request's required checks fail
374+ because code must change), g1t opens an issue and puts g1t-agent on it.
375+ That session shows as **started by g1t** on the project's **Agents** page.
376+ See [security updates](/guides/security/#security-updates).
371377
372−Both run in sandboxes of their own.
378+Each runs in a sandbox of its own.
373379
374380 ## Mentioning g1t-agent
375381
382388 | An issue | A request: `@g1t-agent take this`, `@g1t-agent fix the empty case` | The issue is assigned to the agent, which opens a pull request, as if you had chosen **Assign**. |
383389 | An issue | A question: `@g1t-agent why does search time out?` | The agent reads the code on the default branch and answers in the thread. It changes nothing. |
384390 | A pull request g1t-agent made | A request: `@g1t-agent also handle the empty list` | The agent is sent back to make the change, with your comment as what to address, and the checks and review run again. If it is still working, it gets your comment as a message at its next step. |
385−| Any pull request | `@g1t-agent review` | A review by a g1t agent, as with **Review by a g1t agent**. |
391+| Any pull request | `@g1t-agent review` | A review by a g1t agent, as with **Request review from g1t agent**. |
386392 | Any pull request | A question | The agent reads the change at its head and answers in the thread. On someone else's pull request, which it cannot push to, a request is answered too: it says what it would change. |
387393
388394 A request is a comment whose words after the mention start with what to
447453
448454 ## How model traffic is routed
449455
450−g1t agents send model requests through
451−[Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/). The
452−gateway is where an operator sees each request, caps spend, caches, and
453−holds the provider's key so that no sandbox does. Each request is tagged
454−with the kind of work, the repository and the pull request, so spend can be
455−read per pull request.
456+g1t agents send model requests to g1t's model proxy at
457+`https://models.g1t.sh`, with a token for their run in place of a key; see
458+[your keys never reach a sandbox](/guides/models/#your-keys-never-reach-a-sandbox).
459+Requests for g1t's hosted models go on through
460+[Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/),
461+which holds g1t's key. Each of those requests is tagged with the kind of
462+work, the repository and the pull request, so spend can be read per pull
463+request. Requests for a workspace's own provider go to that provider.
456464
457−If you run your own copy of g1t, these settings on the runner control it:
465+If you run your own copy of g1t, these settings control it:
458466
459−| Setting | What it does |
460−| --- | --- |
461−| `AGENT_ROUTES` | The model for each kind of work: `implement`, `review`, `update` and `plan`. |
462−| `AI_GATEWAY_ID` | The gateway to route through. Empty sends requests to the provider directly. |
463−| `AI_GATEWAY_TOKEN` | Secret. Authenticates to the gateway. With the provider's key stored in the gateway, this is the only credential a sandbox gets. |
464−| `ANTHROPIC_API_KEY` | Secret. The provider's key, if the gateway does not hold it. |
467+| Setting | Where | What it does |
468+| --- | --- | --- |
469+| `AGENT_ROUTES` | Runner | The model for each kind of work: `implement`, `review`, `update` and `plan`. |
470+| `MODELS_URL` | Runner | Where sandboxes send model requests: the model proxy. |
471+| `AI_GATEWAY_ID` | Model proxy | The gateway hosted requests go through. Empty sends them to the provider directly. |
472+| `AI_GATEWAY_TOKEN` | Model proxy | Secret. Authenticates to the gateway. |
473+| `ANTHROPIC_API_KEY` | Model proxy | Secret. The provider's key, if the gateway does not hold it. |
465474
466475 ## What it costs
467476
468477 A workspace pays for the g1t agents that work on its repositories, after
469−they run: each run is charged what AI Gateway priced its model requests
470−at, plus 20%, and its sandbox by the second, at cost plus 20%. See
478+they run: each run is charged its sandbox by the second, at cost plus 20%,
479+and, on g1t's hosted models, what AI Gateway priced its model requests at,
480+plus 20%. A workspace's [own provider](/guides/models/) bills it for the
481+model directly. See
471482 [Usage and billing](/guides/usage-and-billing/) for how prices are set and
472483 the limits on usage not yet paid for.
473484 The workspace's **Usage** page shows what its agents have cost, by day,
+36−3
4141 ## Creating a repository by pushing
4242
4343 Pushing to a repository that does not exist, in a workspace you belong to,
44−creates it as a public repository.
44+creates it as a private repository, so nothing pushed by mistake is
45+published. To make it public, see
46+[change who can see a repository](/guides/managing-repositories/#change-who-can-see-a-repository).
4547
4648 ```sh
4749 git push https://g1t.sh/<workspace>/new-repo.git main
119121
120122 ## Limits
121123
122−Repositories are stored in Cloudflare Artifacts, which limits a repository to
123−1 GB and a single file to 32 MB. A single push is limited to 100 MB.
124+### Size limits
125+
126+Repositories are stored in Cloudflare Artifacts. g1t checks its limits
127+before a push is stored, and declines a push that would cross one. git
128+prints the reason beside each branch (`! [remote rejected] main (…)`), and
129+what to do as `remote:` lines. Nothing in a declined push is stored.
130+
131+| Limit | Size | What happens past it |
132+| --- | --- | --- |
133+| A file | 32 MB | The push is declined, naming the file's size. |
134+| A repository, with its pull requests' forks | 950 MB, as g1t counts what was pushed (the store holds 1 GB) | The push is declined; once full, pushes are refused with the reason before any data is sent. |
135+| A push that push protection can scan before it lands | Most pushes; very large ones are scanned after they land | A very large push goes through and is scanned after it lands; secrets found are open alerts. To have it checked first, push in parts, oldest commits first. |
136+| A push | 100 MB | Refused by the network with HTTP `413` before g1t sees it. |
137+
138+To push a large history in parts:
139+
140+```sh
141+git rev-list --reverse HEAD | awk 'NR % 500 == 0' | xargs -I{} git push origin {}:refs/heads/main
142+git push origin main
143+```
144+
145+Each push sends only what the one before did not.
146+
147+### When the store is busy
148+
149+If Cloudflare Artifacts is rate limiting g1t or not answering, g1t tries
150+reads again for a moment, then answers git with HTTP `429` (rate limited)
151+or `503` (unavailable) and a `Retry-After` header saying how many seconds
152+to wait. Pushes are never tried again on your behalf: run `git push`
153+again. On g1t.sh the page says the git storage is busy instead of failing,
154+and [status.g1t.sh](https://status.g1t.sh) shows **Git storage**.
155+
156+### Git operations
124157
125158 Each clone, fetch and push is a git operation. Every workspace has 50,000
126159 a month included. Past that, a workspace on the g1t plan pays $0.18 per
+9−4
216216 ## Caps
217217
218218 **Cost per run**: the most one run may spend on its model, in US dollars.
219−$5.00 by default; 0 means no cap; at most $100. The harness tracks the
220−run's spend as it goes and stops the agent when it reaches the cap.
219+$2.00 by default, the same as the workspace billing's spend cap per run;
220+0 means no cap here, though billing's cap still applies; at most $100. The
221+harness tracks the run's spend as it goes and stops the agent when it
222+reaches the cap.
221223
222224 **Time per run**: how long each kind of run may take, in minutes. By
223225 default:
237239 At most 240 minutes, but a run's credentials last two hours, so a longer
238240 cap does not give an agent more than that to push.
239241
240−**The workspace's plan** can set lower caps: a new paid workspace's first
241−month, and the trial, cap every run's time and cost (see
242+**The workspace's billing** sets caps too. Every workspace has a spend cap
243+per run, $2 by default, which owners can set from $0.10 to $100, so a run
244+stops at $2 unless an owner raises it (see
245+[caps](/guides/usage-and-billing/#caps)). A new paid workspace's first
246+month, and the trial, also cap every run's time at 60 minutes (see
242247 [who can run agents](/guides/g1t-agents/#who-can-run-agents)). A run gets
243248 the lower of its guardrails' cap and its plan's, for time and for cost,
244249 and its page shows the cap it got.
+3−3
88
99 | Kind | Systems | What it does |
1010 | --- | --- | --- |
11−| [Model providers](/guides/models/) | Anthropic, OpenAI, Google Gemini, and any Anthropic- or OpenAI-compatible endpoint | Your agents' model requests go to your own accounts, routed by kind of work. |
11+| [Model providers](/guides/models/) | Anthropic, OpenAI, Google Gemini, xAI, Mistral, DeepSeek, Azure OpenAI, OpenRouter, Groq, Together AI, Fireworks AI, Cerebras, and any Anthropic- or OpenAI-compatible endpoint | Your agents' model requests go to your own accounts, routed by kind of work. |
1212 | [Alerts](#alerts) | Sentry, Datadog, a signed webhook | A problem opens an issue, once however often it fires, and an agent can start on it at once. |
1313 | [Trackers](#trackers) | Jira, Linear | Agents read the tickets that work mentions, people import tickets as issues, and tickets hear back when the work lands. |
1414
219219 -d '{"provider": "jira", "config": {"site": "https://acme.atlassian.net", "email": "dev@acme.com", "keys": ["TECH"]}, "secret": "<api token>"}'
220220 ```
221221
222−`provider` is `anthropic`, `openai`, `gemini`, `anthropic_endpoint`,
223−`openai_endpoint`, `sentry`, `datadog`, `webhook`, `jira` or `linear`. `config` takes `repo`, `assign`, `label`,
222+`provider` is one of the [model providers](/guides/models/#from-the-api),
223+or `sentry`, `datadog`, `webhook`, `jira` or `linear`. `config` takes `repo`, `assign`, `label`,
224224 `write_back`, `organization`, `site`, `email`, `keys`, `base_url`,
225225 `auth_header` and `model`; each provider uses the ones above. For `datadog`
226226 and `webhook`, the response's `signing_secret` is the only time the secret
+18−11
6666 -d '{"description": "Launches things.", "website": "rocket.acme.dev", "topics": ["cli", "rust"]}'
6767 ```
6868
69−The MCP tool is `update_repo`, with `repo` and the same fields.
69+Over MCP, it is the `repository` tool's `update` action, with `repo` and
70+the same fields.
7071
7172 ## Change the default branch
7273
9697 -d '{"default_branch": "trunk"}'
9798 ```
9899
99−The MCP tool is `update_repo`, with `repo` and `default_branch`. When the
100+Over MCP, it is the `repository` tool's `update` action, with `repo` and
101+`default_branch`. When the
100102 same call changes other fields, they are changed first.
101103
102104 ## Rename a branch
135137 -d '{"new_name": "feature/sign-in"}'
136138 ```
137139
138−The MCP tool is `rename_branch`, with `repo`, `branch` and `new_name`.
140+Over MCP, it is the `repository` tool's `rename_branch` action, with
141+`repo`, `branch` and `new_name`.
139142
140143 ## Rename a repository
141144
182185 -d '{"name": "launcher"}'
183186 ```
184187
185−The MCP tool is `rename_repo`, with `repo` and `name`.
188+Over MCP, it is the `repository` tool's `rename` action, with `repo` and
189+`name`.
186190
187191 ## Change who can see a repository
188192
217221 -d '{"private": true, "confirm": "acme/rocket"}'
218222 ```
219223
220−The MCP tool is `set_repo_visibility`, with `repo`, `private` and
221−`confirm`. `private` on
224+Over MCP, it is the `repository` tool's `set_visibility` action, with
225+`repo`, `private` and `confirm`. `private` on
222226 [`PATCH /repos/{owner}/{name}`](/reference/api/repositories/update-repo/)
223227 makes the same change without the confirmation, for people with Admin.
224228
259263 -H "Authorization: Bearer $G1T_TOKEN"
260264 ```
261265
262−The MCP tools are `archive_repo` and `unarchive_repo`, with `repo`. The
266+Over MCP, they are the `repository` tool's `archive` and `unarchive`
267+actions, with `repo`. The
263268 repository's `archived_at` field says when it was archived, and is null
264269 when it is not.
265270
300305 -d '{"confirm": "acme/rocket"}'
301306 ```
302307
303−The MCP tool is `delete_repo`, with `repo` and `confirm`.
308+Over MCP, it is the `repository` tool's `delete` action, with `repo` and
309+`confirm`.
304310
305311 To move a repository to another workspace instead of deleting it, see
306312 [transferring a repository](/guides/transferring-repositories/).
323329
324330 From the API, list them with
325331 [`GET /workspaces/{workspace}/repos/deleted`](/reference/api/repositories/list-deleted-repos/)
326−(MCP: `list_deleted_repos`), then call
332+(the `repository` tool's `list_deleted` action over MCP), then call
327333 [`POST /repos/{owner}/{name}/restore`](/reference/api/repositories/restore-repo/)
328−with the path it had (MCP: `restore_repo`):
334+with the path it had (the `restore` action):
329335
330336 ```sh
331337 curl https://api.g1t.sh/workspaces/acme/repos/deleted \
356362 -d '{"confirm": "acme/rocket"}'
357363 ```
358364
359−The MCP tool is `purge_repo`, with `repo` and `confirm`.
365+Over MCP, it is the `repository` tool's `purge` action, with `repo` and
366+`confirm`.
360367
361368 A workspace whose only repositories are recently deleted ones can itself
362369 be [deleted](/guides/workspaces/#delete-a-workspace); they are purged with
+5−4
99 on, a pull request is tested together with everything ahead of it before it
1010 lands, and `main` only ever moves to a state whose required checks passed.
1111
12−The merge queue runs in g1t's sandboxes, which work in any workspace with
13−[its own model provider](/guides/models/) and in those g1t's hosted models
14−are open to. Elsewhere, an entry fails at once with a message saying so;
15−turn the queue off to merge directly.
12+The merge queue runs in g1t's sandboxes, which need
13+[the g1t plan](/guides/usage-and-billing/#the-g1t-plan) or the trial after
14+a card check; a public repository can use the open-source pool instead.
15+Without one, an entry fails at once with a message saying so; turn the
16+queue off to merge directly.
1617
1718 ## Turn it on
1819
+14−10
88 - **g1t's hosted models.** g1t chooses the model for each kind of work, pays
99 the provider, and charges your workspace what it cost plus 20%. The
1010 plan's included usage and [the trial](/guides/usage-and-billing/#the-trial)
11− pay for it first. Open only to g1t's own workspaces while payments are
12− in test mode; once they go live, open to all.
11+ pay for it first. While payments are in test mode, they are open only
12+ to a few invited workspaces, g1t's own among them; a card check or a
13+ trial does not open them. Every other workspace connects its own
14+ provider, and an agent assigned without one is refused with a message
15+ that says so. Once payments go live, they are open to all.
1316 - **Your own providers.** Connect as many as you use, then choose, for each
1417 kind of work, which provider and model it runs on. Each provider bills
1518 you directly. Open to every workspace now.
4144 gateway's token, sent as `cf-aig-authorization`.
4245
4346 g1t's agents run Claude Code, which speaks Anthropic's API. Every provider
44−but Anthropic speaks OpenAI's, so g1t's model proxy translates each
47+but Anthropic and an Anthropic-compatible endpoint speaks OpenAI's, so g1t's model proxy translates each
4548 request, and the streamed answer back, tool calls included, and meets each
4649 provider's quirks: the token limits DeepSeek and Groq set, how Mistral
4750 names a required tool, Azure's `api-key` header, and the thought signatures
7275 | Catching up | Bringing a change up to date with `main`. |
7376
7477 Each can go to g1t's models, or to any of your providers on any of its
75−models. An Anthropic provider also offers **g1t's choice of Claude model**,
78+models. An Anthropic provider also offers **g1t's choice of Claude**,
7679 which runs g1t's pick for that kind of work on your key. For example: make
7780 changes on Claude through your Anthropic key, review on GPT through your
7881 OpenAI key, and catch up on a small model through OpenRouter.
107110 work is routed to, translates if the provider speaks OpenAI's API, and
108111 forwards the request. Answers stream straight back.
109112
110−The token stops working when the run ends (three hours at most), or at once
111−if you disconnect the provider. Keys are sealed when you save them, and used
113+The token stops working within seconds of the run finishing, however it
114+ends, and within seconds if you disconnect the provider. A run whose end
115+g1t never hears about loses it three hours after it starts. Keys are sealed when you save them, and used
112116 only by the proxy. g1t's own runs work the same way, with g1t's key.
113117
114118 ## From the API
115119
116−| Tool | Route |
120+| MCP tool and action | Route |
117121 | --- | --- |
118−| `connect_integration` | `POST /workspaces/{workspace}/integrations` with `provider` one of `anthropic`, `openai`, `gemini`, `xai`, `mistral`, `deepseek`, `azure_openai`, `openrouter`, `groq`, `together`, `fireworks`, `cerebras`, `anthropic_endpoint`, `openai_endpoint` |
119−| `get_model_routes` | `GET /workspaces/{workspace}/model-routes` |
120−| `set_model_routes` | `PUT /workspaces/{workspace}/model-routes` |
122+| `workspace` `connect_integration` | `POST /workspaces/{workspace}/integrations` with `provider` one of `anthropic`, `openai`, `gemini`, `xai`, `mistral`, `deepseek`, `azure_openai`, `openrouter`, `groq`, `together`, `fireworks`, `cerebras`, `anthropic_endpoint`, `openai_endpoint` |
123+| `workspace` `get_model_routes` | `GET /workspaces/{workspace}/model-routes` |
124+| `workspace` `set_model_routes` | `PUT /workspaces/{workspace}/model-routes` |
121125
122126 ```sh
123127 curl -X PUT https://api.g1t.sh/workspaces/acme/model-routes \
+5−3
1010 it. g1t agents then work on the issues, as many at once as the dependencies
1111 allow, and the outcome page shows each one until it lands.
1212
13−Planning and g1t agents work in any workspace with
14−[its own model provider](/guides/models/), and in those g1t's hosted models
15−are open to. The agents' runs are charged to the workspace; see
13+Planning and g1t agents work in any workspace that
14+[can run agents](/guides/g1t-agents/#who-can-run-agents) (the plan or the
15+trial) and has a model: [its own model provider](/guides/models/), or g1t's
16+hosted models where they are open to it. The agents' runs are charged to
17+the workspace; see
1618 [usage and billing](/guides/usage-and-billing/). Planning work for a
1719 repository needs the Write [role](/guides/access-and-roles/) or higher on it; anyone who can
1820 read it can see its plans.
+9−3
9797 | --- | --- |
9898 | **General** | The project's name and description, its source, and its **root directory**. A project shows its repository's description, and follows it as it changes, until you give the project one of its own; **Use the repository's description** goes back. |
9999 | **Deployments** | Production, previews, build command, output directory and idle days. See [Deployments](/guides/deployments/#settings). |
100+| **Domains** | Custom domains for production. See [custom domains](/guides/deployments/#custom-domains). |
100101 | **Dependencies** | The projects this one uses, and the ones that use it. See [Dependencies](#dependencies). |
101−| **Secrets and variables** | The project's rows. See [Secrets and variables](/guides/secrets-and-variables/). |
102+| **Agents** | How g1t's agents pick up work here, and what they read first. |
103+| **Guardrails** | What agents may reach, run and spend while they work here. See [guardrails](/guides/guardrails/). |
102104 | **Repository** | The repository's name, description, website, [topics](/guides/search/#what-is-indexed) and default branch, and its danger zone: visibility, archive, transfer and delete. See [Managing a repository](/guides/managing-repositories/). |
105+| **Access** | Who has a [role](/guides/access-and-roles/) on the repository, and invitations. |
103106 | **Branches and merging** | Branch protection, [required status checks](/guides/pull-requests/#required-status-checks), required approvals, the merge queue, auto-merge and how g1t's agents review. |
107+| **Secrets and variables** | The project's rows. See [Secrets and variables](/guides/secrets-and-variables/). |
108+| **Runners** | The project's own [self-hosted runners](/guides/self-hosted-runners/), and where its agents' work runs. |
104109 | **Webhooks** | The repository's [webhooks](/guides/webhooks/). |
105110
106111 The **root directory** says where in the repository the project lives,
112117
113118 | Tab | Needs |
114119 | --- | --- |
115−| **General**, **Dependencies**, and on **Repository** its description, website and topics | Maintain |
120+| **General**, **Dependencies**, **Agents**, and on **Repository** its description, website and topics | Maintain |
116121 | **Branches and merging**, **Guardrails** | Maintain |
117−| **Deployments**, **Domains**, **Secrets and variables**, **Webhooks** | Admin |
122+| **Access**: seeing who has a role; changing it | Write; Admin |
123+| **Deployments**, **Domains**, **Secrets and variables**, **Runners**, **Webhooks** | Admin |
118124 | On **Repository**: its name, default branch, and the danger zone (visibility, archive) | Admin |
119125 | Transfer and delete | An owner of the workspace |
120126
+10−10
2222 | Field | |
2323 | --- | --- |
2424 | **Type** | **Secret**: sealed when saved and never shown again, hidden in logs. For passwords, API keys and tokens. **Config**: readable by whoever may see the list. For values that are not sensitive. Config can be changed to a secret; a secret can never become config. |
25−| **Key** | Letters, digits and underscores, upper-cased: `STRIPE_KEY`. Keys starting with `G1T_` or `GITHUB_` are g1t's own. |
25+| **Key** | Letters, digits and underscores, not starting with a digit, upper-cased: `STRIPE_KEY`. Keys starting with `G1T_` or `GITHUB_` are g1t's own. |
2626 | **Value** | Up to 48 KB. |
2727 | **Note** | Optional: where to rotate it, or who to ask. |
2828 | **Environments** | **All environments**, or only some: **Production**, **Preview**, or any name a workflow job uses in `environment:`, such as `staging`. |
113113 key named without an `id` or `environments` is the key's row for all
114114 environments.
115115
116−| Tool | Route |
116+| MCP (`secret` tool) | Route |
117117 | --- | --- |
118−| `list_actions_secrets` | `GET /repos/{owner}/{repo}/actions/secrets` |
119−| `set_actions_secret` | `PUT /repos/{owner}/{repo}/actions/secrets/{key}` |
120−| `delete_actions_secret` | `DELETE /repos/{owner}/{repo}/actions/secrets/{key}` |
121−| `list_actions_variables` | `GET /repos/{owner}/{repo}/actions/variables` |
122−| `set_actions_variable` | `POST /repos/{owner}/{repo}/actions/variables`, `PATCH …/variables/{key}` |
123−| `delete_actions_variable` | `DELETE /repos/{owner}/{repo}/actions/variables/{key}` |
118+| `list_secrets` | `GET /repos/{owner}/{repo}/actions/secrets` |
119+| `set_secret` | `PUT /repos/{owner}/{repo}/actions/secrets/{key}` |
120+| `delete_secret` | `DELETE /repos/{owner}/{repo}/actions/secrets/{key}` |
121+| `list_variables` | `GET /repos/{owner}/{repo}/actions/variables` |
122+| `set_variable` | `POST /repos/{owner}/{repo}/actions/variables`, `PATCH …/variables/{key}` |
123+| `delete_variable` | `DELETE /repos/{owner}/{repo}/actions/variables/{key}` |
124124
125125 A workspace's are under `/workspaces/{workspace}/actions/secrets` and
126126 `…/variables`. Beyond GitHub's fields, a row takes:
139139 -d '{"value":"sk_live_…","environments":["production"],"available_to":["deployments"]}'
140140 ```
141141
142−Unlike GitHub's, a secret is sent as plain `value` over HTTPS, not
143−encrypted to a public key.
142+A secret is sent as plain `value` over HTTPS, not encrypted to a public
143+key first.
+0−219
1−---
2−title: Security
3−description: How g1t keeps secrets out of your repositories, finds vulnerable dependencies, and has an agent land the upgrades that fix them.
4−---
5−
6−Every project has a **Security** page at `g1t.sh/<owner>/<project>/security`.
7−It shows what g1t has found, and what is being done about each finding:
8−
9−- **Secrets.** A push that adds a key or a token is refused before it lands,
10− and each repository's history is scanned once, in the background.
11−- **Dependencies.** Every package your lockfiles resolve is checked against
12− the [OSV](https://osv.dev) database of known vulnerabilities. Each
13− vulnerable package that has a fix gets an upgrade issue, and a g1t agent
14− lands the upgrade through the usual pull request, checks, review and merge
15− queue.
16−
17−Findings are the workspace's to fix, so the Security page needs a
18−[role](/guides/access-and-roles/) on the repository, whether the project
19−is public or private:
20−
21−| | Needs |
22−| --- | --- |
23−| See findings, **Re-scan now** | Write |
24−| **Upkeep agents** on or off | Maintain |
25−| **Allow**, **Resolve** or **Reopen** a secret | Admin |
26−
27−Someone with Read or Triage is told the page needs Write. The workspace's
28−own page, `g1t.sh/<owner>/-/security`, lists the open findings of every
29−project the member can see them on, most severe first.
30−
31−## The overview
32−
33−The top of the page counts open findings by severity: critical, high,
34−medium, low and unrated. A secret that is open, or that stopped a push,
35−counts as critical. Below the counts:
36−
37−- **Re-scan now** reads the dependencies again at once and scans the
38− history again from the start.
39−- **Upkeep agents** turns upgrade issues on or off for the project. It is on
40− unless someone turns it off. With it off, findings are still listed, and
41− nothing is opened for them.
42−
43−The **Secrets** and **Dependencies** tabs list each finding with its status
44−and the issue or pull request fixing it.
45−
46−## Secret scanning
47−
48−g1t looks for credentials whose format their issuer made recognisable, so a
49−match is nearly always a real secret or a fake made to look like one:
50−
51−| What | What it looks like |
52−| --- | --- |
53−| AWS access keys | `AKIA` or `ASIA` and 16 more characters |
54−| AWS secret access keys | 40 characters on a line that names an AWS secret |
55−| GitHub tokens | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, `github_pat_` |
56−| GitLab tokens | `glpat-`, `gloas-`, `glrt-`, `glptt-`, `gldt-` |
57−| Stripe live keys | `sk_live_`, `rk_live_` (test keys are left alone) |
58−| Slack | `xoxb-`, `xoxp-` and other tokens, and incoming webhook addresses |
59−| Google API keys | `AIza` and 35 more characters |
60−| Anthropic API keys | `sk-ant-` |
61−| OpenAI API keys | `sk-proj-`, `sk-svcacct-`, `sk-admin-`, and older `sk-` keys |
62−| Private keys | A PEM `BEGIN … PRIVATE KEY` header followed by the key |
63−| Service-role JWTs | A JWT whose claims carry `service_role` |
64−| npm tokens | `npm_` |
65−| g1t tokens | `g1t_` and 40 hex characters |
66−| SendGrid keys | `SG.` and two dotted parts |
67−
68−Placeholders such as `ghp_xxxxxxxx…` are not reported. Lockfiles, and files
69−under `node_modules/` and `vendor/`, are not scanned.
70−
71−g1t never stores a secret it finds. It keeps a fingerprint, so it can
72−recognise the same secret again, and a short preview, such as `AKIA…`, so
73−you can recognise it.
74−
75−### Push protection
76−
77−When a push over HTTPS adds a secret, g1t refuses the whole push and nothing
78−is stored. Only the lines the push adds are checked, so a secret that is
79−already in the repository does not block every later push to the same file.
80−This applies to every push, including the pushes g1t's agents make to their
81−pull requests.
82−
83−Git shows why, file and line:
84−
85−```text
86−$ git push
87−remote: g1t found a secret in this push, so nothing was pushed.
88−remote:
89−remote: config/prod.env:3 an AWS access key (commit 4807077)
90−remote:
91−remote: Take the secret out of the commit that adds it (git commit --amend, or
92−remote: git rebase -i for an older commit), rotate it if it was ever real, and
93−remote: push again.
94−remote:
95−remote: If it is not a real secret, such as a test fixture:
96−remote: - add g1t:allow-secret in a comment on its line, or
97−remote: - allow it once at https://g1t.sh/acme/rocket/security?tab=secrets&finding=sec_…
98−remote: Allowing is recorded with your name, then the same push goes through.
99−To https://g1t.sh/acme/rocket.git
100− ! [remote rejected] main -> main (secret found: config/prod.env:3 has an AWS access key)
101−```
102−
103−To fix a real secret:
104−
105−1. Remove it from the commit that adds it: `git commit --amend` for the last
106− commit, `git rebase -i` for an older one.
107−2. Rotate it with whoever issued it. Once a secret has been on any machine
108− but yours, treat it as known.
109−3. Push again.
110−
111−### Allowing a false positive
112−
113−A fake key in a test, or an example in documentation, is still reported, on
114−purpose: it looks exactly like a real one. Two ways to let it through:
115−
116−- **Mark the line.** Put `g1t:allow-secret` anywhere on the line, usually in
117− a comment. The line is never reported, in any push or in history.
118−
119− ```ts
120− const FIXTURE_KEY = "AKIA…"; // g1t:allow-secret
121− ```
122−
123−- **Allow it once.** Open the link in git's message (or the finding on the
124− **Secrets** tab), choose **Allow**, and say why. g1t records who allowed
125− it and why. Then push again, unchanged: that secret no longer stops a push
126− to this project.
127−
128−Allowing needs the Admin [role](/guides/access-and-roles/) on the repository. Someone else
129−pushing to a pull request's fork sees the same message and asks someone
130−who has it.
131−
132−### Secrets in history
133−
134−The first time g1t sees a repository (when it is created, on its next push
135−to the default branch, or when its Security page is first opened) it scans
136−the default branch's history in the background, a page of commits at a
137−time, comparing each commit with its first parent. The page shows how far
138−it has got. Scanning is metered to the workspace like other usage, and it
139−pauses if the workspace reaches its spending limit.
140−
141−A secret found in history is **Open**: it is in the repository, and anyone
142−who could clone it may have it. Rotate it, then choose **Resolve** and say
143−what you did. Removing it from the code is not enough, since it stays in
144−history.
145−
146−| Status | Meaning |
147−| --- | --- |
148−| Open | In the repository's history. Rotate it, then resolve it. |
149−| Push blocked | A push carrying it was refused, so it never landed. |
150−| Allowed | Someone said it is not a real secret; pushes carrying it go through. |
151−| Resolved | Someone rotated or removed it. |
152−
153−**Reopen** undoes an allow or a resolve.
154−
155−## Dependency upkeep
156−
157−g1t reads these lockfiles on the default branch, up to four directories
158−deep, skipping `node_modules`, `vendor`, `target`, `dist` and `build`:
159−
160−| Ecosystem | Files |
161−| --- | --- |
162−| npm | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock` |
163−| Cargo | `Cargo.lock` |
164−| Go | `go.mod` (or `go.sum` where there is no `go.mod`) |
165−| Python | `poetry.lock`, and pinned lines (`name==1.2.3`) in `requirements.txt` |
166−
167−It reads them on every push to the default branch and again every day, and
168−asks OSV about every package at the exact version locked. Each advisory
169−found is listed with its id (its GHSA id when it has one), severity, the
170−version that fixes it, and the lockfile that resolves the vulnerable
171−version. A vulnerability that a later scan no longer finds is marked fixed.
172−
173−### Upgrade issues
174−
175−With **Upkeep agents** on, each vulnerable package that has a fixed version
176−gets one issue, labelled `dependencies` and `security`, such as:
177−
178−> Upgrade lodash to 4.17.21: fixes GHSA-35jh-r3h4-6jhm
179−
180−The issue lists every advisory it fixes, and the target is the lowest
181−version that fixes all of them. It ends with a **Definition of done**: no
182−lockfile resolves a vulnerable version and the tests still pass, followed
183−by the commands that show it, each as "`command` passes.":
184−
185−- a command per lockfile that fails while the lockfile still resolves the
186− vulnerable version, and
187−- the project's tests, run in the lockfile's directory by its package
188− manager:
189−
190− | Lockfile | Tests |
191− | --- | --- |
192− | `package-lock.json` | `npm ci && npm test --if-present` |
193− | `pnpm-lock.yaml` | `pnpm install --frozen-lockfile && pnpm test --if-present` |
194− | `yarn.lock` | `yarn install`, then `yarn test` |
195− | `Cargo.lock` | `cargo test --locked` |
196− | `go.mod`, `go.sum` | `go test ./...` |
197−
198− Python projects get the lockfile command only, since there is no one way
199− to run their tests.
200−
201−The definition of done tells the agent and its reviewer what to verify. The
202−pull request merges on the repository's
203−[required status checks](/guides/pull-requests/#required-status-checks),
204−as any other does.
205−
206−g1t puts its agent on the first new upgrade at once and queues the rest,
207−which start as the project has room for more agents. The agent upgrades the
208−package, changes whatever code the upgrade breaks, and opens a pull request
209−that goes through checks, review and the merge queue like any other. When it
210−merges, the next scan finds the vulnerability fixed.
211−
212−g1t opens at most eight upgrade issues per scan, most severe first, and
213−never a second issue for a package while one is open. If you close an
214−upgrade issue as not planned, g1t does not open it again. If agents cannot
215−run in the workspace, the issue stays open with a comment saying why, for
216−you to assign once they can, or to upgrade by hand.
217−
218−Turn **Upkeep agents** off on the Security page to stop new upgrade issues
219−for a project. Issues already open are left as they are.
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.