Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ spawn delete -c hetzner # Delete a server on Hetzner
| `spawn <agent> <cloud> --config <file>` | Load options from a JSON config file |
| `spawn <agent> <cloud> --steps <list>` | Comma-separated setup steps to enable |
| `spawn <agent> <cloud> --custom` | Show interactive size/region pickers |
| `spawn <agent> gcp --no-secure-boot` | Disable GCP Shielded VM (Secure Boot is on by default) |
| `spawn <agent>` | Show available clouds for an agent |
| `spawn <cloud>` | Show available agents for a cloud |
| `spawn matrix` | Full agent x cloud matrix |
Expand Down Expand Up @@ -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:
Expand Down
27 changes: 26 additions & 1 deletion packages/cli/src/__tests__/gcp-cov.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -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", () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/__tests__/unknown-flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe("Unknown Flag Detection", () => {
"--debug",
"--name",
"--reauth",
"--no-secure-boot",
"--prune",
"--json",
"--yes",
Expand Down Expand Up @@ -216,6 +217,7 @@ describe("KNOWN_FLAGS completeness", () => {
"--clear",
"--custom",
"--reauth",
"--no-secure-boot",
"--zone",
"--region",
"--machine-type",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const KNOWN_FLAGS = new Set([
"--clear",
"--custom",
"--reauth",
"--no-secure-boot",
"--zone",
"--region",
"--machine-type",
Expand Down
25 changes: 25 additions & 0 deletions packages/cli/src/gcp/gcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -781,6 +805,7 @@ export async function createInstance(
]
: []),
`--metadata=ssh-keys=${sshKeysMetadata}`,
...buildShieldedArgs(),
`--project=${_state.project}`,
"--quiet",
];
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function checkUnknownFlags(args: string[]): void {
console.error(` ${pc.cyan("--model, -m <id>")} 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 <path>")} Load config from JSON file`);
console.error(` ${pc.cyan("--steps <list>")} Comma-separated setup steps to enable`);
console.error(` ${pc.cyan("--repo <slug|url>")} Clone a template repo and apply spawn.md`);
Expand Down Expand Up @@ -928,6 +929,14 @@ async function main(): Promise<void> {
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) {
Expand Down
Loading