Skip to content

installer: fix empty -email flag breaking the default install, and make re-runs safe - #43

Merged
TeoSlayer merged 1 commit into
mainfrom
fix/installer-empty-email-flag-and-rerun
Jul 27, 2026
Merged

installer: fix empty -email flag breaking the default install, and make re-runs safe#43
TeoSlayer merged 1 commit into
mainfrom
fix/installer-empty-email-flag-and-rerun

Conversation

@TeoSlayer

Copy link
Copy Markdown
Contributor

Two launch-blocking defects found in container testing.

F1 — malformed systemd unit on the default install (CRITICAL)

The advertised non-interactive curl -fsSL https://pilotprotocol.network/install.sh | sh sets no email, and the unit rendered -email with an empty value:

  -identity /home/<user>/.pilot/identity.json \
  -email  \
  -encrypt

Go's flag parser consumed the next flag (-encrypt) as the email value, so the daemon failed validation every 5s forever — daemon start: invalid email: email address must contain @pilot-daemon.service: Failed with result 'exit-code'. The node never registered, and -encrypt was silently swallowed.

Fix: every optional argument is emitted as a complete flag+value pair or not at all. Omitting -email is the well-defined path — the daemon falls back to ~/.pilot/account.json and, failing that, synthesises <fingerprint>@nodes.pilotprotocol.network, exactly as the installer header already documents. The macOS LaunchAgent plist had the same empty-argument shape and is fixed identically. -hostname / -public were already guarded against this.

F2 — re-running the installer failed on a systemd host

pilot-updater is now auto-enabled and RUNNING after install, so cp over its live binary failed with ETXTBSY ("Text file busy"), aborting mid-install under set -e with binaries partially replaced.

Fix: binaries install via a same-directory temp file + rename(2) — immune to ETXTBSY, and it cannot leave a truncated binary at the real path. Managed services are stopped before the swap and restarted after, so the new image is actually picked up. The whole staged payload is resolved and validated before anything is replaced, so an incomplete download leaves the existing install untouched.

Coupled fix — re-run is now a real repair path

The update path used to exit 0 before the unit was ever rewritten, so re-running could never repair a host installed by a buggy installer — which is the only remedy available to every existing install (historically pinned at its install-time version).

Unit/plist generation, service restart and PATH setup now run on both paths; only the first-run onboarding text is skipped on upgrade. config.json and auto-update.json stay fresh-install-only so a re-run never clobbers operator settings, and -hostname / -public already baked into an existing unit/plist are preserved when the matching env var is not supplied.

A crashlooping unit reports activating/failed rather than active, so the restart set includes an enabled-but-failed unit — precisely the state the -email bug produced.

Container proof

ubuntu:24.04 + systemd as PID 1, arm64, non-root user with passwordless sudo:

Assertion Result
Both defects reproduced against current main -email \ + invalid email crashloop; re-run REAL_EXIT=1 on ETXTBSY
Fresh non-interactive install, no email → no dangling -email unit ends -encrypt, zero -email occurrences
…daemon starts, stays up (3 checks over ~50s) active, NRestarts=0, stable MainPID
…and registers daemon registered node_id=251638 addr=0:0000.0003.D6F6, synthetic email
PILOT_EMAIL set → renders correctly and works -encrypt -email teodor+ctest@vulturelabs.io, active, NRestarts=0, registered
Re-run installer over running services (5×) exit 0 every time, binaries swapped, daemon + updater active after
systemctl is-enabled pilot-updater enabled and active throughout
Broken install repaired by one re-run unit fixed, -hostname pilot-ctest-repair preserved, daemon active + registered
bash -n / sh -n / shellcheck -s sh all clean

All test nodes (251638–251642) were pilotctl deregister'd and confirmed absent — pilotctl lookup returns node not found, pilotctl find pilot-ctest-repair returns hostname not found.

🤖 Generated with Claude Code

…ke re-runs safe

Two launch-blocking defects found in container testing.

F1 — the default non-interactive install produced a malformed systemd unit.
`curl -fsSL https://pilotprotocol.network/install.sh | sh` sets no email, and
the unit rendered `-email` with an EMPTY value:

    -identity /home/u/.pilot/identity.json \
    -email  \
    -encrypt

Go's flag parser then consumed the next flag as the email value, so the daemon
failed validation every 5s forever ("daemon start: invalid email: email address
must contain @") and `-encrypt` was silently swallowed. The node never
registered.

Every optional argument is now emitted as a complete flag+value pair or not at
all. Omitting `-email` is the well-defined path: the daemon falls back to
~/.pilot/account.json and, failing that, synthesises
<fingerprint>@nodes.pilotprotocol.network — exactly as the installer header
already documents. The macOS LaunchAgent plist had the same empty-argument
shape and is fixed identically; -hostname/-public were already guarded.

F2 — re-running the installer died on a systemd host, blocking the upgrade
path. pilot-updater is now auto-enabled and RUNNING after install, so `cp` over
its live binary failed with ETXTBSY ("Text file busy"), aborting mid-install
under `set -e` with binaries partially replaced.

Binaries are now installed via a same-directory temp file + rename(2), which is
immune to ETXTBSY and cannot leave a truncated binary at the real path. Managed
services are stopped before the swap and restarted after, so the new image is
actually picked up. The whole staged payload is resolved and validated before
anything is replaced, so an incomplete download leaves the existing install
untouched.

Coupled fix: the update path used to `exit 0` before the unit was ever
rewritten, so re-running could never repair a host installed by a buggy
installer — which is the only remedy available to every existing install
(historically pinned at its install-time version). Unit/plist generation, the
service restart and PATH setup now run on BOTH paths; only the first-run
onboarding text is skipped on upgrade. config.json and auto-update.json remain
fresh-install-only so a re-run never clobbers operator settings, and
-hostname/-public already baked into an existing unit/plist are preserved when
the matching env var is not supplied.

A crashlooping unit reports "activating"/"failed" rather than "active", so the
restart set includes an enabled-but-failed unit — that is precisely the state
the -email bug produced, and repairing it must bring it back up.

Verified in an ubuntu:24.04 container with systemd as PID 1 (arm64):
  - reproduced both defects against the current main installer
  - fresh non-interactive install, no email: unit has no -email, daemon starts,
    stays up across three checks over ~50s with NRestarts=0, and registers
  - PILOT_EMAIL set: renders `-encrypt -email <addr>`, daemon healthy, registers
  - installer re-run 5x over running services: exit 0 every time, binaries
    swapped, daemon + updater healthy after
  - broken install repaired by a single re-run, with -hostname preserved
  - systemctl is-enabled/is-active pilot-updater: enabled + active throughout
  - bash -n, sh -n, shellcheck -s sh all clean
All test nodes were deregistered and confirmed absent from the registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TeoSlayer
TeoSlayer merged commit 51894f1 into main Jul 27, 2026
@TeoSlayer
TeoSlayer deleted the fix/installer-empty-email-flag-and-rerun branch July 27, 2026 16:02
TeoSlayer added a commit to pilot-protocol/website that referenced this pull request Jul 27, 2026
…un fixes) (#176)

The sync-install-sh workflow updated R2 (the live path) from
pilot-protocol/release#43 but flagged public/install.sh — the Cloudflare Pages
fallback — as drifted. It was still serving the pre-fix script, which renders a
systemd unit with an empty `-email` value and crashloops the daemon, and which
aborts with ETXTBSY when re-run over a running pilot-updater.

Copies the canonical script over the fallback. Byte-identical to
pilot-protocol/release@main install.sh (sha256 906deea1...).

Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
TeoSlayer added a commit to pilot-protocol/pilotprotocol that referenced this pull request Jul 28, 2026
The canonical-drift job added in #432 went red on main immediately after that
PR merged, which is the guard doing exactly its job on its first real run.

pilot-protocol/release#43 landed overnight — "fix empty -email flag breaking
the default install, and make re-runs safe" — so canonical moved from 988ddeb
to 906deea while this repo's copy stayed at 988ddeb.

That follow-up is a direct consequence of #432/release#42: once the installer
actually started the services instead of only writing the unit files, a unit
generated with an empty `-email ` flag became a real failure rather than dead
config. release#43 adds a privilege gate (CAN_PRIV / PILOT_SUDO, never
prompting), recovers an existing email from account.json/config.json on every
run so re-runs don't drop the operator's address, and skips synthesised
@nodes.pilotprotocol.network placeholders so they can't harden into an
override that defeats a later `pilotctl set-email`.

install.sh here is now byte-identical to release@51894f1 (sha256 906deea1…),
which is also what the live URL serves.

Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
Co-authored-by: Claude Opus 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.

2 participants