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 charged | 5 | //! `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 charged | 13 | //! See `g1t_contracts::guardrails` for the rules and how levels merge. |
| 14 | ||
| 15 | use g1t_contracts::access::{self, Capability}; | |
| 16 | use g1t_contracts::audit::{AuditActor, AuditOutcome, AuditTarget, NewAuditEntry, RecordAuditArgs, Surface}; | |
| 17 | use g1t_contracts::agents::RunStatus; | |
| 18 | use g1t_contracts::guardrails::*; | |
| 19 | use g1t_contracts::repos::{Repo, RepoPath}; | |
| 20 | use g1t_contracts::time::rfc3339; | |
| 21 | use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, Viewer, new_id}; | |
| 22 | use g1t_kit::now_ms; | |
| 23 | use serde::Deserialize; | |
| 24 | use worker::Result; | |
| 25 | use worker::wasm_bindgen::JsValue; | |
| 26 | ||
| 27 | use 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. | 28 | use 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 charged | 29 | use 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`. | |
| 33 | pub(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). | |
| 41 | pub(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)] | |
| 46 | struct SettingsRow { | |
| 47 | settings: String, | |
| 48 | updated_by: String, | |
| 49 | updated_at: String, | |
| 50 | } | |
| 51 | ||
| 52 | impl 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. | |
| 62 | fn 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. | |
| 67 | pub(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. | |
| 70 | fn 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 | ||
| 78 | impl 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 tokens | 125 | // 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 charged | 126 | 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 tokens | 149 | // 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 charged | 150 | // 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 charged | 233 | } |
| 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 charged | 310 | }; |
| 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), ¬ed).await?; |
| Billing and Usage reconcile: own-provider runs leave Billing's at-price total, and Usage's not-charged part is at price less charged | 312 | } |
| 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 charged | 319 | fn 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 charged | 327 | } |
| 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 charged | 330 | } |
| 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 charged | 332 | } |
| 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. | |
| 336 | fn 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. | |
| 342 | const WORKING_COPIES: &str = "pulls"; | |
| 343 | ||
| 344 | /// A principal that can read any repository of `workspace`, for the | |
| 345 | /// runner's lookups. | |
| 346 | fn 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)] | |
| 358 | mod 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 charged | 396 | }; |
| 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 charged | 399 | 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 charged | 402 | ); |
| 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 charged | 417 | 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 charged | 422 | 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 charged | 434 | } |
| 435 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.