Skip to content

Harden boot (failure fallback, PATH shims, TMPDIR on disk) + add test suite & CI#2

Open
garrytan wants to merge 2 commits into
chrysb:mainfrom
garrytan:upstream-hardening-tests
Open

Harden boot (failure fallback, PATH shims, TMPDIR on disk) + add test suite & CI#2
garrytan wants to merge 2 commits into
chrysb:mainfrom
garrytan:upstream-hardening-tests

Conversation

@garrytan

Copy link
Copy Markdown

We've been running this template as a 24/7 service on Render (via a fork) and hit a few production failure modes worth fixing upstream. This PR is the generic hardening + the test suite that locks it in — no fork-specific changes.

Commit 1 — boot hardening

  • Crash-loop lockout. CMD ["alphaclaw", "start"] means any startup failure restart-loops the container, which takes the Render Shell tab down with it — the one tool you need to debug. Boot now goes through start.sh: it tees the boot log to /data/start.log (persistent disk) and, if alphaclaw exits, execs a tiny failure-status server so the deploy stays Live (/health 200) and the Shell tab stays reachable. The failure page deliberately renders no logs/env (it's publicly reachable; logs can contain secrets).
  • spawn openclaw ENOENT. alphaclaw spawns openclaw by bare name. Your ENV PATH prepend covers the normal case; this adds /usr/bin wrappers + /usr/local/bin symlinks (Render's runtime env can strip PATH — start.sh also re-exports it), plus a build-time openclaw --version smoke check that fails the image build early if the install is broken.
  • Ephemeral /tmp churn. A 24/7 agent writes scratch forever; TMPDIR/TEMP/TMP=/data/tmp routes it to the persistent disk. Bare /tmp is left untouched (no symlink/bind-mount — Render containers aren't privileged, and hardcoded-/tmp code keeps working). Since the disk mount shadows build-time mkdir, start.sh recreates /data/tmp (sticky bit) every boot.
  • debug-start.sh — optional inert-container mode for the worst case (binds :3000 so Render goes Live, keeps PID 1 alive, dumps env to /data/debug.log). Documented in the README troubleshooting section.

Commit 2 — tests + CI

  • unit — exercises the real failure-server.js in a subprocess; a security test plants sentinel secrets in the env and asserts no HTTP response ever contains them.
  • contract — fast static locks on the load-bearing config (PATH prepend, TMPDIR routing, sticky-bit mkdir, tini/CMD wiring, never touching bare /tmp, and the exact-version alphaclaw pin — a drifting spec never changes package.json, so Docker's npm-install layer cache silently keeps shipping a stale alphaclaw; we got bitten by this in the field).
  • e2e — builds the real image and boots it Render-style, twice: docker.bats with a tmpfs over /data (empty disk, like Render's mount) asserting every invariant at runtime; gateway-ready.bats with a seeded onboarded /data asserting OpenClaw reaches a genuinely working state — [gateway] ready in the logs with usage-tracker initialized. Render marks a service Live as soon as /health answers (which even the failure fallback satisfies), so "Live" alone proves nothing about the gateway; this catches the looks-deployed-but-dead class.
  • CI runs unit+contract and the docker e2e on push/PR.

All verified against this repo's exact @chrysb/alphaclaw@0.9.18 npm dependency: 6 unit + 22 contract + 13 e2e passing, gateway reaches ready in the built image.

🤖 Generated with Claude Code

garrytan and others added 2 commits July 11, 2026 07:30
Three production issues this addresses, all observed running this
template as a 24/7 service on Render:

1. Crash-looping lockout. CMD ran alphaclaw directly, so any startup
   failure restart-looped the container and took the Render Shell tab
   down with it — the one tool needed to debug. Boot now goes through
   start.sh: it tees the boot log to /data/start.log on the persistent
   disk and, if alphaclaw ever exits, execs a minimal failure-status
   server so the deploy stays Live (/health 200) and the Shell tab
   stays reachable. The failure page renders no logs or env values —
   they can contain secrets and the page is publicly reachable.

2. spawn openclaw ENOENT. alphaclaw spawns `openclaw` by bare name and
   inherits PATH. The ENV prepend covers the normal path; the /usr/bin
   wrappers and /usr/local/bin symlinks keep the binaries resolvable
   even if the runtime env is munged, and the build-time
   `openclaw --version` smoke check fails the image build early if the
   install is broken. Render's runtime can also strip PATH, so start.sh
   re-exports it (belt and suspenders).

3. Ephemeral /tmp churn. A 24/7 service writes scratch data forever;
   routing TMPDIR/TEMP/TMP to /data/tmp puts it on the persistent disk
   instead of the container's ephemeral layer. Bare /tmp is deliberately
   left untouched (no symlink or bind-mount — Render containers aren't
   privileged, and hardcoded-/tmp code keeps working). /data is a
   runtime disk mount that shadows build-time mkdir, so start.sh
   (re)creates /data/tmp with the sticky bit on every boot.

debug-start.sh is an optional inert-container mode for when even the
fallback isn't enough (bind :3000, keep PID 1 alive, dump env to
/data/debug.log); README documents both debug paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three layers, split by cost (details in tests/README.md):

- unit: exercises the real failure-server.js in a subprocess — routing,
  /health, and a security property test that plants sentinel secrets in
  the env and asserts no HTTP response ever contains them.
- contract: fast static locks on the load-bearing config — the PATH
  prepend, TMPDIR routing, sticky-bit mkdir on boot, tini/CMD wiring,
  never touching bare /tmp, and the exact-version alphaclaw pin (a
  drifting spec never changes package.json, so the Docker npm-install
  layer cache silently keeps shipping a stale alphaclaw).
- e2e: builds the real image and boots it Render-style, twice.
  docker.bats uses a tmpfs over /data (empty disk, like Render's mount)
  and asserts every invariant at runtime. gateway-ready.bats seeds an
  onboarded /data (gateway.mode=local) and asserts OpenClaw reaches a
  genuinely working state — "[gateway] ready" in the logs with the
  usage-tracker plugin initialized. Render marks a service Live as soon
  as /health answers, which even the failure fallback satisfies, so
  "Live" alone proves nothing about the gateway; this makes CI catch
  the looks-deployed-but-dead class of failure.

CI (.github/workflows/test.yml) runs unit+contract and the docker e2e
on every push and PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant