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
443 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.

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.1//! Guardrails: which hosts a sandbox may reach (the network rules and
2//! workflow-only domains) and how long each kind of run may take, as a
3//! workspace's defaults and each project's overrides. The runner service
4//! reads what a run gets (`run_guardrails`) and enforces it; this service
5//! keeps the settings and ends a run that reached a cap (`halt_run`, from
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides6//! `report_run`).
7//!
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.8//! Command rules, deny patterns and cost caps were for g1t's own agents:
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.9//! levels saved with them still read, and they are dropped when saved again.
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.10//!
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides11//! See `g1t_contracts::guardrails` for the rules and how levels merge.
12
13use g1t_contracts::access::{self, Capability};
14use g1t_contracts::audit::{AuditActor, AuditOutcome, AuditTarget, NewAuditEntry, RecordAuditArgs, Surface};
15use g1t_contracts::agents::RunStatus;
16use g1t_contracts::guardrails::*;
17use g1t_contracts::repos::{Repo, RepoPath};
18use g1t_contracts::time::rfc3339;
19use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, Viewer, new_id};
20use g1t_kit::now_ms;
21use serde::Deserialize;
22use worker::Result;
23use worker::wasm_bindgen::JsValue;
24
25use 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.26use crate::lifecycle::{POLICY_ACTOR_ID, POLICY_ACTOR_NAME};
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides27use crate::runs::member_of;
28
29/// Statements that move a renamed workspace's guardrails to its new slug.
30/// Run with the new slug as `?1` and the old as `?2`.
31pub(crate) const RENAMED: &[&str] = &[
32 "UPDATE guardrails SET scope_key = ?1 WHERE scope = 'workspace' AND scope_key = ?2",
33 "UPDATE guardrails SET workspace = ?1 WHERE workspace = ?2",
34];
35
36/// A repository transferred: its own guardrails follow it to the new
37/// workspace (see `g1t_kit::transfer`; `?3` the workspace now, `?4` the one
38/// before, `?5` the repository's id).
39pub(crate) const TRANSFERRED: &[&str] = &[
40 "UPDATE guardrails SET workspace = ?3 WHERE scope <> 'workspace' AND scope_key = ?5 AND workspace = ?4",
41];
42
43#[derive(Deserialize)]
44struct SettingsRow {
45 settings: String,
46 updated_by: String,
47 updated_at: String,
48}
49
50impl From<SettingsRow> for GuardrailSettings {
51 fn from(row: SettingsRow) -> Self {
52 let mut settings: GuardrailSettings = serde_json::from_str(&row.settings).unwrap_or_default();
53 settings.updated_by = Some(row.updated_by);
54 settings.updated_at = Some(row.updated_at);
55 settings
56 }
57}
58
59/// Whether `actor` is a verified person, not a token or an agent.
60fn is_person(actor: &User) -> bool {
61 actor.verified && actor.kind == PrincipalKind::User
62}
63
64/// What a run stopped for looking like mining says, everywhere it shows.
65pub(crate) const ABUSE_MESSAGE: &str = "Stopped: unusual CPU use; contact support if this was a real job.";
66
67/// What a halted run is told, and what its pull request says.
68fn halt_message(halt: Halt) -> &'static str {
69 match halt {
70 Halt::Budget => "Stopped: it reached its cost cap.",
71 Halt::Time => "Stopped: it reached its time cap.",
72 Halt::Abuse => ABUSE_MESSAGE,
73 }
74}
75
76impl Work {
77 async fn guardrail_level(&self, scope: &str, key: &str) -> Result<GuardrailSettings> {
78 Ok(self
79 .db
80 .prepare("SELECT settings, updated_by, updated_at FROM guardrails WHERE scope = ? AND scope_key = ?")
81 .bind(&[scope.into(), key.into()])?
82 .first::<SettingsRow>(None)
83 .await?
84 .map(GuardrailSettings::from)
85 .unwrap_or_default())
86 }
87
88 /// The project at `path`, which must be in `workspace`.
89 async fn guarded_repo(&self, path: &RepoPath, viewer: &Viewer, workspace: &str) -> Result<Outcome<Repo>> {
90 Ok(match self.repo(path, viewer).await? {
91 Outcome::Ok(repo) if repo.namespace.to_lowercase() == workspace => Outcome::Ok(repo),
92 Outcome::Ok(_) => Outcome::fail(FailureCode::Invalid, "That project is in another workspace."),
93 Outcome::Fail(failure) => Outcome::Fail(failure),
94 })
95 }
96
97 async fn guardrails_view(&self, workspace: &str, repo: Option<&Repo>) -> Result<GuardrailsView> {
98 let level = self.guardrail_level("workspace", workspace).await?;
99 let project = match repo {
100 Some(repo) => Some(self.guardrail_level("project", &repo.id).await?),
101 None => None,
102 };
103 Ok(GuardrailsView::new(level, project))
104 }
105
106 pub(crate) async fn get_guardrails(&self, a: GetGuardrailsArgs) -> Result<Outcome<GuardrailsView>> {
107 let workspace = a.workspace.to_lowercase();
108 let member = a.viewer.as_ref().is_some_and(|viewer| viewer.is_member(&workspace));
109 if !member && a.repo.is_none() {
110 return Ok(Outcome::fail(
111 FailureCode::Forbidden,
112 "Guardrails are for members of the workspace.",
113 ));
114 }
115 let repo = match &a.repo {
116 Some(path) => match self.guarded_repo(path, &a.viewer, &workspace).await? {
117 Outcome::Ok(repo) => Some(repo),
118 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
119 },
120 None => None,
121 };
122 // Members see them; so does anyone else who may change the
Merge main (membership, two-factor, GitHub repo roles) into tokens123 // project's, such as an outside collaborator with Admin.
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides124 if !member
125 && !repo
126 .as_ref()
127 .is_some_and(|repo| access::can(a.viewer.as_ref(), repo, Capability::ManageProtection))
128 {
129 return Ok(Outcome::fail(
130 FailureCode::Forbidden,
131 "Guardrails are for members of the workspace.",
132 ));
133 }
134 Ok(Outcome::Ok(self.guardrails_view(&workspace, repo.as_ref()).await?))
135 }
136
137 pub(crate) async fn update_guardrails(&self, a: UpdateGuardrailsArgs) -> Result<Outcome<GuardrailsView>> {
138 let workspace = a.workspace.to_lowercase();
139 let viewer = Some(a.actor.clone());
140 let repo = match &a.repo {
141 Some(path) => match self.guarded_repo(path, &viewer, &workspace).await? {
142 Outcome::Ok(repo) => Some(repo),
143 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
144 },
145 None => None,
146 };
Merge main (membership, two-factor, GitHub repo roles) into tokens147 // A project's guardrails go with its branch protection (Admin);
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides148 // the defaults every project inherits are the owners'.
149 let allowed = is_person(&a.actor)
150 && match &repo {
151 Some(repo) => access::can(Some(&a.actor), repo, Capability::ManageProtection),
152 None => a.actor.role_in(&workspace) == Some(Role::Owner),
153 };
154 if !allowed {
155 let message = match &repo {
156 Some(repo) => access::needs(
157 Capability::ManageProtection,
158 &format!("{}/{}", repo.namespace, repo.name),
159 ),
160 None => "Only owners of the workspace can change its guardrails.".to_owned(),
161 };
162 return Ok(Outcome::fail(FailureCode::Forbidden, message));
163 }
164 let mut settings = match validate(a.settings) {
165 Ok(settings) => settings,
166 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
167 };
168 settings.updated_by = None;
169 settings.updated_at = None;
170 let (scope, key) = match &repo {
171 Some(repo) => ("project", repo.id.clone()),
172 None => ("workspace", workspace.clone()),
173 };
174 let before = self.guardrail_level(scope, &key).await?;
175 self.db
176 .prepare(
177 "INSERT INTO guardrails (scope, scope_key, workspace, settings, updated_by, updated_at)
178 VALUES (?, ?, ?, ?, ?, ?)
179 ON CONFLICT (scope, scope_key) DO UPDATE SET
180 settings = excluded.settings,
181 updated_by = excluded.updated_by,
182 updated_at = excluded.updated_at",
183 )
184 .bind(&[
185 scope.into(),
186 key.into(),
187 workspace.as_str().into(),
188 serde_json::to_string(&settings)?.into(),
189 a.actor.username.as_str().into(),
190 rfc3339(now_ms()).into(),
191 ])?
192 .run()
193 .await?;
194 let full = repo.as_ref().map(|repo| format!("{}/{}", repo.namespace, repo.name));
195 self.audit_guardrails(&a.actor, &workspace, full, &guardrails_change(&before, &settings)).await;
196 Ok(Outcome::Ok(self.guardrails_view(&workspace, repo.as_ref()).await?))
197 }
198
199 /// Records a change to guardrails in the workspace's audit log. Never
200 /// fails the change: a log that cannot be written is logged.
201 async fn audit_guardrails(&self, actor: &User, workspace: &str, repo: Option<String>, message: &str) {
202 let entry = NewAuditEntry {
203 actor: AuditActor::of(actor),
204 action: "update_guardrails".to_owned(),
205 surface: Surface::Web,
206 target: AuditTarget { workspace: workspace.to_owned(), repo, ..AuditTarget::default() },
207 outcome: AuditOutcome::Allowed,
208 rule: "guardrails".to_owned(),
209 result: Some("ok".to_owned()),
210 message: Some(message.to_owned()),
211 request_id: new_id("req", now_ms()),
212 };
213 let recorded: Result<u32> = g1t_kit::call(&self.events, "audit_record", &RecordAuditArgs { entries: vec![entry] }).await;
214 if let Err(error) = recorded {
215 worker::console_error!("guardrails change not recorded: {error}");
216 }
217 }
218
219 /// What a run in `repo` gets. The runner is trusted: it names the
220 /// repository it is starting a sandbox in.
221 pub(crate) async fn run_guardrails(&self, a: RunGuardrailsArgs) -> Result<Outcome<Guardrails>> {
222 let path = self.project_path(&a).await?;
223 let workspace = path.namespace.to_lowercase();
224 let repo = match self.repo(&path, &member_of_service(&workspace)).await? {
225 Outcome::Ok(repo) => repo,
226 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
227 };
228 let level = self.guardrail_level("workspace", &workspace).await?;
229 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.230 Ok(Outcome::Ok(Guardrails::merge(&level, Some(&project))))
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides231 }
232
233 /// The project a run's guardrails come from, where it is now. By id
234 /// when the runner has it, so a repository renamed or transferred
235 /// since is still found; a pull request's working copy stands for the
236 /// repository the pull request is to (a preview built from it gets
237 /// that project's guardrails, not the `pulls` namespace's). Otherwise
238 /// the path as given.
239 async fn project_path(&self, a: &RunGuardrailsArgs) -> Result<RepoPath> {
240 let id = match (&a.repo_id, working_copy_of(&a.repo)) {
241 (Some(id), _) => Some(id.clone()),
242 (None, Some(pull_id)) => {
243 self.db
244 .prepare("SELECT repo_id AS value FROM pulls WHERE id = ?1 AND fork_name = ?1")
245 .bind(&[pull_id.into()])?
246 .first::<String>(Some("value"))
247 .await?
248 }
249 (None, None) => None,
250 };
251 let Some(id) = id else {
252 return Ok(a.repo.clone());
253 };
254 let current: Option<RepoPath> =
255 g1t_kit::call(&self.repos, "path_by_id", &g1t_contracts::repos::PathByIdArgs { id }).await?;
256 Ok(current.unwrap_or_else(|| a.repo.clone()))
257 }
258
259 /// Ends a run that reached a cap of its guardrails, as stopped, and
260 /// leaves a pull request it was working on for a person, as a stop by
261 /// a person does. Called from `report_run` once its token is checked.
262 #[allow(clippy::too_many_arguments)]
263 pub(crate) async fn halt_run(
264 &self,
265 run_id: &str,
266 repo_id: &str,
267 pull_id: Option<&str>,
268 number: Option<u32>,
269 kind: &str,
270 halt: Halt,
271 detail: Option<String>,
272 cost_usd: Option<f64>,
273 ) -> Result<Outcome<RunStatus>> {
274 let said = detail
275 .map(|detail| crate::runs::one_line(&detail, 1000))
276 .filter(|detail| !detail.is_empty())
277 .unwrap_or_else(|| halt_message(halt).to_owned());
278 let now = rfc3339(now_ms());
279 let claimed = self
280 .db
281 .prepare(
282 "UPDATE agent_runs SET status = 'stopped', halted = ?1, step = ?2, error = ?2,
283 cost_usd = COALESCE(?3, cost_usd), started_at = COALESCE(started_at, ?4),
284 finished_at = ?4, updated_at = ?4
285 WHERE id = ?5 AND finished_at IS NULL RETURNING id AS value",
286 )
287 .bind(&[
288 halt.as_str().into(),
289 said.as_str().into(),
290 cost_usd
291 .filter(|cost| cost.is_finite() && *cost >= 0.0)
292 .map_or(JsValue::NULL, JsValue::from),
293 now.as_str().into(),
294 run_id.into(),
295 ])?
296 .first::<String>(Some("value"))
297 .await?;
298 if claimed.is_none() {
299 return Ok(Outcome::Ok(RunStatus::Stopped));
300 }
301 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.302 // Said on its pull request, if it was for one.
303 if let (Some(_), Some(number)) = (pull_id, number) {
304 let noted = match halt {
305 Halt::Abuse => format!("stopped its {kind} run for unusual CPU use"),
306 Halt::Budget => format!("stopped its {kind} run at its cost cap"),
307 Halt::Time => format!("stopped its {kind} run at its time cap"),
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides308 };
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.309 self.note(repo_id, number, (POLICY_ACTOR_ID, POLICY_ACTOR_NAME), &noted).await?;
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides310 }
311 Ok(Outcome::Ok(RunStatus::Stopped))
312 }
313}
314
315/// What a change to one level did, for the audit log: the workflow-only
316/// domains added and removed, each with what it is limited to, and
317/// whether anything else changed.
318fn guardrails_change(before: &GuardrailSettings, after: &GuardrailSettings) -> String {
319 let describe = |entry: &WorkflowDomain| {
320 let workflows = if entry.workflows.is_empty() { "any workflow".to_owned() } else { entry.workflows.join(", ") };
321 let environments = if entry.environments.is_empty() { "any environment".to_owned() } else { entry.environments.join(", ") };
322 format!("{} ({workflows}; {environments})", entry.domain)
323 };
324 let added: Vec<String> = after.workflow_domains.iter().filter(|e| !before.workflow_domains.contains(e)).map(describe).collect();
325 let removed: Vec<String> = before.workflow_domains.iter().filter(|e| !after.workflow_domains.contains(e)).map(describe).collect();
326 let mut parts = Vec::new();
327 if !added.is_empty() {
328 parts.push(format!("Workflow-only domains added: {}.", added.join("; ")));
329 }
330 if !removed.is_empty() {
331 parts.push(format!("Workflow-only domains removed: {}.", removed.join("; ")));
332 }
333 let rest = |s: &GuardrailSettings| GuardrailSettings { workflow_domains: Vec::new(), updated_by: None, updated_at: None, ..s.clone() };
334 if rest(before) != rest(after) {
335 parts.push("Other guardrails changed.".to_owned());
336 }
337 if parts.is_empty() { "Guardrails saved unchanged.".to_owned() } else { parts.join(" ") }
338}
339
340/// The pull request id of a pull request's working copy
341/// (`pulls/<pull id>`, made by repos `fork_for_pull`), if `path` is one.
342fn working_copy_of(path: &RepoPath) -> Option<String> {
343 (path.namespace.eq_ignore_ascii_case(WORKING_COPIES) && !path.name.is_empty())
344 .then(|| path.name.to_lowercase())
345}
346
347/// Where repos keeps every pull request's working copy.
348const WORKING_COPIES: &str = "pulls";
349
350/// A principal that can read any repository of `workspace`, for the
351/// runner's lookups.
352fn member_of_service(workspace: &str) -> Viewer {
353 member_of(
354 &User {
355 id: "svc_runner".to_owned(),
356 username: "g1t".to_owned(),
357 ..User::default()
358 },
359 workspace,
360 )
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn path(namespace: &str, name: &str) -> RepoPath {
368 RepoPath {
369 namespace: namespace.to_owned(),
370 name: name.to_owned(),
371 }
372 }
373
374 #[test]
375 fn a_working_copy_names_its_pull_request() {
376 assert_eq!(
377 working_copy_of(&path("pulls", "pr_01m45bd1b2e359sayh78w977kv")).as_deref(),
378 Some("pr_01m45bd1b2e359sayh78w977kv")
379 );
380 assert_eq!(working_copy_of(&path("Pulls", "PR_7")).as_deref(), Some("pr_7"));
381 assert_eq!(working_copy_of(&path("flagon-io", "automation-lab")), None);
382 assert_eq!(working_copy_of(&path("pulls", "")), None);
383 }
384
385 #[test]
386 fn run_guardrails_take_an_optional_repo_id() {
387 let old: RunGuardrailsArgs =
388 serde_json::from_str(r#"{"repo":{"namespace":"pulls","name":"pr_1"}}"#).unwrap();
389 assert!(old.repo_id.is_none());
390 let by_id: RunGuardrailsArgs = serde_json::from_str(
391 r#"{"repo":{"namespace":"syntaqx","name":"automation-lab"},"repo_id":"rep_1"}"#,
392 )
393 .unwrap();
394 assert_eq!(by_id.repo_id.as_deref(), Some("rep_1"));
395 }
396
397 #[test]
398 fn a_change_to_workflow_domains_is_described_for_the_audit_log() {
399 let deploy = WorkflowDomain {
400 domain: "api.cloudflare.com".into(),
401 workflows: vec!["deploy.yml".into()],
402 environments: vec!["production".into()],
403 };
404 let open = WorkflowDomain { domain: "*.example.com".into(), ..WorkflowDomain::default() };
405 let before = GuardrailSettings { workflow_domains: vec![open.clone()], ..GuardrailSettings::default() };
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.406 let after = GuardrailSettings { workflow_domains: vec![deploy], restrict_network: Some(false), ..GuardrailSettings::default() };
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides407 assert_eq!(
408 guardrails_change(&before, &after),
409 "Workflow-only domains added: api.cloudflare.com (deploy.yml; production). Workflow-only domains removed: *.example.com (any workflow; any environment). Other guardrails changed."
410 );
411 assert_eq!(guardrails_change(&before, &before), "Guardrails saved unchanged.");
412 }
413
414 #[test]
415 fn renames_take_two_parameters() {
416 for sql in RENAMED {
417 assert!(sql.contains("?1") && sql.contains("?2"), "{sql}");
418 }
419 }
420
421 #[test]
422 fn a_level_reads_back_with_who_changed_it() {
423 let row = SettingsRow {
424 settings: r#"{"budgetUsd":2.5,"domains":["example.com"]}"#.to_owned(),
425 updated_by: "ada".to_owned(),
426 updated_at: "2026-10-04T00:00:00Z".to_owned(),
427 };
428 let settings = GuardrailSettings::from(row);
429 assert_eq!(settings.domains, vec!["example.com"]);
430 assert_eq!(settings.updated_by.as_deref(), Some("ada"));
431 }
432
433 #[test]
434 fn a_corrupt_level_reads_as_inheriting_everything() {
435 let row = SettingsRow {
436 settings: "not json".to_owned(),
437 updated_by: "ada".to_owned(),
438 updated_at: "2026-10-04T00:00:00Z".to_owned(),
439 };
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.440 let settings = GuardrailSettings::from(row);
441 assert!(settings.domains.is_empty() && settings.restrict_network.is_none());
Security: postcss-selector-parser 7.1.6 and basic-ftp 6.2.2, pinned by overrides442 }
443}

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