v0.8.14: manual-tools dispatcher (larry tools) + honest blocked-API detection (_diagnose_api_block) — zero bypass primitives

larry tools list / <name> [args] makes all 24 lib/ Cloverleaf+HL7 tools
discoverable and runnable by hand with no API/LLM; dispatches before
bootstrap/self-update/network. _diagnose_api_block recognizes a blocked
API (curl rc/stderr/body/headers, incl. Cisco Umbrella fingerprints) and
guides the operator to manual-tools mode + IT allowlisting instead of a
raw error dump. Graceful degradation + honest guidance only — NO traffic
masking/proxy-hiding/circumvention on a PHI box.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-05-28 08:31:26 -07:00
parent fe2f67a1aa
commit 6703ee154e
5 changed files with 433 additions and 20 deletions

View File

@ -4,6 +4,54 @@ 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.14 — 2026-05-28
Locked-down-box survivability (Clover): make the full toolkit usable BY HAND
with no API/LLM, and turn a blocked-API failure from a raw error dump into
honest, actionable guidance. Both deliverables are graceful degradation only —
**ZERO traffic-bypass, masking, proxy-hiding, or block-circumvention primitives
were added** (hard security line: larry must not try to defeat a corporate
security control on a PHI box).
1. **`larry tools` — manual-tools dispatcher.** A discoverable, low-friction
entry point so all 24 operator-facing Cloverleaf/HL7 tools in `lib/` are
listable and runnable by hand with no REPL, no API, and no LLM. `larry tools
list` prints every tool grouped (NetConfig read/write, diff & regression,
HL7, site-iteration/format) with a one-line description and a `(missing)`
flag for any registry entry absent on disk; `larry tools <name> [args]` runs
a tool (no args → its own `--help`); `larry tools help` explains the mode.
It dispatches at the very top of `larry.sh` — BEFORE bootstrap, self-update,
the jq gate, and any network call — so it works on a fresh install with the
API unreachable. Only requirement is a `lib/` dir next to `larry.sh` (or in
`$LARRY_HOME/lib`). Internal plumbing (oauth/phi/fetch-safe/cygwin-safe/
ssh-helper/journal/lessons/etc.) is intentionally NOT listed — those are
REPL/agent support, not operator-facing tools. The registry descriptions are
the SSOT for `tools list`.
2. **`_diagnose_api_block` — honest blocked-API detection + guidance.** On a
locked-down box where `api.anthropic.com` is blocked, larry no longer dumps a
bare network error. It inspects the last call's curl exit code, curl stderr,
the response body, and the response headers for block signatures — TLS
interception / MITM cert (`unable to get local issuer certificate`, self-
signed, `certificate verify failed`), DNS-filter / refused / timed-out / TLS-
handshake exit codes, `Could not resolve host` / `Connection refused`,
HTML-where-JSON-was-expected 403/interstitial pages, and explicit Cisco
Umbrella fingerprints in body or headers (`Server: Cisco`, `X-Cisco`,
`Cisco Umbrella`, `This site is blocked`, …). When it recognizes a block it
prints what happened, the target URL, the path into manual-tools mode
(`larry tools list` / `<name> --help` / a worked `nc-parse` example), and the
correct remedy — ask IT to allowlist the API host, or run from a permitting
network. It states plainly that this is a corporate control on a PHI box and
that larry will not, and must not, try to bypass it. Wired into both the
non-streaming and streaming API paths; reads its diagnostics from the
deterministic `$LARRY_HOME/.last-curl-*` / `.last-stream-*` files that
`call_api` persists (the call runs in a subshell, so in-memory globals don't
reach the diagnoser).
Fires before bootstrap/network; compatible with bash 3.2 / Cygwin; no
regression to the v0.8.13 paths.
---
## v0.8.13 — 2026-05-28 ## v0.8.13 — 2026-05-28
Proactive both-mode Cloverleaf-env detection + the `$HCIROOT` login-shell fix + Proactive both-mode Cloverleaf-env detection + the `$HCIROOT` login-shell fix +

View File

@ -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 fabb64714cc0b910805f9d2fdaf8de4a903ce546ef1b3a8923fe0bcb980b9b7e larry.sh 2c10a738cd3fc14012b4d67fcdc58be40147593f604a3ddc66b19b6b4b0ea081
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 414f2b8d3ed8f7c983632c4765179167948b1519b0cfc3596c68db66c9617dc3 VERSION af0c015a6470ca542b68d7084a55652bee7798013d87487cd05fac1484a25980
MANUAL.md 755d98b802cb16a5d2d207d423b12c6ca632f118ee372cb5093fe2320a6515ce MANUAL.md 666128a086b59ff3c31a574aec0c5dd681666d66319da9f078451bf9013ca5e1
CHANGELOG.md bc695402eaafd52bb718e1852355d01a50da7cb86320df980178489de1c683fb CHANGELOG.md aa0bd56caf29a0939a7b7d676bec9daed01606f9ac29f0180c0ac72c990d49be
# Agent personas (system-prompt overlays) # Agent personas (system-prompt overlays)
agents/larry.md 11ea905fa7cac6fa7baeb11b2d62af07b15a666ce90cfe36491bcbc555244397 agents/larry.md 11ea905fa7cac6fa7baeb11b2d62af07b15a666ce90cfe36491bcbc555244397

View File

@ -6,6 +6,40 @@ This page documents every command with copy-paste examples. Print it.
--- ---
## The front door: `larry tools` (v0.8.14)
You don't have to memorize the `lib/` paths. The `larry` command itself is the discoverable entry point for manual-tools mode — it works with **no REPL, no API, no LLM**, so it's the lifeline on a locked-down box where the model API is blocked:
```bash
larry tools list # every tool + a one-line description, grouped
larry tools <name> [args] # run a tool by hand (no args → its --help)
larry tools <name> --help # usage, flags, expected input/output + example
larry tools help # what manual-tools mode is
# Examples — the name is the script minus ".sh":
larry tools nc-parse list-protocols "$HCISITEDIR/NetConfig"
larry tools hl7-field PID.3 /tmp/sample.hl7
larry tools nc-status sites
```
`larry tools …` runs **before** any self-update or network call, so a fresh install with the API blocked still gives you the full toolkit. Equivalent to calling `lib/<name>.sh …` directly (documented below) — use whichever you prefer.
### When the API is blocked
If you launch the interactive REPL on a box where corporate security blocks `api.anthropic.com` (e.g. Cisco Umbrella returns a 403, TLS inspection trips `unable to get local issuer certificate`, or egress is refused), Larry **detects the block and guides you into manual-tools mode** instead of dumping a raw curl error:
```
Can't reach the model API — looks like a corporate network block.
What happened: TLS interception (an untrusted/MITM certificate ...).
The Cloverleaf tools still work — run them by hand (no API/LLM needed):
larry tools list
To use the AI brain: ask IT to allowlist api.anthropic.com, or run from a network that permits it.
```
This is graceful degradation and honest guidance only. Larry will **not** try to bypass, mask, proxy around, or otherwise circumvent a corporate security control on a PHI box — that is off the table by design. The fix is to get the endpoint allowlisted by IT, or run from a permitted network.
---
## Conventions ## Conventions
- `$LARRY_HOME` defaults to `~/.larry/`. Lib scripts live at `$LARRY_HOME/lib/`. - `$LARRY_HOME` defaults to `~/.larry/`. Lib scripts live at `$LARRY_HOME/lib/`.

View File

@ -1 +1 @@
0.8.13 0.8.14

361
larry.sh
View File

@ -5,10 +5,16 @@
# Usage: # Usage:
# larry.sh # interactive in $PWD # larry.sh # interactive in $PWD
# larry.sh /path/to/cloverleaf/root # interactive, cd into that path first # larry.sh /path/to/cloverleaf/root # interactive, cd into that path first
# larry.sh tools list # list the manual Cloverleaf/HL7 tools
# larry.sh tools <name> [args] # run a tool by hand (no API/LLM needed)
# larry.sh --no-update # skip self-update # larry.sh --no-update # skip self-update
# larry.sh --version # print version and exit # larry.sh --version # print version and exit
# larry.sh --help # print help and exit # larry.sh --help # print help and exit
# #
# Manual-tools mode (v0.8.14): `larry tools …` runs the lib/ toolkit standalone,
# with NO REPL, NO API and NO self-update — the operator's lifeline on a
# locked-down box where the model API is blocked. See `larry tools help`.
#
# Env vars: # Env vars:
# LARRY_HOME where to cache config/sessions (default: ~/.larry) # LARRY_HOME where to cache config/sessions (default: ~/.larry)
# LARRY_BASE_URL root URL of the bundle on the server (default # LARRY_BASE_URL root URL of the bundle on the server (default
@ -72,7 +78,7 @@ set -o pipefail
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Config # Config
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.8.13" LARRY_VERSION="0.8.14"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -138,6 +144,14 @@ LARRY_MAX_TOKENS="${LARRY_MAX_TOKENS:-8192}"
LARRY_API_URL="${LARRY_API_URL:-https://api.anthropic.com/v1/messages}" LARRY_API_URL="${LARRY_API_URL:-https://api.anthropic.com/v1/messages}"
LARRY_NO_UPDATE="${LARRY_NO_UPDATE:-0}" LARRY_NO_UPDATE="${LARRY_NO_UPDATE:-0}"
# v0.8.14: last-call diagnostics for API-block detection (graceful degradation
# into manual-tools mode on locked-down boxes). Set by call_api after each
# request; read by _diagnose_api_block. NOTHING here circumvents a block — it
# only RECOGNIZES one and guides the operator to run the tools manually.
LARRY_LAST_CURL_RC=""
LARRY_LAST_CURL_STDERR=""
LARRY_LAST_RESP_HEADERS=""
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# v0.8.10: API key is the DEFAULT / primary auth rail (Bryan's decision, # v0.8.10: API key is the DEFAULT / primary auth rail (Bryan's decision,
# 2026-05-27) # 2026-05-27)
@ -289,6 +303,150 @@ fetch_validate() {
} }
# <<< fetch-safe inline <<< # <<< fetch-safe inline <<<
# ─────────────────────────────────────────────────────────────────────────────
# `larry tools` — manual-tools mode (v0.8.14)
#
# A discoverable, low-friction entry point for the lib/ toolkit so a human can
# run any Cloverleaf/HL7 tool BY HAND — no REPL, no API, no LLM. This is the
# operator's lifeline on a locked-down box where the Anthropic API is blocked.
#
# larry tools list — every manual tool + a one-line description
# larry tools <name> [args] — run a tool (no args → its --help/usage)
# larry tools help — this help
#
# It runs HERE, before bootstrap / self-update / jq-check / any network call,
# so it works on a fresh install with the API unreachable. Only requirement:
# the lib/ directory next to larry.sh (or in $LARRY_HOME/lib).
# ─────────────────────────────────────────────────────────────────────────────
# Resolve the lib/ dir using ONLY what's defined this early (no $LARRY_LIB_DIR
# yet — that's set ~1000 lines down, after self-update). Mirrors the later
# _resolve_lib_dir but standalone-safe.
_tools_resolve_lib_dir() {
local self_dir; self_dir=$(cd "$(dirname "$0")" 2>/dev/null && pwd)
local candidate
for candidate in "$self_dir/lib" "$LARRY_HOME/lib"; do
[ -d "$candidate" ] && [ -f "$candidate/nc-parse.sh" ] && { printf '%s' "$candidate"; return 0; }
done
return 1
}
# Registry of the human-runnable manual tools, grouped. Each entry is
# "file.sh|one-line description". Internal plumbing (oauth.sh, phi-*.sh,
# fetch-safe.sh, cygwin-safe.sh, hl7-schema.sh, headers-sync.sh, journal.sh,
# lessons.sh, ssh-helper.sh, each.sh) is intentionally NOT listed — those are
# REPL/agent support, not operator-facing manual tools. Descriptions are the
# SSOT for `tools list`; keep them in sync when a tool's purpose changes.
_tools_registry() {
cat <<'REG'
#NetConfig (read)
nc-parse.sh|Parse a NetConfig: list/inspect protocols & processes, fields, routes, xlate refs, thread chains
nc-find.sh|Cross-site search for threads/protocols by name/host/port/xlate across every site under $HCIROOT
nc-inbound.sh|List the inbound (server/listener) threads in a NetConfig
nc-status.sh|Engine runtime status (sites/threads/not-up/queued/connections) — wraps the shipped tstat binaries
nc-engine.sh|Engine process control (start/stop/cycle/status) — wraps the shipped hcienginerun binaries
nc-xlate.sh|Visualize and explore a Cloverleaf xlate (.xlt) file — the TCL nested mapping tree
nc-table.sh|Read and modify Cloverleaf lookup tables (.tbl) — every write is backed up and auditable
#NetConfig (write)
nc-create-thread.sh|High-level: create a new thread in a NetConfig (and optionally wire its route)
nc-insert-protocol.sh|Low-level write side: insert/replace a protocol block in a NetConfig
nc-make-jump.sh|Generate the 3-thread "jump" pattern for cross-environment data replay
nc-tclgen.sh|Generate annotated TCL UPOC scaffolding (skeletons for common Cloverleaf proc patterns)
nc-document.sh|Generate a markdown knowledge entry documenting a Cloverleaf subsystem/interface
#Diff & regression
nc-diff-interface.sh|Diff one Cloverleaf interface across two environments
nc-smat-diff.sh|Diff smat (message-archive) content across two environments
nc-regression.sh|End-to-end regression test orchestrator between two Cloverleaf environments
nc-msgs.sh|Native smat query: search/inspect archived messages without hcidbdump
#HL7
hl7-field.sh|Extract a field by path (PID.3, MSH.10, PV1-3-4 …) from an HL7 v2 message
hl7-diff.sh|HL7-aware diff with field-level normalization between two messages
len2nl.sh|Convert length-prefixed / MLLP-framed HL7 to newline-readable form
hl7-sanitize.sh|Tokenize PHI fields in HL7 v2 messages ([[CATEGORY_NNNN]] tokens)
hl7-desanitize.sh|Reverse hl7-sanitize: restore original values from a token map
#Site iteration & format
each-site.sh|Run a command once per site under $HCIROOT (exposes $HCISITE / $HCISITEDIR)
csv-to-table.sh|Convert a 2-column CSV into Cloverleaf .tbl format
table-to-csv.sh|Convert a Cloverleaf .tbl file to CSV
REG
}
_tools_list() {
local lib; lib=$(_tools_resolve_lib_dir || echo "")
printf '%sLarry manual tools%s — run any of these by hand (no API/LLM needed):\n\n' "$C_BOLD" "$C_RESET"
printf ' %slarry tools <name> [args]%s run a tool (no args prints its --help)\n' "$C_CYAN" "$C_RESET"
printf ' %slarry tools <name> --help%s usage, flags, expected input/output + an example\n\n' "$C_CYAN" "$C_RESET"
local group="" name desc line
while IFS= read -r line; do
case "$line" in
'#'*) group="${line#\#}"; printf '%s%s%s\n' "$C_BOLD" "$group" "$C_RESET" ;;
*)
name="${line%%|*}"; desc="${line#*|}"
# Flag a tool the registry lists but that isn't present on disk.
local mark="" base="${name%.sh}"
if [ -n "$lib" ] && [ ! -f "$lib/$name" ]; then mark=" ${C_YELLOW}(missing)${C_RESET}"; fi
printf ' %s%-22s%s %s%s\n' "$C_GREEN" "$base" "$C_RESET" "$desc" "$mark"
;;
esac
done < <(_tools_registry)
printf '\n'
if [ -z "$lib" ]; then
printf '%swarn:%s lib/ not found next to larry.sh or in %s/lib — the tools are not installed here.\n' "$C_YELLOW" "$C_RESET" "$LARRY_HOME"
printf ' Run install-larry.sh, or scp the larry-anywhere/lib/ directory next to larry.sh.\n'
else
printf '%sTip:%s the name is the script minus ".sh" (e.g. %slarry tools nc-parse list-protocols /path/NetConfig%s).\n' "$C_DIM" "$C_RESET" "$C_CYAN" "$C_RESET"
fi
}
# Map a user-typed tool name to a lib file. Accepts "nc-parse" or "nc-parse.sh".
_tools_resolve_name() {
local want="$1"
[ -z "$want" ] && return 1
case "$want" in *.sh) : ;; *) want="$want.sh" ;; esac
printf '%s' "$want"
}
larry_tools_main() {
local sub="${1:-list}"; shift 2>/dev/null || true
case "$sub" in
''|list|ls)
_tools_list; return 0 ;;
help|-h|--help)
sed -n '/^# `larry tools` — manual-tools mode/,/^# ───*$/p' "$0" | sed 's/^# \{0,1\}//'
printf '\n'
_tools_list
return 0 ;;
*)
local lib; lib=$(_tools_resolve_lib_dir || echo "")
if [ -z "$lib" ]; then
err "lib/ tools not found. Looked next to larry.sh and in $LARRY_HOME/lib."
err "Run install-larry.sh, or scp the larry-anywhere/lib/ directory next to larry.sh."
return 1
fi
local file; file=$(_tools_resolve_name "$sub")
if [ ! -f "$lib/$file" ]; then
err "no such tool: $sub"
err "run 'larry tools list' to see available tools."
return 2
fi
# No args → show the tool's own --help so a human can learn it without
# reading source. Otherwise pass args straight through.
if [ "$#" -eq 0 ]; then
bash "$lib/$file" --help 2>&1 || bash "$lib/$file" help 2>&1 || true
printf '\n%s(no args given — showed --help. Re-run with arguments to execute.)%s\n' "$C_DIM" "$C_RESET" >&2
return 0
fi
exec bash "$lib/$file" "$@"
;;
esac
}
if [ "${1:-}" = "tools" ]; then
shift
larry_tools_main "$@"
exit $?
fi
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# CLI args # CLI args
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -4031,26 +4189,67 @@ call_api() {
local _hdrs_file; _hdrs_file=$(mktemp 2>/dev/null || echo "") local _hdrs_file; _hdrs_file=$(mktemp 2>/dev/null || echo "")
local _curl_args=( -sS --max-time 180 ) local _curl_args=( -sS --max-time 180 )
[ -n "$_hdrs_file" ] && _curl_args+=( -D "$_hdrs_file" ) [ -n "$_hdrs_file" ] && _curl_args+=( -D "$_hdrs_file" )
# v0.8.14: capture curl's STDERR (cert/DNS/connect-refused diagnostics) so the
# caller's block-detection (_diagnose_api_block) can tell a corporate block
# apart from a transient network blip. Body still goes to stdout untouched.
local _err_file; _err_file=$(mktemp 2>/dev/null || echo "")
if [ "$LARRY_AUTH_MODE" = "apikey" ]; then if [ "$LARRY_AUTH_MODE" = "apikey" ]; then
# Key travels in the curl config on stdin, NOT in argv. # Key travels in the curl config on stdin, NOT in argv.
_curl_config_apikey | curl "${_curl_args[@]}" --config - \ if [ -n "$_err_file" ]; then
"${auth_args[@]}" \ _curl_config_apikey | curl "${_curl_args[@]}" --config - \
-H "anthropic-version: 2023-06-01" \ "${auth_args[@]}" \
-H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \
--data-binary "@$payload_file" \ -H "content-type: application/json" \
"$LARRY_API_URL" --data-binary "@$payload_file" \
"$LARRY_API_URL" 2>"$_err_file"
else
_curl_config_apikey | curl "${_curl_args[@]}" --config - \
"${auth_args[@]}" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data-binary "@$payload_file" \
"$LARRY_API_URL"
fi
else else
curl "${_curl_args[@]}" \ if [ -n "$_err_file" ]; then
"${auth_args[@]}" \ curl "${_curl_args[@]}" \
-H "anthropic-version: 2023-06-01" \ "${auth_args[@]}" \
-H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \
--data-binary "@$payload_file" \ -H "content-type: application/json" \
"$LARRY_API_URL" --data-binary "@$payload_file" \
"$LARRY_API_URL" 2>"$_err_file"
else
curl "${_curl_args[@]}" \
"${auth_args[@]}" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data-binary "@$payload_file" \
"$LARRY_API_URL"
fi
fi fi
local _curl_rc=$? local _curl_rc=$?
# Stash rc + the response-header dump + curl's stderr for block-detection.
# call_api is usually invoked as `resp=$(call_api ...)` — a command-sub
# SUBSHELL — so in-memory global assignments do NOT reach the parent. We ALSO
# persist to deterministic files so _diagnose_api_block (running in the
# parent) can read them. (Same subshell-survival pattern as the stream path.)
LARRY_LAST_CURL_RC="$_curl_rc"
printf '%s' "$_curl_rc" > "$LARRY_HOME/.last-curl-rc" 2>/dev/null || true
LARRY_LAST_CURL_STDERR=""
: > "$LARRY_HOME/.last-curl-stderr" 2>/dev/null || true
if [ -n "$_err_file" ]; then
LARRY_LAST_CURL_STDERR=$(cat "$_err_file" 2>/dev/null)
cp "$_err_file" "$LARRY_HOME/.last-curl-stderr" 2>/dev/null || true
# Still surface curl's own diagnostic on stderr (preserves prior -sS UX).
[ -s "$_err_file" ] && cat "$_err_file" >&2
rm -f "$_err_file"
fi
LARRY_LAST_RESP_HEADERS=""
# Parse headers regardless of whether the body parse will succeed; headers # Parse headers regardless of whether the body parse will succeed; headers
# carry rate-limit info even on 429s. # carry rate-limit info even on 429s.
if [ -n "$_hdrs_file" ] && [ -s "$_hdrs_file" ]; then if [ -n "$_hdrs_file" ] && [ -s "$_hdrs_file" ]; then
LARRY_LAST_RESP_HEADERS=$(cat "$_hdrs_file" 2>/dev/null)
cp "$_hdrs_file" "$LARRY_HOME/.last-curl-headers" 2>/dev/null || true
_parse_response_headers "$_hdrs_file" 2>/dev/null || true _parse_response_headers "$_hdrs_file" 2>/dev/null || true
rm -f "$_hdrs_file" rm -f "$_hdrs_file"
fi fi
@ -4102,6 +4301,12 @@ call_api_stream() {
# doesn't need to share a variable across the subshell boundary. # doesn't need to share a variable across the subshell boundary.
local _hdrs_file="$LARRY_HOME/.last-stream-headers" local _hdrs_file="$LARRY_HOME/.last-stream-headers"
: > "$_hdrs_file" 2>/dev/null || _hdrs_file="" : > "$_hdrs_file" 2>/dev/null || _hdrs_file=""
# v0.8.14: persist curl's stderr to a deterministic path so the parent shell
# can run block-detection after the subshell pipe exits (same pattern as the
# header file above — the streaming curl runs in a subshell and can't set
# parent globals directly).
local _err_file="$LARRY_HOME/.last-stream-curlerr"
: > "$_err_file" 2>/dev/null || _err_file=""
local _curl_args=( -sN --max-time 300 ) local _curl_args=( -sN --max-time 300 )
[ -n "$_hdrs_file" ] && _curl_args+=( -D "$_hdrs_file" ) [ -n "$_hdrs_file" ] && _curl_args+=( -D "$_hdrs_file" )
if [ "$LARRY_AUTH_MODE" = "apikey" ]; then if [ "$LARRY_AUTH_MODE" = "apikey" ]; then
@ -4111,7 +4316,7 @@ call_api_stream() {
-H "content-type: application/json" \ -H "content-type: application/json" \
-H "accept: text/event-stream" \ -H "accept: text/event-stream" \
--data-binary "@$payload_file" \ --data-binary "@$payload_file" \
"$LARRY_API_URL" "$LARRY_API_URL" ${_err_file:+2>"$_err_file"}
else else
curl "${_curl_args[@]}" \ curl "${_curl_args[@]}" \
"${auth_args[@]}" \ "${auth_args[@]}" \
@ -4119,7 +4324,7 @@ call_api_stream() {
-H "content-type: application/json" \ -H "content-type: application/json" \
-H "accept: text/event-stream" \ -H "accept: text/event-stream" \
--data-binary "@$payload_file" \ --data-binary "@$payload_file" \
"$LARRY_API_URL" "$LARRY_API_URL" ${_err_file:+2>"$_err_file"}
fi fi
} }
@ -4153,6 +4358,110 @@ build_system_prompt() {
printf '%s' "$sys" printf '%s' "$sys"
} }
# ─────────────────────────────────────────────────────────────────────────────
# v0.8.14: API-block detection → guide into manual-tools mode (NO bypass).
#
# On a locked-down Cloverleaf/PHI box, corporate security (e.g. Cisco Umbrella)
# blocks api.anthropic.com — Bryan's Gundersen environment returns a 403
# interstitial, or TLS inspection trips "unable to get local issuer
# certificate", or the egress is simply refused. When that happens we must NOT
# dump a raw curl error; we DETECT the situation and GUIDE the operator to run
# the toolkit by hand.
#
# IMPORTANT — this is graceful degradation + honest guidance ONLY. There is
# DELIBERATELY no traffic-masking, no proxy-hiding, no obfuscation, no
# block-circumvention of any kind. Bypassing a corporate security control on a
# PHI box is off the table. We recognize the block and tell the truth.
#
# _diagnose_api_block BODY — inspect the last call's curl rc + stderr + the
# response body/headers for block signatures. If it looks like a block (vs a
# transient blip), print the guidance to STDERR and return 0. Else return 1.
_diagnose_api_block() {
local body="${1:-}"
local rc="${LARRY_LAST_CURL_RC:-}"
local cerr="${LARRY_LAST_CURL_STDERR:-}"
local hdrs="${LARRY_LAST_RESP_HEADERS:-}"
# call_api / call_api_stream both run in subshells (a command-sub, or the LHS
# of a pipe), so their in-memory globals don't reach us here. Both persist
# their diagnostics to deterministic files; read those as the source of truth.
if [ -z "$rc" ] && [ -f "$LARRY_HOME/.last-curl-rc" ]; then
rc=$(cat "$LARRY_HOME/.last-curl-rc" 2>/dev/null)
fi
if [ -z "$cerr" ]; then
if [ -f "$LARRY_HOME/.last-curl-stderr" ]; then
cerr=$(cat "$LARRY_HOME/.last-curl-stderr" 2>/dev/null)
elif [ -f "$LARRY_HOME/.last-stream-curlerr" ]; then
cerr=$(cat "$LARRY_HOME/.last-stream-curlerr" 2>/dev/null)
fi
fi
if [ -z "$hdrs" ]; then
if [ -f "$LARRY_HOME/.last-curl-headers" ]; then
hdrs=$(cat "$LARRY_HOME/.last-curl-headers" 2>/dev/null)
elif [ -f "$LARRY_HOME/.last-stream-headers" ]; then
hdrs=$(cat "$LARRY_HOME/.last-stream-headers" 2>/dev/null)
fi
fi
local reason=""
# 1) TLS interception (corporate MITM proxy presents an untrusted cert).
case "$cerr" in
*"unable to get local issuer certificate"*|*"self signed certificate"*|*"self-signed certificate"*|*"certificate verify failed"*|*"SSL certificate problem"*)
reason="TLS interception (an untrusted/MITM certificate on the egress path — typical of a corporate proxy doing SSL inspection)" ;;
esac
# 2) curl exit codes that mean "couldn't get there".
if [ -z "$reason" ]; then
case "$rc" in
6) reason="DNS resolution failed (the host name didn't resolve — often a security DNS filter like Umbrella)" ;;
7) reason="connection refused/blocked (egress to the API host is being denied)" ;;
28) reason="the request timed out with no response (egress may be silently dropped)" ;;
35|53|54|58|59|60|66|77|80|82|83|91) reason="a TLS/SSL handshake failure (consistent with a proxy intercepting HTTPS)" ;;
esac
fi
# 3) curl stderr text signatures (when rc didn't already name it).
if [ -z "$reason" ]; then
case "$cerr" in
*"Could not resolve host"*|*"Couldn't resolve host"*) reason="DNS resolution failed (the API host name didn't resolve)" ;;
*"Connection refused"*|*"Failed to connect"*|*"Connection timed out"*) reason="egress to the API host is being blocked/refused" ;;
*[Uu]mbrella*|*"Cisco"*) reason="a Cisco Umbrella block (the corporate web-security gateway intercepted the request)" ;;
esac
fi
# 4) Block-page BODY signatures: a 403/interstitial HTML page where we expected
# JSON. The API only ever returns JSON, so HTML here means something on the
# path answered instead (a proxy/block page).
if [ -z "$reason" ] && [ -n "$body" ]; then
case "$body" in
*[Uu]mbrella*|*"Cisco Umbrella"*|*"This site is blocked"*|*"blocked by"*|*"access has been blocked"*|*"web policy"*|*"content filter"*)
reason="the egress returned a block/interstitial page instead of the API (a corporate web filter)" ;;
"<!DOCTYPE"*|"<!doctype"*|"<html"*|"<HTML"*|*"<title>"*403*|*"403 Forbidden"*|*"Access Denied"*)
# HTML where JSON was expected — only treat as a block if it isn't our
# API's JSON error shape (which _humanize handles). HTML => not the API.
reason="the egress returned an HTML page (likely a 403/proxy block) instead of the API's JSON" ;;
esac
fi
# 5) Response-header signatures (proxy fingerprints).
if [ -z "$reason" ] && [ -n "$hdrs" ]; then
case "$hdrs" in
*[Uu]mbrella*|*"Server: Cisco"*|*"X-Cisco"*) reason="a Cisco Umbrella / corporate-proxy response (per the response headers)" ;;
esac
fi
[ -z "$reason" ] && return 1
# Emit the guidance. Honest, actionable, zero-bypass.
printf '\n%sCan'\''t reach the model API — looks like a corporate network block.%s\n' "$C_YELLOW$C_BOLD" "$C_RESET" >&2
printf '%sWhat happened:%s %s.\n' "$C_DIM" "$C_RESET" "$reason" >&2
printf ' Target: %s\n' "$LARRY_API_URL" >&2
printf '\n' >&2
printf '%sThe Cloverleaf tools still work — run them by hand (no API/LLM needed):%s\n' "$C_BOLD" "$C_RESET" >&2
printf ' %slarry tools list%s see every tool + what it does\n' "$C_CYAN" "$C_RESET" >&2
printf ' %slarry tools <name> --help%s usage for one tool\n' "$C_CYAN" "$C_RESET" >&2
printf ' %slarry tools nc-parse list-protocols /path/to/NetConfig%s (example)\n' "$C_CYAN" "$C_RESET" >&2
printf '\n' >&2
printf '%sTo use the AI brain%s: ask IT to allowlist %s, or run larry from a network that permits it.\n' "$C_BOLD" "$C_RESET" "$LARRY_API_URL" >&2
printf '%s(This is a corporate security control on a PHI box — larry will not, and must not, try to bypass it.)%s\n\n' "$C_DIM" "$C_RESET" >&2
return 0
}
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Agent turn — loop until stop_reason != tool_use # Agent turn — loop until stop_reason != tool_use
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -4746,11 +5055,28 @@ agent_turn() {
rm -f "$payload_file" "$resp_file" rm -f "$payload_file" "$resp_file"
if [ -z "$resp" ]; then if [ -z "$resp" ]; then
# v0.8.14: empty body usually means curl never got a response (cert/DNS/
# connect failure). If that smells like a corporate block, GUIDE into
# manual-tools mode instead of dumping a bare network error.
if _diagnose_api_block ""; then
rm -f "$tools_file" "$system_file"
return 1
fi
err "Network error: empty response from $LARRY_API_URL (timeout, DNS, or connection reset). Check connectivity." err "Network error: empty response from $LARRY_API_URL (timeout, DNS, or connection reset). Check connectivity."
rm -f "$tools_file" "$system_file" rm -f "$tools_file" "$system_file"
return 1 return 1
fi fi
# v0.8.14: a non-empty body that isn't our API's JSON (e.g. a 403 block page
# or proxy interstitial in HTML) means something on the egress answered
# instead of the API. Detect → guide into manual-tools mode (no bypass).
if ! printf '%s' "$resp" | jq -e . >/dev/null 2>&1; then
if _diagnose_api_block "$resp"; then
rm -f "$tools_file" "$system_file"
return 1
fi
fi
local err_type; err_type=$(strip_cr "$(printf '%s' "$resp" | jq -r '.error.type // empty' 2>/dev/null)") local err_type; err_type=$(strip_cr "$(printf '%s' "$resp" | jq -r '.error.type // empty' 2>/dev/null)")
if [ -n "$err_type" ]; then if [ -n "$err_type" ]; then
# v0.8.5: on a rate_limit/overloaded error, retry with backoff (honoring # v0.8.5: on a rate_limit/overloaded error, retry with backoff (honoring
@ -5037,6 +5363,11 @@ ${C_BOLD}Larry-Anywhere v$LARRY_VERSION${C_RESET}
Session: $SESSION_ID Session: $SESSION_ID
Log: $LOG_FILE Log: $LOG_FILE
Manual tools (work even when the API is blocked — run from the shell, not here):
larry tools list list every Cloverleaf/HL7 tool + a one-line desc
larry tools <name> [args] run a tool by hand (no args → its --help)
larry tools <name> --help usage, flags, expected input/output + an example
Slash commands: Slash commands:
/quit /exit /q exit /quit /exit /q exit
/clear clear the terminal screen (distinct from /reset) /clear clear the terminal screen (distinct from /reset)