Skip to content

Add headless deploy guide - #986

Merged
platypii merged 2 commits into
masterfrom
docs/headless-deploy
Aug 24, 2026
Merged

Add headless deploy guide#986
platypii merged 2 commits into
masterfrom
docs/headless-deploy

Conversation

@platypii

@platypiiplatypii commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Adds docs/HEADLESS.md: how to run HypAware on CI runners and headless servers with a pre-minted enrollment token (hyp remote mint, #969). Covers the one-time mint, the per-run join / foreground daemon / sync flush recipe, a GitHub Actions example, long-lived servers, and troubleshooting. Links it from the README docs list.

@platypiiplatypii added the neutral:review Delegate this PR to neutral for a review pass (approve or request changes; never merges) label Aug 21, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review: changes requested

Docs-only change (README link + new docs/HEADLESS.md), reviewed at 70ea5c7. I verified every command, flag, env var, path, and anchor the guide tells a reader to use against the tree at this commit. Most of it checks out. Four claims do not, and three of them are the kind that will send a headless operator down the wrong path.

What verified clean (no action needed): hyp remote mint with --label / --expires-days and the 365-day default (src/core/cli/command_args.js:174,184); stdout-carries-only-the-token (src/core/cli/remote_commands.js:1147-1149); hyp join <url> --no-daemon reading the token from stdin (src/core/commands/central.js:80-85, src/core/cli/core_commands.js:341-342); hyp daemon run --foreground (src/core/cli/core_commands.js:597-600); hyp sync --yes (src/core/cli/core_commands.js:632-636); the "no first-sync review hold on a token join" claim (writeFirstSyncHoldMarker is imported only by the login path, src/core/cli/remote_commands.js:30, and LLP 0298 D1 says so explicitly); one-shared-gateway (LLP 0298 D2); the HTTP 404 troubleshooting line (src/core/cli/remote_commands.js:1104); Node 22.12 (package.json:66-68); the #hyp-remote-mint anchor (docs/CLI_REFERENCE.md:1226); package name and hyp bin (package.json:6-9). No em dash anywhere in either file. test/core/tracked-files.test.js passes.


Finding 1 (medium): "nothing on the server needs cleanup" contradicts what a re-mint actually does

docs/HEADLESS.md:39-40

The token never rotates. When it nears expiry, mint a new one and swap the CI secret; nothing on the server needs cleanup.

Re-minting is not a swap of one credential in place. Per LLP 0298 D2 (#shared-gateway), the minted token is bound to one gateway row, created once at mint time, and the code says so where it explains why the banner must not be captured into the secret store:

src/core/cli/remote_commands.js:1143-1146

 // the token is never shown again, and re-minting creates a second gateway
// row (LLP 0298 D2).

So the consequence of the advice as written is exactly the thing this guide sold three paragraphs earlier at docs/HEADLESS.md:19-21 ("a pipeline's runs stay grouped together"): after an expiry swap, runs before and after the swap sit under two different gateways, and the old row remains server-side. "Nothing needs cleanup" is the opposite of the state the reader is left in.

Recommend: say that minting a replacement creates a new gateway row, so runs before and after the swap group under different gateways, and name whatever the server-side disposition of the old row is (or say it is left in place).

Finding 2 (medium): the join troubleshooting bullet describes failures hyp join cannot produce

docs/HEADLESS.md:116-118

A join that hangs or exits nonzero usually means the URL is a query target (.../v1/mcp) rather than the server base, or the token has expired; mint a fresh token and retry.

runJoin (src/core/commands/central.js:46-180) never contacts the server. It syntax-checks the URL (central.js:58-66, protocol only, /v1/mcp passes), reads the token, validates the seed config, writes it, and with --no-daemon returns 0 at central.js:166-169. Neither a query-target URL nor an expired token can make join hang or exit nonzero: both succeed at join and surface later, at daemon bootstrap. A reader who follows this bullet will re-mint a perfectly good token and see the same symptom.

The failure that genuinely hangs on a CI runner is the one the bullet omits: with no positional token, no --token-file, and a non-TTY stdin that nothing was piped into, join blocks in readAllStdin (central.js:80-85) with no output.

Recommend: rewrite the bullet around the real causes: a join that hangs is waiting on stdin (nothing piped); a wrong URL or an expired token shows up as a daemon bootstrap failure after join succeeded, not as a join failure.

Finding 3 (medium): "survives reboots" is unqualified, in the one section where the qualification matters

docs/HEADLESS.md:100-105

Without --no-daemon, join installs and starts the daemon under launchd or systemd, and it survives reboots.

On Linux the installer writes a systemd user unit: ~/.config/systemd/user/ (src/core/daemon/platform.js:53-59), WantedBy=default.target (src/core/daemon/linux.js:141), enabled with systemctl --user enable (src/core/daemon/linux.js:214,241). A user manager on a headless server is not started at boot and is torn down with the last session unless loginctl enable-linger <user> is set. Nothing in this repo sets or mentions linger (grep for linger across the tree returns only unrelated prose). macOS installs a LaunchAgent, which likewise needs a logged-in user session, not a bare booted machine.

This is the "Long-lived headless machines" section, so it is precisely the reader with no interactive session who is being told the daemon comes back after a reboot.

Recommend: qualify it, e.g. that the Linux unit is a systemd user unit and a headless host needs loginctl enable-linger for it to start at boot, and note the LaunchAgent's session requirement on macOS.

Finding 4 (low): hyp status does not report a gateway

docs/HEADLESS.md:112-113

hyp status on the runner reports whether recording is active and which gateway the machine forwards to.

renderStatusText (src/core/commands/status.js:591 onward) prints active plugins, sources, sinks (instance / plugin / kind, plus a [central - locked] provenance tag), clients, cache, and diagnostics. It never prints a server URL or a gateway id, and the JSON report carries no such field either. The nearest true statement is that the sinks block shows a central sink and its provenance.

Recommend: the phrasing docs/TEAM_SETUP.md:66-68 already uses ("whether recording is active, what is shared with your team versus kept on your machine") is accurate and consistent.


Non-blocking note

docs/HEADLESS.md:82-85: backgrounding hyp daemon run --foreground & inside a run: block leaves the daemon holding the step's inherited stdout/stderr. This mirrors LLP 0298 D4 and docs/CLI_REFERENCE.md:1247-1258 verbatim, so it is not a defect introduced here, and I am not asking for a change. Flagging only because a GitHub Actions example is copy-pasted more literally than a reference snippet, and redirecting the daemon's output is cheap insurance.

Verdict

Changes requested. Findings 1-3 are confidently-wrong statements that would misdirect the reader (rotation hygiene, a troubleshooting step that fixes nothing, and reboot durability on the exact host class the section addresses). Finding 4 is a one-line wording fix. Nothing here touches code or CI, and the structure, scope, and cross-links of the guide are good; this is a text-accuracy pass, not a rewrite.

@philcunliffephilcunliffe added the neutral:changes-requested neutral reviewed an adopted PR and requests changes (non-binding; maintainer decides) label Aug 21, 2026
philcunliffe
philcunliffe previously requested changes Aug 21, 2026

@philcunliffephilcunliffe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking findings (full evidence in the neutral review comment on this PR).

  1. medium - docs/HEADLESS.md:39-40: "nothing on the server needs cleanup" on an expiry swap. Re-minting creates a second gateway row (LLP 0298 D2; src/core/cli/remote_commands.js:1143-1146), so runs before and after the swap land under different gateways, contradicting the grouping promised at docs/HEADLESS.md:19-21, and the old row stays server-side.

  2. medium - docs/HEADLESS.md:116-118: the join troubleshooting bullet blames a /v1/mcp URL or an expired token for a hang / nonzero exit. runJoin never contacts the server (src/core/commands/central.js:46-180): it syntax-checks the URL, writes the seed, and returns 0 at :166-169. Both named causes succeed at join and fail later at daemon bootstrap. The actual CI hang is join blocking in readAllStdin (central.js:80-85) when nothing was piped.

  3. medium - docs/HEADLESS.md:100-105: "it survives reboots" is unqualified in the long-lived-headless-machine section. Linux installs a systemd user unit (src/core/daemon/platform.js:53-59, linux.js:141,214,241), which does not start at boot on a headless host without loginctl enable-linger; nothing in this repo sets or mentions linger. macOS's LaunchAgent needs a logged-in session.

  4. low - docs/HEADLESS.md:112-113: hyp status never prints a server URL or gateway id (src/core/commands/status.js:591 onward, and no such field in the JSON report). docs/TEAM_SETUP.md:66-68 already has accurate phrasing.

Everything else verified clean against the tree at 70ea5c7: mint flags and default expiry, stdout-only token, stdin join, hyp daemon run --foreground, hyp sync --yes, the no-first-sync-hold claim, the 404 line, Node 22.12, the #hyp-remote-mint anchor, and no em dashes. This is a text-accuracy pass, not a rewrite.

@platypii

Copy link
Copy Markdown
ContributorAuthor

Thanks, all four fixed in ae2f05c. Docs-only, docs/HEADLESS.md.

  1. Re-mint cleanup. Now says minting binds a new gateway row at mint time (id printed on stderr next to the token), so runs before and after a swap group under different gateways, and the old row stays in place server-side.
  2. Join troubleshooting. Rewritten as two bullets: a hang is join blocking on stdin with nothing piped in and no positional/--token-file token; and since join never contacts the server, a /v1/mcp URL or an expired token still joins cleanly and surfaces later as a daemon bootstrap failure.
  3. Reboot durability. Qualified: both installers are per-user, Linux is a systemd user unit needing loginctl enable-linger <user> to start at boot and survive logout, macOS is a LaunchAgent needing a logged-in session.
  4. hyp status. Switched to the docs/TEAM_SETUP.md phrasing: whether recording is active, and what is shared with your team versus kept on the machine.

Left the backgrounded hyp daemon run --foreground & in the Actions example as-is, per your non-blocking note.

@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review: approved

Re-review at ae2f05c of the docs-only change (README link + new docs/HEADLESS.md). All four findings from the 70ea5c7 round are fixed, and each fix is factually correct against the tree at this commit rather than merely reworded. No new findings. Nothing actionable.

The four prior findings, re-verified

1. Re-mint cleanup (was medium) - fixed.docs/HEADLESS.md:39-42 now reads "Minting binds a new gateway row at mint time (the id is printed on standard error next to the token), so runs before and after the swap group under different gateways, and the old row stays in place server-side." That matches LLP 0298 D2 (llp/0298-ci-tokens-share-one-gateway.decision.md:33-46: "bound to one gateway row, created once at mint time") and the comment at src/core/cli/remote_commands.js:1143-1146. The stderr-id claim also checks out: the gateway id rides the pre-token banner at src/core/cli/remote_commands.js:1147-1148 (minted CI token for '<name>' (gateway <id>, expires <ts>) on stderr, token alone on stdout at :1149). It no longer contradicts the grouping promise at docs/HEADLESS.md:19-21.

2. Join troubleshooting (was medium) - fixed. Split into two accurate bullets at docs/HEADLESS.md:120-125. The hang bullet now names the real cause: with no positional token and no --token-file, join falls through to readAllStdin on a non-TTY stdin (src/core/commands/central.js:79-85; the TTY branch errors at :81 instead, so this is specifically the CI shape). The second bullet correctly says join never contacts the server, so a /v1/mcp URL or an expired token still joins cleanly and surfaces later at daemon bootstrap - consistent with runJoin doing only a syntax check on the URL (src/core/commands/central.js:58-66, protocol-only) before writing the seed and returning 0 at :166-168.

3. Reboot durability (was medium) - fixed.docs/HEADLESS.md:104-110 now qualifies it: both installers are per-user, Linux is a systemd user unit needing loginctl enable-linger <user> to start at boot and survive the last session logging out, macOS is a LaunchAgent needing a logged-in session. Matches src/core/daemon/platform.js:53-59 (~/.config/systemd/user) and :48-50 (~/Library/LaunchAgents), src/core/daemon/linux.js:141 (WantedBy=default.target) and :40,214,241 (systemctl --user enable). The unqualified "it survives reboots" is gone.

4. hyp status (was low) - fixed.docs/HEADLESS.md:115-116 now uses the docs/TEAM_SETUP.md:66-68 phrasing ("whether recording is active, and what is shared with your team versus kept on the machine"). The false "which gateway the machine forwards to" claim is gone; renderStatusText (src/core/commands/status.js:590 onward) still prints no server URL or gateway id.

Re-verified clean on this commit

Every command, flag, path, and anchor the guide tells a reader to type:

  • hyp remote mint [name] [--label <label>] [--expires-days <n>] and the 365-day default: src/core/cli/command_args.js:174,179,184.
  • Token alone on stdout, advisories on stderr (so hyp remote mint > ci.token captures exactly the secret): src/core/cli/remote_commands.js:1140-1152.
  • HTTP 404 = server predates minting: src/core/cli/remote_commands.js:1103-1106.
  • hyp join <url> [token] [--token-file <path>] [--no-daemon], token on stdin: src/core/commands/central.js:48-49,68-90; --no-daemon returns 0 without installing at :166-168.
  • No first-sync review hold on a token join: writeFirstSyncHoldMarker is reached only through the login path (src/core/cli/remote_commands.js:30,274-276, markFirstSyncHoldBestEffort), never from runJoin. Consistent with LLP 0298.
  • One shared gateway per token: LLP 0298 D2.
  • hyp daemon run --foreground [--config <path>]: src/core/cli/core_commands.js:599-600.
  • hyp sync [instance] [--yes] [--dry-run]: src/core/cli/core_commands.js:632-639.
  • npm install -g hypaware and the hyp bin: package.json:2,6-9. Node 22.12 or newer: package.json:66-68.
  • Links: ./PRIVACY.md, ./TEAM_SETUP.md, ./CLI_REFERENCE.md all exist; the #hyp-remote-mint anchor resolves to docs/CLI_REFERENCE.md:1226. The README entry at README.md:571 sits correctly in the docs/ list.
  • Prose rules: no em dash (U+2014) and no other unicode dashes anywhere in either file. No code added, so the no-semicolons rule does not apply.
  • node --test test/core/tracked-files.test.js passes (4/4) with the new file present.

Nit (not actionable, no change requested)

docs/HEADLESS.md:109 is 96 columns while the rest of the file's prose wraps at 80, a seam left by the finding-3 edit. Purely cosmetic, and README.md, docs/PRIVACY.md, and docs/TEAM_SETUP.md all carry over-80 prose lines too, so there is no rule to violate. Mentioned only so a future editor knows it was seen, not asked for.

The backgrounded hyp daemon run --foreground & in the Actions example stays as-is, as agreed last round: it mirrors LLP 0298 D4 and docs/CLI_REFERENCE.md:1247-1258 verbatim, so it is not a defect introduced by this PR.

Verdict

Approved. Docs-only, factually accurate, internally consistent with the existing guides, links resolve, prose rules honored.

@philcunliffephilcunliffe added neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) and removed neutral:changes-requested neutral reviewed an adopted PR and requests changes (non-binding; maintainer decides) labels Aug 24, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral verdict: approved

All four blocking findings from the 70ea5c7 round are fixed at ae2f05c, and each fix verifies against the code, not just the wording: the re-mint gateway-row consequence (LLP 0298 D2, src/core/cli/remote_commands.js:1143-1148), the stdin-hang vs. deferred-bootstrap-failure split (src/core/commands/central.js:58-90,166-168), the systemd-user-unit / loginctl enable-linger and LaunchAgent qualification (src/core/daemon/platform.js:48-59, linux.js:141,214,241), and the hyp status phrasing now matching docs/TEAM_SETUP.md:66-68.

Re-verified the rest of the guide on this commit: mint flags and the 365-day default, stdout-only token, hyp join --no-daemon on stdin, hyp daemon run --foreground, hyp sync --yes, the no-first-sync-hold claim, the 404 line, Node 22.12, the package name and hyp bin, all three relative links and the #hyp-remote-mint anchor, and no em dashes. test/core/tracked-files.test.js passes.

One non-actionable nit recorded in the full review (docs/HEADLESS.md:109 wraps at 96 columns while the file's prose otherwise wraps at 80). No change requested.

Full evidence in the review-record comment on this PR.

@platypii
platypii enabled auto-merge August 24, 2026 16:50
@platypii
platypii disabled auto-merge August 24, 2026 17:10
@platypii
platypii enabled auto-merge August 24, 2026 17:10
@platypii
platypii disabled auto-merge August 24, 2026 17:10
@platypii
platypii enabled auto-merge August 24, 2026 17:10
@platypii
platypii dismissed philcunliffe’s stale reviewAugust 24, 2026 18:07

neutral subsequently approved

@platypii
platypii added this pull request to the merge queueAug 24, 2026
Merged via the queue into master with commit b93552aAug 24, 2026
8 checks passed
@platypii
platypii deleted the docs/headless-deploy branch August 24, 2026 18:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approvedneutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)neutral:reviewDelegate this PR to neutral for a review pass (approve or request changes; never merges)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@platypii@philcunliffe