Summary
The bridge HTTP server (src/api/http-server.ts) gates all of /api/* behind a single local secret — but only when one is configured:
http-server.ts:139 — const host = secret ? '0.0.0.0' : '127.0.0.1';
http-server.ts:311 — the entire auth block is inside if (secret && !isPublicHealth && !url.startsWith('/api/files/')).
So with api.secret unset (the default in the example config), every API — /api/talk, /api/tasks, bot CRUD, /api/skills/*/publish, peers — is open on loopback with no credential at all. The routes behind it include the chat/talk surface and the skill publish path (the skill-hub issue), so any local process (or a browser page reaching localhost) can talk to agents and publish skills.
When a secret IS set, two secondary issues:
- the server binds 0.0.0.0 and every consumer shares one secret (no per-bot credentials);
- the secret is accepted as a query parameter (
http-server.ts:316-318 token=), so it ends up in access logs, browser history, and any URL logging.
/api/files/ is exempt from auth entirely (:311), serving files by opaque URL.
Details
http-server.ts:138-139 — host selection: loopback only when secret is absent.
http-server.ts:311-340 — auth applies only if (secret && ...); without secret, all routes pass through.
http-server.ts:316-321 — token= query-param auth path (URL logging exposure).
- Routes behind it:
/api/talk, /api/tasks, bots CRUD, /api/skills/:name/publish (skill-routes.ts:46-88), peers.
How to reproduce
# run the bridge with api.secret unset (default config):
curl -X GET http://127.0.0.1:<port>/api/bots # -> 200, no credentials
curl -X POST http://127.0.0.1:<port>/api/talk \
-H 'Content-Type: application/json' -d '{"message": "run this command: ..."}'
# -> 200, agent executes
Impact
With the default (no secret) config, any local process can drive the full bridge API including agent talk and skill publish. With a secret, it's a single shared secret on the LAN, transmitted in query strings.
Suggested change
- Require a secret (or reject startup without one), and keep loopback binding when unset.
- Move the token out of the query string (header-only).
Summary
The bridge HTTP server (src/api/http-server.ts) gates all of
/api/*behind a single local secret — but only when one is configured:http-server.ts:139—const host = secret ? '0.0.0.0' : '127.0.0.1';http-server.ts:311— the entire auth block is insideif (secret && !isPublicHealth && !url.startsWith('/api/files/')).So with
api.secretunset (the default in the example config), every API —/api/talk,/api/tasks, bot CRUD,/api/skills/*/publish, peers — is open on loopback with no credential at all. The routes behind it include the chat/talk surface and the skill publish path (the skill-hub issue), so any local process (or a browser page reaching localhost) can talk to agents and publish skills.When a secret IS set, two secondary issues:
http-server.ts:316-318token=), so it ends up in access logs, browser history, and any URL logging./api/files/is exempt from auth entirely (:311), serving files by opaque URL.Details
http-server.ts:138-139— host selection: loopback only when secret is absent.http-server.ts:311-340— auth applies onlyif (secret && ...); without secret, all routes pass through.http-server.ts:316-321—token=query-param auth path (URL logging exposure)./api/talk,/api/tasks, bots CRUD,/api/skills/:name/publish(skill-routes.ts:46-88), peers.How to reproduce
Impact
With the default (no secret) config, any local process can drive the full bridge API including agent talk and skill publish. With a secret, it's a single shared secret on the LAN, transmitted in query strings.
Suggested change