Skip to content
866 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1//! The security service: what g1t finds wrong in a repository, and the
2//! upkeep that fixes it without a person.
3//!
4//! - Secrets. The repos service refuses pushes that add one
5//! (`push_blocked` says which were allowed and records the rest), and
6//! each repository's history is scanned once, in the background.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily7//! A push too large to scan first is let through, and its new commits
8//! are scanned after they land (`history::advance_push_scan`).
9//! - Alerts are open, dismissed (with a reason, a comment and who) or
10//! fixed, and each keeps an activity log. Likely test values are listed
11//! apart and never block a push or count as critical.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API12//! - Dependencies. On every push to a default branch, and daily, the
13//! lockfiles are read and every package checked against OSV. Each
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily14//! vulnerable package with a fix gets a security update: g1t itself
15//! (`User::system`) opens a pull request raising its version, made in a
16//! sandbox (the runner's `bump`), which lands through the branch's
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent17//! required checks. Only when code has to change is g1t put on an
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily18//! issue for it.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API19//!
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar20//! - The security suite (`g1t_contracts::security_suite`): custom secret
21//! patterns (`patterns`), push protection bypasses, their review and
22//! validity checks (`secret_alerts`), code scanning from SARIF uploads
23//! and its pull request check (`code_scanning`), the dependency graph,
24//! its SBOM and dependency review (`supply_chain`), "Fix with g1t"
25//! (`fixes`), and settings with the workspace's overview (`overview`).
26//! On private repositories its paid parts need the workspace's Security
27//! and quality activation (`suite`); it tells people through events.
28//!
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API29//! Other services reach it over `POST /rpc/<method>`; see
30//! `g1t_contracts::security`.
31
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.32mod advisories;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar33mod code_scanning;
34mod config;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API35mod deps;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar36mod fixes;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API37mod history;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar38mod manifests;
39mod overview;
40mod patterns;
41mod planning;
42mod pull_text;
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches43mod resolved;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar44mod ranges;
45mod registries;
46mod schedule;
47mod secret_alerts;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily48mod security_updates;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API49mod store;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar50mod suite;
51mod suite_store;
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.52mod summaries;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar53mod supply_chain;
54mod timezones;
55mod update_store;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily56mod updates;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar57mod version_updates;
58mod yaml;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API59
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look60use g1t_contracts::access::{self, Capability};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API61use g1t_contracts::events::{Event, WorkspaceRenamed};
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily62use g1t_contracts::security::UPDATE_BRANCH_PREFIX;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look63use g1t_contracts::repos::{GetArgs, PathByIdArgs, Repo, RepoPath, RepoStatus, StatusByIdArgs};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API64use g1t_contracts::security::*;
65use g1t_contracts::time::rfc3339;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily66use g1t_contracts::{FailureCode, Outcome, User};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API67use g1t_kit::{args, now_ms, reply, rpc_method};
68use serde::Deserialize;
69use worker::{Context, Env, Fetcher, MessageBatch, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
70
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily71use store::{Activity, RepoRow, Store};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API72
73/// Repositories whose history is continued per sweep, and pages each.
74const HISTORIES_PER_SWEEP: u32 = 5;
75const PAGES_PER_SWEEP: u32 = 4;
76/// Repositories whose dependencies are read again per sweep.
77const DEPENDENCIES_PER_SWEEP: u32 = 10;
78const DAY_MS: u64 = 24 * 60 * 60 * 1000;
79const MAX_REASON_CHARS: usize = 500;
Merge main (membership, two-factor, GitHub repo roles) into tokens80/// What seeing and dismissing a repository's findings takes: the Write
81/// role, as on GitHub, or a security manager of its workspace. Changing its
82/// security settings takes Admin (`ManageSecurity`).
83const SEE_FINDINGS: Capability = Capability::SecurityAlerts;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API84
85pub struct Security {
86 store: Store,
87 identity: Fetcher,
88 repos: Fetcher,
89 work: Fetcher,
90 runner: Fetcher,
91 billing: Fetcher,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar92 actions: Fetcher,
93 /// The events service: security events for webhooks and the inbox,
94 /// and audit entries. Optional so a deployment without the binding
95 /// still scans.
96 events: Option<Fetcher>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API97}
98
99fn fail<T>(code: FailureCode, message: impl Into<String>) -> Outcome<T> {
100 Outcome::fail(code, message)
101}
102
103/// `git.push`, as far as this service reads it.
104#[derive(Deserialize)]
105#[serde(rename_all = "camelCase")]
106struct Pushed {
107 repo_id: String,
108 #[serde(default)]
109 default_branch: bool,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily110 #[serde(default, rename = "ref")]
111 git_ref: String,
112 #[serde(default)]
113 before: Option<String>,
114 #[serde(default)]
115 after: String,
116 /// Too large to scan before it was stored.
117 #[serde(default)]
118 unscanned: bool,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API119}
120
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily121/// `pull.merged`, `pull.closed` and `checks.completed`, as far as this
122/// service reads them.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API123#[derive(Deserialize)]
124#[serde(rename_all = "camelCase")]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily125struct PullHappened {
126 repo_id: String,
127 number: u32,
128 #[serde(default)]
129 status: Option<String>,
130}
131
132/// The longest comment a dismissal keeps.
133const MAX_COMMENT_CHARS: usize = MAX_REASON_CHARS;
134/// Activity rows the Security page reads, newest first.
135const ACTIVITY_SHOWN: u32 = 500;
136
137#[derive(Deserialize)]
138#[serde(rename_all = "camelCase")]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API139struct Created {
140 repo_id: String,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit141 /// Absent from events older than the field.
142 #[serde(default)]
143 is_private: Option<bool>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API144}
145
146impl Security {
147 fn new(env: &Env) -> Result<Self> {
148 Ok(Security {
149 store: Store { db: env.d1("DB")? },
150 identity: env.service("IDENTITY")?,
151 repos: env.service("REPOS")?,
152 work: env.service("WORK")?,
153 runner: env.service("RUNNER")?,
154 billing: env.service("BILLING")?,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar155 actions: env.service("ACTIONS")?,
156 events: env.service("EVENTS").ok(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API157 })
158 }
159
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look160 /// The repository at `path`, recorded here, if `viewer` may see its
161 /// findings (the Write role) and do `capability`. Findings are for those
162 /// who can change the code: to anyone else the page does not exist,
163 /// public repository or not.
164 async fn member_repo(
165 &self,
166 path: &RepoPath,
167 viewer: &Option<User>,
168 capability: Capability,
169 ) -> Result<Outcome<RepoRow>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API170 let hidden = || fail(FailureCode::NotFound, "Repository not found.");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look171 if viewer.is_none() {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API172 return Ok(hidden());
173 }
174 let repo: Outcome<Repo> =
175 g1t_kit::call(&self.repos, "get", &GetArgs { path: path.clone(), viewer: viewer.clone() }).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look176 let repo = match repo {
177 Outcome::Ok(repo) if repo.fork_of.is_none() => repo,
178 _ => return Ok(hidden()),
179 };
180 if !access::can(viewer.as_ref(), &repo, SEE_FINDINGS) {
181 return Ok(hidden());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API182 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look183 if !access::can(viewer.as_ref(), &repo, capability) {
184 return Ok(fail(
185 FailureCode::Forbidden,
186 access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)),
187 ));
188 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar189 let row = self.store.register(&repo.id, &repo.namespace, &repo.name).await?;
190 self.store.set_private(&repo.id, repo.is_private).await?;
191 Ok(Outcome::Ok(row))
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API192 }
193
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.194 /// Where the repository stands: archived, deleted, a mirror. When repos
195 /// cannot say, it is taken as an ordinary active repository.
196 async fn status(&self, repo_id: &str) -> Result<RepoStatus> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look197 let status: Result<RepoStatus> =
198 g1t_kit::call(&self.repos, "status_by_id", &StatusByIdArgs { id: repo_id.to_owned() }).await;
199 Ok(match status {
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.200 Ok(status) => status,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look201 Err(error) => {
202 worker::console_error!("security: status_by_id {repo_id}: {error}");
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.203 RepoStatus::default()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204 }
205 })
206 }
207
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.208 /// Whether the repository is neither archived nor deleted. Scanning
209 /// goes on in a mirror: it reads.
210 async fn active(&self, repo_id: &str) -> Result<bool> {
211 Ok(self.status(repo_id).await?.active())
212 }
213
214 /// Whether g1t may open branches and pull requests here on its own
215 /// (security and dependency updates): active, and not a mirror short
216 /// of a takeover (`g1t_contracts::mirrors`). `Err` says why not.
217 async fn takes_updates(&self, repo: &RepoRow) -> Result<std::result::Result<(), String>> {
218 let status = self.status(&repo.repo_id).await?;
219 if status.takes_updates() {
220 return Ok(Ok(()));
221 }
222 Ok(Err(match &status.mirror {
223 Some(mirror) if status.active() => g1t_contracts::mirrors::updates_wait_message(&repo.namespace, &repo.name, mirror),
224 _ => "Updates wait while the repository is archived or deleted.".to_owned(),
225 }))
226 }
227
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API228 /// Records a repository named in an event, by id. Forks are not
229 /// recorded: a pull request's findings belong to its repository.
230 async fn register_by_id(&self, repo_id: &str) -> Result<Option<RepoRow>> {
231 if let Some(row) = self.store.repo(repo_id).await? {
232 return Ok(Some(row));
233 }
234 let path: Option<RepoPath> = g1t_kit::call(&self.repos, "path_by_id", &PathByIdArgs { id: repo_id.to_owned() }).await?;
235 match path {
236 Some(path) => Ok(Some(self.store.register(repo_id, &path.namespace, &path.name).await?)),
237 None => Ok(None),
238 }
239 }
240
241 async fn overview(&self, a: OverviewArgs) -> Result<Outcome<SecurityOverview>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 let mut repo = match self.member_repo(&a.repo, &a.viewer, SEE_FINDINGS).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API243 Outcome::Ok(repo) => repo,
244 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
245 };
246 // The first look at a repository reads its dependencies at once;
247 // its history is scanned in the background.
248 if repo.deps_scanned_at.is_none() {
249 self.scan_dependencies(&repo).await?;
250 repo = self.store.repo(&repo.repo_id).await?.unwrap_or(repo);
251 }
252 let (counts, _, _) = self.store.counts(&repo.repo_id).await?;
253 Ok(Outcome::Ok(SecurityOverview {
254 repo_id: repo.repo_id.clone(),
255 counts,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily256 secret_counts: self.store.secret_counts(&repo.repo_id).await?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API257 secrets: self.store.secrets(&repo.repo_id).await?,
258 vulnerabilities: self.store.vulnerabilities(&repo.repo_id).await?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily259 activity: self.store.activity(&repo.repo_id, ACTIVITY_SHOWN).await?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API260 scan: repo.scan_state(),
261 upkeep: repo.upkeep != 0,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar262 version_updates: self.version_updates_view(&repo).await?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API263 }))
264 }
265
Merge main (membership, two-factor, GitHub repo roles) into tokens266 /// What dismissing or reopening alert `id` takes: Write, for a secret
267 /// as for a vulnerable dependency, as on GitHub; a security manager may
268 /// too.
269 fn capability_for(_id: &str) -> Capability {
270 SEE_FINDINGS
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily271 }
272
273 async fn dismiss(&self, a: DismissArgs) -> Result<Outcome<AlertChange>> {
274 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API275 Outcome::Ok(repo) => repo,
276 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
277 };
278 if !a.actor.verified {
279 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
280 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily281 let comment: String = a.comment.trim().chars().take(MAX_COMMENT_CHARS).collect();
282 let comment = (!comment.is_empty()).then_some(comment);
283 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
284 if !a.reason.for_secrets() {
285 return Ok(fail(
286 FailureCode::Invalid,
287 "A secret is dismissed as false_positive, used_in_tests, revoked or wont_fix.",
288 ));
289 }
290 if finding.state != AlertState::Open {
291 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
292 }
293 self.store
294 .dismiss_secret(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
295 .await?;
296 self.store
297 .record(&repo.repo_id, &[Activity {
298 alert_id: &a.id,
299 action: "dismissed",
300 actor: Some(&a.actor.username),
301 reason: Some(a.reason),
302 comment: comment.as_deref(),
303 number: None,
304 }])
305 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar306 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
307 if let Some(secret) = &secret {
308 // Revoked is fixed; any other reason, dismissed.
309 let action = if a.reason == DismissReason::Revoked { "fixed" } else { "dismissed" };
310 let event = g1t_contracts::security_suite::SecurityEvent {
311 reason: Some(a.reason.as_str().to_owned()),
312 ..secret_alerts::secret_event(&repo, secret)
313 };
314 self.alert_event(g1t_contracts::security_suite::AlertType::SecretScanning, action, &repo, event, Some(a.actor.id.clone()))
315 .await;
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.316 let changed = summaries::Changed {
317 scan: "change",
318 fixed: if action == "fixed" { vec![a.id.clone()] } else { Vec::new() },
319 dismissed: u32::from(action == "dismissed"),
320 ..summaries::Changed::default()
321 };
322 self.summarize(&repo, g1t_contracts::security_suite::AlertType::SecretScanning, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar323 }
324 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily325 }
326 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
327 return Ok(fail(FailureCode::NotFound, "No such alert."));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API328 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily329 if a.reason.for_secrets() {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API330 return Ok(fail(
331 FailureCode::Invalid,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily332 "A dependency is dismissed as fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used.",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API333 ));
334 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily335 if vuln.state != AlertState::Open {
336 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
337 }
338 self.store
339 .dismiss_vulnerability(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
340 .await?;
341 self.store
342 .record(&repo.repo_id, &[Activity {
343 alert_id: &a.id,
344 action: "dismissed",
345 actor: Some(&a.actor.username),
346 reason: Some(a.reason),
347 comment: comment.as_deref(),
348 number: None,
349 }])
350 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar351 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
352 if let Some(vuln) = &vulnerability {
353 let event = g1t_contracts::security_suite::SecurityEvent { reason: Some(a.reason.as_str().to_owned()), ..deps::vulnerability_event(&repo, vuln) };
354 self.alert_event(g1t_contracts::security_suite::AlertType::Vulnerability, "dismissed", &repo, event, Some(a.actor.id.clone())).await;
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.355 let changed = summaries::Changed { scan: "change", dismissed: 1, ..summaries::Changed::default() };
356 self.summarize(&repo, g1t_contracts::security_suite::AlertType::Vulnerability, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar357 }
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches358 // A security update whose alerts are now all fixed or dismissed closes.
359 if let Err(error) = self.resolve_updates(&repo, None).await {
360 worker::console_error!("security: updates of {} not resolved after a dismissal: {error}", repo.repo_id);
361 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar362 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily363 }
364
365 async fn reopen(&self, a: ReopenArgs) -> Result<Outcome<AlertChange>> {
366 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
367 Outcome::Ok(repo) => repo,
368 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API369 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily370 if !a.actor.verified {
371 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
372 }
373 let reopened = Activity { alert_id: &a.id, action: "reopened", actor: Some(&a.actor.username), reason: None, comment: None, number: None };
374 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
375 if finding.state == AlertState::Open {
376 return Ok(fail(FailureCode::Conflict, "This alert is already open."));
377 }
378 // A secret that never landed has nothing to reopen to but blocked.
379 let status = if finding.source == "push" && finding.test_value.is_none() { SecretStatus::Blocked } else { SecretStatus::Open };
380 self.store.reopen_secret(&repo.repo_id, &a.id, status).await?;
381 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar382 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
383 if let Some(secret) = &secret {
384 self.alert_event(
385 g1t_contracts::security_suite::AlertType::SecretScanning,
386 "reopened",
387 &repo,
388 secret_alerts::secret_event(&repo, secret),
389 Some(a.actor.id.clone()),
390 )
391 .await;
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.392 // The thread's counts move; nobody is told anew of what they reopened.
393 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
394 self.summarize(&repo, g1t_contracts::security_suite::AlertType::SecretScanning, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar395 }
396 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily397 }
398 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
399 return Ok(fail(FailureCode::NotFound, "No such alert."));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API400 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily401 if vuln.state != AlertState::Dismissed {
402 return Ok(fail(FailureCode::Conflict, "Only a dismissed alert can be reopened; a fixed one reopens when it is found again."));
403 }
404 self.store.reopen_vulnerability(&repo.repo_id, &a.id).await?;
405 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar406 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
407 if let Some(vuln) = &vulnerability {
408 self.alert_event(
409 g1t_contracts::security_suite::AlertType::Vulnerability,
410 "reopened",
411 &repo,
412 deps::vulnerability_event(&repo, vuln),
413 Some(a.actor.id.clone()),
414 )
415 .await;
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.416 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
417 self.summarize(&repo, g1t_contracts::security_suite::AlertType::Vulnerability, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar418 }
419 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API420 }
421
422 async fn rescan(&self, a: RescanArgs) -> Result<Outcome<ScanState>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look423 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), SEE_FINDINGS).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API424 Outcome::Ok(repo) => repo,
425 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
426 };
427 self.store.restart_history(&repo.repo_id).await?;
428 self.scan_dependencies(&repo).await?;
429 if let Some(fresh) = self.store.repo(&repo.repo_id).await? {
430 self.advance_history(&fresh, 1).await?;
431 }
432 let state = self.store.repo(&repo.repo_id).await?.map(|row| row.scan_state()).unwrap_or_default();
433 Ok(Outcome::Ok(state))
434 }
435
436 async fn set_upkeep(&self, a: SetUpkeepArgs) -> Result<Outcome<bool>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look437 // Whether agents keep its dependencies up to date is one of its
438 // settings.
439 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Capability::ManageSettings).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API440 Outcome::Ok(repo) => repo,
441 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
442 };
443 if !a.actor.verified {
444 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
445 }
446 self.store.set_upkeep(&repo.repo_id, a.enabled, &a.actor.username).await?;
447 Ok(Outcome::Ok(a.enabled))
448 }
449
450 async fn workspace(&self, a: WorkspaceArgs) -> Result<Outcome<Vec<RepoSecurity>>> {
451 let workspace = a.workspace.to_lowercase();
452 if !a.viewer.as_ref().is_some_and(|user| user.is_member(&workspace)) {
453 return Ok(fail(FailureCode::NotFound, "Workspace not found."));
454 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit455 // Only the repositories whose findings the viewer may see. The
456 // Write role is never had through being public, so treating each
457 // as private changes nothing.
458 let repos: Vec<RepoRow> = self
459 .store
460 .in_namespace(&workspace)
461 .await?
462 .into_iter()
463 .filter(|repo| {
464 let target = access::RepoRef { id: &repo.repo_id, namespace: &workspace, private: true };
465 access::can(a.viewer.as_ref(), target, SEE_FINDINGS)
466 })
467 .collect();
468 // Every repository's counts at once, not one after another.
469 let counts = futures_util::future::try_join_all(repos.iter().map(|repo| self.store.counts(&repo.repo_id))).await?;
470 let list = repos
471 .into_iter()
472 .zip(counts)
473 .map(|(repo, (counts, secrets, vulnerabilities))| RepoSecurity {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API474 repo_id: repo.repo_id,
475 name: repo.name,
476 counts,
477 secrets,
478 vulnerabilities,
479 upkeep: repo.upkeep != 0,
480 dependencies_scanned_at: repo.deps_scanned_at,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit481 })
482 .collect();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API483 Ok(Outcome::Ok(list))
484 }
485
486 /// Push protection's question: which of these secrets were allowed?
487 /// The others are recorded as blocked, so someone can allow them.
488 async fn push_blocked(&self, a: PushBlockedArgs) -> Result<PushVerdict> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar489 let repo = self.store.register(&a.repo_id, &a.path.namespace, &a.path.name).await?;
490 if let Some(private) = a.private {
491 self.store.set_private(&a.repo_id, private).await?;
492 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API493 let fingerprints: Vec<String> = a.secrets.iter().map(|secret| secret.fingerprint.clone()).collect();
494 let known = self.store.known(&a.repo_id, &fingerprints).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar495 let mut allowed = let_through(&known, &a.secrets);
496 // Bypassed with a reason: let through, whatever the alert says now.
497 allowed.extend(self.store.bypassed(&a.repo_id, &fingerprints).await?);
498 allowed.sort();
499 allowed.dedup();
500 let all = a.secrets.clone();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API501 let fresh: Vec<NewSecret> = a
502 .secrets
503 .into_iter()
504 .filter(|secret| !known.iter().any(|(fingerprint, _, _)| *fingerprint == secret.fingerprint))
505 .collect();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily506 // A likely test value goes through, so it lands: open, not blocked.
507 let (tests, real): (Vec<NewSecret>, Vec<NewSecret>) = fresh.into_iter().partition(|secret| secret.test_value.is_some());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API508 self.store
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily509 .add_secrets(&a.repo_id, &real, SecretStatus::Blocked, "push", a.pusher.as_deref())
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API510 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily511 self.store
512 .add_secrets(&a.repo_id, &tests, SecretStatus::Open, "push", a.pusher.as_deref())
513 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar514 let fresh: Vec<String> = real.iter().map(|secret| secret.fingerprint.clone()).collect();
515 self.secrets_found(&repo, &all, "push", &fresh, a.pusher.as_deref()).await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API516 let ids = self
517 .store
518 .known(&a.repo_id, &fingerprints)
519 .await?
520 .into_iter()
521 .filter(|(fingerprint, _, _)| !allowed.contains(fingerprint))
522 .map(|(fingerprint, id, _)| (fingerprint, id))
523 .collect();
524 Ok(PushVerdict { allowed, ids })
525 }
526
527 /// What happens on the bus that concerns this service.
528 async fn on_event(&self, event: &Event) -> Result<()> {
529 match event.kind.as_str() {
530 "git.push" => {
531 let Ok(pushed) = serde_json::from_value::<Pushed>(event.data.clone()) else {
532 return Ok(());
533 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily534 // Too large to scan before it was stored: its new commits
535 // are scanned now, on whichever branch.
536 if pushed.unscanned
537 && !pushed.after.is_empty()
538 && let Some(repo) = self.register_by_id(&pushed.repo_id).await?
539 {
540 let id = self
541 .store
542 .add_push_scan(&repo.repo_id, &pushed.git_ref, &pushed.after, pushed.before.as_deref(), event.actor.as_deref())
543 .await?;
544 self.advance_push_scan(&id, history::PUSH_PAGES_AT_ONCE).await?;
545 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar546 // A version update's branch, or a grouped security update's,
547 // pushed by its sandbox: time for its pull request.
548 if !pushed.default_branch
549 && let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
550 && self.update_pull_pushed(&pushed.repo_id, branch, &pushed.after).await?
551 {
552 return Ok(());
553 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily554 // A security update's branch, pushed by its sandbox: time for
555 // its pull request.
556 if let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
557 && branch.starts_with(UPDATE_BRANCH_PREFIX)
558 {
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches559 self.update_pushed(&pushed.repo_id, branch, &pushed.after).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily560 return Ok(());
561 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API562 if !pushed.default_branch {
563 return Ok(());
564 }
565 if let Some(repo) = self.register_by_id(&pushed.repo_id).await? {
566 self.scan_dependencies(&repo).await?;
567 if repo.history != "done" {
568 self.advance_history(&repo, 1).await?;
569 }
570 }
571 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily572 "pull.merged" | "pull.closed" | "checks.completed" => {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar573 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone())
574 && !(event.kind != "checks.completed" && self.update_pull_closed(&event.kind, &happened.repo_id, happened.number).await?)
575 {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily576 self.update_pull_event(
577 &event.kind,
578 &happened.repo_id,
579 happened.number,
580 happened.status.as_deref(),
581 event.actor.as_deref(),
582 )
583 .await?;
584 }
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches585 // One of g1t's update pull requests closed or merged, by g1t
586 // or by a person: its branch goes.
587 if event.kind != "checks.completed"
588 && let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone())
589 {
590 self.pull_finished(&happened.repo_id, happened.number).await?;
591 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily592 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar593 "comment.created" => self.update_comment(event).await?,
594 // A pull request opened or its head moved: a dependency update
595 // file it changes is checked (not on `pull.ready`, which moves
596 // nothing), and dependency review runs.
597 "pull.opened" | "pull.updated" | "pull.ready" => {
598 if event.kind != "pull.ready" {
599 self.check_dependabot_file(event).await?;
600 }
601 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) {
602 self.review_pull(&happened.repo_id, happened.number).await?;
603 }
604 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API605 "repo.created" => {
606 if let Ok(created) = serde_json::from_value::<Created>(event.data.clone()) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit607 // Recorded with its visibility, so the overview never takes a
608 // public repository for a private one.
609 if self.register_by_id(&created.repo_id).await?.is_some()
610 && let Some(private) = created.is_private
611 {
612 self.store.set_private(&created.repo_id, private).await?;
613 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API614 }
615 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar616 "repo.visibility_changed" => {
617 if let Ok(changed) = serde_json::from_value::<g1t_contracts::events::RepoVisibilityChanged>(event.data.clone())
618 && self.store.repo(&changed.repo_id).await?.is_some()
619 {
620 self.store.set_private(&changed.repo_id, changed.is_private).await?;
621 }
622 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look623 // A repository transferred or renamed: it is recorded under its new path.
624 "repo.transferred" | "repo.renamed" => {
625 if let Some(moved) = g1t_kit::transfer::read(event) {
626 // Where it is now, so moves heard out of order end in
627 // the same place.
628 let now: Option<g1t_contracts::repos::RepoPath> = g1t_kit::call(
629 &self.repos,
630 "path_by_id",
631 &g1t_contracts::repos::PathByIdArgs { id: moved.repo_id.clone() },
632 )
633 .await?;
634 let current = now.map_or_else(
635 || moved.destination().to_owned(),
636 |path| format!("{}/{}", path.namespace, path.name),
637 );
638 if let Some((namespace, name)) = current.split_once('/') {
639 self.store.moved(&moved.repo_id, namespace, name).await?;
640 }
641 }
642 }
643 // A repository purged: everything found in it goes.
644 "repo.purged" => {
645 if let Some(g1t_kit::lifecycle::Lifecycle::Purged(purged)) = g1t_kit::lifecycle::read(event) {
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.646 self.store.purge_advisories(&purged.repo_id).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar647 self.store.purge_suite(&purged.repo_id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look648 self.store.purge(&purged.repo_id).await?;
649 }
650 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API651 "workspace.renamed" => {
652 if let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) {
653 self.store.rename_namespace(&renamed.stale_slugs(&renamed.to), &renamed.to).await?;
654 }
655 }
656 _ => {}
657 }
658 Ok(())
659 }
660
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily661 /// The sweep: continues history scans and scans of pushes that landed
662 /// unscanned, catches security updates whose sandbox never pushed, and
663 /// reads dependencies that have not been read for a day.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API664 async fn sweep(&self) -> Result<()> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily665 for scan in self.store.pending_push_scans(HISTORIES_PER_SWEEP).await? {
666 if let Err(error) = self.advance_push_scan(&scan.id, PAGES_PER_SWEEP).await {
667 worker::console_error!("security: push scan {} not continued: {error}", scan.id);
668 }
669 }
670 if let Err(error) = self.stalled_updates().await {
671 worker::console_error!("security: stalled security updates not handled: {error}");
672 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar673 if let Err(error) = self.sweep_suite().await {
674 worker::console_error!("security: daily snapshots and validity checks: {error}");
675 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look676 // Archived and deleted repositories wait; a few more are looked at
677 // so that they do not hold up the rest.
678 let mut histories = 0;
679 for repo in self.store.unfinished_histories(HISTORIES_PER_SWEEP * 4).await? {
680 if histories == HISTORIES_PER_SWEEP {
681 break;
682 }
683 if !self.active(&repo.repo_id).await? {
684 continue;
685 }
686 histories += 1;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API687 if let Err(error) = self.advance_history(&repo, PAGES_PER_SWEEP).await {
688 worker::console_error!("security: history of {} not scanned: {error}", repo.repo_id);
689 }
690 }
691 let day_ago = rfc3339(now_ms().saturating_sub(DAY_MS));
692 for repo in self.store.stale_dependencies(&day_ago, DEPENDENCIES_PER_SWEEP).await? {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look693 if !self.active(&repo.repo_id).await? {
694 self.store
695 .skip_dependencies(&repo.repo_id, "Dependencies are not checked while the repository is archived or deleted.")
696 .await?;
697 continue;
698 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API699 if let Err(error) = self.scan_dependencies(&repo).await {
700 worker::console_error!("security: dependencies of {} not read: {error}", repo.repo_id);
701 }
702 }
703 Ok(())
704 }
705}
706
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily707/// The fingerprints a push may carry: those someone dismissed (allowed),
708/// and likely test values, which are recorded but never stop a push.
709/// `known` is (fingerprint, id, status) of findings already recorded.
710fn let_through(known: &[(String, String, String)], secrets: &[NewSecret]) -> Vec<String> {
711 let mut allowed: Vec<String> = known
712 .iter()
713 .filter(|(_, _, status)| status == "allowed")
714 .map(|(fingerprint, _, _)| fingerprint.clone())
715 .chain(secrets.iter().filter(|secret| secret.test_value.is_some()).map(|secret| secret.fingerprint.clone()))
716 .collect();
717 allowed.sort();
718 allowed.dedup();
719 allowed
720}
721
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API722#[event(fetch)]
723async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
724 let Some(method) = rpc_method(&request) else {
725 return Response::error("Not found", 404);
726 };
727 let security = Security::new(&env)?;
728 let body: serde_json::Value = request.json().await?;
729 match method.as_str() {
730 "overview" => reply(&security.overview(args(body)?).await?),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily731 "dismiss" => reply(&security.dismiss(args(body)?).await?),
732 "reopen" => reply(&security.reopen(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API733 "rescan" => reply(&security.rescan(args(body)?).await?),
734 "set_upkeep" => reply(&security.set_upkeep(args(body)?).await?),
735 "workspace" => reply(&security.workspace(args(body)?).await?),
736 "push_blocked" => reply(&security.push_blocked(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar737 "check_updates" => reply(&security.check_updates(args(body)?).await?),
738 // The security suite.
739 "patterns_for" => reply(&security.patterns_for(args(body)?).await?),
740 "custom_patterns" => reply(&security.custom_patterns(args(body)?).await?),
741 "save_custom_pattern" => reply(&security.save_custom_pattern(args(body)?).await?),
742 "delete_custom_pattern" => reply(&security.delete_custom_pattern(args(body)?).await?),
743 "dry_run_pattern" => reply(&security.dry_run_pattern(args(body)?).await?),
744 "secret_alert" => reply(&security.secret_alert(args(body)?).await?),
745 "bypass" => reply(&security.bypass(args(body)?).await?),
746 "bypass_requests" => reply(&security.bypass_requests(args(body)?).await?),
747 "review_bypass" => reply(&security.review_bypass(args(body)?).await?),
748 "check_validity" => reply(&security.check_validity(args(body)?).await?),
749 "upload_sarif" => reply(&security.upload_sarif(args(body)?).await?),
750 "sarif_status" => reply(&security.sarif_status(args(body)?).await?),
751 "code_scanning" => reply(&security.code_scanning(args(body)?).await?),
752 "code_alert" => reply(&security.code_alert(args(body)?).await?),
753 "set_code_alert_state" => reply(&security.set_code_alert_state(args(body)?).await?),
754 "pull_code_scanning" => reply(&security.pull_code_scanning(args(body)?).await?),
755 "fix_alert" => reply(&security.fix_alert(args(body)?).await?),
756 "dependency_graph" => reply(&security.dependency_graph(args(body)?).await?),
757 "sbom" => reply(&security.sbom(args(body)?).await?),
758 "dependency_review" => reply(&security.dependency_review(args(body)?).await?),
759 "security_settings" => reply(&security.security_settings(args(body)?).await?),
760 "set_security_settings" => reply(&security.set_security_settings(args(body)?).await?),
761 "workspace_security_settings" => reply(&security.workspace_security_settings(args(body)?).await?),
762 "set_workspace_security_settings" => reply(&security.set_workspace_security_settings(args(body)?).await?),
763 "security_overview" => reply(&security.security_overview(args(body)?).await?),
764 "workspace_alerts" => reply(&security.workspace_alerts(args(body)?).await?),
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.765 // Reporting: advisories and private vulnerability reports (advisories.rs).
766 "list_advisories" => reply(&security.list_advisories(args(body)?).await?),
767 "get_advisory" => reply(&security.get_advisory(args(body)?).await?),
768 "create_advisory" => reply(&security.create_advisory(args(body)?).await?),
769 "update_advisory" => reply(&security.update_advisory(args(body)?).await?),
770 "set_advisory_state" => reply(&security.set_advisory_state(args(body)?).await?),
771 "report_vulnerability" => reply(&security.report_vulnerability(args(body)?).await?),
A repository's Security overview is one row per feature in three groups, each with a status word (Enabled, Disabled, Needs setup, Coming), a line on what it does and the one thing to do about it: the security policy (View policy, or Set up a security policy, which commits a starting SECURITY.md on a branch and opens it as a pull request), dependency alerts, security updates (the switch, kept on Vulnerabilities and now in Settings too), code scanning (Enabled once an analysis was uploaded), secret scanning (push protection always on) and code quality as Coming; and under Reporting, advisories and the private vulnerability reporting switch. Anyone who can see a public repository but has no role on it sees the public face at the same address: the policy, the published advisories and, when it is on, Report a vulnerability. Advisories get their pages (the list with its state filter, one advisory with Publish, Withdraw and Edit, a form for a new draft or changes) and reports theirs (new ones first, each accepted into a draft advisory or declined), with a Reports page beside Advisories and an Advisories tab; the public report form takes an email when signed out and counts against the front door's heavy limit. Security settings gain Alerts for secrets in the history and Private vulnerability reporting. Settings → Notifications gains a Security section: critical and high alerts at once (always, for the workspaces one owns or manages security for) and the rest in a daily or weekly digest, at once, or not at all, with security alerts now among the email reasons. The notifications guide says how security alerts arrive and what the digest is, the security guide the overview's rows and the public face, secret protection the history alerts switch, webhooks the summary and advisory events, and a new guide covers advisories and private reporting; the report_settings method lets the public page know whether a repository takes reports.772 "report_settings" => reply(&security.report_settings(args(body)?).await?),
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.773 "list_vulnerability_reports" => reply(&security.list_vulnerability_reports(args(body)?).await?),
774 "review_vulnerability_report" => reply(&security.review_vulnerability_report(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API775 _ => Response::error("Unknown method", 404),
776 }
777}
778
779#[event(queue)]
780async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
781 let security = Security::new(&env)?;
782 for message in batch.messages()? {
783 let event = message.body();
784 if let Err(error) = security.on_event(event).await {
785 // Scans are idempotent and the sweep catches up, so one failed
786 // event is logged rather than retried.
787 worker::console_error!("security: {} {} failed: {error}", event.kind, event.id);
788 }
789 }
790 Ok(())
791}
792
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar793/// The cron (wrangler.jsonc) that runs version updates that are due.
794const VERSION_UPDATES_CRON: &str = "*/5 * * * *";
795
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API796#[event(scheduled)]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar797async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API798 match Security::new(&env) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar799 // Version updates run every few minutes, so a schedule's time is kept.
800 Ok(security) if event.cron() == VERSION_UPDATES_CRON => {
801 if let Err(error) = security.version_update_sweep().await {
802 worker::console_error!("security: the version update sweep failed: {error}");
803 }
804 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API805 Ok(security) => {
806 if let Err(error) = security.sweep().await {
807 worker::console_error!("security: the sweep failed: {error}");
808 }
809 }
810 Err(error) => worker::console_error!("security: could not start: {error}"),
811 }
812}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 fn secret(fingerprint: &str, test_value: Option<&str>) -> NewSecret {
819 NewSecret {
820 fingerprint: fingerprint.into(),
821 kind: "aws_access_key".into(),
822 path: "a.env".into(),
823 line: 1,
824 commit: "c".into(),
825 preview: "AKIA…".into(),
826 test_value: test_value.map(str::to_owned),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar827 pattern_id: None,
828 pattern_name: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily829 }
830 }
831
832 #[test]
833 fn dismissed_secrets_and_test_values_go_through() {
834 let known = vec![
835 ("allowed".to_owned(), "sec_1".to_owned(), "allowed".to_owned()),
836 ("resolved".to_owned(), "sec_2".to_owned(), "resolved".to_owned()),
837 ("blocked".to_owned(), "sec_3".to_owned(), "blocked".to_owned()),
838 ];
839 let secrets = [secret("allowed", None), secret("resolved", None), secret("example", Some("it says it is an example")), secret("real", None)];
840 assert_eq!(let_through(&known, &secrets), ["allowed", "example"]);
841 }
842
843 #[test]
844 fn a_push_says_when_it_landed_unscanned() {
845 let pushed: Pushed = serde_json::from_value(serde_json::json!({
846 "repoId": "rep_1", "ref": "refs/heads/import", "after": "abc", "defaultBranch": false, "unscanned": true
847 }))
848 .unwrap();
849 assert!(pushed.unscanned && pushed.before.is_none() && pushed.git_ref == "refs/heads/import");
850 let ordinary: Pushed = serde_json::from_value(serde_json::json!({ "repoId": "rep_1", "ref": "refs/heads/main", "after": "abc", "defaultBranch": true })).unwrap();
851 assert!(!ordinary.unscanned);
852 }
853
854 #[test]
855 fn owners_are_told_what_landed_and_what_to_do() {
856 let one = history::landed_secrets_intro("acme", "rocket", "import", 1);
857 assert!(one.contains("A push to import in acme/rocket") && one.contains("a secret that looks real") && one.contains("Rotate"));
858 assert!(history::landed_secrets_intro("acme", "rocket", "main", 3).contains("3 secrets that look real"));
859 }
860
861 #[test]
862 fn dismissing_a_secret_takes_admin_and_a_dependency_write() {
Merge main (membership, two-factor, GitHub repo roles) into tokens863 assert_eq!(Security::capability_for("sec_1"), Capability::SecurityAlerts);
864 assert_eq!(Security::capability_for("vul_1"), Capability::SecurityAlerts);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily865 }
866}

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