diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1cef2..d18183e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 [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` / ` --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 Proactive both-mode Cloverleaf-env detection + the `$HCIROOT` login-shell fix + diff --git a/MANIFEST b/MANIFEST index f8888ce..8f55c6d 100644 --- a/MANIFEST +++ b/MANIFEST @@ -23,16 +23,16 @@ # scripts/make-manifest.sh and bump VERSION. # Top-level scripts -larry.sh fabb64714cc0b910805f9d2fdaf8de4a903ce546ef1b3a8923fe0bcb980b9b7e +larry.sh 2c10a738cd3fc14012b4d67fcdc58be40147593f604a3ddc66b19b6b4b0ea081 larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 install-larry.sh e97da4e12a0d8863ca18d79b12f6c4294c72fa6d4b11dffeab66504236bb4eb1 # Metadata -VERSION 414f2b8d3ed8f7c983632c4765179167948b1519b0cfc3596c68db66c9617dc3 -MANUAL.md 755d98b802cb16a5d2d207d423b12c6ca632f118ee372cb5093fe2320a6515ce -CHANGELOG.md bc695402eaafd52bb718e1852355d01a50da7cb86320df980178489de1c683fb +VERSION af0c015a6470ca542b68d7084a55652bee7798013d87487cd05fac1484a25980 +MANUAL.md 666128a086b59ff3c31a574aec0c5dd681666d66319da9f078451bf9013ca5e1 +CHANGELOG.md aa0bd56caf29a0939a7b7d676bec9daed01606f9ac29f0180c0ac72c990d49be # Agent personas (system-prompt overlays) agents/larry.md 11ea905fa7cac6fa7baeb11b2d62af07b15a666ce90cfe36491bcbc555244397 diff --git a/MANUAL.md b/MANUAL.md index a87a1d7..1e653ef 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -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 [args] # run a tool by hand (no args → its --help) +larry tools --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/.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 - `$LARRY_HOME` defaults to `~/.larry/`. Lib scripts live at `$LARRY_HOME/lib/`. diff --git a/VERSION b/VERSION index c2f73c6..832bad2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.13 +0.8.14 diff --git a/larry.sh b/larry.sh index 9f89580..243ca51 100755 --- a/larry.sh +++ b/larry.sh @@ -5,10 +5,16 @@ # Usage: # larry.sh # interactive in $PWD # 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 [args] # run a tool by hand (no API/LLM needed) # larry.sh --no-update # skip self-update # larry.sh --version # print version 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: # LARRY_HOME where to cache config/sessions (default: ~/.larry) # LARRY_BASE_URL root URL of the bundle on the server (default @@ -72,7 +78,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.8.13" +LARRY_VERSION="0.8.14" 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_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, # 2026-05-27) @@ -289,6 +303,150 @@ fetch_validate() { } # <<< 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 [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 [args]%s run a tool (no args prints its --help)\n' "$C_CYAN" "$C_RESET" + printf ' %slarry tools --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 # ───────────────────────────────────────────────────────────────────────────── @@ -4031,26 +4189,67 @@ call_api() { local _hdrs_file; _hdrs_file=$(mktemp 2>/dev/null || echo "") local _curl_args=( -sS --max-time 180 ) [ -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 # Key travels in the curl config on stdin, NOT in argv. - _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" + if [ -n "$_err_file" ]; then + _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" 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 - curl "${_curl_args[@]}" \ - "${auth_args[@]}" \ - -H "anthropic-version: 2023-06-01" \ - -H "content-type: application/json" \ - --data-binary "@$payload_file" \ - "$LARRY_API_URL" + if [ -n "$_err_file" ]; then + curl "${_curl_args[@]}" \ + "${auth_args[@]}" \ + -H "anthropic-version: 2023-06-01" \ + -H "content-type: application/json" \ + --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 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 # carry rate-limit info even on 429s. 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 rm -f "$_hdrs_file" fi @@ -4102,6 +4301,12 @@ call_api_stream() { # doesn't need to share a variable across the subshell boundary. local _hdrs_file="$LARRY_HOME/.last-stream-headers" : > "$_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 ) [ -n "$_hdrs_file" ] && _curl_args+=( -D "$_hdrs_file" ) if [ "$LARRY_AUTH_MODE" = "apikey" ]; then @@ -4111,7 +4316,7 @@ call_api_stream() { -H "content-type: application/json" \ -H "accept: text/event-stream" \ --data-binary "@$payload_file" \ - "$LARRY_API_URL" + "$LARRY_API_URL" ${_err_file:+2>"$_err_file"} else curl "${_curl_args[@]}" \ "${auth_args[@]}" \ @@ -4119,7 +4324,7 @@ call_api_stream() { -H "content-type: application/json" \ -H "accept: text/event-stream" \ --data-binary "@$payload_file" \ - "$LARRY_API_URL" + "$LARRY_API_URL" ${_err_file:+2>"$_err_file"} fi } @@ -4153,6 +4358,110 @@ build_system_prompt() { 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)" ;; + ""*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 --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 # ───────────────────────────────────────────────────────────────────────────── @@ -4746,11 +5055,28 @@ agent_turn() { rm -f "$payload_file" "$resp_file" 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." rm -f "$tools_file" "$system_file" return 1 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)") if [ -n "$err_type" ]; then # 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 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 [args] run a tool by hand (no args → its --help) + larry tools --help usage, flags, expected input/output + an example + Slash commands: /quit /exit /q exit /clear clear the terminal screen (distinct from /reset)