| 43 | 43 | | * paying with real money: when billing is off, or its cards are pretend. |
| 44 | 44 | | * Once billing is live, anyone may, and the workspace is charged. |
| 45 | 45 | | */ |
| 46 | | − | HOSTED_AGENT_USERS: string; |
| 47 | 46 | | /** |
| 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 | + | /** |
| 48 | 53 | | * Which model each kind of work runs on, as JSON: |
| 49 | 54 | | * `{ implement, review, update }`, each `{ modelName, model }`. |
| 50 | 55 | | * `modelName` is what people see; `model` is sent to the provider. |
| ⋯ |
| 375 | 380 | | } |
| 376 | 381 | | |
| 377 | 382 | | /** |
| 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. |
| 381 | 386 | | */ |
| 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()); |
| 388 | 390 | | } |
| 389 | 391 | | |
| 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 { |
| 391 | 398 | | 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)); |
| 393 | 404 | | } |
| 394 | 405 | | |
| 395 | 406 | | /** |
| ⋯ |
| 482 | 493 | | if (next.action === "none") return; |
| 483 | 494 | | const { job } = next; |
| 484 | 495 | | 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."); |
| 487 | 498 | | } |
| 488 | 499 | | if (next.action === "review") { |
| 489 | 500 | | const started = await this.startReview(pullId); |
| ⋯ |
| 558 | 569 | | private async buildQueue(repoId: string): Promise<void> { |
| 559 | 570 | | const work = workClient(this.env.WORK); |
| 560 | 571 | | 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 | + | } |
| 561 | 586 | | // A state whose sandbox could not start fails at once, rather than |
| 562 | 587 | | // holding the queue until it times out. |
| 563 | 588 | | await Promise.all( |
| ⋯ |
| 668 | 693 | | if (!started.ok) return false; |
| 669 | 694 | | const job: CheckJob = started.value; |
| 670 | 695 | | // 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)) { |
| 677 | 699 | | await work.reportChecks(job.runId, job.token, { skip: true }); |
| 678 | 700 | | return false; |
| 679 | 701 | | } |
| ⋯ |
| 709 | 731 | | * they do not belong to or that has no credit. |
| 710 | 732 | | */ |
| 711 | 733 | | 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.`); |
| 714 | 742 | | } |
| 715 | 743 | | const billing = billingClient(this.env.BILLING); |
| 716 | 744 | | if (!(await billing.status()).enabled) return null; |
| ⋯ |
| 949 | 977 | | return applied; |
| 950 | 978 | | } |
| 951 | 979 | | |
| 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); |
| 954 | 982 | | } |
| 955 | 983 | | |
| 956 | 984 | | async run( |