Commit graph

11 commits

Author SHA1 Message Date
Ahmed Abushagur
5103a763b4
fix: packer build — OOM kill and history builtin (#2274)
* fix: claude snapshot build — remove npm fallback from install command

The native install (curl | bash) succeeds but exits non-zero due to a
PATH warning. The || fallback then tries `npm install` which doesn't
exist on the "minimal" tier → exit 127.

Fix: replace npm fallback with binary existence check (same pattern
as hermes agent). If install exits non-zero but ~/.local/bin/claude
exists, the build succeeds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: snapshot cleanup and lookup — use name prefix instead of tags

DO Packer builder `tags` only apply to the temporary build droplet,
not the resulting snapshot image. Both the workflow cleanup step and
the CLI's findSpawnSnapshot() were querying by `tag_name` which
returned nothing — old snapshots piled up and the CLI couldn't find
existing snapshots.

Fix: filter by snapshot name prefix (`spawn-{agent}-`) instead of
tags, in both the workflow and the CLI. Remove misleading `tags`
from the Packer template. Add test cases for name-prefix filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: packer build failures — OOM kill + history builtin

Two issues introduced by PR #2271 (marketplace compliance):

1. Droplet downsized to s-1vcpu-1gb (1GB RAM) — Claude's native
   installer and zeroclaw's Rust build get OOM-killed. Restore
   s-2vcpu-2gb.

2. Cleanup provisioner uses `history -c` which is a bash builtin.
   Packer runs scripts with /bin/sh (dash on Ubuntu) which doesn't
   have it → exit 127 on ALL agents. Remove it — the .bash_history
   file deletion already handles persistent history.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:15:39 -05:00
Ahmed Abushagur
d77a067aa4
fix: snapshot cleanup + claude install (name-prefix filter) (#2273)
* fix: claude snapshot build — remove npm fallback from install command

The native install (curl | bash) succeeds but exits non-zero due to a
PATH warning. The || fallback then tries `npm install` which doesn't
exist on the "minimal" tier → exit 127.

Fix: replace npm fallback with binary existence check (same pattern
as hermes agent). If install exits non-zero but ~/.local/bin/claude
exists, the build succeeds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: snapshot cleanup and lookup — use name prefix instead of tags

DO Packer builder `tags` only apply to the temporary build droplet,
not the resulting snapshot image. Both the workflow cleanup step and
the CLI's findSpawnSnapshot() were querying by `tag_name` which
returned nothing — old snapshots piled up and the CLI couldn't find
existing snapshots.

Fix: filter by snapshot name prefix (`spawn-{agent}-`) instead of
tags, in both the workflow and the CLI. Remove misleading `tags`
from the Packer template. Add test cases for name-prefix filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:32:58 -08:00
A
c3cb98daab
feat: add DO Marketplace compliance to Packer build pipeline (#2271)
- Switch build droplet from s-2vcpu-2gb to s-1vcpu-1gb ($6/mo) per DO
  Marketplace recommendation for cross-size snapshot compatibility
- Add ufw firewall provisioner (deny incoming, allow SSH, enable)
- Replace basic apt-get clean with full DO Marketplace cleanup sequence:
  removes SSH authorized_keys, clears bash history, truncates /var/log,
  resets machine-id, and runs cloud-init clean so each launched droplet
  gets a fresh identity on first boot
- Add img_check.sh validation step (from digitalocean/marketplace-partners)
  to verify firewall active, no root password, and security posture before
  the snapshot is finalized — build fails if image doesn't meet requirements

Fixes #2269

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 00:20:35 -05:00
Ahmed Abushagur
955a6081c1
fix: Packer build region/size and PATH for agent installs (#2270)
* feat: restore Packer DO snapshot pipeline for fast agent boot

Restores the nightly Packer snapshot build pipeline (reverted in #2205)
that pre-bakes agent images as DigitalOcean snapshots. When a snapshot
exists on the user's account, droplet boot skips cloud-init and tarball
install entirely — cutting provisioning from ~10min to ~2min.

- Add `packer/digitalocean.pkr.hcl` HCL2 template with multi-region
  distribution, apt-lock wait, and snapshot marker
- Add `.github/workflows/packer-snapshots.yml` nightly build with
  matrix strategy, auto-cleanup of old snapshots, and injection-safe
  env var handling
- Add `findSpawnSnapshot()` to query DO API for pre-built snapshots
- Add `waitForSshOnly()` for snapshot boots (skip cloud-init wait)
- Modify `createServer()` to accept optional `snapshotId` param
- Wire snapshot detection in DO `main.ts` orchestrator
- Add `skipAgentInstall` to `CloudOrchestrator` interface to skip
  tarball + install steps when booting from snapshot
- Add 5 unit tests for snapshot lookup (happy path, empty, error,
  invalid ID, network failure)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use repo-root-relative path for tier scripts in Packer template

Packer resolves script paths relative to cwd (repo root), not relative
to the .pkr.hcl file. Changed `scripts/tier-*.sh` to
`packer/scripts/tier-*.sh`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: Packer build region/size and PATH for agent installs

Two issues causing build failures:

1. `s-2vcpu-4gb` not available in `nyc3` — changed build region to
   `sfo3` and size to `s-2vcpu-2gb` (universally available, cheaper,
   sufficient for building snapshots)

2. Claude install puts binary in `~/.local/bin` which isn't in PATH
   during Packer provisioning — added full PATH to environment_vars
   on both the install and marker provisioners so agent binaries and
   subsequent scripts can find each other

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:45:39 -05:00
Ahmed Abushagur
e7b6b0b9fd
fix: Packer tier script path relative to repo root (#2266)
* feat: restore Packer DO snapshot pipeline for fast agent boot

Restores the nightly Packer snapshot build pipeline (reverted in #2205)
that pre-bakes agent images as DigitalOcean snapshots. When a snapshot
exists on the user's account, droplet boot skips cloud-init and tarball
install entirely — cutting provisioning from ~10min to ~2min.

- Add `packer/digitalocean.pkr.hcl` HCL2 template with multi-region
  distribution, apt-lock wait, and snapshot marker
- Add `.github/workflows/packer-snapshots.yml` nightly build with
  matrix strategy, auto-cleanup of old snapshots, and injection-safe
  env var handling
- Add `findSpawnSnapshot()` to query DO API for pre-built snapshots
- Add `waitForSshOnly()` for snapshot boots (skip cloud-init wait)
- Modify `createServer()` to accept optional `snapshotId` param
- Wire snapshot detection in DO `main.ts` orchestrator
- Add `skipAgentInstall` to `CloudOrchestrator` interface to skip
  tarball + install steps when booting from snapshot
- Add 5 unit tests for snapshot lookup (happy path, empty, error,
  invalid ID, network failure)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use repo-root-relative path for tier scripts in Packer template

Packer resolves script paths relative to cwd (repo root), not relative
to the .pkr.hcl file. Changed `scripts/tier-*.sh` to
`packer/scripts/tier-*.sh`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:40:57 -08:00
Ahmed Abushagur
cefcd56327
feat: restore Packer DO snapshot pipeline for fast agent boot (#2262)
Restores the nightly Packer snapshot build pipeline (reverted in #2205)
that pre-bakes agent images as DigitalOcean snapshots. When a snapshot
exists on the user's account, droplet boot skips cloud-init and tarball
install entirely — cutting provisioning from ~10min to ~2min.

- Add `packer/digitalocean.pkr.hcl` HCL2 template with multi-region
  distribution, apt-lock wait, and snapshot marker
- Add `.github/workflows/packer-snapshots.yml` nightly build with
  matrix strategy, auto-cleanup of old snapshots, and injection-safe
  env var handling
- Add `findSpawnSnapshot()` to query DO API for pre-built snapshots
- Add `waitForSshOnly()` for snapshot boots (skip cloud-init wait)
- Modify `createServer()` to accept optional `snapshotId` param
- Wire snapshot detection in DO `main.ts` orchestrator
- Add `skipAgentInstall` to `CloudOrchestrator` interface to skip
  tarball + install steps when booting from snapshot
- Add 5 unit tests for snapshot lookup (happy path, empty, error,
  invalid ID, network failure)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 16:32:05 -08:00
Ahmed Abushagur
4ac19a375a
fix: capture claude symlink target + verify PATH (#2245)
* fix: tarball workflow failures (root ownership, swapfile, hermes TTY)

- Use sudo mv + chown for tarball in release step (root-owned from capture)
- Skip swapfile creation if /swapfile already exists (GitHub Actions runners)
- Tolerate hermes setup wizard failure when /dev/tty unavailable in CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: capture claude symlink target in tarball + fix verify PATH

The claude installer creates a symlink at ~/.local/bin/claude pointing
to ~/.local/share/claude/versions/X.Y.Z. The capture script was missing
~/.local/share/claude/, causing a broken symlink in the tarball.

Also add ~/.npm-global/bin to the verify PATH check for claude (npm
fallback install path).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-06 10:55:09 -05:00
Ahmed Abushagur
ba9690ea23
fix: tarball workflow failures (root ownership, swapfile, hermes TTY) (#2240)
- Use sudo mv + chown for tarball in release step (root-owned from capture)
- Skip swapfile creation if /swapfile already exists (GitHub Actions runners)
- Tolerate hermes setup wizard failure when /dev/tty unavailable in CI

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 05:48:46 -05:00
Ahmed Abushagur
8072c084c2
feat: pre-built agent tarballs for fast install (#2232)
* feat: pre-built agent tarballs on GitHub Releases for fast install

Adds a nightly GitHub Actions workflow that builds and uploads agent
tarballs to rolling GitHub Releases. During provisioning, the CLI now
attempts to download and extract a tarball before falling back to live
install. Priority chain: snapshot > tarball > live install.

- New workflow: .github/workflows/agent-tarballs.yml
- New capture script: packer/scripts/capture-agent.sh
- New module: packages/cli/src/shared/agent-tarball.ts
- Orchestrate tries tarball first on non-local clouds
- Skip tarball when using DO snapshot (skipTarball flag)
- Tests for tarball install + orchestration integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use global.fetch mock pattern and address security review

- Use `global.fetch = mock(...)` instead of `spyOn(globalThis, "fetch")`
  to match codebase convention and fix CI mock interception
- Add URL validation regex to reject shell metacharacters (CRITICAL)
- Add agent name validation in workflow input (MEDIUM)
- Add `jq has()` check before executing install commands (CRITICAL)
- Use `tar -T` instead of unquoted word-splitting in capture-agent.sh (MEDIUM)
- Resolve merge conflicts with upstream/main (keep Docker fields, adapt
  to simplified DO flow, bump version to 0.15.0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use globalThis.fetch for testability in CI

Bun's native fetch binding doesn't go through global.fetch property
lookup, so global.fetch = mock(...) doesn't intercept it. Using
globalThis.fetch explicitly ensures the mock interception works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing packer dependencies and harden install command safety

- Add packer/agents.json (agent tier + install command definitions)
- Add packer/scripts/tier-{minimal,node,bun,full}.sh (dependency scripts)
- Add basic command safety check rejecting suspicious patterns
- Document packer/agents.json as a trust boundary requiring PR review

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): fix npm prefix mismatch, add apt-get update, cleanup

- Add apt-get update -y before apt-get install in all tier scripts
- Add --prefix ~/.npm-global to npm install commands in agents.json
  so installed packages land where capture-agent.sh expects them
- Rename misleading MARKER_DIR → MARKER_FILE in capture-agent.sh
- Remove stale comment referencing packer snapshots in workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): detect empty agent installs in capture script

The "no files found" check was dead code — the marker file is always
created before filtering, so FILTERED_FILE always had at least one
entry. Now we count non-marker entries to catch cases where the agent
install silently fails and no actual files are on disk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): use bare fetch() for Bun mock compatibility in CI

In Bun, global.fetch = mock(...) overrides bare fetch() calls but NOT
globalThis.fetch() calls. Every other source file in the codebase uses
bare fetch() and their mocks work fine in CI. Switch to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): use dependency injection for fetch in tests

Bun's global.fetch mock doesn't reliably intercept bare fetch() calls
across all Bun versions in CI. Instead of fighting the runtime, accept
an optional fetchFn parameter (defaults to fetch) and pass mock fetch
directly in tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): bypass mock.module bleed in agent-tarball tests

orchestrate.test.ts uses mock.module("../shared/agent-tarball", ...)
which is process-global in Bun and bleeds into agent-tarball.test.ts.
Import via URL (import.meta.url resolution) to bypass the specifier-
based mock matching and get the real module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tarballs): eliminate mock.module bleed between test files

Bun's mock.module is process-global — orchestrate.test.ts mocking
agent-tarball poisoned agent-tarball.test.ts (the mock function
ignored the fetchFn parameter and always returned false).

Fix: make tryTarballInstall injectable via OrchestrationOptions.
orchestrate.test.ts passes the mock directly via options instead
of using mock.module. agent-tarball.test.ts imports the real module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tests): mock Bun.which in credential priority tests

Tests assumed no cloud CLIs were installed, but machines with hcloud/
doctl would get "CLI installed" hint overrides, failing the assertion.
Spy on Bun.which to return null so tests are environment-independent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: fix import ordering after rebase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* security: add curl domain allowlist and expand command blocklist

Addresses security review findings:
- Add domain allowlist for curl/wget targets (claude.ai, opencode.ai,
  raw.githubusercontent.com, registry.npmjs.org, crates.io, github.com)
- Expand suspicious command blocklist (python -c, perl -e, ruby -e, dd, /dev/)
- Document 4-layer security model in workflow comments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* security: add rm -rf to command blocklist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 04:49:39 -05:00
Ahmed Abushagur
07c2c08e3a
revert: remove Packer snapshot pipeline (#2205)
DO snapshots are private and account-scoped — users on different
accounts cannot see snapshots built by the CI token. Docker images
are the better approach for cross-account pre-built agents.

Removes: packer/, packer-snapshots workflow, snapshot lookup code,
and snapshot test. Reverts DO CLI to plain cloud-init flow.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:48:52 -05:00
Ahmed Abushagur
ed98a59318
feat(digitalocean): Packer nightly snapshot pipeline for fast boot (#2198)
* feat(digitalocean): Packer nightly snapshot pipeline for fast boot

Add pre-built Packer snapshots for DigitalOcean droplets. Instead of
10-20 min cloud-init + agent install on every boot, snapshot-based
droplets boot in ~2-3 min (SSH only, agent pre-installed).

- Packer HCL2 template with parametrized agent/tier builds
- Agent build matrix (packer/agents.json) for all 7 agents
- Tier scripts mirroring cloud-init.ts package tiers
- Nightly GitHub Actions workflow (4 AM UTC, max-parallel: 3)
- Automatic cleanup: keeps only latest snapshot per agent
- CLI: findSpawnSnapshot() looks up pre-built images via DO API
- CLI: waitForSshOnly() skips cloud-init when using snapshots
- CLI: createServer() accepts optional snapshotId, skips user_data
- CLI: main.ts routes to fast path when snapshot detected
- Tests for findSpawnSnapshot() (5 cases, all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(packer): use var-file for install_commands to avoid shell quoting issues

The previous approach passed install_commands as `-var` inline, but
GitHub Actions expands `${{ }}` before shell evaluation — JSON arrays
with `|`, `&&`, and `"` characters break shell quoting.

Fix: generate a `.auto.pkrvars.json` file (auto-loaded by Packer)
using jq with --argjson for safe JSON handling. Also route all
`${{ inputs }}` and `${{ matrix }}` values through env vars to
prevent script injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:47:46 -08:00