A host a sandbox was refused is a note on the run, never what its card says it is doing, and tools in a sandbox with only allowed hosts are told not to send usage reports home
4 files+55−30/4 viewed
| 83 | 83 | ||
| 84 | 84 | Each refused host appears once as a step of the run, such as | |
| 85 | 85 | `Blocked: example.com (not an allowed domain)`, on the run's page under | |
| 86 | − | **Agents**. If a run needs a host, allow it and start the work again. | |
| 86 | + | **Agents**. It is a note, not the run's state: the run carries on, and its | |
| 87 | + | card keeps showing what it is doing. If a run needs a host, allow it and | |
| 88 | + | start the work again. | |
| 89 | + | ||
| 90 | + | Tools that send usage reports home are told not to in a sandbox with only | |
| 91 | + | allowed hosts (`DO_NOT_TRACK=1`, and each common tool's own switch, such | |
| 92 | + | as `WRANGLER_SEND_METRICS=false`), so those reports are not refused and | |
| 93 | + | noted on every run. | |
| 87 | 94 | ||
| 88 | 95 | Setting **Only allowed hosts** to Open gives that project's sandboxes the | |
| 89 | 96 | whole internet, as before guardrails. |
| 80 | 80 | defaultBranch: "trunk", | |
| 81 | 81 | }); | |
| 82 | 82 | assert.equal(restricted.NODE_EXTRA_CA_CERTS, "/etc/cloudflare/certs/cloudflare-containers-ca.crt"); | |
| 83 | + | // Tools that would report usage home are told not to: it would be refused. | |
| 84 | + | assert.equal(restricted.WRANGLER_SEND_METRICS, "false"); | |
| 85 | + | assert.equal(restricted.DO_NOT_TRACK, "1"); | |
| 83 | 86 | // Open, as the operator's switch makes it: no certificate to trust. | |
| 84 | 87 | const open = harnessEnv(guard, {}, false); | |
| 85 | 88 | assert.equal(JSON.parse(open.GUARDRAILS).restrictNetwork, false); | |
| 86 | 89 | assert.equal(open.NODE_EXTRA_CA_CERTS, undefined); | |
| 90 | + | assert.equal(open.DO_NOT_TRACK, undefined); | |
| 87 | 91 | }); | |
| 88 | 92 | ||
| 89 | 93 | test("each refused host is one step, up to a limit", () => { |
| 108 | 108 | GIT_SSL_CAINFO: SYSTEM_BUNDLE, | |
| 109 | 109 | }; | |
| 110 | 110 | ||
| 111 | + | /** | |
| 112 | + | * Tools that report usage home by default, told not to: in a guarded | |
| 113 | + | * sandbox the report would only be refused, and a refused host is noted on | |
| 114 | + | * the run. `DO_NOT_TRACK` is the convention many tools follow; the rest are | |
| 115 | + | * the tools' own switches. | |
| 116 | + | */ | |
| 117 | + | export const QUIET_ENV: Record<string, string> = { | |
| 118 | + | DO_NOT_TRACK: "1", | |
| 119 | + | WRANGLER_SEND_METRICS: "false", | |
| 120 | + | NEXT_TELEMETRY_DISABLED: "1", | |
| 121 | + | ASTRO_TELEMETRY_DISABLED: "1", | |
| 122 | + | NUXT_TELEMETRY_DISABLED: "1", | |
| 123 | + | GATSBY_TELEMETRY_DISABLED: "1", | |
| 124 | + | STORYBOOK_DISABLE_TELEMETRY: "1", | |
| 125 | + | TURBO_TELEMETRY_DISABLED: "1", | |
| 126 | + | DOTNET_CLI_TELEMETRY_OPTOUT: "1", | |
| 127 | + | HOMEBREW_NO_ANALYTICS: "1", | |
| 128 | + | CHECKPOINT_DISABLE: "1", | |
| 129 | + | }; | |
| 130 | + | ||
| 111 | 131 | /** What a sandbox's guardrails come to for one run. */ | |
| 112 | 132 | export type RunGuard = { | |
| 113 | 133 | policy: Guardrails; | |
| ⋯ | |||
| 130 | 150 | defaultBranch: sandboxEnv.UPSTREAM_BRANCH ?? null, | |
| 131 | 151 | }), | |
| 132 | 152 | }; | |
| 133 | − | return restricted ? { ...vars, ...EGRESS_ENV } : vars; | |
| 153 | + | return restricted ? { ...vars, ...EGRESS_ENV, ...QUIET_ENV } : vars; | |
| 134 | 154 | } | |
| 135 | 155 | ||
| 136 | 156 | /** | |
| 24 | 24 | use crate::rows::SessionRow; | |
| 25 | 25 | use crate::{Work, optional}; | |
| 26 | 26 | ||
| 27 | + | /// How the runner starts a step that notes something about the sandbox | |
| 28 | + | /// rather than what the run is doing: a host it was refused (the runner's | |
| 29 | + | /// egress.ts `blockedStep`). | |
| 30 | + | const NOTE_PREFIXES: [&str; 1] = ["Blocked: "]; | |
| 31 | + | ||
| 32 | + | /// Where a report leaves the run: its newest step that says what the run | |
| 33 | + | /// is doing. A note (a refused host) stays in the steps but never stands | |
| 34 | + | /// for the run, which carries on past it. `None` when there is only notes. | |
| 35 | + | fn current_step(steps: &[String]) -> Option<String> { | |
| 36 | + | steps.iter().rev().find(|step| !NOTE_PREFIXES.iter().any(|prefix| step.starts_with(prefix))).cloned() | |
| 37 | + | } | |
| 38 | + | ||
| 27 | 39 | /// The most steps a run keeps; older ones fall off the start. | |
| 28 | 40 | const MAX_STEPS: u32 = 200; | |
| 29 | 41 | /// The most steps taken from one report. | |
| ⋯ | |||
| 343 | 355 | .as_deref() | |
| 344 | 356 | .map(|step| one_line(step, MAX_STEP_CHARS)) | |
| 345 | 357 | .filter(|step| !step.is_empty()) | |
| 346 | − | .or_else(|| steps.last().cloned()); | |
| 358 | + | .or_else(|| current_step(&steps)); | |
| 347 | 359 | let outcome = a | |
| 348 | 360 | .outcome | |
| 349 | 361 | .filter(|outcome| matches!(outcome, RunStatus::Succeeded | RunStatus::Failed)); | |
| ⋯ | |||
| 662 | 674 | use super::*; | |
| 663 | 675 | ||
| 664 | 676 | #[test] | |
| 677 | + | fn a_refused_host_is_noted_but_never_where_the_run_is() { | |
| 678 | + | let steps = |list: &[&str]| list.iter().map(|step| (*step).to_owned()).collect::<Vec<String>>(); | |
| 679 | + | assert_eq!(current_step(&steps(&["Running npm ci", "Blocked: sparrow.cloudflare.com (not an allowed domain)"])).as_deref(), Some("Running npm ci")); | |
| 680 | + | assert_eq!(current_step(&steps(&["Blocked: a.com (not an allowed domain)"])), None); | |
| 681 | + | assert_eq!(current_step(&steps(&["Blocked: a.com (not an allowed domain)", "Typecheck"])).as_deref(), Some("Typecheck")); | |
| 682 | + | assert_eq!(current_step(&[]), None); | |
| 683 | + | } | |
| 684 | + | ||
| 685 | + | #[test] | |
| 665 | 686 | fn steps_are_one_short_line() { | |
| 666 | 687 | assert_eq!(one_line(" Read\n src/lib.rs ", 40), "Read src/lib.rs"); | |
| 667 | 688 | let long = one_line(&"x".repeat(300), 10); | |