Skip to content

Commit

Merge branch 'worktree-agent-a0f6ecec8d42fe107'

syntaqxcommitted Parents8ab7d42572eaeeBrowse files
56 files+337−14698 files not counted0/56 viewed
+7−11
1919 # makes no deployment.
2020 #
2121 # Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row, with
22−# Containers write), the variable CLOUDFLARE_ACCOUNT_ID, and
23−# api.cloudflare.com among the project's workflow-only domains for
24−# deploy.yml in production (Settings, Guardrails), and
25−# registry.cloudflare.com there too, to find the runner's image and make
26−# it. A job that must make that image (the `runner-image` group) builds the
27−# runner's binary on a larger machine and makes the image in the registry
28−# with no Docker: one layer with the binary on the base's layers, pulling
29−# none of the base (scripts/deploy/registry.mjs). Optionally, the
30−# secret STATUS_DEPLOY_TOKEN (the status Worker's secret of the same name)
31−# and status.g1t.sh among the same workflow-only domains, so status.g1t.sh
32−# hears each deploy start and finish.
22+# Containers write) and the variable CLOUDFLARE_ACCOUNT_ID. A job that
23+# must make the runner's image (the `runner-image` group) builds the
24+# runner's binary on a larger machine and makes the image in
25+# registry.cloudflare.com with no Docker: one layer with the binary on the
26+# base's layers, pulling none of the base (scripts/deploy/registry.mjs).
27+# Optionally, the secret STATUS_DEPLOY_TOKEN (the status Worker's secret of
28+# the same name), so status.g1t.sh hears each deploy start and finish.
3329 name: Deploy
3430
3531 on:
+2−4
1919 #
2020 # DESKTOP_WINDOWS_RUNNER=true a self-hosted runner on Windows, labels `self-hosted` and `windows`,
2121 # with Rust, Node and the Visual Studio build tools (the NSIS installer is fetched)
22−# DESKTOP_WINDOWS_FROM_LINUX=true no Windows machine: cross-build on g1t's Linux machine with cargo-xwin and NSIS.
23−# Needs aka.ms and download.visualstudio.microsoft.com among the project's
24−# workflow-only domains for desktop-release.yml in production (Settings, Guardrails),
25−# where cargo-xwin fetches the MSVC CRT from.
22+# DESKTOP_WINDOWS_FROM_LINUX=true no Windows machine: cross-build on g1t's Linux machine with cargo-xwin and NSIS
23+# (cargo-xwin fetches the MSVC CRT from aka.ms and download.visualstudio.microsoft.com).
2624 # DESKTOP_MACOS_RUNNER=true a self-hosted runner on a Mac, labels `self-hosted` and `macos`.
2725 name: Desktop release
2826
+1−15
1212 # the app's code from `npx expo start` on your computer.
1313 #
1414 # Needs the repository secret EXPO_TOKEN (an Expo access token with access
15−# to @flagon-io/g1t), and Expo's hosts allowed for these workflows alone in
16−# the project's Settings, Guardrails, Workflow-only domains:
17−#
18−# api.expo.dev | mobile-build.yml, mobile-release.yml
19−# expo.dev | mobile-build.yml, mobile-release.yml
20−# storage.googleapis.com | mobile-build.yml, mobile-release.yml
21−# *.eascdn.net | mobile-build.yml, mobile-release.yml
22−#
23−# (the API, its artifact links, where EAS uploads the project's archive,
24−# and where an artifact link sends the APK's download).
15+# to @flagon-io/g1t).
2516 name: Mobile build
2617
2718 on:
7263 run: |
7364 # A token pasted with a newline breaks the request's header.
7465 export EXPO_TOKEN="$(printf %s "$EXPO_TOKEN" | tr -d '[:space:]')"
75− # Without a token Expo answers 200; a bare 403 is the sandbox's egress refusing it.
76− code="$(curl -s -o /dev/null -w '%{http_code}' -X POST https://api.expo.dev/graphql \
77− -H 'Content-Type: application/json' -d '{"query":"{ meUserActor { id } }"}')"
78− echo "api.expo.dev answers this runner with $code"
79− if [ "$code" = 403 ]; then echo "::error::api.expo.dev is not allowed: add the workflow-only domains above in the project's Guardrails"; exit 1; fi
8066 npx --yes eas-cli@24.12.1 whoami
8167 npx --yes eas-cli@24.12.1 build --platform android --profile "$PROFILE" \
8268 --non-interactive --wait --json > build.json
+1−2
1515 #
1616 # Needs the repository secret EXPO_TOKEN (an Expo access token with access
1717 # to the g1t project), CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as the
18−# deploy has them, and Expo's hosts as workflow-only domains (mobile-build.yml
19−# lists them).
18+# deploy has them.
2019 name: Mobile release
2120
2221 on:
+2−2
145145 `apps/desktop/src-tauri/Cargo.toml`, merge, and push a
146146 `desktop-v<version>` tag. `.g1t/workflows/desktop-release.yml` builds
147147 the installers (Linux on g1t's machines; Windows and macOS on machines
148− registered for them, or Windows cross-built on Linux once its download
149− hosts are allowed, each switched on by a repository variable the
148+ registered for them, or Windows cross-built on Linux, each switched on
149+ by a repository variable the
150150 workflow's header names; a platform with no machine is left out of that
151151 release), signs the updater's files and publishes them, and installed
152152 apps update themselves. `scripts/desktop-release.mjs` does the
+1−2
194194
195195 - **Why.** Jobs run in Cloudflare Containers, which let a container run
196196 Docker but not route a container network of its own out, or change its
197− packet filter. Sharing the job's network is also what keeps the job's
198− guardrails on every container.
197+ packet filter.
199198 - **Instead.** Give containers that would clash different ports.
200199 - **Status.** Not scheduled.
201200
+35−69
574574
575575 ### What a job can reach
576576
577−A job's network is restricted by its project's
578−[guardrails](/guides/guardrails/): it reaches the hosts its project's
579−guardrails allow, g1t itself, and what builds need, and nothing else.
580−What builds need is the package registries (npm, PyPI, crates.io, the Go
581−proxy, RubyGems, Packagist, NuGet, Maven and Gradle, Debian's mirrors),
582−GitHub, where `uses:` actions and the setup actions' downloads come from,
583−the toolchains' download sites (`nodejs.org`, `go.dev`,
584−`static.rust-lang.org`), and the public container registries (Docker Hub,
585−GitHub's, Quay, and `mirror.gcr.io`, the mirror of Docker Hub that a job's
586−Engine asks first). A request anywhere else gets `403`, and its status
587−line names the host (`403 Forbidden by g1t guardrails: example.com is not
588−an allowed domain`), so even a tool that reports only the status says which
589−host to allow. To reach another host, someone with the Maintain [role](/guides/access-and-roles/) or
590−higher adds it to the project's
591−allowed domains under **Settings → Guardrails**; a project whose guardrails
592−turn the network restriction off runs its jobs with an open network.
577+A job reaches the internet, as on GitHub's machines: package registries,
578+GitHub, toolchains' download sites, container registries, your cloud's
579+APIs and any other host. There is no list of hosts to allow. A job that
580+may be given [brokered secrets](#brokered-secrets) sends its HTTP and
581+HTTPS through g1t's runner Worker, which fills them in and sends every
582+request on; every other job's requests go straight out. See
583+[network](/guides/guardrails/#network).
593584
594−A request to an allowed host that fails on the way is handled for the job.
595−A `GET` or `HEAD` with no body is sent again, twice, after a short wait; one
596−that still fails gets `502` with `x-g1t-egress: upstream-failed` and a body
597−naming the host and the error, which npm, pip and most tools retry. A
598−request with a body is never sent twice. An answer up to 8 MB that breaks
599−off partway is fetched again whole, and a larger one, such as a 50 MB
600−package tarball, is resumed from where it stopped with a `Range` request
601−when its host takes ranges, so the job receives one unbroken answer.
602−
603−A host only workflows should reach, such as the API a deploy uploads to,
604−goes in **Workflow-only domains** instead, limited to the workflows and
605−environments that need it: `api.cloudflare.com | deploy.yml | production`
606−lets only `deploy.yml`'s jobs with `environment: production` reach it.
607−Runs of pull requests from forks never reach those hosts. See [workflow-only domains](/guides/guardrails/#workflow-only-domains).
585+A request that passes the Worker and fails on the way is handled for the
586+job. A `GET` or `HEAD` with no body is sent again, twice, after a short
587+wait; one that still fails gets `502` with `x-g1t-egress: upstream-failed`
588+and a body naming the host and the error, which npm, pip and most tools
589+retry. A request with a body is never sent twice. An answer up to 8 MB
590+that breaks off partway is fetched again whole, and a larger one, such as
591+a 50 MB package tarball, is resumed from where it stopped with a `Range`
592+request when its host takes ranges, so the job receives one unbroken
593+answer.
608594
609595 ### Brokered secrets
610596
614600 leave its sandbox, in headers (Basic auth included) and the query string.
615601 The value never enters the job, so neither its steps nor a compromised
616602 dependency can read it. A stand-in sent to any other host goes as it is,
617−and the log says so once. This works on g1t's machines, whether the
618−project's network is restricted or open, and on self-hosted runners whose
603+and the log says so once. This works on g1t's machines, and on self-hosted runners whose
619604 jobs run on Linux or macOS, which pass the job's requests to those hosts
620605 through g1t so the value never reaches the runner's computer. A job on
621606 Windows gets the stand-in too, never the value, and its log warns that it
769754 - **One Engine per job.** It runs inside the job's own sandbox, a virtual
770755 machine of its own, and is gone with it. No Docker socket of g1t's, or of
771756 any machine, is shared with a job.
772−- **The job's guardrails hold.** Containers use the job's network, so a
773− container, a build step or an image pull reaches only what the job may
774− reach. A host off the list gets `403` with the reason, as any step does.
775−- **HTTPS keeps working.** In a job whose network is restricted, every
776− container and build step is given the certificate the job's HTTPS is
777− checked with, in `/dev/g1t-egress`, and `SSL_CERT_FILE`,
757+- **The job's network.** Containers use the job's network, so a
758+ container, a build step or an image pull reaches what the job reaches.
759+- **HTTPS keeps working.** In a job whose requests pass g1t's runner
760+ Worker for its brokered secrets, every container and build step is
761+ given the certificate the job's HTTPS is re-signed with, in
762+ `/dev/g1t-egress`, and `SSL_CERT_FILE`,
778763 `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, `PIP_CERT`,
779764 `GIT_SSL_CAINFO` and `CARGO_HTTP_CAINFO` pointing at it, unless the
780765 container sets them itself. None of it is written into an image's layers.
11171102
11181103 ### Reaching your cloud
11191104
1120−Asking for the token goes to `api.g1t.sh`, which every job reaches. Trading
1121−it for credentials goes to your cloud's token service, which a job reaches
1122−only when its project allows it (see [what a job can reach](#what-a-job-can-reach)).
1123−Add the hosts under **Settings → Guardrails → Workflow-only domains**,
1124−limited to the workflow and environment that deploy, so other workflows
1125−never reach them:
1126−
1127−```text
1128−sts.us-east-1.amazonaws.com | deploy.yml | production
1129−s3.us-east-1.amazonaws.com | deploy.yml | production
1130−sts.googleapis.com | deploy.yml | production
1131−iamcredentials.googleapis.com | deploy.yml | production
1132−login.microsoftonline.com | deploy.yml | production
1133−management.azure.com | deploy.yml | production
1134−```
1105+Asking for the token goes to `api.g1t.sh`, and trading it for credentials
1106+goes to your cloud's token service. A job reaches both, as it reaches any
1107+host (see [what a job can reach](#what-a-job-can-reach)); there is nothing
1108+to allow on g1t's side. Which repositories, branches and environments may
1109+take on a role is up to the trust policy you write on your cloud's side.
11351110
11361111 | Cloud | Token exchange | Then |
11371112 | --- | --- | --- |
1138−| AWS | `sts.<region>.amazonaws.com`, the region `aws-region` names | Each service the job uses, such as `s3.<region>.amazonaws.com`, or `*.amazonaws.com` for all of them |
1113+| AWS | `sts.<region>.amazonaws.com`, the region `aws-region` names | Each service the job uses, such as `s3.<region>.amazonaws.com` |
11391114 | Google Cloud | `sts.googleapis.com`, `iamcredentials.googleapis.com` | Each API the job uses, such as `run.googleapis.com` |
11401115 | Azure | `login.microsoftonline.com` | `management.azure.com`, and each service the job uses |
11411116
1142−Without them, the login step fails with `403` and a line naming the host
1143−that is not allowed.
1144−
11451117 ### Claims
11461118
11471119 Each token carries the claims GitHub's do, so trust policies written for
11951167 }
11961168 ```
11971169
1198−3. Allow `sts.<region>.amazonaws.com`, and what the job deploys to, as
1199− [workflow-only domains](#reaching-your-cloud).
1200−4. Use `aws-actions/configure-aws-credentials@v4` with `role-to-assume`,
1170+3. Use `aws-actions/configure-aws-credentials@v4` with `role-to-assume`,
12011171 as above.
12021172
12031173 ### Google Cloud
12211191 --member="principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/g1t/attribute.repository/acme/web"
12221192 ```
12231193
1224−3. Allow `sts.googleapis.com` and `iamcredentials.googleapis.com`, and
1225− the APIs the job uses, as [workflow-only domains](#reaching-your-cloud).
1226−4. Use `google-github-actions/auth@v2` with
1194+3. Use `google-github-actions/auth@v2` with
12271195 `workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/g1t/providers/g1t`
12281196 and `service_account`. It asks for the provider's own name as the
12291197 audience, which the provider accepts unless you change its allowed
12471215 ```
12481216
12491217 2. Give it a role on what it deploys to, as for any identity.
1250−3. Allow `login.microsoftonline.com` and `management.azure.com` as
1251− [workflow-only domains](#reaching-your-cloud).
1252−4. Use `azure/login@v2` with `client-id`, `tenant-id` and
1218+3. Use `azure/login@v2` with `client-id`, `tenant-id` and
12531219 `subscription-id`, and no secret.
12541220
12551221 A federated credential matches the subject exactly: add one per
12581224 ### Cloudflare
12591225
12601226 Cloudflare's API takes API tokens, not OIDC tokens. Keep a token scoped
1261−to what the workflow deploys in a secret, available to that workflow only
1262−(see [workflow-only domains](/guides/guardrails/#workflow-only-domains)
1263−for limiting where it can be sent).
1227+to what the workflow deploys in a secret, available to that workflow only,
1228+and broker it to `api.cloudflare.com` so the job never holds its value
1229+(see [brokered secrets](#brokered-secrets)).
12641230
12651231 A Worker of your own can trust g1t's jobs directly, by checking the token
12661232 a job sends it against g1t's keys:
+10−21
141141 sccache kept in the Actions cache.
142142
143143 The deploy jobs use `environment: production`, so only they can read the
144−Cloudflare token. They reach Cloudflare through the project's
145−[workflow-only domains](/guides/guardrails/), under **Settings →
146−Guardrails**:
147−
148−```text
149−api.cloudflare.com | deploy.yml | production
150−registry.cloudflare.com | deploy.yml, runner-base.yml | production
151−```
144+Cloudflare token. Jobs on g1t's machines reach Cloudflare's API and
145+registry as they reach any host; there is nothing to allow.
152146
153147 Every deploy shows on the repository's Deployments page. A deploy run by
154148 hand reports one too when `G1T_DEPLOY_TOKEN` holds a g1t token with
338332 `pull` (Docker checks for layers before it uploads them). If it needs
339333 `pull`, do not add it: a pull login reads every app's image. Find
340334 another way first, such as Cloudflare scoping logins to a repository.
341−3. **The registry's hosts.** A push from a guarded sandbox reaches only
342− `registry.cloudflare.com` (on the deploy builds' list in
343− `services/runner/src/egress.ts`). If its layers upload to another host,
344− add that host there.
345−4. **Checking a digest.** g1t asks
335+3. **Checking a digest.** g1t asks
346336 `HEAD https://registry.cloudflare.com/v2/<account>/g1t-<app>/manifests/sha256:…`
347337 with a pull login of its own, following a `WWW-Authenticate: Bearer`
348338 challenge. It answers 200 for an image that was pushed.
349−5. **The app's Worker.** A script uploaded to the dispatch namespace with
339+4. **The app's Worker.** A script uploaded to the dispatch namespace with
350340 `exports: { "G1tApp": { "type": "durable-object", "storage": "sqlite" } }`
351341 and `containers: [{ "class_name": "G1tApp" }]` in its metadata is taken,
352342 and taken again on the next deploy with nothing changed about the class.
353−6. **Its Durable Object namespace.**
343+5. **Its Durable Object namespace.**
354344 `GET /accounts/{id}/workers/durable_objects/namespaces` lists it with
355345 `class` `G1tApp`, `script` the app's name and `dispatch_namespace`
356346 `g1t-deployments`.
357−7. **The Container application.** `POST /accounts/{id}/containers/applications`
347+6. **The Container application.** `POST /accounts/{id}/containers/applications`
358348 with `scheduling_policy` `default`, the image by digest, `instance_type`,
359349 `max_instances` 1 and `durable_objects.namespace_id` creates it; a
360350 rollout (`…/rollouts`, `strategy` `rolling`, `step_percentage` 100,
361351 `kind` `full_auto`) moves it to a new image; `DELETE` removes it.
362−8. **Starting.** `ctx.container.start({ env, enableInternet: true })`
352+7. **Starting.** `ctx.container.start({ env, enableInternet: true })`
363353 works under the default policy from a Worker in a dispatch namespace,
364354 the container gets its environment, and `getTcpPort(port).fetch()`
365355 reaches it; the dispatcher's limits (50 ms of CPU, 50 subrequests) leave
366356 room for a page that waits on a container starting.
367−9. **Sleeping and billing.** Five minutes after the last request the
357+8. **Sleeping and billing.** Five minutes after the last request the
368358 container stops (the app's object stops it itself), Cloudflare's billable
369359 usage for Containers stops growing, and `container_meters` in the
370360 deployments database has the minutes the app's Worker reported, which
371361 should be at least what Cloudflare billed for the same time.
372−10. **Pausing and taking down.** A workspace over its limit gets its
362+9. **Pausing and taking down.** A workspace over its limit gets its
373363 container app's Worker put back paused, which stops the container, and
374364 taking an app down deletes its application before its Worker.
375365
418408 Restarts during a deploy can look like an outage to the status page's
419409 checks. Set the same value as the status Worker's secret
420410 `STATUS_DEPLOY_TOKEN` and as the repository's Actions secret
421−`STATUS_DEPLOY_TOKEN`, and add the status page's host to the workflow-only
422−domains. Deploys then announce their start and finish, and the status page
411+`STATUS_DEPLOY_TOKEN`. Deploys then announce their start and finish, and the status page
423412 drafts no incident during them or for 3 minutes after.
424413
425414 ### Browser push
+3−5
767767 reason and what to do, and **Redeploy** shows the same reason.
768768 2. It starts a build in a sandbox of its own, the same machines that run
769769 [GitHub Actions](/guides/actions/). The sandbox checks out
770− the commit with a read-only token that expires in 30 minutes. Its
771− network is restricted to the project's allowed domains, the package
772− registries, GitHub and Cloudflare's API, as
773− [workflow jobs'](/guides/actions/#what-a-job-can-reach) are, and it
770+ the commit with a read-only token that expires in 30 minutes. It
771+ reaches the internet, as
772+ [workflow jobs](/guides/actions/#what-a-job-can-reach) do, and it
774773 stops at 30 minutes.
775774 When it stops, what it cost is settled against what was reserved.
776775 3. The sandbox builds, then lists its files. g1t opens an upload with
864863 | "Deployments come with the g1t plan … and `<workspace>` does not have it" | An owner starts the g1t plan under Billing. |
865864 | A deployment **Skipped**, saying it reached its spend limit or that new compute is paused | The build was refused before it started. Follow the link in the message: raise the spend limit, prepay, or answer the spike, on Billing. |
866865 | "Stopped: unusual CPU use" | The build looked like it was mining. Contact support if it was a real build; see [abuse and mining](/guides/guardrails/#abuse-and-mining). |
867−| "403" from a host in the build log | The build's network is restricted. Add the host to the project's allowed domains under **Settings → Guardrails**. |
868866 | "found nothing to serve" | Add a `Dockerfile`, a `build` script, an `index.html`, or a Workers config; or set **Output directory**. |
869867 | "Docker deployments are not switched on for g1t.page yet" | The project is a container, which g1t.page does not run yet. Set **Build as** to Static site or Workers if it can be built as one. See [Docker deployments](#docker-deployments). |
870868 | "**Build as** is Docker, but this project has no Dockerfile" | Add a `Dockerfile`, or set **Dockerfile** to its path. |
+1−1
533533 | The runner's release key | The app trusts a `g1t-runner` release only when its manifest is signed with g1t's runner release key, whose public half (the repository variable `RUNNER_RELEASE_PUBLIC_KEY`) is built into the app, in `src-tauri/src/runner/native.rs`. To build the app for a g1t of your own, whose runner releases you sign with your own key, set `G1T_RUNNER_RELEASE_KEY` to its public key, base64, when you build it. |
534534 | The updater's signing key | `node scripts/desktop-release.mjs keygen`, once. The private key is the repository secret `DESKTOP_SIGNING_KEY`; the public key is `plugins.updater.pubkey` in `tauri.conf.json`. Losing the private key means every installed app stops updating: keep it. |
535535 | Linux builds | g1t's own Linux machines, in every release. |
536−| Windows builds | Either a Windows machine registered as a [self-hosted runner](/guides/self-hosted-runners/) with the labels `self-hosted` and `windows` (Rust, Node and the Visual Studio build tools on it) and the repository variable `DESKTOP_WINDOWS_RUNNER` set to `true`; or, with no Windows machine, `DESKTOP_WINDOWS_FROM_LINUX` set to `true`, which cross-builds on Linux with `cargo-xwin` and NSIS and needs `aka.ms` and `download.visualstudio.microsoft.com` among the project's [workflow-only domains](/guides/guardrails/#workflow-only-domains) for `desktop-release.yml` in `production`, where the MSVC runtime is fetched from. |
536+| Windows builds | Either a Windows machine registered as a [self-hosted runner](/guides/self-hosted-runners/) with the labels `self-hosted` and `windows` (Rust, Node and the Visual Studio build tools on it) and the repository variable `DESKTOP_WINDOWS_RUNNER` set to `true`; or, with no Windows machine, `DESKTOP_WINDOWS_FROM_LINUX` set to `true`, which cross-builds on Linux with `cargo-xwin` and NSIS, fetching the MSVC runtime from `aka.ms` and `download.visualstudio.microsoft.com`. |
537537 | macOS builds | A Mac, registered as a self-hosted runner with the labels `self-hosted` and `macos`, and the repository variable `DESKTOP_MACOS_RUNNER` set to `true`. |
538538 | A platform with no machine | Its job is skipped and the release goes out with the rest; the download page says which platforms a release lacks, and the next release fills them in. |
539539 | Where releases are served | `g1t.sh/downloads/desktop/<version>/<file>`, from the same bucket as the runner's and the CLI's releases. |
+53−172
11 ---
22 title: Guardrails
3−description: Which hosts a project's jobs, checks, merge queue and deploy builds may reach on g1t's machines, how long a run may take, and how each rule is enforced.
3+description: How long a project's checks, merge queue and merge checks may run on g1t's machines, how a sandbox reaches the internet, and how g1t watches for mining.
44 ---
55
6−Guardrails decide what a project's work may do in g1t's sandboxes: which
7−hosts a sandbox can reach, and how long a run may take. A workspace sets
8−defaults, and each project can override them.
6+Guardrails decide how long a project's work may run in g1t's sandboxes. A
7+workspace sets defaults, and each project can override them.
98
10−They apply to every sandbox g1t starts for a project: its Actions jobs,
11−its checks, its merge queue and its deploy builds. Jobs and deploy builds
12−get the network list with what builds need added (see [builds](#builds)),
13−and their own time limit. Merge checks are not covered; see
14−[what is not covered](#what-is-not-covered). Every sandbox, whatever it
15−runs, is watched for [mining](#abuse-and-mining).
9+They apply to the runs g1t starts for a project: its checks, its merge
10+queue and its merge checks. Actions jobs and deploy builds keep their own
11+time limits. Every sandbox, whatever it runs, is watched for
12+[mining](#abuse-and-mining).
1613
1714 ## Where to set them
1815
2320 its repository can see and change them; the page is not shown to anyone
2421 else.
2522
26−Every setting on a project's page starts as "As the workspace", which
27−follows the workspace's default, whatever it is now. Choose a value to
28−override it for that project only. Allowed domains and workflow-only
29−domains add up: a project's are added to the workspace's, never
30−instead of them.
23+A time left empty on a project's page follows the workspace's default,
24+whatever it is now, shown faded. Enter a number to override it for that
25+project only.
3126
3227 Changes apply to runs that start after you save. A run that is under way
33−keeps the guardrails it started with.
34−
35−## Network
36−
37−With **Only allowed hosts** restricted (the default), a sandbox can reach:
38−
39−- g1t's own hosts, always: `g1t.sh`, `api.g1t.sh` and `mcp.g1t.sh`, for
40− cloning, pushing and reporting;
41−- the package registries that are on (all of them by default):
42−
43− | Registry | Hosts |
44− | --- | --- |
45− | npm and Yarn | `registry.npmjs.org`, `registry.yarnpkg.com`, `repo.yarnpkg.com` |
46− | PyPI | `pypi.org`, `files.pythonhosted.org` |
47− | crates.io and Rust toolchains | `crates.io`, `index.crates.io`, `static.crates.io`, `static.rust-lang.org` |
48− | Go module proxy | `proxy.golang.org`, `sum.golang.org` |
49− | GitHub downloads | `codeload.github.com`, `raw.githubusercontent.com`, `objects.githubusercontent.com` |
50−
51−- the domains you list: `api.stripe.com` allows exactly that host, and
52− `*.example.com` allows every subdomain of `example.com` (not
53− `example.com` itself; list both if you need both).
54−
55−`github.com` itself is not on the default list. A project with
56−dependencies fetched with git from GitHub, rather than as archives, needs it
57−listed.
58−
59−### How it is enforced
60−
61−This is enforced outside the sandbox, by Cloudflare Containers' outbound
62−interception:
63−
64−- A restricted sandbox starts with no internet connection at all. Its DNS
65− resolves nothing on its own, and no protocol or port other than HTTP (80)
66− and HTTPS (443) has any route out: SSH, raw TCP and UDP connections fail.
67−- Every HTTP and HTTPS request it makes is handed to g1t's runner Worker
68− before it leaves. The Worker forwards requests to allowed hosts and
69− answers every other with `403`. The refusal names the host three times
70− over, since tools differ in what they show of a failed request: in its
71− status line (`403 Forbidden by g1t guardrails: example.com is not an
72− allowed domain`, which is what a tool that prints only the status shows),
73− in an `x-g1t-blocked-host` header beside `x-g1t-guardrails: blocked`, and
74− in a plain-text body saying where to allow it: **Allowed domains** for
75− every sandbox, or **Workflow-only domains** for the jobs of one workflow
76− and environment.
77−- To see the host of an HTTPS request, the connection is re-encrypted with
78− a certificate the sandbox is given when it starts, which g1t adds to the
79− sandbox's trusted certificates (and points Node.js, Python, curl, Cargo
80− and git at). A program that brings its own fixed list of certificates and
81− ignores the system's cannot connect anywhere, allowed or not.
28+keeps the guardrails it started with. Each change is recorded in the
29+workspace's [audit log](/guides/audit-log/) as `update_guardrails`, saying
30+which time caps were set, changed or cleared.
8231
83−Nothing running in the sandbox, root included, can change this: the rule
84−is applied where the sandbox's traffic leaves it, not inside it.
32+## Time per run
8533
86−Each refused host appears once as a step of the run, such as
87−`Blocked: example.com (not an allowed domain)`. It is a note, not the
88−run's state: the run carries on. If a run needs a host, allow it and run
89−it again.
34+How long each kind of run may take, in minutes. By default:
9035
91−Tools that send usage reports home are told not to in a sandbox with only
92−allowed hosts (`DO_NOT_TRACK=1`, and each common tool's own switch, such
93−as `WRANGLER_SEND_METRICS=false`), so those reports are not refused and
94−noted on every run.
36+| Kind of run | Minutes |
37+| --- | --- |
38+| Checks | 45 |
39+| Merge queue | 45 |
40+| Merge check | 10 |
9541
96−Setting **Only allowed hosts** to Open gives that project's sandboxes the
97−whole internet, as before guardrails.
42+At most 240 minutes. A run that reaches its time cap fails, with
43+"Stopped: it reached its time cap of 45 minutes." The sandbox is stopped
44+whatever is running in it. Actions jobs keep their own limit: at most 60
45+minutes on g1t's machines, or less with `timeout-minutes` (see
46+[Actions](/guides/actions/)). Your plan can cap a run's time lower still.
9847
99−### Builds
48+## Network
10049
101−GitHub Actions jobs and deploy builds reach the project's list, plus what
102−real builds need, which no setting removes:
50+Every sandbox reaches the internet, as a CI machine does: package
51+registries, GitHub, container registries, your cloud's APIs and anything
52+else a job or build fetches from or sends to. No setting limits the hosts
53+a sandbox may reach.
10354
104−- GitHub, where `uses:` actions, `actions/checkout`'s helpers and the
105− setup actions' downloads come from: `github.com`, `api.github.com`,
106− `codeload.github.com`, `objects.githubusercontent.com`,
107− `raw.githubusercontent.com`, `release-assets.githubusercontent.com`,
108− `ghcr.io`;
109−- toolchains: `nodejs.org`, `go.dev`, `dl.google.com`,
110− `static.rust-lang.org`, `sh.rustup.rs`;
111−- every package registry above, whatever the project turned on, and
112− RubyGems, Packagist, NuGet, Maven Central, Gradle and
113− Debian's mirrors;
114−- container registries, for a job's own Docker Engine: Docker Hub
115− (`registry-1.docker.io`, `auth.docker.io` and the CDNs its layers come
116− from), `mirror.gcr.io`, Quay (`quay.io` and its CDNs), and Docker's
117− package repository, `download.docker.com`;
118−- for deploy builds, Cloudflare's API, which the build uploads its app to.
55+A sandbox's requests go straight out. Two kinds of request pass g1t's
56+runner Worker on the way:
11957
120−The repository itself is cloned from g1t, which is always reachable. A
121−project whose guardrails set **Only allowed hosts** to Open runs its jobs
122−and builds with an open network too.
58+- **A sandbox's report that it stopped itself for mining**, to
59+ `sandbox.g1t.internal`, which never leaves g1t.
60+- **Every HTTP and HTTPS request of a workflow job that may be given
61+ [brokered secrets](#brokered-secrets)**, so they can be filled in. The
62+ Worker sends each one on, to any host, and tries a download that fails
63+ midway again. To see a request's host and headers over HTTPS, the
64+ connection is re-encrypted with a certificate the sandbox is given when
65+ it starts, which g1t adds to the sandbox's trusted certificates (and
66+ points Node.js, Python, curl, Cargo and git at). The job's other
67+ protocols go straight out.
12368
12469 The containers a job starts with [Docker](/guides/actions/#docker), its
125−services and its build steps share the job's network, so this list is
126−theirs too: an image from another registry, or a build step that
127−downloads from another host, needs that host allowed, as a step would.
128−
129−### Workflow-only domains
130−
131−Some hosts only a workflow should reach: the API a deploy uploads to, a
132−release server, a package registry you publish to. Listing them under
133−allowed domains would open them to every sandbox as well. List them under
134−**Workflow-only domains** instead, one per line:
135−
136−```
137−api.cloudflare.com | deploy.yml | production
138−uploads.example.com | release.yml, nightly.yml
139−*.internal.example.com
140−```
141−
142−| Part | |
143−| --- | --- |
144−| The domain | As for allowed domains: `example.com`, or `*.example.com` for its subdomains. |
145−| Workflows | Workflow files by name, comma-separated, as they are in `.g1t/workflows/`. Left out: any workflow. |
146−| Environments | The environments a job must name with `environment:`, comma-separated. Left out: any job. |
147−
148−A domain is reached only by:
149−
150−- jobs of the workflows and environments its line names;
151−- in a run that is not of a pull request from a fork, which runs code
152− anyone could write.
153−
154−Checks, the merge queue and deploy builds never reach these hosts, whatever the line says. A job that names its environment with an
155−expression (`environment: ${{ inputs.target }}`) matches only lines with
156−no environments.
157−
158−Workflow-only domains are set by the same people as the rest of the page:
159−owners for the workspace's, Admin for a project's. Each change
160−is recorded in the workspace's [audit log](/guides/audit-log/) as
161−`update_guardrails`, saying which domains were added or removed and what
162−they were limited to.
163−
164−A deploy that signs in to AWS, Google Cloud or Azure with the job's
165−[OIDC token](/guides/actions/#oidc-tokens), rather than a key kept in a
166−secret, needs its cloud's token service listed here, such as
167−`sts.us-east-1.amazonaws.com | deploy.yml | production`. See
168−[reaching your cloud](/guides/actions/#reaching-your-cloud) for each
169−cloud's hosts.
70+services and its build steps share the job's network.
17071
17172 ### Brokered secrets
17273
173−The same interception keeps a workflow's API keys out of its jobs. A
74+The Worker keeps a workflow's API keys out of its jobs. A
17475 [brokered secret](/guides/secrets-and-variables/#brokered-secrets) names
17576 the hosts it is for, and a job is given a stand-in in place of its value.
17677 As a request to one of those hosts passes the runner Worker, the Worker
18182 stand-ins with that job's own token and keeps them only while the job
18283 runs.
18384
184−Brokering does not widen the network: a secret's hosts must still be ones
185−the job may reach. A job whose project's network is open, but which may be
186−given a brokered secret, still sends its HTTP and HTTPS through the
187−Worker, which reaches every host for it, and trusts the certificate as a
188−restricted sandbox does; its other protocols go straight out, as before.
18985 Self-hosted runners' traffic does not pass through g1t, so a runner whose
19086 jobs run on Linux or macOS sends the requests that carry a stand-in to g1t
19187 itself, through a proxy it runs for the job, and g1t fills them in the same
19288 way; see
19389 [on a self-hosted runner](/guides/secrets-and-variables/#on-a-self-hosted-runner).
19490 On Windows, brokered secrets stay stand-ins.
195−
196−## Time per run
197−
198−How long each kind of run may take, in minutes. By default:
199−
200−| Kind of run | Minutes |
201−| --- | --- |
202−| Checks | 45 |
203−| Merge queue | 45 |
204−| Merge check | 10 |
205−
206−At most 240 minutes. A run that reaches its time cap fails, with
207−"Stopped: it reached its time cap of 45 minutes." The sandbox is stopped
208−whatever is running in it. Actions jobs keep their own limit: at most 60
209−minutes on g1t's machines, or less with `timeout-minutes` (see
210−[Actions](/guides/actions/)).
21191
21292 ## Abuse and mining
21393
214−g1t does not run cryptocurrency miners, on any plan. Mining needs a mining
215−pool and hours of CPU; g1t's sandboxes withhold the first and watch for the
216−second:
94+g1t does not run cryptocurrency miners, on any plan. Mining needs hours of
95+CPU, and g1t's sandboxes watch for it:
21796
218−- **No pool to reach.** No mining pool is on any allowed list, so a
219− restricted sandbox's miner has nowhere to send its work.
22097 - **Miners by name.** A check, a build command, a workflow step or a container a job starts whose image or
22198 command names a known miner (`xmrig`, `cpuminer`, `t-rex` and others), a
22299 pool address (`stratum+tcp://`) or a miner's flags (`--donate-level`,
242119 staff are told, with the measurements, and look at what ran. If it was a
243120 real job, write to hey@flagon.io and say which run.
244121
122+Beyond that, what a sandbox may do is bounded by its time cap, your
123+plan's limits on how much may run and how long, the short-lived
124+credentials each run is given, and branch protection, which g1t enforces
125+on every push whatever happened in the sandbox.
126+
245127 ## What is not covered
246128
247−- **Merge checks** only merge two commits; they get no network list.
248−- **Self-hosted runners** run on your machines, whose network g1t does not
249− see: the network list is not enforced there. See
129+- **Self-hosted runners** run on your machines, and reach whatever those
130+ machines can. See
250131 [self-hosted runners](/guides/self-hosted-runners/#security).
+1−1
232232 | **General** | The project's name and description, its source, its **root directory**, [what it is](#what-a-project-is), its [links](#links), and its [production image](#uploading-an-image). A project shows its repository's description, and follows it as it changes, until you give the project one of its own; **Use the repository's description** goes back. |
233233 | **Deployments** | Production, previews, build command, output directory and idle days. See [Deployments](/guides/deployments/#settings). |
234234 | **Domains** | Custom domains for production. See [custom domains](/guides/deployments/#custom-domains). |
235−| **Guardrails** | What g1t's sandboxes may reach while they work here. See [guardrails](/guides/guardrails/). |
235+| **Guardrails** | How long g1t's checks, merge queue and merge checks may run here. See [guardrails](/guides/guardrails/). |
236236 | **Repository** | The repository's name, description, website, [topics](/guides/search/#what-is-indexed) and default branch, and its danger zone: visibility, archive, transfer and delete. See [Managing a repository](/guides/managing-repositories/). |
237237 | **Access** | Who has a [role](/guides/access-and-roles/) on the repository, and invitations. |
238238 | **Branches and merging** | Auto-merge, the CODEOWNERS file's errors, and what the rules hold for the default branch. |
+9−12
112112
113113 1. Open **Settings → Secrets and variables**, and add or edit the secret.
114114 2. Under **Brokered**, tick **Only for these hosts**.
115−3. List the hosts it is for, one per line, as for
116− [allowed domains](/guides/guardrails/#network): `registry.npmjs.org`,
117− or `*.example.com` for every subdomain of `example.com`.
115+3. List the hosts it is for, one per line: `registry.npmjs.org`, or
116+ `*.example.com` for every subdomain of `example.com` (not
117+ `example.com` itself; list both if you need both).
118118 4. Save.
119119
120120 Workflows use it as they would any secret: `${{ secrets.NPM_TOKEN }}` in an
138138 A request to a host the secret is not for goes out with the stand-in,
139139 which is useless to whoever receives it, and the job's log says so once:
140140 `NPM_TOKEN was sent to example.com, which it is not for, so g1t did not
141−fill it in.` A host must also be one the job may reach (its project's
142−allowed domains, what builds need, or a
143−[workflow-only domain](/guides/guardrails/#workflow-only-domains)):
144−brokering a secret for a host does not open the network to it.
141+fill it in.`
145142
146143 If an answer echoes the value back, it is swapped for the stand-in again
147144 in the answer's headers, and in its body when that is plain text (JSON,
151148
152149 ### Where it works
153150
154−- **Workflow jobs on g1t's machines.** Brokering works whether the
155− project's network is restricted or open: a job that may be given a
156− brokered secret sends its HTTP and HTTPS through g1t either way, and
157− trusts the certificate HTTPS is re-signed with, as a restricted one does.
158− Other protocols are not affected.
151+- **Workflow jobs on g1t's machines.** A job that may be given a
152+ brokered secret sends its HTTP and HTTPS through g1t's runner Worker,
153+ which fills the value in and sends every request on, to any host, and
154+ the job trusts the certificate its HTTPS is re-signed with. Other jobs'
155+ requests go straight out. Other protocols are not affected.
159156 - **Self-hosted runners on Linux and macOS**, in Docker, in the desktop
160157 app's sandbox, or directly on the computer: the job's requests to the
161158 secret's hosts go through g1t, which fills the value in, so it never
+1−4
666666 short-lived credential that can do only what that kind of run may, in
667667 that repository, revoked when it ends.
668668 - Its machine time is free, as for every job on your runners.
669−- [Guardrails](/guides/guardrails/) cannot enforce their network list on
670− your machine.
671669
672670 A project can have its own setting, or follow the workspace's.
673671
707705 runner's user and can reach whatever that user can; host mode is
708706 deprecated and refuses public repositories' work. Use ephemeral runners
709707 for code you do not trust. See [isolation](#isolation).
710−- **Network**: g1t's network guardrails are not enforced on your machines.
711− A job reaches whatever the machine can.
708+- **Network**: a job reaches whatever the machine can.
712709 - **Audit**: making a registration token, registering, removing, and every
713710 job a runner takes are in the [audit log](/guides/audit-log/), with the
714711 runner as the actor.
+1−1
733733 | | **Emoji** | Members | The workspace's own emoji. `-/emoji` |
734734 | **Artifacts** | **Spaces and sharing** | | Soon: which spaces a workspace starts with, and how a new artifact is shared by default. |
735735 | **Security** | **Security settings** | Members | The workspace's security policies. `-/security/settings` |
736−| | [**Guardrails**](/guides/guardrails/) | Members | What Actions jobs on g1t's machines may reach. `-/guardrails` |
736+| | [**Guardrails**](/guides/guardrails/) | Members | How long checks, the merge queue and merge checks may run. `-/guardrails` |
737737 | | [**Rules**](/guides/rules/) | Members | Rulesets across projects. `-/rules` |
738738 | | [**Audit log**](/guides/audit-log/) | Members | Every change, by whom, to what. `-/audit` |
739739 | **Integrations** | [**Integrations**](/guides/integrations/) | Members | The tools connected for everyone. `-/integrations` |
+3−162
11 /**
22 * Guardrails: the form a workspace sets its defaults with and a project its
3− * overrides: the network a sandbox may reach, and how long its runs may take.
3+ * overrides: how long its runs may take.
44 */
5−import { type ReactNode, useState } from "react";
5+import type { ReactNode } from "react";
66 import { Form } from "react-router";
77
88 import { type GuardrailSettings, type Guardrails, type GuardrailsView, RUN_KIND_LABEL } from "@g1t/contracts";
99
10−import { SANDBOX_RUN_KINDS, tri, workflowDomainLine } from "../lib/guardrails";
10+import { SANDBOX_RUN_KINDS } from "../lib/guardrails";
1111 import { ErrorText, Input, SubmitButton, TimeAgo } from "./ui";
1212 import { Card } from "./ui/card";
13−import { CheckboxOption } from "./ui/checkbox";
14−import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
15−import { Textarea } from "./ui/textarea";
1613
1714 function Section({ title, about, children }: { title: string; about: ReactNode; children: ReactNode }) {
1815 return (
2623 );
2724 }
2825
29−/** A choice of inheriting, or on or off, for one setting. */
30−function TriSelect({
31− name,
32− value,
33− inherited,
34− parent,
35− labels,
36− title,
37− children,
38−}: {
39− name: string;
40− value: boolean | null | undefined;
41− inherited: boolean;
42− parent: string;
43− labels: [on: string, off: string];
44− title: string;
45− children: ReactNode;
46−}) {
47− return (
48− <Card className="flex flex-col gap-3 p-4 sm:flex-row sm:items-start">
49− <div className="min-w-0 grow">
50− <p className="text-sm font-medium">{title}</p>
51− <p className="mt-1 text-sm text-muted">{children}</p>
52− </div>
53− <Select name={name} defaultValue={tri(value)}>
54− <SelectTrigger size="sm" aria-label={title} className="w-full shrink-0 sm:w-auto sm:min-w-44">
55− <SelectValue />
56− </SelectTrigger>
57− <SelectContent align="end">
58− <SelectItem value="inherit">
59− As {parent} ({(inherited ? labels[0] : labels[1]).toLowerCase()})
60− </SelectItem>
61− <SelectItem value="on">{labels[0]}</SelectItem>
62− <SelectItem value="off">{labels[1]}</SelectItem>
63− </SelectContent>
64− </Select>
65− </Card>
66− );
67−}
68−
69−/** Hosts in a compact list. */
70−function Hosts({ hosts }: { hosts: string[] }) {
71− return (
72− <span className="font-mono text-xs wrap-break-word text-faint">{hosts.length ? hosts.join(", ") : "none"}</span>
73− );
74−}
75−
7626 /**
7727 * One level's guardrails. `level` says which: the workspace's defaults,
7828 * over g1t's, or a project's overrides, over its workspace's.
9747 // What this level inherits, and what it is called.
9848 const base: Guardrails = level === "project" ? view.inherited : view.defaults;
9949 const parent = level === "project" ? "the workspace" : "g1t's default";
100− const [registryMode, setRegistryMode] = useState(own.registries ? "custom" : "inherit");
101− const shownRegistries = own.registries ?? base.registries;
102− const inheritedDomains = level === "project" ? view.inherited.domains : [];
103− const inheritedWorkflowDomains = level === "project" ? (view.inherited.workflowDomains ?? []) : [];
10450 return (
10551 // Keyed to the last change, so after a save the fields show what was kept, as it was tidied.
10652 <Form key={own.updatedAt ?? "unset"} method="post" className="max-w-4xl space-y-8">
10753 <fieldset disabled={!editable} className="min-w-0 space-y-8">
108− <Section
109− title="Network"
110− about={
111− <>
112− Which hosts a sandbox may reach. Requests anywhere else are refused at the sandbox's edge, and
113− each refused host shows on the run as a step.
114− </>
115− }
116− >
117− <TriSelect
118− name="restrictNetwork"
119− value={own.restrictNetwork}
120− inherited={base.restrictNetwork}
121− parent={parent}
122− labels={["Restricted", "Open"]}
123− title="Only allowed hosts"
124− >
125− Restricted, a sandbox reaches g1t, the registries below and the domains you list, over HTTP and
126− HTTPS only. Open, it reaches the whole internet.
127− </TriSelect>
128− <Card className="p-4">
129− <p className="text-sm font-medium">Always allowed</p>
130− <p className="mt-1 text-sm text-muted">
131− g1t's own hosts, for cloning, pushing and reporting: <Hosts hosts={view.g1tHosts} />
132− </p>
133− </Card>
134− <Card className="p-4">
135− <div className="flex flex-col gap-3 sm:flex-row sm:items-start">
136− <div className="min-w-0 grow">
137− <p className="text-sm font-medium">Package registries</p>
138− <p className="mt-1 text-sm text-muted">Where installs fetch dependencies from.</p>
139− </div>
140− <Select name="registries" value={registryMode} onValueChange={setRegistryMode}>
141− <SelectTrigger size="sm" aria-label="Package registries" className="w-full shrink-0 sm:w-auto sm:min-w-44">
142− <SelectValue />
143− </SelectTrigger>
144− <SelectContent align="end">
145− <SelectItem value="inherit">As {parent}</SelectItem>
146− <SelectItem value="custom">Choose</SelectItem>
147− </SelectContent>
148− </Select>
149− </div>
150− <div className="mt-4 grid gap-3 sm:grid-cols-2">
151− {view.registries.map((registry) => (
152− <CheckboxOption
153− key={`${registry.id}-${registryMode}`}
154− name={`registry:${registry.id}`}
155− disabled={registryMode !== "custom"}
156− defaultChecked={(registryMode === "custom" ? shownRegistries : base.registries).includes(registry.id)}
157− label={registry.name}
158− description={registry.hosts.join(", ")}
159− />
160− ))}
161− </div>
162− </Card>
163− <Card className="p-4">
164− <label htmlFor="guardrail-domains" className="text-sm font-medium">
165− Allowed domains
166− </label>
167− <p className="mt-1 text-sm text-muted">
168− One per line: <span className="font-mono text-xs">api.stripe.com</span>, or{" "}
169− <span className="font-mono text-xs">*.example.com</span> for its subdomains.
170− {level === "project" && " These add to the workspace's."}
171− </p>
172− <Textarea
173− id="guardrail-domains"
174− name="domains"
175− className="mt-3 font-mono text-xs"
176− rows={4}
177− defaultValue={(own.domains ?? []).join("\n")}
178− placeholder="api.stripe.com"
179− />
180− {inheritedDomains.length > 0 && (
181− <p className="mt-2 text-xs text-faint">
182− From the workspace: <Hosts hosts={inheritedDomains} />
183− </p>
184− )}
185− </Card>
186− <Card className="p-4">
187− <label htmlFor="guardrail-workflow-domains" className="text-sm font-medium">
188− Workflow-only domains
189− </label>
190− <p className="mt-1 text-sm text-muted">
191− Hosts that only workflow jobs may reach, such as the API a deploy uploads to. One
192− per line: the domain, then the workflows and the environments it is for, each comma-separated;
193− leave either out for any. Only jobs of runs that are not pull requests from forks get them.
194− {level === "project" && " These add to the workspace's."}
195− </p>
196− <Textarea
197− id="guardrail-workflow-domains"
198− name="workflowDomains"
199− className="mt-3 font-mono text-xs"
200− rows={3}
201− defaultValue={(own.workflowDomains ?? []).map(workflowDomainLine).join("\n")}
202− placeholder="api.cloudflare.com | deploy.yml | production"
203− />
204− {inheritedWorkflowDomains.length > 0 && (
205− <p className="mt-2 text-xs text-faint">
206− From the workspace:{" "}
207− <span className="font-mono wrap-break-word">{inheritedWorkflowDomains.map(workflowDomainLine).join("; ")}</span>
208− </p>
209− )}
210− </Card>
211− </Section>
212−
21354 <Section title="Time limits" about="How long one run may take. A run that reaches it is stopped.">
21455 <Card className="p-4">
21556 <p className="text-sm font-medium">Time per run, in minutes</p>
+1−1
277277 icon: <Eye size={18} />,
278278 title: "Guardrails",
279279 about:
280− "Which hosts a project's jobs, checks and deploy builds may reach, and how long they may run, enforced in g1t's sandboxes.",
280+ "How long a project's checks and merge queue may run, enforced in g1t's sandboxes.",
281281 to: `${DOCS}/guides/guardrails/`,
282282 },
283283 {
+1−1
55 "": { title: "General", about: "The project's name, where its code lives, and what it is for." },
66 deployments: { title: "Deployments", about: "How the project builds, where it runs, and its previews." },
77 domains: { title: "Domains", about: "Addresses of your own for the production deployment." },
8− guardrails: { title: "Guardrails", about: "What this project's sandboxes may reach, and how long their runs may take." },
8+ guardrails: { title: "Guardrails", about: "How long this project's runs may take." },
99 repository: { title: "Repository", about: "Its name, details and default branch, who can see it, and archiving, moving or deleting it." },
1010 access: { title: "Access", about: "Who can see and change the repository, with which role, and invitations to it." },
1111 keys: { title: "Deploy keys", about: "SSH keys that let a machine clone this repository, or push to it, and reach nothing else." },
+0−3
439439 </span>
440440 <Switch name="forks" checked={forks} onCheckedChange={setForks} disabled={!manage} />
441441 </label>
442− <p className="text-xs text-faint">
443− g1t&apos;s network guardrails are not enforced on your own machines: a job there reaches whatever the machine can.
444− </p>
445442 {manage && (
446443 <div className="flex flex-wrap gap-3">
447444 <SubmitButton name="intent" value="settings" pending="Saving…">
+1−1
2020
2121 - **Integrations**, such as GitHub, Linear, Jira, Sentry and Datadog.
2222 - **Webhooks** you add, which send events to the addresses you choose.
23−- **Hosts your sandboxes reach**, such as package registries and anything you allow in your guardrails.
23+- **Hosts your sandboxes reach**, such as package registries and anything your jobs and builds fetch from or send to.
2424 - **Custom domains** you point at your deployments.
2525
2626 Questions about subprocessors: [hey@flagon.io](mailto:hey@flagon.io).
+1−1
5353
5454 - **An agent acts on behalf of the person who started its work**, within the limits of that person's access and the workspace's guardrails. Its actions are recorded as such in the audit log.
5555 - **You're responsible for what you ask agents to do**, and for checking what they produce before you rely on it, merge it or deploy it, as you would for a teammate's work. Agents make mistakes. Their output can be wrong, insecure, or similar to code that exists elsewhere, and you're responsible for making sure your use of it complies with the licenses and laws that apply.
56−- **Guardrails** (network limits, command rules and caps) reduce risk. They're a safety net, not a guarantee that an agent won't do something you didn't intend.
56+- **Guardrails** (time caps on runs) reduce risk. They're a safety net, not a guarantee that an agent won't do something you didn't intend.
5757 - **What agents produce for you is your content.** As between you and us, Flagon claims no rights in it.
5858 - **Agents cost money.** Runs, sandbox time and model use are charged to the workspace that owns the repository, as described on [pricing](/pricing).
5959
+2−3
2424
2525 Each workspace decides what its agents may do, and each project can tighten it:
2626
27−- **A network allowlist.** By default a sandbox can reach only g1t itself, the package registries you turn on, and the hosts you list. It's enforced outside the sandbox, where its traffic leaves it: a restricted sandbox has no other route to the internet, and nothing running inside it, root included, can change that.
2827 - **A command hook.** The agent's harness checks every tool call before it runs, and refuses force-pushes, rewriting the default branch, reading files outside the project, printing the environment, `sudo`, and any rules you add. With **No sudo** on, the sandbox gives up root before the agent starts.
2928 - **Caps** on what one run may spend and how long it may take, enforced by the harness and, for time, by stopping the sandbox itself.
3029 - **Branch protection**, enforced by g1t when a push arrives, whatever happened in the sandbox.
3130
32−Command rules guard against an agent's mistakes; the network allowlist, the run's credentials and branch protection are the hard boundaries. The [guardrails guide](https://docs.g1t.sh/guides/guardrails/) explains each one and how it's enforced.
31+Command rules guard against an agent's mistakes; the run's credentials and branch protection are the hard boundaries. The [guardrails guide](https://docs.g1t.sh/guides/guardrails/) explains each one and how it's enforced.
3332
3433 ## Isolated sandboxes
3534
7574
7675 - g1t.sh, api.g1t.sh, mcp.g1t.sh, og.g1t.sh, docs.g1t.sh and status.g1t.sh;
7776 - the g1t.page platform itself: routing, isolation between apps, custom domains;
78−- the sandboxes: escaping one, reaching another run or workspace, or getting around guardrails, run credentials or the network allowlist;
77+- the sandboxes: escaping one, reaching another run or workspace, or getting around guardrails or run credentials;
7978 - authentication, authorization, the API, the MCP server and git over HTTPS;
8079 - the [open-source code](/flagon-io/g1t), as deployed.
8180
+8−43
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4−import { capShare, formatCap, lines, settingsFromForm, tri, workflowDomainLine, workflowDomains } from "./guardrails.ts";
4+import { capShare, formatCap, settingsFromForm } from "./guardrails.ts";
55
6−const catalog = {
7− registries: ["npm", "pypi", "crates"],
8− kinds: ["implement", "review"] as const,
9−};
6+const catalog = { kinds: ["implement", "review"] as const };
107
118 function form(entries: Record<string, string>): FormData {
129 const data = new FormData();
1411 return data;
1512 }
1613
17−test("workflow-only domains are read one per line, and written back the same way", () => {
18− const read = workflowDomains(
19− " api.cloudflare.com | deploy.yml | production \n\n*.example.com\nhooks.example.com | | staging, production\nci.example.com | ci.yml, release.yml\n | deploy.yml",
20− );
21− assert.deepEqual(read, [
22− { domain: "api.cloudflare.com", workflows: ["deploy.yml"], environments: ["production"] },
23− { domain: "*.example.com", workflows: [], environments: [] },
24− { domain: "hooks.example.com", workflows: [], environments: ["staging", "production"] },
25− { domain: "ci.example.com", workflows: ["ci.yml", "release.yml"], environments: [] },
26− ]);
27− assert.deepEqual(read.map(workflowDomainLine), [
28− "api.cloudflare.com | deploy.yml | production",
29− "*.example.com",
30− "hooks.example.com | | staging, production",
31− "ci.example.com | ci.yml, release.yml",
32− ]);
33− const settings = settingsFromForm(form({ workflowDomains: "api.cloudflare.com | deploy.yml | production" }), catalog);
34− assert.deepEqual(settings.workflowDomains, [{ domain: "api.cloudflare.com", workflows: ["deploy.yml"], environments: ["production"] }]);
35−});
36−
3714 test("an untouched form inherits everything", () => {
38− const settings = settingsFromForm(form({ restrictNetwork: "inherit", registries: "inherit" }), catalog);
39− assert.equal(settings.restrictNetwork, null);
40− assert.equal(settings.registries, null);
41− assert.deepEqual(settings.minutes, {});
42− assert.deepEqual(settings.domains, []);
15+ assert.deepEqual(settingsFromForm(form({}), catalog), { minutes: {} });
4316 });
4417
45−test("choices of a level's own are kept", () => {
18+test("time caps of a level's own are kept, and nothing else is sent", () => {
4619 const settings = settingsFromForm(
4720 form({
48− restrictNetwork: "off",
49− registries: "custom",
50− "registry:npm": "on",
51− "registry:crates": "on",
5221 "minutes:implement": "45",
5322 "minutes:review": "",
54− domains: "api.stripe.com\n\n api.stripe.com \r\n*.example.com",
23+ // Fields of a page from before network settings were removed.
24+ restrictNetwork: "on",
25+ domains: "api.stripe.com",
5526 }),
5627 catalog,
5728 );
58− assert.equal(settings.restrictNetwork, false);
59− assert.deepEqual(settings.registries, ["npm", "crates"]);
60− assert.deepEqual(settings.minutes, { implement: 45 });
61− assert.deepEqual(settings.domains, ["api.stripe.com", "*.example.com"]);
29+ assert.deepEqual(settings, { minutes: { implement: 45 } });
6230 });
6331
6432 test("nonsense is passed on for the service to refuse", () => {
7240 });
7341
7442 test("helpers", () => {
75− assert.equal(tri(undefined), "inherit");
76− assert.equal(tri(false), "off");
77− assert.deepEqual(lines(" a \nb\na"), ["a", "b"]);
7843 assert.equal(capShare(1, 4), 0.25);
7944 assert.equal(capShare(9, 4), 1);
8045 assert.equal(capShare(1, null), null);
+7−65
1−import type { GuardrailSettings, RunKind, WorkflowDomain } from "@g1t/contracts";
1+import type { GuardrailSettings, RunKind } from "@g1t/contracts";
22 import { microsOf, money } from "./money.ts";
3−
4−/** What a level's form field means: inherit, or a choice of its own. */
5−export type Tri = "inherit" | "on" | "off";
6−
7−export function tri(value: boolean | null | undefined): Tri {
8− return value == null ? "inherit" : value ? "on" : "off";
9−}
10−
11−/** Lines of a textarea, trimmed, without blanks or repeats. */
12−export function lines(value: FormDataEntryValue | null): string[] {
13− const seen = new Set<string>();
14− for (const line of String(value ?? "").split(/\r?\n/)) {
15− const trimmed = line.trim();
16− if (trimmed) seen.add(trimmed);
17− }
18− return [...seen];
19−}
20−
21−/**
22− * Workflow-only domains, one per line, as
23− * `domain | workflows | environments`: the last two comma-separated, and
24− * either left out or empty for any. `api.cloudflare.com | deploy.yml |
25− * production` lets only deploy.yml's jobs in production reach it.
26− */
27−export function workflowDomains(value: FormDataEntryValue | null): WorkflowDomain[] {
28− const list = (text: string | undefined) =>
29− (text ?? "")
30− .split(",")
31− .map((part) => part.trim())
32− .filter(Boolean);
33− const out: WorkflowDomain[] = [];
34− for (const line of lines(value)) {
35− const [domain, workflows, environments] = line.split("|").map((part) => part.trim());
36− if (!domain) continue;
37− out.push({ domain, workflows: list(workflows), environments: list(environments) });
38− }
39− return out;
40−}
41−
42−/** A workflow-only domain as a line of the form's field. */
43−export function workflowDomainLine(entry: WorkflowDomain): string {
44− const parts = [entry.domain, entry.workflows.join(", "), entry.environments.join(", ")];
45− while (parts.length > 1 && !parts[parts.length - 1]) parts.pop();
46− return parts.join(" | ");
47−}
483
494 /** A number from a field; empty means inherit. Not a number is kept, to be refused. */
505 function amount(value: FormDataEntryValue | null): number | null {
5712 /** The kinds of run that still use a sandbox: checks, the merge queue and merge checks. */
5813 export const SANDBOX_RUN_KINDS = ["checks", "queue", "mergecheck"] as const satisfies readonly RunKind[];
5914
60−/** What one level's form says, as the settings that level keeps. */
61−export function settingsFromForm(
62− form: FormData,
63− catalog: { registries: string[]; kinds: readonly RunKind[] },
64−): GuardrailSettings {
65− const choice = (name: string): boolean | null => {
66− const value = form.get(name);
67− return value === "on" ? true : value === "off" ? false : null;
68− };
15+/**
16+ * What one level's form says, as the settings that level keeps: a time cap
17+ * for each kind of run it sets, the rest inherited.
18+ */
19+export function settingsFromForm(form: FormData, catalog: { kinds: readonly RunKind[] }): GuardrailSettings {
6920 const minutes: Record<string, number> = {};
7021 for (const kind of catalog.kinds) {
7122 const cap = amount(form.get(`minutes:${kind}`));
7223 if (cap != null) minutes[kind] = Number.isNaN(cap) ? 0 : Math.trunc(cap);
7324 }
74− return {
75− restrictNetwork: choice("restrictNetwork"),
76− registries:
77− form.get("registries") === "custom"
78− ? catalog.registries.filter((id) => form.get(`registry:${id}`) === "on")
79− : null,
80− domains: lines(form.get("domains")),
81− workflowDomains: workflowDomains(form.get("workflowDomains")),
82− minutes,
83− };
25+ return { minutes };
8426 }
8527
8628 /** A cost in dollars, or "no cap". */
+1−1
144144 <span className="min-w-0 grow">
145145 <span className="block text-sm font-medium">Guardrails</span>
146146 <span className="mt-0.5 block text-sm text-muted">
147− What this repository's sandboxes may reach, and how long their runs may take.
147+ How long this repository's runs may take.
148148 </span>
149149 </span>
150150 <ChevronRight size={16} className="shrink-0 text-faint transition-transform group-hover:translate-x-0.5" />
+2−7
2626 const user = requireUser(context, request);
2727 await requireCapability(context, params, "manage_protection");
2828 const form = await request.formData();
29− const current = await guardrails.getGuardrails(user, params.owner);
30− if (!current.ok) return { saved: false, error: current.error.message };
31− const settings = settingsFromForm(form, {
32− registries: current.value.registries.map((registry) => registry.id),
33− kinds: SANDBOX_RUN_KINDS,
34− });
29+ const settings = settingsFromForm(form, { kinds: SANDBOX_RUN_KINDS });
3530 const saved = await guardrails.updateGuardrails(user, params.owner, { namespace: params.owner, name: params.repo }, settings);
3631 return saved.ok ? { saved: true, error: null } : { saved: false, error: saved.error.message };
3732 }
4237 <>
4338 <RepoSettingsHeading base={base} />
4439 <p className="mb-8 max-w-3xl text-sm text-muted">
45− What g1t, its checks and the merge queue may do in this project's sandboxes. Anything left as the
40+ How long this project's checks, merge queue and merge checks may run. Anything left as the
4641 workspace's follows the{" "}
4742 <Link to={`/${params.owner}/-/guardrails`} className="text-fg underline-offset-2 hover:underline">
4843 workspace's defaults
+1−6
2424 assertSameOrigin(request);
2525 const user = requireUser(context, request);
2626 const form = await request.formData();
27− const current = await guardrails.getGuardrails(user, params.owner);
28− if (!current.ok) return { saved: false, error: current.error.message };
29− const settings = settingsFromForm(form, {
30− registries: current.value.registries.map((registry) => registry.id),
31− kinds: SANDBOX_RUN_KINDS,
32− });
27+ const settings = settingsFromForm(form, { kinds: SANDBOX_RUN_KINDS });
3328 const saved = await guardrails.updateGuardrails(user, params.owner, null, settings);
3429 return saved.ok ? { saved: true, error: null } : { saved: false, error: saved.error.message };
3530 }
+1−1
103103 },
104104 guardrails: {
105105 title: "Guardrails",
106− about: "What every project's sandboxes may reach, and how long their runs may take. Each project can override these under its settings.",
106+ about: "How long every project's runs may take. Each project can override these under its settings.",
107107 },
108108 webhooks: {
109109 title: "Webhooks",
+1−1
9090 Policies
9191 </h2>
9292 <p className="mt-1.5 text-sm text-muted">
93− What sandboxes may reach, the rules every repository follows, and how the workspace&apos;s security is set up.
93+ How long runs may take, the rules every repository follows, and how the workspace&apos;s security is set up.
9494 </p>
9595 <div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm">
9696 <Link to={`/${slug}/-/guardrails`} className="text-accent hover:underline">
+9−14
10401040 pub repo: RepoPath,
10411041 /// Minutes before the job is stopped.
10421042 pub timeout_minutes: u32,
1043− /// The workflow file the job is in (`.g1t/workflows/deploy.yml`), for
1044− /// the guardrails' workflow-only domains.
1045− #[serde(default)]
1046− pub workflow: Option<String>,
1047− /// The environment the job names with `environment:`, when it names
1048− /// one plainly (not with an expression).
1049− #[serde(default)]
1050− pub environment: Option<String>,
1051− /// Whether its run is trusted: not a pull request from a fork. Only a
1052− /// trusted run's jobs reach workflow-only domains.
1053− #[serde(default)]
1054− pub trusted: bool,
10551043 /// The machine its `runs-on` asked for, by label (`instance_for`):
10561044 /// `g1t-2core` or `g1t-4core`; absent, the standard one.
10571045 #[serde(default)]
10581046 pub instance: Option<String>,
10591047 /// Whether the job may be given brokered secrets, so the runner sends
1060− /// its HTTP and HTTPS through the Worker even when its network is open.
1048+ /// its HTTP and HTTPS through the Worker, which fills them in.
10611049 #[serde(default)]
10621050 pub brokered: bool,
10631051 }
17811769 "job": "job_1", "token": "t", "repo": { "namespace": "acme", "name": "web" }, "timeoutMinutes": 30
17821770 }))
17831771 .unwrap();
1784− assert!(args.workflow.is_none() && args.environment.is_none() && !args.trusted && args.instance.is_none());
1772+ assert!(args.instance.is_none() && !args.brokered);
1773+ // Callers from when workflow-only domains existed still read.
1774+ let older: StartJobArgs = serde_json::from_value(serde_json::json!({
1775+ "job": "job_1", "token": "t", "repo": { "namespace": "acme", "name": "web" }, "timeoutMinutes": 30,
1776+ "workflow": ".g1t/workflows/deploy.yml", "environment": "production", "trusted": true
1777+ }))
1778+ .unwrap();
1779+ assert_eq!(older.timeout_minutes, 30);
17851780 }
17861781 }
17871782
+59−373
1−//! Guardrails: what a workspace lets its sandboxes do. Kept by the work
1+//! Guardrails: how long a workspace's runs may take. Kept by the work
22 //! service.
33 //!
4−//! A workspace sets defaults and each project may override them. Two kinds
5−//! of rule come out of the two:
6−//!
7−//! - **Network**: which hosts a sandbox may reach. g1t's own hosts always,
8−//! the package registries the project needs, and any domains listed.
9−//! Everything else is refused at the sandbox's edge. Some domains can be
10−//! opened to workflow jobs only.
11−//! - **Time**: how long each kind of run may take before g1t stops it.
4+//! A workspace sets defaults and each project may override them: a time
5+//! cap for each kind of run, after which g1t stops it. Sandboxes reach the
6+//! internet as any CI machine does; guardrails say nothing about the
7+//! network.
128 //!
13−//! Levels saved when guardrails also had command rules, deny patterns and
14−//! a cost cap (for g1t's own agents, since removed) still read: those
15−//! fields are ignored.
9+//! Levels saved when guardrails also had network rules (restricting the
10+//! network, package registries, allowed domains and workflow-only
11+//! domains), command rules, deny patterns and a cost cap still read: those
12+//! fields are ignored, and dropped when the level is saved again.
1613 //!
1714 //! Each `*Args` struct is the argument of the method of the same name,
1815 //! served at `POST /rpc/<method>`.
2421 use crate::agents::RunKind;
2522 use crate::repos::RepoPath;
2623 use crate::{User, Viewer};
27−
28−/// g1t's own hosts. Always reachable: without them a sandbox could not
29−/// clone, push or report.
30−pub const G1T_HOSTS: &[&str] = &["g1t.sh", "api.g1t.sh", "mcp.g1t.sh"];
31−
32−/// A package registry, which a project turns on or off as one.
33−#[derive(Clone, Copy, Debug)]
34−pub struct Registry {
35− pub id: &'static str,
36− pub name: &'static str,
37− pub hosts: &'static [&'static str],
38−}
39−
40−/// The registries a sandbox can be given, all on by default.
41−pub const REGISTRIES: &[Registry] = &[
42− Registry {
43− id: "npm",
44− name: "npm and Yarn",
45− hosts: &["registry.npmjs.org", "registry.yarnpkg.com", "repo.yarnpkg.com"],
46− },
47− Registry {
48− id: "pypi",
49− name: "PyPI",
50− hosts: &["pypi.org", "files.pythonhosted.org"],
51− },
52− Registry {
53− id: "crates",
54− name: "crates.io and Rust toolchains",
55− hosts: &["crates.io", "index.crates.io", "static.crates.io", "static.rust-lang.org"],
56− },
57− Registry {
58− id: "go",
59− name: "Go module proxy",
60− hosts: &["proxy.golang.org", "sum.golang.org"],
61− },
62− Registry {
63− id: "github",
64− name: "GitHub downloads",
65− hosts: &[
66− "codeload.github.com",
67− "raw.githubusercontent.com",
68− "objects.githubusercontent.com",
69− ],
70− },
71−];
7224
73−/// The most domains one level keeps.
74−pub const MAX_DOMAINS: usize = 100;
75−/// The most workflow-only domains one level keeps.
76−pub const MAX_WORKFLOW_DOMAINS: usize = 50;
77−const MAX_NAME_CHARS: usize = 255;
7825 /// The longest any run may be allowed to take, in minutes.
7926 pub const MAX_MINUTES: u32 = 240;
8027
9946
10047 /// What one level, the workspace or a project, sets. Anything left unset
10148 /// is inherited: a project from its workspace, a workspace from g1t's
102−/// defaults. Domains add up across the two levels.
49+/// defaults.
10350 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
10451 #[serde(rename_all = "camelCase", default)]
10552 pub struct GuardrailSettings {
106− /// Whether sandboxes may reach only the allowed hosts.
107− pub restrict_network: Option<bool>,
108− /// The registries that are on, by id. Replaces the inherited list.
109− pub registries: Option<Vec<String>>,
110− /// More hosts to allow: `example.com`, or `*.example.com` for its
111− /// subdomains.
112− pub domains: Vec<String>,
113− /// Hosts only workflow jobs may reach, never agents: a deploy's API,
114− /// say. Each can be limited to some workflows and environments. They
115− /// add to the other level's.
116− pub workflow_domains: Vec<WorkflowDomain>,
11753 /// How long a run may take, in minutes, by kind of run.
11854 pub minutes: BTreeMap<String, u32>,
11955 /// Username of whoever last changed this level.
12056 pub updated_by: Option<String>,
12157 /// RFC 3339.
12258 pub updated_at: Option<String>,
123−}
124−
125−/// A host that only workflow jobs may reach: jobs of a trusted run (not a
126−/// pull request from a fork), of the workflows named, in the environments
127−/// named. Agents, checks, the merge queue and g1t.page builds never do.
128−#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
129−#[serde(rename_all = "camelCase", default)]
130−pub struct WorkflowDomain {
131− /// `api.example.com`, or `*.example.com` for its subdomains.
132− pub domain: String,
133− /// Workflow files by name, such as `deploy.yml`. Empty: any workflow.
134− pub workflows: Vec<String>,
135− /// The environments a job must name with `environment:`, such as
136− /// `production`. Empty: any job, whether it names one or not.
137− pub environments: Vec<String>,
138−}
139−
140−impl WorkflowDomain {
141− /// Whether a job of `workflow` (its path or file name) in `environment`
142− /// may reach this domain. Names compare without regard to case.
143− pub fn applies_to(&self, workflow: &str, environment: Option<&str>) -> bool {
144− let file = workflow_file(workflow);
145− let workflow_ok = self.workflows.is_empty() || self.workflows.iter().any(|w| workflow_file(w).eq_ignore_ascii_case(file));
146− let environment_ok = self.environments.is_empty()
147− || environment.is_some_and(|env| self.environments.iter().any(|e| e.eq_ignore_ascii_case(env.trim())));
148− workflow_ok && environment_ok
149− }
150−}
151−
152−/// A workflow's file name: `.g1t/workflows/deploy.yml` is `deploy.yml`.
153−fn workflow_file(path: &str) -> &str {
154− let path = path.trim();
155− path.rsplit(['/', '\\']).next().unwrap_or(path)
15659 }
15760
15861 /// The guardrails a run actually gets: g1t's defaults, then the
16063 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
16164 #[serde(rename_all = "camelCase")]
16265 pub struct Guardrails {
163− pub restrict_network: bool,
164− pub registries: Vec<String>,
165− /// The domains listed at either level, workspace first.
166− pub domains: Vec<String>,
167− /// Every host a sandbox may reach: g1t's, the registries', the domains.
168− pub hosts: Vec<String>,
169− /// Hosts only some workflow jobs may reach, from both levels,
170− /// workspace first. Never in `hosts`.
171− #[serde(default)]
172− pub workflow_domains: Vec<WorkflowDomain>,
17366 /// Every kind of run.
17467 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>,
17576 }
17677
17778 impl Guardrails {
178− /// g1t's defaults: network restricted to g1t and every registry, and a
179− /// time cap for each kind of run.
79+ /// g1t's defaults: a time cap for each kind of run.
18080 pub fn defaults() -> Self {
181− let mut defaults = Guardrails {
182− restrict_network: true,
183− registries: REGISTRIES.iter().map(|registry| registry.id.to_owned()).collect(),
184− domains: Vec::new(),
185− hosts: Vec::new(),
186− workflow_domains: Vec::new(),
81+ Guardrails {
18782 minutes: RunKind::ALL
18883 .into_iter()
18984 .map(|kind| (kind.as_str().to_owned(), default_minutes(kind)))
19085 .collect(),
191− };
192− defaults.hosts = defaults.allowed_hosts();
193− defaults
86+ restrict_network: false,
87+ hosts: Vec::new(),
88+ }
19489 }
19590
19691 /// One level laid over what it inherits.
19792 pub fn apply(mut self, level: &GuardrailSettings) -> Self {
198− if let Some(restrict) = level.restrict_network {
199− self.restrict_network = restrict;
200− }
201− if let Some(registries) = &level.registries {
202− self.registries = REGISTRIES
203− .iter()
204− .filter(|registry| registries.iter().any(|id| id == registry.id))
205− .map(|registry| registry.id.to_owned())
206− .collect();
207− }
208− for domain in &level.domains {
209− if !self.domains.contains(domain) {
210− self.domains.push(domain.clone());
211− }
212− }
213− for entry in &level.workflow_domains {
214− if !self.workflow_domains.contains(entry) {
215− self.workflow_domains.push(entry.clone());
216− }
217− }
21893 for (kind, minutes) in &level.minutes {
21994 if let Some(cap) = self.minutes.get_mut(kind) {
22095 *cap = *minutes;
22196 }
22297 }
223− self.hosts = self.allowed_hosts();
22498 self
22599 }
226100
230104 match project {
231105 Some(project) => inherited.apply(project),
232106 None => inherited,
233− }
234− }
235−
236− fn allowed_hosts(&self) -> Vec<String> {
237− let mut hosts: Vec<String> = G1T_HOSTS.iter().map(|host| (*host).to_owned()).collect();
238− for registry in REGISTRIES {
239− if self.registries.iter().any(|id| id == registry.id) {
240− hosts.extend(registry.hosts.iter().map(|host| (*host).to_owned()));
241− }
242− }
243− for domain in &self.domains {
244− if !hosts.contains(domain) {
245− hosts.push(domain.clone());
246− }
247− }
248− hosts
249− }
250−
251− /// The hosts a job of `workflow` (its path) in `environment` may
252− /// reach on top of `hosts`: the workflow-only domains that apply to
253− /// it. For workflow jobs of trusted runs only; the runner never adds
254− /// them for anything else.
255− pub fn workflow_hosts(&self, workflow: &str, environment: Option<&str>) -> Vec<String> {
256− let mut hosts: Vec<String> = Vec::new();
257− for entry in self.workflow_domains.iter().filter(|entry| entry.applies_to(workflow, environment)) {
258− if !hosts.contains(&entry.domain) {
259− hosts.push(entry.domain.clone());
260− }
261107 }
262− hosts
263108 }
264109
265110 /// The time cap of a kind of run, in minutes.
271116 }
272117 }
273118
274−/// A domain as it is kept: lower case, no scheme, path or port, optionally
275−/// `*.` for its subdomains. Refused if it is not a host name.
119+/// A host name as it is kept: lower case, no scheme, path or port,
120+/// optionally `*.` for its subdomains. Refused if it is not a host name.
121+/// The hosts a brokered secret is for are kept this way.
276122 pub fn normalize_domain(input: &str) -> Result<String, String> {
277123 let mut domain = input.trim().to_lowercase();
278124 for scheme in ["https://", "http://"] {
304150
305151 /// One level's settings, checked and tidied before they are kept.
306152 pub fn validate(settings: GuardrailSettings) -> Result<GuardrailSettings, String> {
307− let mut domains = Vec::new();
308− for domain in &settings.domains {
309− if domain.trim().is_empty() {
310− continue;
311− }
312− let domain = normalize_domain(domain)?;
313− if !domains.contains(&domain) {
314− domains.push(domain);
315− }
316− }
317− if domains.len() > MAX_DOMAINS {
318− return Err(format!("At most {MAX_DOMAINS} domains can be listed."));
319− }
320− let mut workflow_domains: Vec<WorkflowDomain> = Vec::new();
321− for entry in &settings.workflow_domains {
322− if entry.domain.trim().is_empty() {
323− continue;
324− }
325− let entry = validate_workflow_domain(entry)?;
326− if !workflow_domains.contains(&entry) {
327− workflow_domains.push(entry);
328− }
329− }
330− if workflow_domains.len() > MAX_WORKFLOW_DOMAINS {
331− return Err(format!("At most {MAX_WORKFLOW_DOMAINS} workflow-only domains can be listed."));
332− }
333153 let mut minutes = BTreeMap::new();
334154 for (kind, cap) in settings.minutes {
335155 if RunKind::parse(&kind).is_none() {
340160 }
341161 minutes.insert(kind, cap);
342162 }
343− let registries = settings.registries.map(|ids| {
344− REGISTRIES
345− .iter()
346− .filter(|registry| ids.iter().any(|id| id == registry.id))
347− .map(|registry| registry.id.to_owned())
348− .collect()
349− });
350163 Ok(GuardrailSettings {
351− restrict_network: settings.restrict_network,
352− registries,
353− domains,
354− workflow_domains,
355164 minutes,
356165 updated_by: settings.updated_by,
357166 updated_at: settings.updated_at,
358167 })
359−}
360−
361−/// A workflow-only domain, tidied: its domain as `normalize_domain` keeps
362−/// it, workflows as file names ending in `.yml` or `.yaml`, and
363−/// environments as given, each list without repeats.
364−pub fn validate_workflow_domain(entry: &WorkflowDomain) -> Result<WorkflowDomain, String> {
365− let domain = normalize_domain(&entry.domain)?;
366− let mut workflows: Vec<String> = Vec::new();
367− for workflow in entry.workflows.iter().map(|w| workflow_file(w).to_owned()).filter(|w| !w.is_empty()) {
368− let lower = workflow.to_lowercase();
369− let valid = (lower.ends_with(".yml") || lower.ends_with(".yaml"))
370− && workflow.chars().count() <= MAX_NAME_CHARS
371− && workflow.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'));
372− if !valid {
373− return Err(format!("{workflow} is not a workflow file. Name it as it is in .g1t/workflows, such as deploy.yml."));
374− }
375− if !workflows.iter().any(|w| w.eq_ignore_ascii_case(&workflow)) {
376− workflows.push(workflow);
377− }
378− }
379− let mut environments: Vec<String> = Vec::new();
380− for environment in entry.environments.iter().map(|e| e.trim().to_owned()).filter(|e| !e.is_empty()) {
381− if environment.chars().count() > MAX_NAME_CHARS || environment.contains(['\n', '\r']) || environment.contains("${{") {
382− return Err(format!("{environment} is not an environment name."));
383− }
384− if !environments.iter().any(|e| e.eq_ignore_ascii_case(&environment)) {
385− environments.push(environment);
386− }
387− }
388− Ok(WorkflowDomain { domain, workflows, environments })
389−}
390−
391−/// A registry as the settings page shows it.
392−#[derive(Clone, Debug, Serialize, Deserialize)]
393−pub struct RegistryInfo {
394− pub id: String,
395− pub name: String,
396− pub hosts: Vec<String>,
397168 }
398169
399170 /// Everything the settings pages show: each level as it was set, what
409180 pub inherited: Guardrails,
410181 /// What runs get: the project's, or with no project, the workspace's.
411182 pub effective: Guardrails,
412− pub g1t_hosts: Vec<String>,
413− pub registries: Vec<RegistryInfo>,
414183 }
415184
416185 impl GuardrailsView {
423192 defaults: Guardrails::defaults(),
424193 inherited,
425194 effective,
426− g1t_hosts: G1T_HOSTS.iter().map(|host| (*host).to_owned()).collect(),
427− registries: REGISTRIES
428− .iter()
429− .map(|registry| RegistryInfo {
430− id: registry.id.to_owned(),
431− name: registry.name.to_owned(),
432− hosts: registry.hosts.iter().map(|host| (*host).to_owned()).collect(),
433− })
434− .collect(),
435195 }
436196 }
437197 }
508268 }
509269
510270 #[test]
511− fn defaults_restrict_to_g1t_and_every_registry() {
271+ fn defaults_cap_every_kind_of_run() {
512272 let defaults = Guardrails::defaults();
513− assert!(defaults.restrict_network);
514− assert!(defaults.hosts.iter().any(|host| host == "api.g1t.sh"));
515− assert!(defaults.hosts.iter().any(|host| host == "registry.npmjs.org"));
516− assert!(defaults.hosts.iter().any(|host| host == "codeload.github.com"));
517− assert!(!defaults.hosts.iter().any(|host| host == "github.com"));
518− assert!(!defaults.hosts.iter().any(|host| host == "models.g1t.sh"));
519273 assert_eq!(defaults.minutes_for(RunKind::Implement), 90);
274+ assert_eq!(defaults.minutes.len(), RunKind::ALL.len());
520275 }
521276
522277 #[test]
527282 #[test]
528283 fn a_project_overrides_its_workspace() {
529284 let workspace = GuardrailSettings {
530− registries: Some(vec!["npm".into(), "pypi".into()]),
531− minutes: BTreeMap::from([("implement".to_owned(), 30)]),
285+ minutes: BTreeMap::from([("implement".to_owned(), 30), ("review".to_owned(), 20)]),
532286 ..level()
533287 };
534288 let project = GuardrailSettings {
535− registries: Some(vec!["crates".into()]),
289+ minutes: BTreeMap::from([("review".to_owned(), 15)]),
536290 ..level()
537291 };
538292 let inherited = Guardrails::merge(&workspace, None);
539− assert_eq!(inherited.registries, vec!["npm", "pypi"]);
540− assert!(inherited.hosts.iter().any(|host| host == "pypi.org"));
541− assert!(!inherited.hosts.iter().any(|host| host == "crates.io"));
542−
293+ assert_eq!(inherited.minutes_for(RunKind::Review), 20);
543294 let effective = Guardrails::merge(&workspace, Some(&project));
544− assert_eq!(effective.registries, vec!["crates"]);
545− assert!(effective.hosts.iter().any(|host| host == "crates.io"));
546− assert!(!effective.hosts.iter().any(|host| host == "pypi.org"));
547295 // Inherited where the project says nothing.
548296 assert_eq!(effective.minutes_for(RunKind::Implement), 30);
549− assert_eq!(effective.minutes_for(RunKind::Review), 30);
550− // g1t's own hosts can never be turned off.
551− assert!(effective.hosts.iter().any(|host| host == "g1t.sh"));
297+ assert_eq!(effective.minutes_for(RunKind::Review), 15);
552298 }
553299
554300 #[test]
555− fn domains_add_up() {
556− let workspace = GuardrailSettings { domains: vec!["api.stripe.com".into()], ..level() };
557− let project = GuardrailSettings { domains: vec!["*.example.com".into(), "api.stripe.com".into()], ..level() };
558− let effective = Guardrails::merge(&workspace, Some(&project));
559− assert_eq!(effective.domains, vec!["api.stripe.com", "*.example.com"]);
560− assert!(effective.hosts.iter().any(|host| host == "*.example.com"));
561− }
562−
563− fn workflow_domain(domain: &str, workflows: &[&str], environments: &[&str]) -> WorkflowDomain {
564− WorkflowDomain {
565− domain: domain.into(),
566− workflows: workflows.iter().map(|w| (*w).to_owned()).collect(),
567− environments: environments.iter().map(|e| (*e).to_owned()).collect(),
568− }
569− }
570−
571− #[test]
572− fn workflow_domains_are_never_hosts_and_reach_only_the_jobs_named() {
573− let workspace = GuardrailSettings {
574− workflow_domains: vec![workflow_domain("api.cloudflare.com", &["deploy.yml"], &["production"])],
575− ..level()
576− };
577− let project = GuardrailSettings {
578− workflow_domains: vec![
579− workflow_domain("api.cloudflare.com", &["deploy.yml"], &["production"]),
580− workflow_domain("*.example.com", &[], &[]),
581− ],
582− ..level()
583− };
584− let effective = Guardrails::merge(&workspace, Some(&project));
585− // Added up across the levels, once each, and never for agents.
586− assert_eq!(effective.workflow_domains.len(), 2);
587− assert!(!effective.hosts.iter().any(|host| host == "api.cloudflare.com" || host == "*.example.com"));
588− // deploy.yml's jobs in production, by path or name, in any case.
589− assert_eq!(effective.workflow_hosts(".g1t/workflows/deploy.yml", Some("production")), ["api.cloudflare.com", "*.example.com"]);
590− assert_eq!(effective.workflow_hosts("DEPLOY.yml", Some("Production")), ["api.cloudflare.com", "*.example.com"]);
591− // Another environment, none, or another workflow: only the open one.
592− assert_eq!(effective.workflow_hosts(".g1t/workflows/deploy.yml", Some("staging")), ["*.example.com"]);
593− assert_eq!(effective.workflow_hosts(".g1t/workflows/deploy.yml", None), ["*.example.com"]);
594− assert_eq!(effective.workflow_hosts(".g1t/workflows/ci.yml", Some("production")), ["*.example.com"]);
595− }
596−
597− #[test]
598− fn workflow_domains_are_tidied_or_refused() {
599− let settings = validate(GuardrailSettings {
600− workflow_domains: vec![
601− workflow_domain(" HTTPS://API.Cloudflare.com/client/v4 ", &[".g1t/workflows/deploy.yml", "deploy.yml"], &[" production ", "Production"]),
602− workflow_domain("api.cloudflare.com", &["deploy.yml"], &["production"]),
603− workflow_domain(" ", &[], &[]),
604− ],
605− ..level()
606− })
301+ fn levels_saved_with_network_rules_still_read_and_drop_them() {
302+ // Network rules, command rules, deny patterns and a cost cap, from
303+ // before, are ignored, and gone once the level is saved again.
304+ let old: GuardrailSettings = serde_json::from_str(
305+ r#"{"restrictNetwork":true,"registries":["npm"],"domains":["example.com"],
306+ "workflowDomains":[{"domain":"api.cloudflare.com","workflows":["deploy.yml"],"environments":[]}],
307+ "rules":{"sudo":false},"deny":["Bash(rm:*)"],"budgetUsd":2.5,"minutes":{"implement":45}}"#,
308+ )
607309 .unwrap();
608− assert_eq!(settings.workflow_domains, vec![workflow_domain("api.cloudflare.com", &["deploy.yml"], &["production"])]);
609− let refused = |entry: WorkflowDomain| validate(GuardrailSettings { workflow_domains: vec![entry], ..level() }).is_err();
610− assert!(refused(workflow_domain("localhost", &[], &[])));
611− assert!(refused(workflow_domain("api.example.com", &["deploy"], &[])));
612− assert!(refused(workflow_domain("api.example.com", &["de ploy.yml"], &[])));
613− assert!(refused(workflow_domain("api.example.com", &[], &["${{ inputs.env }}"])));
614− let many = (0..=MAX_WORKFLOW_DOMAINS).map(|i| workflow_domain(&format!("h{i}.example.com"), &[], &[])).collect();
615− assert!(validate(GuardrailSettings { workflow_domains: many, ..level() }).is_err());
616− }
617−
618− #[test]
619− fn levels_saved_before_workflow_domains_still_read() {
620− let old: GuardrailSettings = serde_json::from_str(r#"{"domains":["example.com"]}"#).unwrap();
621− assert!(old.workflow_domains.is_empty());
310+ assert_eq!(old.minutes, BTreeMap::from([("implement".to_owned(), 45)]));
311+ let saved = serde_json::to_value(validate(old).unwrap()).unwrap();
312+ assert_eq!(saved, serde_json::json!({ "minutes": { "implement": 45 }, "updatedBy": null, "updatedAt": null }));
313+ // What a run got before reads as its time caps.
622314 let old: Guardrails = serde_json::from_value(serde_json::json!({
623− "restrictNetwork": true, "registries": [], "domains": [], "hosts": [], "rules": {}, "deny": [], "budgetUsd": null, "minutes": {}
315+ "restrictNetwork": true, "registries": [], "domains": [], "hosts": ["g1t.sh"], "workflowDomains": [], "minutes": { "implement": 90 }
624316 }))
625317 .unwrap();
626− assert!(old.workflow_hosts("deploy.yml", Some("production")).is_empty());
627− // Command rules, deny patterns and a cost cap, from when g1t's own
628− // agents had them, are ignored.
629− let old: GuardrailSettings =
630− serde_json::from_str(r#"{"rules":{"sudo":false},"deny":["Bash(rm:*)"],"budgetUsd":2.5,"domains":["example.com"]}"#).unwrap();
631− assert_eq!(old.domains, vec!["example.com"]);
632− assert!(validate(old).is_ok());
318+ assert_eq!(old.minutes_for(RunKind::Implement), 90);
319+ assert!(!old.restrict_network && old.hosts.is_empty());
633320 }
634321
635322 #[test]
636− fn unrestricted_is_kept() {
637− let project = GuardrailSettings { restrict_network: Some(false), ..level() };
638− let effective = Guardrails::merge(&level(), Some(&project));
639− assert!(!effective.restrict_network);
323+ fn a_runner_from_before_reads_an_open_network() {
324+ let sent = serde_json::to_value(Guardrails::defaults()).unwrap();
325+ assert_eq!(sent["restrictNetwork"], false);
326+ assert_eq!(sent["hosts"], serde_json::json!([]));
640327 }
641328
642329 #[test]
651338 }
652339
653340 #[test]
654− fn validation_clamps_and_drops_unknowns() {
655− let settings = validate(GuardrailSettings {
656− registries: Some(vec!["npm".into(), "nonsense".into()]),
657− domains: vec!["Example.com".into(), "example.com".into(), " ".into()],
658− ..level()
659− })
660− .unwrap();
661− assert_eq!(settings.registries, Some(vec!["npm".to_owned()]));
662− assert_eq!(settings.domains, vec!["example.com"]);
341+ fn validation_refuses_bad_caps() {
663342 assert!(
664343 validate(GuardrailSettings {
665344 minutes: BTreeMap::from([("implement".to_owned(), 0)]),
674353 })
675354 .is_err()
676355 );
356+ assert!(
357+ validate(GuardrailSettings {
358+ minutes: BTreeMap::from([("implement".to_owned(), MAX_MINUTES)]),
359+ ..level()
360+ })
361+ .is_ok()
362+ );
677363 }
678364 }
+1−1
674674 return (false, BTreeMap::new());
675675 }
676676 if let Some(driver) = with.get("driver").filter(|d| !d.is_empty() && *d != "docker") {
677− self.log.line(&format!("`driver: {driver}` is not used on g1t's machines: builds run on this job's own Docker Engine (BuildKit, the `docker` driver), which keeps the sandbox's network and guardrails."));
677+ self.log.line(&format!("`driver: {driver}` is not used on g1t's machines: builds run on this job's own Docker Engine (BuildKit, the `docker` driver), which keeps the sandbox's network."));
678678 }
679679 for input in ["driver-opts", "buildkitd-flags", "buildkitd-config", "buildkitd-config-inline", "endpoint"] {
680680 if with.get(input).is_some_and(|v| !v.trim().is_empty()) {
+1−1
66 //! - **A container's network.** A sandbox cannot route a container's
77 //! bridge network out (Cloudflare Containers allow no iptables and no IP
88 //! forwarding), so containers join the job's own network (`host`), the
9−//! one its guardrails apply to. The names a container would have had on
9+//! one its requests leave by. The names a container would have had on
1010 //! its network (its name, its aliases, its Compose service) resolve to
1111 //! 127.0.0.1 in later containers and in the job's own steps, and a port
1212 //! published under another number (`-p 8080:80`) is forwarded to it.
+2−2
194194 let result = start();
195195 match &result {
196196 Ok(version) => super::super::note(format!(
197− "Docker: started this job's own Docker Engine {version} in {:.1}s. Its containers run in this job's sandbox, on the job's network and under its guardrails, and end with the job.",
197+ "Docker: started this job's own Docker Engine {version} in {:.1}s. Its containers run in this job's sandbox, on the job's network, and end with the job.",
198198 begun.elapsed().as_secs_f64()
199199 )),
200200 Err(problem) => super::super::note(format!("##[error]Docker could not start: {problem}")),
217217 let _ = std::fs::remove_file(&shim);
218218 std::os::unix::fs::symlink(&exe, &shim).map_err(|e| format!("could not link runc: {e}"))?;
219219
220− // A guarded job's certificate, for its containers.
220+ // The certificate of a job whose HTTPS passes the Worker, for its containers.
221221 let mut ca_dir = None;
222222 if let Ok(ca) = std::env::var("G1T_EGRESS_CA")
223223 && Path::new(&ca).exists()
+3−3
66 //! Containers run Docker), with no iptables and no IP forwarding, which
77 //! a sandbox does not have.
88 //! - `api` is `/var/run/docker.sock`: the Engine's API, with containers
9−//! moved to the job's own network, where its guardrails apply.
9+//! moved to the job's own network.
1010 //! - `oci` is the `runc` the Engine runs containers with: build steps on
11−//! the job's network, and a guarded job's egress certificate in every
12−//! container.
11+//! the job's network, and, in a job whose HTTPS passes the Worker, its
12+//! egress certificate in every container.
1313 //! - `http` is the HTTP/1.1 the proxy reads.
1414 //!
1515 //! Nothing here is shared with another job: each job has its own sandbox,
+3−3
88 //! job's own network instead (their network namespace and libnetwork's
99 //! hook are taken out of the container's config), because a sandbox has
1010 //! no route out of a bridge. `RUN --network=none` stays without one.
11−//! - **In a guarded job**, every container (`docker run`, services, build
11+//! - **In a job whose HTTPS passes the Worker** (one that may be given brokered secrets), every container (`docker run`, services, build
1212 //! steps, `docker exec`) gets the certificate the job's HTTPS is
1313 //! re-signed with: the folder of certificates at `/dev/g1t-egress`, and
1414 //! the variables that point common tools at it, unless the container
2727 /// The egress certificate alone.
2828 pub(crate) const EGRESS: &str = "egress-ca.crt";
2929
30−/// The variables a container is given in a guarded job, as the sandbox's
30+/// The variables a container is given in a job whose HTTPS passes the Worker, as the sandbox's
3131 /// own (services/runner egress.ts `EGRESS_ENV`) point its tools.
3232 pub(crate) fn ca_variables() -> Vec<(&'static str, String)> {
3333 let bundle = format!("{CA_MOUNT}/{BUNDLE}");
103103 }
104104
105105 /// Changes a container's `config.json`. `ca_dir`: the folder of
106−/// certificates to give it, in a guarded job. Returns whether it changed.
106+/// certificates to give it, in a job whose HTTPS passes the Worker. Returns whether it changed.
107107 pub(crate) fn patch_config(config: &mut Value, bundle: &str, ca_dir: Option<&str>) -> bool {
108108 let mut changed = false;
109109 // A BuildKit step bound for a bridge network: the job's network instead.
+11−10
11 //! Trusting the certificate a sandbox's HTTPS is re-signed with.
22 //!
3−//! When a sandbox's network is restricted by its project's guardrails (or a
4−//! workflow job may be given brokered secrets), its HTTP and HTTPS pass the
5−//! runner Worker, which re-signs HTTPS with a certificate Cloudflare puts in
6−//! the sandbox. The network list, the credentials a sandbox holds, and
7−//! branch protection on g1t's side are what bound a sandbox's work.
3+//! When a workflow job may be given brokered secrets, its HTTP and HTTPS
4+//! pass the runner Worker, which fills them in and re-signs HTTPS with a
5+//! certificate Cloudflare puts in the sandbox. Other sandboxes reach the
6+//! internet directly. The credentials a sandbox holds and branch
7+//! protection on g1t's side are what bound a sandbox's work.
88
99 use std::path::Path;
1010 use std::process::{Command, Stdio};
1111
12−/// The certificate the sandbox's HTTPS is re-signed with, when it is guarded.
12+/// The certificate the sandbox's HTTPS is re-signed with, when it passes
13+/// the Worker.
1314 const EGRESS_CA: &str = "/etc/cloudflare/certs/cloudflare-containers-ca.crt";
1415
1516 /// Runs a command as root without asking. False where that is not allowed.
2425 .is_ok_and(|status| status.success())
2526 }
2627
27−/// Trusts the certificate a guarded sandbox's HTTPS is re-signed with, so
28−/// that git, package managers and this program can reach allowed hosts.
29−/// Does nothing in a sandbox whose network is open. Called first thing,
30−/// before any request is made.
28+/// Trusts the certificate a sandbox's HTTPS is re-signed with, so that
29+/// git, package managers and this program can reach the hosts it
30+/// intercepts. Does nothing where Cloudflare put no certificate. Called
31+/// first thing, before any request is made.
3132 pub fn trust_egress_ca() {
3233 const TRUSTED: &str = "/usr/local/share/ca-certificates/cloudflare-containers-ca.crt";
3334 // Once per sandbox.
+1−1
9393 if selfhosted::is_command(&args) {
9494 std::process::exit(selfhosted::main(args));
9595 }
96− // A guarded sandbox's HTTPS is re-signed on its way out: trust that
96+ // A sandbox whose HTTPS passes the Worker has it re-signed: trust that
9797 // before anything is fetched.
9898 guard::trust_egress_ca();
9999 // Watches for mining for as long as the sandbox runs (abuse.rs).
+4−34
11 /**
2− * Guardrails: which hosts a workspace's sandboxes may reach and how long a
3− * run may take, kept by the work service. Mirrors `g1t_contracts::guardrails`.
2+ * Guardrails: how long a workspace's runs may take, kept by the work
3+ * service. Mirrors `g1t_contracts::guardrails`. Sandboxes reach the
4+ * internet as any CI machine does; guardrails say nothing about the
5+ * network.
46 */
57 import type { ServiceBinding } from "./clients";
68 import type { User, Viewer } from "./identity";
911
1012 /** What one level, the workspace or a project, sets. Unset is inherited. */
1113 export type GuardrailSettings = {
12− restrictNetwork?: boolean | null;
13− /** The registries that are on, by id. Replaces the inherited list. */
14− registries?: string[] | null;
15− /** More hosts to allow; they add to the other level's. */
16− domains?: string[];
17− /** Hosts only workflow jobs may reach, never agents; they add to the other level's. */
18− workflowDomains?: WorkflowDomain[];
1914 /** How long each kind of run may take, in minutes. */
2015 minutes?: Record<string, number>;
2116 updatedBy?: string | null;
2419
2520 /** What a run actually gets. */
2621 export type Guardrails = {
27− restrictNetwork: boolean;
28− registries: string[];
29− domains: string[];
30− /** Every host a sandbox may reach. `*.example.com` covers subdomains. */
31− hosts: string[];
32− /** Hosts only some workflow jobs may reach (never in `hosts`). */
33− workflowDomains?: WorkflowDomain[];
3422 /**
3523 * No longer sent: command rules, deny patterns and the cost cap were for
3624 * g1t's own agents. Kept optional for the runner, which still passes
4230 minutes: Record<string, number>;
4331 };
4432
45−/**
46− * A host only workflow jobs may reach: jobs of a trusted run (not a pull
47− * request from a fork), of the workflows named, in the environments
48− * named. Agents, checks, the merge queue and g1t.page builds never do.
49− */
50−export type WorkflowDomain = {
51− /** `api.example.com`, or `*.example.com` for its subdomains. */
52− domain: string;
53− /** Workflow files by name, such as `deploy.yml`. Empty: any workflow. */
54− workflows: string[];
55− /** Environments a job must name with `environment:`. Empty: any job. */
56− environments: string[];
57−};
58−
59−export type RegistryInfo = { id: string; name: string; hosts: string[] };
60−
6133 export type GuardrailsView = {
6234 workspace: GuardrailSettings;
6335 project: GuardrailSettings | null;
6537 /** g1t's defaults with the workspace's: what a project inherits. */
6638 inherited: Guardrails;
6739 effective: Guardrails;
68− g1tHosts: string[];
69− registries: RegistryInfo[];
7040 };
7141
7242 export interface GuardrailsApi {
+8−26
Loading the diff…
+1−1
Loading the diff…
+5−7
Loading the diff…
+1−15
Loading the diff…
+0−16
Loading the diff…
+29−78
Loading the diff…
+27−246
Loading the diff…
+3−2
Loading the diff…
+11−18
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…