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:
parent
fe2f67a1aa
commit
6703ee154e
48
CHANGELOG.md
48
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 <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
|
||||
|
||||
Proactive both-mode Cloverleaf-env detection + the `$HCIROOT` login-shell fix +
|
||||
|
||||
8
MANIFEST
8
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
|
||||
|
||||
34
MANUAL.md
34
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 <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
|
||||
|
||||
- `$LARRY_HOME` defaults to `~/.larry/`. Lib scripts live at `$LARRY_HOME/lib/`.
|
||||
|
||||
337
larry.sh
337
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 <name> [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 <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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@ -4031,14 +4189,35 @@ 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.
|
||||
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
|
||||
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[@]}" \
|
||||
@ -4047,10 +4226,30 @@ call_api() {
|
||||
--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)" ;;
|
||||
"<!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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@ -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 <name> [args] run a tool by hand (no args → its --help)
|
||||
larry tools <name> --help usage, flags, expected input/output + an example
|
||||
|
||||
Slash commands:
|
||||
/quit /exit /q exit
|
||||
/clear clear the terminal screen (distinct from /reset)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user