knoxville-byoa-adapter
An openclaw plugin that connects your own openclaw agent to the Knoxville AI platform as a bring-your-own-agent (BYOA) drive-thru.
Install it, set one secret, and your agent answers user turns the platform proxies to you — with the agent's own reasoning, threaded by conversation.
What it does
The Knoxville platform proxies each user turn to a messaging URL you register on your drive-thru listing. This plugin makes your agent speak that contract:
- Registers a messaging endpoint on your gateway's HTTP server
(
POST /knox/messagesby default) that:- verifies
Authorization: Bearer <your shared secret>(the only trust anchor — the platform sends no JWT), - runs the turn through your agent (via the gateway's
OpenAI-compatible
/v1/chat/completions, threaded onconversation_id), - replies as SSE (streaming, default) or JSON, per the contract.
- verifies
- Registers an unauthenticated health probe (
GET /healthz). - Prints the exact Messaging URL + shared secret to paste into your listing on boot — connecting is copy-paste, not guesswork.
It refuses to start "open" (no auth) unless you explicitly opt in.
Install
openclaw plugins install clawhub:knoxville-byoa-adapter
Then enable it and set your shared secret (see below), and restart your gateway.
Required configuration
The shared secret is the only thing you must set. Everything else has a sensible default.
| Env var | Plugin config key | Default | Purpose |
|---|---|---|---|
KNOX_SHARED_SECRET | sharedSecret | (generated if unset) | Bearer the platform must present. Set this. |
OPENCLAW_GATEWAY_TOKEN | gatewayToken | — | Bearer for your local gateway (you already have this). |
OPENCLAW_GATEWAY_URL | gatewayUrl | http://127.0.0.1:18789 | Your gateway's OpenAI-compatible surface. |
KNOX_BYOA_PORT | port | 8790 | Port for the standalone runner (ignored when mounted as a plugin route). |
KNOX_BYOA_PATH | messagePath | /knox/messages | Path the platform POSTs to. May embed {conversation_id}. |
KNOX_BYOA_HEALTH_PATH | healthPath | /healthz | Liveness path. Change if it collides with the gateway's own. |
KNOX_BYOA_RESPONSE_MODE | responseMode | auto | auto (negotiate on Accept, SSE-first), sse, or json. |
KNOX_BYOA_PUBLIC_URL | publicUrl | — | Public base URL the platform reaches (for the boot banner). |
KNOX_BYOA_MODEL_ROUTE | modelRoute | openclaw/default | Model route passed to the gateway. |
KNOX_BYOA_ALLOW_OPEN | allowOpen | false | Run with no auth. Dangerous; off by default. |
KNOX_BYOA_HOME | home | — | Dir to persist a generated secret across restarts. |
KNOX_BYOA_GATEWAY_TIMEOUT_MS | gatewayTimeoutMs | 120000 | Per-turn timeout. |
Set the secret via env (recommended) so it stays out of config files:
export KNOX_SHARED_SECRET="$(openssl rand -base64 32)"
How to find your endpoint URL
On boot the plugin prints a banner with the exact values:
Knoxville BYOA adapter is live.
Paste these into your Knoxville drive-thru listing:
Messaging URL: https://your-gateway-host/knox/messages
Shared secret: <your secret>
Health check: https://your-gateway-host/healthz
The route is served on your gateway's HTTP server, so the host is wherever
your gateway is reachable. Set KNOX_BYOA_PUBLIC_URL to your public https
address and the banner prints the full, paste-ready URL.
How to register the listing
In the Knoxville console, open your drive-thru's BYOA settings and paste:
- Messaging URL → the
Messaging URLfrom the banner. - Shared secret → the
Shared secretfrom the banner.
The platform sends an Authorization: Bearer <shared secret> on every turn;
the plugin rejects anything else with 401. That's the whole handshake.
The contract (what the platform sends / expects)
Request — POST to your messaging URL:
Authorization: Bearer <shared secret>
Content-Type: application/json
Accept: text/event-stream, application/json
X-Knox-Conversation-Id: <uuid>
X-Knox-Caller-Kind: user | anonymous | agent
X-Knox-Caller-Id: <opaque>
X-Knox-Caller-Email: <email> # signed-in users only
{ "content": "<user message>", "conversation_id": "<uuid>",
"caller_kind": "anonymous", "caller_id": "<opaque>" }
Response — one of:
- JSON:
200,{ "reply": "<text>" }(failure:{ "error": "<msg>" }or non-2xx). - SSE:
200,text/event-stream:
hard failure:data: {"type":"token","delta":"<chunk>"} data: {"type":"done","status":"complete"} # complete | interrupted | errordata: {"type":"error","error":"<msg>"}.
start_task/ long-running tasks are not in scope for BYOA yet.
Running standalone (sidecar)
If you'd rather not load it as a gateway plugin, run it as its own process:
KNOX_SHARED_SECRET=... OPENCLAW_GATEWAY_TOKEN=... npx byoa-adapter
# or, from a checkout: npm run build && node bin/byoa-adapter.mjs
It binds KNOX_BYOA_PORT (default 8790) and serves the same contract.
Verify it works
A self-contained smoke test (mock gateway + adapter + the real platform request, JSON & SSE & health & bad-auth):
bash scripts/verify.sh
Or hit a running adapter the way the platform does:
curl -sS -X POST "$ENDPOINT" \
-H "Authorization: Bearer $KNOX_SHARED_SECRET" \
-H "Content-Type: application/json" \
-H "X-Knox-Conversation-Id: test-123" \
-H "X-Knox-Caller-Kind: anonymous" \
-d '{"content":"hello","conversation_id":"test-123","caller_kind":"anonymous"}'
Development
npm install # also pulls the openclaw peer dep for types
npm run typecheck # src
npm test # type-checks tests, then runs node:test against a mock agent
npm run build # emit dist/
Tests cover: auth pass/fail (401), JSON reply shape, SSE reply shape, health, malformed/missing body, gateway-error mapping, and caller-header threading.
Publishing to ClawHub
Pushing changes under plugins/knoxville-byoa-adapter/ to main triggers the
repo's publish-plugins.yml workflow, which builds, tests, and runs
clawhub package publish for the changed plugin. Bump version in both
package.json and openclaw.plugin.json, then merge.
To publish manually:
npm ci && npm run build && npm test
clawhub package publish . # add --dry-run first to preview
Notes / things to verify against your openclaw version
- Built and type-checked against
openclaw@2026.6.x(plugin-sdk/plugin-entry,api.registerHttpRoute,api.pluginConfig). The SDK pins are declared inpackage.json(peerDependencies.openclaw,openclaw.compat.pluginApi,openclaw.build.openclawVersion); reconcile if your gateway is older/newer. - The reply is produced by calling the gateway's loopback
/v1/chat/completions(the same surface Knoxville's own vessel uses). A future version could call the in-process agent API instead.