Commit

g1t's agents only for listed workspaces, whatever the state of billing

Agents, planning, reviews, revisions, catch-ups, acceptance-check sandboxes and the merge queue now run only in repositories of the workspaces in HOSTED_AGENT_WORKSPACES (syntaqx for now), even once payments are live. Everyone can still sign up, host repositories, push, open issues and pull requests, and bring their own agent.

syntaqxcommitted Parentf278251Browse files
8 files+69−330/8 viewed
+4−4
131131 A pull request whose checks have not passed cannot be merged, unless a
132132 member of the workspace chooses to merge anyway.
133133
134−Running checks is in preview. They run when the issue's author or the pull
135−request's author is an account that g1t's sandboxes are enabled for.
134+Running checks is in preview: they run in repositories of the workspaces
135+g1t's sandboxes are enabled for.
136136
137137 ## Review
138138
267267
268268 - **Milestones.**
269269 - **g1t agents for everyone.** g1t can put its own agents on an issue, each
270− in a sandbox. This is in preview and limited to selected accounts; anyone
271− can bring their own agent today.
270+ in a sandbox. This is in preview and enabled for selected workspaces;
271+ anyone can sign up, host repositories and bring their own agent today.
+4−0
244244
245245 ## Limits in the preview
246246
247+- g1t's own agents, and the sandboxes that run acceptance checks and the
248+ merge queue, are enabled for selected workspaces while they are in
249+ preview. Everywhere else, everything else works: repositories, issues,
250+ pull requests, review, and your own agent through MCP.
247251 - An agent is given one fork and the issue. Its credential, though, is your
248252 account's for the length of the run; credentials limited to the pull
249253 request are planned.
+1−1
4848 const [found, labels, agentsEnabled, members] = await Promise.all([
4949 work.getIssue(path, number, viewer),
5050 work.listLabels(path, viewer),
51− env.RUNNER.enabled(viewer),
51+ env.RUNNER.enabled(viewer, path),
5252 // A member picks assignees from the workspace's people.
5353 roleIn(viewer, params.owner) ? identity.listMembers(params.owner, viewer) : null,
5454 ]);
+1−1
3030 const [issues, labels, agentsEnabled] = await Promise.all([
3131 work.listIssues(path, viewer, { state, label: label || undefined }),
3232 work.listLabels(path, viewer),
33− env.RUNNER.enabled(viewer),
33+ env.RUNNER.enabled(viewer, path),
3434 ]);
3535 return {
3636 issues: unwrap(issues),
+1−1
7979 work.getPull(path, number, viewer),
8080 repos.get(path, viewer),
8181 work.getSettings(path, viewer),
82− env.RUNNER.enabled(viewer),
82+ env.RUNNER.enabled(viewer, path),
8383 // A member picks reviewers and assignees from the workspace's people.
8484 member ? identity.listMembers(params.owner, viewer) : null,
8585 ]);
+5−1
1616 */
1717 export interface RunnerApi {
1818 /** Whether this viewer may put g1t agents to work. */
19− enabled(viewer: Viewer): Promise<boolean>;
2019 /**
20+ * Whether `viewer` may put g1t's agents to work: in `repo`'s workspace,
21+ * or with none named, in any of theirs.
22+ */
23+ enabled(viewer: Viewer, repo?: RepoPath): Promise<boolean>;
24+ /**
2125 * Assigns the issue to a g1t agent: opens a draft pull request for it,
2226 * made by an agent in a sandbox of its own. Returns as soon as the
2327 * sandbox is starting; progress shows up in the pull request's session.
+52−24
4343 * paying with real money: when billing is off, or its cards are pretend.
4444 * Once billing is live, anyone may, and the workspace is charged.
4545 */
46− HOSTED_AGENT_USERS: string;
4746 /**
47+ * The workspaces whose repositories may use g1t's agents and sandboxes,
48+ * comma-separated, or `*` for all. Everything else on g1t works for
49+ * everyone; this is what costs money.
50+ */
51+ HOSTED_AGENT_WORKSPACES: string;
52+ /**
4853 * Which model each kind of work runs on, as JSON:
4954 * `{ implement, review, update }`, each `{ modelName, model }`.
5055 * `modelName` is what people see; `model` is sent to the provider.
375380 }
376381
377382 /**
378− * Whether sandboxes may be started on this person's say-so. Where
379− * workspaces pay with real money, anyone's. Until then g1t is paying, or
380− * the cards are pretend, so only the people listed.
383+ * Whether a workspace's repositories may use g1t's agents and sandboxes.
384+ * Only those listed, whatever the state of billing: in the preview g1t
385+ * pays for the models, so nobody else can spend on them.
381386 */
382− private async enabledFor(username: string): Promise<boolean> {
383− const billing = await billingClient(this.env.BILLING).status();
384− if (billing.enabled && billing.live) return true;
385− return this.env.HOSTED_AGENT_USERS.split(",")
386− .map((name) => name.trim())
387− .includes(username);
387+ private workspaceAllowed(namespace: string): boolean {
388+ const listed = this.env.HOSTED_AGENT_WORKSPACES.split(",").map((name) => name.trim().toLowerCase());
389+ return listed.includes("*") || listed.includes(namespace.toLowerCase());
388390 }
389391
390− private async allowed(viewer: Viewer): Promise<boolean> {
392+ /**
393+ * Whether `viewer` may put agents to work: in `repo`'s workspace, which
394+ * must be allowed and theirs, or with no repo named, in any workspace of
395+ * theirs that is allowed.
396+ */
397+ private allowed(viewer: Viewer, repo?: RepoPath): boolean {
391398 if (!viewer || !canReachModel(this.env)) return false;
392− return this.enabledFor(viewer.username);
399+ const theirs = (viewer.workspaces ?? []).map((membership) => membership.slug.toLowerCase());
400+ if (repo) {
401+ return this.workspaceAllowed(repo.namespace) && theirs.includes(repo.namespace.toLowerCase());
402+ }
403+ return theirs.some((slug) => this.workspaceAllowed(slug));
393404 }
394405
395406 /**
482493 if (next.action === "none") return;
483494 const { job } = next;
484495 try {
485− if (!canReachModel(this.env) || !(await this.enabledFor(job.author.username))) {
486− throw new Error("g1t agents are not enabled for this pull request's author.");
496+ if (!canReachModel(this.env) || !this.workspaceAllowed(job.repo.namespace)) {
497+ throw new Error("g1t agents are not enabled for this workspace yet.");
487498 }
488499 if (next.action === "review") {
489500 const started = await this.startReview(pullId);
558569 private async buildQueue(repoId: string): Promise<void> {
559570 const work = workClient(this.env.WORK);
560571 const jobs = await work.queueBuild(repoId);
572+ // Merge queue sandboxes, like any other, only where they are enabled.
573+ const blocked = jobs.filter((job) => !this.workspaceAllowed(job.repo.namespace));
574+ if (blocked.length > 0) {
575+ await Promise.all(
576+ blocked.map((job) =>
577+ work.failQueue(
578+ job.entryId,
579+ job.token,
580+ "The merge queue runs in g1t's sandboxes, which are not enabled for this workspace yet. Turn the queue off to merge directly.",
581+ ),
582+ ),
583+ );
584+ return;
585+ }
561586 // A state whose sandbox could not start fails at once, rather than
562587 // holding the queue until it times out.
563588 await Promise.all(
668693 if (!started.ok) return false;
669694 const job: CheckJob = started.value;
670695 // Checks are commands one person wrote, run against code another
671− // pushed, on g1t's machines. In the preview they run only when one of
672− // the two is someone sandboxes are enabled for.
673− if (
674− !(await this.enabledFor(job.requestedBy)) &&
675− !(await this.enabledFor(job.author.username))
676− ) {
696+ // pushed, on g1t's machines: in the preview, only for the workspaces
697+ // sandboxes are enabled for.
698+ if (!this.workspaceAllowed(job.repo.namespace)) {
677699 await work.reportChecks(job.runId, job.token, { skip: true });
678700 return false;
679701 }
709731 * they do not belong to or that has no credit.
710732 */
711733 private async refusal(actor: User, repo: RepoPath): Promise<Result<never> | null> {
712− if (!(await this.allowed(actor))) {
713− return fail("forbidden", "g1t agents are not enabled for your account.");
734+ if (!this.workspaceAllowed(repo.namespace)) {
735+ return fail(
736+ "forbidden",
737+ `g1t agents are in preview and not enabled for the ${repo.namespace} workspace yet. Everything else works, and you can bring your own agent.`,
738+ );
739+ }
740+ if (!this.allowed(actor, repo)) {
741+ return fail("forbidden", `Only members of ${repo.namespace} can put g1t agents to work there.`);
714742 }
715743 const billing = billingClient(this.env.BILLING);
716744 if (!(await billing.status()).enabled) return null;
949977 return applied;
950978 }
951979
952− async enabled(viewer: Viewer): Promise<boolean> {
953− return await this.allowed(viewer);
980+ async enabled(viewer: Viewer, repo?: RepoPath): Promise<boolean> {
981+ return this.allowed(viewer, repo);
954982 }
955983
956984 async run(
+1−1
3838 "consumers": [{ "queue": "g1t-events-runner", "max_batch_size": 20, "max_batch_timeout": 1 }]
3939 },
4040 "vars": {
41− "HOSTED_AGENT_USERS": "syntaqx",
41+ "HOSTED_AGENT_WORKSPACES": "syntaqx",
4242 // Which model each kind of work runs on. Nobody assigning an agent
4343 // chooses; this is g1t's policy. "modelName" is shown to people in the
4444 // session; "model" is sent to the provider.