Skip to content

installer: transport auto/compat, HTTPS-proxy sandboxes, root without systemd - #49

Merged
TeoSlayer merged 6 commits into
mainfrom
feat/installer-proxy-transport
Sep 24, 2026
Merged

TeoSlayer merged 6 commits into
mainfrom
feat/installer-proxy-transport

Conversation

@TeoSlayer

@TeoSlayer TeoSlayer commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

What

This PR fixes the installer users actually run, curl -fsSL https://pilotprotocol.network/install.sh | sh, for hosted agent sandboxes. The target is Meta Muse, where:

  • the agent runs as root in a VM without systemd;
  • the only way out is an authenticating HTTPS CONNECT proxy on :443;
  • DNS for the Pilot hosts is poisoned;
  • the proxy credentials in HTTPS_PROXY rotate every few minutes.

Normal hosts install exactly as before (same files, same config.json). Their output now states the transport the installed daemon actually uses.

It is the installer half of pilot-protocol/pilotprotocol#470 (feat/native-https-proxy). At head c36ad9b this branch's install.sh (sha256 4384f0cb2890177cb87a2e2bc49e1b814d2850434d24c54d1730c9955fbbb671) is byte-identical to pilotprotocol#470's copy (synced in pilotprotocol@df090a8a, where TestInstallerSavesTheSandboxProxyCmd also pins SANDBOX_PROXY_CMD to pilotctl's command) and to the website fallback in pilot-protocol/website#263 (website@a7f94bc).

How it deploys (checked)

  • Serving. worker/ (Cloudflare Worker pilot-release, route pilotprotocol.network/install.sh) reads install.sh from the R2 bucket pilot-release-assets on every request (cache-control: public, max-age=300). The script is not bundled into the Worker at wrangler deploy, so the Worker does not need redeploying for this change.
  • Upload. R2 is written by pilot-protocol/website .github/workflows/sync-install-sh.yml. That workflow:
    1. fetches raw.githubusercontent.com/pilot-protocol/release/main/install.sh;
    2. runs bash -n on it;
    3. runs wrangler r2 object put pilot-release-assets/install.sh;
    4. diffs the live URL against it;
    5. finally fails if the website's own fallback public/install.sh has drifted.
  • Trigger. This repo's notify-install-sh-sync.yml runs on a push to main that touches install.sh. It sends repository_dispatch: release-install-sh-changed to the website repo. A daily 06:17 UTC cron and workflow_dispatch are the safety nets.
  • Result. Merging this PR updates https://pilotprotocol.network/install.sh within about a minute, plus up to 5 minutes of client or proxy cache.
  • Fallback copy. The website fallback copy is updated in install.sh: sync the Pages fallback with pilot-protocol/release#49 website#263 (draft). Merge it right after this PR, otherwise the website sync run goes red on its drift step (the live URL is still correct, because R2 is written first).
  • No CI here. This repo has no PR CI for install.sh, so the tests below were run by hand.

Changes

Root and transport

  • Root is allowed on Linux without /run/systemd/system (containers and VMs such as Muse) when root itself runs the installer, and prints a note. Root is still refused, unless PILOT_ALLOW_ROOT=1:
    • on hosts with systemd, on macOS, and with launchd;
    • on any host when root comes from sudo, doas or pkexec for a regular user (SUDO_UID/SUDO_USER, DOAS_USER, PKEXEC_UID). curl … | sudo sh on WSL, OpenRC/runit hosts or dev containers would otherwise install into /root/.pilot, which that user cannot run, and sudo -E would leave a root-owned ~/.pilot. The error names the user and gives the command to run instead.
  • --transport auto|udp|compat (or PILOT_TRANSPORT):
    • udp and compat are saved.
    • auto is never saved.
    • A saved transport=auto is rewritten to udp for a daemon that predates it.
    • Leaving compat restores the raw registry that an old-pilotctl compat install wrote.

No raw-IP endpoints behind a proxy

  • A fresh config.json leaves out 34.71.57.205:9000 and 34.71.57.205:9001 when the node runs compat, or auto behind a proxy. The daemon then applies the endpoint that fits its transport: registry.pilotprotocol.network:443 over TLS in compat mode or through a proxy.
  • With a pilotctl that predates --transport (v1.13.x forwards config.json's registry verbatim), a compat install gets registry.pilotprotocol.network:443 by name.
  • The service units omit the stock endpoints in the same cases.
  • A custom PILOT_REGISTRY or PILOT_BEACON is always kept.

Rotating credentials. Nothing credential-bearing is ever written or printed.

  • proxy_cmd is saved in config.json: $PILOT_PROXY_CMD, or in a sandbox whose HTTPS_PROXY/https_proxy carries credentials, the credential-preferring command pilotctl uses:
    bash -c 'case $https_proxy in *@*) printf %s "$https_proxy";; *) printf %s "${HTTPS_PROXY:-$https_proxy}";; esac'
    • An existing proxy_cmd is never replaced.
    • It is also saved for a daemon that predates -proxy-cmd: that daemon ignores the key until it is upgraded, and the installer prints a note saying so.
  • Downloads survive a rotation mid-install. pcurl retries once after re-reading the proxy URL from that command, mirroring the daemon's 407 → refresh → retry-once. The re-read URL only changes this process's environment. pilotctl skills check gets the refreshed credentials too.
  • A PILOT_PROXY http(s) URL carries the downloads when no *_PROXY variable is set.
  • The proxy is always shown redacted (http://***@host:port).
  • The service-unit note says that saving a credentialed proxy stores the credentials, and offers proxy_cmd instead.

Honest output with today's releases (v1.13.9, v1.13.10: no -proxy)

  • When a proxy is set but the installed daemon cannot use one, the installer warns in every transport. The warning points at step 3 of the pilot-sandbox recipe (https://pilotprotocol.network/learn/install-pilot-skills-in-meta-muse) rather than at a release that is not out yet.
  • Where the proxy is the way out (a credential-bearing proxy on Linux without systemd, which is Muse, or PILOT_PROXY_CMD set), no output tells the agent to run pilotctl daemon start. That covers the warning, the no-systemd hint, GET STARTED step 0 and the re-run summary: each says not to run it and points at the recipe. On other hosts with such a proxy, the command stays, with the condition and the recipe next to it.
  • auto is no longer announced before the download. The install and update summaries state the transport of the installed daemon. For v1.13.9 that is udp (… predates auto and never falls back to TCP 443 by itself), plus, when nothing chose udp, the --transport compat re-run for UDP-blocked hosts. compat saved for a daemon without -transport is reported as udp.

Release pins (pre-existing bug)

  • --version <tag> and --channel beta aborted with "integrity anchors disagree" for every tag except latest_stable, because the manifest only hashes latest_stable. The manifest hash is now compared only for the tag it describes: its platform url names the tag, or the tag is latest_stable, which covers the managed-runtime manifest without urls. Any other tag is verified against checksums.txt. The tag the manifest describes still needs both anchors to agree.
  • --help prints the usage header up to "WHAT THIS SCRIPT DOES" instead of a fixed line range.

Onboarding text

  • Replies are read from the output of send-message --wait (JSON: .data.reply), not from "the newest inbox message".
  • The appstore install examples drop the routine --force.
  • An empty Email: line now explains the synthesized identity.
  • The manual pointer names pilotctl skills instead of one harness path.

Safety

  • The pilotctl daemon start --help probe now runs only for compat with a daemon that has -transport (v1.11+). Per-command help exists since v1.10, so the probe can never start a daemon. tests/managed-install.sh caught the unconditional version.

Round 2 review fixes (c36ad9b)

  • R49-A2: sandbox proxy_cmd only for credentials a fresh shell can print. ENV_PROXY_CREDS is read before PILOT_PROXY is copied into https_proxy/HTTPS_PROXY for the downloads, and the sandbox command is chosen only when the proxy environment carries credentials and PILOT_PROXY is unset or auto (pilot-daemon runs a proxy command in place of the URL it would use, so it would have replaced an explicit PILOT_PROXY, as pilotctl already avoids). Before, PILOT_PROXY=http://u:p@… saved a command that printed nothing and the summary claimed "rotation needs no restart".
  • R49-A1: restart advice stops the running daemon first. On a proxy-only host the "No managed service was running" advice now prints Stop the running daemon first: pilotctl daemon stop, then the recipe; the recipe starts a daemon but never stops one, and following the old advice ran two daemons with one identity.
  • R49-A4: macOS. The LaunchAgent start line goes through start_hint like every other start line (condition on PROXY_UNSUPPORTED, "Do not run" on PROXY_ONLY), and nothing points macOS at the Linux-root-only recipe: PROXY_REMEDY there is "re-run this installer once a Pilot release whose pilot-daemon -h lists -proxy is out".
  • R49-A5: --transport auto with a pre-auto daemon no longer says "keeps its default (udp)" while a saved compat stays in effect; it says the saved transport is kept and how to switch.
  • R49-A3 (pilot-mcp#23 did not recognise this installer's proxy_cmd): fixed in pilot-mcp#23 (its SANDBOX_PROXY_CMD is now this exact command; the older pre-release one is still recognised).

Tests for round 2 (tests/proxy-transport-install.sh, new darwin fixture with a fake launchctl): 4d (PILOT_PROXY credentials: no proxy_cmd, no rotation claim, no leak; explicit PILOT_PROXY next to a credentialed HTTPS_PROXY: no proxy_cmd), the re-run advice keeps pilotctl daemon stop, 3b (old daemon, --transport compat then --transport auto: compat stays and is reported), 10a (macOS with a proxy: conditioned LaunchAgent line, no Linux recipe; with PILOT_PROXY_CMD: "Do not run launchctl load -w …"). Passes on macOS sh and in a Linux container as root, as a regular user and with /run/systemd/system; tests/managed-install.sh passes; shellcheck -s sh install.sh tests/*.sh clean.

E2E (round 3 Muse sim, rotating-credential CONNECT-443 proxy, root, no systemd, packet sink for direct dials): installed through pilot-skills#34's one-shot with PILOT_INSTALL_URL=file:// this c36ad9b script — with the next release's binaries: rc=0, config.json transport=compat + proxy_cmd (the pilotctl command), node registered through the proxy; with released v1.13.10 and root without CAP_SYS_ADMIN: the WARNING, the no-systemd hint and GET STARTED name the recipe and say not to run pilotctl daemon start (no output line tells the agent to run it).

Tests

Fixture tests

  • sh tests/proxy-transport-install.sh: OK under macOS sh and dash. It uses a fixture release with fake curl, uname, id and sudo, and needs no network. New cases:
    • a credential rotation mid-install: the fake curl returns 407 once the password rotates, and BASH_ENV stands in for Muse's fresh-shell credentials;
    • no raw-IP endpoint for auto or compat behind a proxy;
    • compat with an old pilotctl;
    • downloads through PILOT_PROXY;
    • old daemon: proxy_cmd saved, plus the warning with the recipe link;
    • root refused on macOS;
    • root refused under sudo (SUDO_UID+SUDO_USER, SUDO_USER alone) and doas. It is allowed under sudo from root (SUDO_UID=0) and with PILOT_ALLOW_ROOT=1;
    • v1.13.x in a proxy-only sandbox, on the first install, a re-run and --transport compat: no line tells the agent to run pilotctl daemon start (or stop && start, or systemctl enable), and the warning, the no-systemd hint and GET STARTED all point at the recipe. With a proxy that has no credentials, the command stays with the condition. A daemon that can use the proxy keeps the command and gets no recipe;
    • v1.13.x without a proxy: Transport: udp (… predates auto, never Transport: auto, plus the compat hint. There is no hint after --transport udp or with a daemon that has auto;
    • a daemon without -transport and --transport compat: the summary says udp;
    • --version v9.9.8 --yes and --channel beta against a manifest that hashes only latest_stable, with and without platform urls: installed and verified by checksums.txt. The latest_stable tag with a disagreeing manifest hash, and a pinned tag that the manifest's url names, are still refused;
    • the --help range;
    • no credentials in the output or anywhere under ~/.pilot.
    • PILOT_TEST_SH=dash dash tests/proxy-transport-install.sh runs the installer itself under dash.
    • Mutation check: disabling the refresh, the endpoint omission, or the PILOT_PROXY export each fails the suite. So does each review fix disabled on its own: the sudo refusal, the manifest-tag rule (either half), proxy-only detection, GET STARTED step 0, the compat hint, or restoring the early auto banner.
  • sh tests/managed-install.sh: OK.
  • shellcheck -s sh install.sh tests/*.sh: clean. sh -n passes under busybox, dash and bash.

Docker, review fixes (head 67e83fc, real v1.13.9 downloads)

Check Result
ubuntu:24.04, no systemd, user agent with NOPASSWD sudo: cat install.sh | sudo sh and sudo -E sh install.sh Both rc=1: refusing to install as root: this runs under sudo for agent. No /root/.pilot, no ~/.pilot, no /usr/local/bin links. Base 36c38dc refuses too; the previous head installed into /root/.pilot.
Same container, root directly (HOME=/root, no SUDO_*) rc=0 with the sandbox note; links point to /root/.pilot/bin.
Same container as agent, plain install rc=0. No auto banner. Summary: Transport: udp (this pilot-daemon, v1.13.9, predates auto …) plus the --transport compat re-run. That re-run saves compat with registry.pilotprotocol.network:443 and the update summary says compat.
--version v1.13.8 --yes, --channel beta (v1.13.10-rc.2), --version v1.10.1 --transport compat --yes All rc=0, Verified SHA-256 (checksums.txt), and pilotctl version matches. Base 36c38dc: --version v1.13.8 gives integrity anchors disagree.
Muse rig: internal network, CONNECT-443-only authenticating proxy that rotates its password every 5s, BASH_ENV fresh credentials, poisoned DNS, root HOME=/root, no systemd. Fresh install, re-run, and --transport compat All rc=0. For each: 0 lines that tell the agent to run pilotctl daemon start, 3 recipe pointers (warning, no-systemd hint, GET STARTED), 0 Transport: auto, 0 credential matches in the output, /root or /usr/local/bin. Three 407s from rotations were each retried and allowed. The proxy log has only :443 CONNECTs.

Docker, served path

curl -fsSL https://raw.githubusercontent.com/pilot-protocol/release/<head>/install.sh | sh in a Muse-like rig with:

  • an --internal network;
  • an authenticating CONNECT-443-only proxy that reads its password per request;
  • the password rotated every 5–6s;
  • BASH_ENV exporting the current credentials;
  • the Pilot hosts poisoned to 198.18.67.197;
  • root with HOME=/root and no systemd.
Scenario Result
A: --transport compat, v1.13.9, 5 runs rc=0 in 7–9s each. Rotations hit 407 on github.com, release-assets.githubusercontent.com and pilotprotocol.network, and every one was retried and allowed. The proxy log has only :443 CONNECTs; direct egress fails (Could not resolve host). config.json = {transport: compat, registry: registry.pilotprotocol.network:443, proxy_cmd: <command>}, no raw IP. 0 credential matches in the output, /root or /usr/local/bin. The v1.13.9 warning names the sandbox recipe.
A2: plain curl | sh, v1.13.9, 3 runs rc=0. v1.13.9 has no auto, so the transport is udp and the stock endpoints stay, which is correct for that daemon. proxy_cmd is saved and the warning is shown.
B: same rig, plain install, with the next release's binaries Binaries built from pilotprotocol feat/native-https-proxy @ 5097bc22 and copied over the download by a test-only copy of this script. Output: Transport: auto. config.json has no registry or beacon and has proxy_cmd. Then pilotctl daemon start registered in 5s (transport auto-selected transport=compat, registry=registry.pilotprotocol.network:443, node 251998, since deregistered). Across 3+ rotations (30s period): 4 registry reconnects got 407 and were each retried and allowed. pilotctl --json send-message list-agents --data '/data {"search":"weather","limit":2}' --wait returned .data.reply, and pilotctl appstore catalogue worked. 0 credential matches in the daemon log.
B, compat / re-run / --transport udp Compat: no registry key. A re-run keeps the config. --transport udp saves udp.
C: normal network, non-root (agent), no proxy rc=0. config.json has the stock registry and beacon as before. No root note.
C, root Root with /run/systemd/system present is refused (refusing to install as root, no ~/.pilot). Root without systemd is allowed with the note.

Deploy order (all six PRs)

  1. installer: transport auto/compat, HTTPS-proxy sandboxes, root without systemd #49 merge → notify-install-sh-sync dispatches to pilot-protocol/website → sync-install-sh.yml puts install.sh into R2 pilot-release-assets/install.sh (the pilot-release Worker reads R2 per request; no Worker deploy). Check: curl -fsSL https://pilotprotocol.network/install.sh | shasum -a 256 → 4384f0cb2890177cb87a2e2bc49e1b814d2850434d24c54d1730c9955fbbb671 (up to 5 min cache).
  2. install.sh: sync the Pages fallback with pilot-protocol/release#49 website#263 merge (Pages fallback public/install.sh, same bytes) → the sync workflow's drift step goes green.
  3. feat: native HTTPS-proxy support and -transport=auto (Meta Muse / proxy-only sandboxes) pilotprotocol#470: re-run install.sh matches pilot-protocol/release, merge.
  4. Tag pilotprotocol v1.13.11 on main (release workflow: binaries, checksums.txt, signed latest.json). v1.13.10 shipped without #470, so until this tag every install gets a daemon without -proxy. Check pilot-daemon -h lists -proxy and -proxy-cmd.
  5. feat(muse): one-shot Pilot-in-Muse installer and pilot-up.sh TeoSlayer/pilot-skills#34 merge → CI regenerates skills.json/setups.json and skills/pilotctl/SKILL.md, dispatches the website deploy; the Muse one-shot (raw.githubusercontent.com/TeoSlayer/pilot-skills/main/muse/install.sh) is live at merge.
  6. feat(setup): install and start Pilot behind an HTTPS egress proxy (Meta Muse) pilot-mcp#23 merge → release 0.3.0 (behaviour change; last published 0.2.13): bump package.json, package-lock.json, server.json (both fields), .well-known/mcp/server-card.json, src/version.js, src/openclaw-plugin/openclaw.plugin.json, src/openclaw-plugin/evaluate.js and the pinned pilotprotocol-mcp@0.2.13 strings in test/{hermes-setup,openclaw-plugin,native-harness-setup,harness-config-contracts}.test.js (test/release-contract.test.js locks them), move [Unreleased] to [0.3.0], push tag v0.3.0 → publish.yml (tag must equal v+version; lint + tests; npm via OIDC, MCP Registry server.json, ghcr.io/pilot-protocol/pilot-mcp).
  7. Add marker-gated skill targets (gatedTools) and the Meta Muse format skillinject#42 merge → tag v0.2.5 → pilotprotocol follow-up PR go get github.com/pilot-protocol/skillinject@v0.2.5 && go mod tidy (GOWORK=off) → next daemon release (v1.13.12; or fold the bump in before tagging v1.13.11).
  8. inject-manifest: gate Meta Muse under a new gatedTools key TeoSlayer/pilot-skills#35 merge (manifest gatedTools muse row). Released daemons drop the key (CI released-skillinject v0.2.2–v0.2.4). No manifest signing step: DefaultManifestPublicKeyHex is empty; only hosts that installed ~/.pilot/skillinject.pub verify inject-manifest.json.sig.

🤖 Generated with Claude Code

… systemd

Ports the installer half of pilot-protocol/pilotprotocol#feat/native-https-proxy
onto the canonical script. The managed-node code (--managed-url, --no-start,
PILOT_ENROLLMENT_TOKEN) is unchanged; in managed mode the new "start the
daemon manually" hints stay silent because the managed flow starts it.

- --transport <auto|udp|compat> (or PILOT_TRANSPORT). udp and compat are
  saved in config.json; auto (the default) never is, and --transport auto
  removes a saved transport. A saved transport=auto is rewritten to udp for
  a pilot-daemon that predates it (reinstalling an older --version).
- Root is allowed in a Linux container/VM without systemd (hosted agent
  sandboxes such as Meta Muse run the agent as root); hosts with systemd or
  launchd still refuse root unless PILOT_ALLOW_ROOT=1.
- proxy_cmd: in such a sandbox, when HTTPS_PROXY carries credentials and the
  daemon supports -proxy-cmd, save bash -c 'printf %s "${https_proxy:-$HTTPS_PROXY}"'
  (or $PILOT_PROXY_CMD) so the daemon re-reads rotating proxy credentials.
  An existing proxy_cmd is never replaced.
- Compat installs leave the raw-TCP default registry/beacon off the service
  units; switching back from compat restores the raw registry an older
  compat install saved. Service units carry PILOT_TRANSPORT_DEFAULT=auto for
  daemons that support it.
- Download failures name the proxy (redacted) and the hosts it must allow;
  proxy credentials never reach the output.

tests/proxy-transport-install.sh covers the above with a fixture release
(no network, never sudo); tests/managed-install.sh still passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
teovl and others added 2 commits September 24, 2026 09:04
…, clean onboarding

Proxy / sandbox (Meta Muse: root, no systemd, rotating HTTPS_PROXY credentials):
- Fresh config.json leaves out the stock raw-TCP registry and UDP beacon
  (34.71.57.205) when the node runs compat, or auto behind a proxy; the
  daemon applies the endpoint that fits its transport. A compat install with
  a pilotctl that predates --transport gets registry.pilotprotocol.network:443
  by name (it forwards config.json's registry verbatim). Service units omit
  the stock endpoints in the same cases. Custom PILOT_REGISTRY/PILOT_BEACON
  are always kept.
- Downloads survive a credential rotation mid-install: pcurl retries once
  after re-reading the proxy URL from $PILOT_PROXY_CMD or, in a sandbox, a
  fresh bash (only this process's environment changes; nothing is printed or
  written).
- The sandbox proxy_cmd is the credential-preferring command pilotctl uses
  (pilotprotocol#470): whichever of $https_proxy/$HTTPS_PROXY carries
  credentials. It is saved also for a daemon that predates -proxy-cmd (it
  ignores the key until upgraded), with a note.
- PILOT_PROXY (http/https URL) carries the downloads when no *_PROXY is set.
- A proxy the installed daemon cannot use (v1.13.x) now gets a warning in
  every transport, pointing at the pilot-sandbox recipe instead of a release
  that does not exist yet; the no-systemd start hint refers to it.
- The service-unit proxy note no longer suggests writing a credentialed URL
  without saying so, and offers proxy_cmd.
- The `daemon start --help` probe runs only for compat with a daemon that has
  -transport (v1.11+; per-command help exists since v1.10), so it can never
  start a daemon.

Onboarding text:
- Replies are read from `send-message --wait` output (JSON: .data.reply), not
  "the newest inbox message", which may answer an older question.
- `appstore install` examples drop the routine --force (it reinstalls over
  the app and can delete its saved state).
- A blank "Email:" line now says what the daemon uses; the manual pointer
  names `pilotctl skills` instead of one harness path.

Tests: tests/proxy-transport-install.sh adds the rotation retry (BASH_ENV
stands in for the sandbox), no raw-IP endpoint for compat/auto behind a
proxy, old-pilotctl compat, PILOT_PROXY downloads, root refused on macOS,
--help range, and no credentials in output or ~/.pilot. Mutation-checked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first skills pass runs after the downloads; in a sandbox whose proxy
credentials rotated in between, pilotctl inherited stale ones (seen as a
407 on raw.githubusercontent.com in the rotating-proxy rig). Refresh from
PROXY_REFRESH_CMD first; a no-op everywhere else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TeoSlayer pushed a commit to pilot-protocol/pilotprotocol that referenced this pull request Sep 24, 2026
Byte-identical copy of the canonical installer on
pilot-protocol/release feat/installer-proxy-transport, so canonical-drift
passes once release#49 merges. Takes this branch's credential-preferring
sandbox proxy_cmd, and adds: no raw-IP registry/beacon in config.json for
compat or auto behind a proxy, downloads retried once after a proxy
credential rotation, proxy_cmd saved for older daemons too, PILOT_PROXY
downloads, the v1.13.x proxy warning pointing at the pilot-sandbox recipe,
and onboarding text that reads replies from send-message --wait and drops
routine appstore --force. Only install.sh changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…passes the proxy, honest transport, pinned tags install

- Root reached through sudo/doas/pkexec for a regular user (SUDO_UID/SUDO_USER,
  DOAS_USER, PKEXEC_UID) is refused again on every host, with the reason and
  the command to run instead. `curl | sudo sh` on WSL/OpenRC/dev containers
  installed into /root/.pilot (0700) and left the user with "command not
  found"; `sudo -E` left a root-owned ~/.pilot. A root agent (Meta Muse) is
  unaffected; PILOT_ALLOW_ROOT=1 still overrides.
- With a daemon that cannot use the proxy (v1.13.9) on a host whose proxy is
  the way out (credential-bearing proxy without systemd, or PILOT_PROXY_CMD),
  no output tells the agent to run `pilotctl daemon start` (warning,
  no-systemd hint, GET STARTED step 0, re-run summary): each points at the
  pilot-sandbox recipe instead. Elsewhere the command stays, with the
  condition next to it.
- auto is no longer announced before the download. The summary (install and
  update) states the transport the installed daemon runs; a release without
  auto says udp and, where nothing chose udp, how to get compat on a
  UDP-blocked host. compat saved for a daemon without -transport is reported
  as udp.
- --version / --channel beta: the manifest hash is compared only for the tag
  the manifest describes (its platform url names it, or latest_stable), so a
  pinned or beta tag installs against checksums.txt instead of aborting with
  "integrity anchors disagree". The tag it describes still needs both.
- --help prints the usage header up to "WHAT THIS SCRIPT DOES" instead of a
  fixed line range.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TeoSlayer pushed a commit to pilot-protocol/website that referenced this pull request Sep 24, 2026
…eview fixes)

Byte-identical to release@67e83fc: root refused under sudo, no
`pilotctl daemon start` advice on proxy-only hosts with a daemon that cannot
use the proxy, the transport stated after the download, and --version /
--channel beta installable again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TeoSlayer pushed a commit to pilot-protocol/pilotprotocol that referenced this pull request Sep 24, 2026
Byte-identical to release@67e83fc (review fixes: root refused under sudo,
no `pilotctl daemon start` advice on proxy-only hosts with a daemon that
cannot use the proxy, the transport stated after the download, --version /
--channel beta installable again).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
teovl and others added 2 commits September 24, 2026 12:02
…ound 2)

- Save the sandbox proxy_cmd only when the proxy environment itself carries
  the credentials (read before PILOT_PROXY is copied into it for the
  downloads) and no explicit PILOT_PROXY is set. Credentials that arrive in
  PILOT_PROXY never reach a fresh shell, so the command printed nothing and
  the summary still claimed "rotation needs no restart"; next to an explicit
  PILOT_PROXY the command would have replaced it (pilotctl leaves it alone
  the same way).
- Restart advice on a proxy-only host ("No managed service was running")
  now says to stop the running daemon first (`pilotctl daemon stop`) before
  the pilot-sandbox recipe starts one: the recipe never stops a daemon, and
  following the old advice ran two daemons with one identity.
- macOS: the LaunchAgent start line goes through start_hint like every other
  start line (condition on PROXY_UNSUPPORTED, "Do not run" on PROXY_ONLY),
  and nothing points macOS at the Linux-root-only recipe: PROXY_REMEDY there
  is a re-run once a release whose pilot-daemon -h lists -proxy is out.
- `--transport auto` with a daemon that predates auto no longer claims
  "keeps its default (udp)" while a saved compat stays in effect: it says
  the saved transport is kept and how to switch.

tests/proxy-transport-install.sh covers each case (new darwin fixture with a
fake launchctl); passes on macOS and in a Linux container as root, as a
regular user and with /run/systemd/system present. shellcheck clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TeoSlayer pushed a commit to pilot-protocol/pilotprotocol that referenced this pull request Sep 24, 2026
…all.sh sync with release#49 (c36ad9b)

- pilotctl daemon start: when the daemon re-reads its credentials with a
  proxy command and the proxy still rejects them, the hint names the command
  in use and says to check what it prints from a fresh shell, instead of
  telling the operator to set one (E2E scenario 3: the 407 hint said "give
  the daemon a command that prints the current proxy URL" with config.json
  proxy_cmd in place). The daemon's own CredentialHint is worded for both.
- TestInstallerSavesTheSandboxProxyCmd: install.sh's SANDBOX_PROXY_CMD must
  evaluate to pilotctl's sandboxProxyCmd. A saved proxy_cmd turns off the one
  `daemon start` hands the daemon, so the two must never drift (web4-470
  review: the installer kept the pre-change command).
- gosec: #nosec G204 with the reason on proxyconf.CommandSource (the
  operator's proxy command run with sh -c is the function's purpose), and
  the new G104 warnings (unchecked Close/Remove) in relay.go and pilotctl.
- install.sh: byte-identical to pilot-protocol/release#49 c36ad9b (sandbox
  proxy_cmd only for credentials a fresh shell sees and never over an
  explicit PILOT_PROXY; restart advice stops the running daemon before the
  sandbox recipe; macOS LaunchAgent line conditioned and never sent to the
  Linux-only recipe; truthful --transport auto note for pre-auto daemons).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TeoSlayer pushed a commit to pilot-protocol/website that referenced this pull request Sep 24, 2026
…36ad9b)

Byte-identical to release@c36ad9b (sha256 4384f0cb...): sandbox proxy_cmd
only for credentials a fresh shell sees and never over an explicit
PILOT_PROXY; restart advice stops the running daemon before the sandbox
recipe; macOS LaunchAgent start line conditioned and never sent to the
Linux-only recipe; truthful --transport auto note for pre-auto daemons.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@TeoSlayer
TeoSlayer merged commit 480e16c into main Sep 24, 2026
@TeoSlayer
TeoSlayer deleted the feat/installer-proxy-transport branch September 24, 2026 10:21
TeoSlayer added a commit to pilot-protocol/website that referenced this pull request Sep 24, 2026
)

* install.sh: sync the Pages fallback with pilot-protocol/release#49

public/install.sh must stay byte-identical to pilot-protocol/release:install.sh
(sync-install-sh.yml fails its last step when it drifts). This is the
installer from release#49 (ae447bc): root allowed in a Linux container/VM
without systemd (Meta Muse), --transport auto|udp|compat, proxy_cmd for
rotating proxy credentials (never the credentials), no raw-IP registry for
compat or auto behind a proxy, downloads retried once after a credential
rotation, and onboarding text that reads replies from send-message --wait.

Merge right AFTER release#49: merging it first leaves the fallback ahead of
the canonical script until #49 lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* install.sh: sync the Pages fallback with pilot-protocol/release#49 (review fixes)

Byte-identical to release@67e83fc: root refused under sudo, no
`pilotctl daemon start` advice on proxy-only hosts with a daemon that cannot
use the proxy, the transport stated after the download, and --version /
--channel beta installable again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* install.sh: sync the Pages fallback with pilot-protocol/release#49 (c36ad9b)

Byte-identical to release@c36ad9b (sha256 4384f0cb...): sandbox proxy_cmd
only for credentials a fresh shell sees and never over an explicit
PILOT_PROXY; restart advice stops the running daemon before the sandbox
recipe; macOS LaunchAgent start line conditioned and never sent to the
Linux-only recipe; truthful --transport auto note for pre-auto daemons.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TeoSlayer added a commit to pilot-protocol/pilotprotocol that referenced this pull request Sep 24, 2026
…xy-only sandboxes) (#470)

* feat(pilotctl): daemon start --transport/--proxy and proxy env passthrough

`pilotctl daemon start` could not bring a node online from sandboxes whose
only way out is an authenticating HTTPS proxy (Meta Muse):

- config.json from `pilotctl init` holds the raw-TCP registry default
  34.71.57.205:9000 and daemon start forwarded it as an explicit -registry,
  which stops pilot-daemon from switching to registry.pilotprotocol.network:443
  (TLS) in compat mode. In compat mode the compiled-in registry/beacon
  defaults are now left off argv (a default-equal $PILOT_REGISTRY is dropped
  from the child env too).
- PILOT_TRANSPORT never took effect: the daemon's -transport flag defaults to
  "udp", masking the env var. pilotctl now resolves --transport, then
  $PILOT_TRANSPORT, then config "transport" and passes an explicit -transport.

New: --transport <udp|compat> and --proxy <auto|off|URL> (plus config keys
"transport"/"proxy", validated by `config --set`). Both reach the daemon only
when set; a pilot-daemon whose -help lacks them gets them dropped with a
warning instead of a flag-parse crash. A proxy URL with credentials travels
as $PILOT_PROXY (never argv, PILOT-290) and is redacted in config output.
The child env explicitly keeps HTTPS_PROXY/HTTP_PROXY/ALL_PROXY/NO_PROXY
(both cases), PILOT_PROXY, PILOT_TRANSPORT, SSL_CERT_FILE, SSL_CERT_DIR on
both the fork and --foreground paths. --compat-beacon, --registry-trust,
--registry-fingerprint, --tls-trust and the documented-but-dropped
--endpoint/--motd-* are forwarded when given.

Also: create ~/.pilot before the O_EXCL PID claim (a fresh HOME failed with
"PID file locked" forever), and registry dial failures behind a proxy now say
pilotctl's direct registry dials do not use the proxy yet (TODO(netproxy)).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(install): --transport compat for UDP-blocked / proxy-only hosts

install.sh --transport compat (or PILOT_TRANSPORT=compat) merges
"transport": "compat" and "proxy": "auto" into ~/.pilot/config.json via
`pilotctl config --set` (atomic, 0600, other keys kept) and generates compat
systemd/launchd units (-transport compat, raw-TCP registry/beacon defaults
left to the daemon). A re-run without the flag keeps a compat config.

Audited for a no-root, no-systemd VM whose only egress is an authenticating
CONNECT-:443 proxy with poisoned local DNS for *.pilotprotocol.network: every
network call is curl over HTTPS (proxy env honored, CONNECT by hostname, no
wget / raw IP / non-443 / registry or beacon probes). Proxy URLs are only
ever printed redacted. Download failures now name the proxy and the hosts it
must allow instead of falling silently into "Go is required". With an older
release (pilotctl without --transport) compat points config's registry at
registry.pilotprotocol.network:443, and a pilot-daemon without -proxy gets
an explicit warning when HTTPS_PROXY is set. No-systemd hosts get the
`pilotctl daemon start` hint; service-manager units get a note that they do
not inherit the shell's HTTPS_PROXY.

Tested in docker (debian bookworm, non-root, temp HOME) on an --internal
network whose only egress is an authenticating CONNECT-443 proxy, with
poisoned /etc/hosts for the Pilot names: v1.13.9 download + SHA-256 verify +
install succeeds; missing/wrong proxy credentials fail with a clear hint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(daemon): native HTTPS proxy support (-proxy / PILOT_PROXY)

Hosted agent sandboxes such as Meta Muse block outbound UDP, poison
local DNS for *.pilotprotocol.network, and allow only an authenticating
HTTP proxy that CONNECTs to :443. Until now the daemon ignored
HTTPS_PROXY entirely, so compat mode needed root, an SNI router and a
mount-namespace /etc/hosts override to get online.

New flag -proxy (env PILOT_PROXY, config key "proxy"), resolved once at
startup by daemon.ResolveProxy into a common/netproxy policy:

  auto (default)  -transport=compat: HTTPS_PROXY/https_proxy, falling
                  back to ALL_PROXY/all_proxy, honoring NO_PROXY.
                  -transport=udp: no policy, dialing exactly as before.
  off             never proxy (HTTP clients also stop following env).
  http(s)://URL   that proxy for every outbound TCP/HTTP connection.

The policy (daemon.Config.Proxy) is applied to every outbound
connection: the registry client (primary, pool and reconnects, via
registry.WithDialer), the compat WSS beacon (wss.Config.Proxy), the
MOTD fetch, and http.DefaultTransport for plugin HTTP clients
(catalogue pins, skillinject, trustedagents, webhook, enterprise
control). Targets are CONNECTed by host name and never resolved
locally; TLS, SNI and pinned-fingerprint checks stay end to end.
One startup line logs the transport and the redacted proxy.

Compat mode now treats the compiled-in raw-TCP registry default
(34.71.57.205:9000, which pilotctl passes on every daemon start) as not
explicit, so it still switches to registry.pilotprotocol.network:443.
-transport honors PILOT_TRANSPORT. -beacon-rtt-probe is skipped in
compat mode (its UDP probes cannot leave the host).

Pins github.com/pilot-protocol/common to the netproxy feature commit
(v0.5.14-0.20260923210943-65ea5b1a3d2d); move to a tagged release
before merge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(deps): pin github.com/pilot-protocol/common v0.5.14

Replace the feature-branch pseudo-version with the tagged release that
ships netproxy (HTTP CONNECT dialer, independent proxy env parsing,
credential-safe URL handling) and registry/client WithDialer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: -transport=auto, proxy precedence, proxy-aware pilotctl, Muse onboarding

Unifies the daemon (-proxy) and pilotctl/install.sh (--transport/--proxy)
branches and fixes every confirmed review finding on both.

pilot-daemon
- -transport=auto (also $PILOT_TRANSPORT, config "transport"): udp when
  the beacon answers a UDP discover (one round trip; two attempts within
  1.5s), else compat when the compat beacon accepts TCP through the proxy
  compat would use, else udp as before. Logged once. Never picks compat
  for a private registry/beacon. The binary default stays udp.
  pkg/daemon exports SelectTransport/NormalizeTransport; the embedder
  path (TransportMode "" or "auto") uses the same check.
- Precedence for -transport, -proxy, -registry-trust,
  -registry-fingerprint: flag, then $PILOT_TRANSPORT / $PILOT_PROXY /
  $PILOT_REGISTRY_TRUST / $PILOT_REGISTRY_FINGERPRINT, then config.json,
  then default. Literal flag defaults: -help never prints $PILOT_PROXY.
- registry_trust/registry_fingerprint from config/env survive compat
  mode; a fingerprint alone selects pinned trust. compat keeps the raw
  registry with an explicit -registry-tls=false (TCP/9000 fallback) and a
  custom registry from config.json. registry.pilotprotocol.network:443
  always gets TLS, also in udp mode.
- internal/proxyconf (on common/netproxy): off also accepts
  none/no/false/direct; bare words and scheme-less values are errors, not
  proxy host names. Loopback targets are never proxied (DefaultTransport,
  daemon HTTP clients, registry and WSS dials), even with an explicit URL.
- WSS beacon dials through a netproxy dialer (wss.Config.DialContext
  replaces Config.Proxy): an https:// proxy is verified with the system
  roots; the beacon TLS config applies to the beacon only.
- Unusable HTTP_PROXY/ALL_PROXY no longer drop a valid HTTPS_PROXY
  (common v0.5.14); skipped variables are logged. Certificate errors
  against the system store name SSL_CERT_FILE and the fingerprint.

pilotctl
- daemon start asks a daemon that supports it for -transport=auto when
  no transport is configured; the daemon is probed once (-help) and
  flags/values it predates are dropped (auto -> udp) with a warning. The
  ready summary reports the transport the daemon chose.
- --proxy, then $PILOT_PROXY, then config "proxy"; any '@' means
  credentials, which travel as $PILOT_PROXY only, and nothing on argv
  contradicts them. Redaction/validation via internal/proxyconf.
- lookup/register/rotate-key, the auto-handshake visibility check and
  recovery dial the registry through the egress proxy (CONNECT by name),
  using registry.pilotprotocol.network:443 over TLS when proxied or in
  compat mode (pinned with registry_fingerprint when configured), with a
  one-shot TLS fallback when the raw registry is unreachable directly.
- config --set transport accepts auto and normalizes; leaving compat
  restores the raw-TCP registry an older compat install saved.
- withTempHomeFull clears PILOT_TRANSPORT/PILOT_PROXY/*_PROXY.

install.sh
- --transport auto|udp|compat; fresh installs save auto when the daemon
  supports it; no "proxy" key is written; units take the transport from
  config.json; --transport udp restores the raw registry.
- Root is allowed in a Linux container/VM without systemd (hosted agent
  sandboxes); regular hosts still refuse without PILOT_ALLOW_ROOT.

Docs: README compat/proxy section (precedence, SSL_CERT_FILE, pinned
registry), env table, CHANGELOG, regenerated docs/cli-reference.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: rotating proxy credentials, auto/compat and registry routing re-review

Fixes the six re-review findings on feat/native-https-proxy.

muse-proxy-cred-rotation-unhandled (high)
- New -proxy-cmd / $PILOT_PROXY_CMD / config.json proxy_cmd: a command
  whose stdout is the current proxy URL. internal/proxyconf.Policy wraps
  it: re-run every 60s (Config.ProxyRefreshInterval) and whenever a
  CONNECT gets 407 (or the malformed status line sandbox proxies send),
  then the dial is retried once with the new URL. The registry client
  (primary, pool, every redial), the WSS beacon (every reconnect),
  daemon HTTP clients (retrying RoundTripper) and http.DefaultTransport
  (refresh on 407 via OnProxyConnectResponse) all follow it. Output and
  stderr are never logged; PILOT_ADMIN_TOKEN/PILOT_WEBHOOK_SECRET are
  kept from the command. A failing first run falls back to the launch
  environment's proxy.
- pkg/daemon: Config.ProxyPolicy (*ProxyPolicy), StaticProxyPolicy,
  NewCommandProxyPolicy; Start runs the refresher until Stop.
- install.sh saves proxy_cmd=bash -c 'printf %s "${https_proxy:-$HTTPS_PROXY}"'
  in a Linux container/VM without systemd whose HTTPS_PROXY carries
  credentials (or PILOT_PROXY_CMD when set); README documents it.

version-skew-config-transport-auto-bricks-older-daemon (medium)
- install.sh never saves transport=auto (--transport auto removes a saved
  transport); service units get Environment PILOT_TRANSPORT_DEFAULT=auto,
  which a pre-auto daemon ignores. pilot-daemon honours
  $PILOT_TRANSPORT_DEFAULT only when flag, $PILOT_TRANSPORT and config.json
  choose nothing.
- install.sh --version <pre-auto tag> and pilotctl update --pin rewrite a
  saved transport=auto to udp. pilotctl config --set transport= (proxy=,
  proxy_cmd=) removes the key.

compat-explicit-registry-tls-now-pinned-fatal (low)
- applyRegistryDefaults defaults trust (pinned with a fingerprint, else
  system) whenever TLS is on in compat or for the compat registry
  address, also when -registry-tls was explicit.

auto-compat-check-tcp-only-fatal-on-beacon-outage (low)
- SelectTransport's compat check now does TLS + GET of the beacon path
  and requires 426 Upgrade Required (live WebSocket endpoint); a TCP front
  with the beacon down (502/503/close) keeps auto on udp.

daemon-proxied-udp-registry-stays-raw-9000 (low)
- When the registry dial goes through the proxy (any transport), the
  compiled-in raw registry moves to registry.pilotprotocol.network:443
  over TLS, the rule pilotctl already applied.

pilotctl-auto-proxy-regardless-of-transport (low)
- pilotctl's registry routes follow the daemon: an explicit proxy URL
  always; the environment's proxy only when the transport is compat (the
  running daemon's, from the new info "transport" field, else
  $PILOT_TRANSPORT / config.json). udp hosts dial directly; unknown
  transport tries direct first and the proxied TLS registry as fallback
  for the production registry only; private raw registries are never sent
  to the environment's proxy outside compat.

Tests: proxyconf policy unit tests (407 retry for dials and HTTP, NO_PROXY,
Run loop, command runner); pkg/daemon compat start through a rotating
proxy (registry + WSS reconnect); cmd/daemon end-to-end runs for
PILOT_PROXY_CMD rotation, command failure fallback, proxied udp registry,
explicit -registry-tls trust, PILOT_TRANSPORT_DEFAULT; SelectTransport
front-up/beacon-down; pilotctl route matrix, private registry direct,
fitTransportToDaemon, config key clearing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: installer changes via pilot-protocol/release, proxy-first pilotctl registry dial

Fixes the two re-review findings on feat/native-https-proxy.

web4-install-sh-edits-not-in-canonical-installer (high)
- install.sh here is a synced copy of pilot-protocol/release:install.sh
  (the script https://pilotprotocol.network/install.sh serves; the
  canonical-drift job enforces byte equality). The installer changes are
  now ported onto the canonical script in pilot-protocol/release branch
  feat/installer-proxy-transport, keeping its managed-node mode
  (--managed-url, --no-start, PILOT_ENROLLMENT_TOKEN), and this copy is
  byte-identical to that branch. canonical-drift passes once the release
  change merges; merge it first.
- Rotation no longer depends on the installer: on Linux without systemd,
  when $HTTPS_PROXY / $https_proxy carries credentials, the proxy setting
  is auto, no proxy_cmd is configured ($PILOT_PROXY_CMD, config.json /
  --config file) and the daemon supports -proxy-cmd, `pilotctl daemon
  start` hands the daemon
  PILOT_PROXY_CMD=bash -c 'printf %s "${https_proxy:-$HTTPS_PROXY}"'.
  A node set up by the currently served installer (PILOT_ALLOW_ROOT=1, no
  proxy_cmd) therefore re-reads rotated credentials too. The ready summary
  says so (JSON: "proxy_cmd").
- README/CHANGELOG describe both paths.

web4-pilotctl-raw-route-defeated-by-muse-guard (low)
- With the transport unknown (no daemon answering, nothing configured) and
  a proxy in the environment, pilotctl now tries the TLS registry through
  the proxy first and the raw registry directly second. The direct
  fallback is probed: a peer that sends data or hangs up within 300ms of
  connecting (a sandbox network guard) is rejected, so the proxied route's
  error is reported instead of a broken pipe. NO_PROXY exempting the
  registry keeps direct-first.

Tests: route plan table (proxy first, probe flag, NO_PROXY), probedDirectDial
(guard that talks, guard that closes, silent registry usable, dial error),
dialRegistry against a local guard + authenticating CONNECT proxy + pinned
TLS registry (proxy allowed, proxy refusing, raw fallback to a real
registry), sandboxProxyCmdFor matrix, CLI daemon start passes/keeps
PILOT_PROXY_CMD. Reviewer repro (muse-sim-vm, --network none, guard on
34.71.57.205:9000): CONNECT registry.pilotprotocol.network:443 is now the
first attempt and the error names the proxy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(changelog): link the installer change to pilot-protocol/release#49

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(proxy): netproxy credential refresh (common v0.5.15); auto stays on the proxy; daemon start names the proxy error

Credential refresh now comes from common v0.5.15's netproxy instead of the
branch-local proxy policy (internal/proxyconf/policy.go is gone):

- -proxy-cmd / $PILOT_PROXY_CMD / config.json proxy_cmd build the
  resolver with netproxy.WithRefreshCommand. The command runs at startup,
  again once 60s have passed (lookup-driven), and whenever a proxy answers
  407; the rejected connection is then retried once.
- Registry (primary, pool, every redial) and the compat WSS beacon (and its
  reconnects) dial through netproxy.Dialer; daemon-owned HTTP clients use
  netproxy.RefreshingTransport (proxyconf.RoundTripper, loopback direct);
  http.DefaultTransport is configured in place so plugin clients take the
  current credentials and a 407 refreshes them for the next request.
- pkg/daemon: Config.Proxy is the only knob (ProxyPolicy,
  ProxyRefreshInterval, StaticProxyPolicy, NewCommandProxyPolicy removed);
  ResolveProxy takes netproxy options.
- pilotctl daemon start --proxy-cmd (passed as $PILOT_PROXY_CMD, never on
  argv); pilotctl's own registry commands use $PILOT_PROXY_CMD, config
  proxy_cmd or the sandbox default too, so a stale HTTPS_PROXY in its
  environment is refreshed and a 407 is retried.

E2E product gap (phase-2 scenario 2c): with a proxy configured and
-transport=auto, a proxy error during the compat check (407, 403, garbled
answer, proxy unreachable) no longer falls back to udp, which dialed the
raw registry directly past the proxy. SelectTransport picks compat and
returns the proxy error; the daemon logs it at WARN with a hint, and the
registry/compat dial failures end with a hint naming the fix. Fatal
startup errors are logged at ERROR (log.Fatalf printed them at INFO).

pilotctl daemon start notices a daemon that exits during startup instead
of polling until the deadline, and on exit or timeout prints the daemon's
last error and last proxy error from its log with a hint, instead of only
"did not become ready".

gosec: annotate the daemon exec and log read, handle probe Close errors.

Tests: in-process rotating-credential CONNECT proxies for proxyconf
(dial retry, RoundTripper retry, DefaultTransport refresh), pkg/daemon
(registry + WSS reconnect after rotation, timed refresh, MOTD client
retry, auto stays compat on 407/403/unreachable), cmd/daemon (subprocess:
auto + 407 stays on the proxy, mutation-checked), pilotctl (registry dial
follows rotated credentials, proxy-first routes, daemon start failure
reporting with fake daemons, and an end-to-end run of the real daemon
behind a rejecting proxy).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(pilotctl): --json trusted list; warn when an old daemon will ignore HTTPS_PROXY

Two phase-2 E2E onboarding papercuts:

- `pilotctl --json trusted list` ignored --json and printed the table.
- A new pilotctl paired with a daemon that predates -proxy (v1.13.9) in a
  shell with HTTPS_PROXY/ALL_PROXY started it silently; the failure then
  surfaced only as "did not become ready". daemon start now warns that the
  daemon will not use the proxy (not with --proxy off, and not twice when
  an explicit --proxy was already dropped).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(daemon): -transport=auto help says a refusing proxy keeps compat

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(daemon): record the beacon's authenticated node before auth_ok

TestStartCompatModeThroughConnectProxy failed on ubuntu CI with
"beacon authenticated node 0, want 1": the daemon's Start returns as soon
as it reads auth_ok, and the fake beacon stored the node only after
writing it. Store first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(proxy): apps follow rotating proxy credentials via a loopback relay; Muse rejection diagnostics; credential-preferring sandbox command

Phase-3 review findings on #470:

web4-470-apps-inherit-stale-proxy-creds (high). App-store apps are
processes the daemon spawns with its environment, so they kept the
launch-time HTTPS_PROXY credentials: after one rotation every app that
opened a new connection failed ("node online, all apps broken"), also
after a respawn. New internal/proxyconf.Relay: a CONNECT proxy on
127.0.0.1:<random> that opens each tunnel upstream with a netproxy.Dialer
(current credentials, refresh + one retry on a 407 or a garbled answer),
CONNECT only, guarded by its own random credentials, never inside the TLS
tunnel. A proxying daemon runs one; with -proxy-cmd it points
HTTPS_PROXY/https_proxy (and PILOT_PROXY when it is a URL) at it before
any plugin starts, so every app it spawns inherits the relay instead of
credentials. The daemon itself never uses the relay: the refresh command
now runs in the launch environment (proxyconf.CommandSource, same
process-group/timeout/output-cap rules as netproxy's), a refresh that
names the relay is refused, and a remote restart re-execs with the launch
environment.

web4-470-configuretransport-ignores-garbled-rejection. net/http never
passes an unparseable CONNECT answer to OnProxyConnectResponse, so
http.DefaultTransport clients never refreshed on Muse's rejection form and
quoted the proxy's bytes. ConfigureTransport now takes the relay: https
requests tunnel through it (refresh + retry, netproxy's wording in errors,
the relay's upstream error surfaced to the client); http:// keeps going to
the proxy. Doc comments and CHANGELOG corrected.

web4-470-muse-rejection-diagnostics-misdirect. proxyconf.CredentialHint /
UnreadableConnectReply recognise netproxy's "read CONNECT response: ...
(response text withheld)"; daemon.ProxyRefusalHint, the auto-selected WARN
and pilotctl's start-failure hint now name the credentials and proxy_cmd
for it. No hint suggests -transport=udp unconditionally any more.

web4-470-sandbox-cmd-variable-precedence. The sandbox proxy_cmd (pilotctl
and install.sh) now prints whichever of $https_proxy / $HTTPS_PROXY
carries credentials ($https_proxy when both do, the daemon's order when
neither does), so it can never replace a credentialed URL with a bare one.
install.sh differs from release#49 by that one line; #49 must take it.

Tests: real daemon + app-store supervisor + sideloaded app across garbled
rotations (fails with "malformed HTTP status code 4O7" without the relay),
relay unit tests, the reviewer's ConfigureTransport repro, CommandSource,
hints (daemon subprocess, pkg/daemon, pilotctl CLI), and the sandbox
command through bash for 7 variable combinations.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* install.sh: sync with pilot-protocol/release#49 (ae447bc)

Byte-identical copy of the canonical installer on
pilot-protocol/release feat/installer-proxy-transport, so canonical-drift
passes once release#49 merges. Takes this branch's credential-preferring
sandbox proxy_cmd, and adds: no raw-IP registry/beacon in config.json for
compat or auto behind a proxy, downloads retried once after a proxy
credential rotation, proxy_cmd saved for older daemons too, PILOT_PROXY
downloads, the v1.13.x proxy warning pointing at the pilot-sandbox recipe,
and onboarding text that reads replies from send-message --wait and drops
routine appstore --force. Only install.sh changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* install.sh: sync with pilot-protocol/release#49 (67e83fc)

Byte-identical to release@67e83fc (review fixes: root refused under sudo,
no `pilotctl daemon start` advice on proxy-only hosts with a daemon that
cannot use the proxy, the transport stated after the download, --version /
--channel beta installable again).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(proxy): hints that fit a daemon already on proxy_cmd; gosec; install.sh sync with release#49 (c36ad9b)

- pilotctl daemon start: when the daemon re-reads its credentials with a
  proxy command and the proxy still rejects them, the hint names the command
  in use and says to check what it prints from a fresh shell, instead of
  telling the operator to set one (E2E scenario 3: the 407 hint said "give
  the daemon a command that prints the current proxy URL" with config.json
  proxy_cmd in place). The daemon's own CredentialHint is worded for both.
- TestInstallerSavesTheSandboxProxyCmd: install.sh's SANDBOX_PROXY_CMD must
  evaluate to pilotctl's sandboxProxyCmd. A saved proxy_cmd turns off the one
  `daemon start` hands the daemon, so the two must never drift (web4-470
  review: the installer kept the pre-change command).
- gosec: #nosec G204 with the reason on proxyconf.CommandSource (the
  operator's proxy command run with sh -c is the function's purpose), and
  the new G104 warnings (unchecked Close/Remove) in relay.go and pilotctl.
- install.sh: byte-identical to pilot-protocol/release#49 c36ad9b (sandbox
  proxy_cmd only for credentials a fresh shell sees and never over an
  explicit PILOT_PROXY; restart advice stops the running daemon before the
  sandbox recipe; macOS LaunchAgent line conditioned and never sent to the
  Linux-only recipe; truthful --transport auto note for pre-auto daemons).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* proxyconf: name the rotation hint so gosec G101 does not read it as a credential

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(proxyconf): fixed order in the relay garbled-rejection test

TestConfigureTransportViaRelayHandlesGarbledRejection ranged over a map of
the two transports and afterwards restored the proxy to "p-clone", the
password the clone iteration rotates to. Go randomizes map order, so half the
runs restored a password the relay no longer held and the 403 check saw a
garbled 407 instead (architecture-gates race run on 30c034b). Iterate in a
fixed order. -race -count=5 passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* deps: skillinject v0.2.5 (gated Meta Muse injection target)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
Co-authored-by: Claude Opus 5.5 (1M context) <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