From 5b57742707e7cdcfcbf36608a8d9e3e88678feca Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 23 Sep 2026 06:46:59 +0000 Subject: [PATCH 1/8] feat(paseo): bump pinned Paseo CLI to 0.9.1 Adopt the latest Paseo release (0.9.1) by updating the two-value version pin: PASEO_CLI_VERSION in spec.yaml and the matching in-script fallback default in paseo-start.sh (plus its header doc comment). --- .../isolation/acq-kits/paseo/files/home/paseo-start.sh | 4 ++-- integrations/isolation/acq-kits/paseo/spec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh index cb609245..27b0125c 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh @@ -31,7 +31,7 @@ # # Pins are provided via the environment, with an in-script fallback default kept # in sync with the kit spec's documented pin: -# PASEO_CLI_VERSION — @getpaseo/cli version to install (default 0.7.0) +# PASEO_CLI_VERSION — @getpaseo/cli version to install (default 0.9.1) # PASEO_LISTEN — daemon bind address (default 0.0.0.0:6767) # PASEO_RESTART_DELAY — seconds to wait before respawning the daemon (default 5) @@ -75,7 +75,7 @@ case ":$PATH:" in *":$HOME/.npm-global/bin:"*) : ;; *) PATH="$HOME/.npm-global/b export PATH # --- Install the pinned Paseo CLI if it is missing or out of date. ----------- -_ver="${PASEO_CLI_VERSION:-0.7.0}" +_ver="${PASEO_CLI_VERSION:-0.9.1}" _installed_ver="" if command -v paseo >/dev/null 2>&1; then _installed_ver="$(paseo --version 2>/dev/null | sed -n 's/^v//; s/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p; q')" diff --git a/integrations/isolation/acq-kits/paseo/spec.yaml b/integrations/isolation/acq-kits/paseo/spec.yaml index f0cb0886..c8b7b5e4 100644 --- a/integrations/isolation/acq-kits/paseo/spec.yaml +++ b/integrations/isolation/acq-kits/paseo/spec.yaml @@ -182,7 +182,7 @@ commands: - sh - -c - | - PASEO_CLI_VERSION="0.7.0" \ + PASEO_CLI_VERSION="0.9.1" \ sh /home/agent/paseo-start.sh & # Neutral published port (schema hybrid/v1). Publish the single container/guest From 450d77efcea8ca06702cd53a180185022f1a2354 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 23 Sep 2026 23:25:00 +0000 Subject: [PATCH 2/8] fix(paseo): adapt daemon startup for 0.9 --- .../isolation/acq-kits/paseo/README.md | 3 +- .../acq-kits/paseo/TROUBLESHOOTING.md | 25 +++----- .../docs/decisions/paseo-0.9-daemon-run.md | 59 +++++++++++++++++++ .../decisions/paseo-single-port-daemon.md | 6 +- ...isor-owns-daemon-wrapper-pins-worktrees.md | 36 +++++------ .../paseo/files/home/paseo-agent-shim | 44 +++++++------- .../files/home/paseo-register-mounts.mjs | 7 ++- .../acq-kits/paseo/files/home/paseo-start.sh | 51 +++++++++++----- .../isolation/acq-kits/paseo/scripts/verify | 17 ++++-- .../isolation/acq-kits/paseo/spec.yaml | 34 +++++------ 10 files changed, 186 insertions(+), 96 deletions(-) create mode 100644 integrations/isolation/acq-kits/paseo/docs/decisions/paseo-0.9-daemon-run.md mode change 100644 => 100755 integrations/isolation/acq-kits/paseo/scripts/verify diff --git a/integrations/isolation/acq-kits/paseo/README.md b/integrations/isolation/acq-kits/paseo/README.md index 8c9a2cb7..f63ffeb0 100644 --- a/integrations/isolation/acq-kits/paseo/README.md +++ b/integrations/isolation/acq-kits/paseo/README.md @@ -131,8 +131,7 @@ This kit does the best achievable thing: it pins the one global root to a subdirectory of the **first mounted project directory**. When you run the `opencode` wrapper on the `acq run` path (whose working directory is that primary workspace), the kit shim writes `worktrees.root = /.paseo-worktrees` -into `config.json` and **restarts the daemon** so the change takes effect (Paseo -reads `worktrees.root` only at daemon startup — there is no signal to reread it). +into `config.json` and **restarts the daemon worker** so the change takes effect. Before you run it once, worktrees default to `$PASEO_HOME/worktrees`. To override the location, pre-set `worktrees.root` to an absolute path in diff --git a/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md b/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md index d5a62788..c06083b4 100644 --- a/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md +++ b/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md @@ -306,15 +306,11 @@ acq exec -- sh -c 'cat "${PASEO_HOME:-$HOME/.paseo}/config.json"' `acq run opencode ` once. - `worktrees.root` points at `/.paseo-worktrees` but new worktrees still land elsewhere → the daemon may not have restarted to pick it up. The wrapper - bounces the daemon only when the value **changes**; force a bounce by killing - the daemon child (the supervisor respawns it): + restarts the daemon only when the value **changes**; force a restart with the + supported CLI command: ```bash - acq exec -- sh -c ' - for pid in $(pgrep -u "$(id -u)" -f "paseo daemon start"); do - tr "\0" " " -- paseo daemon restart --home /home/agent/.paseo ``` - **The root must be absolute.** A relative `worktrees.root` resolves against @@ -353,18 +349,15 @@ serves the API + WebSocket + UI locally; `curl http://127.0.0.1:6767/api/health` returns 200 throughout. If the **host** browser sees a refused connection, that is a port-mapping issue (see "The browser UI never loads" above), not the relay. -**Fix.** The kit now sets `PASEO_RELAY_ENABLED=false` in `spec.yaml` (read at -daemon config load, precedence over persisted config, survives restarts/self- -updates), so the relay is off by default. If you are running an older sandbox that -predates this, disable it live and bounce the daemon: +**Fix.** The kit sets `PASEO_RELAY_ENABLED=false` in `spec.yaml` and persists +`daemon.relay.enabled=false` in config.json for Paseo 0.9 managed/config tooling, +so the relay is off by default. If you are running an older sandbox that predates +this, disable it live and restart the daemon: ```bash acq exec -- sh -c ' - node -e "const f=process.env.HOME+\"/.paseo/config.json\";const c=require(f);(c.daemon??={}).relay={enabled:false};require(\"fs\").writeFileSync(f,JSON.stringify(c,null,2)+\"\n\",{mode:0o600})" - for pid in $(pgrep -u "$(id -u)" -f "paseo daemon start"); do - tr "\0" " " /.paseo-worktrees`, and writes - it to `config.json`. **If the value changed**, it restarts the daemon so the new - root takes effect. (The pin/bounce lives in the shim; see + it to `config.json`. **If the value changed**, it restarts the daemon worker so + the new root takes effect. (The pin/restart lives in the shim; see `paseo-mixin-generic-wrapper.md` for the wrapper/shim split.) ### The bounce mechanism (why it's safe) @@ -44,21 +44,21 @@ The process tree is: ``` sh -c (marker: supervisor:paseo-daemon) ← the startup script's respawn loop - └─ paseo daemon start --foreground ← node CLI + └─ paseo daemon run ← node CLI └─ node supervisor-entrypoint.js ← "Paseo Supervisor"; holds $PASEO_HOME/paseo.pid └─ node daemon-worker.js ← the listener on :6767 (SIGTERM = graceful) ``` -The wrapper kills the PIDs whose argv matches `paseo daemon start` **but do NOT -carry the `supervisor:paseo-daemon` marker** — i.e. the Paseo-owned tree, never -our shell respawn loop. `kill` (SIGTERM) lets `daemon-worker` shut down -gracefully, free `:6767`, and release the PID lock. Our shell loop then respawns -`paseo daemon start`, which re-reads `config.json` and picks up the new -`worktrees.root`. +The wrapper first uses `paseo daemon restart`, which asks Paseo's own supervisor +to restart the worker and re-read `config.json`. If that command cannot run, the +fallback kills PIDs whose argv matches `paseo daemon run` **but do NOT carry the +`supervisor:paseo-daemon` marker** — i.e. the Paseo-owned tree, never our shell +respawn loop. `kill` (SIGTERM) lets `daemon-worker` shut down gracefully and free +`:6767`. The fallback also clears `paseo.pid` before the shell loop respawns +`paseo daemon run`, avoiding stale-lock races. -The PID lock is stale-tolerant (`acquirePidLock` reclaims a dead owner's lock and -treats a >5-min-old lock as stale), so even an ungraceful death does not deadlock -the relaunch. +The normal `paseo daemon restart` path keeps the outer supervisor and PID lock +intact; the marker-scoped kill is only a recovery fallback. ## Alternatives considered @@ -79,8 +79,8 @@ the relaunch. daemon bounce (idempotent; no bounce if unchanged). - Two independent "supervisors" exist conceptually — our shell respawn loop and Paseo's own `supervisor-entrypoint.js` — but they nest cleanly (ours respawns - the whole `paseo daemon start` tree; Paseo's manages the worker within one - start). The kill is marker-scoped so our loop is never the target. + the whole `paseo daemon run` tree; Paseo's manages the worker within one run). + The fallback kill is marker-scoped so our loop is never the target. ## Links diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim b/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim index d34dcbb7..037f9351 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim @@ -23,11 +23,10 @@ # relative root resolves against PASEO_HOME. On `acq run ` the entrypoint # runs with cwd = the primary (first mounted) workspace, so the shim computes # /.paseo-worktrees and writes it to $PASEO_HOME/config.json (via -# paseo-set-worktrees-root.mjs). The daemon reads worktrees.root ONLY at boot -# (no SIGHUP / no runtime reread), so when the value CHANGES the shim bounces -# the daemon — it kills the supervised daemon child (never the supervisor), -# and the startup script's supervisor:paseo-daemon loop respawns it, re-reading -# the new root. +# paseo-set-worktrees-root.mjs). The daemon reads worktrees.root at worker +# start, so when the value CHANGES the shim restarts the daemon worker via +# `paseo daemon restart` (falling back to a marker-scoped process kill only +# if the CLI restart cannot run). # 2. PRINT HOST-CONNECT INSTRUCTIONS and, when it IS PID 1 (the `acq run` # entrypoint), HOLD the foreground so that session's sandbox — and thus the # daemon/UI — stays up. @@ -79,13 +78,12 @@ if [ -f "$SET_ROOT_SCRIPT" ] && command -v node >/dev/null 2>&1; then esac fi -# 2. If the root changed, bounce the daemon so it re-reads config.json. We kill -# ONLY the daemon process tree, never the startup script's supervisor loop, so -# the supervisor relaunches the daemon with the new worktrees.root. The -# supervisor loop's argv carries the marker "supervisor:paseo-daemon"; the -# actual daemon is a `paseo daemon start --foreground ...` CHILD of that loop. -# We select PIDs whose argv matches the daemon-start pattern but EXCLUDE the -# supervisor marker, and kill just those. +# 2. If the root changed, restart the daemon worker so it re-reads config.json. +# Paseo 0.9 exposes `paseo daemon restart` for this. If the daemon is wedged or +# the CLI surface changes again, fall back to killing ONLY the supervised Paseo +# tree, never the startup script's supervisor loop. The supervisor loop's argv +# carries the marker "supervisor:paseo-daemon"; the actual daemon child is a +# `paseo daemon run` tree beneath that loop. daemon_up() { # /api/health is unauthenticated and returns 200 when the daemon is listening. curl -fsS "http://127.0.0.1:$PASEO_PORT/api/health" >/dev/null 2>&1 @@ -130,14 +128,20 @@ ui_ready() { } if [ -n "$worktrees_changed" ]; then echo "paseo: pinned Paseo worktree root to $WORKTREES_ROOT; restarting the daemon to apply it ..." - for _pid in $(pgrep -u "$(id -u)" -f "paseo daemon start" 2>/dev/null); do - # Skip the supervisor loop (its cmdline carries the marker). - if tr '\0' ' ' < "/proc/$_pid/cmdline" 2>/dev/null | grep -q "supervisor:paseo-daemon"; then - continue - fi - kill "$_pid" 2>/dev/null || true - done - # Wait for the supervisor to bring it back on the new root (bounded). + if command -v paseo >/dev/null 2>&1 && paseo daemon restart --home "$PASEO_HOME_DIR" --timeout 30 >/dev/null 2>&1; then + : + else + echo "paseo: daemon restart command failed; falling back to supervisor respawn" >&2 + for _pid in $(pgrep -u "$(id -u)" -f "paseo daemon run" 2>/dev/null); do + # Skip the supervisor loop (its cmdline carries the marker). + if tr '\0' ' ' < "/proc/$_pid/cmdline" 2>/dev/null | grep -q "supervisor:paseo-daemon"; then + continue + fi + kill "$_pid" 2>/dev/null || true + done + rm -f "$PASEO_HOME_DIR/paseo.pid" 2>/dev/null || true + fi + # Wait for the daemon to return on the new root (bounded). _i=0 while [ "$_i" -lt 30 ]; do daemon_up && break diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs b/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs index 2f6ea579..4f30f470 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs @@ -225,7 +225,12 @@ async function main() { let client; try { - client = await connectToDaemon({}); + client = await connectToDaemon({ + target: { + kind: "instance", + home: process.env.PASEO_HOME || `${process.env.HOME}/.paseo`, + }, + }); } catch (err) { log(`could not connect to the Paseo daemon: ${err.message}`); return; // fail-open (daemon may still be coming up on a very early boot) diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh index 27b0125c..23f36836 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh @@ -3,10 +3,11 @@ # daemon (which serves the API, the WebSocket, AND the bundled web UI on one port) # so it auto-restarts if it exits. # -# SCOPE: this startup script manages the Paseo daemon only. It supervises a single -# `paseo daemon start --foreground --listen 0.0.0.0:6767 --web-ui`. The daemon -# runs under a respawn loop, so a crash, a self-update, or a wrapper-triggered -# bounce (to apply a new worktrees.root) self-heals. Because this script runs as a +# SCOPE: this startup script manages the Paseo daemon only. It persists the +# daemon settings Paseo 0.9 reads from config.json, then supervises a single +# `paseo daemon run`. The daemon runs under a respawn loop, so a crash, a +# self-update, or a wrapper-triggered restart (to apply a new worktrees.root) +# self-heals. Because this script runs as a # `startup` command — which fires on EVERY sandbox start, including a detached # `acq create` with nobody attached, and runs under the sandbox's tini keepalive # (PID 1), independent of any interactive session — the daemon + UI come up on @@ -53,6 +54,8 @@ PASEO_PORT="$(printf '%s' "$PASEO_LISTEN" | sed -n 's/.*:\([0-9][0-9]*\)$/\1/p') # directory with the daemon so the web UI pre-lists them). Backend-agnostic; see # the file header and docs/decisions/prepopulate-projects-from-mounts.md. REGISTER_MOUNTS_SCRIPT="${HOME}/paseo-register-mounts.mjs" +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/paseo" +mkdir -p "$STATE_DIR" 2>/dev/null || true # npm global installs land in this prefix's bin, which is NOT on the startup # shell's PATH by default. Put both possible bins on PATH up front so a @@ -142,15 +145,31 @@ command -v paseo >/dev/null 2>&1 || { exit 0 # never fail the sandbox over an optional UI } +# --- Persist daemon configuration that Paseo 0.9 no longer accepts as flags. --- +# Paseo 0.9 removed `paseo daemon start --listen/--web-ui/--foreground` and, in +# managed `daemon start` mode, strips PASEO_* daemon-setting env vars before the +# worker starts. Persist the settings the sandbox depends on so both `daemon run` +# and any local status/config tooling agree on the expected shape. +PASEO_HOME_DIR="${PASEO_HOME:-$HOME/.paseo}" +mkdir -p "$PASEO_HOME_DIR" 2>/dev/null || true +if ! paseo daemon config set --home "$PASEO_HOME_DIR" --string daemon.listen "$PASEO_LISTEN" >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: warning: could not persist daemon.listen=$PASEO_LISTEN; see $STATE_DIR/paseo-config.log" >&2 +fi +if ! paseo daemon config set --home "$PASEO_HOME_DIR" features.webUi.enabled true >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: warning: could not persist features.webUi.enabled=true; see $STATE_DIR/paseo-config.log" >&2 +fi +if ! paseo daemon config set --home "$PASEO_HOME_DIR" daemon.relay.enabled false >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: warning: could not persist daemon.relay.enabled=false; see $STATE_DIR/paseo-config.log" >&2 +fi + # --- Supervise the Paseo daemon (idempotent). -------------------------------- # The daemon runs UNSECURED (no PASEO_PASSWORD) — the sandbox is the security -# boundary, and the published host port is loopback only. `--foreground` keeps it -# in the foreground so the supervisor can see it exit and restart it (after a -# crash, a self-update, or a wrapper-triggered bounce to apply a new -# worktrees.root). `--web-ui` (redundant with PASEO_WEB_UI_ENABLED=true from the -# kit env, set for belt-and-suspenders) serves the bundled browser UI on the same -# port. No systemd — this script is launched in the background by the kit's -# startup command, so the supervisor loop runs backgrounded here and lives for the +# boundary, and the published host port is loopback only. `daemon run` is Paseo +# 0.9's foreground/deployment launch path: it keeps daemon-setting env overrides +# available and runs Paseo's own supervisor-entrypoint (IPC restart + crash +# restart) in the foreground, so our outer loop can still see it exit and relaunch +# it. No systemd — this script is launched in the background by the kit's startup +# command, so the supervisor loop runs backgrounded here and lives for the # container's lifetime, held open by the sandbox's tini keepalive (PID 1) # regardless of whether anyone attaches. # @@ -168,14 +187,18 @@ supervisor_running() { pgrep -u "$(id -u)" -f "supervisor:$1" >/dev/null 2>&1; } DAEMON_LOG="$HOME/.local/state/paseo/paseo-daemon.log" mkdir -p "$(dirname "$DAEMON_LOG")" 2>/dev/null || true -# Start the daemon supervisor unless already running. `paseo daemon start -# --foreground` blocks, so the supervisor can see it exit and relaunch it. +# Start the daemon supervisor unless already running. `paseo daemon run` blocks, +# so the supervisor can see it exit and relaunch it. # argv to the inner sh -c: $0=marker, $1=RESTART_DELAY, $2=PASEO_LISTEN. if ! supervisor_running paseo-daemon; then ( sh -c ' while :; do echo "[supervisor] starting paseo daemon at $(date -u +%FT%TZ)" - paseo daemon start --foreground --listen "$2" --web-ui || true + # If a prior daemon died ungracefully, clear its stale PID lock before + # relaunching. Paseo treats a dead owner lock as stale, but clearing it + # here avoids repeated lock-acquire races in a tight respawn loop. + rm -f "${PASEO_HOME:-$HOME/.paseo}/paseo.pid" + PASEO_LISTEN="$2" PASEO_WEB_UI_ENABLED=true PASEO_RELAY_ENABLED=false paseo daemon run --home "${PASEO_HOME:-$HOME/.paseo}" || true echo "[supervisor] paseo daemon exited; restarting in ${1}s" sleep "$1" done diff --git a/integrations/isolation/acq-kits/paseo/scripts/verify b/integrations/isolation/acq-kits/paseo/scripts/verify old mode 100644 new mode 100755 index e8591d99..2e462dc0 --- a/integrations/isolation/acq-kits/paseo/scripts/verify +++ b/integrations/isolation/acq-kits/paseo/scripts/verify @@ -352,9 +352,16 @@ case "$wrapper_path" in esac [ -n "$(in_sbx 'test -x ~/.local/bin/opencode && echo yes')" ] \ && ok "wrapper file is executable" \ - || bad "~/.local/bin/opencode missing or not executable" + || bad "\$HOME/.local/bin/opencode missing or not executable" info "5. Paseo daemon + web UI (started by the startup supervisor, on its own)" +removed_flags="$(in_sbx 'grep -Ev "^[[:space:]]*#" /home/agent/paseo-start.sh 2>/dev/null | grep -E "paseo daemon start|--listen|--web-ui|--foreground" || true')" +if [ -n "$removed_flags" ]; then + bad "startup script still references removed Paseo 0.9 launch flags:" + printf '%s\n' "$removed_flags" | sed 's/^/ | /' +else + ok "startup script uses Paseo 0.9-compatible daemon launch surface" +fi if [ -n "$daemon_up" ]; then ok "daemon answers /api/health on 127.0.0.1:$PASEO_PORT (started by the startup supervisor)" else @@ -365,7 +372,7 @@ fi if [ "$(in_sbx "curl -fsS http://127.0.0.1:$PASEO_PORT/ >/dev/null 2>&1 && echo yes")" = "yes" ]; then ok "bundled web UI is served on the same port ($PASEO_PORT)" elif [ -n "$daemon_up" ]; then - warn "daemon is up but the web UI root did not return 200 (PASEO_WEB_UI_ENABLED / --web-ui?)" + warn "daemon is up but the web UI root did not return 200 (PASEO_WEB_UI_ENABLED / features.webUi.enabled?)" else skip "web UI check (daemon not up)" fi @@ -648,14 +655,14 @@ else fi # (b) daemon self-heal: if the daemon dies, supervisor:paseo-daemon must relaunch -# it. Kill ONLY the daemon tree (argv matches "paseo daemon start" but NOT the +# it. Kill ONLY the daemon tree (argv matches "paseo daemon run" but NOT the # supervisor marker), assert the supervisor survives and the daemon comes back. if [ -z "$daemon_up" ]; then skip "self-heal guard: daemon was not up to kill" else sup_before="$(in_sbx 'pgrep -u "$(id -u)" -f "supervisor:paseo-daemon" | sort | tr "\n" " "')" in_sbx ' - for pid in $(pgrep -u "$(id -u)" -f "paseo daemon start"); do + for pid in $(pgrep -u "$(id -u)" -f "paseo daemon run"); do if tr "\0" " " /dev/null | grep -q "supervisor:paseo-daemon"; then continue fi @@ -714,7 +721,7 @@ else [ -f "$CLIENT" ] || exit 0 CLIENT_URL="file://$CLIENT" node --input-type=module -e " const { connectToDaemon } = await import(process.env.CLIENT_URL); - const c = await connectToDaemon({}); + const c = await connectToDaemon({ target: { kind: "instance", home: process.env.PASEO_HOME || (process.env.HOME + "/.paseo") } }); try { const r = await c.listProjects(); for (const p of r.projects ?? []) { diff --git a/integrations/isolation/acq-kits/paseo/spec.yaml b/integrations/isolation/acq-kits/paseo/spec.yaml index c8b7b5e4..0135cf71 100644 --- a/integrations/isolation/acq-kits/paseo/spec.yaml +++ b/integrations/isolation/acq-kits/paseo/spec.yaml @@ -15,12 +15,12 @@ # # DESIGN (see docs/decisions/): # - The STARTUP script (paseo-start.sh) owns the daemon: on every sandbox start -# it installs/updates the pinned Paseo CLI and supervises the daemon with -# `paseo daemon start --foreground --listen 0.0.0.0:6767 --web-ui`. A single -# Paseo process serves the daemon API, the WebSocket, AND the bundled web UI, -# all on ONE container port (6767). Because a `startup` command fires on EVERY -# start — including a detached `acq create` with nobody attached, held open by -# the sandbox's tini keepalive (PID 1) — the daemon + UI come up on their own. +# it installs/updates the pinned Paseo CLI, persists the daemon settings Paseo +# 0.9 reads from config.json, and supervises `paseo daemon run`. A single Paseo +# process serves the daemon API, the WebSocket, AND the bundled web UI, all on +# ONE container port (6767). Because a `startup` command fires on EVERY start — +# including a detached `acq create` with nobody attached, held open by the +# sandbox's tini keepalive (PID 1) — the daemon + UI come up on their own. # There is deliberately NO install phase (see the install note below). # - The sandbox entrypoint runs the bare command `opencode`. This kit drops a # THIN, agent-named WRAPPER at ~/.local/bin/opencode, which is first on the @@ -63,10 +63,10 @@ # runs). The daemon reads worktrees.root ONCE at boot (no SIGHUP / no runtime # reread — verified against the Paseo source), so the shim — which runs in the # primary-workspace cwd on `acq run` — writes worktrees.root = /.paseo-worktrees -# into config.json and BOUNCES the daemon (kills the supervised child; the -# supervisor respawns it and re-reads config). The supervisor starts the daemon -# immediately with Paseo's default root; the shim rewrites + restarts only when -# the value actually changes. See docs/decisions/worktrees-root-global-only.md and +# into config.json and restarts the daemon worker with `paseo daemon restart`. +# The supervisor starts the daemon immediately with Paseo's default root; the shim +# rewrites + restarts only when the value actually changes. See +# docs/decisions/worktrees-root-global-only.md and # supervisor-owns-daemon-wrapper-pins-worktrees.md. # # PROJECT PRE-POPULATION: at startup (after the daemon answers /api/health) the @@ -196,10 +196,11 @@ publishedPorts: protocol: tcp name: paseo -# Non-secret guest env. PASEO_WEB_UI_ENABLED enables the bundled UI (equivalent to -# the daemon's --web-ui flag; both are set, harmlessly). PASEO_LISTEN fixes the -# bind address/port. ONNXRUNTIME_NODE_INSTALL=skip mirrors Paseo's own Docker -# image and keeps the voice dep's install from attempting an onnxruntime fetch. +# Non-secret guest env. PASEO_WEB_UI_ENABLED enables the bundled UI on the +# `daemon run` path, and paseo-start.sh also persists the same value into +# config.json for Paseo 0.9 managed/config tooling. PASEO_LISTEN fixes the bind +# address/port. ONNXRUNTIME_NODE_INSTALL=skip mirrors Paseo's own Docker image and +# keeps the voice dep's install from attempting an onnxruntime fetch. # No secret goes here (PASEO_PASSWORD is intentionally unset — unsecured; see # AUTH note above). # @@ -226,9 +227,8 @@ publishedPorts: # daemon logs an endless stream of `relay_error` / `relay_control_disconnected` # (ECONNRESET). The relay is unnecessary here — the host reaches the web UI over # the loopback-published port (see publishedPorts + README "Reaching it from the -# host"), not through Paseo's cloud relay. The env var is read at daemon config -# load (server/config.js) and takes precedence over persisted config, so it holds -# across daemon restarts/self-updates without editing config.json. +# host"), not through Paseo's cloud relay. paseo-start.sh also persists this to +# config.json because Paseo 0.9 managed launches strip daemon-setting env vars. environment: PASEO_WEB_UI_ENABLED: "true" PASEO_LISTEN: "0.0.0.0:6767" From 5dc3a6e6466902c038f4fba225d623d245dda38a Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 23 Sep 2026 23:32:07 +0000 Subject: [PATCH 3/8] test(paseo): fix project listing for 0.9 verify --- integrations/isolation/acq-kits/paseo/scripts/verify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/isolation/acq-kits/paseo/scripts/verify b/integrations/isolation/acq-kits/paseo/scripts/verify index 2e462dc0..51d91503 100755 --- a/integrations/isolation/acq-kits/paseo/scripts/verify +++ b/integrations/isolation/acq-kits/paseo/scripts/verify @@ -721,7 +721,7 @@ else [ -f "$CLIENT" ] || exit 0 CLIENT_URL="file://$CLIENT" node --input-type=module -e " const { connectToDaemon } = await import(process.env.CLIENT_URL); - const c = await connectToDaemon({ target: { kind: "instance", home: process.env.PASEO_HOME || (process.env.HOME + "/.paseo") } }); + const c = await connectToDaemon({ target: { kind: \"instance\", home: process.env.PASEO_HOME || (process.env.HOME + \"/.paseo\") } }); try { const r = await c.listProjects(); for (const p of r.projects ?? []) { From 6c0883b7159149433d57a7b7e249a6335db4a975 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 30 Sep 2026 17:30:50 +0000 Subject: [PATCH 4/8] feat(paseo): bump pinned Paseo CLI to 0.10.2 Update the Paseo acq kit pin from 0.9.1 to 0.10.2 in the startup spec and fallback script default. Release notes reviewed: 0.10.x keeps the 0.9 daemon-run/config approach while adding OpenCode v2 support and daemon robustness fixes, so no kit wiring changes are needed. Co-authored-by: OpenCode --- .../isolation/acq-kits/paseo/files/home/paseo-start.sh | 4 ++-- integrations/isolation/acq-kits/paseo/spec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh index 23f36836..3a6ce24a 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh @@ -32,7 +32,7 @@ # # Pins are provided via the environment, with an in-script fallback default kept # in sync with the kit spec's documented pin: -# PASEO_CLI_VERSION — @getpaseo/cli version to install (default 0.9.1) +# PASEO_CLI_VERSION — @getpaseo/cli version to install (default 0.10.2) # PASEO_LISTEN — daemon bind address (default 0.0.0.0:6767) # PASEO_RESTART_DELAY — seconds to wait before respawning the daemon (default 5) @@ -78,7 +78,7 @@ case ":$PATH:" in *":$HOME/.npm-global/bin:"*) : ;; *) PATH="$HOME/.npm-global/b export PATH # --- Install the pinned Paseo CLI if it is missing or out of date. ----------- -_ver="${PASEO_CLI_VERSION:-0.9.1}" +_ver="${PASEO_CLI_VERSION:-0.10.2}" _installed_ver="" if command -v paseo >/dev/null 2>&1; then _installed_ver="$(paseo --version 2>/dev/null | sed -n 's/^v//; s/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p; q')" diff --git a/integrations/isolation/acq-kits/paseo/spec.yaml b/integrations/isolation/acq-kits/paseo/spec.yaml index 0135cf71..6f5c9664 100644 --- a/integrations/isolation/acq-kits/paseo/spec.yaml +++ b/integrations/isolation/acq-kits/paseo/spec.yaml @@ -182,7 +182,7 @@ commands: - sh - -c - | - PASEO_CLI_VERSION="0.9.1" \ + PASEO_CLI_VERSION="0.10.2" \ sh /home/agent/paseo-start.sh & # Neutral published port (schema hybrid/v1). Publish the single container/guest From 18179531ed5e251ca02d2923240e5a91ad416ce6 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 30 Sep 2026 17:40:18 +0000 Subject: [PATCH 5/8] fix(paseo): harden daemon config persistence Make startup fail closed if required daemon settings cannot be written and verified, add a shared config-json lock for startup and worktree-root writes, and stop deleting the daemon PID lock during ordinary respawn. Add live verify coverage for the persisted daemon settings and focused helper tests for config preservation and lock contention. Co-authored-by: OpenCode --- .../docs/decisions/paseo-0.9-daemon-run.md | 5 +- ...isor-owns-daemon-wrapper-pins-worktrees.md | 8 ++- .../paseo/files/home/paseo-agent-shim | 7 ++- .../files/home/paseo-set-worktrees-root.mjs | 54 +++++++++++++---- .../acq-kits/paseo/files/home/paseo-start.sh | 48 ++++++++++++--- .../isolation/acq-kits/paseo/scripts/verify | 7 +++ .../tests/paseo-set-worktrees-root.test.mjs | 58 +++++++++++++++++++ 7 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 integrations/isolation/acq-kits/paseo/tests/paseo-set-worktrees-root.test.mjs diff --git a/integrations/isolation/acq-kits/paseo/docs/decisions/paseo-0.9-daemon-run.md b/integrations/isolation/acq-kits/paseo/docs/decisions/paseo-0.9-daemon-run.md index 4b57cd41..a1d3d0ef 100644 --- a/integrations/isolation/acq-kits/paseo/docs/decisions/paseo-0.9-daemon-run.md +++ b/integrations/isolation/acq-kits/paseo/docs/decisions/paseo-0.9-daemon-run.md @@ -34,7 +34,10 @@ Before launching it, persist the settings the sandbox depends on into Keep the matching environment variables on the `daemon run` invocation as a belt-and-suspenders deployment override, but treat persisted config as the -source of truth for Paseo 0.9 managed/config tooling. +source of truth for Paseo 0.9 managed/config tooling. Startup takes the shared +`$PASEO_HOME/.config-json.lock`, writes the settings, and reads them back before +launching the daemon; if the required settings cannot be verified, it does not +start an unauthenticated daemon with unknown relay/listen defaults. When the entrypoint shim changes `worktrees.root`, prefer the supported `paseo daemon restart` command so Paseo restarts the worker and re-reads diff --git a/integrations/isolation/acq-kits/paseo/docs/decisions/supervisor-owns-daemon-wrapper-pins-worktrees.md b/integrations/isolation/acq-kits/paseo/docs/decisions/supervisor-owns-daemon-wrapper-pins-worktrees.md index 67ea430e..574c5153 100644 --- a/integrations/isolation/acq-kits/paseo/docs/decisions/supervisor-owns-daemon-wrapper-pins-worktrees.md +++ b/integrations/isolation/acq-kits/paseo/docs/decisions/supervisor-owns-daemon-wrapper-pins-worktrees.md @@ -54,11 +54,13 @@ to restart the worker and re-read `config.json`. If that command cannot run, the fallback kills PIDs whose argv matches `paseo daemon run` **but do NOT carry the `supervisor:paseo-daemon` marker** — i.e. the Paseo-owned tree, never our shell respawn loop. `kill` (SIGTERM) lets `daemon-worker` shut down gracefully and free -`:6767`. The fallback also clears `paseo.pid` before the shell loop respawns -`paseo daemon run`, avoiding stale-lock races. +`:6767`. The fallback clears `paseo.pid` only after it actually kills a daemon +process, so it does not delete a live worker's lock during ordinary respawn. The normal `paseo daemon restart` path keeps the outer supervisor and PID lock -intact; the marker-scoped kill is only a recovery fallback. +intact; the marker-scoped kill is only a recovery fallback. Both startup config +writes and wrapper worktree-root writes use the same coarse config lock before +modifying `$PASEO_HOME/config.json`. ## Alternatives considered diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim b/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim index 037f9351..b6989a1a 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim @@ -132,14 +132,17 @@ if [ -n "$worktrees_changed" ]; then : else echo "paseo: daemon restart command failed; falling back to supervisor respawn" >&2 + killed_daemon="" for _pid in $(pgrep -u "$(id -u)" -f "paseo daemon run" 2>/dev/null); do # Skip the supervisor loop (its cmdline carries the marker). if tr '\0' ' ' < "/proc/$_pid/cmdline" 2>/dev/null | grep -q "supervisor:paseo-daemon"; then continue fi - kill "$_pid" 2>/dev/null || true + if kill "$_pid" 2>/dev/null; then + killed_daemon=yes + fi done - rm -f "$PASEO_HOME_DIR/paseo.pid" 2>/dev/null || true + [ -n "$killed_daemon" ] && rm -f "$PASEO_HOME_DIR/paseo.pid" 2>/dev/null || true fi # Wait for the daemon to return on the new root (bounded). _i=0 diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs b/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs index 10f0a566..2dd99a80 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs @@ -24,10 +24,11 @@ // SAFETY: this is a read-modify-write of a JSON file. It preserves every existing // key, only sets/overwrites `worktrees.root` (and creates the `worktrees` object // if absent). It refuses a non-absolute root (Paseo would resolve it against -// PASEO_HOME, defeating the purpose). It writes atomically (temp file + rename) -// with private 0600 perms, matching how Paseo itself stores config.json. +// PASEO_HOME, defeating the purpose). It takes the same coarse lock used by the +// startup script, then writes atomically (temp file + rename) with private 0600 +// perms, matching how Paseo itself stores config.json. -import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from "node:fs"; +import { mkdirSync, readFileSync, renameSync, rmdirSync, writeFileSync, existsSync } from "node:fs"; import { homedir } from "node:os"; import path from "node:path"; @@ -59,16 +60,33 @@ function resolvePaseoHome(explicit) { return path.join(homedir(), ".paseo"); } -function main() { - const { root, paseoHome: paseoHomeArg } = parseArgs(process.argv.slice(2)); - - if (!path.isAbsolute(root)) { - // A relative root would be resolved by Paseo against PASEO_HOME, not the - // project — refuse rather than silently mis-place worktrees. - throw new Error(`--root must be an absolute path (got: ${root})`); +function withConfigLock(paseoHome, fn) { + const lockDir = path.join(paseoHome, ".config-json.lock"); + const timeoutMs = Number.parseInt(process.env.PASEO_CONFIG_LOCK_TIMEOUT_MS || "30000", 10); + const deadline = Date.now() + timeoutMs; + while (true) { + try { + mkdirSync(lockDir); + break; + } catch (err) { + if (err.code !== "EEXIST" || Date.now() >= deadline) { + throw new Error(`cannot lock ${path.join(paseoHome, "config.json")}: ${err.message}`); + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250); + } + } + try { + return fn(); + } finally { + try { + rmdirSync(lockDir); + } catch { + // Best effort cleanup; a later run times out rather than racing config writes. + } } +} - const paseoHome = resolvePaseoHome(paseoHomeArg); +function updateConfig(root, paseoHome) { const configPath = path.join(paseoHome, "config.json"); // Read the existing config if present. The daemon initializes config.json on @@ -128,6 +146,20 @@ function main() { process.stdout.write("changed\n"); } +function main() { + const { root, paseoHome: paseoHomeArg } = parseArgs(process.argv.slice(2)); + + if (!path.isAbsolute(root)) { + // A relative root would be resolved by Paseo against PASEO_HOME, not the + // project — refuse rather than silently mis-place worktrees. + throw new Error(`--root must be an absolute path (got: ${root})`); + } + + const paseoHome = resolvePaseoHome(paseoHomeArg); + mkdirSync(paseoHome, { recursive: true }); + withConfigLock(paseoHome, () => updateConfig(root, paseoHome)); +} + try { main(); } catch (err) { diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh index 3a6ce24a..0f2f14c4 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh @@ -152,14 +152,49 @@ command -v paseo >/dev/null 2>&1 || { # and any local status/config tooling agree on the expected shape. PASEO_HOME_DIR="${PASEO_HOME:-$HOME/.paseo}" mkdir -p "$PASEO_HOME_DIR" 2>/dev/null || true +CONFIG_LOCK_DIR="$PASEO_HOME_DIR/.config-json.lock" +_have_config_lock="" +cleanup_config_lock() { + [ "$_have_config_lock" = yes ] && rmdir "$CONFIG_LOCK_DIR" 2>/dev/null || true +} +trap cleanup_config_lock EXIT HUP INT TERM +acquire_config_lock() { + _deadline=$(( $(date +%s) + 30 )) + while ! mkdir "$CONFIG_LOCK_DIR" 2>/dev/null; do + [ "$(date +%s)" -ge "$_deadline" ] && return 1 + sleep 1 + done + _have_config_lock=yes +} +release_config_lock() { + cleanup_config_lock + _have_config_lock="" +} + +config_ok=yes +if ! acquire_config_lock; then + echo "paseo: error: could not lock $PASEO_HOME_DIR/config.json; not starting unverified daemon" >&2 + exit 0 +fi if ! paseo daemon config set --home "$PASEO_HOME_DIR" --string daemon.listen "$PASEO_LISTEN" >>"$STATE_DIR/paseo-config.log" 2>&1; then - echo "paseo: warning: could not persist daemon.listen=$PASEO_LISTEN; see $STATE_DIR/paseo-config.log" >&2 + echo "paseo: error: could not persist daemon.listen=$PASEO_LISTEN; see $STATE_DIR/paseo-config.log" >&2 + config_ok="" fi if ! paseo daemon config set --home "$PASEO_HOME_DIR" features.webUi.enabled true >>"$STATE_DIR/paseo-config.log" 2>&1; then - echo "paseo: warning: could not persist features.webUi.enabled=true; see $STATE_DIR/paseo-config.log" >&2 + echo "paseo: error: could not persist features.webUi.enabled=true; see $STATE_DIR/paseo-config.log" >&2 + config_ok="" fi if ! paseo daemon config set --home "$PASEO_HOME_DIR" daemon.relay.enabled false >>"$STATE_DIR/paseo-config.log" 2>&1; then - echo "paseo: warning: could not persist daemon.relay.enabled=false; see $STATE_DIR/paseo-config.log" >&2 + echo "paseo: error: could not persist daemon.relay.enabled=false; see $STATE_DIR/paseo-config.log" >&2 + config_ok="" +fi +if ! PASEO_HOME_DIR="$PASEO_HOME_DIR" PASEO_EXPECTED_LISTEN="$PASEO_LISTEN" node -e 'const fs = require("fs"); const path = require("path"); const config = JSON.parse(fs.readFileSync(path.join(process.env.PASEO_HOME_DIR, "config.json"), "utf8")); if (config?.daemon?.listen !== process.env.PASEO_EXPECTED_LISTEN || config?.features?.webUi?.enabled !== true || config?.daemon?.relay?.enabled !== false) process.exit(1);' >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: error: config.json does not contain required daemon settings; not starting unverified daemon" >&2 + config_ok="" +fi +release_config_lock +if [ -z "$config_ok" ]; then + exit 0 fi # --- Supervise the Paseo daemon (idempotent). -------------------------------- @@ -194,10 +229,9 @@ if ! supervisor_running paseo-daemon; then ( sh -c ' while :; do echo "[supervisor] starting paseo daemon at $(date -u +%FT%TZ)" - # If a prior daemon died ungracefully, clear its stale PID lock before - # relaunching. Paseo treats a dead owner lock as stale, but clearing it - # here avoids repeated lock-acquire races in a tight respawn loop. - rm -f "${PASEO_HOME:-$HOME/.paseo}/paseo.pid" + # Let Paseo own its PID lock. It already reclaims dead-owner/stale locks, + # and deleting the lock here could remove a live worker lock during + # restart races. PASEO_LISTEN="$2" PASEO_WEB_UI_ENABLED=true PASEO_RELAY_ENABLED=false paseo daemon run --home "${PASEO_HOME:-$HOME/.paseo}" || true echo "[supervisor] paseo daemon exited; restarting in ${1}s" sleep "$1" diff --git a/integrations/isolation/acq-kits/paseo/scripts/verify b/integrations/isolation/acq-kits/paseo/scripts/verify index 51d91503..9d428d36 100755 --- a/integrations/isolation/acq-kits/paseo/scripts/verify +++ b/integrations/isolation/acq-kits/paseo/scripts/verify @@ -368,6 +368,13 @@ else bad "daemon did not answer /api/health on :$PASEO_PORT (see the daemon log)" in_sbx 'tail -n 20 ~/.local/state/paseo/paseo-daemon.log' | sed 's/^/ | /' fi + +config_assert="$(in_sbx "PASEO_EXPECTED_LISTEN='0.0.0.0:$PASEO_PORT' node -e 'const fs = require(\"fs\"); const path = require(\"path\"); const config = JSON.parse(fs.readFileSync(path.join(process.env.PASEO_HOME || (process.env.HOME + \"/.paseo\"), \"config.json\"), \"utf8\")); const failures = []; if (config?.daemon?.listen !== process.env.PASEO_EXPECTED_LISTEN) failures.push(\"daemon.listen=\" + JSON.stringify(config?.daemon?.listen)); if (config?.features?.webUi?.enabled !== true) failures.push(\"features.webUi.enabled=\" + JSON.stringify(config?.features?.webUi?.enabled)); if (config?.daemon?.relay?.enabled !== false) failures.push(\"daemon.relay.enabled=\" + JSON.stringify(config?.daemon?.relay?.enabled)); if (failures.length) { console.log(failures.join(\", \")); process.exit(1); } console.log(\"ok\");' 2>&1")" +if [ "$config_assert" = "ok" ]; then + ok "config.json persists required daemon settings (listen, web UI, relay disabled)" +else + bad "config.json missing required daemon settings: ${config_assert:-no output}" +fi # The bundled web UI is served on the same origin: the root should return HTML. if [ "$(in_sbx "curl -fsS http://127.0.0.1:$PASEO_PORT/ >/dev/null 2>&1 && echo yes")" = "yes" ]; then ok "bundled web UI is served on the same port ($PASEO_PORT)" diff --git a/integrations/isolation/acq-kits/paseo/tests/paseo-set-worktrees-root.test.mjs b/integrations/isolation/acq-kits/paseo/tests/paseo-set-worktrees-root.test.mjs new file mode 100644 index 00000000..708566c6 --- /dev/null +++ b/integrations/isolation/acq-kits/paseo/tests/paseo-set-worktrees-root.test.mjs @@ -0,0 +1,58 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const script = new URL("../files/home/paseo-set-worktrees-root.mjs", import.meta.url); + +async function tempDir() { + return mkdtemp(path.join(os.tmpdir(), "paseo-set-worktrees-root-")); +} + +async function runSetRoot(paseoHome, root, env = {}) { + return execFileAsync(process.execPath, [script.pathname, "--paseo-home", paseoHome, "--root", root], { + env: { ...process.env, ...env }, + timeout: 35_000, + }); +} + +test("sets worktrees.root without clobbering daemon settings", async () => { + const paseoHome = await tempDir(); + await writeFile( + path.join(paseoHome, "config.json"), + JSON.stringify({ + daemon: { listen: "0.0.0.0:6767", relay: { enabled: false } }, + features: { webUi: { enabled: true } }, + }), + ); + const root = path.join(paseoHome, "project", ".paseo-worktrees"); + + const first = await runSetRoot(paseoHome, root); + const second = await runSetRoot(paseoHome, root); + const config = JSON.parse(await readFile(path.join(paseoHome, "config.json"), "utf8")); + + assert.equal(first.stdout, "changed\n"); + assert.equal(second.stdout, "unchanged\n"); + assert.equal(config.worktrees.root, root); + assert.equal(config.daemon.listen, "0.0.0.0:6767"); + assert.equal(config.daemon.relay.enabled, false); + assert.equal(config.features.webUi.enabled, true); +}); + +test("fails closed when the shared config lock is held", async () => { + const paseoHome = await tempDir(); + await mkdir(path.join(paseoHome, ".config-json.lock")); + + await assert.rejects( + runSetRoot(paseoHome, path.join(paseoHome, ".paseo-worktrees"), { + PASEO_CONFIG_LOCK_TIMEOUT_MS: "50", + }), + /cannot lock/, + ); + + await rm(path.join(paseoHome, ".config-json.lock"), { recursive: true, force: true }); +}); From e4c7cd94c72606e749a05982cbc60cc4109f80ab Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 30 Sep 2026 17:50:49 +0000 Subject: [PATCH 6/8] chore(ci): update markdown linter dependency Bump markdownlint-cli2 and its markdown-it override so the Markdown Lint job's npm audit step passes with no moderate advisory. Co-authored-by: OpenCode --- .github/linters/package-lock.json | 228 +++++++++++------------------- .github/linters/package.json | 4 +- 2 files changed, 88 insertions(+), 144 deletions(-) diff --git a/.github/linters/package-lock.json b/.github/linters/package-lock.json index cd5ae58e..6b1e4a3c 100644 --- a/.github/linters/package-lock.json +++ b/.github/linters/package-lock.json @@ -8,14 +8,13 @@ "name": "linters", "version": "1.0.0", "dependencies": { - "markdownlint-cli2": "0.22.1" + "markdownlint-cli2": "0.23.3" } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -28,7 +27,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", "engines": { "node": ">= 8" } @@ -37,7 +35,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -50,7 +47,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "license": "MIT", "engines": { "node": ">=18" }, @@ -62,7 +58,6 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", "dependencies": { "@types/ms": "*" } @@ -70,26 +65,22 @@ "node_modules/@types/katex": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", - "license": "MIT" + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==" }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "engines": { "node": ">=12" }, @@ -107,7 +98,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -119,7 +109,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -129,7 +118,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -139,7 +127,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -149,7 +136,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", "engines": { "node": ">= 12" } @@ -158,7 +144,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -175,7 +160,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", "dependencies": { "character-entities": "^2.0.0" }, @@ -188,7 +172,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -197,7 +180,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", "dependencies": { "dequal": "^2.0.0" }, @@ -207,12 +189,11 @@ } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -222,7 +203,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -235,10 +215,9 @@ } }, "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", "dependencies": { "reusify": "^1.0.4" } @@ -247,7 +226,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -256,10 +234,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "license": "MIT", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "engines": { "node": ">=18" }, @@ -271,7 +248,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -280,15 +256,15 @@ } }, "node_modules/globby": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", - "integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", - "license": "MIT", + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.4.tgz", + "integrity": "sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "is-path-inside": "^4.0.0", + "micromatch": "^4.0.8", "slash": "^5.1.0", "unicorn-magic": "^0.4.0" }, @@ -300,10 +276,9 @@ } }, "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.11.tgz", + "integrity": "sha512-YChdK5txDjwGUvgR7oCJcLGkwi3LDh8Zx8tS7ndYciLEg/oOCL26VUBSUEtgS7EjiybcBqFxh5j0rAdDX7/bbg==", "engines": { "node": ">= 4" } @@ -312,7 +287,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -322,7 +296,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" @@ -336,7 +309,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -346,7 +318,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -355,7 +326,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -367,7 +337,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -377,7 +346,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -386,7 +354,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "license": "MIT", "engines": { "node": ">=12" }, @@ -439,7 +406,6 @@ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], - "license": "MIT", "dependencies": { "commander": "^8.3.0" }, @@ -467,9 +433,9 @@ } }, "node_modules/markdown-it": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", - "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.1.tgz", + "integrity": "sha512-9/7gE95FNPkfUWrjJIoHZza2iLmuJlPD0UNMxPi7bxUrbCR525YZY0r+zyfes0dZI5ZZ/uNIXUJca0pJvtw41g==", "funding": [ { "type": "github", @@ -480,24 +446,42 @@ "url": "https://github.com/sponsors/markdown-it" } ], - "license": "MIT", "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.1", - "mdurl": "^2.0.0", + "argparse": "^3.0.0", + "entities": "^8.0.0", + "linkify-it": "^6.0.0", + "mdurl": "^2.1.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "uc.micro": "^3.0.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.2.tgz", + "integrity": "sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ] + }, + "node_modules/markdown-it/node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==" + }, "node_modules/markdownlint": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.40.0.tgz", - "integrity": "sha512-UKybllYNheWac61Ia7T6fzuQNDZimFIpCg2w6hHjgV1Qu0w1TV0LlSgryUGzM0bkKQCBhy2FDhEELB73Kb0kAg==", - "license": "MIT", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.41.1.tgz", + "integrity": "sha512-qHKeU2E1bdyNAT077go2FVTNXvYcktN5IHtF6XyeD1l0PClxzSp2tUApAV14ORI8DGX4H9bNKZEzelZp4qn8IA==", "dependencies": { "micromark": "4.0.2", "micromark-core-commonmark": "2.0.3", @@ -507,36 +491,35 @@ "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", "micromark-util-types": "2.0.2", - "string-width": "8.1.0" + "string-width": "8.2.1" }, "engines": { - "node": ">=20" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" } }, "node_modules/markdownlint-cli2": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.22.1.tgz", - "integrity": "sha512-X14ZbytybDCXAViDmtN4DKLt9ZTrRn+oOrxTYlg3a65jS6QcYYbAkGPh/En2L/GDNbFYJ6lKaQSUNrrbN1bPrw==", - "license": "MIT", + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.23.3.tgz", + "integrity": "sha512-xAr5o/TGpC3v6lE6cKIW4b5eOFRrRX5u7Vtjae9ix3RALv8nNOd94XMkD/1OXXBtpMcJ4uQGbpSo3hv5UqS4uQ==", "dependencies": { - "globby": "16.2.0", - "js-yaml": "4.1.1", + "globby": "16.2.4", + "js-yaml": "5.4.1", "jsonc-parser": "3.3.1", "jsonpointer": "5.0.1", - "markdown-it": "14.1.1", - "markdownlint": "0.40.0", + "markdown-it": "15.0.1", + "markdownlint": "0.41.1", "markdownlint-cli2-formatter-default": "0.0.6", "micromatch": "4.0.8", - "smol-toml": "1.6.1" + "smol-toml": "1.8.0" }, "bin": { "markdownlint-cli2": "markdownlint-cli2-bin.mjs" }, "engines": { - "node": ">=20" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" @@ -555,16 +538,14 @@ } }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", "engines": { "node": ">= 8" } @@ -583,7 +564,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -618,7 +598,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", @@ -642,7 +621,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", @@ -661,7 +639,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", @@ -677,7 +654,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", @@ -697,7 +673,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", @@ -714,7 +689,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", @@ -743,7 +717,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -764,7 +737,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -773,9 +745,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", "funding": [ { "type": "GitHub Sponsors", @@ -786,7 +758,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -806,7 +777,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -828,7 +798,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -850,7 +819,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -870,7 +838,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } @@ -889,7 +856,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -910,7 +876,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -930,7 +895,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } @@ -948,8 +912,7 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", @@ -964,8 +927,7 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", @@ -981,7 +943,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } @@ -1000,7 +961,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" } @@ -1019,7 +979,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", @@ -1040,7 +999,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", @@ -1061,8 +1019,7 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-types": { "version": "2.0.2", @@ -1077,14 +1034,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1096,14 +1051,12 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", @@ -1122,7 +1075,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1156,14 +1108,12 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -1187,7 +1137,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -1196,7 +1145,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "license": "MIT", "engines": { "node": ">=14.16" }, @@ -1217,13 +1165,12 @@ } }, "node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "license": "MIT", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { "node": ">=20" @@ -1236,7 +1183,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" }, @@ -1251,7 +1197,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -1266,10 +1211,9 @@ "license": "MIT" }, "node_modules/unicorn-magic": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", - "license": "MIT", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.1.tgz", + "integrity": "sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==", "engines": { "node": ">=20" }, diff --git a/.github/linters/package.json b/.github/linters/package.json index 493cd997..6b99845f 100644 --- a/.github/linters/package.json +++ b/.github/linters/package.json @@ -7,11 +7,11 @@ "lint:md": "markdownlint-cli2 '**/*.md' '#node_modules'" }, "dependencies": { - "markdownlint-cli2": "0.22.1" + "markdownlint-cli2": "0.23.3" }, "overrides": { "js-yaml": "4.3.2", - "markdown-it": "14.2.0", + "markdown-it": "15.0.1", "linkify-it": "5.0.2", "smol-toml": "1.7.1" } From 06aa7de96b6034d14567403cdd4d4ce3c89b0763 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 30 Sep 2026 19:14:49 +0000 Subject: [PATCH 7/8] test(paseo): cover daemon config assertion offline Move the daemon config readback into a shared helper used by startup and verify, and add offline Node tests for required listen, web UI, and relay settings. Co-authored-by: OpenCode --- .../files/home/paseo-assert-daemon-config.mjs | 68 +++++++++++++++++++ .../acq-kits/paseo/files/home/paseo-start.sh | 2 +- .../isolation/acq-kits/paseo/scripts/verify | 24 +++++-- .../isolation/acq-kits/paseo/spec.yaml | 4 ++ .../tests/paseo-assert-daemon-config.test.mjs | 51 ++++++++++++++ 5 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 integrations/isolation/acq-kits/paseo/files/home/paseo-assert-daemon-config.mjs create mode 100644 integrations/isolation/acq-kits/paseo/tests/paseo-assert-daemon-config.test.mjs diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-assert-daemon-config.mjs b/integrations/isolation/acq-kits/paseo/files/home/paseo-assert-daemon-config.mjs new file mode 100644 index 00000000..cc23f05c --- /dev/null +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-assert-daemon-config.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +// paseo-assert-daemon-config.mjs — fail closed unless config.json contains the +// daemon settings this kit requires before launching an unauthenticated daemon. + +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +function resolvePaseoHome(explicit) { + if (explicit && explicit.trim()) return path.resolve(explicit.trim()); + const env = process.env.PASEO_HOME; + if (env && env.trim()) { + const raw = env.trim(); + if (raw === "~") return homedir(); + if (raw.startsWith("~/")) return path.join(homedir(), raw.slice(2)); + return path.resolve(raw); + } + return path.join(homedir(), ".paseo"); +} + +function parseArgs(argv) { + const args = { paseoHome: undefined, listen: undefined }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--paseo-home") { + args.paseoHome = argv[++i]; + } else if (arg === "--listen") { + args.listen = argv[++i]; + } else { + throw new Error(`unknown argument: ${arg}`); + } + } + if (!args.listen) throw new Error("missing required --listen "); + return args; +} + +export function assertDaemonConfig({ paseoHome, listen }) { + const configPath = path.join(resolvePaseoHome(paseoHome), "config.json"); + let config; + try { + config = JSON.parse(readFileSync(configPath, "utf8")); + } catch (err) { + throw new Error(`cannot read required daemon config ${configPath}: ${err.message}`); + } + + const failures = []; + if (config?.daemon?.listen !== listen) { + failures.push(`daemon.listen=${JSON.stringify(config?.daemon?.listen)}`); + } + if (config?.features?.webUi?.enabled !== true) { + failures.push(`features.webUi.enabled=${JSON.stringify(config?.features?.webUi?.enabled)}`); + } + if (config?.daemon?.relay?.enabled !== false) { + failures.push(`daemon.relay.enabled=${JSON.stringify(config?.daemon?.relay?.enabled)}`); + } + if (failures.length > 0) { + throw new Error(`config.json missing required daemon settings: ${failures.join(", ")}`); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + try { + assertDaemonConfig(parseArgs(process.argv.slice(2))); + } catch (err) { + process.stderr.write(`error: ${err.message}\n`); + process.exit(1); + } +} diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh index 0f2f14c4..1d5af04c 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh @@ -188,7 +188,7 @@ if ! paseo daemon config set --home "$PASEO_HOME_DIR" daemon.relay.enabled false echo "paseo: error: could not persist daemon.relay.enabled=false; see $STATE_DIR/paseo-config.log" >&2 config_ok="" fi -if ! PASEO_HOME_DIR="$PASEO_HOME_DIR" PASEO_EXPECTED_LISTEN="$PASEO_LISTEN" node -e 'const fs = require("fs"); const path = require("path"); const config = JSON.parse(fs.readFileSync(path.join(process.env.PASEO_HOME_DIR, "config.json"), "utf8")); if (config?.daemon?.listen !== process.env.PASEO_EXPECTED_LISTEN || config?.features?.webUi?.enabled !== true || config?.daemon?.relay?.enabled !== false) process.exit(1);' >>"$STATE_DIR/paseo-config.log" 2>&1; then +if ! node "$HOME/paseo-assert-daemon-config.mjs" --paseo-home "$PASEO_HOME_DIR" --listen "$PASEO_LISTEN" >>"$STATE_DIR/paseo-config.log" 2>&1; then echo "paseo: error: config.json does not contain required daemon settings; not starting unverified daemon" >&2 config_ok="" fi diff --git a/integrations/isolation/acq-kits/paseo/scripts/verify b/integrations/isolation/acq-kits/paseo/scripts/verify index 9d428d36..f0c13eb9 100755 --- a/integrations/isolation/acq-kits/paseo/scripts/verify +++ b/integrations/isolation/acq-kits/paseo/scripts/verify @@ -173,15 +173,25 @@ fi # --- Offline check of the mount registrar helper (no sandbox needed). ---------- info "1c. mount registrar helper parses (offline)" +ASSERTCFG="$KIT_DIR/files/home/paseo-assert-daemon-config.mjs" REGMOUNTS="$KIT_DIR/files/home/paseo-register-mounts.mjs" if ! command -v node >/dev/null 2>&1; then - skip "node not on PATH — cannot exercise paseo-register-mounts.mjs" -elif [ ! -f "$REGMOUNTS" ]; then - bad "paseo-register-mounts.mjs missing at $REGMOUNTS" -elif node --check "$REGMOUNTS" >/dev/null 2>&1; then - ok "paseo-register-mounts.mjs parses (node --check)" + skip "node not on PATH — cannot exercise paseo-register-mounts.mjs or paseo-assert-daemon-config.mjs" else - bad "paseo-register-mounts.mjs failed node --check" + if [ ! -f "$ASSERTCFG" ]; then + bad "paseo-assert-daemon-config.mjs missing at $ASSERTCFG" + elif node --check "$ASSERTCFG" >/dev/null 2>&1; then + ok "paseo-assert-daemon-config.mjs parses (node --check)" + else + bad "paseo-assert-daemon-config.mjs failed node --check" + fi + if [ ! -f "$REGMOUNTS" ]; then + bad "paseo-register-mounts.mjs missing at $REGMOUNTS" + elif node --check "$REGMOUNTS" >/dev/null 2>&1; then + ok "paseo-register-mounts.mjs parses (node --check)" + else + bad "paseo-register-mounts.mjs failed node --check" + fi fi # --- Select the live driver. ------------------------------------------------- @@ -369,7 +379,7 @@ else in_sbx 'tail -n 20 ~/.local/state/paseo/paseo-daemon.log' | sed 's/^/ | /' fi -config_assert="$(in_sbx "PASEO_EXPECTED_LISTEN='0.0.0.0:$PASEO_PORT' node -e 'const fs = require(\"fs\"); const path = require(\"path\"); const config = JSON.parse(fs.readFileSync(path.join(process.env.PASEO_HOME || (process.env.HOME + \"/.paseo\"), \"config.json\"), \"utf8\")); const failures = []; if (config?.daemon?.listen !== process.env.PASEO_EXPECTED_LISTEN) failures.push(\"daemon.listen=\" + JSON.stringify(config?.daemon?.listen)); if (config?.features?.webUi?.enabled !== true) failures.push(\"features.webUi.enabled=\" + JSON.stringify(config?.features?.webUi?.enabled)); if (config?.daemon?.relay?.enabled !== false) failures.push(\"daemon.relay.enabled=\" + JSON.stringify(config?.daemon?.relay?.enabled)); if (failures.length) { console.log(failures.join(\", \")); process.exit(1); } console.log(\"ok\");' 2>&1")" +config_assert="$(in_sbx "node /home/agent/paseo-assert-daemon-config.mjs --paseo-home \"\${PASEO_HOME:-\$HOME/.paseo}\" --listen '0.0.0.0:$PASEO_PORT' 2>&1 && echo ok")" if [ "$config_assert" = "ok" ]; then ok "config.json persists required daemon settings (listen, web UI, relay disabled)" else diff --git a/integrations/isolation/acq-kits/paseo/spec.yaml b/integrations/isolation/acq-kits/paseo/spec.yaml index 6f5c9664..65fea9b5 100644 --- a/integrations/isolation/acq-kits/paseo/spec.yaml +++ b/integrations/isolation/acq-kits/paseo/spec.yaml @@ -152,6 +152,10 @@ files: mode: "0755" source: files/home/paseo-set-worktrees-root.mjs description: Idempotently merge worktrees.root into $PASEO_HOME/config.json (used by the shim) + - path: /home/agent/paseo-assert-daemon-config.mjs + mode: "0755" + source: files/home/paseo-assert-daemon-config.mjs + description: Assert required Paseo daemon settings are persisted before daemon launch - path: /home/agent/paseo-register-mounts.mjs mode: "0755" source: files/home/paseo-register-mounts.mjs diff --git a/integrations/isolation/acq-kits/paseo/tests/paseo-assert-daemon-config.test.mjs b/integrations/isolation/acq-kits/paseo/tests/paseo-assert-daemon-config.test.mjs new file mode 100644 index 00000000..3a495b53 --- /dev/null +++ b/integrations/isolation/acq-kits/paseo/tests/paseo-assert-daemon-config.test.mjs @@ -0,0 +1,51 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { assertDaemonConfig } from "../files/home/paseo-assert-daemon-config.mjs"; + +async function tempDir() { + return mkdtemp(path.join(os.tmpdir(), "paseo-assert-daemon-config-")); +} + +async function writeConfig(paseoHome, config) { + await writeFile(path.join(paseoHome, "config.json"), `${JSON.stringify(config)}\n`); +} + +test("accepts the required daemon settings", async () => { + const paseoHome = await tempDir(); + await writeConfig(paseoHome, { + daemon: { listen: "0.0.0.0:6767", relay: { enabled: false } }, + features: { webUi: { enabled: true } }, + }); + + assert.doesNotThrow(() => assertDaemonConfig({ paseoHome, listen: "0.0.0.0:6767" })); +}); + +test("rejects a missing relay-disable setting", async () => { + const paseoHome = await tempDir(); + await writeConfig(paseoHome, { + daemon: { listen: "0.0.0.0:6767" }, + features: { webUi: { enabled: true } }, + }); + + assert.throws( + () => assertDaemonConfig({ paseoHome, listen: "0.0.0.0:6767" }), + /daemon\.relay\.enabled=undefined/, + ); +}); + +test("rejects wrong listen and disabled web UI", async () => { + const paseoHome = await tempDir(); + await writeConfig(paseoHome, { + daemon: { listen: "127.0.0.1:6767", relay: { enabled: false } }, + features: { webUi: { enabled: false } }, + }); + + assert.throws( + () => assertDaemonConfig({ paseoHome, listen: "0.0.0.0:6767" }), + /daemon\.listen="127\.0\.0\.1:6767".*features\.webUi\.enabled=false/, + ); +}); From 85b7c7f394ce76b1e58aee2d2e4da63331dd813f Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Wed, 30 Sep 2026 21:48:39 +0000 Subject: [PATCH 8/8] fix(paseo): narrow removed flag detection Avoid flagging the daemon config assertion helper's --listen argument as a removed Paseo daemon launch flag, and add the same launch-surface check to the offline verifier. Co-authored-by: OpenCode --- .../isolation/acq-kits/paseo/scripts/verify | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/integrations/isolation/acq-kits/paseo/scripts/verify b/integrations/isolation/acq-kits/paseo/scripts/verify index f0c13eb9..e4859af4 100755 --- a/integrations/isolation/acq-kits/paseo/scripts/verify +++ b/integrations/isolation/acq-kits/paseo/scripts/verify @@ -73,6 +73,7 @@ RUN_SBX="${RUN_SBX:-0}" SBX_KIT_DIR="${SBX_KIT_DIR:-}" # Container port the daemon binds (API + WS + web UI). Keep in sync with spec. PASEO_PORT="${PASEO_PORT:-6767}" +REMOVED_DAEMON_LAUNCH_PATTERN='paseo[[:space:]]+daemon[[:space:]]+start|paseo[[:space:]]+daemon[[:space:]]+(start|run)[^#]*--(listen|web-ui|foreground)' # How long to wait for the background startup, which on FIRST boot also installs # the Paseo CLI (npm). Override with READY_TIMEOUT. READY_TIMEOUT="${READY_TIMEOUT:-120}" @@ -194,6 +195,21 @@ else fi fi +# --- Offline check of the daemon launch surface (no sandbox needed). ---------- +info "1d. startup daemon launch surface is 0.9-compatible (offline)" +STARTUP_SCRIPT="$KIT_DIR/files/home/paseo-start.sh" +if [ ! -f "$STARTUP_SCRIPT" ]; then + bad "paseo-start.sh missing at $STARTUP_SCRIPT" +else + removed_flags="$(grep -Ev '^[[:space:]]*#' "$STARTUP_SCRIPT" 2>/dev/null | grep -E "$REMOVED_DAEMON_LAUNCH_PATTERN" || true)" + if [ -n "$removed_flags" ]; then + bad "startup script still references removed Paseo 0.9 daemon launch flags:" + printf '%s\n' "$removed_flags" | sed 's/^/ | /' + else + ok "startup script uses Paseo 0.9-compatible daemon launch surface" + fi +fi + # --- Select the live driver. ------------------------------------------------- DRIVER="" if [ "$RUN_ACQ" = "1" ]; then @@ -365,7 +381,7 @@ esac || bad "\$HOME/.local/bin/opencode missing or not executable" info "5. Paseo daemon + web UI (started by the startup supervisor, on its own)" -removed_flags="$(in_sbx 'grep -Ev "^[[:space:]]*#" /home/agent/paseo-start.sh 2>/dev/null | grep -E "paseo daemon start|--listen|--web-ui|--foreground" || true')" +removed_flags="$(in_sbx "grep -Ev '^[[:space:]]*#' /home/agent/paseo-start.sh 2>/dev/null | grep -E '$REMOVED_DAEMON_LAUNCH_PATTERN' || true")" if [ -n "$removed_flags" ]; then bad "startup script still references removed Paseo 0.9 launch flags:" printf '%s\n' "$removed_flags" | sed 's/^/ | /'