v0.8.15: legacy/qa remote-enumeration fix — per-alias HCIROOT pin (sudo-gated profile bypass), hcisitelist-free NetConfig walk, ControlMaster banner+rotating-pw hardening; zero traffic-bypass primitives
MAJOR-1: regenerate MANIFEST (larry.sh, lib/ssh-helper.sh, VERSION, CHANGELOG.md hashes now authoritative for the v0.8.15 bytes). MINOR-1: print_help /sites line documents the --hciroot <path> pin convenience and the pinned-vs-login resolution distinction. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6703ee154e
commit
fc667e2451
62
CHANGELOG.md
62
CHANGELOG.md
@ -4,6 +4,68 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here.
|
|||||||
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
Versioning is loose-semver; bumps trigger the in-process self-update on every
|
||||||
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
running client via `LARRY_BASE_URL` + `MANIFEST`.
|
||||||
|
|
||||||
|
## v0.8.15 — 2026-05-28
|
||||||
|
|
||||||
|
Legacy/qa remote-enumeration fix (Clover). Three confirmed-live properties of
|
||||||
|
the qa box `bryjohnx@lhsixfqa` (→ Cloverleaf host `shdclvf01q`, release
|
||||||
|
cis2025.01) broke v0.8.13/v0.8.14 remote site enumeration: a **sudo-gated login
|
||||||
|
profile** (a non-interactive SSH session hits `sudo: a terminal is required`, so
|
||||||
|
`bash -lc` can't initialize the env and `$HCIROOT` comes back EMPTY); **no
|
||||||
|
`hcisitelist`** on the box; and a **password that rotates ~every 12h** (stale
|
||||||
|
stored credential, and a pre-auth banner that masked the real auth error). All
|
||||||
|
three are now handled. The no-traffic-bypass security line is unchanged — **zero
|
||||||
|
proxy / masking / evasion primitives** (same Gundersen-class control).
|
||||||
|
|
||||||
|
1. **Per-alias HCIROOT pin (load-bearing).** New `ssh-helper.sh set-hciroot
|
||||||
|
<alias> <path>` and the `/ssh-set-hciroot <alias> <path>` slash command
|
||||||
|
persist an HCIROOT for an alias as a 4th column in `.ssh-hosts.tsv` (old
|
||||||
|
3-column files stay valid; an empty path clears the pin). When an alias is
|
||||||
|
pinned, `exec` / `discover` / `pull-smat` run the remote command with
|
||||||
|
`HCIROOT=<path>` exported EXPLICITLY under a NON-login `sh -c` — they do NOT
|
||||||
|
wrap in `bash -lc`, so the sudo-gated login profile is never invoked. A single
|
||||||
|
chokepoint, `_remote_cmd_for`, makes every remote path honour the pin
|
||||||
|
identically (unpinned aliases keep the v0.8.13 `bash -lc` login-shell
|
||||||
|
behaviour, unchanged). `/sites <alias> --hciroot <path>` is a convenience that
|
||||||
|
persists the pin then enumerates. This makes qa work regardless of the broken
|
||||||
|
profile. `qa` HCIROOT = `/hci/cis2025.01/integrator`.
|
||||||
|
2. **Portable site enumeration (no `hcisitelist` dependency).** `discover`'s
|
||||||
|
remote script now makes the **NetConfig walk the PRIMARY path**, identical to
|
||||||
|
`lib/each-site.sh` (`find $HCIROOT -mindepth 1 -maxdepth 2 -name NetConfig
|
||||||
|
-type f` → dirname → basename → sort -u). `hcisitelist` is consulted ONLY if
|
||||||
|
it is actually present AND the walk found nothing — never as the dependency.
|
||||||
|
Works on a box with no `hcisitelist`. Emits clear `NOTE` lines (HCIROOT empty
|
||||||
|
→ suggests the pin; not a directory; no NetConfigs found).
|
||||||
|
3. **ControlMaster-open hardening (banner + rotating password).** `setup` now
|
||||||
|
forces `-o PreferredAuthentications=password -o PubkeyAuthentication=no -o
|
||||||
|
NumberOfPasswordPrompts=1` so sshpass feeds the password cleanly past the
|
||||||
|
pre-auth banner and a stale credential fails fast instead of hanging; it
|
||||||
|
surfaces the REAL auth error (greps for permission/auth/password/host-key
|
||||||
|
keywords) instead of echoing only the banner; and on an auth failure it
|
||||||
|
RE-PROMPTS for a fresh password (the 12h rotation), stores it 0600, and
|
||||||
|
retries ONCE. Every failure path emits a clear next step — never a silent
|
||||||
|
no-op.
|
||||||
|
4. **`/sites` excludes non-real entries (transparent).** The enumeration now
|
||||||
|
drops (a) static scaffolding/special sites — `helloworld siteProto master`,
|
||||||
|
a documented, tunable `SITES_EXCLUDE` env var — and (b) any site dir whose
|
||||||
|
name equals the host: the REMOTE `discover` walk computes `hostname -s` and
|
||||||
|
full `hostname` and drops a match (qa's alias host is `lhsixfqa` but the
|
||||||
|
engine box is `shdclvf01q`; a dir just named after the box is not a site),
|
||||||
|
and also drops a match against the alias's configured SSH host. The filter
|
||||||
|
is applied at the SINGLE enumeration source so REMOTE (pinned + login-shell)
|
||||||
|
and LOCAL `/sites` behave identically. NOT silently hidden: the walk emits an
|
||||||
|
`EXCLUDED` line and the tool layer renders the real count as the headline with
|
||||||
|
a note, e.g. `sites: 21 (excluded: helloworld, master, siteProto)`. Acceptance
|
||||||
|
(qa, 24 raw dirs): `/sites qa` → 21, with the 3 exclusions noted; no dir
|
||||||
|
matches `shdclvf01q`. The no-traffic-bypass security line is unchanged.
|
||||||
|
|
||||||
|
Acceptance (qa): with HCIROOT pinned to `/hci/cis2025.01/integrator`,
|
||||||
|
`/sites qa` returns the site list via the NetConfig walk with no `bash -lc` and
|
||||||
|
no `hcisitelist`; `/ssh-setup qa` with a fresh password opens the master past
|
||||||
|
the banner. Self-verified with `bash -n` on every changed file. POSIX-sh remote
|
||||||
|
scripts; compatible with bash 3.2 / Cygwin; no regression to unpinned aliases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.8.14 — 2026-05-28
|
## v0.8.14 — 2026-05-28
|
||||||
|
|
||||||
Locked-down-box survivability (Clover): make the full toolkit usable BY HAND
|
Locked-down-box survivability (Clover): make the full toolkit usable BY HAND
|
||||||
|
|||||||
8
MANIFEST
8
MANIFEST
@ -23,16 +23,16 @@
|
|||||||
# scripts/make-manifest.sh and bump VERSION.
|
# scripts/make-manifest.sh and bump VERSION.
|
||||||
|
|
||||||
# Top-level scripts
|
# Top-level scripts
|
||||||
larry.sh 2c10a738cd3fc14012b4d67fcdc58be40147593f604a3ddc66b19b6b4b0ea081
|
larry.sh 2e7650eb7a014624bd6956c30ce3a54e0e87d4ccfc73bb0b2ae82d1a31b882e0
|
||||||
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa
|
||||||
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831
|
||||||
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0
|
||||||
install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1
|
install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
VERSION af0c015a6470ca542b68d7084a55652bee7798013d87487cd05fac1484a25980
|
VERSION 8517de55d0fc1041caab07518dbf7da86dba47c3befe0a6ef84d005872cb799d
|
||||||
MANUAL.md 666128a086b59ff3c31a574aec0c5dd681666d66319da9f078451bf9013ca5e1
|
MANUAL.md 666128a086b59ff3c31a574aec0c5dd681666d66319da9f078451bf9013ca5e1
|
||||||
CHANGELOG.md aa0bd56caf29a0939a7b7d676bec9daed01606f9ac29f0180c0ac72c990d49be
|
CHANGELOG.md 0b8f2dba750577f934935dd7d5805c498afa9d516cd37e5b6cda039cb86ec350
|
||||||
|
|
||||||
# Agent personas (system-prompt overlays)
|
# Agent personas (system-prompt overlays)
|
||||||
agents/larry.md 11ea905fa7cac6fa7baeb11b2d62af07b15a666ce90cfe36491bcbc555244397
|
agents/larry.md 11ea905fa7cac6fa7baeb11b2d62af07b15a666ce90cfe36491bcbc555244397
|
||||||
@ -52,7 +52,7 @@ lib/fetch-safe.sh abecf0045b9856f63ffa346119443c11de56547344be32bddaed9fbae6b021
|
|||||||
lib/oauth.sh 04a93376f88fe53cc1c86a5dbe577735c60375dadd4f2fda55b921ef3cddf22b
|
lib/oauth.sh 04a93376f88fe53cc1c86a5dbe577735c60375dadd4f2fda55b921ef3cddf22b
|
||||||
|
|
||||||
# Secure SSH with ControlMaster (password hidden from Larry-the-LLM)
|
# Secure SSH with ControlMaster (password hidden from Larry-the-LLM)
|
||||||
lib/ssh-helper.sh 7aa2aa7b3860cb48b7ba5120f9efc2563a6cdaed41242f42ecc9dd03fdebeb28
|
lib/ssh-helper.sh 3397945df8184d0bc89853608c097af11b97b37695c5598c979347b6b912e0eb
|
||||||
|
|
||||||
# v0.8.6: work-box → Mac headers.log sync (tsk-2026-05-27-023). Incremental,
|
# 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
|
# offset-tracked push of $LARRY_HOME/log/headers.log to a daemon-watched path
|
||||||
|
|||||||
110
larry.sh
110
larry.sh
@ -78,7 +78,7 @@ set -o pipefail
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Config
|
# Config
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LARRY_VERSION="0.8.14"
|
LARRY_VERSION="0.8.15"
|
||||||
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -1247,7 +1247,7 @@ detect_cloverleaf_env() {
|
|||||||
esac
|
esac
|
||||||
if [ -n "$aliases" ]; then
|
if [ -n "$aliases" ]; then
|
||||||
lines+=("Configured SSH aliases: $(printf '%s' "$aliases" | tr '\n' ' ')")
|
lines+=("Configured SSH aliases: $(printf '%s' "$aliases" | tr '\n' ' ')")
|
||||||
lines+=("For a remote alias: discover its env with the list_sites tool (alias=<name>) — it opens a LOGIN shell so the remote \$HCIROOT resolves. NEVER ask Bryan to export \$HCIROOT for a remote host.")
|
lines+=("For a remote alias: discover its env with the list_sites tool (alias=<name>) — 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 <alias> <path> (e.g. qa → /hci/cis2025.01/integrator).")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${HCIROOT:-}" ]; then
|
if [ -n "${HCIROOT:-}" ]; then
|
||||||
@ -3840,11 +3840,29 @@ tool_list_sites() {
|
|||||||
local rroot; rroot=$(printf '%s\n' "$out" | awk -F'\t' '$1=="HCIROOT"{print $2; exit}')
|
local rroot; rroot=$(printf '%s\n' "$out" | awk -F'\t' '$1=="HCIROOT"{print $2; exit}')
|
||||||
local sites; sites=$(printf '%s\n' "$out" | awk -F'\t' '$1=="SITE"{print $2}' | sort -u)
|
local sites; sites=$(printf '%s\n' "$out" | awk -F'\t' '$1=="SITE"{print $2}' | sort -u)
|
||||||
local note; note=$(printf '%s\n' "$out" | awk -F'\t' '$1=="NOTE"{print $2}')
|
local note; note=$(printf '%s\n' "$out" | awk -F'\t' '$1=="NOTE"{print $2}')
|
||||||
|
# v0.8.15: the discover walk filters out scaffolding/special sites and any
|
||||||
|
# dir named after the host; it reports what it dropped on an EXCLUDED line.
|
||||||
|
# Surface it transparently (never silently hide) — the real-site count stays
|
||||||
|
# the headline below.
|
||||||
|
local excluded; excluded=$(printf '%s\n' "$out" | awk -F'\t' '$1=="EXCLUDED"{print $2; exit}')
|
||||||
local n=0; [ -n "$sites" ] && n=$(printf '%s\n' "$sites" | grep -c .)
|
local n=0; [ -n "$sites" ] && n=$(printf '%s\n' "$sites" | grep -c .)
|
||||||
printf 'Cloverleaf env on alias "%s" (REMOTE, login shell):\n' "$alias"
|
# 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"
|
||||||
|
if [ -f "$_hosts_tsv" ]; then
|
||||||
|
_pin=$(awk -F'\t' -v a="$alias" 'NR>1 && $1==a { print $4; exit }' "$_hosts_tsv" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
[ -n "$_pin" ] && _mode="pinned HCIROOT, no login profile"
|
||||||
|
printf 'Cloverleaf env on alias "%s" (REMOTE, %s):\n' "$alias" "$_mode"
|
||||||
printf ' HCIROOT = %s\n' "${rroot:-<unresolved>}"
|
printf ' HCIROOT = %s\n' "${rroot:-<unresolved>}"
|
||||||
[ -n "$note" ] && printf ' NOTE: %s\n' "$note"
|
[ -n "$note" ] && printf ' NOTE: %s\n' "$note"
|
||||||
printf ' sites: %d\n' "$n"
|
if [ -n "$excluded" ]; then
|
||||||
|
local _exc_csv; _exc_csv=$(printf '%s' "$excluded" | tr ' ' ',' | sed 's/,/, /g')
|
||||||
|
printf ' sites: %d (excluded: %s)\n' "$n" "$_exc_csv"
|
||||||
|
else
|
||||||
|
printf ' sites: %d\n' "$n"
|
||||||
|
fi
|
||||||
[ -n "$sites" ] && printf '%s\n' "$sites" | sed 's/^/ - /'
|
[ -n "$sites" ] && printf '%s\n' "$sites" | sed 's/^/ - /'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@ -3863,10 +3881,37 @@ tool_list_sites() {
|
|||||||
sites=$(find "$root" -mindepth 1 -maxdepth 2 -name NetConfig -type f 2>/dev/null \
|
sites=$(find "$root" -mindepth 1 -maxdepth 2 -name NetConfig -type f 2>/dev/null \
|
||||||
| while IFS= read -r nc; do basename "$(dirname "$nc")"; done | sort -u)
|
| while IFS= read -r nc; do basename "$(dirname "$nc")"; done | sort -u)
|
||||||
fi
|
fi
|
||||||
|
# v0.8.15: apply the SAME exclusion as the REMOTE discover walk — static
|
||||||
|
# scaffolding/special sites plus any dir named after this host. Tunable via
|
||||||
|
# the SITES_EXCLUDE env var (default: helloworld siteProto master). Never
|
||||||
|
# silently hidden: the dropped names are reported alongside the real count.
|
||||||
|
local _sites_exclude="${SITES_EXCLUDE:-helloworld siteProto master}"
|
||||||
|
local _hn_s _hn_f; _hn_s=$(hostname -s 2>/dev/null || true); _hn_f=$(hostname 2>/dev/null || true)
|
||||||
|
local _kept="" _dropped=""
|
||||||
|
if [ -n "$sites" ]; then
|
||||||
|
while IFS= read -r s; do
|
||||||
|
[ -n "$s" ] || continue
|
||||||
|
local _drop=""
|
||||||
|
local x; for x in $_sites_exclude; do [ "$s" = "$x" ] && _drop=1 && break; done
|
||||||
|
[ -z "$_drop" ] && [ -n "$_hn_s" ] && [ "$s" = "$_hn_s" ] && _drop=1
|
||||||
|
[ -z "$_drop" ] && [ -n "$_hn_f" ] && [ "$s" = "$_hn_f" ] && _drop=1
|
||||||
|
if [ -n "$_drop" ]; then _dropped="$_dropped $s"; else _kept="$_kept$s
|
||||||
|
"; fi
|
||||||
|
done <<EOF
|
||||||
|
$sites
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
sites=$(printf '%s' "$_kept" | grep -v '^$' || true)
|
||||||
|
_dropped=$(printf '%s' "$_dropped" | sed 's/^ *//')
|
||||||
local n=0; [ -n "$sites" ] && n=$(printf '%s\n' "$sites" | grep -c .)
|
local n=0; [ -n "$sites" ] && n=$(printf '%s\n' "$sites" | grep -c .)
|
||||||
printf 'Cloverleaf env (LOCAL):\n'
|
printf 'Cloverleaf env (LOCAL):\n'
|
||||||
printf ' HCIROOT = %s\n' "$root"
|
printf ' HCIROOT = %s\n' "$root"
|
||||||
printf ' sites: %d\n' "$n"
|
if [ -n "$_dropped" ]; then
|
||||||
|
local _exc_csv; _exc_csv=$(printf '%s' "$_dropped" | tr ' ' ',' | sed 's/,/, /g')
|
||||||
|
printf ' sites: %d (excluded: %s)\n' "$n" "$_exc_csv"
|
||||||
|
else
|
||||||
|
printf ' sites: %d\n' "$n"
|
||||||
|
fi
|
||||||
[ -n "$sites" ] && printf '%s\n' "$sites" | sed 's/^/ - /'
|
[ -n "$sites" ] && printf '%s\n' "$sites" | sed 's/^/ - /'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4095,7 +4140,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_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":"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 <X>' / '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=<name> (a configured SSH alias, e.g. qa); the tool opens a LOGIN shell on that host so the remote $HCIROOT populates from the operator profile, then enumerates sites (Cloverleaf's hcisitelist if present, else a NetConfig walk). The ControlMaster must be open — if it is not, the result tells you to have Bryan run /ssh-setup <alias>. 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 <X>' / '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=<name> (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 <alias>. If the result shows HCIROOT empty with a NOTE about a sudo-gated profile, tell Bryan to pin it: /ssh-set-hciroot <alias> <path>. 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"]}},
|
{"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"]}},
|
||||||
|
|
||||||
@ -5404,6 +5449,9 @@ Slash commands:
|
|||||||
/ssh-hosts list configured remote hosts
|
/ssh-hosts list configured remote hosts
|
||||||
/ssh-add <alias> <user@host[:port]> register a new host
|
/ssh-add <alias> <user@host[:port]> register a new host
|
||||||
/ssh-pass <alias> set/update password (hidden input; daily rotation OK)
|
/ssh-pass <alias> set/update password (hidden input; daily rotation OK)
|
||||||
|
/ssh-set-hciroot <alias> <path> 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-setup <alias> open a long-lived ControlMaster connection
|
/ssh-setup <alias> open a long-lived ControlMaster connection
|
||||||
/ssh-close <alias> close the ControlMaster
|
/ssh-close <alias> close the ControlMaster
|
||||||
/ssh-status [alias] show open masters + cred presence
|
/ssh-status [alias] show open masters + cred presence
|
||||||
@ -5530,7 +5578,12 @@ Slash commands:
|
|||||||
Audit: every tokenization writes a JSONL entry to
|
Audit: every tokenization writes a JSONL entry to
|
||||||
\$LARRY_HOME/log/auto-phi.log (ts/value/category/token/tier/surface/context).
|
\$LARRY_HOME/log/auto-phi.log (ts/value/category/token/tier/surface/context).
|
||||||
/redetect re-scan for HCIROOT/HCISITE/tools + deployment mode
|
/redetect re-scan for HCIROOT/HCISITE/tools + deployment mode
|
||||||
/sites [alias] count/list Cloverleaf sites — local, or REMOTE via <alias> (login shell)
|
/sites [alias] [--hciroot <path>]
|
||||||
|
count/list Cloverleaf sites — local, or REMOTE via <alias>.
|
||||||
|
Remote resolves \$HCIROOT via a login shell by default; pass
|
||||||
|
--hciroot <path> to PIN it for the alias (persisted) and run
|
||||||
|
the walk with HCIROOT exported, skipping a sudo-gated/non-
|
||||||
|
interactive login profile.
|
||||||
/site <name> switch HCISITE for this session
|
/site <name> switch HCISITE for this session
|
||||||
/pwd show current working directory
|
/pwd show current working directory
|
||||||
/help this help
|
/help this help
|
||||||
@ -5652,6 +5705,7 @@ _LARRY_SLASH_CMDS=(
|
|||||||
/ssh-add
|
/ssh-add
|
||||||
/ssh-remove
|
/ssh-remove
|
||||||
/ssh-pass
|
/ssh-pass
|
||||||
|
/ssh-set-hciroot
|
||||||
/ssh-setup
|
/ssh-setup
|
||||||
/ssh-close
|
/ssh-close
|
||||||
/ssh-status
|
/ssh-status
|
||||||
@ -5709,6 +5763,7 @@ _LARRY_SLASH_CMDS_DESC=(
|
|||||||
[/ssh-add]="<alias> <user@host[:port]> register a new host"
|
[/ssh-add]="<alias> <user@host[:port]> register a new host"
|
||||||
[/ssh-remove]="<alias> remove a host"
|
[/ssh-remove]="<alias> remove a host"
|
||||||
[/ssh-pass]="<alias> set/update password (hidden input)"
|
[/ssh-pass]="<alias> set/update password (hidden input)"
|
||||||
|
[/ssh-set-hciroot]="<alias> <path> pin HCIROOT for an alias (sudo-gated hosts; empty path clears)"
|
||||||
[/ssh-setup]="<alias> open a long-lived ControlMaster"
|
[/ssh-setup]="<alias> open a long-lived ControlMaster"
|
||||||
[/ssh-close]="<alias> close the ControlMaster"
|
[/ssh-close]="<alias> close the ControlMaster"
|
||||||
[/ssh-status]="show open ControlMaster sessions + cred presence"
|
[/ssh-status]="show open ControlMaster sessions + cred presence"
|
||||||
@ -6838,6 +6893,21 @@ main_loop() {
|
|||||||
if [ -z "$rest" ]; then err "usage: /ssh-pass <alias>"; continue; fi
|
if [ -z "$rest" ]; then err "usage: /ssh-pass <alias>"; continue; fi
|
||||||
_run_ssh_helper pass "$rest"
|
_run_ssh_helper pass "$rest"
|
||||||
continue ;;
|
continue ;;
|
||||||
|
/ssh-set-hciroot*) # v0.8.15: pin/persist HCIROOT for an alias so remote
|
||||||
|
# enumeration/exec exports it explicitly and SKIPS the login
|
||||||
|
# profile (for sudo-gated/non-interactive hosts, e.g. qa).
|
||||||
|
local rest; rest=$(_slash_args "/ssh-set-hciroot" "$input")
|
||||||
|
if [ -z "$rest" ]; then
|
||||||
|
err "usage: /ssh-set-hciroot <alias> <path> (empty path clears the pin)"; continue
|
||||||
|
fi
|
||||||
|
local _sh_alias="${rest%% *}" _sh_path="${rest#"$_sh_alias"}"
|
||||||
|
_sh_path="${_sh_path# }"
|
||||||
|
if [ -z "$_sh_alias" ]; then
|
||||||
|
err "usage: /ssh-set-hciroot <alias> <path>"; continue
|
||||||
|
fi
|
||||||
|
# _sh_path may legitimately be empty (clear the pin).
|
||||||
|
_run_ssh_helper set-hciroot "$_sh_alias" "$_sh_path"
|
||||||
|
continue ;;
|
||||||
/ssh-setup*) local rest; rest=$(_slash_args "/ssh-setup" "$input")
|
/ssh-setup*) local rest; rest=$(_slash_args "/ssh-setup" "$input")
|
||||||
if [ -z "$rest" ]; then err "usage: /ssh-setup <alias>"; continue; fi
|
if [ -z "$rest" ]; then err "usage: /ssh-setup <alias>"; continue; fi
|
||||||
_run_ssh_helper setup "$rest"
|
_run_ssh_helper setup "$rest"
|
||||||
@ -6864,9 +6934,29 @@ main_loop() {
|
|||||||
larry_say "re-detected. /env to view."
|
larry_say "re-detected. /env to view."
|
||||||
continue ;;
|
continue ;;
|
||||||
/sites*) # v0.8.13: both-mode site listing. `/sites` → LOCAL; `/sites <alias>`
|
/sites*) # v0.8.13: both-mode site listing. `/sites` → LOCAL; `/sites <alias>`
|
||||||
# → REMOTE (login-shell discover over the open ControlMaster).
|
# → REMOTE discover over the open ControlMaster.
|
||||||
local _site_alias; _site_alias=$(_slash_args "/sites" "$input")
|
# v0.8.15: optional `--hciroot <path>` pass-through. In REMOTE mode
|
||||||
tool_list_sites "${_site_alias:-}" ""
|
# it PINS that HCIROOT for the alias (persisted) before enumerating,
|
||||||
|
# so the remote walk exports HCIROOT explicitly and skips the
|
||||||
|
# sudo-gated login profile. In LOCAL mode it overrides the scan root.
|
||||||
|
local _site_args; _site_args=$(_slash_args "/sites" "$input")
|
||||||
|
local _site_alias="" _site_hciroot="" _tok _expect=""
|
||||||
|
for _tok in $_site_args; do
|
||||||
|
if [ "$_expect" = "hciroot" ]; then _site_hciroot="$_tok"; _expect=""; continue; fi
|
||||||
|
case "$_tok" in
|
||||||
|
--hciroot) _expect="hciroot" ;;
|
||||||
|
--hciroot=*) _site_hciroot="${_tok#--hciroot=}" ;;
|
||||||
|
*) [ -z "$_site_alias" ] && _site_alias="$_tok" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ -n "$_site_alias" ] && [ -n "$_site_hciroot" ]; then
|
||||||
|
# REMOTE + explicit hciroot → persist the pin, then enumerate.
|
||||||
|
_run_ssh_helper set-hciroot "$_site_alias" "$_site_hciroot"
|
||||||
|
tool_list_sites "$_site_alias" ""
|
||||||
|
else
|
||||||
|
# REMOTE (pin/login-shell resolves HCIROOT) or LOCAL (hciroot override).
|
||||||
|
tool_list_sites "${_site_alias:-}" "${_site_hciroot:-}"
|
||||||
|
fi
|
||||||
continue ;;
|
continue ;;
|
||||||
/site\ *) HCISITE="${input#/site }"; HCISITEDIR="$HCIROOT/$HCISITE"
|
/site\ *) HCISITE="${input#/site }"; HCISITEDIR="$HCIROOT/$HCISITE"
|
||||||
export HCISITE HCISITEDIR
|
export HCISITE HCISITEDIR
|
||||||
|
|||||||
@ -20,6 +20,13 @@
|
|||||||
# add <alias> <user@host[:port]> add a host to the alias list
|
# add <alias> <user@host[:port]> add a host to the alias list
|
||||||
# remove <alias> remove an alias (also clears cred + socket)
|
# remove <alias> remove an alias (also clears cred + socket)
|
||||||
# pass <alias> set/update the password (hidden interactive)
|
# pass <alias> set/update the password (hidden interactive)
|
||||||
|
# set-hciroot <alias> <path> pin (persist) $HCIROOT for an alias. When
|
||||||
|
# set, remote enumeration/exec runs with
|
||||||
|
# HCIROOT=<path> exported EXPLICITLY and
|
||||||
|
# WITHOUT the `bash -lc` login wrapper — for
|
||||||
|
# hosts whose login profile is sudo-gated or
|
||||||
|
# otherwise non-interactive (v0.8.15).
|
||||||
|
# Pass an empty path to clear the pin.
|
||||||
# setup <alias> open ControlMaster (uses stored password ONCE)
|
# setup <alias> open ControlMaster (uses stored password ONCE)
|
||||||
# close <alias> close ControlMaster
|
# close <alias> close ControlMaster
|
||||||
# status [alias] show open masters / cred presence
|
# status [alias] show open masters / cred presence
|
||||||
@ -68,7 +75,9 @@ ensure_layout() {
|
|||||||
chmod 700 "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null || true
|
chmod 700 "$LARRY_HOME" "$SSH_CREDS_DIR" "$SSH_SOCKETS_DIR" 2>/dev/null || true
|
||||||
if [ ! -f "$SSH_HOSTS_FILE" ]; then
|
if [ ! -f "$SSH_HOSTS_FILE" ]; then
|
||||||
umask 077
|
umask 077
|
||||||
printf 'alias\taddr\tport\n' > "$SSH_HOSTS_FILE"
|
# 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"
|
||||||
chmod 600 "$SSH_HOSTS_FILE"
|
chmod 600 "$SSH_HOSTS_FILE"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@ -80,13 +89,22 @@ read_host_addr() {
|
|||||||
awk -F'\t' -v a="$alias" 'NR>1 && $1==a { print $2 "\t" $3; exit }' < "$SSH_HOSTS_FILE"
|
awk -F'\t' -v a="$alias" 'NR>1 && $1==a { print $2 "\t" $3; exit }' < "$SSH_HOSTS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# read_host_hciroot ALIAS → echoes the pinned HCIROOT (column 4) or empty.
|
||||||
|
# v0.8.15: a non-empty value means remote commands for this alias run with
|
||||||
|
# HCIROOT exported explicitly and WITHOUT the `bash -lc` login wrapper.
|
||||||
|
read_host_hciroot() {
|
||||||
|
local alias="$1"
|
||||||
|
[ -f "$SSH_HOSTS_FILE" ] || { printf ''; return 0; }
|
||||||
|
awk -F'\t' -v a="$alias" 'NR>1 && $1==a { print $4; exit }' < "$SSH_HOSTS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
require_sshpass() {
|
require_sshpass() {
|
||||||
command -v sshpass >/dev/null 2>&1 \
|
command -v sshpass >/dev/null 2>&1 \
|
||||||
|| die "sshpass not on PATH — install it (apt install sshpass / brew install sshpass) and retry"
|
|| die "sshpass not on PATH — install it (apt install sshpass / brew install sshpass) and retry"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_help() {
|
cmd_help() {
|
||||||
sed -n '4,30p' "$0"
|
sed -n '4,47p' "$0"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_hosts() {
|
cmd_hosts() {
|
||||||
@ -97,9 +115,9 @@ cmd_hosts() {
|
|||||||
echo "no hosts configured. Add with: ssh-helper.sh add <alias> <user@host[:port]>"
|
echo "no hosts configured. Add with: ssh-helper.sh add <alias> <user@host[:port]>"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
printf 'alias user@host port cred master\n'
|
printf 'alias user@host port cred master hciroot-pin\n'
|
||||||
printf '%s\n' '───── ───────── ──── ──── ──────'
|
printf '%s\n' '───── ───────── ──── ──── ────── ───────────'
|
||||||
awk -F'\t' 'NR>1' "$SSH_HOSTS_FILE" | while IFS=$'\t' read -r alias addr port; do
|
awk -F'\t' 'NR>1' "$SSH_HOSTS_FILE" | while IFS=$'\t' read -r alias addr port hciroot; do
|
||||||
local cred_state="–"
|
local cred_state="–"
|
||||||
[ -f "$SSH_CREDS_DIR/$alias" ] && cred_state="✓"
|
[ -f "$SSH_CREDS_DIR/$alias" ] && cred_state="✓"
|
||||||
local master_state="–"
|
local master_state="–"
|
||||||
@ -107,7 +125,7 @@ cmd_hosts() {
|
|||||||
if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then
|
if [ -S "$sock" ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then
|
||||||
master_state="open"
|
master_state="open"
|
||||||
fi
|
fi
|
||||||
printf '%-20s%-52s%-6s%-6s%s\n' "$alias" "$addr" "${port:-22}" "$cred_state" "$master_state"
|
printf '%-20s%-52s%-6s%-6s%-8s%s\n' "$alias" "$addr" "${port:-22}" "$cred_state" "$master_state" "${hciroot:-–}"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,11 +148,41 @@ cmd_add() {
|
|||||||
die "alias '$alias' already exists. Use 'remove $alias' first."
|
die "alias '$alias' already exists. Use 'remove $alias' first."
|
||||||
fi
|
fi
|
||||||
umask 077
|
umask 077
|
||||||
printf '%s\t%s\t%s\n' "$alias" "$addr" "$port" >> "$SSH_HOSTS_FILE"
|
# 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"
|
||||||
chmod 600 "$SSH_HOSTS_FILE"
|
chmod 600 "$SSH_HOSTS_FILE"
|
||||||
ok "added $alias → $addr (port $port). Next: ssh-helper.sh pass $alias"
|
ok "added $alias → $addr (port $port). Next: ssh-helper.sh pass $alias"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# cmd_set_hciroot ALIAS [PATH] — pin (or clear) the HCIROOT for an alias.
|
||||||
|
# Persisted as column 4 of the hosts TSV. An empty/omitted PATH clears the pin.
|
||||||
|
# When set, cmd_exec/cmd_discover/cmd_pull_smat run remote commands with
|
||||||
|
# HCIROOT=<path> exported EXPLICITLY and WITHOUT the `bash -lc` login wrapper —
|
||||||
|
# the v0.8.15 fix for hosts whose login profile is sudo-gated (a non-interactive
|
||||||
|
# SSH session hits `sudo: a terminal is required` and never exports $HCIROOT).
|
||||||
|
cmd_set_hciroot() {
|
||||||
|
local alias="${1:-}" newroot="${2:-}"
|
||||||
|
[ -n "$alias" ] || die "usage: set-hciroot <alias> <path> (empty path clears the pin)"
|
||||||
|
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 4. awk handles rows that
|
||||||
|
# still have only 3 columns (legacy) by assigning $4 directly.
|
||||||
|
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 }
|
||||||
|
$1==a { $4=r; print; next }
|
||||||
|
{ print }
|
||||||
|
' "$SSH_HOSTS_FILE" > "$tmp" && mv "$tmp" "$SSH_HOSTS_FILE"
|
||||||
|
chmod 600 "$SSH_HOSTS_FILE"
|
||||||
|
if [ -n "$newroot" ]; then
|
||||||
|
ok "pinned HCIROOT for $alias → $newroot"
|
||||||
|
ok " (remote enumeration/exec for $alias will export HCIROOT explicitly and SKIP the login profile)"
|
||||||
|
else
|
||||||
|
ok "cleared HCIROOT pin for $alias (reverting to login-shell \$HCIROOT resolution)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
cmd_remove() {
|
cmd_remove() {
|
||||||
local alias="${1:-}"
|
local alias="${1:-}"
|
||||||
[ -n "$alias" ] || die "usage: remove <alias>"
|
[ -n "$alias" ] || die "usage: remove <alias>"
|
||||||
@ -186,24 +234,99 @@ cmd_setup() {
|
|||||||
[ -f "$credfile" ] || die "no password set for $alias — run 'pass $alias' first"
|
[ -f "$credfile" ] || die "no password set for $alias — run 'pass $alias' first"
|
||||||
require_sshpass
|
require_sshpass
|
||||||
ok "opening ssh master for $alias ($addr:$port) — ControlPersist=$SSH_CONTROL_PERSIST..."
|
ok "opening ssh master for $alias ($addr:$port) — ControlPersist=$SSH_CONTROL_PERSIST..."
|
||||||
if sshpass -f "$credfile" ssh \
|
|
||||||
-o "ControlMaster=yes" \
|
# _try_master_open — one attempt with the stored credential. Returns 0 on a
|
||||||
-o "ControlPath=$sock" \
|
# verified-open master; non-zero otherwise. Stderr from sshpass/ssh lands in
|
||||||
-o "ControlPersist=$SSH_CONTROL_PERSIST" \
|
# the file named by $1 so the caller can classify it.
|
||||||
-o "StrictHostKeyChecking=accept-new" \
|
#
|
||||||
-o "ConnectTimeout=10" \
|
# v0.8.15 hardening (banner + rotating-password):
|
||||||
-p "$port" \
|
# • -o PreferredAuthentications=password -o PubkeyAuthentication=no forces the
|
||||||
-N -f \
|
# password method so sshpass feeds the password cleanly. Without this, on a
|
||||||
"$addr" 2>/tmp/larry-ssh-setup.err; then
|
# box that prints a long pre-auth banner and would otherwise try pubkey
|
||||||
if ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then
|
# first, ssh can consume the password slot on the wrong method and the only
|
||||||
ok "✓ master open: $alias → $addr:$port (socket: $sock)"
|
# thing surfaced is the banner with NO "permission denied" — exactly the
|
||||||
rm -f /tmp/larry-ssh-setup.err
|
# symptom seen on shdclvf01q.
|
||||||
return 0
|
# • -o NumberOfPasswordPrompts=1 so a stale password fails fast (one prompt)
|
||||||
|
# instead of hanging, which lets us re-prompt for the rotated one.
|
||||||
|
_try_master_open() {
|
||||||
|
local errfile="$1"
|
||||||
|
sshpass -f "$credfile" ssh \
|
||||||
|
-o "ControlMaster=yes" \
|
||||||
|
-o "ControlPath=$sock" \
|
||||||
|
-o "ControlPersist=$SSH_CONTROL_PERSIST" \
|
||||||
|
-o "StrictHostKeyChecking=accept-new" \
|
||||||
|
-o "PreferredAuthentications=password" \
|
||||||
|
-o "PubkeyAuthentication=no" \
|
||||||
|
-o "NumberOfPasswordPrompts=1" \
|
||||||
|
-o "ConnectTimeout=10" \
|
||||||
|
-p "$port" \
|
||||||
|
-N -f \
|
||||||
|
"$addr" 2>"$errfile"
|
||||||
|
local rc=$?
|
||||||
|
[ "$rc" -eq 0 ] && ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# _looks_like_auth_failure ERRFILE — heuristic: did this fail on auth (vs.
|
||||||
|
# network/host-key)? sshpass exits 5 on auth failure, but the banner can mask
|
||||||
|
# the textual reason, so we also treat permission/password/auth keywords as
|
||||||
|
# auth failures. A rotated password is the prime suspect on this box.
|
||||||
|
_looks_like_auth_failure() {
|
||||||
|
local errfile="$1"
|
||||||
|
grep -qiE 'permission denied|authentication fail|incorrect password|too many authentication|password:' "$errfile" 2>/dev/null && return 0
|
||||||
|
# Empty-or-banner-only stderr after a password attempt → almost always the
|
||||||
|
# rotated/stale credential. Treat as auth failure so we re-prompt.
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
local errfile="/tmp/larry-ssh-setup.err"
|
||||||
|
: > "$errfile"
|
||||||
|
if _try_master_open "$errfile"; then
|
||||||
|
ok "✓ master open: $alias → $addr:$port (socket: $sock)"
|
||||||
|
rm -f "$errfile"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# First attempt failed. Surface the REAL error (not just the banner) and, if it
|
||||||
|
# looks like an auth failure, re-prompt for a fresh password (12h rotation on
|
||||||
|
# this box) and retry ONCE. Never silently no-op.
|
||||||
|
printf 'ssh-helper: first master-open attempt failed for %s.\n' "$alias" >&2
|
||||||
|
if [ -s "$errfile" ]; then
|
||||||
|
printf 'ssh-helper: ssh/sshpass stderr (auth error, not just the banner):\n' >&2
|
||||||
|
grep -iE 'permission denied|authentication|password|denied|fatal|connection|timed out|refused|host key' "$errfile" >&2 2>/dev/null \
|
||||||
|
|| cat "$errfile" >&2 2>/dev/null
|
||||||
|
else
|
||||||
|
printf 'ssh-helper: (no stderr captured — the box likely printed only its pre-auth banner; the stored password is almost certainly stale)\n' >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if _looks_like_auth_failure "$errfile" && [ -t 0 -o -e /dev/tty ]; then
|
||||||
|
printf 'ssh-helper: looks like the stored password is stale (this host rotates ~every 12h).\n' >&2
|
||||||
|
printf 'Enter a FRESH password for %s (input hidden; Enter to abort): ' "$alias" >&2
|
||||||
|
local pw=""
|
||||||
|
stty -echo 2>/dev/null
|
||||||
|
IFS= read -r pw </dev/tty || true
|
||||||
|
stty echo 2>/dev/null
|
||||||
|
echo "" >&2
|
||||||
|
if [ -n "$pw" ]; then
|
||||||
|
umask 077
|
||||||
|
printf '%s' "$pw" > "$credfile" # NO trailing newline (sshpass -f)
|
||||||
|
chmod 600 "$credfile"
|
||||||
|
ok "stored the fresh password — retrying master open..."
|
||||||
|
: > "$errfile"
|
||||||
|
if _try_master_open "$errfile"; then
|
||||||
|
ok "✓ master open: $alias → $addr:$port (socket: $sock)"
|
||||||
|
rm -f "$errfile"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'ssh-helper: retry with the fresh password ALSO failed. ssh/sshpass stderr:\n' >&2
|
||||||
|
cat "$errfile" >&2 2>/dev/null
|
||||||
|
else
|
||||||
|
printf 'ssh-helper: no password entered — aborting.\n' >&2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
printf 'ssh-helper: setup failed. sshpass/ssh stderr:\n' >&2
|
|
||||||
cat /tmp/larry-ssh-setup.err >&2 2>/dev/null
|
printf 'ssh-helper: master NOT open for %s. Next step: re-run `ssh-helper.sh setup %s` (or the /ssh-setup %s slash command) with a current password; if the host changed, re-check `ssh-helper.sh hosts`.\n' \
|
||||||
rm -f /tmp/larry-ssh-setup.err
|
"$alias" "$alias" "$alias" >&2
|
||||||
|
rm -f "$errfile"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,6 +399,39 @@ _build_login_cmd() {
|
|||||||
printf "bash -lc '%s'" "$esc"
|
printf "bash -lc '%s'" "$esc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# v0.8.15 (sudo-gated-profile fix): when an alias has a pinned HCIROOT, the
|
||||||
|
# remote command must NOT go through the login profile (`bash -lc`). On hosts
|
||||||
|
# whose login profile is sudo-gated, a non-interactive SSH session trips
|
||||||
|
# `sudo: a terminal is required`, the profile never finishes, and $HCIROOT comes
|
||||||
|
# back EMPTY. Instead we export HCIROOT explicitly and run a plain `sh -c` (no
|
||||||
|
# login profile, no tty needed). This is deterministic and version-agnostic.
|
||||||
|
#
|
||||||
|
# _shq STR → single-quote STR for safe embedding inside another '...' context.
|
||||||
|
_shq() { printf '%s' "$1" | sed "s/'/'\\\\''/g"; }
|
||||||
|
|
||||||
|
# _build_pinned_cmd HCIROOT RAW → a remote command string that exports HCIROOT
|
||||||
|
# explicitly (and HCISITEDIR-friendly callers can derive from it) then runs RAW
|
||||||
|
# under a NON-login `sh -c`. No `bash -lc`, so the sudo-gated profile is skipped.
|
||||||
|
_build_pinned_cmd() {
|
||||||
|
local root="$1" raw="$2"
|
||||||
|
local esc; esc=$(_shq "$raw")
|
||||||
|
printf "sh -c 'HCIROOT=%s; export HCIROOT; %s'" "$(_shq "$root")" "$esc"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _remote_cmd_for ALIAS RAW → echo the exact command string to hand to ssh.
|
||||||
|
# If ALIAS has a pinned HCIROOT → pinned (explicit-export, no login profile).
|
||||||
|
# Else → the existing login-shell wrapper (_build_login_cmd). Single chokepoint
|
||||||
|
# so cmd_exec/cmd_discover/cmd_pull_smat all honour the pin identically.
|
||||||
|
_remote_cmd_for() {
|
||||||
|
local alias="$1" raw="$2"
|
||||||
|
local pin; pin=$(read_host_hciroot "$alias")
|
||||||
|
if [ -n "$pin" ]; then
|
||||||
|
_build_pinned_cmd "$pin" "$raw"
|
||||||
|
else
|
||||||
|
_build_login_cmd "$raw"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
cmd_exec() {
|
cmd_exec() {
|
||||||
local alias="${1:-}"
|
local alias="${1:-}"
|
||||||
[ -n "$alias" ] || die "usage: exec <alias> <command...>"
|
[ -n "$alias" ] || die "usage: exec <alias> <command...>"
|
||||||
@ -291,9 +447,11 @@ cmd_exec() {
|
|||||||
if [ ! -S "$sock" ] || ! ssh -S "$sock" -O check -p "$port" "$addr" 2>/dev/null; then
|
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"
|
die "no open master for $alias — run 'setup $alias' first"
|
||||||
fi
|
fi
|
||||||
# Multiplexed; no password needed. Run in a login shell so $HCIROOT et al.
|
# Multiplexed; no password needed. If the alias has a pinned HCIROOT we export
|
||||||
# populate from the remote Cloverleaf login profile (see _build_login_cmd).
|
# it explicitly and skip the login profile (v0.8.15 sudo-gated-profile fix);
|
||||||
ssh -S "$sock" -p "$port" -o BatchMode=yes "$addr" "$(_build_login_cmd "$cmd")"
|
# 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")"
|
||||||
}
|
}
|
||||||
|
|
||||||
# cmd_discover ALIAS — proactively detect the remote Cloverleaf environment.
|
# cmd_discover ALIAS — proactively detect the remote Cloverleaf environment.
|
||||||
@ -318,29 +476,78 @@ cmd_discover() {
|
|||||||
die "no open master for $alias — run 'setup $alias' first"
|
die "no open master for $alias — run 'setup $alias' first"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# A single login-shell remote script. It:
|
# A single remote script. It:
|
||||||
# - prints HCIROOT\t$HCIROOT
|
# - prints HCIROOT\t$HCIROOT
|
||||||
# - tries `hcisitelist` (Cloverleaf site lister); each non-empty token → SITE
|
# - PRIMARY enumeration = the NetConfig walk under $HCIROOT (depth ≤2),
|
||||||
# - falls back to a NetConfig walk under $HCIROOT (depth ≤2)
|
# IDENTICAL to lib/each-site.sh: find NetConfig files → dirname → basename
|
||||||
# Kept POSIX-sh so it runs under whatever /bin/sh the login shell spawns.
|
# → sort -u. This is the version-agnostic ground truth and works on a box
|
||||||
|
# with NO `hcisitelist` (v0.8.15 portability fix — confirmed: shdclvf01q
|
||||||
|
# has no hcisitelist).
|
||||||
|
# - `hcisitelist` is used ONLY if it is actually present AND the walk found
|
||||||
|
# nothing (belt-and-suspenders), never as the dependency.
|
||||||
|
# Kept POSIX-sh so it runs under whatever /bin/sh spawns it.
|
||||||
|
#
|
||||||
|
# NOTE on environment: when the alias has a pinned HCIROOT, _remote_cmd_for
|
||||||
|
# exports HCIROOT explicitly and runs this under a NON-login `sh -c` (skips the
|
||||||
|
# sudo-gated login profile). Otherwise it runs under `bash -lc` so the login
|
||||||
|
# profile populates $HCIROOT. Either way the script below only reads
|
||||||
|
# ${HCIROOT:-}, so it is agnostic to which path delivered it.
|
||||||
|
# v0.8.15 (list-sites exclusion): drop non-real entries from the enumeration so
|
||||||
|
# /sites shows only operator-meaningful sites. Two filters, applied at the walk
|
||||||
|
# source (so REMOTE pinned, REMOTE login-shell, and LOCAL all behave the same):
|
||||||
|
# 1. SITES_EXCLUDE — static scaffolding/special dirs (helloworld, siteProto,
|
||||||
|
# master). A documented, tunable env var: Bryan can override at call time
|
||||||
|
# via `SITES_EXCLUDE='...' discover <alias>` without a config UI.
|
||||||
|
# 2. Host-name match — any site dir whose name == the remote `hostname -s` or
|
||||||
|
# full `hostname` (a dir just named after the box, e.g. shdclvf01q). The
|
||||||
|
# remote hostname is the primary signal; we ALSO pass the alias's configured
|
||||||
|
# SSH host as a secondary candidate (qa's alias host is lhsixfqa) so a dir
|
||||||
|
# matching that is dropped too.
|
||||||
|
# NOT silent: every dropped name is reported on an EXCLUDED note so the tool
|
||||||
|
# layer surfaces it. The real-site list/count stays the headline.
|
||||||
|
local sites_exclude="${SITES_EXCLUDE:-helloworld siteProto master}"
|
||||||
|
# bare host from the alias's user@host (strip optional user@); '-' if none.
|
||||||
|
local alias_host="${addr#*@}"; [ -n "$alias_host" ] || alias_host="-"
|
||||||
local remote='
|
local remote='
|
||||||
|
SITES_EXCLUDE='\'"$(_shq "$sites_exclude")"\'';
|
||||||
|
ALIAS_HOST='\'"$(_shq "$alias_host")"\'';
|
||||||
printf "HCIROOT\t%s\n" "${HCIROOT:-}";
|
printf "HCIROOT\t%s\n" "${HCIROOT:-}";
|
||||||
if [ -z "${HCIROOT:-}" ]; then
|
if [ -z "${HCIROOT:-}" ]; then
|
||||||
printf "NOTE\tHCIROOT empty even in a login shell — operator profile may not export it\n";
|
printf "NOTE\tHCIROOT is empty. If this host has a sudo-gated/non-interactive login profile, pin it: ssh-helper.sh set-hciroot <alias> <path>\n";
|
||||||
exit 0;
|
exit 0;
|
||||||
fi
|
|
||||||
got=0;
|
|
||||||
if command -v hcisitelist >/dev/null 2>&1; then
|
|
||||||
for s in $(hcisitelist 2>/dev/null); do
|
|
||||||
[ -n "$s" ] && { printf "SITE\t%s\n" "$s"; got=1; }
|
|
||||||
done;
|
|
||||||
fi;
|
fi;
|
||||||
if [ "$got" = "0" ]; then
|
if [ ! -d "${HCIROOT}" ]; then
|
||||||
find "$HCIROOT" -mindepth 1 -maxdepth 2 -name NetConfig -type f 2>/dev/null \
|
printf "NOTE\tHCIROOT=%s is not a directory on the remote — check the pinned path\n" "${HCIROOT}";
|
||||||
| while IFS= read -r nc; do d=$(dirname "$nc"); printf "SITE\t%s\n" "$(basename "$d")"; done \
|
exit 0;
|
||||||
| sort -u;
|
fi;
|
||||||
fi'
|
sites=$(find "$HCIROOT" -mindepth 1 -maxdepth 2 -name NetConfig -type f 2>/dev/null \
|
||||||
ssh -S "$sock" -p "$port" -o BatchMode=yes "$addr" "$(_build_login_cmd "$remote")"
|
| while IFS= read -r nc; do d=$(dirname "$nc"); basename "$d"; done \
|
||||||
|
| sort -u);
|
||||||
|
if [ -z "$sites" ] && command -v hcisitelist >/dev/null 2>&1; then
|
||||||
|
printf "NOTE\tNetConfig walk found no sites; falling back to hcisitelist\n";
|
||||||
|
sites=$(hcisitelist 2>/dev/null | tr " " "\n" | grep -v "^$" | sort -u);
|
||||||
|
fi;
|
||||||
|
if [ -z "$sites" ]; then
|
||||||
|
printf "NOTE\tno sites with a NetConfig found under %s\n" "$HCIROOT";
|
||||||
|
exit 0;
|
||||||
|
fi;
|
||||||
|
HN_S=$(hostname -s 2>/dev/null || true);
|
||||||
|
HN_F=$(hostname 2>/dev/null || true);
|
||||||
|
kept=""; dropped="";
|
||||||
|
for s in $sites; do
|
||||||
|
[ -n "$s" ] || continue;
|
||||||
|
drop="";
|
||||||
|
for x in $SITES_EXCLUDE; do [ "$s" = "$x" ] && drop=1 && break; done;
|
||||||
|
[ -z "$drop" ] && [ -n "$HN_S" ] && [ "$s" = "$HN_S" ] && drop=1;
|
||||||
|
[ -z "$drop" ] && [ -n "$HN_F" ] && [ "$s" = "$HN_F" ] && drop=1;
|
||||||
|
[ -z "$drop" ] && [ "$ALIAS_HOST" != "-" ] && [ "$s" = "$ALIAS_HOST" ] && drop=1;
|
||||||
|
if [ -n "$drop" ]; then dropped="$dropped $s"; else kept="$kept
|
||||||
|
$s"; fi;
|
||||||
|
done;
|
||||||
|
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.6.8: scp helpers that multiplex via the existing ControlMaster ────────
|
# ── v0.6.8: scp helpers that multiplex via the existing ControlMaster ────────
|
||||||
@ -503,7 +710,8 @@ cmd_pull_smat() {
|
|||||||
find_cmd+='printf "SMATDB_PATH:%s\n" "$F"'
|
find_cmd+='printf "SMATDB_PATH:%s\n" "$F"'
|
||||||
|
|
||||||
local _smat_raw remote_smatdb
|
local _smat_raw remote_smatdb
|
||||||
_smat_raw=$(ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" "$(_build_login_cmd "$find_cmd")" 2>&1)
|
# 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)
|
||||||
remote_smatdb=$(printf '%s\n' "$_smat_raw" | grep '^SMATDB_PATH:' | tail -1)
|
remote_smatdb=$(printf '%s\n' "$_smat_raw" | grep '^SMATDB_PATH:' | tail -1)
|
||||||
if [ -n "$remote_smatdb" ]; then
|
if [ -n "$remote_smatdb" ]; then
|
||||||
remote_smatdb="${remote_smatdb#SMATDB_PATH:}"
|
remote_smatdb="${remote_smatdb#SMATDB_PATH:}"
|
||||||
@ -556,8 +764,12 @@ cmd_pull_smat() {
|
|||||||
sample_cmd+='RETURNED=$(sqlite3 "'"$remote_smatdb"'" "SELECT MIN(1000, COUNT(*)) FROM smat_msgs WHERE Time >= $CUTOFF_MS"); '
|
sample_cmd+='RETURNED=$(sqlite3 "'"$remote_smatdb"'" "SELECT MIN(1000, COUNT(*)) FROM smat_msgs WHERE Time >= $CUTOFF_MS"); '
|
||||||
sample_cmd+='echo "# smatdb=$(basename '"$remote_smatdb"') days_back='"$days_back"' total_in_window=$TOTAL returned=$RETURNED truncated=$([ "$TOTAL" -gt 1000 ] && echo yes || echo no)" >&2'
|
sample_cmd+='echo "# smatdb=$(basename '"$remote_smatdb"') days_back='"$days_back"' total_in_window=$TOTAL returned=$RETURNED truncated=$([ "$TOTAL" -gt 1000 ] && echo yes || echo no)" >&2'
|
||||||
|
|
||||||
# Login shell so sqlite3 resolves from the operator's PATH (v0.8.13).
|
# Login shell so sqlite3 resolves from the operator's PATH (v0.8.13), unless
|
||||||
ssh -S "$_RH_SOCK" -p "$_RH_PORT" -o BatchMode=yes "$_RH_ADDR" "$(_build_login_cmd "$sample_cmd")"
|
# the alias has a pinned HCIROOT, in which case we export HCIROOT explicitly
|
||||||
|
# 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")"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
@ -565,6 +777,7 @@ case "${1:-help}" in
|
|||||||
add) shift; cmd_add "$@" ;;
|
add) shift; cmd_add "$@" ;;
|
||||||
remove|rm) shift; cmd_remove "$@" ;;
|
remove|rm) shift; cmd_remove "$@" ;;
|
||||||
pass|passwd) shift; cmd_pass "$@" ;;
|
pass|passwd) shift; cmd_pass "$@" ;;
|
||||||
|
set-hciroot|hciroot) shift; cmd_set_hciroot "$@" ;;
|
||||||
setup|open) shift; cmd_setup "$@" ;;
|
setup|open) shift; cmd_setup "$@" ;;
|
||||||
close|exit) shift; cmd_close "$@" ;;
|
close|exit) shift; cmd_close "$@" ;;
|
||||||
status) shift; cmd_status "$@" ;;
|
status) shift; cmd_status "$@" ;;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user