Adds explicit upstream_proxy / ca_bundle configuration, an additive CA
bundle loader in shared/platform/cabundle, and tightens the SSRF guard
for proxied and self-hosted upstreams. Documents both in the technical
configuration and security pages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rs9RexUQkL3USsbdMVTpy8
Fix shared listener lifetime so a proxy keeps running across wrapped/native sessions and dead local routes are not exported. Remove the default upstream timeout, stop replaying ambiguous provider failures, preserve Content-Encoding on encoded requests, and flush streaming responses based on wire content-type instead of JSON-only flags. Adds regression tests covering listener lifetime, retry safety, and streaming/encoding edge cases.
- cache original base URLs per model, not per provider, so a same-provider
model with its own endpoint stays direct (#973)
- proxy publishes its named compat mounts in the run-state file; the pi
extension routes a custom provider through /w/pi/compat/<name> only when a
mount with that name and a matching host is published (#952, #970)
- hostOf compares host:port so loopback relays on different ports differ
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2f7Lf7G9erA7YNn5SXPEj
* fix(proxy): send x-api-key on compat /v1/messages
A named compat mount mapped every credential to Authorization: Bearer.
The Anthropic client in Pi sends x-api-key to /v1/messages. OpenCode Go
rejects a Bearer header on that path with 401 "Missing API key.". Thus
the opencode-go anthropic-messages models failed through the proxy.
The named adapter now maps the credential by the wire protocol of the
path. A request to /compat/<name>/v1/messages gets x-api-key and a
default anthropic-version. Every other path keeps Bearer. The gateway
env fallback puts the key in the header that the adapter emitted, so
the upstream never gets two credentials.
New tests cover the header mapping at the adapter, the gateway fallback,
and the full standalone handler with the /w/pi prefix. A test also
shows that a user compat.opencode-go entry replaces the built-in
upstream on the adapter path. The docs describe the header rule.
Fixes#967Fixes#968
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Zsolt Gaspar <17578237+zsltg@users.noreply.github.com>
* docs(proxy): add the compat header rule to AGENTS.md
proxy/AGENTS.md carries the same auth bullet as proxy/CLAUDE.md. This
commit adds the same header rule to both files.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Zsolt Gaspar <17578237+zsltg@users.noreply.github.com>
* fix(proxy): keep a real bearer on compat /v1/messages
Review of #969 found two risks. The Anthropic-path remap relabeled a
real inbound Bearer token as x-api-key on every named compat mount.
This broke the rule that an inbound credential keeps its auth scheme.
The remap now skips a real Bearer token. The gateway placeholder is not
a real credential, so the remap still moves it to x-api-key, and the
env fallback replaces it in that header.
The fallback test for x-api-key used a direct map lookup that depends
on canonical header keys. It now uses Header.Get. The openai and
openai_compatible fallback cases are separate, so each guard list
names only the headers that its adapter can emit.
New tests show that a real Bearer stays Bearer on /v1/messages at the
adapter, in the gateway fallback, and through the standalone handler.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Zsolt Gaspar <17578237+zsltg@users.noreply.github.com>
---------
Signed-off-by: Zsolt Gaspar <17578237+zsltg@users.noreply.github.com>
Co-authored-by: Zsolt Gaspar <17578237+zsltg@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
A foreign listener on 127.0.0.1:8787 used to get the banner but still
receive the routed launch, which would send the operator's provider
keys to a process caveman does not own (#945). Treat it like a gate
mismatch and fall back to a direct launch.
Also: Claude Code refuses Remote Control behind any non-first-party
ANTHROPIC_BASE_URL, so `caveman claude remote-control` launches direct
and the limitation is documented (#947); caveman-compress reads the
first text block instead of content[0] so a leading tool_use block no
longer crashes after a paid call (closes#932's report); hook checksum
manifest regenerated after #794.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A bare proxy serves whichever client connects, so a machine-level MCP
install marker can never bind to one request. Read the namespaced
caveman retrieve tool out of the request's own tool list instead, so
subscription and OAuth sessions on a standalone proxy reach the live
zone when their agent carries the tool (#908).
Cache-breakpoint planning and the provider-native cache optimizers now
default on in optimization modes; explicit off and unknown values still
fail closed, and record mode never runs them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The proxy now registers /compat/opencode-go/ with a default upstream
and the OPENCODE_API_KEY credential variable. Add the mount and the
user override to the provider docs. Add the variable to the
credential list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Zsolt Gaspar <17578237+zsltg@users.noreply.github.com>
Mode was applied per session but stored in one file per machine
($CLAUDE_CONFIG_DIR/.caveman-active). Four bugs followed from that one
fact: parallel windows shared a mode; deactivation was spelled "no flag
file", so SessionStart re-derived the configured default and undid an
explicit "stop caveman" after every auto-compaction; the statusline badge
rendered whichever window wrote last; and "off" could not survive a
SessionStart at all.
payload's `source`, but it read the legacy flag, where off is absence — a
deactivated session found nothing stored and fell back to getDefaultMode()
regardless. The other three are consequences of the storage shape, not of
when the hook re-derives.
State now lives in .caveman-sessions/<session_id>.mode, keyed by the
session_id Claude Code puts in every hook payload and in the statusline's
stdin JSON. The legacy flag stays as a last-write-wins compat mirror and
never receives the literal 'off': an older hook reading that would inject
"CAVEMAN MODE ACTIVE (off)" and an older statusline would render
[CAVEMAN:OFF]. Both spellings of off are read everywhere — a missing file
(old) and a literal 'off' (new, durable).
caveman-config.js gains the primitives: validateSessionId (a whitelist,
because a session id becomes part of a path), resolveActiveMode,
readSessionModeRaw, writeSessionMode, per-session prev for one-shot skills
(#599), gcSessionStore, and a session_id-tagged recordModeChange (#601).
Every one takes a sessionId that may be null or malformed and degrades to
the previous machine-wide behavior — the old code path IS the fallback
branch, which is why the existing suites, nonid,
pass unchanged.
SessionStart re-derives the default only on `startup` and `clear`; compact,
resume, fork, an unrecognized source and the n`
read the stored value. Grouping `clear` with the resets deliberately
differs from #691's comment: per-session storage makes the distinction
cheap, and nothing else in the conversation survives /clear either. The
watchdog must not reset, or a slow payload on a compaction re-arms a
session the user turned off.
The three entrypoints resolve the new helpers individually instead of
demanding them in their requireSibling shape checks (#848): a
caveman-config.js from before this change satisfies those checks, so
hard-failing would trade "machine-wide mode, no
state at all" on exactly the plugin-cache drift that guard exists for.
Both statusline ports take session_id from the stdin JSON with bounded,
TTY-guarded reads (integer timeout — bash 3.2 rejects fractional read -t)
and render nothing for a deactivated session. verify_repo.py greps them
against SESSIONS_DIRNAME and SESSION_ID_RE, tws
badge. caveman-stats filters the transition l
window's switches no longer skew another's es
(bin/install.js, uninstall.sh, uninstall.ps1) removes the session store;
.caveman-history.jsonl is kept on purpose. opide
behaviour — it writes its own flag in its own config dir and never sees a
Claude Code session_id.
Tests: 25 unit cases in tests/hooks/caveman-config.test.mjs,
SessionStartSourceTests, SessionScopedModeTests, 5 statusline cases, and
tests/manual/session-mode-smoke.sh (22 checks, end-to-end against a
throwaway config dir). docs/testing-session-modes.md carries the full plan
including the live-Claude-Code checks. Hook ced.
npm test 197/197 · tests/*.js clean · unittesss
· smoke 22/22
Signed-off-by: Pavel Filippenko <filippenko.ps@phystech.edu>
Upgrade `caveman learn` from a Claude/Codex setup linter into a broader local optimizer. It now scans Gemini, opencode, and opt-in aider history, uses catalog-backed context windows and measured turn-1 prefix evidence, adds new behavioral detectors, and ranks grouped portfolio moves without mixing historical totals into forward rates.
The CLI/proxy/TUI now support `learn applied`, `learn simulate`, `--repo`, per-repo reporting, and confirmed longitudinal fix verdicts (`improved`, `unchanged`, `regressed`, `insufficient_data`). Docs, the embedded `caveman-learn` skill, and tests were updated to cover the new learn v2 behavior and planning artifacts.