Commit

Free while g1t is being built out; agents can check out their own forks

Billing gains FREE_WHILE_BUILDING (on): runs are still recorded with what they cost, so Usage works, but nothing is charged and no credit is needed. The app, the docs and llms.txt say so, as for now and not forever. Workflows run in every workspace that uses g1t's agents, including with only its own model provider. A pull request's workflow runs now get their token and secrets when its author is a member (asked of the workspace, since stored authors carry no memberships) or the repository is private, so actions/checkout can fetch an agent's fork. An agent sent back for failed workflows is told which repository to read the runs in.

syntaqxcommitted Parent397bbd0Browse files
17 files+114−330/17 viewed
+1−1
537537 "A workspace's integrations: its own model provider, the alert sources that open issues (Sentry, Datadog, webhooks), and the trackers whose tickets agents can read (Jira, Linear). Secrets are never returned. Members only."
538538 }
539539 Op::ConnectIntegration => {
540− "Connect a workspace to an outside system. provider is a model provider (anthropic, openai, gemini, xai, mistral, deepseek, azure_openai, openrouter, groq, together, fireworks, cerebras, anthropic_endpoint or openai_endpoint: your own key, billed by that provider, with g1t charging a flat orchestration fee per run; a workspace can connect several and route each kind of work with set_model_routes), or sentry, datadog, webhook, jira or linear. config holds the settings each needs; secret is the API key or token. For datadog and webhook, g1t makes the signing secret and returns it once. Owners only."
540+ "Connect a workspace to an outside system. provider is a model provider (anthropic, openai, gemini, xai, mistral, deepseek, azure_openai, openrouter, groq, together, fireworks, cerebras, anthropic_endpoint or openai_endpoint: your own key, billed by that provider, and free on g1t while it is being built out; a workspace can connect several and route each kind of work with set_model_routes), or sentry, datadog, webhook, jira or linear. config holds the settings each needs; secret is the API key or token. For datadog and webhook, g1t makes the signing secret and returns it once. Owners only."
541541 }
542542 Op::DisconnectIntegration => {
543543 "Remove an integration and its secrets. Agents already running on a model provider being removed stop reaching it. Owners only."
+4−2
114114
115115 ## Who may run workflows
116116
117−While g1t is in preview, workflows run in the workspaces g1t has opened
118−them to. Elsewhere a run is recorded with its jobs failed and the reason.
117+Workflows run in every workspace that uses g1t's agents: one with its own
118+[model provider](/guides/models/) connected, or one g1t has opened its
119+hosted models to. They are free while g1t is being built out. Elsewhere a
120+run is recorded with its jobs failed and the reason.
119121
120122 ## From the API
121123
+5−3
205205
206206 ## What it costs
207207
208−A workspace pays for the g1t agents that work on its repositories, from
209−credit an owner buys in advance: each run is charged what the model cost,
210−plus 20%. Acceptance checks are free. With no credit, agents do not start.
208+While g1t is being built out, its agents cost nothing: runs are recorded
209+with what they cost, and nothing is charged. Once pricing starts, a
210+workspace will pay for the g1t agents that work on its repositories from
211+credit an owner buys in advance: each run charged what the model cost, plus
212+20%, and acceptance checks free.
211213 The workspace's **Usage** page shows what its agents have cost, by day,
212214 kind of work, repository, model and pull request. See
213215 [usage and billing](/guides/usage-and-billing/).
+4−2
8383
8484 ## What it costs
8585
86−On your own providers, they bill you for the models, and g1t charges your
87−credit a flat **$0.10 per run** for the sandbox and orchestration. A
86+While g1t is being built out, g1t charges nothing for runs on your own
87+providers: they bill you for the models, and that is all. Once pricing
88+starts, g1t will charge your credit a flat **$0.10 per run** for the
89+sandbox and orchestration. A
8890 change, a review, a revision, a catch-up and a plan are each a run. The
8991 statement marks these runs "on your own model provider" and names the
9092 model and provider; the Usage page shows what they cost at the provider,
+7−0
33 description: What g1t agents cost, how a workspace pays for them, and what is free.
44 ---
55
6+> **Free while g1t is being built out.** For now, using g1t costs nothing:
7+> agents, reviews, checks and workflows. Bring your own model provider and
8+> its usage is billed by that provider, not by g1t. Runs are still recorded
9+> with what they cost, so the Usage page shows what you are using. This is
10+> for now, not forever: the pricing below is how g1t will charge once it
11+> starts, and we will say so well before anything is charged.
12+
613 Hosting repositories, issues, pull requests, review and your own agent cost
714 nothing on g1t. What costs money is g1t's own agents: each run uses a
815 model, and a workspace pays for the runs on its repositories from credit it
+10−4
6363 } | null;
6464 /** The workspace's agent credit, if billing is on and they may see it. */
6565 creditMicros: number | null;
66+ /** Whether g1t charges nothing for now, while it is being built out. */
67+ free?: boolean;
6668 /** What its agents have cost since the start of the month. */
6769 monthSpentMicros: number | null;
6870 };
190192 </span>
191193 <span className="mt-2 flex items-baseline justify-between">
192194 <span className="font-mono text-sm tabular-nums">${(spent / MICROS_PER_DOLLAR).toFixed(2)}</span>
193− {left != null && (
194− <span className={`text-xs ${left <= 0 ? "text-warn" : "text-faint"}`}>
195− ${(left / MICROS_PER_DOLLAR).toFixed(2)} left
196− </span>
195+ {shell.free ? (
196+ <span className="text-xs text-accent">Free for now</span>
197+ ) : (
198+ left != null && (
199+ <span className={`text-xs ${left <= 0 ? "text-warn" : "text-faint"}`}>
200+ ${(left / MICROS_PER_DOLLAR).toFixed(2)} left
201+ </span>
202+ )
197203 )}
198204 </span>
199205 <span className="mt-2 block h-1 overflow-hidden rounded-full bg-raised">
+3−1
109109 pulls: counts.value.pulls,
110110 }
111111 : null,
112+ // While g1t is being built out nothing is charged, so no credit is shown.
112113 creditMicros:
113− account?.ok && account.value.status.enabled ? account.value.balanceMicros : null,
114+ account?.ok && account.value.status.enabled && !account.value.status.free ? account.value.balanceMicros : null,
115+ free: account?.ok ? Boolean(account.value.status.free) : false,
114116 monthSpentMicros: usage?.ok ? usage.value.spentMicros : null,
115117 };
116118 }
+2−1
281281 action: "Add",
282282 },
283283 {
284− done: shell?.creditMicros == null || shell.creditMicros > 0,
284+ // Nothing to pay while g1t is being built out.
285+ done: Boolean(shell?.free) || shell?.creditMicros == null || shell.creditMicros > 0,
285286 title: "Add agent credit",
286287 about: "g1t's agents are paid for from the workspace's credit, at what the model costs plus 20%.",
287288 to: workspace ? `/${workspace}/-/billing` : null,
+11−0
7878 return (
7979 <div className="grid gap-10 lg:grid-cols-[1fr_20rem]">
8080 <div className="min-w-0">
81+ {status.free && (
82+ <div className="mb-8 rounded-xl border border-accent/30 bg-accent/5 p-5">
83+ <h2 className="font-medium">Free while g1t is being built out</h2>
84+ <p className="mt-1.5 max-w-2xl text-sm text-muted">
85+ While we build g1t out, using it costs nothing: agents, reviews, checks and workflows. Bring your own
86+ model provider under Integrations and its usage is billed by that provider, not by g1t. Runs are still
87+ recorded with what they cost, so Usage shows what you are using. This is for now, not forever: pricing
88+ will come later, and we will say so well before anything is charged.
89+ </p>
90+ </div>
91+ )}
8192 <h2 className="font-medium">Agent credit</h2>
8293 <p className="mt-1 max-w-2xl text-sm text-muted">
8394 g1t agents that work on this workspace's repositories are paid for from
+2−1
215215 as it uses, with each
216216 kind of work routed to one of them or to g1t's hosted models
217217 (`PUT /workspaces/{workspace}/model-routes`). Those providers bill the
218− workspace and g1t charges $0.10 a run. Sandboxes never hold a key.
218+ workspace; g1t charges nothing while it is being built out (later, $0.10
219+ a run). Sandboxes never hold a key.
219220 - **Alerts** (`sentry`, `datadog`, `webhook`): each problem opens one issue
220221 in a chosen repository, optionally with an agent put on it at once.
221222 Senders sign requests to `https://api.g1t.sh/hooks/{integration}`.
+5−0
2727 /// False while the payment provider is in its test mode, where cards
2828 /// are not real.
2929 pub live: bool,
30+ /// True while g1t is being built out: runs are recorded, with what
31+ /// they cost, but nothing is charged and no credit is needed. Not a
32+ /// promise that it stays free.
33+ #[serde(default)]
34+ pub free: bool,
3035 }
3136
3237 /// A workspace's standing.
+5−0
1414 enabled: boolean;
1515 /** False while the card processor is in its test mode, where cards are not real. */
1616 live: boolean;
17+ /**
18+ * True while g1t is being built out: runs are recorded with what they
19+ * cost, but nothing is charged and no credit is needed. Not forever.
20+ */
21+ free?: boolean;
1722 };
1823
1924 /** A workspace's standing. */
+17−9
66 use g1t_actions::workflow::{self, Trigger, Workflow};
77 use g1t_contracts::actions::{DispatchArgs, WorkflowRun};
88 use g1t_contracts::events::Event;
9−use g1t_contracts::identity::{AGENT_ID, AGENT_NAME, UsernameArgs, UsernamesArgs};
9+use g1t_contracts::identity::{AGENT_ID, AGENT_NAME, UsernamesArgs};
1010 use g1t_contracts::repos::{Commit, CompareArgs, Comparison, LogArgs, Repo, RepoPath};
1111 use g1t_contracts::work::{IssueDetail, PullDetail, ViewArgs};
12−use g1t_contracts::{FailureCode, Outcome, User, Viewer, new_id};
12+use g1t_contracts::{FailureCode, Outcome, User, new_id};
1313 use g1t_kit::now_ms;
1414 use serde_json::{Map, Value, json};
1515 use worker::Result;
5151 Ok(names.get(id).cloned())
5252 }
5353
54− /// Whether someone belongs to the workspace, so their pull requests'
55− /// runs get the secrets.
56− async fn insider(&self, author: &User, namespace: &str) -> Result<bool> {
57− if author.id == AGENT_ID || author.is_member(&namespace.to_lowercase()) {
54+ /// Whether a pull request's author belongs to the workspace, so its
55+ /// runs get the secrets and a token. On a private repository only
56+ /// members can open one at all.
57+ async fn insider(&self, author: &User, repo: &Repo, ws: &User) -> Result<bool> {
58+ let slug = repo.namespace.to_lowercase();
59+ if repo.is_private || author.id == AGENT_ID || author.is_member(&slug) {
5860 return Ok(true);
5961 }
60− let found: Viewer = g1t_kit::call(&self.identity, "user_by_username", &UsernameArgs { username: author.username.clone() }).await?;
61− Ok(found.is_some_and(|user| user.is_member(&namespace.to_lowercase())))
62+ // Stored authors carry no memberships: ask the workspace.
63+ let members: Outcome<Vec<g1t_contracts::identity::Member>> = g1t_kit::call(
64+ &self.identity,
65+ "list_members",
66+ &g1t_contracts::identity::ListMembersArgs { slug, viewer: Some(ws.clone()) },
67+ )
68+ .await?;
69+ Ok(members.into_result().unwrap_or_default().iter().any(|m| m.username.eq_ignore_ascii_case(&author.username)))
6270 }
6371
6472 async fn commits(&self, repo: &Repo, actor: &User, after: &str, before: Option<&str>) -> Result<Vec<Commit>> {
195203 "user": review.map(|r| payload::user(&r.author.username)),
196204 });
197205 }
198− let trusted = self.insider(&pull.author, &repo.namespace).await?;
206+ let trusted = self.insider(&pull.author, repo, ws).await?;
199207 let head_ref = payload::head_ref(pull);
200208 if event_name == "pull_request_target" {
201209 // In the base's context: its workflows, its head.
+15−1
124124 margin_percent: u32,
125125 /// Charged for a run on the workspace's own model provider.
126126 orchestration_fee_micros: i64,
127+ /// While g1t is being built out, nothing is charged (`FREE_WHILE_BUILDING`).
128+ free: bool,
127129 }
128130
129131 impl Billing {
131133 Status {
132134 enabled: self.stripe.is_some(),
133135 live: self.stripe.as_ref().is_some_and(Stripe::live),
136+ free: self.free,
134137 }
135138 }
136139
443446
444447 /// A refusal if the workspace has no credit to start an agent with.
445448 async fn out_of_credit<T>(&self, workspace: &str) -> Result<Option<Outcome<T>>> {
449+ // While g1t is being built out, no one needs credit.
450+ if self.free {
451+ return Ok(None);
452+ }
446453 let balance = self
447454 .row(workspace)
448455 .await?
531538 }
532539 // On the workspace's own provider, the model was paid for there:
533540 // g1t charges its fee, and keeps the provider's cost to show.
534− let charge = if run.own_provider() {
541+ let charge = if self.free {
542+ // Recorded, with what it cost, but not charged.
543+ 0
544+ } else if run.own_provider() {
535545 self.orchestration_fee_micros
536546 } else {
537547 charge_micros(a.cost_usd, self.margin_percent)
545555 if run.own_provider() {
546556 description.push_str(", on your own model provider");
547557 }
558+ if self.free {
559+ description.push_str(" (free while g1t is being built out)");
560+ }
548561 self.enter(
549562 &run.workspace,
550563 EntryKind::Usage,
592605 .ok()
593606 .and_then(|fee| fee.to_string().parse().ok())
594607 .unwrap_or(100_000),
608+ free: env.var("FREE_WHILE_BUILDING").is_ok_and(|v| v.to_string() == "true"),
595609 };
596610 match method.as_str() {
597611 "status" => reply(&billing.status()),
+5−1
2525 // What a run on a workspace's own model provider is charged, in
2626 // millionths of a dollar: the sandbox and the orchestration around
2727 // it, with the model paid for at the provider. $0.10.
28− "ORCHESTRATION_FEE_MICROS": "100000"
28+ "ORCHESTRATION_FEE_MICROS": "100000",
29+ // While g1t is being built out, workspaces pay nothing: runs are
30+ // recorded with what they cost, and nothing is charged. Set to
31+ // "false" when pricing starts.
32+ "FREE_WHILE_BUILDING": "true"
2933 },
3034 // Secret: STRIPE_SECRET_KEY. Without it nothing is charged and the
3135 // runner decides who may start agents some other way.
+4−3
555555 repo: RepoPath;
556556 timeoutMinutes: number;
557557 }): Promise<Result<true>> {
558− const status = await billingClient(this.env.BILLING).status();
559− if (!(this.previewListed(args.repo.namespace) || (status.enabled && status.live))) {
558+ // The same workspaces that may use g1t's sandboxes for agents.
559+ if (!(await this.workspaceAllowed(args.repo.namespace))) {
560560 return {
561561 ok: false,
562562 error: {
563563 code: "forbidden",
564− message: "Workflows run on g1t's hosted runners, which are not open to this workspace yet.",
564+ message:
565+ "Workflows run on g1t's runners for workspaces that use g1t's agents: connect your own model provider under Integrations, free while g1t is being built out.",
565566 },
566567 };
567568 }
+14−4
627627 .map(|s| {
628628 let run = s.target_url.as_deref().and_then(|url| url.rsplit('/').next()).unwrap_or_default();
629629 format!(
630− "- {} ({}): run `{run}`",
630+ "- {} ({}): run `{run}`, {}",
631631 s.context,
632− s.description.as_deref().unwrap_or("failed")
632+ s.description.as_deref().unwrap_or("failed"),
633+ s.target_url.as_deref().unwrap_or_default()
633634 )
634635 })
635636 .collect();
637+ // Named outright: the agent cannot guess it from its fork.
638+ let repo = g1t_kit::call::<_, Option<RepoPath>>(
639+ &self.repos,
640+ "path_by_id",
641+ &g1t_contracts::repos::PathByIdArgs { id: pull.repo_id.clone() },
642+ )
643+ .await?
644+ .map(|path| format!("{}/{}", path.namespace, path.name))
645+ .unwrap_or_default();
636646 Ok(format!(
637− "These GitHub Actions workflows failed on your latest commit:\n\n{}\n\n\
638− Read why with the `get_workflow_run` tool (this repository, and the run's id), \
647+ "These GitHub Actions workflows failed on your latest commit to {repo}:\n\n{}\n\n\
648+ Read why with the `get_workflow_run` tool (repo `{repo}` and the run's id), \
639649 then `get_job_logs` for the job that failed. Fix the cause in the code, not the workflow, \
640650 unless the workflow itself is wrong.",
641651 failed.join("\n")