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

Commit

The spend caps of g1t's former agents are gone end to end. Billing no longer serves set_caps (the owners' run and issue caps, which nothing called), reads the owners' caps from limits, or puts a run cap and an issue cap in a workspace's entitlements; ISSUE_CAP_MICROS and RUN_CAP_MICROS are no longer read, and a staff override no longer carries either, so sudo stops sending them. The runner no longer turns a run cap into a cost cap, and the GUARDRAILS it hands a sandbox says the run's time cap and default branch only, without the command rules, deny patterns and cost cap nothing reads. A run's guardrails no longer carry restrictNetwork and hosts, sent only for runners from before network rules were removed. Stored data still reads: the limits and billing_accounts columns stay, no migration is touched, and a run recorded with a budget still shows it. Checked: cargo test for g1t-billing (243) and g1t-contracts (307), cargo check of the workspace, the TypeScript typecheck, the runner's 88 tests, sudo's 153, the contracts' 48 and the deploy tool's 92.

syntaqxcommitted Parent4bfc322Browse files
18 files+24−2340/18 viewed
+0−4
122122 ossRepoMicros: null,
123123 trialMicros: null,
124124 maxConcurrentAgents: null,
125− runCapMicros: null,
126− issueCapMicros: null,
127125 auditRetentionDays: null,
128126 hold: null,
129127 },
138136 trialMicros: 2_500_000,
139137 // Retired with g1t's own agents: always cleared, whatever an old form sends.
140138 maxConcurrentAgents: null,
141− runCapMicros: null,
142− issueCapMicros: null,
143139 auditRetentionDays: 365,
144140 hold: "Card disputed; waiting on the bank",
145141 },
+0−2
228228 ossRepoMicros: oss.value,
229229 trialMicros: trial.value,
230230 maxConcurrentAgents: null,
231− runCapMicros: null,
232− issueCapMicros: null,
233231 auditRetentionDays,
234232 hold: hold || null,
235233 },
+0−29
795795 /// the trial, then 10). None: the default.
796796 #[serde(default)]
797797 pub max_concurrent_agents: Option<u32>,
798− /// One run's spend cap, in place of `RUN_CAP_MICROS` and the owners'
799− /// own. None: theirs, or the default.
800− #[serde(default)]
801− pub run_cap_micros: Option<i64>,
802− /// What the agents on one issue may spend in all, in place of
803− /// `ISSUE_CAP_MICROS` and the owners' own. None: theirs, or the default.
804− #[serde(default)]
805− pub issue_cap_micros: Option<i64>,
806798 /// Days of audit log its workspaces keep, in place of the plan's (7
807799 /// free, 90 on the plan), longer or shorter. None: the plan's.
808800 #[serde(default)]
980972 /// The longest one run may take: 60 minutes in the first month or on
981973 /// the trial; otherwise the guardrails' own caps (`MAX_MINUTES`).
982974 pub max_run_minutes: u32,
983− /// One run's spend cap (`RUN_CAP_MICROS`, $2 by default); staff can
984− /// override it.
985− pub run_cap_micros: i64,
986− /// What agents may spend on one issue in all (`ISSUE_CAP_MICROS`, $10
987− /// by default); the owners can set it (`set_caps`), and staff override.
988− pub issue_cap_micros: i64,
989975 /// Where on-demand work stops: g1t's ceiling on usage not yet paid
990976 /// for. `UNLIMITED_MICROS` for g1t's own workspaces; 0 for a free one,
991977 /// which has no on-demand usage.
12401226 pub workspace: String,
12411227 #[serde(alias = "keep_going")]
12421228 pub keep_going: bool,
1243−}
1244−
1245−/// `set_caps`: the owners' own caps on agents: one run's spend ($0.10 to
1246−/// $100) and what the agents on one issue may spend in all ($1 to $1,000).
1247−/// None goes back to the default ($2 and $10). A cap g1t staff set for the
1248−/// account wins over both. Owners only. Returns `Outcome<Entitlements>`.
1249−#[derive(Debug, Serialize, Deserialize)]
1250−#[serde(rename_all = "camelCase")]
1251−pub struct SetCapsArgs {
1252− pub actor: User,
1253− pub workspace: String,
1254− #[serde(default, alias = "run_cap_micros")]
1255− pub run_cap_micros: Option<i64>,
1256− #[serde(default, alias = "issue_cap_micros")]
1257− pub issue_cap_micros: Option<i64>,
12581229 }
12591230
12601231 /// What staff see beside a request: the workspace's history with g1t.
+1−21
2525 /// The longest any run may be allowed to take, in minutes.
2626 pub const MAX_MINUTES: u32 = 240;
2727
28−/// One run's spend cap unless an owner or g1t staff set another, in
29−/// micro-dollars: billing's spend cap per run.
30−pub const DEFAULT_RUN_CAP_MICROS: i64 = 2_000_000;
31−
3228 /// How long each kind of run may take unless the workspace says otherwise.
3329 pub fn default_minutes(kind: RunKind) -> u32 {
3430 match kind {
6561 pub struct Guardrails {
6662 /// Every kind of run.
6763 pub minutes: BTreeMap<String, u32>,
68− /// Always false, and `hosts` always empty: sent for a runner deployed
69− /// before network rules were removed, which reads them to start its
70− /// sandboxes with an open network rather than failing to start them.
71− /// Never read. Remove once the runner after this change is deployed.
72− #[serde(skip_deserializing)]
73− pub restrict_network: bool,
74− #[serde(skip_deserializing)]
75− pub hosts: Vec<String>,
7664 }
7765
7866 impl Guardrails {
8371 .into_iter()
8472 .map(|kind| (kind.as_str().to_owned(), default_minutes(kind)))
8573 .collect(),
86− restrict_network: false,
87− hosts: Vec::new(),
8874 }
8975 }
9076
316302 }))
317303 .unwrap();
318304 assert_eq!(old.minutes_for(RunKind::Implement), 90);
319− assert!(!old.restrict_network && old.hosts.is_empty());
320− }
321−
322− #[test]
323− fn a_runner_from_before_reads_an_open_network() {
324305 let sent = serde_json::to_value(Guardrails::defaults()).unwrap();
325− assert_eq!(sent["restrictNetwork"], false);
326− assert_eq!(sent["hosts"], serde_json::json!([]));
306+ assert!(sent.get("restrictNetwork").is_none() && sent.get("hosts").is_none());
327307 }
328308
329309 #[test]
+1−2
9292 model?: string | null;
9393 sandbox: string;
9494 startedBy?: string | null;
95− /** Its caps, from its guardrails. */
96− budgetUsd?: number | null;
95+ /** Its time cap, from its guardrails. */
9796 timeCapMinutes?: number | null;
9897 };
9998
+0−13
184184 trialMicros: number | null;
185185 /** Agents at once, in place of the plan's (2 in the first month or on the trial, then 10); null for the default. */
186186 maxConcurrentAgents?: number | null;
187− /** One run's spend cap, in place of the owners' and the default $2; null for none. */
188− runCapMicros?: number | null;
189− /** What one issue's agents may spend in all, in place of the owners' and the default $10; null for none. */
190− issueCapMicros?: number | null;
191187 /** Days of audit log its workspaces keep, in place of the plan's (7 free, 90 on the plan), longer or shorter; null for the plan's. */
192188 auditRetentionDays?: number | null;
193189 /** A hold on new compute, with why; null for none. */
255251 maxConcurrentAgents: number;
256252 /** 60 in the first month or on the trial; otherwise the guardrails' own caps. */
257253 maxRunMinutes: number;
258− /** One run's spend cap, $2 by default; staff can override it. */
259− runCapMicros: number;
260− /** Agent spend on one issue in all, $10 by default. */
261− issueCapMicros: number;
262254 /** g1t's ceiling on usage not yet paid for; `UNLIMITED_MICROS` for g1t's own; 0 for free. */
263255 ceilingMicros: number;
264256 /** Usage not yet paid for this month, prepayment taken off. */
10241016 ): Promise<Result<LimitRequest>>;
10251017 /** The workspace's requests and their answers, newest first. Members only. */
10261018 limitRequests(workspace: string, viewer: Viewer): Promise<Result<LimitRequest[]>>;
1027− /**
1028− * The owners' own caps on agents: one run's spend ($0.10 to $100) and one issue's ($1 to $1,000).
1029− * Null goes back to the default ($2 and $10). A cap staff set wins. Owners only.
1030− */
1031− setCaps(actor: User, workspace: string, caps: { runCapMicros: number | null; issueCapMicros: number | null }): Promise<Result<Entitlements>>;
10321019 /** An owner's answer to a spend spike: keep going for 24 hours, or stop. */
10331020 confirmSpike(actor: User, workspace: string, keepGoing: boolean): Promise<Result<Entitlements>>;
10341021 /** Every metered price and the recent changes. Public. */
+0−1
598598 requestLimit: (actor, workspace, request) => call("request_limit", { actor, workspace, ...request }),
599599 limitRequests: (workspace, viewer) => call("limit_requests", { workspace, viewer }),
600600 confirmSpike: (actor, workspace, keepGoing) => call("confirm_spike", { actor, workspace, keepGoing }),
601− setCaps: (actor, workspace, caps) => call("set_caps", { actor, workspace, ...caps }),
602601 usageReport: (workspace, viewer, range) =>
603602 call("usage_report", { workspace, viewer, from: range.from, until: range.until, products: range.products ?? [], projects: range.projects ?? [] }),
604603 setBudget: (actor, workspace, budget) => call("set_budget", { actor, workspace, ...budget }),
+0−3
6868 firstMonth: boolean;
6969 /** The longest any sandbox may run, in minutes; zero means no cap. */
7070 maxRunMinutes: number;
71− /** The most one sandbox run may cost; zero means no cap. */
72− runCapMicros: number;
7371 ceilingMicros: number;
7472 exposureMicros: number;
7573 /** Why g1t paused the workspace's compute; null when it is not paused. */
121119 trialVerified: field(raw, "trialVerified") === true,
122120 firstMonth: field(raw, "firstMonth") === true,
123121 maxRunMinutes: num(field(raw, "maxRunMinutes")),
124− runCapMicros: num(field(raw, "runCapMicros")),
125122 ceilingMicros: num(field(raw, "ceilingMicros")),
126123 exposureMicros: num(field(raw, "exposureMicros")),
127124 paused: typeof paused === "string" && paused.trim() ? paused.trim() : null,
+0−8
1919
2020 /** What a run actually gets. */
2121 export type Guardrails = {
22− /**
23− * No longer sent: command rules, deny patterns and the cost cap were for
24− * g1t's own agents. Kept optional for the runner, which still passes
25− * them on (and a plan's cost cap) when they are there.
26− */
27− rules?: Record<string, boolean>;
28− deny?: string[];
29− budgetUsd?: number | null;
3022 minutes: Record<string, number>;
3123 };
3224
+6−24
5252 #[serde(default)]
5353 max_concurrent_agents: Option<u32>,
5454 #[serde(default)]
55− run_cap_micros: Option<i64>,
56− #[serde(default)]
57− issue_cap_micros: Option<i64>,
58− #[serde(default)]
5955 audit_retention_days: Option<u32>,
6056 #[serde(default)]
6157 hold: Option<String>,
6965 oss_repo_micros: self.oss_repo_micros,
7066 trial_micros: self.trial_micros,
7167 max_concurrent_agents: self.max_concurrent_agents,
72− run_cap_micros: self.run_cap_micros,
73− issue_cap_micros: self.issue_cap_micros,
7468 audit_retention_days: self.audit_retention_days,
7569 hold: self.hold.clone().filter(|h| !h.trim().is_empty()),
7670 }
508502 return Ok(Outcome::fail(FailureCode::Invalid, "Say who is making the change, and why, in the note."));
509503 }
510504 let money = |m: Option<i64>| m.is_none_or(|m| (0..=1_000 * g1t_contracts::billing::MICROS_PER_DOLLAR).contains(&m));
511− if !money(a.allowances.oss_repo_micros) || !money(a.allowances.trial_micros) || !money(a.allowances.run_cap_micros) || !money(a.allowances.issue_cap_micros) {
512− return Ok(Outcome::fail(FailureCode::Invalid, "A pool share or run cap is between $0 and $1,000."));
505+ if !money(a.allowances.oss_repo_micros) || !money(a.allowances.trial_micros) {
506+ return Ok(Outcome::fail(FailureCode::Invalid, "A pool share or trial is between $0 and $1,000."));
513507 }
514508 if a.allowances.max_concurrent_agents.is_some_and(|n| n == 0 || n > 1_000) {
515509 return Ok(Outcome::fail(FailureCode::Invalid, "Agents at once is between 1 and 1,000."));
526520 self.db
527521 .prepare(
528522 "INSERT INTO billing_accounts (id, kind, name, terms_kind, discount_percent, note, created_by, created_at,
529− team_granted, oss_repo_micros, trial_micros, max_concurrent_agents, run_cap_micros, hold, issue_cap_micros,
530− audit_retention_days)
531− VALUES (?1, ?2, ?3, 'standard', 0, '', ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
523+ team_granted, oss_repo_micros, trial_micros, max_concurrent_agents, hold, audit_retention_days)
524+ VALUES (?1, ?2, ?3, 'standard', 0, '', ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
532525 ON CONFLICT (id) DO UPDATE SET team_granted = ?6, oss_repo_micros = ?7, trial_micros = ?8,
533− max_concurrent_agents = ?9, run_cap_micros = ?10, hold = ?11, issue_cap_micros = ?12,
534− audit_retention_days = ?13",
526+ max_concurrent_agents = ?9, hold = ?10, audit_retention_days = ?11",
535527 )
536528 .bind(&[
537529 account.id.as_str().into(),
543535 opt(a.allowances.oss_repo_micros),
544536 opt(a.allowances.trial_micros),
545537 a.allowances.max_concurrent_agents.map_or(JsValue::NULL, JsValue::from),
546− opt(a.allowances.run_cap_micros),
547538 optional(a.allowances.hold.as_deref().map(str::trim).filter(|h| !h.is_empty())),
548− opt(a.allowances.issue_cap_micros),
549539 a.allowances.audit_retention_days.map_or(JsValue::NULL, JsValue::from),
550540 ])?
551541 .run()
711701 }
712702 if let Some(n) = a.max_concurrent_agents {
713703 parts.push(format!("{n} agents at once"));
714− }
715− if let Some(m) = a.run_cap_micros {
716− parts.push(format!("run cap {}", crate::features::dollars(m)));
717704 }
718− if let Some(m) = a.issue_cap_micros {
719− parts.push(format!("issue cap {}", crate::features::dollars(m)));
720− }
721705 if let Some(days) = a.audit_retention_days {
722706 parts.push(format!("audit log {days} days"));
723707 }
810794 oss_repo_micros: Some(5_000_000),
811795 trial_micros: Some(2_000_000),
812796 max_concurrent_agents: Some(4),
813− run_cap_micros: Some(3_000_000),
814− issue_cap_micros: None,
815797 audit_retention_days: Some(365),
816798 hold: Some("mining".into()),
817799 };
818800 assert_eq!(
819801 describe_allowances(&given),
820− "plan given, open-source share $5.00 a repository, trial $2.00, 4 agents at once, run cap $3.00, audit log 365 days, hold: mining"
802+ "plan given, open-source share $5.00 a repository, trial $2.00, 4 agents at once, audit log 365 days, hold: mining"
821803 );
822804 }
823805
+4−74
2727 //! stop. Runs already under way finish. g1t's own workspaces are watched
2828 //! but never paused.
2929
30−use futures_util::future::{try_join, try_join4, try_join5};
30+use futures_util::future::{try_join, try_join4};
3131 use g1t_contracts::billing::{
32− BillingAccount, ComputeKind, ConfirmSpikeArgs, SetCapsArgs, Entitlements, EntitlementsArgs, LimitState, PaidBy, PlanKind, Reservation,
32+ BillingAccount, ComputeKind, ConfirmSpikeArgs, Entitlements, EntitlementsArgs, LimitState, PaidBy, PlanKind, Reservation,
3333 ReserveArgs, SettleArgs, Spike, UNLIMITED_MICROS, UsageAlert, RESERVATION_HOURS,
3434 };
3535 use g1t_contracts::time::rfc3339;
473473 };
474474 Ok::<_, worker::Error>((checked, grant))
475475 };
476− let counts = try_join5(
477− self.owner_caps(&workspace),
476+ let counts = try_join4(
478477 self.private_storage(&workspace),
479478 self.oss_paid(&workspace, &month),
480479 self.held(&account.workspaces),
481480 async { if has_plan { self.allowance_used("plan_credit", &workspace, &month).await } else { Ok(0) } },
482481 );
483482 let more = try_join(self.allowance_used("build_seconds", &workspace, &month), self.git_operations_this_month(&workspace));
484− let ((limit, (paused, spike, _)), (verified, grant), (owners, stored, oss, held, included_used), (build_seconds, git_operations)) =
483+ let ((limit, (paused, spike, _)), (verified, grant), (stored, oss, held, included_used), (build_seconds, git_operations)) =
485484 try_join4(standing, trial, counts, more).await?;
486485 let trial_left = grant.as_ref().map_or(0, |g| left(g.granted_micros, g.used_micros));
487486 let first_month = limit.first_month;
500499 first_month,
501500 max_concurrent_agents: max_agents,
502501 max_run_minutes: max_minutes,
503− run_cap_micros: account.allowances.run_cap_micros.or(owners.0).unwrap_or(self.plans.run_cap_micros),
504− issue_cap_micros: account.allowances.issue_cap_micros.or(owners.1).unwrap_or(self.plans.issue_cap_micros),
505502 ceiling_micros: ceiling,
506503 exposure_micros: limit.exposure_micros,
507504 paused,
736733 &account.id,
737734 "spike",
738735 &format!("{workspace}: {} after {} in an hour", if a.keep_going { "kept going" } else { "stopped" }, dollars(spike.hour_micros)),
739− &a.actor.username,
740− )
741− .await?;
742− Ok(Outcome::Ok(self.entitlements(EntitlementsArgs { workspace }).await?))
743− }
744−
745− /// The owners' own run and issue caps, if they set them.
746− async fn owner_caps(&self, workspace: &str) -> Result<(Option<i64>, Option<i64>)> {
747− #[derive(Deserialize)]
748− struct Row {
749− run_cap_micros: Option<i64>,
750− issue_cap_micros: Option<i64>,
751− }
752− Ok(self
753− .db
754− .prepare("SELECT run_cap_micros, issue_cap_micros FROM limits WHERE workspace = ?")
755− .bind(&[workspace.into()])?
756− .first::<Row>(None)
757− .await?
758− .map_or((None, None), |r| (r.run_cap_micros, r.issue_cap_micros)))
759− }
760−
761− /// `set_caps`: the owners' own run and issue caps.
762− pub(crate) async fn set_caps(&self, a: SetCapsArgs) -> Result<Outcome<Entitlements>> {
763− let workspace = a.workspace.to_lowercase();
764− if !a.actor.manages_billing(&workspace) {
765− return Ok(Outcome::fail(FailureCode::Forbidden, "Only an owner or a billing manager can set the workspace's caps."));
766− }
767− if let Err(why) = cap_bounds(a.run_cap_micros, a.issue_cap_micros) {
768− return Ok(Outcome::fail(FailureCode::Invalid, why));
769− }
770− let opt = |m: Option<i64>| m.map_or(JsValue::NULL, |m| (m as f64).into());
771− let now = rfc3339(now_ms());
772− self.db
773− .prepare(
774− "INSERT INTO limits (workspace, run_cap_micros, issue_cap_micros, updated_at) VALUES (?1, ?2, ?3, ?4)
775− ON CONFLICT (workspace) DO UPDATE SET run_cap_micros = ?2, issue_cap_micros = ?3, updated_at = ?4",
776− )
777− .bind(&[workspace.as_str().into(), opt(a.run_cap_micros), opt(a.issue_cap_micros), now.into()])?
778− .run()
779− .await?;
780− let account = self.account_of(&workspace).await?;
781− let shown = |m: Option<i64>| m.map_or_else(|| "the default".to_owned(), dollars);
782− self.audit(
783− &account.id,
784− "caps",
785− &format!("{workspace}: run cap {}, issue cap {}", shown(a.run_cap_micros), shown(a.issue_cap_micros)),
786736 &a.actor.username,
787737 )
788738 .await?;
859809
860810 /// Whether owners' caps are in bounds: a run $0.10 to $100 (the
861811 /// guardrails' most), an issue $1 to $1,000.
862−pub(crate) fn cap_bounds(run: Option<i64>, issue: Option<i64>) -> std::result::Result<(), String> {
863− if run.is_some_and(|m| !(100_000..=100_000_000).contains(&m)) {
864− return Err("A run's cap is between $0.10 and $100.".to_owned());
865− }
866− if issue.is_some_and(|m| !(1_000_000..=1_000_000_000).contains(&m)) {
867− return Err("An issue's cap is between $1 and $1,000.".to_owned());
868− }
869− Ok(())
870−}
871−
872812 pub(crate) fn paid_by_text(paid_by: PaidBy) -> &'static str {
873813 match paid_by {
874814 PaidBy::Credit => "credit",
1032972 // A day later: watched afresh.
1033973 assert!(!still_continued(Some(until), "2026-10-06T12:00:01Z", 6_000_000, 1_000_000));
1034974 assert!(!still_continued(None, "2026-10-06T11:00:00Z", 6_000_000, 1));
1035− }
1036−
1037− #[test]
1038− fn owners_caps_stay_in_bounds() {
1039− assert!(cap_bounds(None, None).is_ok());
1040− assert!(cap_bounds(Some(5_000_000), Some(50_000_000)).is_ok());
1041− assert!(cap_bounds(Some(50_000), None).is_err());
1042− assert!(cap_bounds(Some(101_000_000), None).is_err());
1043− assert!(cap_bounds(None, Some(500_000)).is_err());
1044− assert!(cap_bounds(None, Some(2_000_000_000)).is_err());
1045975 }
1046976
1047977 #[test]
+0−10
7272 pub free_audit_days: u32,
7373 pub audit_days: u32,
7474 pub audit_max_days: u32,
75− /// `RUN_CAP_MICROS` and `ISSUE_CAP_MICROS`: one run's spend cap, and
76− /// agents' spend on one issue in all.
77− pub run_cap_micros: i64,
78− pub issue_cap_micros: i64,
7975 /// `LIMIT_PAID_START_MICROS`: a new paid workspace's ceiling in its
8076 /// first month.
8177 pub paid_start_micros: i64,
109105 free_audit_days: 7,
110106 audit_days: 90,
111107 audit_max_days: 400,
112− run_cap_micros: g1t_contracts::guardrails::DEFAULT_RUN_CAP_MICROS,
113− issue_cap_micros: 10_000_000,
114108 paid_start_micros: 100_000_000,
115109 spike_factor: 5,
116110 spike_floor_micros: 5_000_000,
140134 free_audit_days: number("FREE_AUDIT_RETENTION_DAYS", d.free_audit_days.into()).max(1) as u32,
141135 audit_days: number("AUDIT_RETENTION_DAYS", d.audit_days.into()).max(1) as u32,
142136 audit_max_days: number("AUDIT_MAX_DAYS", d.audit_max_days.into()).max(1) as u32,
143− run_cap_micros: number("RUN_CAP_MICROS", d.run_cap_micros),
144− issue_cap_micros: number("ISSUE_CAP_MICROS", d.issue_cap_micros),
145137 paid_start_micros: number("LIMIT_PAID_START_MICROS", d.paid_start_micros),
146138 spike_factor: number("SPIKE_FACTOR", d.spike_factor).max(1),
147139 spike_floor_micros: number("SPIKE_FLOOR_MICROS", d.spike_floor_micros),
782774 assert_eq!(c.free_audit_days, 7);
783775 assert_eq!(c.audit_days, 90);
784776 assert_eq!(c.audit_max_days, 400);
785− assert_eq!(c.run_cap_micros, g1t_contracts::guardrails::DEFAULT_RUN_CAP_MICROS);
786− assert_eq!(c.issue_cap_micros, 10_000_000);
787777 assert_eq!(c.paid_start_micros, 100_000_000);
788778 assert_eq!(c.forgive_cost_micros, 50_000_000);
789779 assert_eq!(c.git_included, 50_000);
+0−1
12131213 "request_limit" => reply(&billing.request_limit(args(body)?).await?),
12141214 "limit_requests" => reply(&billing.limit_requests(args(body)?).await?),
12151215 "confirm_spike" => reply(&billing.confirm_spike(args(body)?).await?),
1216− "set_caps" => reply(&billing.set_caps(args(body)?).await?),
12171216 "admin_limit_requests" => reply(&billing.admin_limit_requests(args(body)?).await?),
12181217 "admin_decide_limit_request" => reply(&billing.admin_decide_limit_request(args(body)?).await?),
12191218 "admin_overages" => reply(&billing.admin_overages(args(body)?).await?),
+0−6
8888 "LIMIT_PAID_START_MICROS": "100000000",
8989 "LIMIT_PAID_MIN_MICROS": "25000000",
9090 "LIMIT_PAID_MAX_MICROS": "1000000000",
91− // Caps on agents (src/compute.rs): what the agents on one issue may
92− // spend in all. One run's spend cap is DEFAULT_RUN_CAP_MICROS in
93− // crates/contracts (guardrails.rs), shared with the guardrails' cost
94− // per run; RUN_CAP_MICROS here would override it. Owners and staff
95− // can change both.
96− "ISSUE_CAP_MICROS": "10000000",
9791 // A spike: an hour's spend above 5 times the usual hour over the last
9892 // week, and at least $5, pauses new compute until an owner confirms.
9993 "SPIKE_FACTOR": "5",
+2−8
2727 assert.equal(allows(hosts, "badexample.com"), false);
2828 });
2929
30−test("the harness is told the run's rules, caps and branch, and nothing about the network", () => {
31− const guard = {
32− policy: { rules: { sudo: true }, deny: ["Bash(kubectl:*)"], budgetUsd: 5, minutes: { implement: 90 } },
33− minutes: 90,
34− };
30+test("the harness is told the run's time cap and branch, and nothing about the network", () => {
31+ const guard = { policy: { minutes: { checks: 90 } }, minutes: 90 };
3532 const vars = harnessEnv(guard, { UPSTREAM_BRANCH: "trunk" });
3633 assert.deepEqual(Object.keys(vars), ["GUARDRAILS"]);
3734 assert.deepEqual(JSON.parse(vars.GUARDRAILS), {
38− rules: { sudo: true },
39− deny: ["Bash(kubectl:*)"],
40− budgetUsd: 5,
4135 minutes: 90,
4236 defaultBranch: "trunk",
4337 });
+4−13
7272 export function harnessEnv(guard: RunGuard, sandboxEnv: Record<string, string>): Record<string, string> {
7373 return {
7474 GUARDRAILS: JSON.stringify({
75− rules: guard.policy.rules,
76− deny: guard.policy.deny,
77− budgetUsd: guard.policy.budgetUsd,
7875 minutes: guard.minutes,
7976 defaultBranch: sandboxEnv.UPSTREAM_BRANCH ?? null,
8077 }),
152149 return caps.length ? Math.min(...caps) : null;
153150 }
154151
155−/** A plan's caps on one run, from its entitlements; null where it sets none. */
156−export type PlanLimits = { minutes?: number | null; budgetUsd?: number | null };
152+/** A plan's cap on one run, from its entitlements; null where it sets none. */
153+export type PlanLimits = { minutes?: number | null };
157154
158−/**
159− * A run's guardrails under its workspace's plan: the time and cost caps are
160− * each the lower of the two.
161− */
155+/** A run's guardrails under its workspace's plan: the time cap is the lower of the two. */
162156 export function withPlanLimits(guard: RunGuard, limits: PlanLimits | null | undefined): RunGuard {
163157 if (!limits) return guard;
164− return {
165− policy: { ...guard.policy, budgetUsd: lower(guard.policy.budgetUsd, limits.budgetUsd) },
166− minutes: lower(guard.minutes, limits.minutes) ?? guard.minutes,
167− };
158+ return { policy: guard.policy, minutes: lower(guard.minutes, limits.minutes) ?? guard.minutes };
168159 }
+4−9
2828 trialVerified: true,
2929 firstMonth: false,
3030 maxRunMinutes: 0,
31− runCapMicros: 2_000_000,
3231 ceilingMicros: 100_000_000,
3332 exposureMicros: 0,
3433 paused: null,
233232
234233 // ---- Caps -------------------------------------------------------------------------------
235234
236−test("caps are the lower of the guardrails' and the plan's", () => {
237− const guard = { policy: { budgetUsd: 5 } as never, minutes: 90 };
238− const limited = withPlanLimits(guard as never, { minutes: 60, budgetUsd: 2 });
239− assert.equal(limited.minutes, 60);
240− assert.equal((limited.policy as { budgetUsd: number }).budgetUsd, 2);
241− const unlimited = withPlanLimits(guard as never, { minutes: null, budgetUsd: null });
242− assert.equal(unlimited.minutes, 90);
243− assert.equal((unlimited.policy as { budgetUsd: number }).budgetUsd, 5);
235+test("the time cap is the lower of the guardrails' and the plan's", () => {
236+ const guard = { policy: { minutes: {} }, minutes: 90 };
237+ assert.equal(withPlanLimits(guard, { minutes: 60 }).minutes, 60);
238+ assert.equal(withPlanLimits(guard, { minutes: null }).minutes, 90);
244239 });
245240
246241 // ---- Sandboxes' network ------------------------------------------------------------------
+2−6
468468 ...track,
469469 model: null,
470470 sandbox: this.ctx.id.toString(),
471− budgetUsd: guard?.policy.budgetUsd ?? null,
472471 timeCapMinutes: guard?.minutes ?? null,
473472 })
474473 .catch((error: unknown) => ({ ok: false as const, error: { message: String(error) } }));
744743 mergecheck: 10,
745744 };
746745
747−/** A plan's caps on one run, for the sandbox to apply under its guardrails'. */
746+/** A plan's cap on one run, for the sandbox to apply under its guardrails'. */
748747 function limitsOf(ent: ComputeEntitlements | null): PlanLimits {
749− return {
750− minutes: ent && ent.maxRunMinutes > 0 ? ent.maxRunMinutes : null,
751− budgetUsd: ent && ent.runCapMicros > 0 ? ent.runCapMicros / 1_000_000 : null,
752− };
748+ return { minutes: ent && ent.maxRunMinutes > 0 ? ent.maxRunMinutes : null };
753749 }
754750
755751 /** The shorter of a kind's time cap and the plan's, for an estimate. */