Skip to content

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

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

Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.1//! Guardrails: how long each kind of run may take, as a workspace's
2//! defaults and each project's overrides. The runner service reads what a
3//! run gets (`run_guardrails`) and enforces it; this service keeps the
4//! settings and ends a run that reached a cap (`halt_run`, from
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged5//! `report_run`).
6//!
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.7//! Network rules (restricting the network, registries, allowed and
8//! workflow-only domains), command rules, deny patterns and cost caps are
9//! gone: levels saved with them still read, and they are dropped when
10//! saved again. The `guardrails` table keeps each level as JSON, so
11//! nothing in it needed to change.
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.12//!
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged13//! See `g1t_contracts::guardrails` for the rules and how levels merge.
14
15use g1t_contracts::access::{self, Capability};
16use g1t_contracts::audit::{AuditActor, AuditOutcome, AuditTarget, NewAuditEntry, RecordAuditArgs, Surface};
17use g1t_contracts::agents::RunStatus;
18use g1t_contracts::guardrails::*;
19use g1t_contracts::repos::{Repo, RepoPath};
20use g1t_contracts::time::rfc3339;
21use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, Viewer, new_id};
22use g1t_kit::now_ms;
23use serde::Deserialize;
24use worker::Result;
25use worker::wasm_bindgen::JsValue;
26
27use crate::Work;
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.28use crate::lifecycle::{POLICY_ACTOR_ID, POLICY_ACTOR_NAME};
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged29use crate::runs::member_of;
30
31/// Statements that move a renamed workspace's guardrails to its new slug.
32/// Run with the new slug as `?1` and the old as `?2`.
33pub(crate) const RENAMED: &[&str] = &[
34 "UPDATE guardrails SET scope_key = ?1 WHERE scope = 'workspace' AND scope_key = ?2",
35 "UPDATE guardrails SET workspace = ?1 WHERE workspace = ?2",
36];
37
38/// A repository transferred: its own guardrails follow it to the new
39/// workspace (see `g1t_kit::transfer`; `?3` the workspace now, `?4` the one
40/// before, `?5` the repository's id).
41pub(crate) const TRANSFERRED: &[&str] = &[
42 "UPDATE guardrails SET workspace = ?3 WHERE scope <> 'workspace' AND scope_key = ?5 AND workspace = ?4",
43];
44
45#[derive(Deserialize)]
46struct SettingsRow {
47 settings: String,
48 updated_by: String,
49 updated_at: String,
50}
51
52impl From<SettingsRow> for GuardrailSettings {
53 fn from(row: SettingsRow) -> Self {
54 let mut settings: GuardrailSettings = serde_json::from_str(&row.settings).unwrap_or_default();
55 settings.updated_by = Some(row.updated_by);
56 settings.updated_at = Some(row.updated_at);
57 settings
58 }
59}
60
61/// Whether `actor` is a verified person, not a token or an agent.
62fn is_person(actor: &User) -> bool {
63 actor.verified && actor.kind == PrincipalKind::User
64}
65
66/// What a run stopped for looking like mining says, everywhere it shows.
67pub(crate) const ABUSE_MESSAGE: &str = "Stopped: unusual CPU use; contact support if this was a real job.";
68
69/// What a halted run is told, and what its pull request says.
70fn halt_message(halt: Halt) -> &'static str {
71 match halt {
72 Halt::Budget => "Stopped: it reached its cost cap.",
73 Halt::Time => "Stopped: it reached its time cap.",
74 Halt::Abuse => ABUSE_MESSAGE,
75 }
76}
77
78impl Work {
79 async fn guardrail_level(&self, scope: &str, key: &str) -> Result<GuardrailSettings> {
80 Ok(self
81 .db
82 .prepare("SELECT settings, updated_by, updated_at FROM guardrails WHERE scope = ? AND scope_key = ?")
83 .bind(&[scope.into(), key.into()])?
84 .first::<SettingsRow>(None)
85 .await?
86 .map(GuardrailSettings::from)
87 .unwrap_or_default())
88 }
89
90 /// The project at `path`, which must be in `workspace`.
91 async fn guarded_repo(&self, path: &RepoPath, viewer: &Viewer, workspace: &str) -> Result<Outcome<Repo>> {
92 Ok(match self.repo(path, viewer).await? {
93 Outcome::Ok(repo) if repo.namespace.to_lowercase() == workspace => Outcome::Ok(repo),
94 Outcome::Ok(_) => Outcome::fail(FailureCode::Invalid, "That project is in another workspace."),
95 Outcome::Fail(failure) => Outcome::Fail(failure),
96 })
97 }
98
99 async fn guardrails_view(&self, workspace: &str, repo: Option<&Repo>) -> Result<GuardrailsView> {
100 let level = self.guardrail_level("workspace", workspace).await?;
101 let project = match repo {
102 Some(repo) => Some(self.guardrail_level("project", &repo.id).await?),
103 None => None,
104 };
105 Ok(GuardrailsView::new(level, project))
106 }
107
108 pub(crate) async fn get_guardrails(&self, a: GetGuardrailsArgs) -> Result<Outcome<GuardrailsView>> {
109 let workspace = a.workspace.to_lowercase();
110 let member = a.viewer.as_ref().is_some_and(|viewer| viewer.is_member(&workspace));
111 if !member && a.repo.is_none() {
112 return Ok(Outcome::fail(
113 FailureCode::Forbidden,
114 "Guardrails are for members of the workspace.",
115 ));
116 }
117 let repo = match &a.repo {
118 Some(path) => match self.guarded_repo(path, &a.viewer, &workspace).await? {
119 Outcome::Ok(repo) => Some(repo),
120 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
121 },
122 None => None,
123 };
124 // Members see them; so does anyone else who may change the
Merge main (membership, two-factor, GitHub repo roles) into tokens125 // project's, such as an outside collaborator with Admin.
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged126 if !member
127 && !repo
128 .as_ref()
129 .is_some_and(|repo| access::can(a.viewer.as_ref(), repo, Capability::ManageProtection))
130 {
131 return Ok(Outcome::fail(
132 FailureCode::Forbidden,
133 "Guardrails are for members of the workspace.",
134 ));
135 }
136 Ok(Outcome::Ok(self.guardrails_view(&workspace, repo.as_ref()).await?))
137 }
138
139 pub(crate) async fn update_guardrails(&self, a: UpdateGuardrailsArgs) -> Result<Outcome<GuardrailsView>> {
140 let workspace = a.workspace.to_lowercase();
141 let viewer = Some(a.actor.clone());
142 let repo = match &a.repo {
143 Some(path) => match self.guarded_repo(path, &viewer, &workspace).await? {
144 Outcome::Ok(repo) => Some(repo),
145 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
146 },
147 None => None,
148 };
Merge main (membership, two-factor, GitHub repo roles) into tokens149 // A project's guardrails go with its branch protection (Admin);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged150 // the defaults every project inherits are the owners'.
151 let allowed = is_person(&a.actor)
152 && match &repo {
153 Some(repo) => access::can(Some(&a.actor), repo, Capability::ManageProtection),
154 None => a.actor.role_in(&workspace) == Some(Role::Owner),
155 };
156 if !allowed {
157 let message = match &repo {
158 Some(repo) => access::needs(
159 Capability::ManageProtection,
160 &format!("{}/{}", repo.namespace, repo.name),
161 ),
162 None => "Only owners of the workspace can change its guardrails.".to_owned(),
163 };
164 return Ok(Outcome::fail(FailureCode::Forbidden, message));
165 }
166 let mut settings = match validate(a.settings) {
167 Ok(settings) => settings,
168 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
169 };
170 settings.updated_by = None;
171 settings.updated_at = None;
172 let (scope, key) = match &repo {
173 Some(repo) => ("project", repo.id.clone()),
174 None => ("workspace", workspace.clone()),
175 };
176 let before = self.guardrail_level(scope, &key).await?;
177 self.db
178 .prepare(
179 "INSERT INTO guardrails (scope, scope_key, workspace, settings, updated_by, updated_at)
180 VALUES (?, ?, ?, ?, ?, ?)
181 ON CONFLICT (scope, scope_key) DO UPDATE SET
182 settings = excluded.settings,
183 updated_by = excluded.updated_by,
184 updated_at = excluded.updated_at",
185 )
186 .bind(&[
187 scope.into(),
188 key.into(),
189 workspace.as_str().into(),
190 serde_json::to_string(&settings)?.into(),
191 a.actor.username.as_str().into(),
192 rfc3339(now_ms()).into(),
193 ])?
194 .run()
195 .await?;
196 let full = repo.as_ref().map(|repo| format!("{}/{}", repo.namespace, repo.name));
197 self.audit_guardrails(&a.actor, &workspace, full, &guardrails_change(&before, &settings)).await;
198 Ok(Outcome::Ok(self.guardrails_view(&workspace, repo.as_ref()).await?))
199 }
200
201 /// Records a change to guardrails in the workspace's audit log. Never
202 /// fails the change: a log that cannot be written is logged.
203 async fn audit_guardrails(&self, actor: &User, workspace: &str, repo: Option<String>, message: &str) {
204 let entry = NewAuditEntry {
205 actor: AuditActor::of(actor),
206 action: "update_guardrails".to_owned(),
207 surface: Surface::Web,
208 target: AuditTarget { workspace: workspace.to_owned(), repo, ..AuditTarget::default() },
209 outcome: AuditOutcome::Allowed,
210 rule: "guardrails".to_owned(),
211 result: Some("ok".to_owned()),
212 message: Some(message.to_owned()),
213 request_id: new_id("req", now_ms()),
214 };
215 let recorded: Result<u32> = g1t_kit::call(&self.events, "audit_record", &RecordAuditArgs { entries: vec![entry] }).await;
216 if let Err(error) = recorded {
217 worker::console_error!("guardrails change not recorded: {error}");
218 }
219 }
220
221 /// What a run in `repo` gets. The runner is trusted: it names the
222 /// repository it is starting a sandbox in.
223 pub(crate) async fn run_guardrails(&self, a: RunGuardrailsArgs) -> Result<Outcome<Guardrails>> {
224 let path = self.project_path(&a).await?;
225 let workspace = path.namespace.to_lowercase();
226 let repo = match self.repo(&path, &member_of_service(&workspace)).await? {
227 Outcome::Ok(repo) => repo,
228 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
229 };
230 let level = self.guardrail_level("workspace", &workspace).await?;
231 let project = self.guardrail_level("project", &repo.id).await?;
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.232 Ok(Outcome::Ok(Guardrails::merge(&level, Some(&project))))
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged233 }
234
235 /// The project a run's guardrails come from, where it is now. By id
236 /// when the runner has it, so a repository renamed or transferred
237 /// since is still found; a pull request's working copy stands for the
238 /// repository the pull request is to (a preview built from it gets
239 /// that project's guardrails, not the `pulls` namespace's). Otherwise
240 /// the path as given.
241 async fn project_path(&self, a: &RunGuardrailsArgs) -> Result<RepoPath> {
242 let id = match (&a.repo_id, working_copy_of(&a.repo)) {
243 (Some(id), _) => Some(id.clone()),
244 (None, Some(pull_id)) => {
245 self.db
246 .prepare("SELECT repo_id AS value FROM pulls WHERE id = ?1 AND fork_name = ?1")
247 .bind(&[pull_id.into()])?
248 .first::<String>(Some("value"))
249 .await?
250 }
251 (None, None) => None,
252 };
253 let Some(id) = id else {
254 return Ok(a.repo.clone());
255 };
256 let current: Option<RepoPath> =
257 g1t_kit::call(&self.repos, "path_by_id", &g1t_contracts::repos::PathByIdArgs { id }).await?;
258 Ok(current.unwrap_or_else(|| a.repo.clone()))
259 }
260
261 /// Ends a run that reached a cap of its guardrails, as stopped, and
262 /// leaves a pull request it was working on for a person, as a stop by
263 /// a person does. Called from `report_run` once its token is checked.
264 #[allow(clippy::too_many_arguments)]
265 pub(crate) async fn halt_run(
266 &self,
267 run_id: &str,
268 repo_id: &str,
269 pull_id: Option<&str>,
270 number: Option<u32>,
271 kind: &str,
272 halt: Halt,
273 detail: Option<String>,
274 cost_usd: Option<f64>,
275 ) -> Result<Outcome<RunStatus>> {
276 let said = detail
277 .map(|detail| crate::runs::one_line(&detail, 1000))
278 .filter(|detail| !detail.is_empty())
279 .unwrap_or_else(|| halt_message(halt).to_owned());
280 let now = rfc3339(now_ms());
281 let claimed = self
282 .db
283 .prepare(
284 "UPDATE agent_runs SET status = 'stopped', halted = ?1, step = ?2, error = ?2,
285 cost_usd = COALESCE(?3, cost_usd), started_at = COALESCE(started_at, ?4),
286 finished_at = ?4, updated_at = ?4
287 WHERE id = ?5 AND finished_at IS NULL RETURNING id AS value",
288 )
289 .bind(&[
290 halt.as_str().into(),
291 said.as_str().into(),
292 cost_usd
293 .filter(|cost| cost.is_finite() && *cost >= 0.0)
294 .map_or(JsValue::NULL, JsValue::from),
295 now.as_str().into(),
296 run_id.into(),
297 ])?
298 .first::<String>(Some("value"))
299 .await?;
300 if claimed.is_none() {
301 return Ok(Outcome::Ok(RunStatus::Stopped));
302 }
303 self.add_step(run_id, &now, &said)?.run().await?;
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.304 // Said on its pull request, if it was for one.
305 if let (Some(_), Some(number)) = (pull_id, number) {
306 let noted = match halt {
307 Halt::Abuse => format!("stopped its {kind} run for unusual CPU use"),
308 Halt::Budget => format!("stopped its {kind} run at its cost cap"),
309 Halt::Time => format!("stopped its {kind} run at its time cap"),
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged310 };
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.311 self.note(repo_id, number, (POLICY_ACTOR_ID, POLICY_ACTOR_NAME), &noted).await?;
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged312 }
313 Ok(Outcome::Ok(RunStatus::Stopped))
314 }
315}
316
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.317/// What a change to one level did, for the audit log: each time cap set,
318/// changed or cleared (inherited again).
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged319fn guardrails_change(before: &GuardrailSettings, after: &GuardrailSettings) -> String {
320 let mut parts = Vec::new();
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.321 for (kind, minutes) in &after.minutes {
322 match before.minutes.get(kind) {
323 Some(was) if was == minutes => {}
324 Some(was) => parts.push(format!("{kind} {was} to {minutes} minutes")),
325 None => parts.push(format!("{kind} set to {minutes} minutes")),
326 }
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged327 }
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.328 for kind in before.minutes.keys().filter(|kind| !after.minutes.contains_key(*kind)) {
329 parts.push(format!("{kind} inherited"));
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged330 }
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.331 if parts.is_empty() { "Guardrails saved unchanged.".to_owned() } else { format!("Time caps: {}.", parts.join("; ")) }
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged332}
333
334/// The pull request id of a pull request's working copy
335/// (`pulls/<pull id>`, made by repos `fork_for_pull`), if `path` is one.
336fn working_copy_of(path: &RepoPath) -> Option<String> {
337 (path.namespace.eq_ignore_ascii_case(WORKING_COPIES) && !path.name.is_empty())
338 .then(|| path.name.to_lowercase())
339}
340
341/// Where repos keeps every pull request's working copy.
342const WORKING_COPIES: &str = "pulls";
343
344/// A principal that can read any repository of `workspace`, for the
345/// runner's lookups.
346fn member_of_service(workspace: &str) -> Viewer {
347 member_of(
348 &User {
349 id: "svc_runner".to_owned(),
350 username: "g1t".to_owned(),
351 ..User::default()
352 },
353 workspace,
354 )
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn path(namespace: &str, name: &str) -> RepoPath {
362 RepoPath {
363 namespace: namespace.to_owned(),
364 name: name.to_owned(),
365 }
366 }
367
368 #[test]
369 fn a_working_copy_names_its_pull_request() {
370 assert_eq!(
371 working_copy_of(&path("pulls", "pr_01m45bd1b2e359sayh78w977kv")).as_deref(),
372 Some("pr_01m45bd1b2e359sayh78w977kv")
373 );
374 assert_eq!(working_copy_of(&path("Pulls", "PR_7")).as_deref(), Some("pr_7"));
375 assert_eq!(working_copy_of(&path("flagon-io", "automation-lab")), None);
376 assert_eq!(working_copy_of(&path("pulls", "")), None);
377 }
378
379 #[test]
380 fn run_guardrails_take_an_optional_repo_id() {
381 let old: RunGuardrailsArgs =
382 serde_json::from_str(r#"{"repo":{"namespace":"pulls","name":"pr_1"}}"#).unwrap();
383 assert!(old.repo_id.is_none());
384 let by_id: RunGuardrailsArgs = serde_json::from_str(
385 r#"{"repo":{"namespace":"syntaqx","name":"automation-lab"},"repo_id":"rep_1"}"#,
386 )
387 .unwrap();
388 assert_eq!(by_id.repo_id.as_deref(), Some("rep_1"));
389 }
390
391 #[test]
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.392 fn a_change_to_time_caps_is_described_for_the_audit_log() {
393 let caps = |pairs: &[(&str, u32)]| GuardrailSettings {
394 minutes: pairs.iter().map(|(kind, minutes)| ((*kind).to_owned(), *minutes)).collect(),
395 ..GuardrailSettings::default()
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged396 };
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.397 let before = caps(&[("implement", 90), ("review", 30)]);
398 let after = caps(&[("implement", 60), ("checks", 20)]);
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged399 assert_eq!(
400 guardrails_change(&before, &after),
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.401 "Time caps: checks set to 20 minutes; implement 90 to 60 minutes; review inherited."
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged402 );
403 assert_eq!(guardrails_change(&before, &before), "Guardrails saved unchanged.");
404 }
405
406 #[test]
407 fn renames_take_two_parameters() {
408 for sql in RENAMED {
409 assert!(sql.contains("?1") && sql.contains("?2"), "{sql}");
410 }
411 }
412
413 #[test]
414 fn a_level_reads_back_with_who_changed_it() {
415 let row = SettingsRow {
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.416 settings: r#"{"budgetUsd":2.5,"restrictNetwork":true,"domains":["example.com"],"minutes":{"review":15}}"#.to_owned(),
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged417 updated_by: "ada".to_owned(),
418 updated_at: "2026-10-04T00:00:00Z".to_owned(),
419 };
420 let settings = GuardrailSettings::from(row);
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.421 assert_eq!(settings.minutes.get("review"), Some(&15));
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged422 assert_eq!(settings.updated_by.as_deref(), Some("ada"));
423 }
424
425 #[test]
426 fn a_corrupt_level_reads_as_inheriting_everything() {
427 let row = SettingsRow {
428 settings: "not json".to_owned(),
429 updated_by: "ada".to_owned(),
430 updated_at: "2026-10-04T00:00:00Z".to_owned(),
431 };
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.432 let settings = GuardrailSettings::from(row);
Guardrails no longer restrict the network: every sandbox on g1t's machines reaches the internet, as a GitHub-hosted runner does, and the settings for it are gone at every level, the workspace's defaults and each project's overrides: Only allowed hosts (restrict_network), the package registries, allowed domains and workflow-only domains. Guardrails are now the time caps on checks, the merge queue and merge checks, and nothing else.433 assert!(settings.minutes.is_empty());
Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged434 }
435}

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