diff --git a/README.md b/README.md index 15d8156da..1e42d3fc4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ spawn delete -c hetzner # Delete a server on Hetzner | `spawn --config ` | Load options from a JSON config file | | `spawn --steps ` | Comma-separated setup steps to enable | | `spawn --custom` | Show interactive size/region pickers | +| `spawn gcp --no-secure-boot` | Disable GCP Shielded VM (Secure Boot is on by default) | | `spawn ` | Show available clouds for an agent | | `spawn ` | Show available agents for a cloud | | `spawn matrix` | Full agent x cloud matrix | @@ -159,6 +160,22 @@ spawn claude gcp --beta tarball --beta parallel `--fast` enables `tarball`, `images`, and `parallel` (not `recursive`). +#### Secure Boot (GCP) + +GCP instances are provisioned as [Shielded VMs](https://cloud.google.com/security/shielded-cloud/shielded-vm) +**by default** — Secure Boot, vTPM, and integrity monitoring are all enabled. This +hardens the boot chain and is required for the Cloudflare (CF) skill to attest the VM. +GCP's default Ubuntu LTS images are Shielded-VM-compatible, so this works out of the box. + +Opt out for a custom image that is not UEFI/Secure-Boot-capable: + +```bash +spawn claude gcp --no-secure-boot +``` + +> Secure Boot is a GCP-only feature here. AWS spawns run on Lightsail, which does not +> expose Secure Boot; the flag is a no-op on other clouds. + #### Recursive Spawn Use `--beta recursive` to let spawned VMs create their own child VMs: diff --git a/packages/cli/src/__tests__/gcp-cov.test.ts b/packages/cli/src/__tests__/gcp-cov.test.ts index c0d72d282..7952afc0b 100644 --- a/packages/cli/src/__tests__/gcp-cov.test.ts +++ b/packages/cli/src/__tests__/gcp-cov.test.ts @@ -3,7 +3,7 @@ import { mockBunSpawn, mockClackPrompts } from "./test-helpers"; mockClackPrompts(); -import { DEFAULT_MACHINE_TYPE, DEFAULT_ZONE, getConnectionInfo } from "../gcp/gcp"; +import { buildShieldedArgs, DEFAULT_MACHINE_TYPE, DEFAULT_ZONE, getConnectionInfo } from "../gcp/gcp"; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -82,6 +82,31 @@ describe("gcp/getConnectionInfo", () => { }); }); +// ─── buildShieldedArgs (Secure Boot default) ───────────────────────────────── + +describe("gcp/buildShieldedArgs", () => { + it("enables Shielded VM (Secure Boot) by default", () => { + delete process.env.GCP_NO_SECURE_BOOT; + expect(buildShieldedArgs()).toEqual([ + "--shielded-secure-boot", + "--shielded-vtpm", + "--shielded-integrity-monitoring", + ]); + }); + + it("returns no shielded flags when GCP_NO_SECURE_BOOT=1", () => { + process.env.GCP_NO_SECURE_BOOT = "1"; + expect(buildShieldedArgs()).toEqual([]); + }); + + it("only opts out for the exact value '1' (any other value stays on)", () => { + process.env.GCP_NO_SECURE_BOOT = "0"; + expect(buildShieldedArgs()).toContain("--shielded-secure-boot"); + process.env.GCP_NO_SECURE_BOOT = "true"; + expect(buildShieldedArgs()).toContain("--shielded-secure-boot"); + }); +}); + // ─── promptMachineType ─────────────────────────────────────────────────────── describe("gcp/promptMachineType", () => { diff --git a/packages/cli/src/__tests__/unknown-flags.test.ts b/packages/cli/src/__tests__/unknown-flags.test.ts index 4f1a74632..af4b430be 100644 --- a/packages/cli/src/__tests__/unknown-flags.test.ts +++ b/packages/cli/src/__tests__/unknown-flags.test.ts @@ -87,6 +87,7 @@ describe("Unknown Flag Detection", () => { "--debug", "--name", "--reauth", + "--no-secure-boot", "--prune", "--json", "--yes", @@ -216,6 +217,7 @@ describe("KNOWN_FLAGS completeness", () => { "--clear", "--custom", "--reauth", + "--no-secure-boot", "--zone", "--region", "--machine-type", diff --git a/packages/cli/src/flags.ts b/packages/cli/src/flags.ts index e2be3de2c..b2734513c 100644 --- a/packages/cli/src/flags.ts +++ b/packages/cli/src/flags.ts @@ -24,6 +24,7 @@ export const KNOWN_FLAGS = new Set([ "--clear", "--custom", "--reauth", + "--no-secure-boot", "--zone", "--region", "--machine-type", diff --git a/packages/cli/src/gcp/gcp.ts b/packages/cli/src/gcp/gcp.ts index e29621f76..3eae69476 100644 --- a/packages/cli/src/gcp/gcp.ts +++ b/packages/cli/src/gcp/gcp.ts @@ -148,6 +148,30 @@ export const DEFAULT_ZONE = "us-central1-a"; export const DEFAULT_DISK_SIZE_GB = 40; +// ─── Shielded VM (Secure Boot) ─────────────────────────────────────────────── + +/** + * GCP Shielded VM flags, enabled by DEFAULT on every spawned instance. + * + * Secure Boot + a measured (vTPM) and integrity-monitored boot chain is what + * the Cloudflare (CF) skill needs to attest the VM, and it is good hygiene for + * every spawn. GCP's Ubuntu LTS images (the default here) are + * Shielded-VM-compatible, so turning this on does not break boots. + * + * Opt out with the `--no-secure-boot` CLI flag (sets `GCP_NO_SECURE_BOOT=1`), + * for the rare case of a custom image that is not UEFI/Secure-Boot-capable. + */ +export function buildShieldedArgs(): string[] { + if (process.env.GCP_NO_SECURE_BOOT === "1") { + return []; + } + return [ + "--shielded-secure-boot", + "--shielded-vtpm", + "--shielded-integrity-monitoring", + ]; +} + // ─── State ────────────────────────────────────────────────────────────────── interface GcpState { @@ -781,6 +805,7 @@ export async function createInstance( ] : []), `--metadata=ssh-keys=${sshKeysMetadata}`, + ...buildShieldedArgs(), `--project=${_state.project}`, "--quiet", ]; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 04256cd04..e81df6047 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -146,6 +146,7 @@ function checkUnknownFlags(args: string[]): void { console.error(` ${pc.cyan("--model, -m ")} Set the LLM model (e.g. openai/gpt-5.3-codex)`); console.error(` ${pc.cyan("--name")} Set the spawn/resource name`); console.error(` ${pc.cyan("--reauth")} Force re-prompting for cloud credentials`); + console.error(` ${pc.cyan("--no-secure-boot")} Disable GCP Shielded VM (Secure Boot, on by default)`); console.error(` ${pc.cyan("--config ")} Load config from JSON file`); console.error(` ${pc.cyan("--steps ")} Comma-separated setup steps to enable`); console.error(` ${pc.cyan("--repo ")} Clone a template repo and apply spawn.md`); @@ -928,6 +929,14 @@ async function main(): Promise { process.env.SPAWN_REAUTH = "1"; } + // Extract --no-secure-boot boolean flag — opt out of GCP Shielded VM + // (Secure Boot + vTPM + integrity monitoring), which is on by default. + const noSecureBootIdx = filteredArgs.indexOf("--no-secure-boot"); + if (noSecureBootIdx !== -1) { + filteredArgs.splice(noSecureBootIdx, 1); + process.env.GCP_NO_SECURE_BOOT = "1"; + } + // Extract --fast boolean flag — enables images + tarballs + parallel setup const fastIdx = filteredArgs.indexOf("--fast"); if (fastIdx !== -1) {