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 <noreply@anthropic.com>
This commit is contained in:
parent
d55e222341
commit
65807308d8
58
CHANGELOG.md
58
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 <alias> <local> <addr>:<remote>` 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
|
||||
|
||||
10
MANIFEST
10
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
|
||||
|
||||
@ -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.
|
||||
|
||||
43
larry.sh
43
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() {
|
||||
|
||||
@ -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 <alias> <local_path> <remote_path>"
|
||||
[ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user