From d55e22234119c80f3e231abd3a30123c4ee26c03 Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Thu, 28 May 2026 09:42:37 -0700 Subject: [PATCH] =?UTF-8?q?v0.8.17:=20per-alias=20DIRECT=20(no-multiplex)?= =?UTF-8?q?=20SSH=20mode=20for=20servers=20that=20reject=20ControlMaster?= =?UTF-8?q?=20session=20multiplexing=20=E2=80=94=20/ssh-set-direct=20+=20p?= =?UTF-8?q?er-command=20sshpass=20(forced=20password=20auth),=20banner/sud?= =?UTF-8?q?o=20stderr=20filter;=20zero=20traffic-bypass=20primitives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 85 ++++++++++ MANIFEST | 8 +- VERSION | 2 +- larry.sh | 59 ++++++- lib/ssh-helper.sh | 393 ++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 504 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f71a7..4e4e36e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,91 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here. Versioning is loose-semver; bumps trigger the in-process self-update on every running client via `LARRY_BASE_URL` + `MANIFEST`. +## v0.8.17 — 2026-05-28 + +Per-alias DIRECT (no-multiplex) SSH mode (Clover). The real unblock for +`/sites qa` on Bryan's Legacy→qa box. + +**Confirmed root cause (live-verified on the qa box).** The qa server +(`bryjohnx@lhsixfqa` → Cloverleaf host `shdclvf01q`, release cis2025.01) +REJECTS SSH ControlMaster session multiplexing. The master opens and +authenticates fine, but any session multiplexed over it dies with +`read from master failed: Connection reset by peer`, then ssh falls back to a +fresh connection that fails auth. A DIRECT per-command connection WORKS and +returns the 24 site dirs (21 after the helloworld/siteProto/master filter). + +**Fix — per-alias DIRECT mode.** A new TSV column 5 (`direct`, on|off) opts an +alias out of the ControlMaster entirely. When on, ALL remote ops for that alias +(`exec`, `discover`, `pull-smat`, `pull`) run a FRESH per-command +`sshpass -f ssh -o PreferredAuthentications=password -o +PubkeyAuthentication=no -o StrictHostKeyChecking=accept-new -o ControlMaster=no +-o ControlPath=none -o ConnectTimeout= ''`. The +remote command is shaped by the SAME `_remote_cmd_for` chokepoint as the master +path, so a pinned HCIROOT (`set-hciroot`) is honoured identically — only the +transport changes. NO traffic bypass: legitimate forced-password auth, no +proxy/tunnel/masking, host-key checking stays on (accept-new). + +1. `lib/ssh-helper.sh` — TSV column 5 (`direct`); `read_host_direct` / + `_alias_is_direct` readers; `cmd_set_direct` (`set-direct on|off`, + whitespace-trimmed, legacy <5-column files backfilled, pinned HCIROOT in + col 4 never clobbered); `_run_direct` (per-command sshpass dispatch + + stderr filtering); `_direct_scp` (fresh-sshpass scp for pull); centralised + `_dispatch_remote` (direct vs master, identical command shaping); `cmd_exec` + / `cmd_discover` / `cmd_pull_smat` / `cmd_pull` route through it and SKIP the + open-master requirement in direct mode; `cmd_setup` in direct mode VALIDATES + the stored password with one trivial direct command instead of opening a + (pointless) master; `cmd_hosts` shows the `direct` column (`master=n/a` for + direct aliases). New `set-direct|direct` subcommand + help. +2. `larry.sh` — `/ssh-set-direct on|off` slash command (set-u-safe + split, the v0.8.16 idiom); added to the completion array + descriptions + + `print_help`; `tool_list_sites` reports the transport (`direct sshpass` vs + `ControlMaster`) and gives a transport-correct recovery hint on discover + failure (stale password in direct mode, not "closed master"); `list_sites` + tool description + the system-prompt remote-alias guidance updated for the + multiplex-rejection symptom and the `/ssh-set-direct` recovery. + +**Clean output (UX).** The qa login profile emits a pre-auth banner +("Unauthorized access…/monitored") AND `sudo: a terminal is required` / +`sudo: a password is required` on STDERR for non-interactive sessions. The +parsed site list is on STDOUT (already clean). `_filter_direct_stderr` strips +exactly those known-benign lines; remaining stderr is surfaced ONLY on an +actual non-zero command failure. So `/sites qa` shows just the 21 sites + the +`(excluded: …)` note — no banner/sudo noise. + +**No regression.** Aliases WITHOUT the direct flag keep the existing +ControlMaster-multiplex path unchanged (and still die "no open master" when +none is open). + +**New flow for qa.** `/ssh-pass qa` → `/ssh-set-hciroot qa +/hci/cis2025.01/integrator` → `/ssh-set-direct qa on` → `/sites qa`. No master +step in direct mode. + +**Self-verify (this gate, pre-Vera).** Driven non-interactively on this Mac: +flag round-trip (add → set-direct on/off → hosts → raw TSV; col 4 HCIROOT +preserved across col 5 writes); legacy 3-column file backfilled to 5 columns; +command-shaping (`_remote_cmd_for`) identical pinned-export for direct and +master, login-shell for unpinned; `_alias_is_direct` 0/1; stderr filter against +a sample banner+sudo+real-error (only the genuine `find: … Permission denied` +survives; pure banner+sudo → empty); `_run_direct` end-to-end via a stubbed +`sshpass` — STDOUT clean + STDERR empty on success, real error surfaced + banner +stripped + rc propagated on failure; `tool_list_sites qa` over a stubbed direct +discover → `sites: 21 (excluded: helloworld, siteProto, master)`, clean, +correct mode line; direct-mode `setup`/`discover` skip the open-master die; +non-direct `exec`/`discover` still die "no open master" (no regression); and the +`/ssh-set-direct` REPL slash handler (+ neighbours `/ssh-set-hciroot`, +`/ssh-setup`, `/ssh`) drive through a `case` dispatcher under `set -u` + +`compat32` with no unbound-variable abort and correct routing. (Live connect to +the qa box not run here — `sshpass` is the production host's tool, absent on the +Mac; the path up to the sshpass invocation is fully exercised via the stub.) + +**Vera v0.8.16 caveat closed (evidence attached).** Repo-wide self-ref-class +grep `grep -rnE '^[[:space:]]*(local|declare|typeset|readonly|export)[[:space:]]+[A-Za-z_].*=.*$' larry.sh lib/` +(712 superficial matches) + a targeted same-statement self-reference detector: +**ZERO** same-statement self-references — every match is the safe sequential +`;`-separated idiom. `bash -n larry.sh lib/*.sh` → clean (36/36 files OK). + +--- + ## v0.8.16 — 2026-05-28 Hotfix (Clover): `set -u` unbound-variable abort in the v0.8.15 diff --git a/MANIFEST b/MANIFEST index eb79cbf..26b9338 100644 --- a/MANIFEST +++ b/MANIFEST @@ -23,16 +23,16 @@ # scripts/make-manifest.sh and bump VERSION. # Top-level scripts -larry.sh 7bbd920f7a29379aadad3e59a298ff416333e9299241db7c7bb8c42cc7750f9d +larry.sh 7bdbe0743d7aec58ccedadfebd766e8bbfd828d47e33e51b2bac75a4d0706f5b larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1 # Metadata -VERSION 088faf7f331988e309c37e27272af81cd41fdcf4466022059ffdd3184e3c7d34 +VERSION a8c64c5df539331e33b8b4b5c1534d12f6238dbbbd313e7cebbf1cff1df0fe87 MANUAL.md 666128a086b59ff3c31a574aec0c5dd681666d66319da9f078451bf9013ca5e1 -CHANGELOG.md 3db03a7913b3a66310f9cb282eaa6c3554d5ed59ef92eca78ef02257dc97277f +CHANGELOG.md a329fda8be2e5caa33f1dfec7a5c68adc7d7d19b8449032cbfc9542a766292b6 # Agent personas (system-prompt overlays) agents/larry.md 11ea905fa7cac6fa7baeb11b2d62af07b15a666ce90cfe36491bcbc555244397 @@ -52,7 +52,7 @@ lib/fetch-safe.sh abecf0045b9856f63ffa346119443c11de56547344be32bddaed9fbae6b021 lib/oauth.sh 04a93376f88fe53cc1c86a5dbe577735c60375dadd4f2fda55b921ef3cddf22b # Secure SSH with ControlMaster (password hidden from Larry-the-LLM) -lib/ssh-helper.sh 3397945df8184d0bc89853608c097af11b97b37695c5598c979347b6b912e0eb +lib/ssh-helper.sh e9e2f33bb893d951e668d81dfe88057d235013b60cdba0e3441d1400d877a6d4 # v0.8.6: work-box → Mac headers.log sync (tsk-2026-05-27-023). Incremental, # offset-tracked push of $LARRY_HOME/log/headers.log to a daemon-watched path diff --git a/VERSION b/VERSION index ac7dffa..9bba175 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.16 +0.8.17 diff --git a/larry.sh b/larry.sh index cb0138a..4d932b7 100755 --- a/larry.sh +++ b/larry.sh @@ -78,7 +78,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.8.16" +LARRY_VERSION="0.8.17" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" # ───────────────────────────────────────────────────────────────────────────── @@ -1247,7 +1247,7 @@ detect_cloverleaf_env() { esac if [ -n "$aliases" ]; then lines+=("Configured SSH aliases: $(printf '%s' "$aliases" | tr '\n' ' ')") - lines+=("For a remote alias: discover its env with the list_sites tool (alias=) — it resolves the remote \$HCIROOT (login shell, or an explicit pin if set) and walks NetConfigs. NEVER ask Bryan to export \$HCIROOT for a remote host. If list_sites reports HCIROOT empty with a sudo-gated-profile NOTE, have Bryan pin it once: /ssh-set-hciroot (e.g. qa → /hci/cis2025.01/integrator).") + lines+=("For a remote alias: discover its env with the list_sites tool (alias=) — it resolves the remote \$HCIROOT (login shell, or an explicit pin if set) and walks NetConfigs. NEVER ask Bryan to export \$HCIROOT for a remote host. If list_sites reports HCIROOT empty with a sudo-gated-profile NOTE, have Bryan pin it once: /ssh-set-hciroot (e.g. qa → /hci/cis2025.01/integrator). If a remote op fails with 'read from master failed: Connection reset by peer' (the host rejects SSH session multiplexing), have Bryan switch that alias to DIRECT mode: /ssh-set-direct on — then ALL remote ops use a fresh per-command sshpass connection (no ControlMaster). In DIRECT mode the recovery for a discover failure is a stale password (/ssh-pass then /ssh-setup to re-validate), NOT a closed master.") fi if [ -n "${HCIROOT:-}" ]; then @@ -3833,8 +3833,20 @@ tool_list_sites() { local out rc out=$("$helper" discover "$alias" 2>&1); rc=$? if [ "$rc" -ne 0 ]; then - printf '%s\n[list_sites: discover failed for alias=%s rc=%d. If the master is closed, tell Bryan to run /ssh-setup %s]\n' \ - "$out" "$alias" "$rc" "$alias" + # v0.8.17: the recovery hint depends on the alias's transport mode. In + # DIRECT mode there is no master — a failure is most likely a stale/rotated + # password (re-run /ssh-pass then /ssh-setup to re-validate). In master mode + # the usual cause is a closed master (re-run /ssh-setup). + local _ls_direct="" + local _ls_tsv="${LARRY_HOME:-$HOME/.larry}/.ssh-hosts.tsv" + [ -f "$_ls_tsv" ] && _ls_direct=$(awk -F'\t' -v a="$alias" 'NR>1 && $1==a && $5=="on"{print "on"; exit}' "$_ls_tsv" 2>/dev/null) + if [ "$_ls_direct" = "on" ]; then + printf '%s\n[list_sites: discover failed for alias=%s rc=%d (DIRECT mode). Likely a stale/rotated password — tell Bryan to re-run /ssh-pass %s then /ssh-setup %s to re-validate.]\n' \ + "$out" "$alias" "$rc" "$alias" "$alias" + else + printf '%s\n[list_sites: discover failed for alias=%s rc=%d. If the master is closed, tell Bryan to run /ssh-setup %s]\n' \ + "$out" "$alias" "$rc" "$alias" + fi return 0 fi local rroot; rroot=$(printf '%s\n' "$out" | awk -F'\t' '$1=="HCIROOT"{print $2; exit}') @@ -3849,11 +3861,14 @@ tool_list_sites() { # v0.8.15: report the actual resolution mode. If the alias has a pinned # HCIROOT (4th column of the hosts TSV) the discover ran with HCIROOT # exported explicitly and NO login profile; otherwise it used a login shell. - local _hosts_tsv="${LARRY_HOME:-$HOME/.larry}/.ssh-hosts.tsv" _pin="" _mode="login shell" + local _hosts_tsv="${LARRY_HOME:-$HOME/.larry}/.ssh-hosts.tsv" _pin="" _direct="" _mode="login shell" if [ -f "$_hosts_tsv" ]; then _pin=$(awk -F'\t' -v a="$alias" 'NR>1 && $1==a { print $4; exit }' "$_hosts_tsv" 2>/dev/null) + _direct=$(awk -F'\t' -v a="$alias" 'NR>1 && $1==a && $5=="on" { print "on"; exit }' "$_hosts_tsv" 2>/dev/null) fi [ -n "$_pin" ] && _mode="pinned HCIROOT, no login profile" + # v0.8.17: note the transport too — direct (per-command sshpass) vs master. + if [ "$_direct" = "on" ]; then _mode="$_mode; direct sshpass"; else _mode="$_mode; ControlMaster"; fi printf 'Cloverleaf env on alias "%s" (REMOTE, %s):\n' "$alias" "$_mode" printf ' HCIROOT = %s\n' "${rroot:-}" [ -n "$note" ] && printf ' NOTE: %s\n' "$note" @@ -4140,7 +4155,7 @@ TOOLS_JSON=$(cat <<'TOOLS_END' {"name":"ssh_exec","description":"Run a shell command on a remote test/dev host via an authenticated SSH ControlMaster session. Bryan must have already configured the alias (via /ssh-add) and opened the master (via /ssh-setup). The password is stored locally and you CANNOT see it — do not ask Bryan for it; if the master is closed, tell him to run the /ssh-setup ALIAS slash command. Use ssh_status first to confirm which aliases are open. Output capped at max_lines (default 500). Tool result includes the remote exit code as a [ssh_exec: exit rc=N] footer.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"Host alias Bryan configured. Run ssh_status to see the list."},"command":{"type":"string","description":"Shell command to execute on the remote. Quote as needed; will be passed through ssh as a single string."},"max_lines":{"type":"integer","description":"Cap output lines (default 500). Increase for known-large output, but prefer targeted commands."}},"required":["alias","command"]}}, {"name":"ssh_status","description":"List the SSH hosts Bryan has configured and which ones have an open ControlMaster session. Call this BEFORE ssh_exec to confirm an alias exists and the master is open. Each line shows: alias, user@host, port, cred (present/absent), master (open or dash). If the master is not open for an alias you need, ask Bryan to run the /ssh-setup ALIAS slash command. Do NOT attempt to authenticate yourself — you have no access to the password.","input_schema":{"type":"object","properties":{},"required":[]}}, - {"name":"list_sites","description":"List and COUNT the Cloverleaf sites in the environment. This is your proactive answer to 'how many sites are on ' / 'what sites exist' — NEVER ask Bryan to export or hand you $HCIROOT first; this tool resolves it for you. Works in BOTH deployment modes. REMOTE mode: pass alias= (a configured SSH alias, e.g. qa); the tool resolves the remote $HCIROOT and enumerates sites via a NetConfig walk (the version-agnostic ground truth; Cloverleaf's hcisitelist is used only if present AND the walk found nothing). If the alias has a PINNED HCIROOT (set via /ssh-set-hciroot), the walk runs with HCIROOT exported explicitly and SKIPS the login profile — this is required on hosts whose login profile is sudo-gated/non-interactive (a plain login shell there returns an EMPTY $HCIROOT). Otherwise it opens a LOGIN shell so the operator profile populates $HCIROOT. The ControlMaster must be open — if it is not, the result tells you to have Bryan run /ssh-setup . If the result shows HCIROOT empty with a NOTE about a sudo-gated profile, tell Bryan to pin it: /ssh-set-hciroot . LOCAL mode: omit alias; the tool enumerates sites under the locally-detected $HCIROOT (or the hciroot override). Returns the resolved HCIROOT, a site count, and the site names.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"REMOTE mode: an SSH alias from ssh_status (e.g. 'qa'). Omit for LOCAL mode (sites on this box)."},"hciroot":{"type":"string","description":"LOCAL mode only: override the detected $HCIROOT."}},"required":[]}}, + {"name":"list_sites","description":"List and COUNT the Cloverleaf sites in the environment. This is your proactive answer to 'how many sites are on ' / 'what sites exist' — NEVER ask Bryan to export or hand you $HCIROOT first; this tool resolves it for you. Works in BOTH deployment modes. REMOTE mode: pass alias= (a configured SSH alias, e.g. qa); the tool resolves the remote $HCIROOT and enumerates sites via a NetConfig walk (the version-agnostic ground truth; Cloverleaf's hcisitelist is used only if present AND the walk found nothing). If the alias has a PINNED HCIROOT (set via /ssh-set-hciroot), the walk runs with HCIROOT exported explicitly and SKIPS the login profile — this is required on hosts whose login profile is sudo-gated/non-interactive (a plain login shell there returns an EMPTY $HCIROOT). Otherwise it opens a LOGIN shell so the operator profile populates $HCIROOT. TRANSPORT: if the alias is in DIRECT mode (set via /ssh-set-direct on — for hosts that reject SSH session multiplexing) the walk runs over a fresh per-command sshpass connection and NO ControlMaster is needed; otherwise the ControlMaster must be open and if it is not, the result tells you to have Bryan run /ssh-setup . If the result shows HCIROOT empty with a NOTE about a sudo-gated profile, tell Bryan to pin it: /ssh-set-hciroot . LOCAL mode: omit alias; the tool enumerates sites under the locally-detected $HCIROOT (or the hciroot override). Returns the resolved HCIROOT, a site count, and the site names.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"REMOTE mode: an SSH alias from ssh_status (e.g. 'qa'). Omit for LOCAL mode (sites on this box)."},"hciroot":{"type":"string","description":"LOCAL mode only: override the detected $HCIROOT."}},"required":[]}}, {"name":"hl7_diff","description":"HL7-aware diff between two message files (or multi-message dumps). Compares segment-by-segment, field-by-field, with component and subcomponent precision. Ignores configured fields (default MSH.7 timestamp) so timestamp-only diffs do not show up as noise. Use for regression testing between environments (e.g. test vs prod route-test outputs).","input_schema":{"type":"object","properties":{"left":{"type":"string","description":"Path to left HL7 file."},"right":{"type":"string","description":"Path to right HL7 file."},"ignore":{"type":"string","description":"Comma-separated list of fields to ignore (e.g. MSH.7,MSH.10,EVN.6). Default MSH.7."},"include":{"type":"string","description":"If set, ONLY these fields are compared (overrides ignore for that set)."},"format":{"type":"string","enum":["text","tsv","count"],"description":"text=human-readable diff, tsv=machine-parseable, count=just the difference count."}},"required":["left","right"]}}, @@ -5452,7 +5467,14 @@ Slash commands: /ssh-set-hciroot pin HCIROOT for an alias (sudo-gated/non-interactive hosts that don't export it in a non-login shell; empty path clears the pin) + /ssh-set-direct on|off DIRECT (no-multiplex) mode: ALL remote ops for the + alias run a FRESH per-command sshpass connection, + bypassing the ControlMaster — for hosts that reject + SSH session multiplexing ("read from master failed: + Connection reset by peer"). off (or empty) reverts. /ssh-setup open a long-lived ControlMaster connection + (DIRECT mode: skips the master, just validates the + stored password with one direct command) /ssh-close close the ControlMaster /ssh-status [alias] show open masters + cred presence /ssh run command on the remote (you-driven, ad-hoc) @@ -5706,6 +5728,7 @@ _LARRY_SLASH_CMDS=( /ssh-remove /ssh-pass /ssh-set-hciroot + /ssh-set-direct /ssh-setup /ssh-close /ssh-status @@ -5764,7 +5787,8 @@ _LARRY_SLASH_CMDS_DESC=( [/ssh-remove]=" remove a host" [/ssh-pass]=" set/update password (hidden input)" [/ssh-set-hciroot]=" pin HCIROOT for an alias (sudo-gated hosts; empty path clears)" - [/ssh-setup]=" open a long-lived ControlMaster" + [/ssh-set-direct]=" on|off toggle DIRECT no-multiplex SSH (hosts that reject ControlMaster)" + [/ssh-setup]=" open a long-lived ControlMaster (DIRECT mode: validates password, no master)" [/ssh-close]=" close the ControlMaster" [/ssh-status]="show open ControlMaster sessions + cred presence" [/redetect]="re-scan for HCIROOT/HCISITE/tools" @@ -6913,6 +6937,27 @@ main_loop() { # _sh_path may legitimately be empty (clear the pin). _run_ssh_helper set-hciroot "$_sh_alias" "$_sh_path" continue ;; + /ssh-set-direct*) # v0.8.17: toggle DIRECT (no-multiplex) mode for an alias. + # When on, ALL remote ops bypass the ControlMaster and run a + # fresh per-command sshpass connection (for hosts that reject + # SSH session multiplexing, e.g. qa → shdclvf01q). + local rest; rest=$(_slash_args "/ssh-set-direct" "$input") + if [ -z "$rest" ]; then + err "usage: /ssh-set-direct on|off"; continue + fi + # set-u-safe split (same idiom as /ssh-set-hciroot): declare + # first, assign the alias, THEN reference it. A single-line + # `local a=… b="…$a…"` aborts under set -u (bash 3.2/Cygwin AND + # modern bash) — the v0.8.16 bug class. + local _sd_alias _sd_mode + _sd_alias="${rest%% *}" + _sd_mode="${rest#"$_sd_alias"}"; _sd_mode="${_sd_mode# }" + if [ -z "$_sd_alias" ]; then + err "usage: /ssh-set-direct on|off"; continue + fi + # _sd_mode empty → treated as "off" (clear) by the helper. + _run_ssh_helper set-direct "$_sd_alias" "$_sd_mode" + continue ;; /ssh-setup*) local rest; rest=$(_slash_args "/ssh-setup" "$input") if [ -z "$rest" ]; then err "usage: /ssh-setup "; continue; fi _run_ssh_helper setup "$rest" diff --git a/lib/ssh-helper.sh b/lib/ssh-helper.sh index 268a328..12f74b1 100755 --- a/lib/ssh-helper.sh +++ b/lib/ssh-helper.sh @@ -27,7 +27,25 @@ # hosts whose login profile is sudo-gated or # otherwise non-interactive (v0.8.15). # Pass an empty path to clear the pin. -# setup open ControlMaster (uses stored password ONCE) +# set-direct on|off toggle (persist) DIRECT mode for an alias +# (v0.8.17). When ON, ALL remote ops for the +# alias BYPASS the ControlMaster and run a +# FRESH per-command sshpass connection with +# forced password auth. For hosts that reject +# SSH session multiplexing ("read from master +# failed: Connection reset by peer") — the +# master opens but multiplexed sessions die. +# The pinned HCIROOT (set-hciroot) is still +# honoured: the remote command is shaped by +# the same _remote_cmd_for path, just sent +# over the direct connection. NO traffic +# bypass — plain forced-password ssh, no +# proxy/tunnel/masking, host-key checked +# (accept-new). Persisted as TSV column 5. +# setup open ControlMaster (uses stored password ONCE). +# In DIRECT mode, skips the (pointless) master +# and instead VALIDATES the password with one +# trivial direct command, then reports ready. # close close ControlMaster # status [alias] show open masters / cred presence # exec run command via master (returns output) @@ -77,7 +95,9 @@ ensure_layout() { umask 077 # v0.8.15: 4th column = pinned HCIROOT (optional). Older 3-column files stay # valid — readers treat a missing $4 as "no pin". - printf 'alias\taddr\tport\thciroot\n' > "$SSH_HOSTS_FILE" + # v0.8.17: 5th column = direct flag (on|off, optional). Older 3-/4-column + # files stay valid — readers treat a missing/empty $5 as "off" (master mode). + printf 'alias\taddr\tport\thciroot\tdirect\n' > "$SSH_HOSTS_FILE" chmod 600 "$SSH_HOSTS_FILE" fi } @@ -98,13 +118,28 @@ read_host_hciroot() { awk -F'\t' -v a="$alias" 'NR>1 && $1==a { print $4; exit }' < "$SSH_HOSTS_FILE" } +# read_host_direct ALIAS → echoes "on" if DIRECT mode is set (column 5 == on), +# else empty. v0.8.17: when on, ALL remote ops for the alias bypass the +# ControlMaster and run a fresh per-command sshpass connection (for hosts that +# reject session multiplexing). Missing/empty/anything-but-"on" → master mode. +read_host_direct() { + local alias="$1" + [ -f "$SSH_HOSTS_FILE" ] || { printf ''; return 0; } + awk -F'\t' -v a="$alias" 'NR>1 && $1==a && $5=="on" { print "on"; exit }' < "$SSH_HOSTS_FILE" +} + +# _alias_is_direct ALIAS → returns 0 (true) if the alias is in DIRECT mode. +_alias_is_direct() { + [ "$(read_host_direct "$1")" = "on" ] +} + require_sshpass() { command -v sshpass >/dev/null 2>&1 \ || die "sshpass not on PATH — install it (apt install sshpass / brew install sshpass) and retry" } cmd_help() { - sed -n '4,47p' "$0" + sed -n '4,65p' "$0" } cmd_hosts() { @@ -115,17 +150,26 @@ cmd_hosts() { echo "no hosts configured. Add with: ssh-helper.sh add " return 0 fi - printf 'alias user@host port cred master hciroot-pin\n' - printf '%s\n' '───── ───────── ──── ──── ────── ───────────' - awk -F'\t' 'NR>1' "$SSH_HOSTS_FILE" | while IFS=$'\t' read -r alias addr port hciroot; do + printf 'alias user@host port cred master direct hciroot-pin\n' + printf '%s\n' '───── ───────── ──── ──── ────── ────── ───────────' + awk -F'\t' 'NR>1' "$SSH_HOSTS_FILE" | while IFS=$'\t' read -r alias addr port hciroot direct; do local cred_state="–" [ -f "$SSH_CREDS_DIR/$alias" ] && cred_state="✓" + # v0.8.17: in DIRECT mode the master column reads "n/a" — there is no master + # to probe (and probing it would be a meaningless socket check). The direct + # column shows on/–. + local direct_state="–" + [ "$direct" = "on" ] && direct_state="on" local master_state="–" - local sock="$SSH_SOCKETS_DIR/$alias.sock" - if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then - master_state="open" + if [ "$direct" = "on" ]; then + master_state="n/a" + else + local sock="$SSH_SOCKETS_DIR/$alias.sock" + if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then + master_state="open" + fi fi - printf '%-20s%-52s%-6s%-6s%-8s%s\n' "$alias" "$addr" "${port:-22}" "$cred_state" "$master_state" "${hciroot:-–}" + printf '%-20s%-52s%-6s%-6s%-8s%-8s%s\n' "$alias" "$addr" "${port:-22}" "$cred_state" "$master_state" "$direct_state" "${hciroot:-–}" done } @@ -149,7 +193,8 @@ cmd_add() { fi umask 077 # v0.8.15: write an empty 4th (hciroot) field so the row layout is uniform. - printf '%s\t%s\t%s\t%s\n' "$alias" "$addr" "$port" "" >> "$SSH_HOSTS_FILE" + # v0.8.17: write an empty 5th (direct) field too — uniform 5-column rows. + printf '%s\t%s\t%s\t%s\t%s\n' "$alias" "$addr" "$port" "" "" >> "$SSH_HOSTS_FILE" chmod 600 "$SSH_HOSTS_FILE" ok "added $alias → $addr (port $port). Next: ssh-helper.sh pass $alias" } @@ -167,10 +212,12 @@ cmd_set_hciroot() { local addr_port; addr_port=$(read_host_addr "$alias") [ -n "$addr_port" ] || die "no such alias: $alias (run 'add' first)" # Rewrite the row in place, setting/replacing column 4. awk handles rows that - # still have only 3 columns (legacy) by assigning $4 directly. + # still have only 3 columns (legacy) by assigning $4 directly. v0.8.17: also + # backfill the column-5 (direct) header so the layout stays uniform; existing + # column-5 values on data rows are preserved untouched (we only touch $4). local tmp; tmp=$(mktemp) awk -F'\t' -v OFS='\t' -v a="$alias" -v r="$newroot" ' - NR==1 { if (NF < 4) { $4="hciroot" } print; next } + NR==1 { if (NF < 4) { $4="hciroot" } if (NF < 5) { $5="direct" } print; next } $1==a { $4=r; print; next } { print } ' "$SSH_HOSTS_FILE" > "$tmp" && mv "$tmp" "$SSH_HOSTS_FILE" @@ -183,6 +230,51 @@ cmd_set_hciroot() { fi } +# cmd_set_direct ALIAS on|off — toggle (or clear) DIRECT mode for an alias. +# Persisted as column 5 of the hosts TSV. v0.8.17. +# +# When ON, cmd_exec/cmd_discover/cmd_pull_smat run remote commands over a FRESH +# per-command sshpass connection (forced password auth) instead of multiplexing +# through a ControlMaster socket — the fix for hosts (e.g. qa → shdclvf01q) where +# the master opens & authenticates fine but any multiplexed session dies with +# "read from master failed: Connection reset by peer". The pinned HCIROOT (set +# via set-hciroot) is still honoured — the remote command is shaped by the same +# _remote_cmd_for path; only the dispatch (direct vs master socket) changes. +cmd_set_direct() { + local alias="${1:-}" mode="${2:-}" + [ -n "$alias" ] || die "usage: set-direct on|off" + # Trim surrounding whitespace so a trailing-space arg from the slash path + # (e.g. `/ssh-set-direct qa on `) still normalizes cleanly to on|off. + mode="${mode#"${mode%%[![:space:]]*}"}" # leading + mode="${mode%"${mode##*[![:space:]]}"}" # trailing + case "$mode" in + on|ON|On) mode="on" ;; + off|OFF|Off|'') mode="" ;; # empty/off → clear the flag (master mode) + *) die "usage: set-direct on|off (got: $mode)" ;; + esac + ensure_layout + local addr_port; addr_port=$(read_host_addr "$alias") + [ -n "$addr_port" ] || die "no such alias: $alias (run 'add' first)" + # Rewrite the row in place, setting/replacing column 5. awk backfills the + # column-4 (hciroot) and column-5 (direct) headers for legacy <5-column files, + # and pads a matching data row to 5 columns before assigning $5 so we never + # clobber a pinned HCIROOT in column 4. + local tmp; tmp=$(mktemp) + awk -F'\t' -v OFS='\t' -v a="$alias" -v m="$mode" ' + NR==1 { if (NF < 4) { $4="hciroot" } if (NF < 5) { $5="direct" } print; next } + $1==a { if (NF < 4) { $4="" } $5=m; print; next } + { print } + ' "$SSH_HOSTS_FILE" > "$tmp" && mv "$tmp" "$SSH_HOSTS_FILE" + chmod 600 "$SSH_HOSTS_FILE" + if [ "$mode" = "on" ]; then + ok "DIRECT mode ON for $alias" + ok " (all remote ops bypass the ControlMaster — fresh per-command sshpass, forced password auth)" + ok " next: ssh-helper.sh setup $alias (validates the password; no master opened in direct mode)" + else + ok "DIRECT mode OFF for $alias (reverting to ControlMaster multiplexing)" + fi +} + cmd_remove() { local alias="${1:-}" [ -n "$alias" ] || die "usage: remove " @@ -225,6 +317,48 @@ cmd_setup() { addr=$(printf '%s' "$addr_port" | cut -f1) port=$(printf '%s' "$addr_port" | cut -f2) ensure_layout + + # v0.8.17: DIRECT mode — opening a ControlMaster is pointless (the box rejects + # multiplexing). Instead, VALIDATE that the stored password authenticates by + # running one trivial direct command, then report ready. No master socket is + # created. This makes Bryan's flow: + # /ssh-pass → /ssh-set-hciroot → /ssh-set-direct on → /sites + if _alias_is_direct "$alias"; then + local credfile="$SSH_CREDS_DIR/$alias" + [ -f "$credfile" ] || die "no password set for $alias — run 'pass $alias' first" + require_sshpass + ok "DIRECT mode for $alias ($addr:$port) — validating the stored password (no master in direct mode)..." + local errfile; errfile=$(mktemp 2>/dev/null || echo "/tmp/larry-ssh-direct-setup.err.$$") + # A trivial, side-effect-free probe. Forced password auth, host-key checked, + # no master. STDERR (banner/sudo) is captured for failure diagnosis only. + sshpass -f "$credfile" ssh \ + -o "PreferredAuthentications=password" \ + -o "PubkeyAuthentication=no" \ + -o "NumberOfPasswordPrompts=1" \ + -o "StrictHostKeyChecking=accept-new" \ + -o "ControlMaster=no" \ + -o "ControlPath=none" \ + -o "ConnectTimeout=$_DIRECT_CONNECT_TIMEOUT" \ + -p "$port" \ + "$addr" 'true' 2>"$errfile" + local vrc=$? + if [ "$vrc" -eq 0 ]; then + rm -f "$errfile" + ok "✓ direct auth OK: $alias → $addr:$port (no master; ready for /sites $alias)" + return 0 + fi + printf 'ssh-helper: direct validation FAILED for %s (rc=%d).\n' "$alias" "$vrc" >&2 + local filtered; filtered=$(_filter_direct_stderr < "$errfile") + if [ -n "$filtered" ]; then + printf 'ssh-helper: remote stderr (benign banner/sudo lines stripped):\n' >&2 + printf '%s\n' "$filtered" >&2 + else + printf 'ssh-helper: no non-benign stderr — almost certainly the stored password is stale/rotated. Re-run: ssh-helper.sh pass %s\n' "$alias" >&2 + fi + rm -f "$errfile" + return 1 + fi + local sock="$SSH_SOCKETS_DIR/$alias.sock" if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then ok "master already open for $alias ($addr:$port)" @@ -432,12 +566,146 @@ _remote_cmd_for() { fi } -cmd_exec() { - local alias="${1:-}" - [ -n "$alias" ] || die "usage: exec " - shift - local cmd="$*" - [ -n "$cmd" ] || die "no command given" +# ── v0.8.17: DIRECT (no-multiplex) dispatch ────────────────────────────────── +# +# Some Cloverleaf hosts reject SSH ControlMaster session multiplexing: the master +# opens and authenticates, but every session multiplexed over it dies with +# "read from master failed: Connection reset by peer", then ssh falls back to a +# fresh connection that fails auth. Confirmed live on qa (bryjohnx@lhsixfqa → +# shdclvf01q, cis2025.01). The fix is to run each remote command as its OWN +# fresh ssh connection with forced password auth (sshpass -f ) — NO +# master socket. This is legitimate password auth, NOT a traffic bypass: no +# proxy, no tunnel, no masking, and host-key checking stays on (accept-new). +# +# _DIRECT_CONNECT_TIMEOUT — seconds for ssh ConnectTimeout (env-overridable). +_DIRECT_CONNECT_TIMEOUT="${LARRY_SSH_DIRECT_TIMEOUT:-10}" + +# _direct_creds ALIAS → echoes the credfile path (the file /ssh-pass writes), +# or empty (and warns) if absent. Same file the ControlMaster path uses. +_direct_creds() { + local alias="$1" + local credfile="$SSH_CREDS_DIR/$alias" + [ -f "$credfile" ] && { printf '%s' "$credfile"; return 0; } + printf '' + return 1 +} + +# _filter_direct_stderr — strip known-benign noise from a direct session's +# STDERR so the parsed STDOUT result is presented clean. The qa login profile +# emits a pre-auth banner ("Unauthorized access…/monitored", "WARNING", etc.) +# AND `sudo: a terminal is required` / `sudo: a password is required` / +# `sudo: no tty present` on STDERR for non-interactive sessions. Those are +# expected and harmless for our read-only enumeration — drop them. ANYTHING +# ELSE that remains is a real signal and is surfaced by the caller, but ONLY on +# an actual non-zero command failure (see _run_direct). Reads stderr on stdin; +# echoes the filtered remainder. The patterns are intentionally narrow so we +# never swallow a genuine error message. +_filter_direct_stderr() { + grep -ivE \ + 'unauthorized (access|use)|access is monitored|monitored and recorded|this (system|computer|is a) .*(private|restricted|government|corporate)|by (logging in|accessing|using) .*(you )?(consent|agree)|all activ(ity|ities) .*(may be|are) (monitored|logged|recorded)|disconnect immediately|^[[:space:]]*\*+[[:space:]]*$|^[[:space:]]*WARNING[[:space:]]*[:!]?|sudo: a terminal is required|sudo: a password is required|sudo: no tty present|sudo: sorry, you must have a tty' \ + 2>/dev/null || true +} + +# _run_direct ALIAS REMOTE_CMD → run REMOTE_CMD on ALIAS over a FRESH per-command +# sshpass connection (no ControlMaster). REMOTE_CMD must already be shaped by +# _remote_cmd_for (so the HCIROOT pin / login-shell wrapper is honoured). STDOUT +# is passed through verbatim (the parsed-clean result). STDERR is captured, +# filtered for the known-benign banner+sudo lines, and surfaced ONLY when the +# remote command exits non-zero. Returns the remote command's exit code. +_run_direct() { + local alias="$1" remote_cmd="$2" + require_sshpass + local addr_port; addr_port=$(read_host_addr "$alias") + [ -n "$addr_port" ] || die "no such alias: $alias" + local addr port + addr=$(printf '%s' "$addr_port" | cut -f1) + port=$(printf '%s' "$addr_port" | cut -f2) + local credfile; credfile=$(_direct_creds "$alias") \ + || die "no password set for $alias — run 'pass $alias' first (direct mode needs the stored credential per command)" + + local errfile; errfile=$(mktemp 2>/dev/null || echo "/tmp/larry-ssh-direct.err.$$") + # NO ControlMaster/ControlPath. Forced password method so sshpass feeds the + # password cleanly past any pre-auth banner (same rationale as the master + # path's v0.8.15 PreferredAuthentications=password hardening). BatchMode is + # NOT set — sshpass supplies the password non-interactively via the askpass + # file descriptor; BatchMode would suppress that path on some builds. + sshpass -f "$credfile" ssh \ + -o "PreferredAuthentications=password" \ + -o "PubkeyAuthentication=no" \ + -o "NumberOfPasswordPrompts=1" \ + -o "StrictHostKeyChecking=accept-new" \ + -o "ControlMaster=no" \ + -o "ControlPath=none" \ + -o "ConnectTimeout=$_DIRECT_CONNECT_TIMEOUT" \ + -p "$port" \ + "$addr" "$remote_cmd" 2>"$errfile" + local rc=$? + + # On a real failure, surface the FILTERED stderr (banner + sudo noise removed) + # so the operator sees the genuine reason without the boilerplate. On success, + # the benign noise is simply dropped — stdout is already the clean result. + if [ "$rc" -ne 0 ]; then + local filtered; filtered=$(_filter_direct_stderr < "$errfile") + if [ -n "$filtered" ]; then + printf 'ssh-helper: direct command failed for %s (rc=%d). Remote stderr (benign banner/sudo lines stripped):\n' "$alias" "$rc" >&2 + printf '%s\n' "$filtered" >&2 + else + printf 'ssh-helper: direct command failed for %s (rc=%d) with no non-benign stderr — likely an auth failure (stale/rotated password?) or a connection reset. Re-check: ssh-helper.sh setup %s\n' "$alias" "$rc" "$alias" >&2 + fi + fi + rm -f "$errfile" + return "$rc" +} + +# _direct_scp ALIAS SRC DST → scp SRC→DST over a FRESH sshpass connection (no +# ControlMaster), forced password auth, host-key checked. Either SRC or DST is a +# remote spec of the form ":" supplied by the caller (cmd_pull builds +# it). Returns scp's exit code. STDERR (incl. banner/sudo noise) is filtered the +# same way as _run_direct and surfaced only on failure. v0.8.17. +_direct_scp() { + local alias="$1" src="$2" dst="$3" + require_sshpass + local addr_port; addr_port=$(read_host_addr "$alias") + [ -n "$addr_port" ] || die "no such alias: $alias" + local port; port=$(printf '%s' "$addr_port" | cut -f2) + local credfile; credfile=$(_direct_creds "$alias") \ + || die "no password set for $alias — run 'pass $alias' first" + local errfile; errfile=$(mktemp 2>/dev/null || echo "/tmp/larry-scp-direct.err.$$") + sshpass -f "$credfile" scp -q \ + -o "PreferredAuthentications=password" \ + -o "PubkeyAuthentication=no" \ + -o "NumberOfPasswordPrompts=1" \ + -o "StrictHostKeyChecking=accept-new" \ + -o "ControlMaster=no" \ + -o "ControlPath=none" \ + -o "ConnectTimeout=$_DIRECT_CONNECT_TIMEOUT" \ + -P "$port" \ + "$src" "$dst" 2>"$errfile" + local rc=$? + if [ "$rc" -ne 0 ]; then + local filtered; filtered=$(_filter_direct_stderr < "$errfile") + printf 'ssh-helper: direct scp failed for %s (rc=%d):\n' "$alias" "$rc" >&2 + [ -n "$filtered" ] && printf '%s\n' "$filtered" >&2 + fi + rm -f "$errfile" + return "$rc" +} + +# _dispatch_remote ALIAS RAW_CMD → run RAW_CMD on ALIAS, choosing the transport: +# DIRECT mode (column 5 == on) → fresh per-command sshpass (_run_direct) +# else → existing ControlMaster multiplex +# In BOTH cases the remote command is shaped identically by _remote_cmd_for, so +# the HCIROOT pin and login-shell semantics are unchanged across transports. +# Requires (master mode only) that the master is open — the master-mode branch +# preserves the prior die-on-closed-master behaviour exactly. +_dispatch_remote() { + local alias="$1" raw="$2" + local shaped; shaped=$(_remote_cmd_for "$alias" "$raw") + if _alias_is_direct "$alias"; then + _run_direct "$alias" "$shaped" + return $? + fi + # ── ControlMaster path (unchanged for non-direct aliases) ──────────────── local addr_port; addr_port=$(read_host_addr "$alias") [ -n "$addr_port" ] || die "no such alias: $alias" local addr port @@ -447,11 +715,25 @@ cmd_exec() { if [ ! -S "$sock" ] || ! ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then die "no open master for $alias — run 'setup $alias' first" fi - # Multiplexed; no password needed. If the alias has a pinned HCIROOT we export - # it explicitly and skip the login profile (v0.8.15 sudo-gated-profile fix); - # otherwise we run in a login shell so $HCIROOT et al. populate from the remote - # Cloverleaf login profile (see _build_login_cmd / _remote_cmd_for). - ssh -S "$sock" -p "$port" -o BatchMode=yes "$addr" "$(_remote_cmd_for "$alias" "$cmd")" + ssh -S "$sock" -p "$port" -o BatchMode=yes "$addr" "$shaped" +} + +cmd_exec() { + local alias="${1:-}" + [ -n "$alias" ] || die "usage: exec " + shift + local cmd="$*" + [ -n "$cmd" ] || die "no command given" + local addr_port; addr_port=$(read_host_addr "$alias") + [ -n "$addr_port" ] || die "no such alias: $alias" + # v0.8.17: transport selection is centralised in _dispatch_remote. + # • DIRECT mode → a fresh per-command sshpass connection (no master), with + # benign banner/sudo stderr stripped and real errors surfaced on failure. + # • else → the existing ControlMaster multiplex (no password needed). + # In both cases the remote command is shaped identically: a pinned HCIROOT is + # exported explicitly + login profile skipped (v0.8.15); otherwise a login + # shell populates $HCIROOT et al. (see _remote_cmd_for / _build_login_cmd). + _dispatch_remote "$alias" "$cmd" } # cmd_discover ALIAS — proactively detect the remote Cloverleaf environment. @@ -471,9 +753,14 @@ cmd_discover() { local addr port addr=$(printf '%s' "$addr_port" | cut -f1) port=$(printf '%s' "$addr_port" | cut -f2) - local sock="$SSH_SOCKETS_DIR/$alias.sock" - if [ ! -S "$sock" ] || ! ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then - die "no open master for $alias — run 'setup $alias' first" + # v0.8.17: in DIRECT mode there is no master to check — _dispatch_remote runs a + # fresh per-command sshpass connection below. Only validate an open master for + # the (unchanged) multiplex path. + if ! _alias_is_direct "$alias"; then + local sock="$SSH_SOCKETS_DIR/$alias.sock" + if [ ! -S "$sock" ] || ! ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then + die "no open master for $alias — run 'setup $alias' first" + fi fi # A single remote script. It: @@ -547,7 +834,11 @@ $s"; fi; dropped=$(printf "%s" "$dropped" | sed "s/^ *//"); [ -n "$dropped" ] && printf "EXCLUDED\t%s\n" "$dropped"; printf "%s\n" "$kept" | while IFS= read -r s; do [ -n "$s" ] && printf "SITE\t%s\n" "$s"; done' - ssh -S "$sock" -p "$port" -o BatchMode=yes "$addr" "$(_remote_cmd_for "$alias" "$remote")" + # v0.8.17: dispatch over DIRECT sshpass or the ControlMaster, per the alias's + # flag. The TSV that the tool layer parses is on STDOUT and stays clean; the + # qa banner/sudo noise on STDERR is stripped by _run_direct's filter (direct + # mode) and surfaced only on a real non-zero failure. + _dispatch_remote "$alias" "$remote" } # ── v0.6.8: scp helpers that multiplex via the existing ControlMaster ──────── @@ -594,6 +885,34 @@ _pull_cache_path() { cmd_pull() { local alias="${1:-}" remote="${2:-}" local_path="${3:-}" [ -n "$alias" ] && [ -n "$remote" ] || die "usage: pull [local_path]" + + # v0.8.17: DIRECT mode — no ControlMaster. The remote-size probe rides a fresh + # per-command sshpass connection (_dispatch_remote → _run_direct), and the + # transfer uses _direct_scp (also fresh sshpass). Everything else (cache path, + # size verification, the clean final-line local path) is identical. + if _alias_is_direct "$alias"; then + local addr_port; addr_port=$(read_host_addr "$alias") + [ -n "$addr_port" ] || die "no such alias: $alias" + local _d_addr; _d_addr=$(printf '%s' "$addr_port" | cut -f1) + [ -z "$local_path" ] && local_path=$(_pull_cache_path "$alias" "$remote") + mkdir -p "$(dirname "$local_path")" 2>/dev/null + local remote_size + remote_size=$(coerce_int "$(_dispatch_remote "$alias" "wc -c < $(printf '%q' "$remote") 2>/dev/null" 2>/dev/null)" "") + if [ -z "$remote_size" ] || ! [[ "$remote_size" =~ ^[0-9]+$ ]]; then + die "remote file not found or not readable: $remote" + fi + if _direct_scp "$alias" "$_d_addr:$remote" "$local_path"; then + local got; got=$(coerce_int "$(wc -c < "$local_path" 2>/dev/null)" 0) + if [ "$got" != "$remote_size" ]; then + die "partial transfer: remote=$remote_size bytes, local=$got bytes ($local_path)" + fi + ok "pulled $alias:$remote → $local_path ($got bytes, direct)" + printf '%s\n' "$local_path" + return 0 + fi + return 1 + fi + _resolve_open_master "$alias" [ -z "$local_path" ] && local_path=$(_pull_cache_path "$alias" "$remote") mkdir -p "$(dirname "$local_path")" 2>/dev/null @@ -687,7 +1006,13 @@ cmd_pull_smat() { local alias="${1:-}" site="${2:-}" thread="${3:-}" days_back="${4:-}" [ -n "$alias" ] && [ -n "$site" ] && [ -n "$thread" ] \ || die "usage: pull-smat [days_back]" - _resolve_open_master "$alias" + # v0.8.17: in DIRECT mode every remote op is a fresh per-command sshpass + # connection — there is no master to resolve. Only require an open master for + # the (unchanged) multiplex path; both the find/sample command dispatches and + # the full-file scp below pick the transport via the direct flag. + if ! _alias_is_direct "$alias"; then + _resolve_open_master "$alias" + fi # Discover the remote .smatdb path. $HCISITEDIR/$HCIROOT are resolved by the # LOGIN shell (see _build_login_cmd) — the v0.8.13 fix — so we no longer @@ -711,7 +1036,11 @@ cmd_pull_smat() { local _smat_raw remote_smatdb # v0.8.15: honour a pinned HCIROOT (explicit export, no sudo-gated login profile). - _smat_raw=$(ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" "$(_remote_cmd_for "$alias" "$find_cmd")" 2>&1) + # v0.8.17: dispatch over DIRECT sshpass or the ControlMaster per the alias flag. + # We capture stdout+stderr together (2>&1) as before — the SMATDB_PATH sentinel + # / ERROR: parsing already tolerates banner/sudo noise interleaved on stderr, + # so the direct path needs no extra filtering here. + _smat_raw=$(_dispatch_remote "$alias" "$find_cmd" 2>&1) remote_smatdb=$(printf '%s\n' "$_smat_raw" | grep '^SMATDB_PATH:' | tail -1) if [ -n "$remote_smatdb" ]; then remote_smatdb="${remote_smatdb#SMATDB_PATH:}" @@ -769,7 +1098,8 @@ cmd_pull_smat() { # and skip the sudo-gated login profile (v0.8.15). Note: when pinned, sqlite3 # must be resolvable on the default non-login PATH; if it is not, the # sample_cmd already emits a clear "ERROR: sqlite3 not on remote PATH". - ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" "$(_remote_cmd_for "$alias" "$sample_cmd")" + # v0.8.17: dispatch over DIRECT sshpass or the ControlMaster per the alias flag. + _dispatch_remote "$alias" "$sample_cmd" } case "${1:-help}" in @@ -778,6 +1108,7 @@ case "${1:-help}" in remove|rm) shift; cmd_remove "$@" ;; pass|passwd) shift; cmd_pass "$@" ;; set-hciroot|hciroot) shift; cmd_set_hciroot "$@" ;; + set-direct|direct) shift; cmd_set_direct "$@" ;; setup|open) shift; cmd_setup "$@" ;; close|exit) shift; cmd_close "$@" ;; status) shift; cmd_status "$@" ;;