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

Fixes from the page sweep1//! What g1t pays for itself, and two caps on it.
2//!
3//! Every charge that settles (an agent run, sandbox time, a build) is
4//! split by what paid for it, at cost: a customer's real money, or g1t's.
5//! g1t's part goes to `g1t_spend` by day, bucket and billing account:
6//!
7//! - `comped`: work on a comped account (g1t's own, Flagon's), all of it.
8//! - `trial`, `oss`: the trial credit and the open-source pool.
9//! - `given`: a free workspace's overrun past its last bit of trial.
10//! - `unpaid`: charged, but with no real money behind it: Stripe's test
11//! key, or `FREE_WHILE_BUILDING`.
12//!
13//! The plan's included usage and on-demand charges with live payments are
14//! revenue, not g1t's. A workspace's own model provider costs g1t nothing.
15//!
16//! Two caps read it:
17//!
18//! 1. **A comped account's monthly budget**: `COMPED_MONTHLY_CEILING_MICROS`
19//! ($150), or the account's own limit in its terms (sudo, Accounts →
20//! Terms → Limit). Staff are emailed at 50, 75, 90 and 100%, once each a
21//! month; at 100% new work on it is refused until staff raise it or the
22//! month turns. Work already running finishes.
23//! 2. **The daily breaker**: when g1t's part across every workspace today
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.24//! (UTC) reaches `PLATFORM_DAILY_SPEND_CAP_MICROS` ($75), new compute
25//! that g1t would pay for (sandboxes for workflows, checks, the merge
26//! queue and agents alike) is paused for the rest of the day: everyone's
27//! except workspaces paying with real money on the plan or an enterprise
28//! contract, and comped accounts, which have their own monthly budget. Staff are emailed at once and sudo
Fixes from the page sweep29//! shows a red bar; staff can lift it for the day.
30//!
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.31//! Zero for either variable turns that cap off.
Fixes from the page sweep32
33use g1t_contracts::billing::{
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging34 AdminLiftBreakerArgs, BillingAccount, CompedBudget, ComputeKind, PlanKind, SpendBucket, SpendCaps,
Fixes from the page sweep35};
36use g1t_contracts::time::rfc3339;
37use g1t_contracts::{FailureCode, Outcome};
38use g1t_kit::now_ms;
39use serde::Deserialize;
40use worker::{Env, Result};
41
42use crate::Billing;
43use crate::credits::Drawn;
44use crate::limits::alert_level;
45
46/// The caps, from the billing service's variables.
47#[derive(Clone, Debug)]
48pub(crate) struct Caps {
49 /// `COMPED_MONTHLY_CEILING_MICROS`: a comped account's monthly budget
50 /// at cost, unless its terms set one. Zero: none.
51 pub comped_monthly: i64,
52 /// `PLATFORM_DAILY_SPEND_CAP_MICROS`: g1t's own spend a day before the
53 /// breaker trips. Zero: no breaker.
54 pub daily: i64,
55 /// `CLOUDFLARE_FIXED_MONTHLY_MICROS`: Cloudflare's subscriptions, an
56 /// estimate for sudo.
57 pub fixed_monthly: i64,
58 /// `COSTS_ALERT_EMAIL`. Empty: nothing is emailed.
59 pub alert_to: String,
60}
61
62impl Caps {
63 pub(crate) fn from_env(env: &Env) -> Self {
64 let number = |name: &str, default: i64| {
65 env.var(name).ok().and_then(|v| v.to_string().trim().parse::<i64>().ok()).unwrap_or(default).max(0)
66 };
67 Caps {
68 comped_monthly: number("COMPED_MONTHLY_CEILING_MICROS", 150_000_000),
69 daily: number("PLATFORM_DAILY_SPEND_CAP_MICROS", 75_000_000),
70 fixed_monthly: number("CLOUDFLARE_FIXED_MONTHLY_MICROS", 30_000_000),
71 alert_to: env.var("COSTS_ALERT_EMAIL").map(|v| v.to_string().trim().to_owned()).unwrap_or_default(),
72 }
73 }
74}
75
76/// g1t's part of one charge, at cost, by what paid for it.
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
78pub(crate) struct Share {
79 pub comped: i64,
80 pub trial: i64,
81 pub oss: i64,
82 pub given: i64,
83 pub unpaid: i64,
84}
85
86impl Share {
87 pub fn total(&self) -> i64 {
88 self.comped + self.trial + self.oss + self.given + self.unpaid
89 }
90
91 pub fn parts(&self) -> [(&'static str, i64); 5] {
92 [("comped", self.comped), ("trial", self.trial), ("oss", self.oss), ("given", self.given), ("unpaid", self.unpaid)]
93 }
94}
95
96/// What of a charge costing g1t `cost` g1t paid itself. `charged` is what
97/// the workspace was charged after `drawn` paid its part (both at price);
98/// `real_money` is whether payments are live. The plan's included usage
99/// and what the workspace is charged are revenue only with real money.
100pub(crate) fn share(cost: i64, charged: i64, drawn: &Drawn, comped: bool, real_money: bool) -> Share {
101 if cost <= 0 {
102 return Share::default();
103 }
104 if comped {
105 return Share { comped: cost, ..Share::default() };
106 }
107 let gross = charged.max(0) + drawn.total();
108 if gross <= 0 {
109 // Charged nothing at all (free while g1t is being built out).
110 return Share { unpaid: cost, ..Share::default() };
111 }
112 let part = |paid: i64| (i128::from(cost) * i128::from(paid.max(0)) / i128::from(gross)) as i64;
113 let (trial, oss, given) = (part(drawn.trial), part(drawn.oss), part(drawn.given));
114 let unpaid = if real_money { 0 } else { (cost - trial - oss - given).max(0) };
115 Share { comped: 0, trial, oss, given, unpaid }
116}
117
118/// A comped account's monthly budget: its own (terms' limit) or the
119/// default; and whether it is the default. Zero: none.
120pub(crate) fn comped_ceiling(own: Option<i64>, default: i64) -> (i64, bool) {
121 match own {
122 Some(own) => (own.max(0), false),
123 None => (default.max(0), true),
124 }
125}
126
127/// Whether a budget is used up.
128pub(crate) fn used_up(used: i64, ceiling: i64) -> bool {
129 ceiling > 0 && used >= ceiling
130}
131
132/// Whether the breaker stops new runs: on, reached, and not lifted today.
133pub(crate) fn breaker_open(today: i64, cap: i64, lifted: bool) -> bool {
134 cap > 0 && today >= cap && !lifted
135}
136
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.137/// Whether the breaker is about this start: any compute g1t would pay
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.138/// for. Every kind costs g1t sandbox time, so none is left out; a
139/// workspace whose spend is covered is left out by [`covered_by_revenue`]
140/// instead.
141pub(crate) fn breaker_applies(_kind: ComputeKind) -> bool {
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.142 true
Fixes from the page sweep143}
144
145/// Whether a workspace's spend is covered by revenue, so the breaker
146/// leaves it alone: live payments, not comped, and on the plan it pays for
147/// (not given it by staff) or an enterprise contract.
148pub(crate) fn covered_by_revenue(plan: PlanKind, comped: bool, plan_given: bool, live: bool) -> bool {
149 live && !comped && match plan {
150 PlanKind::Enterprise => true,
151 PlanKind::Paid => !plan_given,
152 _ => false,
153 }
154}
155
156/// The alert to send now: the level reached, if higher than any sent this
157/// month.
158pub(crate) fn alert_to_send(level: u32, sent: u32) -> Option<u32> {
159 (level > 0 && level > sent).then_some(level)
160}
161
162/// `$150.00`: whole cents.
163pub(crate) fn cents(micros: i64) -> String {
164 let cents = (micros as f64 / 10_000.0).round() as i64;
165 format!("{}${}.{:02}", if cents < 0 { "-" } else { "" }, cents.abs() / 100, cents.abs() % 100)
166}
167
168/// What a start on a comped account past its budget is told. Staff-only:
169/// only comped (g1t's own) accounts see it.
170pub(crate) fn comped_refusal(name: &str, used: i64, ceiling: i64) -> String {
171 format!(
172 "{name}'s monthly budget for g1t's own agents is used up ({} of {} this month at cost), so new runs wait. Staff can raise it in sudo: Accounts, {name}, Terms, Limit.",
173 cents(used),
174 cents(ceiling)
175 )
176}
177
178/// What a hosted-model start is told while the breaker is open.
179pub(crate) fn breaker_refusal(today: i64, cap: i64) -> String {
180 format!(
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.181 "g1t's daily spend breaker is open: g1t has paid {} of its {} a day for work today, so new runs g1t pays for wait until 00:00 UTC. Work on the paid plan still runs.",
Fixes from the page sweep182 cents(today),
183 cents(cap)
184 )
185}
186
187#[derive(Deserialize)]
188struct Sum {
189 micros: Option<i64>,
190}
191
192#[derive(Deserialize)]
193struct BreakerRow {
194 tripped_at: Option<String>,
195 told_at: Option<String>,
196 lifted_by: Option<String>,
197 lifted_at: Option<String>,
198 lift_note: Option<String>,
199}
200
201fn today() -> String {
202 rfc3339(now_ms())[..10].to_owned()
203}
204
205impl Billing {
206 fn live(&self) -> bool {
207 self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live)
208 }
209
210 /// Counts g1t's part of a charge that just settled, and trips the
211 /// breaker if today reached its cap. Never fails the charge: a problem
212 /// here is logged.
213 pub(crate) async fn count_spend(&self, workspace: &str, cost: i64, charged: i64, drawn: &Drawn) {
214 if let Err(error) = self.try_count_spend(workspace, cost, charged, drawn).await {
215 worker::console_error!("could not count g1t's spend for {workspace}: {error}");
216 }
217 }
218
219 async fn try_count_spend(&self, workspace: &str, cost: i64, charged: i64, drawn: &Drawn) -> Result<()> {
220 if cost <= 0 {
221 return Ok(());
222 }
223 let account = self.account_of(workspace).await?;
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging224 let paid = share(cost, charged, drawn, account.terms.full_discount(), self.live());
Fixes from the page sweep225 if paid.total() == 0 {
226 return Ok(());
227 }
228 let day = today();
229 let mut writes = vec![];
230 for (bucket, micros) in paid.parts() {
231 if micros > 0 {
232 writes.push(
233 self.db
234 .prepare(
235 "INSERT INTO g1t_spend (day, bucket, account, micros) VALUES (?1, ?2, ?3, ?4)
236 ON CONFLICT (day, bucket, account) DO UPDATE SET micros = micros + ?4",
237 )
238 .bind(&[day.as_str().into(), bucket.into(), account.id.as_str().into(), (micros as f64).into()])?,
239 );
240 }
241 }
242 self.db.batch(writes).await?;
243 if self.caps.daily <= 0 {
244 return Ok(());
245 }
246 let total = self.spent_on(&day).await?;
247 if total < self.caps.daily {
248 return Ok(());
249 }
250 // Tripped: recorded once a day, and staff told at once.
251 let now = rfc3339(now_ms());
252 let tripped = self
253 .db
254 .prepare(
255 "INSERT INTO spend_breaker (day, tripped_at, tripped_micros) VALUES (?1, ?2, ?3)
256 ON CONFLICT (day) DO UPDATE SET tripped_at = ?2, tripped_micros = ?3 WHERE spend_breaker.tripped_at IS NULL
257 RETURNING day",
258 )
259 .bind(&[day.as_str().into(), now.as_str().into(), (total as f64).into()])?
260 .first::<serde_json::Value>(None)
261 .await?;
262 if tripped.is_some() {
263 self.tell_breaker(&day, total).await?;
264 }
265 Ok(())
266 }
267
268 /// g1t's own spend on `day`, across every workspace.
269 async fn spent_on(&self, day: &str) -> Result<i64> {
270 Ok(self
271 .db
272 .prepare("SELECT SUM(micros) AS micros FROM g1t_spend WHERE day = ?")
273 .bind(&[day.into()])?
274 .first::<Sum>(None)
275 .await?
276 .and_then(|s| s.micros)
277 .unwrap_or(0))
278 }
279
280 async fn breaker_row(&self, day: &str) -> Result<Option<BreakerRow>> {
281 self.db
282 .prepare("SELECT tripped_at, told_at, lifted_by, lifted_at, lift_note FROM spend_breaker WHERE day = ?")
283 .bind(&[day.into()])?
284 .first::<BreakerRow>(None)
285 .await
286 }
287
288 /// Emails staff that the breaker tripped, and notes it was told.
289 async fn tell_breaker(&self, day: &str, total: i64) -> Result<()> {
290 if self.caps.alert_to.is_empty() {
291 return Ok(());
292 }
293 let lifted = self.breaker_row(day).await?.and_then(|row| row.lifted_by);
294 let mut lines = vec![
295 format!(
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.296 "g1t paid {} for work today ({day}, UTC), its daily cap of {} (PLATFORM_DAILY_SPEND_CAP_MICROS). New compute that g1t pays for (workflows, checks, the merge queue and other runs) is paused until 00:00 UTC; workspaces paying with real money, and comped accounts, are not affected. Runs already going finish.",
Fixes from the page sweep297 cents(total),
298 cents(self.caps.daily)
299 ),
300 "To let them start again today: sudo, Costs & margin, Lift for today. To change the cap: PLATFORM_DAILY_SPEND_CAP_MICROS in services/billing/wrangler.jsonc.".to_owned(),
301 ];
302 if let Some(by) = lifted {
303 lines.insert(1, format!("{by} had already lifted it for today, so nothing is paused."));
304 }
305 match crate::margin::email_staff(&self.env, &self.caps.alert_to, &format!("g1t: the daily spend breaker tripped at {}", cents(total)), &lines).await {
306 Ok(()) => {
307 self.db
308 .prepare("UPDATE spend_breaker SET told_at = ? WHERE day = ?")
309 .bind(&[rfc3339(now_ms()).into(), day.into()])?
310 .run()
311 .await?;
312 }
313 Err(error) => worker::console_error!("could not email the breaker: {error}"),
314 }
315 Ok(())
316 }
317
318 /// Why a start is refused by the breaker, if it is.
319 pub(crate) async fn breaker_refuses(
320 &self,
321 plan: PlanKind,
322 account: &BillingAccount,
323 kind: ComputeKind,
324 ) -> Result<Option<String>> {
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.325 if self.caps.daily <= 0 || !breaker_applies(kind) {
Fixes from the page sweep326 return Ok(None);
327 }
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging328 let comped = account.terms.full_discount();
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.329 // A comped account is held by its own monthly budget instead.
330 if comped || covered_by_revenue(plan, comped, account.allowances.plan, self.live()) {
Fixes from the page sweep331 return Ok(None);
332 }
333 let day = today();
334 let spent = self.spent_on(&day).await?;
335 if spent < self.caps.daily {
336 return Ok(None);
337 }
338 let lifted = self.breaker_row(&day).await?.is_some_and(|row| row.lifted_at.is_some());
339 Ok(breaker_open(spent, self.caps.daily, lifted).then(|| breaker_refusal(spent, self.caps.daily)))
340 }
341
342 /// A comped account's budget this month.
343 pub(crate) async fn comped_budget(&self, account: &BillingAccount) -> Result<CompedBudget> {
344 let month = &rfc3339(now_ms())[..7];
345 let used = self
346 .db
347 .prepare("SELECT SUM(micros) AS micros FROM g1t_spend WHERE account = ? AND bucket = 'comped' AND day >= ?")
348 .bind(&[account.id.as_str().into(), format!("{month}-01").into()])?
349 .first::<Sum>(None)
350 .await?
351 .and_then(|s| s.micros)
352 .unwrap_or(0);
353 let (ceiling, default_ceiling) = comped_ceiling(account.terms.ceiling_micros, self.caps.comped_monthly);
354 Ok(CompedBudget {
355 account: account.id.clone(),
356 name: account.name.clone(),
357 used_micros: used,
358 ceiling_micros: ceiling,
359 default_ceiling,
360 level: alert_level(used, ceiling),
361 })
362 }
363
364 /// Why new work on a comped account is refused, if its budget is used
365 /// up. None for every other account.
366 pub(crate) async fn comped_stop(&self, account: &BillingAccount) -> Result<Option<String>> {
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging367 if !account.terms.full_discount() {
Fixes from the page sweep368 return Ok(None);
369 }
370 let budget = self.comped_budget(account).await?;
371 Ok(used_up(budget.used_micros, budget.ceiling_micros).then(|| comped_refusal(&account.name, budget.used_micros, budget.ceiling_micros)))
372 }
373
374 /// Every 15 minutes: comped budgets' alerts, once each level a month,
375 /// and a tripped breaker staff were not yet told about.
376 pub(crate) async fn watch_spend(&self) -> Result<()> {
377 if self.caps.alert_to.is_empty() {
378 return Ok(());
379 }
380 let month = rfc3339(now_ms())[..7].to_owned();
381 #[derive(Deserialize)]
382 struct Id {
383 id: String,
384 }
385 let comped = self
386 .db
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging387 .prepare(format!("SELECT id FROM billing_accounts WHERE {}", crate::sales::FULL_DISCOUNT_SQL))
Fixes from the page sweep388 .all()
389 .await?
390 .results::<Id>()?;
391 #[derive(Deserialize)]
392 struct Sent {
393 level: Option<i64>,
394 }
395 for Id { id } in comped {
396 let Some(account) = self.find_account(&id).await? else { continue };
397 let budget = self.comped_budget(&account).await?;
398 let sent = self
399 .db
400 .prepare("SELECT MAX(level) AS level FROM budget_alerts WHERE account = ? AND month = ?")
401 .bind(&[id.as_str().into(), month.as_str().into()])?
402 .first::<Sent>(None)
403 .await?
404 .and_then(|s| s.level)
405 .unwrap_or(0);
406 let Some(level) = alert_to_send(budget.level, u32::try_from(sent).unwrap_or(0)) else { continue };
407 let name = &account.name;
408 let mut lines = vec![format!(
409 "{name}'s work has cost g1t {} this month, {level}% of its {} monthly budget ({}).",
410 cents(budget.used_micros),
411 cents(budget.ceiling_micros),
412 if budget.default_ceiling { "COMPED_MONTHLY_CEILING_MICROS" } else { "its own limit, in its terms" }
413 )];
414 lines.push(if level >= 100 {
415 format!("New agent runs, checks and builds on {name} are refused until the budget is raised or the month turns. Runs already going finish. To raise it: sudo, Accounts, {name}, Terms, Limit.")
416 } else {
417 format!("At 100%, new work on {name} is refused until staff raise the budget. To raise it now: sudo, Accounts, {name}, Terms, Limit.")
418 });
419 let subject = format!("g1t: {name} has used {level}% of its monthly budget");
420 match crate::margin::email_staff(&self.env, &self.caps.alert_to, &subject, &lines).await {
421 Ok(()) => {
422 self.db
423 .prepare("INSERT OR IGNORE INTO budget_alerts (account, month, level, sent_at) VALUES (?, ?, ?, ?)")
424 .bind(&[id.as_str().into(), month.as_str().into(), level.into(), rfc3339(now_ms()).into()])?
425 .run()
426 .await?;
427 }
428 Err(error) => worker::console_error!("could not email {name}'s budget alert: {error}"),
429 }
430 }
431 // A trip whose email did not go out when it happened.
432 let day = today();
433 if self.breaker_row(&day).await?.is_some_and(|row| row.tripped_at.is_some() && row.told_at.is_none()) {
434 let total = self.spent_on(&day).await?;
435 self.tell_breaker(&day, total).await?;
436 }
437 Ok(())
438 }
439
440 /// `admin_spend_caps`: g1t's own spend against its caps.
441 pub(crate) async fn spend_caps(&self) -> Result<SpendCaps> {
442 let day = today();
443 let month = day[..7].to_owned();
444 let month_start = format!("{month}-01");
445 let today_micros = self.spent_on(&day).await?;
446 let row = self.breaker_row(&day).await?;
447 let lifted = row.as_ref().is_some_and(|r| r.lifted_at.is_some());
448 #[derive(Deserialize)]
449 struct Bucket {
450 bucket: String,
451 micros: Option<i64>,
452 }
453 let rows = self
454 .db
455 .prepare("SELECT bucket, SUM(micros) AS micros FROM g1t_spend WHERE day >= ? GROUP BY bucket")
456 .bind(&[month_start.as_str().into()])?
457 .all()
458 .await?
459 .results::<Bucket>()?;
460 let month_buckets = ["comped", "trial", "oss", "given", "unpaid"]
461 .iter()
462 .map(|bucket| SpendBucket {
463 bucket: (*bucket).to_owned(),
464 title: bucket_title(bucket).to_owned(),
465 micros: rows.iter().find(|r| r.bucket == *bucket).and_then(|r| r.micros).unwrap_or(0),
466 })
467 .collect();
468 #[derive(Deserialize)]
469 struct Id {
470 id: String,
471 }
472 let ids = self
473 .db
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging474 .prepare(format!("SELECT id FROM billing_accounts WHERE {} ORDER BY id", crate::sales::FULL_DISCOUNT_SQL))
Fixes from the page sweep475 .all()
476 .await?
477 .results::<Id>()?;
478 let mut comped = vec![];
479 for Id { id } in ids {
480 if let Some(account) = self.find_account(&id).await? {
481 comped.push(self.comped_budget(&account).await?);
482 }
483 }
484 // Free workspaces' share of the reconciled costs that are not on
485 // the ledger (models, sandboxes and builds are, above).
486 let free_tier_micros = self
487 .db
488 .prepare(format!(
489 "SELECT SUM(cost_micros) AS micros FROM workspace_costs
490 WHERE day >= ?1 AND bucket NOT IN ('models', 'sandboxes', 'deployments')
491 AND workspace NOT IN ({internal})
492 AND workspace NOT IN (SELECT workspace FROM workspace_costs WHERE day >= ?1 GROUP BY workspace HAVING SUM(revenue_micros) > 0)",
493 internal = crate::sales::INTERNAL_SQL
494 ))
495 .bind(&[month_start.as_str().into()])?
496 .first::<Sum>(None)
497 .await?
498 .and_then(|s| s.micros)
499 .unwrap_or(0);
500 let revenue_micros = self
501 .db
502 .prepare("SELECT SUM(cash_micros) AS micros FROM margin_days WHERE day >= ?")
503 .bind(&[month_start.as_str().into()])?
504 .first::<Sum>(None)
505 .await?
506 .and_then(|s| s.micros)
507 .unwrap_or(0);
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises508 // What a testing reset wiped from the ledger is still here.
509 #[derive(Deserialize)]
510 struct Reset {
511 account: String,
512 micros: Option<i64>,
513 }
514 let reset = self
515 .db
516 .prepare(RESET_SPEND_SQL)
517 .bind(&[month_start.as_str().into()])?
518 .all()
519 .await?
520 .results::<Reset>()?;
521 let reset_micros = reset.iter().filter_map(|r| r.micros).sum();
522 let reset_workspaces = reset.into_iter().map(|r| r.account.strip_prefix("ws_").unwrap_or(&r.account).to_owned()).collect();
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing523 let fixed = self.fixed_monthly(self.caps.fixed_monthly).await?;
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)524 // This month's days so far, each its billing cycle's share: the
525 // same accrual as the statement's range (`cycle::accrued`).
526 let fixed_month_micros = crate::cycle::accrued(fixed.monthly_micros, &month_start, &day, self.cycle_anchor().await?);
Fixes from the page sweep527 Ok(SpendCaps {
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises528 reset_micros,
529 reset_workspaces,
Fixes from the page sweep530 day,
531 month,
532 today_micros,
533 daily_cap_micros: self.caps.daily,
534 tripped: breaker_open(today_micros, self.caps.daily, lifted),
535 tripped_at: row.as_ref().and_then(|r| r.tripped_at.clone()),
536 lifted_by: row.as_ref().and_then(|r| r.lifted_by.clone()),
537 lifted_at: row.as_ref().and_then(|r| r.lifted_at.clone()),
538 lift_note: row.as_ref().and_then(|r| r.lift_note.clone()),
539 month_buckets,
540 comped,
541 free_tier_micros,
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing542 fixed_monthly_micros: fixed.monthly_micros,
543 fixed_source: fixed.source.into(),
544 fixed_read_at: fixed.read_at,
545 fixed_items: fixed.items,
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)546 fixed_month_micros,
Fixes from the page sweep547 revenue_micros,
548 })
549 }
550
551 /// `admin_lift_breaker`: hosted-model runs start again for the rest of
552 /// today (UTC).
553 pub(crate) async fn admin_lift_breaker(&self, a: AdminLiftBreakerArgs) -> Result<Outcome<SpendCaps>> {
554 let (by, note) = (a.by.trim(), a.note.trim());
555 if by.is_empty() || note.len() < 5 {
556 return Ok(Outcome::fail(FailureCode::Invalid, "Say who is lifting it, and why, in the note."));
557 }
558 let day = today();
559 let now = rfc3339(now_ms());
560 let note: String = note.chars().take(500).collect();
561 self.db
562 .prepare(
563 "INSERT INTO spend_breaker (day, lifted_by, lifted_at, lift_note) VALUES (?1, ?2, ?3, ?4)
564 ON CONFLICT (day) DO UPDATE SET lifted_by = ?2, lifted_at = ?3, lift_note = ?4",
565 )
566 .bind(&[day.as_str().into(), by.into(), now.as_str().into(), note.as_str().into()])?
567 .run()
568 .await?;
569 let spent = self.spent_on(&day).await?;
570 self.audit("costs", "breaker_lifted", &format!("{day}: lifted at {} of {}: {note}", cents(spent), cents(self.caps.daily)), by)
571 .await?;
572 Ok(Outcome::Ok(self.spend_caps().await?))
573 }
574}
575
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises576/// g1t's own spend since `?1` on accounts a testing reset wiped later than
577/// the day it was spent (`admin_actions`, action `reset`), by account.
578pub(crate) const RESET_SPEND_SQL: &str = "SELECT s.account, SUM(s.micros) AS micros FROM g1t_spend s
579 WHERE s.day >= ?1 AND EXISTS (SELECT 1 FROM admin_actions a WHERE a.action = 'reset' AND a.account = s.account AND substr(a.created_at, 1, 10) >= s.day)
580 GROUP BY s.account HAVING SUM(s.micros) > 0 ORDER BY s.account";
581
Fixes from the page sweep582/// How sudo names a bucket of g1t's own spend.
583pub(crate) fn bucket_title(bucket: &str) -> &'static str {
584 match bucket {
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging585 "comped" => "100% discount (g1t's own)",
Fixes from the page sweep586 "trial" => "Trial pool",
587 "oss" => "Open-source pool",
588 "given" => "Free overruns g1t covered",
589 "unpaid" => "Charged without real money",
590 _ => "Other",
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn drawn(credit: i64, trial: i64, oss: i64, given: i64) -> Drawn {
599 Drawn { credit, trial, oss, given }
600 }
601
602 #[test]
603 fn comped_work_is_all_g1ts_at_cost() {
604 let s = share(1_000_000, 0, &Drawn::default(), true, true);
605 assert_eq!(s, Share { comped: 1_000_000, ..Share::default() });
606 // Nothing that cost nothing is counted.
607 assert_eq!(share(0, 0, &Drawn::default(), true, true).total(), 0);
608 assert_eq!(share(-5, 0, &Drawn::default(), false, false).total(), 0);
609 }
610
611 #[test]
612 fn pools_pay_their_share_of_the_cost_not_the_price() {
613 // $1 of cost charged at $1.20, all from the trial: $1 is g1t's.
614 assert_eq!(share(1_000_000, 0, &drawn(0, 1_200_000, 0, 0), false, true), Share { trial: 1_000_000, ..Share::default() });
615 // Half the open-source pool, half charged on a live card: half is g1t's.
616 assert_eq!(share(1_000_000, 600_000, &drawn(0, 0, 600_000, 0), false, true), Share { oss: 500_000, ..Share::default() });
617 // A free workspace's overrun past its trial.
618 let s = share(1_000_000, 0, &drawn(0, 300_000, 0, 900_000), false, true);
619 assert_eq!((s.trial, s.given), (250_000, 750_000));
620 }
621
622 #[test]
623 fn revenue_is_only_revenue_with_real_money() {
624 // Plan credit and an on-demand charge, live: none of it is g1t's.
625 assert_eq!(share(1_000_000, 600_000, &drawn(600_000, 0, 0, 0), false, true).total(), 0);
626 // The same in test mode: all of it.
627 assert_eq!(share(1_000_000, 600_000, &drawn(600_000, 0, 0, 0), false, false), Share { unpaid: 1_000_000, ..Share::default() });
628 // Free while building: charged nothing, all g1t's.
629 assert_eq!(share(1_000_000, 0, &Drawn::default(), false, true), Share { unpaid: 1_000_000, ..Share::default() });
630 }
631
632 #[test]
633 fn a_comped_account_gets_the_default_budget_unless_its_terms_set_one() {
634 assert_eq!(comped_ceiling(None, 150_000_000), (150_000_000, true));
635 assert_eq!(comped_ceiling(Some(400_000_000), 150_000_000), (400_000_000, false));
636 // Zero: no budget.
637 assert_eq!(comped_ceiling(None, 0), (0, true));
638 assert!(!used_up(1_000_000_000, 0));
639 }
640
641 #[test]
642 fn a_comped_budget_refuses_new_work_at_one_hundred_percent() {
643 let ceiling = 150_000_000;
644 assert!(!used_up(149_999_999, ceiling));
645 assert!(used_up(150_000_000, ceiling));
646 assert!(used_up(151_000_000, ceiling));
647 let message = comped_refusal("flagon-io", 150_000_000, ceiling);
648 assert!(message.contains("used up") && message.contains("$150.00 of $150.00") && message.contains("sudo"), "{message}");
649 }
650
651 #[test]
652 fn budget_alerts_go_once_per_level_each_month() {
653 let ceiling = 150_000_000;
654 let mut sent = 0;
655 let mut emailed = vec![];
656 // Spend climbs through the month, checked every 15 minutes.
657 for used in [10_000_000, 74_000_000, 75_000_000, 80_000_000, 112_500_000, 120_000_000, 135_000_000, 140_000_000, 150_000_000, 170_000_000] {
658 if let Some(level) = alert_to_send(alert_level(used, ceiling), sent) {
659 emailed.push(level);
660 sent = level;
661 }
662 }
663 assert_eq!(emailed, vec![50, 75, 90, 100]);
664 // A jump straight past several levels sends only the highest.
665 assert_eq!(alert_to_send(alert_level(140_000_000, ceiling), 0), Some(90));
666 // A new month starts from nothing sent.
667 assert_eq!(alert_to_send(alert_level(80_000_000, ceiling), 0), Some(50));
668 }
669
670 #[test]
671 fn the_breaker_trips_at_the_cap_and_staff_can_lift_it_for_the_day() {
672 let cap = 75_000_000;
673 assert!(!breaker_open(74_999_999, cap, false));
674 assert!(breaker_open(75_000_000, cap, false));
675 // Lifted: open no more today.
676 assert!(!breaker_open(90_000_000, cap, true));
677 // Off.
678 assert!(!breaker_open(1_000_000_000, 0, false));
679 // Tomorrow's total starts at zero: the breaker resets by itself.
680 assert!(!breaker_open(0, cap, false));
681 let message = breaker_refusal(80_000_000, cap);
682 assert!(message.contains("$80.00 of its $75.00") && message.contains("00:00 UTC"), "{message}");
683 }
684
685 #[test]
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.686 fn the_breaker_is_about_all_compute_g1t_pays_for() {
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.687 assert!(breaker_applies(ComputeKind::Check));
688 assert!(breaker_applies(ComputeKind::Workflow));
689 assert!(breaker_applies(ComputeKind::Deploy));
Fixes from the page sweep690 }
691
692 #[test]
693 fn workspaces_paying_with_real_money_are_never_paused_by_the_breaker() {
694 assert!(covered_by_revenue(PlanKind::Paid, false, false, true));
695 assert!(covered_by_revenue(PlanKind::Enterprise, false, false, true));
696 // Test-mode payments are not money.
697 assert!(!covered_by_revenue(PlanKind::Paid, false, false, false));
698 // The plan given by staff, comped, free: g1t pays.
699 assert!(!covered_by_revenue(PlanKind::Paid, false, true, true));
700 assert!(!covered_by_revenue(PlanKind::Internal, true, false, true));
701 assert!(!covered_by_revenue(PlanKind::Free, false, false, true));
702 }
703
704 #[test]
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises705 fn spend_a_testing_reset_wiped_is_found_by_the_reset_after_it() {
706 // syntaqx's $7.41 of 2026-10-02 to 10-05, reset on 10-07: still
707 // g1t's spend, gone from its ledger.
708 assert_eq!(crate::rename::parameters(RESET_SPEND_SQL), 1);
709 assert!(RESET_SPEND_SQL.contains("a.action = 'reset'") && RESET_SPEND_SQL.contains("substr(a.created_at, 1, 10) >= s.day"));
710 }
711
712 #[test]
Fixes from the page sweep713 fn amounts_read_in_cents() {
714 assert_eq!(cents(150_000_000), "$150.00");
715 assert_eq!(cents(1_234_567), "$1.23");
716 assert_eq!(cents(5_000), "$0.01");
717 }
718}

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