## Description
Hardens the bundled OpenClaw plugin so configured proxy routing is
fail-closed and `autoStart` is opt-in.
Closes: N/A
This follow-up is intentionally separate from the ContentRouter cache
fix because it changes plugin/gateway behavior rather than core
compression routing.
The plugin should not mutate upstream provider routing unless a
configured proxy URL is reachable and looks like Headroom. It should
also avoid unhandled startup promise rejections when proxy startup is
fire-and-forget.
Why this shape:
- `autoStart: false` by default matches deployments where Headroom is
supervised externally, for example by systemd. The plugin should not
silently start or assume ownership of a proxy unless the operator opted
in.
- Provider routing is fail-closed: a configured URL must first respond
like Headroom, not merely expose a generic liveness endpoint. This
prevents accidentally routing model traffic through the wrong local
service.
- `/readyz` is treated as liveness, not identity. Identity comes from
Headroom-shaped stats endpoints (`/v1/retrieve/stats` or `/stats`)
because those are harder for unrelated services to satisfy by accident.
- Startup remains asynchronous, but errors are captured and exposed
instead of becoming unhandled promise rejections.
- This is a separate PR because the core cache fix is about compression
correctness, while this patch is about integration safety around
OpenClaw gateway routing.
## Type of Change
- [x] Bug fix (non-breaking change fixes issue)
- [ ] New feature (non-breaking change adds functionality)
- [ ] Breaking change (fix or feature would cause existing functionality
change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Make proxy `autoStart` opt-in (`default: false`).
- Probe configured `proxyUrl` before applying provider routing.
- Treat `/readyz` as liveness only; require Headroom-shaped
`/v1/retrieve/stats` or `/stats` for identity.
- Observe fire-and-forget startup promise rejection and expose startup
error for callers.
- Isolate proxy-ready listener failures.
- Keep provider routing deferred when no active/probed Headroom proxy
exists.
- Register retrieve tool with explicit `headroom_retrieve` name.
- Extend plugin/unit tests for configured proxy failures, generic
non-Headroom endpoints, path collisions, and routing behavior.
Changed files:
- `plugins/openclaw/README.md`
- `plugins/openclaw/openclaw.plugin.json`
- `plugins/openclaw/src/engine.ts`
- `plugins/openclaw/src/plugin/index.ts`
- `plugins/openclaw/src/proxy-manager.ts`
- `plugins/openclaw/test/engine.test.ts`
- `plugins/openclaw/test/gateway-config.test.ts`
- `plugins/openclaw/test/plugin-runtime-routing.test.ts`
- `plugins/openclaw/test/proxy-manager.test.ts`
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added new functionality
- [x] Manual testing performed
### Test Output
```text
$ npm test
Test Files 6 passed (6)
Tests 74 passed (74)
$ npm run typecheck
tsc --noEmit
$ npm run build
tsup && node prepare-dist.mjs
Build success
```
## Real Behavior Proof
- Environment: local OpenClaw plugin package in the Headroom repo.
- Exact command / steps:
- Run plugin test suite.
- Run TypeScript typecheck.
- Run plugin build.
- Observed result:
- Tests passed: `74/74`.
- Typecheck passed.
- Build passed.
- Not tested:
- Full OpenClaw Gateway integration as part of this standalone PR prep.
## Review Readiness
- [x] I performed self-review
- [x] This PR ready for human review
## Checklist
- [x] My code follows project's style guidelines
- [x] I performed self-review my code
- [ ] I commented my code, particularly in hard-to-understand areas
- [x] I made corresponding changes documentation
- [x] My changes generate no new warnings
- [x] I added tests prove fix is effective or feature works
- [x] New and existing unit tests pass locally my changes
- [ ] I updated CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A.
## Additional Notes
Checklist items left unchecked intentionally:
- No CHANGELOG update included.
- No extra comments were needed beyond existing code structure.
Co-authored-by: Björn-Christian Bönkost <bjoern@v2202603344248440850.hotsrv.de>
- Extract shared defaultLogger constant, use in both ProxyManager and
HeadroomContextEngine (was duplicated inline in both constructors)
- Extract parseProxyUrl() helper, reused by normalizeAndValidateProxyUrl
and withDefaultPort (eliminates redundant URL parsing)
- Add test helpers: stubProbeSuccess/NonHeadroom/Unreachable to replace
6 identical inline fetch mock constructions
- Remove duplicate probe tests from engine.test.ts (already covered in
proxy-manager.test.ts)
- Clean up unused probeHeadroomProxy import from engine.test.ts
Net: -26 lines, same coverage (28 tests)
OpenClaw plugin:
- Fix assistant content always returned as array (fixes flatMap crash)
- Clean up debug logging, restore clean plugin entry point
- Add test for text-only assistant round-trip
- Set headroom-ai dependency to npm (not local path)
Telemetry:
- Switch from sb_publishable_ to JWT anon key (PostgREST needs JWT for RLS)
- Switch from upsert to plain INSERT (anon role upsert was failing with 42501)
- Verified: full payload with all JSONB columns writes successfully (201)
Cost tracker:
- Use output_buffer instead of hardcoded 500 for cost estimation in client.py
- CostTracker counterfactual: value removed tokens at avg effective $/token
(actual_spend / billed_tokens) instead of uncached list price — aligns
savings with real billing mix (cache reads, writes, uncached)
- Add debug log for /stats summary payload
- Add 3 tests for new cost savings calculation
TypeScript SDK:
- Add tokenBudget param to compress(), HeadroomClient, CompressOptions
- Proxy /v1/compress accepts optional token_budget to override model limit