From 65807308d887127ccfc4b42177a9e95ff8237184 Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Thu, 28 May 2026 09:57:36 -0700 Subject: [PATCH] v0.8.18: readable terminal output (vertical entity lists + verbatim-fenced aligned tables) + cmd_push direct-mode branch + _direct_ssh_opts dedup Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 58 +++++++++++++++++++++++++++++++++ MANIFEST | 10 +++--- VERSION | 2 +- agents/larry.md | 12 +++++++ larry.sh | 43 ++++++++++++++++++++++-- lib/ssh-helper.sh | 83 +++++++++++++++++++++++++++++++++++------------ 6 files changed, 178 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4e36e..4699d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,64 @@ 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.18 — 2026-05-28 + +Readable terminal output + two DIRECT-mode follow-ups from Vera's v0.8.17 gate +(Clover). larry runs in a plain monospace terminal that does NOT render +markdown — this release makes tabular results actually line up and site lists +read vertically, and closes the deferred `ssh_push` direct-mode gap plus the +duplicated direct-ssh options. + +**1. Readable output in a monospace terminal (Bryan's primary ask).** +The on-server LLM was re-rendering tool results as markdown tables +(`| col | col |`, `|---|`) — which print raw and never align — and collapsing +the site list into a comma-joined inline sentence. The tools already emit clean +data (nc-find/nc-inbound pre-align columns with `%-*s`; `list_sites` prints one +site per line with a `sites: N (excluded: …)` headline). The fix steers the +model to PRESERVE that, and reinforces it at the tool boundary so it can't drift: + +- `agents/larry.md` — new **TERMINAL OUTPUT CONTRACT** section (5 rules): never + emit a markdown table; reproduce a tool's pre-aligned/fenced table VERBATIM in + a ```text fence; never hand-align columns (the tools do it deterministically); + render entity lists ONE PER LINE vertically (never comma-joined inline), and + keep `list_sites`'s count headline + one-per-line list as returned. +- `larry.sh` — new `_fence_aligned_table` helper wraps an already-aligned tool + table in a ```text fence with an explicit "reproduce VERBATIM; do NOT convert + to a markdown table" marker. `tool_nc_find` and `tool_nc_find_inbound` route + their `--format table` output through it (tsv/jsonl data formats pass through + unfenced). Empty / error / usage output passes through UNCHANGED (the operator + must still see errors plainly); the table bytes are never altered — only the + fence + marker lines are added. + +**2. `cmd_push` DIRECT-mode branch (closes Vera's deferred MAJOR).** +`cmd_push` called `_resolve_open_master` unconditionally, so a DIRECT-mode alias +(no ControlMaster) died "no open master" — breaking `ssh_push` (exposed tool; +used by `nc_regression` phase 4 to push cross-env input bundles, central to the +`epic_adt_in` cross-env goal). Added the symmetric direct branch mirroring +`cmd_pull`: `_direct_scp :` for the transfer, then +post-transfer size verification via `_dispatch_remote` (fresh per-command +`_run_direct`). `lib/ssh-helper.sh`. + +**3. De-duplicated the direct-ssh options (closes Vera's MINOR).** +The shared direct-mode `-o` flags (the five security-critical ones — +`PreferredAuthentications=password`, `PubkeyAuthentication=no`, +`StrictHostKeyChecking=accept-new`, `ControlMaster=no`, `ControlPath=none` — +plus `NumberOfPasswordPrompts=1` and `ConnectTimeout`) were copied in three +places (`cmd_setup` probe, `_run_direct`, `_direct_scp`). Extracted into one +`_direct_ssh_opts` helper so a security-option change can't drift across copies; +all three sites now splat `$(_direct_ssh_opts)`. ssh `-o` ordering is immaterial +(no conflicting duplicate keys) so this is byte-equivalent in BEHAVIOR — verified +the reconstructed argv matches the prior inline copies token-for-token. +`lib/ssh-helper.sh`. + +**No traffic bypass (unchanged, absolute).** DIRECT mode is legitimate +forced-password auth only — no proxy, no tunnel, no masking, and host-key +checking STAYS ON (`StrictHostKeyChecking=accept-new`). The dedup helper keeps +that posture in a single source of truth. + +`bash -n` clean on every changed file. `/sites` slash path drives clean under +`set -u`. VERSION + larry.sh:81 bumped to 0.8.18; MANIFEST regenerated. + ## v0.8.17 — 2026-05-28 Per-alias DIRECT (no-multiplex) SSH mode (Clover). The real unblock for diff --git a/MANIFEST b/MANIFEST index 26b9338..991b758 100644 --- a/MANIFEST +++ b/MANIFEST @@ -23,19 +23,19 @@ # scripts/make-manifest.sh and bump VERSION. # Top-level scripts -larry.sh 7bdbe0743d7aec58ccedadfebd766e8bbfd828d47e33e51b2bac75a4d0706f5b +larry.sh 087cc26634aa330049d46940ff6370dad2b84b267a8d4ce87b528eb8bd333d5d larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1 # Metadata -VERSION a8c64c5df539331e33b8b4b5c1534d12f6238dbbbd313e7cebbf1cff1df0fe87 +VERSION 1d14fd69d4f2d2b8118fa821e3c9a3d88f0a45cb6b262645ff643b4ae101d2b2 MANUAL.md 666128a086b59ff3c31a574aec0c5dd681666d66319da9f078451bf9013ca5e1 -CHANGELOG.md a329fda8be2e5caa33f1dfec7a5c68adc7d7d19b8449032cbfc9542a766292b6 +CHANGELOG.md 41763bdd066ed12d25a0f212378102fac4b5cfd91895a330f34e0859ae697d91 # Agent personas (system-prompt overlays) -agents/larry.md 11ea905fa7cac6fa7baeb11b2d62af07b15a666ce90cfe36491bcbc555244397 +agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 agents/clover.md d1bbfd6cc4642c2bff6e15dcbdf051d71b063b3fe29e0be97d17b3180d3c7ac5 agents/cloverleaf-cheatsheet.md c0a2aab91f1ddf092bce312def02cc6f3f62a1f653ca5af67a9430c3fcef4c3f agents/regress.md bb05ed1439b1e35d6e9799e32d683bfab166472c72115c1f02757e227c74e42f @@ -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 e9e2f33bb893d951e668d81dfe88057d235013b60cdba0e3441d1400d877a6d4 +lib/ssh-helper.sh bd205aa87bc9e53821cac45888faa9434c1e182bee2bf16d6d838dcb79bfac3e # 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 9bba175..492fd93 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.17 +0.8.18 diff --git a/agents/larry.md b/agents/larry.md index 7d84c6b..c237315 100644 --- a/agents/larry.md +++ b/agents/larry.md @@ -40,6 +40,18 @@ You have access to a small but sharp tool set: You do **not** have subagent dispatch in portable mode. You are Larry + Clover (and any other specialist you need to channel) in one head. Be honest about that limitation when it matters. +## TERMINAL OUTPUT CONTRACT (mandatory — you render to a plain monospace terminal) + +Your output is read in a **plain monospace terminal that does NOT render markdown.** Markdown tables, bold, and headings print as raw literal characters. Follow these rules every time: + +1. **NEVER emit a markdown table.** Do not write `| col | col |` rows or `|---|---|` separator lines — they print raw and the columns do NOT line up. If a tool already returned an aligned, fenced table block (the table tools do — see below), reproduce that block VERBATIM. Do not re-flow it, re-pad it, re-sort it, or convert it back into a markdown table. +2. **Tool output that is already a table → present it verbatim in a fenced code block.** Tools like `nc_find`, `nc_find_inbound`, and the table tools return columns that are ALREADY aligned with spaces by the tool, wrapped in a ```text … ``` fence. Echo that fenced block exactly as received. The alignment is deterministic and done by the tool — you must not "improve" it. Adding or removing a single space breaks the alignment in the terminal. +3. **Do not align columns yourself.** You are bad at counting monospace width; the tools do it deterministically. Your job is to pass the pre-aligned block through unchanged, then add a one-line takeaway ABOVE or BELOW the block (never inside it). +4. **Entity lists render ONE PER LINE, vertically — never comma-joined inline.** When you list sites, threads, files, processes, or any set of named things, put each on its own line (e.g. ` - ancout`). Do NOT write `ancout, ancout2, ancout3, …` on one line. The `list_sites` tool already prints sites one per line and prints a `sites: N (excluded: …)` headline — keep the count headline and the one-per-line list exactly as returned; do not collapse the list into a sentence. +5. **Keep prose minimal around data.** Lead with the answer (the count, the match), show the verbatim block, then stop. No restating every row in prose. + +These rules override any instinct to "format nicely with markdown." In this environment, plain pre-aligned text IS the nice format. + ## Working style - **Read before you write.** When pointed at a Cloverleaf root, start with `list_dir` and a targeted `grep_files` to map the lay of the land before proposing changes. diff --git a/larry.sh b/larry.sh index 4d932b7..f5f04ee 100755 --- a/larry.sh +++ b/larry.sh @@ -78,7 +78,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.8.17" +LARRY_VERSION="0.8.18" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" # ───────────────────────────────────────────────────────────────────────────── @@ -1604,6 +1604,31 @@ _lib_err_if_missing() { return 1 } +# v0.8.18: _fence_aligned_table — wrap an already-space-aligned tool table in a +# ```text fence so the on-server LLM reproduces it VERBATIM in the monospace +# terminal instead of re-rendering it as a (mis-aligned) markdown table. Reads +# the tool's stdout/stderr on STDIN; the table tools (nc-find, nc-inbound) emit +# columns padded with %-*s, so the bytes are ALREADY aligned — we only need to +# fence them and tell the model not to touch them. +# +# Pass-through guarantee: if the captured text is empty, or looks like an error / +# usage line (so there is no real table to protect), we emit it UNCHANGED — the +# model must still see error text plainly. We never alter the table bytes; the +# fence and the two marker lines are the only additions. +_fence_aligned_table() { + local body; body=$(cat) + # Empty or obvious error/diagnostic → pass through untouched. + if [ -z "$body" ] || printf '%s' "$body" \ + | grep -qiE '^(ERROR|nc-[a-z]+:|usage:|\[)' ; then + printf '%s\n' "$body" + return 0 + fi + printf '%s\n' "TABLE (monospace, pre-aligned by the tool — reproduce VERBATIM in a code block; do NOT convert to a markdown table):" + printf '%s\n' '```text' + printf '%s\n' "$body" + printf '%s\n' '```' +} + tool_nc_list_protocols() { local nc="$1" _lib_err_if_missing || return @@ -1651,7 +1676,13 @@ tool_nc_xlate_refs() { tool_nc_find_inbound() { local nc="$1" mode="${2:-all}" fmt="${3:-tsv}" _lib_err_if_missing || return - "$LARRY_LIB_DIR/nc-inbound.sh" "$nc" --mode "$mode" --format "$fmt" 2>&1 + # v0.8.18: fence the table format so the model reproduces it verbatim in the + # monospace terminal. tsv/jsonl are data formats — passed through unfenced. + if [ "$fmt" = "table" ]; then + "$LARRY_LIB_DIR/nc-inbound.sh" "$nc" --mode "$mode" --format "$fmt" 2>&1 | _fence_aligned_table + else + "$LARRY_LIB_DIR/nc-inbound.sh" "$nc" --mode "$mode" --format "$fmt" 2>&1 + fi } tool_nc_make_jump() { local nc="$1" inbound="$2" new_host="$3" jump_port="$4" @@ -1708,7 +1739,13 @@ tool_nc_find() { name|port|host|process|where|xlate|tclproc) args+=(--"$mode" "$query") ;; *) echo "ERROR: unknown nc_find mode: $mode"; return 1 ;; esac - "$LARRY_LIB_DIR/nc-find.sh" "${args[@]}" 2>&1 + # v0.8.18: fence the table format so the model reproduces it verbatim in the + # monospace terminal. tsv/jsonl are data formats — passed through unfenced. + if [ "$format" = "table" ]; then + "$LARRY_LIB_DIR/nc-find.sh" "${args[@]}" 2>&1 | _fence_aligned_table + else + "$LARRY_LIB_DIR/nc-find.sh" "${args[@]}" 2>&1 + fi } tool_nc_insert_protocol() { diff --git a/lib/ssh-helper.sh b/lib/ssh-helper.sh index 12f74b1..c5ca1f1 100755 --- a/lib/ssh-helper.sh +++ b/lib/ssh-helper.sh @@ -331,14 +331,9 @@ cmd_setup() { 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. + # v0.8.18: shared DIRECT options via _direct_ssh_opts (was an inline copy). 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" \ + $(_direct_ssh_opts) \ -p "$port" \ "$addr" 'true' 2>"$errfile" local vrc=$? @@ -580,6 +575,37 @@ _remote_cmd_for() { # _DIRECT_CONNECT_TIMEOUT — seconds for ssh ConnectTimeout (env-overridable). _DIRECT_CONNECT_TIMEOUT="${LARRY_SSH_DIRECT_TIMEOUT:-10}" +# _direct_ssh_opts → emit the shared ssh/scp `-o` option tokens for every DIRECT +# (no-ControlMaster) transport, one token per word, on STDOUT. v0.8.18: extracted +# so the security posture lives in ONE place and a change can't drift across the +# three call sites (cmd_setup probe, _run_direct, _direct_scp). The five +# security-critical flags are: +# PreferredAuthentications=password — force the password method so sshpass +# feeds the password cleanly past a banner +# PubkeyAuthentication=no — never silently fall back to a key +# StrictHostKeyChecking=accept-new — host-key checking STAYS ON (TOFU). This +# is the no-traffic-bypass guarantee: we +# never disable host verification. +# ControlMaster=no / ControlPath=none — never multiplex (the box rejects it) +# Plus two shared connection knobs identical across all three callers: +# NumberOfPasswordPrompts=1 — a stale password fails fast (one prompt) +# ConnectTimeout=$_DIRECT_CONNECT_TIMEOUT +# ssh `-o` ordering is immaterial (no conflicting duplicate keys), so emitting +# these as one contiguous block is byte-equivalent in BEHAVIOR to the prior +# inline copies. Callers splat the words unquoted: `ssh $(_direct_ssh_opts) ...`. +# Every token here is a single shell word (no spaces inside any -o value), so the +# unquoted expansion is safe. +_direct_ssh_opts() { + printf '%s\n' \ + -o "PreferredAuthentications=password" \ + -o "PubkeyAuthentication=no" \ + -o "NumberOfPasswordPrompts=1" \ + -o "StrictHostKeyChecking=accept-new" \ + -o "ControlMaster=no" \ + -o "ControlPath=none" \ + -o "ConnectTimeout=$_DIRECT_CONNECT_TIMEOUT" +} + # _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() { @@ -629,14 +655,9 @@ _run_direct() { # 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. + # v0.8.18: shared DIRECT options via _direct_ssh_opts (was an inline copy). 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" \ + $(_direct_ssh_opts) \ -p "$port" \ "$addr" "$remote_cmd" 2>"$errfile" local rc=$? @@ -671,14 +692,10 @@ _direct_scp() { 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.$$") + # v0.8.18: shared DIRECT options via _direct_ssh_opts (was an inline copy). + # scp reads the same ssh-style -o options; only the port flag differs (-P). 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" \ + $(_direct_ssh_opts) \ -P "$port" \ "$src" "$dst" 2>"$errfile" local rc=$? @@ -959,6 +976,30 @@ cmd_push() { [ -n "$alias" ] && [ -n "$local_path" ] && [ -n "$remote" ] \ || die "usage: push " [ -f "$local_path" ] || die "local file not found: $local_path" + + # v0.8.18: DIRECT mode — symmetric with cmd_pull's direct branch. No + # ControlMaster (the host rejects multiplexing); the transfer uses _direct_scp + # (fresh per-command sshpass), and the post-transfer size verification rides a + # fresh per-command connection via _dispatch_remote → _run_direct. Without this + # branch, ssh_push (an exposed tool, used by nc_regression phase 4 to push + # cross-env input bundles) died "no open master" for any DIRECT-mode alias. + 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) + local local_size; local_size=$(coerce_int "$(wc -c < "$local_path" 2>/dev/null)" 0) + if _direct_scp "$alias" "$local_path" "$_d_addr:$remote"; then + local got + got=$(coerce_int "$(_dispatch_remote "$alias" "wc -c < $(printf '%q' "$remote") 2>/dev/null" 2>/dev/null)" 0) + if [ "$got" != "$local_size" ]; then + die "partial transfer: local=$local_size bytes, remote=$got bytes ($alias:$remote)" + fi + ok "pushed $local_path → $alias:$remote ($got bytes, direct)" + return 0 + fi + return 1 + fi + _resolve_open_master "$alias" # v0.7.5: coerce_int on wc output — Cygwin wc.exe CR-taint defense.