ghostbus 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/docs/hosted.md +53 -2
- package/package.json +1 -1
- package/src/core.mjs +1 -1
- package/src/handler.mjs +7 -0
- package/src/hosted-server.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.7 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
Security hardening, round 6 — the credential lifecycle, and the serverless pack's quiet drift. This round audited how every credential is born, stored, leaked (or not), and killed. The Node hosted server's core answers were mostly already right and are now pinned by tests; the real findings were a rotation path that only existed for one client type, and a serverless pack that had silently fallen behind four separate hardening decisions from earlier rounds.
|
|
6
|
+
|
|
7
|
+
- **Agent tokens are rotatable over plain REST (fixed gap)** — rotation existed in the core and as the `bus_rotate_token` MCP tool, but the HTTP handler had no route for it: a pure-REST agent (curl, a webhook, a non-MCP client) whose token leaked had no way to rotate it. `POST /api/agents/rotate-token` now exists on the relay, the hosted server, and the serverless pack — current token authorizes, new token returns once, old hash is replaced before the reply. Rotation still requires the current token on an enforced bus, so a stranger who knows only an agent's name cannot rotate its owner out.
|
|
8
|
+
- **Rotation immediacy, verified end to end** — workspace-key rotation already replaced the registry hash and refreshed the cached meta in the same request (round 2's design); round 6 pins it over real HTTP on the hosted server and in the serverless lib: the old credential's *very next* request fails, the new one works. Agent-token rotation is pinned the same way (core, REST, serverless). Serverless rotation is effective on the next request by construction — a fresh bus is built from the KV per request, so there is no cache that could keep an old key alive.
|
|
9
|
+
- **Delivered credentials verified, not assumed** — workspace keys and agent tokens are `crypto.randomBytes(24)` in every issuance path (hosted create/import/rotate, core register/rotate, serverless create/import/rotate): 48 lowercase hex chars, 192 bits, now asserted by format checks in both suites. The admin key stays operator-chosen with the ≥16-character startup floor — and that floor now exists in the serverless pack too (it previously accepted anything non-empty).
|
|
10
|
+
- **Leak audit: clean, now pinned** — every surface a raw credential could escape through was checked: API responses (credentials appear only in their one-time issuance replies), the board payload (the key lives in local storage, travels as a header — round 4), error messages, server stderr in normal operation including the v0.5.3 audit lines (re-verified after rounds 4–5), and the provenance/event log — which matters most, because `/api/events` and `bus_sync` are readable by *every* workspace member: the log records that a rotation happened and never the token. Export envelopes carry agent `tokenHash` values and nothing else: SHA-256 of a 192-bit random token is not a practical offline brute-force target, so an export is a backup, not a credential — the suite asserts the hashes' shape and the absence of every live credential it issued.
|
|
11
|
+
- **Serverless parity closed (the round's biggest find)** — the Netlify pack shares the core but had drifted: (1) per-agent token enforcement defaulted **off** (wrapper opt-in) while the Node hosted server has enforced by default since v0.5.2; (2) the admin key had no minimum length; (3) workspace **rotate-key, export, and import did not exist** — a leaked serverless workspace key could only be answered by deleting the workspace. All four are aligned: enforcement is the pack's default too (`GHOSTBUS_REQUIRE_TOKENS=0` is the only opt-out, same spelling as Node), construction refuses a short admin key, and the three admin routes mirror the Node semantics (deep snapshot validation at import, fresh key on import, audit lines in the function log). The lib test grew **6 → 12 checks** to hold the line — enforcement default, REST + workspace rotation immediacy, the round-3 reserved-file-name refusal and round-4 limit clamps flowing through the shared core, timing-safe compares, export→import across two independent cores.
|
|
12
|
+
- **Documented, deliberately not built** — two things this round chose to write down instead of code. **No credential expiry:** nothing expires on a timer; the lifecycle is issue-once / hash-at-rest / rotate-on-suspicion, with the trade-off stated where operators will read it (`SECURITY.md`, "Credential lifecycle"). **Single-scope authorization:** a workspace key is full access to its one workspace (token minting included), an agent token is that agent, the admin key is root — there are no read-only keys, and that is a stated pilot-stage design decision with its reasoning, not a gap dressed as one (`SECURITY.md`, "Authorization model"). Two serverless divergences also remain, documented rather than hidden: no app-level failed-auth throttle in the pack (stateless per request; the platform's traffic layer plus 192-bit keys carry it) and platform-bounded request bodies instead of the app-level 1 MB cap.
|
|
13
|
+
- **Tests** — main suite **70 checks** (was 69): one hosted check covering REST rotation immediacy, delivered-credential formats, events-log and export hash-only assertions, and stderr secret absence. Serverless lib **12 checks** (was 6). Red→green: the pre-fix probe showed a tokenless act succeeding on the serverless default (200 vs Node's 400), rotate/export routes absent, a 5-char admin key accepted, and REST rotation 404 — all closed. Full suite, serverless, and both demos green.
|
|
14
|
+
- **Scope boundary, honestly** — no API shapes changed for existing clients; the one new route is additive. The single relay / stdio / CLI local trust model is untouched. Credential expiry and scoped keys were audited and consciously deferred with their reasoning recorded — if either ever ships, it ships as a feature, not as a silent default change.
|
|
15
|
+
|
|
3
16
|
## 0.5.6 — 2026-10-11
|
|
4
17
|
|
|
5
18
|
Security hardening, round 5 — load & abuse resilience: what happens when many agents share one process for a long time. This round built the first repeatable load bench before changing anything, and the honest headline is that load broke no correctness property — zero errors, zero lost writes, exact reconciliation at every size. What it (and the audit around it) found instead: an unbounded subscription surface, missing server timeouts whose platform defaults do not do what their names suggest, and a measured, structural scaling limit that is now documented instead of discovered by operators.
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Built and in use: 29 MCP tools across presence, messages, tasks, a shared worksp
|
|
|
23
23
|
```bash
|
|
24
24
|
git clone https://github.com/littlestjames82-sys/ghostbus.git ghostbus && cd ghostbus
|
|
25
25
|
node examples/two-agent-demo.mjs # watch a planner and a builder cooperate
|
|
26
|
-
node test.mjs #
|
|
26
|
+
node test.mjs # 70 checks: core, tokens, capsules, dependencies, sync, persistence, concurrency, MCP stdio, HTTP, SSE, CLI, hosted security, hostile input, input fuzz, load & resilience, token lifecycle
|
|
27
27
|
node examples/three-agent-team.mjs # planner + builder + reviewer, no human relaying
|
|
28
28
|
```
|
|
29
29
|
|
package/docs/hosted.md
CHANGED
|
@@ -47,6 +47,20 @@ The same snapshot format works without a server: `ghostbus-cli export backup.jso
|
|
|
47
47
|
and `ghostbus-cli --store other.json import backup.json`. Restores are recorded
|
|
48
48
|
in the workspace's provenance log (`workspace.restore`).
|
|
49
49
|
|
|
50
|
+
Agent tokens rotate the same way — immediately, old credential dead on the
|
|
51
|
+
next request — via the `bus_rotate_token` MCP tool or, since v0.5.7, plain
|
|
52
|
+
REST (presenting the current token):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl -X POST localhost:8388/w/acme/api/agents/rotate-token \
|
|
56
|
+
-H 'x-bus-key: <workspace key>' -H 'x-agent-token: <current agent token>' \
|
|
57
|
+
-d '{"agent":"builder"}'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The full credential lifecycle (issue / storage / rotation / the no-expiry
|
|
61
|
+
posture) and the single-scope authorization model are documented in
|
|
62
|
+
`SECURITY.md` at the repo root.
|
|
63
|
+
|
|
50
64
|
### Agent flow
|
|
51
65
|
|
|
52
66
|
Everything from the single-workspace relay works under the prefix:
|
|
@@ -189,6 +203,43 @@ including MCP clients and any agent driving one:
|
|
|
189
203
|
and referers. `/health` and `/probe` return exactly their documented
|
|
190
204
|
counts-only shapes.
|
|
191
205
|
|
|
206
|
+
Round 6 (v0.5.7) audited the credential lifecycle and stopped the
|
|
207
|
+
serverless pack from drifting away from the rules above:
|
|
208
|
+
|
|
209
|
+
- **Rotation is immediate everywhere, and now reachable everywhere.**
|
|
210
|
+
Workspace-key rotation replaced the registry hash before replying
|
|
211
|
+
(with the cached meta refreshed) — pinned by test: the old key's next
|
|
212
|
+
request is a 401. Agent tokens were rotatable only through MCP; the
|
|
213
|
+
REST route (`POST /api/agents/rotate-token`) exists now, on both
|
|
214
|
+
servers and in the serverless pack. Nothing expires on a timer; the
|
|
215
|
+
posture and the rotation runbook are in `SECURITY.md`.
|
|
216
|
+
- **Delivered credentials verified, not assumed.** Workspace keys and
|
|
217
|
+
agent tokens are `crypto.randomBytes(24)` — 48 hex chars, 192 bits —
|
|
218
|
+
asserted by format checks in the suite. Only SHA-256 hashes are ever
|
|
219
|
+
stored, and the leak audit covered API responses, the board payload,
|
|
220
|
+
error messages, the provenance/event log (readable by every workspace
|
|
221
|
+
member — a token there would be a cross-agent compromise; rotation
|
|
222
|
+
events name the act, never the token), server stderr in normal
|
|
223
|
+
operation, and export envelopes: hashes only, everywhere.
|
|
224
|
+
- **Serverless parity closed.** The pack had quietly drifted: token
|
|
225
|
+
enforcement defaulted OFF (the Node server enforces by default), the
|
|
226
|
+
admin key had no 16-character floor, and rotate-key / export / import
|
|
227
|
+
did not exist — a leaked serverless workspace key could only be
|
|
228
|
+
answered by deleting the workspace. All aligned now, and the lib test
|
|
229
|
+
grew from 6 to 12 checks to keep them aligned (enforcement default,
|
|
230
|
+
rotation immediacy, deep snapshot validation at import, the round-3/4
|
|
231
|
+
core fixes flowing through the shared code).
|
|
232
|
+
- **Two serverless divergences remain — documented, not silent.** There
|
|
233
|
+
is no application-level failed-auth throttle in the pack: serverless
|
|
234
|
+
instances are stateless per request, so the Node server's in-memory
|
|
235
|
+
per-IP counter has no equivalent; the mitigations are the platform's
|
|
236
|
+
own traffic layer plus 192-bit keys and timing-safe compares (online
|
|
237
|
+
guessing is not a practical attack on either). Request bodies are
|
|
238
|
+
likewise bounded by the platform's function payload limits rather
|
|
239
|
+
than the app-level 1 MB cap. Admin actions ARE audit-logged in the
|
|
240
|
+
pack since v0.5.7 — the same one-line JSON records, in the function
|
|
241
|
+
log.
|
|
242
|
+
|
|
192
243
|
## Capacity & limits (measured, v0.5.6)
|
|
193
244
|
|
|
194
245
|
First published load figures, from `scripts/load-bench.mjs` — a repeatable
|
|
@@ -255,10 +306,10 @@ single-file ceiling is documented future work, not a flag you can flip.
|
|
|
255
306
|
`deploy/netlify/` contains a Netlify Function implementing the same multi-workspace semantics over Netlify Blobs (strong consistency):
|
|
256
307
|
|
|
257
308
|
1. Create a Netlify site using `deploy/netlify/` as the site root (or copy `functions/` + `netlify.toml` into a site).
|
|
258
|
-
2. Set site env: `GHOSTBUS_ADMIN_KEY` (required
|
|
309
|
+
2. Set site env: `GHOSTBUS_ADMIN_KEY` (required, at least 16 characters — the pack refuses to construct without one, same floor as the Node server). Per-agent token enforcement is ON by default here too (since v0.5.7, matching the Node hosted server); `GHOSTBUS_REQUIRE_TOKENS=0` is the only opt-out.
|
|
259
310
|
3. Add `@netlify/blobs` to that site's dependencies (`npm i @netlify/blobs` in the site root) and deploy.
|
|
260
311
|
|
|
261
|
-
The routing, admin API, per-workspace keys, and REST/MCP surface match Option A, with two honest differences: **no board page, no SSE, no long-poll** (serverless invocations can't hold them) — agents poll `/w/<id>/probe` + inbox or use `bus_sync`. State per workspace is one Blobs entry, written per request from a fresh bus instance; concurrent writes to the *same* workspace can race (last writer wins a single operation), so very hot single-workspace traffic belongs on Option A.
|
|
312
|
+
The routing, admin API (create / list / delete / **rotate-key / export / import** — parity since v0.5.7), per-workspace keys, and REST/MCP surface match Option A, with two honest differences: **no board page, no SSE, no long-poll** (serverless invocations can't hold them) — agents poll `/w/<id>/probe` + inbox or use `bus_sync`. Two smaller divergences are documented in the Round 6 notes above (no app-level failed-auth throttle; platform-bounded request bodies). State per workspace is one Blobs entry, written per request from a fresh bus instance; concurrent writes to the *same* workspace can race (last writer wins a single operation), so very hot single-workspace traffic belongs on Option A.
|
|
262
313
|
|
|
263
314
|
The serverless logic (`deploy/netlify/functions/lib.mjs`) is unit-tested with an in-memory KV through real Web Request/Response objects: `node deploy/netlify/test-lib.mjs`.
|
|
264
315
|
|
package/package.json
CHANGED
package/src/core.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import path from 'node:path';
|
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
15
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
16
|
|
|
17
|
-
export const GHOSTBUS_VERSION = '0.5.
|
|
17
|
+
export const GHOSTBUS_VERSION = '0.5.7';
|
|
18
18
|
const sha256 = (s) => crypto.createHash('sha256').update(String(s)).digest('hex');
|
|
19
19
|
// Timing-safe equality for hex digests (v0.5.3): secret comparisons must not
|
|
20
20
|
// short-circuit on the first differing character the way === does.
|
package/src/handler.mjs
CHANGED
|
@@ -306,6 +306,13 @@ export function createBusHandler({ id = 'default', bus, key = '' }) {
|
|
|
306
306
|
if (req.method === 'GET' && url.pathname === '/api/status') return send(200, await bus.status());
|
|
307
307
|
if (req.method === 'GET' && url.pathname === '/api/agents') return send(200, { agents: await bus.listAgents() });
|
|
308
308
|
if (req.method === 'POST' && url.pathname === '/api/agents') return send(200, await bus.registerAgent({ name: safeBody.agent, role: safeBody.role, capabilities: safeBody.capabilities, token: agentTokenOf(req, safeBody) }));
|
|
309
|
+
// Agent-token rotation over plain REST (v0.5.7): until this route the
|
|
310
|
+
// only rotation path was the MCP tool — a pure-REST agent whose token
|
|
311
|
+
// leaked had no way to rotate it. Same semantics as the core/MCP path:
|
|
312
|
+
// the current token authorizes (on an enforced bus), the new token is
|
|
313
|
+
// returned once, and the old hash is replaced before the reply, so the
|
|
314
|
+
// old token is dead the moment this returns.
|
|
315
|
+
if (req.method === 'POST' && url.pathname === '/api/agents/rotate-token') return send(200, await bus.rotateAgentToken({ name: safeBody.agent, token: agentTokenOf(req, safeBody) }));
|
|
309
316
|
if (req.method === 'POST' && url.pathname === '/api/messages') return send(200, await bus.sendMessage({ from: safeBody.agent, to: safeBody.to ?? '*', body: safeBody.body, channel: safeBody.channel ?? 'general', threadId: safeBody.threadId ?? null, token: agentTokenOf(req, safeBody) }));
|
|
310
317
|
if (req.method === 'GET' && url.pathname === '/api/inbox') return send(200, { messages: await bus.inbox(q.agent, { unreadOnly: q.unread === '1', limit: Number(q.limit || 50), token: agentTokenOf(req, {}, q) }) });
|
|
311
318
|
if (req.method === 'POST' && url.pathname === '/api/heartbeat') return send(200, await bus.heartbeat({ name: safeBody.agent, token: agentTokenOf(req, safeBody) }));
|
package/src/hosted-server.mjs
CHANGED
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
* route matching (regex), workspace resolution, and the registry itself
|
|
32
32
|
* when loaded: ids become file names inside the data dir, so a planted or
|
|
33
33
|
* hand-edited registry entry must never turn one into a path outside it.
|
|
34
|
+
* - v0.5.7: the credential lifecycle was audited end to end (rotation is
|
|
35
|
+
* immediate and is now reachable over plain REST for agent tokens, not
|
|
36
|
+
* just MCP) and the serverless pack was realigned with this server —
|
|
37
|
+
* enforcement default, admin-key floor, rotate/export/import. See
|
|
38
|
+
* SECURITY.md ("Credential lifecycle") and docs/hosted.md round 6.
|
|
34
39
|
*/
|
|
35
40
|
import http from 'node:http';
|
|
36
41
|
import fs from 'node:fs';
|