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 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 # 69 checks: core, tokens, capsules, dependencies, sync, persistence, concurrency, MCP stdio, HTTP, SSE, CLI, hosted security, hostile input, input fuzz, load & resilience
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), optionally `GHOSTBUS_REQUIRE_TOKENS=1`. (The serverless pack keeps token enforcement opt-in via this env; the Node hosted server above enforces by default since v0.5.2.)
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
@@ -44,5 +44,5 @@
44
44
  "hosted": "node src/hosted-server.mjs"
45
45
  },
46
46
  "type": "module",
47
- "version": "0.5.6"
47
+ "version": "0.5.7"
48
48
  }
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.6';
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) }));
@@ -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';