ghostbus 0.5.6 → 0.5.10
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 +48 -0
- package/README.md +4 -4
- package/SECURITY.md +137 -0
- package/docs/hosted.md +54 -3
- package/docs/security.md +275 -0
- package/package.json +4 -2
- 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,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.10 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
Release-pipeline fix — the round-7 pack gate did its job *too* literally, and two releases paid for it. The v0.5.8 and v0.5.9 GitHub releases were created, but their publish runs failed the suite step and **never reached npm** (the registry stayed at 0.5.7); this entry exists so the gap is explained in the record rather than discovered. This release supersedes both on npm; their GitHub release notes carry a pointer here.
|
|
6
|
+
|
|
7
|
+
- **Root cause: the pack-manifest parser assumed one npm major's output shape** — `scripts/check-pack.mjs` parsed `npm pack --dry-run --json` as a JSON array (`parsed[0]`). The publish workflow installs `npm@latest` before running the suite (required for Trusted Publishing), and npm 12 changed the output to an object keyed by package name — so the parse produced `undefined` and the check threw. Local development ran npm 10/11, where the array shape still holds, so nothing failed until the runner's npm 12 met the new gate. The gate blocking the publish was the design working; the parser's single-shape assumption was the bug.
|
|
8
|
+
- **The fix: shape-tolerant parsing with verification** — the parser now accepts the array form (npm ≤ 11), a bare result object, or the name-keyed object (npm 12), and refuses (exit 2, "manifest NOT verified") if none of them yields a result with a real `files` array. Verified against live output from npm 10, 11, and 12 — all three parse to the same 22-file manifest. The check's assertions are unchanged.
|
|
9
|
+
- **What did *not* change** — no runtime code, no API, no behavior: `src/` differs from 0.5.9 only in the version constant. The 0.5.8 (supply-chain) and 0.5.9 (final sweep) work ships to npm for the first time in this version, on top of everything those entries describe.
|
|
10
|
+
- **Tests** — main suite **71 checks**, serverless **12**, both demos green, pack gate green under three npm majors.
|
|
11
|
+
|
|
12
|
+
## 0.5.9 — 2026-10-11
|
|
13
|
+
|
|
14
|
+
Security hardening, round 8 (final) — the fresh-eyes sweep. Rounds 1–7 each hardened one surface; this round re-audited the *whole* surface as if none of them had happened — every route in the HTTP handler, the hosted router, and the serverless lib re-read side by side, hunting for the parity lesson (a check one surface has and another forgot), an unhandled await that can 500 or hang, and any place a round 2–6 invariant silently doesn't hold — and then re-ran the accumulated hostile-input battery against the current code on all five surfaces. The honest headline: **no new vulnerability was found and no runtime code changed.** What this round ships is the regression evidence, the consolidated posture document, and three stale claims corrected.
|
|
15
|
+
|
|
16
|
+
- **The sweep: 71 probes, five surfaces, all landing as designed** — a purpose-built battery (run against the shipped tree, not the suite's smaller checks) covering: no-key / wrong-key / cross-workspace-key refusals on the relay, the hosted server, and the serverless lib; the counts-only `/probe` shape asserted by exact key set; workspace-id traversal refused at create, at import (explicit id, envelope id, and state id), at route matching, and in a **planted registry file** with a state file waiting outside the data dir (the entry is filtered at load; the outside file is never read); reserved file names (`__proto__` / `constructor` / `prototype`) refused at write on HTTP and in the core, path-like file reads inert; `__proto__` payloads driven through the context writer (HTTP + tools layer), snapshot restore, and hosted import, with `Object.prototype` canaries after each; snapshot refusals for `agents: [null]`, a `__proto__` file, and `workspace: null`; MCP hostile frames on stdio (`null`, a bare string, a number, an array, a paramless `tools/call`) and wrong-typed arguments over both stdio and HTTP — clean `isError` results, server alive and answering afterwards, clean exit; the numeric edges (`limit` 0 / −1 / `abc` / 1e9, garbage sync cursor, `timeout=-5`) on relay, hosted, and serverless alike; the 1 MB body cap (413); rotation immediacy for workspace keys **and** agent tokens on both hosted surfaces (old credential's next request fails); the 16-char admin-key floor refusing startup with exit 2; and board/landing security headers. 70 of 71 probes passed as written.
|
|
17
|
+
- **The one red probe was the probe's own expectation** — it expected an HTTP 400 for a `tools/call` frame with no `params`; the handler in fact answers a clean JSON-RPC `isError` result (HTTP 200), which is the codebase's standing convention for tool-call failures (the same frame on stdio is dropped, not fatal, per round 4). Verified by hand against both transports; behavior left as designed and recorded here so a future sweep doesn't "fix" the code to match a wrong probe.
|
|
18
|
+
- **Fresh-eyes code read: no parity gap found** — the hosted workspace routes and the single-relay routes run the *same* handler (the hosted router gates on the stored key hash, then delegates), the serverless lib's route table mirrors it route for route (board/SSE/long-poll absent there by documented design), and every route's auth position was re-checked against its siblings: admin before admin routes, workspace key before workspace routes, agent tokens inside the core. Unhandled-await audit: both Node servers wrap their handlers (generic 500 + stderr stack), the handler's own catch maps core errors to deliberate 400s, the serverless catch does the same, and the two long-lived paths (SSE, `/api/wait`) carry their own close/error cleanup. One examined non-fix: deleting a workspace does not sever an already-open SSE stream to it — but no route can reach the orphaned bus afterwards (the router 404s first), so the stream can only ever idle until the client leaves; noted, not patched.
|
|
19
|
+
- **`docs/security.md` — the consolidated posture document (new)** — everything an evaluator needs in one file: architecture and trust boundaries, the eight rounds' controls grouped by family (authentication & authorization, input validation, transport & headers, resource limits, durability, supply chain, serverless parity), the credential-lifecycle summary, the load figures with their ceiling, and a "Known limitations — stated, not hidden" section: the single-file JSON store and its O(state) per-op cost (the networked store is future work *by owner decision*), single-process/no HA, no credential expiry (rotation is the runbook), single-scope authorization, no uptime SLA in the pilot, and the two documented serverless divergences. It closes with the external review's five steps to 9/10 mapped honestly — steps 1 (harden the security model) and 5 (prove it under load) done with evidence, step 2 (transactional persistence) deferred as a recorded decision, steps 3–4 (orchestration, coding lifecycle) explicitly *not* claimed, because they were never security work. The document ships in the tarball (via `docs/`) and is now a required file in the pack gate; `SECURITY.md` stays the policy (reporting, lifecycle, authorization model) and cross-links it.
|
|
20
|
+
- **Consistency pass: three stale claims corrected** — the README's install line still said "(v0.5.0)" (the first-publish version) and now names the current release; the `docs/hosted.md` operator example showed a created key as `<64 hex>` — keys have been `crypto.randomBytes(24)` (48 hex chars, 192 bits) in every issuance path since round 6 format-verified them, so the example was a leftover and now says `<48 hex>`; `SECURITY.md` gained its cross-link to the posture doc. Everything else was audited to agreement across README / `SECURITY.md` / `docs/hosted.md`: 71 main + 12 serverless checks, the SSE caps (100 / 1000), the timeouts (10 s / 30 s / 5 s), the throttle (30/min), the 1 MB body cap, the rate limiter (300/min), the load table, and every `GHOSTBUS_*` env var named in the docs exists in the code with the documented default.
|
|
21
|
+
- **Tests** — main suite stays **71 checks**, serverless stays **12**, both demos green: the sweep found nothing that needed a fix, so it added no checks — the suite already pins every invariant the battery re-proved, and padding the count would say the opposite of what this round found. The pack gate is green with `docs/security.md` required and shipping (22 files). The only runtime-file change in the round is the version constant.
|
|
22
|
+
- **Scope boundary, honestly — and the loop's end state** — eight rounds close here because the final sweep came back clean, not because the calendar said so. What remains open is exactly the limitations list in `docs/security.md`, and every item on it is a recorded decision with its reasoning (defer the store, no expiry, single scope, no pilot SLA), not an oversight waiting to be found. If a ninth round ever runs, it should start the way this one did — fresh eyes, a hostile battery, and no credit taken for the previous rounds' word.
|
|
23
|
+
|
|
24
|
+
## 0.5.8 — 2026-10-11
|
|
25
|
+
|
|
26
|
+
Security hardening, round 7 — supply chain & release integrity: the pipeline that turns this repository into the npm package people install. Nothing in the runtime changed this round; everything here is about what *else* a user has to trust — the CI actions, the publish workflow, the tarball's exact contents, and the version claims the package makes about itself. One claim turned out to be unproven and is corrected; one document that should have shipped with the code now does.
|
|
27
|
+
|
|
28
|
+
- **Workflow actions pinned to commit SHAs** — both GitHub workflows (publish + CI) referenced `actions/checkout@v4` and `actions/setup-node@v4` by mutable tag: whoever controls the tag controls code that runs with publish credentials. Both are now pinned to full commit SHAs resolved from the GitHub API, with the release noted in a comment: `actions/checkout@11d5960a…` (# v4.4.0) and `actions/setup-node@49933ea5…` (# v4.4.0) — in each case the v4 tag and the newest v4.x.y tag resolve to the same commit, so the pin changes nothing that runs today, only what *can* run tomorrow.
|
|
29
|
+
- **Publish workflow audited end to end** — the trigger surface is a published GitHub Release, or a manual dispatch that defaults to *dry* (pack-and-verify only; publishing requires unchecking it explicitly). Checkout fetches exactly the release's tag tree — there is no branch swap, artifact hand-off, or other path by which a stray branch can influence what publishes. The job already held least-privilege permissions (`contents: read`, `id-token: write`, nothing else) and runs in the protected `npm` environment; both are now stated in the file so a future edit has to argue with them. One deliberate non-fix: the workflow installs `npm@latest` before publishing, because npm Trusted Publishing (OIDC) requires npm ≥ 11.5.1 — pinning the npm CLI to an older version would break the very mechanism that lets this repo publish with **no stored npm token at all**.
|
|
30
|
+
- **Provenance: verified present, now documented** — the registry metadata for v0.5.5 and v0.5.6 carries an SLSA provenance attestation (predicate `https://slsa.dev/provenance/v1`) and two registry signatures; v0.5.7 went through the identical workflow. `SECURITY.md` gained a "Supply chain" section saying how the package is produced and how to verify an installed copy (`npm audit signatures`). One honest caveat from this round's bench: the local `npm audit signatures` run could not complete from the development sandbox because the Sigstore CDN is unreachable from it — the verdict above rests on the registry's own attestation metadata, fetched directly, not on that failed run.
|
|
31
|
+
- **Tarball contents are now allowlisted, not assumed** — a real pack + extraction audit confirmed the shipped tarball was already clean (20 files: `src/`, `docs/`, `examples/`, README, PLAN, CHANGELOG, LICENSE — no tests, no deploy kits, no state, no dotfiles, and a secret-pattern scan over the extracted tree came back empty). That cleanliness is now enforced instead of accidental: `scripts/check-pack.mjs` (new) asserts the manifest against an allowlist with required runtime files present and tests / `deploy/` / `scripts/` / CI config / runtime state / tarballs explicitly forbidden, and it runs as a check in the suite and as a gate step in the publish workflow. Proven both ways: green on the real tree, and red with every smuggled path named when `files` was doctored in a scratch copy. Two judgment calls, recorded: `test.mjs` stays **out** of the tarball (the suite is a repo gate; shipping 100 KB of tests adds surface, not safety) and `scripts/` stays out too (the load bench is a dev tool). `SECURITY.md` now ships **in** the tarball (21 files) — the security policy should travel with the code it describes.
|
|
32
|
+
- **CI upgraded in place, not duplicated** — a `ci.yml` already ran the suite on push/PR (Node 20 + 22, green on every push this week); rather than add a second workflow beside it, it gained the SHA pins and Node 24 in the matrix (20 / 22 / 24), so the suite's role as a gate now covers the Node the studio actually runs.
|
|
33
|
+
- **`engines` corrected to the evidence: Node ≥ 20** — the package claimed `>=18`, but nothing has ever run the suite on 18: CI covered 20/22 and the studio runs 24. A source scan found no API that requires anything newer than 18, but honesty is about evidence, not plausibility — the floor is now `>=20`, the lowest version with green CI runs behind it, and the README's "Node 18+" claims were corrected to match.
|
|
34
|
+
- **Zero dependencies, stated with evidence** — `npm audit` in a scratch copy (the repo carries no lockfile, for the same zero-dependency reason) reports **0 vulnerabilities across 0 dependencies**: there is no dependency tree to compromise. The current tree was also scanned for committed secrets (key blocks, cloud/GitHub/npm token shapes, credential assignments, long high-entropy literals in shipped code): clean; the only match anywhere is a deliberately-wrong fixture token in the test suite. Current tree only — history was not audited and is not claimed. `.gitignore` now also covers the hosted server's default data dir (`ghostbus-hosted/`) and `*.tgz`, so runtime state and pack artifacts can't be committed by accident.
|
|
35
|
+
- **Tests** — main suite **71 checks** (was 70): the pack-manifest allowlist runs as a suite check (skipping cleanly where no npm CLI exists, and gating hard in the publish workflow, where it always does). Serverless stays **12 checks**; both demos green. The pack check adds about a second to the suite.
|
|
36
|
+
- **Scope boundary, honestly** — no runtime code changed except the version constant; no API, storage, or behavior changes. The pinned actions are the v4 line the workflows already used — upgrading action *majors* (v7 exists) is a separate, testable change, not something to smuggle into a pinning round. Git history was not secret-scanned (see above), and the npm-CLI-at-latest decision is documented in the workflow rather than "fixed."
|
|
37
|
+
|
|
38
|
+
## 0.5.7 — 2026-10-11
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
- **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.
|
|
43
|
+
- **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.
|
|
44
|
+
- **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).
|
|
45
|
+
- **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.
|
|
46
|
+
- **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.
|
|
47
|
+
- **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.
|
|
48
|
+
- **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.
|
|
49
|
+
- **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.
|
|
50
|
+
|
|
3
51
|
## 0.5.6 — 2026-10-11
|
|
4
52
|
|
|
5
53
|
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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Agents today work alone in separate windows — when two of them need to cooperate, a human copy-pastes between them. GhostBus is the room they meet in instead: any MCP-capable agent (Claude, Cursor, Antigravity, Windsurf, or your own) joins one shared workspace where agents can find each other, message each other, hand off tasks, and share files and context — with a human approval gate and a full provenance log built in.
|
|
6
6
|
|
|
7
|
-
Zero dependencies. Node
|
|
7
|
+
Zero dependencies. Node 20+. MIT.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ Zero dependencies. Node 18+. MIT.
|
|
|
12
12
|
npm install ghostbus
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
GhostBus is published on npm as [`ghostbus`](https://www.npmjs.com/package/ghostbus) (v0.5.
|
|
15
|
+
GhostBus is published on npm as [`ghostbus`](https://www.npmjs.com/package/ghostbus) (v0.5.10) — zero dependencies, Node 20+. You can also run it straight from a clone of this repository, as the quick start below shows.
|
|
16
16
|
|
|
17
17
|
## Status
|
|
18
18
|
|
|
@@ -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 # 71 checks: core, tokens, capsules, dependencies, sync, persistence, concurrency, MCP stdio, HTTP, SSE, CLI, hosted security, hostile input, input fuzz, load & resilience, token lifecycle, pack manifest
|
|
27
27
|
node examples/three-agent-team.mjs # planner + builder + reviewer, no human relaying
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -58,7 +58,7 @@ GHOSTBUS_ADMIN_KEY=pick-a-long-secret node src/hosted-server.mjs --port 8388
|
|
|
58
58
|
|
|
59
59
|
One process serves many **isolated** workspaces under `/w/<id>/` — each with its own key (issued once at creation, hash-only in the registry), board, REST, MCP, SSE and probe. The admin API creates/lists/deletes workspaces, **rotates keys** (old key dies instantly), and **exports/imports** full workspace state for backup and host migration — imports always mint a fresh key. A Dockerfile and a serverless Netlify/Blobs pack are included. Operator guide: `docs/hosted.md`.
|
|
60
60
|
|
|
61
|
-
Hosted mode is the deployment that serves strangers, so it is hardened by default (v0.5.2): per-agent tokens are **enforced** in every workspace (`GHOSTBUS_REQUIRE_TOKENS=0` opts out, loudly), the admin key must be at least 16 characters, and any public deployment belongs behind HTTPS. Details: the "Security & deployment hardening" section of `docs/hosted.md`.
|
|
61
|
+
Hosted mode is the deployment that serves strangers, so it is hardened by default (v0.5.2): per-agent tokens are **enforced** in every workspace (`GHOSTBUS_REQUIRE_TOKENS=0` opts out, loudly), the admin key must be at least 16 characters, and any public deployment belongs behind HTTPS. Details: the "Security & deployment hardening" section of `docs/hosted.md`. The consolidated security posture — controls, measured load limits, and the known limitations stated plainly — is `docs/security.md`.
|
|
62
62
|
|
|
63
63
|
Same tools over `POST /mcp`, plus a REST mirror under `/api/*`. `/health` and a counts-only `/probe` stay open for monitoring and wake hooks. A **live web board** is served at `/`, and agents can subscribe to **pushed events** over SSE (`GET /api/stream?agent=<name>`) instead of polling. Set `GHOSTBUS_REQUIRE_TOKENS=1` to enforce the per-agent tokens issued at registration. There's also a terminal CLI (`src/cli.mjs`) and full client setup docs in `docs/client-setup.md`.
|
|
64
64
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Only the **latest release** of GhostBus is supported with security fixes.
|
|
6
|
+
If you are running an older version, upgrade first — the fix will land on
|
|
7
|
+
the newest release, not on backports.
|
|
8
|
+
|
|
9
|
+
## Reporting a vulnerability
|
|
10
|
+
|
|
11
|
+
Email **gdev6145@gmail.com** with:
|
|
12
|
+
|
|
13
|
+
- what you found and where (file, endpoint, or hosted route),
|
|
14
|
+
- the version you tested,
|
|
15
|
+
- steps to reproduce, if you have them.
|
|
16
|
+
|
|
17
|
+
Please do not open a public issue for a security report, and give us a
|
|
18
|
+
chance to fix it before disclosing publicly.
|
|
19
|
+
|
|
20
|
+
**What to expect:** an acknowledgement within **one business day**, and a
|
|
21
|
+
plain-language answer about whether we can reproduce it and what happens
|
|
22
|
+
next. GhostBus is built by Ghost Developer Studio — a one-person studio —
|
|
23
|
+
so fixes are prioritized by real-world impact, but every report gets read
|
|
24
|
+
by the person who wrote the code.
|
|
25
|
+
|
|
26
|
+
## Credential lifecycle
|
|
27
|
+
|
|
28
|
+
GhostBus has three credential classes. None of them expires on a timer —
|
|
29
|
+
the lifecycle is *issue once, store hashed, rotate on suspicion* — and
|
|
30
|
+
this section is the honest description of that posture:
|
|
31
|
+
|
|
32
|
+
| credential | issued | stored | entropy |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| admin key | operator-chosen at startup | process env only | operator's choice; the hosted server and the serverless pack both refuse to start with one under 16 characters (use 32+ random hex) |
|
|
35
|
+
| workspace key | once, at workspace create / import / rotate | SHA-256 hash in the registry | 192 bits (`crypto.randomBytes(24)`) |
|
|
36
|
+
| agent token | once, at agent registration / rotation | SHA-256 hash in the workspace state | 192 bits (`crypto.randomBytes(24)`) |
|
|
37
|
+
|
|
38
|
+
**Rotation is immediate, and it is the incident response.** There is no
|
|
39
|
+
grace period and no second valid credential:
|
|
40
|
+
|
|
41
|
+
- *Workspace key:* `POST /api/workspaces/<id>/rotate-key` (admin key) —
|
|
42
|
+
the registry hash is replaced before the call returns (on the
|
|
43
|
+
serverless pack, before the next request), so the old key's very next
|
|
44
|
+
use fails. Agents keep working; only the workspace key they present
|
|
45
|
+
changes.
|
|
46
|
+
- *Agent token:* the `bus_rotate_token` MCP tool, or
|
|
47
|
+
`POST /w/<id>/api/agents/rotate-token` over REST with the current token
|
|
48
|
+
(header or body) — the hash in state is replaced before the reply, the
|
|
49
|
+
old token is dead immediately, and the rotation is recorded in the
|
|
50
|
+
provenance log (the log names the event; it never contains a token).
|
|
51
|
+
Rotation requires the current token on an enforced bus, so a leaked
|
|
52
|
+
token can be rotated away by its owner but a stranger who knows only
|
|
53
|
+
the agent's *name* cannot lock the owner out.
|
|
54
|
+
- *Admin key:* restart the server with a new one. Workspace keys and
|
|
55
|
+
agent tokens are unaffected — they are stored as hashes, not derived
|
|
56
|
+
from the admin key.
|
|
57
|
+
|
|
58
|
+
Because nothing expires, a credential that leaks and is never rotated
|
|
59
|
+
stays valid — that is the trade-off of this design, stated plainly.
|
|
60
|
+
Mitigations are the hashes at rest (a leaked state file or registry
|
|
61
|
+
exposes no usable credential, and an export envelope carries the same
|
|
62
|
+
hashes only — SHA-256 of a 192-bit random token is not a practical
|
|
63
|
+
offline brute-force target), timing-safe comparisons, failed-auth
|
|
64
|
+
throttling on the Node hosted server, and the rotation paths above.
|
|
65
|
+
If you suspect a leak: rotate first, then figure out how it happened.
|
|
66
|
+
|
|
67
|
+
## Authorization model (single scope, by design)
|
|
68
|
+
|
|
69
|
+
There is exactly one scope per credential, and this is a deliberate
|
|
70
|
+
pilot-stage design decision, not an oversight we plan to paper over:
|
|
71
|
+
|
|
72
|
+
- A **workspace key** is full access to its one workspace: read, write,
|
|
73
|
+
minting and rotating agent tokens included. It cannot reach any other
|
|
74
|
+
workspace (verified by test).
|
|
75
|
+
- An **agent token** proves "I am this agent" inside that workspace;
|
|
76
|
+
everything that agent may do, its token may do.
|
|
77
|
+
- The **admin key** is operator root: every workspace, plus create /
|
|
78
|
+
delete / rotate / export / import.
|
|
79
|
+
|
|
80
|
+
There are **no read-only keys** and no per-tool or per-agent scopes. For
|
|
81
|
+
the deployments GhostBus targets today — one operator handing a key to
|
|
82
|
+
one workspace's owner, whose agents are all theirs — a scope system
|
|
83
|
+
would be machinery without a customer. If you need a party to *observe*
|
|
84
|
+
a workspace without being able to act in it, the honest options today
|
|
85
|
+
are the counts-only `/probe`, or a periodic export they can read
|
|
86
|
+
offline. A scoped/read-only credential is a known future feature, not a
|
|
87
|
+
current claim.
|
|
88
|
+
|
|
89
|
+
## Supply chain
|
|
90
|
+
|
|
91
|
+
How the package you install is produced, and how to check it yourself:
|
|
92
|
+
|
|
93
|
+
- **Zero dependencies.** GhostBus has no runtime dependencies at all —
|
|
94
|
+
there is no dependency tree to compromise, substitute, or audit.
|
|
95
|
+
`npm audit` on an installed copy reports 0 vulnerabilities because
|
|
96
|
+
there is nothing to have one. (The repository carries no lockfile for
|
|
97
|
+
the same reason; `npm audit` needs one, so run it on an installed
|
|
98
|
+
copy, or in a scratch copy with `npm i --package-lock-only` first.)
|
|
99
|
+
- **No stored npm token.** Releases publish from GitHub Actions via npm
|
|
100
|
+
Trusted Publishing (OIDC): the workflow proves its identity to npm
|
|
101
|
+
for that one run, and no long-lived publish credential exists
|
|
102
|
+
anywhere to steal. The workflow runs in the protected `npm`
|
|
103
|
+
environment and holds the minimum permissions it needs
|
|
104
|
+
(`contents: read`, `id-token: write`).
|
|
105
|
+
- **Actions pinned by commit SHA.** The third-party GitHub Actions the
|
|
106
|
+
workflows use (`actions/checkout`, `actions/setup-node`) are pinned
|
|
107
|
+
to full commit SHAs with the resolved release noted in a comment —
|
|
108
|
+
a retagged or compromised tag cannot change what runs.
|
|
109
|
+
- **Provenance attestations.** Every release since v0.5.5 carries an
|
|
110
|
+
SLSA provenance attestation (predicate `https://slsa.dev/provenance/v1`)
|
|
111
|
+
and registry signatures, visible in the registry metadata
|
|
112
|
+
(`dist.attestations`) and as the provenance badge on the npm package
|
|
113
|
+
page. To verify an installed copy: `npm audit signatures`.
|
|
114
|
+
- **The tarball is allowlisted.** `scripts/check-pack.mjs` asserts the
|
|
115
|
+
exact shape of what publishes — runtime (`src/`), docs, examples,
|
|
116
|
+
and top-level project files only; tests, deploy kits, dev scripts,
|
|
117
|
+
CI config, and runtime state are forbidden — and it runs both in the
|
|
118
|
+
test suite and as a gate in the publish workflow. `SECURITY.md`
|
|
119
|
+
itself ships in the tarball so this page travels with the code.
|
|
120
|
+
|
|
121
|
+
The consolidated posture document — architecture and trust
|
|
122
|
+
boundaries, every control family with its evidence, measured load
|
|
123
|
+
limits, and the known limitations stated plainly — is
|
|
124
|
+
[`docs/security.md`](docs/security.md); it ships in the tarball too.
|
|
125
|
+
|
|
126
|
+
## Scope notes
|
|
127
|
+
|
|
128
|
+
- Self-hosted deployments are operated by whoever runs them; the operator
|
|
129
|
+
guidance for the hosted server (keys, HTTPS, throttling, backups) lives
|
|
130
|
+
in [`docs/hosted.md`](docs/hosted.md), in the "Security & deployment
|
|
131
|
+
hardening" section.
|
|
132
|
+
- Workspace keys and agent tokens are only ever stored as SHA-256 hashes;
|
|
133
|
+
a report based on reading your own data files is your deployment working
|
|
134
|
+
as designed, not a leak.
|
|
135
|
+
- The counts-only `/probe` and `/health` endpoints are intentionally
|
|
136
|
+
unauthenticated for monitoring; they expose no message, task, or file
|
|
137
|
+
content.
|
package/docs/hosted.md
CHANGED
|
@@ -18,7 +18,7 @@ Data layout in the data dir: `registry.json` (workspace metadata + key **hashes*
|
|
|
18
18
|
# create a workspace (returns its key ONCE)
|
|
19
19
|
curl -X POST localhost:8388/api/workspaces -H 'x-bus-key: <admin>' \
|
|
20
20
|
-d '{"id":"acme", "name":"Acme Team"}'
|
|
21
|
-
# -> {"id":"acme","name":"Acme Team","key":"<
|
|
21
|
+
# -> {"id":"acme","name":"Acme Team","key":"<48 hex>","keyNote":"..."}
|
|
22
22
|
curl localhost:8388/api/workspaces -H 'x-bus-key: <admin>' # list + counts
|
|
23
23
|
curl -X DELETE localhost:8388/api/workspaces/acme -H 'x-bus-key: <admin>'
|
|
24
24
|
```
|
|
@@ -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/docs/security.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# GhostBus security posture
|
|
2
|
+
|
|
3
|
+
This document is the consolidated security posture of GhostBus as of
|
|
4
|
+
**v0.5.10**: the architecture and its trust boundaries, the controls in
|
|
5
|
+
place (grouped by family, with the hardening round that introduced
|
|
6
|
+
each), the measured load evidence with its honest ceiling, and the
|
|
7
|
+
known limitations — stated, not hidden.
|
|
8
|
+
|
|
9
|
+
It is the *posture and evidence* document. The *policy* — how to report
|
|
10
|
+
a vulnerability, supported versions, the credential rotation runbook,
|
|
11
|
+
the authorization model — lives in [`SECURITY.md`](../SECURITY.md) at
|
|
12
|
+
the repo root, and operator deployment guidance lives in
|
|
13
|
+
[`docs/hosted.md`](hosted.md). Where this document and those disagree,
|
|
14
|
+
tell us; they are maintained to agree.
|
|
15
|
+
|
|
16
|
+
## Architecture & trust boundaries
|
|
17
|
+
|
|
18
|
+
GhostBus is one transport-agnostic core (`src/core.mjs`) — agents,
|
|
19
|
+
messages, tasks, shared files, context, capsules, and an append-only
|
|
20
|
+
provenance event log over a pluggable store — behind four transports
|
|
21
|
+
that share the core and, where applicable, the same request handler:
|
|
22
|
+
|
|
23
|
+
| surface | process | intended exposure |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| MCP over stdio (`src/mcp-server.mjs`) | one per client machine | local only — the operator's machine is the boundary |
|
|
26
|
+
| HTTP relay (`src/http-server.mjs`) | single workspace | one team's workspace, key-protected |
|
|
27
|
+
| Hosted server (`src/hosted-server.mjs`) | many isolated workspaces | the public/paid surface; strictest defaults |
|
|
28
|
+
| Serverless pack (`deploy/netlify/`) | per-request functions over a KV | same hosted semantics on Netlify Blobs |
|
|
29
|
+
|
|
30
|
+
Three credential classes mark the boundaries (full lifecycle in
|
|
31
|
+
`SECURITY.md`): the **admin key** (operator root, hosted surfaces
|
|
32
|
+
only), one **workspace key** per workspace (full access to that one
|
|
33
|
+
workspace, nothing else), and **per-agent tokens** (prove "I am this
|
|
34
|
+
agent" inside a workspace). Keys and tokens exist at rest only as
|
|
35
|
+
SHA-256 hashes.
|
|
36
|
+
|
|
37
|
+
Intentionally open, on every HTTP surface: `/health`, a counts-only
|
|
38
|
+
`/probe` (workspace id, queue/agent counts, version — no content), and
|
|
39
|
+
the static board page shell (the page itself fetches data with the
|
|
40
|
+
workspace key). Everything else requires the workspace key; the hosted
|
|
41
|
+
admin API additionally requires the admin key.
|
|
42
|
+
|
|
43
|
+
## Controls, by family
|
|
44
|
+
|
|
45
|
+
Eight hardening rounds ran between v0.5.2 and v0.5.9. The CHANGELOG
|
|
46
|
+
carries the per-round detail; this is the same ground grouped the way
|
|
47
|
+
an evaluator reads it.
|
|
48
|
+
|
|
49
|
+
### Authentication & authorization
|
|
50
|
+
|
|
51
|
+
- Per-agent token enforcement is the **default** on the hosted server
|
|
52
|
+
and the serverless pack (v0.5.2/v0.5.7); opting out requires an
|
|
53
|
+
explicit env var and prints a loud startup warning. The admin key has
|
|
54
|
+
a 16-character startup floor on both hosted surfaces.
|
|
55
|
+
- All secret comparisons are timing-safe (`crypto.timingSafeEqual`
|
|
56
|
+
over SHA-256 digests) — workspace keys, agent tokens, admin key, on
|
|
57
|
+
every surface including the serverless pack (v0.5.3).
|
|
58
|
+
- Rotation is immediate and is the incident response: workspace-key
|
|
59
|
+
rotation replaces the registry hash before the call returns (the old
|
|
60
|
+
key's next request fails — pinned by test on both hosted surfaces);
|
|
61
|
+
agent tokens rotate via MCP or plain REST with the current token as
|
|
62
|
+
the authorizer (v0.5.7). Nothing expires on a timer — the trade-off
|
|
63
|
+
and mitigations are stated in `SECURITY.md`.
|
|
64
|
+
- Authorization is single-scope by design: a workspace key is full
|
|
65
|
+
access to its one workspace, an agent token is that agent, the admin
|
|
66
|
+
key is root. There are no read-only keys. The reasoning — and the
|
|
67
|
+
honest alternatives (`/probe`, offline exports) — is in `SECURITY.md`.
|
|
68
|
+
- Workspaces are isolated by construction (separate stores, separate
|
|
69
|
+
keys) and the isolation is tested, including cross-workspace key
|
|
70
|
+
refusal over real HTTP.
|
|
71
|
+
|
|
72
|
+
### Input validation
|
|
73
|
+
|
|
74
|
+
- Workspace ids are validated at every boundary — create, import,
|
|
75
|
+
route matching, workspace resolution, and the registry file itself
|
|
76
|
+
when loaded — because an id becomes a file name inside the data dir;
|
|
77
|
+
a planted or hand-edited registry entry cannot turn one into a path
|
|
78
|
+
outside it (v0.5.4).
|
|
79
|
+
- Shared-file names are state keys, never filesystem paths. The three
|
|
80
|
+
names that are special to a JavaScript object (`__proto__`,
|
|
81
|
+
`constructor`, `prototype`) are refused at write, treated as absent
|
|
82
|
+
at read, and refused inside snapshots (v0.5.4).
|
|
83
|
+
- Snapshots/imports are deeply validated before they land: collection
|
|
84
|
+
entries must be usable objects, agents need string names, files need
|
|
85
|
+
string text, counters finite and non-negative, counts sanity-capped.
|
|
86
|
+
A state that would crash the bus on first use is refused at the
|
|
87
|
+
door; every export produced by v0.5.0–v0.5.3 still imports (v0.5.4).
|
|
88
|
+
- Prototype pollution is not reachable: state enters only through
|
|
89
|
+
`JSON.parse`, JSON round-trip cloning, and object spread (all
|
|
90
|
+
own-property semantics); there is no recursive merge and no
|
|
91
|
+
`Object.assign` of caller data anywhere in the codebase. Canary tests
|
|
92
|
+
attempt it anyway, through the context writer, restore payloads, and
|
|
93
|
+
tool arguments (v0.5.4/v0.5.5).
|
|
94
|
+
- MCP tool arguments are validated against each tool's declared schema
|
|
95
|
+
at the tools boundary: wrong-typed values are rejected with a clean
|
|
96
|
+
error result, never coerced into shared state (an object message
|
|
97
|
+
body was previously stored as the literal `"[object Object]"`), and
|
|
98
|
+
malformed JSON-RPC frames can never kill the stdio server process
|
|
99
|
+
(v0.5.5).
|
|
100
|
+
- Numeric parameters are clamped in the core: garbage, negative, or
|
|
101
|
+
absurd `limit`/`sinceSeq`/timeout values fall back to defaults and
|
|
102
|
+
documented caps instead of slice-arithmetic surprises — `limit=0`
|
|
103
|
+
once returned *everything*, and a garbage sync cursor once reported
|
|
104
|
+
an agent as caught up while replaying nothing (v0.5.5).
|
|
105
|
+
|
|
106
|
+
### Transport & headers
|
|
107
|
+
|
|
108
|
+
- HTML pages (board, hosted landing) answer with `X-Frame-Options:
|
|
109
|
+
DENY` and a Content-Security-Policy (`default-src 'self'`,
|
|
110
|
+
`frame-ancestors 'none'`, `object-src 'none'`); JSON answers carry
|
|
111
|
+
`nosniff` and `Referrer-Policy: no-referrer` (v0.5.3).
|
|
112
|
+
- The board page escapes every interpolated value, keeps the workspace
|
|
113
|
+
key in local storage, and sends it only as a header — never in a
|
|
114
|
+
URL, where it would land in logs and referers (v0.5.5).
|
|
115
|
+
- The servers speak plain HTTP; **HTTPS is required for any public
|
|
116
|
+
deployment**, terminated in front (reverse proxy or a *named* tunnel
|
|
117
|
+
with a stable hostname). A quick tunnel is explicitly not for paid
|
|
118
|
+
use. See `docs/hosted.md`.
|
|
119
|
+
|
|
120
|
+
### Resource limits & abuse resistance
|
|
121
|
+
|
|
122
|
+
- JSON request bodies are capped at 1 MB on every Node HTTP route;
|
|
123
|
+
over the cap is a clean `413` (v0.5.3). (The serverless pack relies
|
|
124
|
+
on platform payload limits instead — documented divergence, below.)
|
|
125
|
+
- Failed authentication is throttled at the hosted layer: 30 failed
|
|
126
|
+
attempts per minute per IP gets `429`s on the admin surface and
|
|
127
|
+
workspace routes; successful auth never counts (v0.5.3). A per-IP,
|
|
128
|
+
per-workspace request limiter (300/min, tunable) covers traffic
|
|
129
|
+
inside a workspace.
|
|
130
|
+
- Both Node servers enforce explicit connection deadlines (headers
|
|
131
|
+
10 s, declared body 30 s, keep-alive 5 s; env-tunable) via a guard
|
|
132
|
+
that only destroys genuinely incomplete requests — long-lived SSE
|
|
133
|
+
streams and long-polls are never bounded by them (v0.5.6).
|
|
134
|
+
- SSE subscriptions are capped (100 per workspace, 1000 per server,
|
|
135
|
+
clean `429`), with idempotent cleanup so churned clients leak
|
|
136
|
+
neither listeners nor cap slots (v0.5.6).
|
|
137
|
+
- Error hygiene: the servers log the real error to stderr and answer a
|
|
138
|
+
generic `internal error`; deliberate validation/auth errors (400s)
|
|
139
|
+
are client-facing by design (v0.5.3). Admin actions on the hosted
|
|
140
|
+
surfaces write one structured audit line (timestamp, action,
|
|
141
|
+
workspace id, source IP — never a key or hash) (v0.5.3/v0.5.7).
|
|
142
|
+
- Data files (registry, workspace state) are written owner-only
|
|
143
|
+
(0600) through atomic temp-file + rename writes (v0.5.3).
|
|
144
|
+
|
|
145
|
+
### Durability & state integrity
|
|
146
|
+
|
|
147
|
+
- Every mutation runs inside a per-instance serialized
|
|
148
|
+
load → mutate → save cycle (`_serialized`, v0.5.1), so concurrent
|
|
149
|
+
operations on one bus cannot silently lose each other's updates;
|
|
150
|
+
the suite proves it with concurrent-writer reconciliation.
|
|
151
|
+
- The kill drill is a suite check, not a claim: the hosted server is
|
|
152
|
+
SIGKILLed mid-load and restarted on the same data directory — the
|
|
153
|
+
state file parses (atomic rename), every HTTP-200-acknowledged
|
|
154
|
+
operation is present, and each agent's stored messages form a
|
|
155
|
+
contiguous prefix of its attempts (v0.5.6).
|
|
156
|
+
- History trims bound state growth (10k messages, 5k events, 10
|
|
157
|
+
versions per file), and exports/imports give portable, hash-only
|
|
158
|
+
backups (v0.5.0).
|
|
159
|
+
|
|
160
|
+
### Load evidence (summary)
|
|
161
|
+
|
|
162
|
+
`scripts/load-bench.mjs` boots the real hosted server with token
|
|
163
|
+
enforcement and drives concurrent agents through mixed operations,
|
|
164
|
+
then reconciles the final state **exactly** (sent == stored, created
|
|
165
|
+
== completed, put == stored). On the development machine (Node 24):
|
|
166
|
+
25 agents × 30 s → 2,506 ops, ~83 ops/s, **0 errors**, exact
|
|
167
|
+
reconciliation; 25 agents × 60 s → 3,438 ops, ~57 ops/s, 0 errors.
|
|
168
|
+
The full table, the memory behavior, and the reading of the numbers
|
|
169
|
+
live in `docs/hosted.md` ("Capacity & limits"). These are
|
|
170
|
+
one-machine figures about the *shape* of the limits, not an SLA.
|
|
171
|
+
|
|
172
|
+
### Supply chain
|
|
173
|
+
|
|
174
|
+
- **Zero runtime dependencies** — there is no dependency tree to
|
|
175
|
+
compromise; `npm audit` reports 0 vulnerabilities across 0
|
|
176
|
+
dependencies (v0.5.8).
|
|
177
|
+
- Releases publish from GitHub Actions via npm **Trusted Publishing**
|
|
178
|
+
(OIDC): no stored npm token exists anywhere to steal; the workflow
|
|
179
|
+
holds least-privilege permissions (`contents: read`,
|
|
180
|
+
`id-token: write`) in the protected `npm` environment (v0.5.8).
|
|
181
|
+
- The third-party actions used are **pinned to full commit SHAs**
|
|
182
|
+
(v0.5.8). Releases carry **SLSA provenance attestations** and
|
|
183
|
+
registry signatures — verify an installed copy with
|
|
184
|
+
`npm audit signatures`.
|
|
185
|
+
- The published tarball is allowlisted and gated: `scripts/check-pack.mjs`
|
|
186
|
+
asserts its exact shape in the suite and in the publish workflow;
|
|
187
|
+
tests, deploy kits, dev scripts, CI config, and runtime state are
|
|
188
|
+
forbidden from shipping (v0.5.8). This document and `SECURITY.md`
|
|
189
|
+
both ship in the tarball.
|
|
190
|
+
|
|
191
|
+
### Serverless parity
|
|
192
|
+
|
|
193
|
+
The Netlify pack shares the core but keeps its own routing layer, so
|
|
194
|
+
parity is a maintained property, not an assumption: enforcement
|
|
195
|
+
default, the admin-key floor, rotate/export/import, REST agent-token
|
|
196
|
+
rotation, timing-safe compares, and the shared core's validation and
|
|
197
|
+
clamps were audited and aligned in v0.5.7, and the lib test (12
|
|
198
|
+
checks) holds the line. Two divergences remain, documented rather
|
|
199
|
+
than silent: no application-level failed-auth throttle (stateless
|
|
200
|
+
per request; the platform traffic layer, 192-bit keys, and
|
|
201
|
+
timing-safe compares carry it) and platform-bounded request bodies
|
|
202
|
+
instead of the app-level 1 MB cap.
|
|
203
|
+
|
|
204
|
+
### The final sweep (v0.5.9)
|
|
205
|
+
|
|
206
|
+
Round 8 re-audited the whole surface fresh and re-ran the accumulated
|
|
207
|
+
hostile-input battery against the current code on all five surfaces —
|
|
208
|
+
a 71-probe run covering, among others: no-key/wrong-key/cross-workspace
|
|
209
|
+
refusals on relay, hosted, and serverless; traversal ids at create,
|
|
210
|
+
import, route, and in a planted registry file; reserved file names and
|
|
211
|
+
path-like file reads; `__proto__` payloads through the context writer,
|
|
212
|
+
snapshots, and imports, with `Object.prototype` canaries; `agents:
|
|
213
|
+
[null]` and `workspace: null` import refusals; MCP hostile frames
|
|
214
|
+
(null/string/number/array/missing-params) and wrong-typed arguments on
|
|
215
|
+
stdio and HTTP; limit/cursor/timeout edge values; the 1 MB body cap;
|
|
216
|
+
rotation immediacy for workspace keys and agent tokens on both hosted
|
|
217
|
+
surfaces; and the short-admin-key startup refusal. **Outcome: no new
|
|
218
|
+
vulnerability found and no code change required** — every probe landed
|
|
219
|
+
as the earlier rounds designed (one probe's own expectation was
|
|
220
|
+
corrected in the run: a `tools/call` frame without `params` over HTTP
|
|
221
|
+
answers a clean JSON-RPC `isError` result, the codebase's convention,
|
|
222
|
+
rather than an HTTP 400). The round's code-adjacent work was
|
|
223
|
+
documentation: this posture document, and three stale claims corrected
|
|
224
|
+
(the README's install version, a key-length example in
|
|
225
|
+
`docs/hosted.md`, on top of round 7's `engines` correction).
|
|
226
|
+
|
|
227
|
+
## Known limitations — stated, not hidden
|
|
228
|
+
|
|
229
|
+
- **Single-file JSON store.** State is one JSON document per
|
|
230
|
+
workspace, fully loaded and re-saved per operation: per-operation
|
|
231
|
+
cost is O(state size), throughput declines as a workspace grows
|
|
232
|
+
(measured above), and concurrent writers coordinate at
|
|
233
|
+
single-operation granularity (last writer wins a colliding op).
|
|
234
|
+
Integrity within that model is proven (atomic rename, kill drill,
|
|
235
|
+
exact reconciliation under load); it is not a transactional,
|
|
236
|
+
multi-writer database. The networked store is **future work by the
|
|
237
|
+
owner's decision** — deferred, designed as an optional adapter, not
|
|
238
|
+
a current claim and not faked anywhere in the code or these docs.
|
|
239
|
+
- **Single process, no high availability.** The hosted server is one
|
|
240
|
+
Node process. If it dies, workspaces are unavailable until it
|
|
241
|
+
restarts (state on disk is intact). There is no clustering,
|
|
242
|
+
failover, or replication.
|
|
243
|
+
- **No credential expiry.** Keys and tokens do not expire on a timer;
|
|
244
|
+
rotation is the revocation mechanism. A leaked credential that is
|
|
245
|
+
never rotated stays valid. The posture, runbook, and mitigations
|
|
246
|
+
are in `SECURITY.md` ("Credential lifecycle").
|
|
247
|
+
- **Single-scope authorization.** No read-only or per-tool scoped
|
|
248
|
+
credentials exist (see above and `SECURITY.md`).
|
|
249
|
+
- **No uptime SLA in the pilot.** The hosted pilot is honestly run by
|
|
250
|
+
a one-person studio: scheduled export backups and self-export
|
|
251
|
+
anytime, data never locked in, and no SLA yet. That wording is the
|
|
252
|
+
offer, not fine print.
|
|
253
|
+
- **Serverless divergences.** The two documented above (no app-level
|
|
254
|
+
failed-auth throttle; platform-bounded bodies), plus the functional
|
|
255
|
+
ones: no board page, no SSE, no long-poll on the serverless pack,
|
|
256
|
+
and per-request fresh-bus semantics where concurrent writes to the
|
|
257
|
+
same workspace can race at single-operation granularity.
|
|
258
|
+
|
|
259
|
+
## The five steps, mapped
|
|
260
|
+
|
|
261
|
+
An external review (ChatGPT, source inspection, Oct 11 2026 — 7.4/10
|
|
262
|
+
overall, security maturity 5.5) named five steps toward 9/10. The
|
|
263
|
+
hardening loop's honest accounting:
|
|
264
|
+
|
|
265
|
+
| # | Step | Status after 8 rounds | Evidence |
|
|
266
|
+
|---|---|---|---|
|
|
267
|
+
| 1 | Harden the security model | **Done** — enforcement defaults, timing-safe compares, throttles, caps, deep input validation, MCP boundary validation, token lifecycle + rotation, serverless parity, supply-chain pinning; final sweep clean | This document; CHANGELOG v0.5.2–v0.5.9; suites 71 + 12 checks |
|
|
268
|
+
| 2 | Upgrade persistence to a transactional DB | **Deferred by owner decision** — not built; the JSON store's integrity properties are proven and its ceiling is measured and documented, but it is not transactional and not multi-writer | "Known limitations" above; `docs/hosted.md` capacity section |
|
|
269
|
+
| 3 | Intelligent orchestration | **Not a hardening-loop item** — the bus's existing gates (approval-required tasks, `blockedBy` dependencies, exclusive claim leases) are product features that predate the loop; no orchestration claim is made or implied by these rounds | README "How work flows"; PLAN.md |
|
|
270
|
+
| 4 | Connect the coding lifecycle | **Not addressed** — product roadmap, outside the security loop's scope; nothing in these rounds should be read as progress on it | PLAN.md |
|
|
271
|
+
| 5 | Prove it under load | **Done** — repeatable bench, 0 errors and exact reconciliation at 25 concurrent agents, timeout/SSE abuse limits, SIGKILL recovery drill in the suite | `docs/hosted.md` "Capacity & limits"; CHANGELOG v0.5.6 |
|
|
272
|
+
|
|
273
|
+
Steps 1 and 5 were the loop's to close and are closed with evidence.
|
|
274
|
+
Step 2 is a recorded owner decision. Steps 3 and 4 were never security
|
|
275
|
+
work and are listed so the mapping doesn't quietly claim them.
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"description": "Agent-to-agent message bus and shared workspace, exposed as an MCP server. Zero dependencies.",
|
|
9
9
|
"engines": {
|
|
10
|
-
"node": ">=
|
|
10
|
+
"node": ">=20"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"src",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"README.md",
|
|
17
17
|
"PLAN.md",
|
|
18
18
|
"CHANGELOG.md",
|
|
19
|
+
"SECURITY.md",
|
|
19
20
|
"LICENSE"
|
|
20
21
|
],
|
|
21
22
|
"homepage": "https://github.com/littlestjames82-sys/ghostbus#readme",
|
|
@@ -40,9 +41,10 @@
|
|
|
40
41
|
"relay": "node src/http-server.mjs",
|
|
41
42
|
"start": "node src/mcp-server.mjs",
|
|
42
43
|
"test": "node test.mjs",
|
|
44
|
+
"check-pack": "node scripts/check-pack.mjs",
|
|
43
45
|
"test:serverless": "node deploy/netlify/test-lib.mjs",
|
|
44
46
|
"hosted": "node src/hosted-server.mjs"
|
|
45
47
|
},
|
|
46
48
|
"type": "module",
|
|
47
|
-
"version": "0.5.
|
|
49
|
+
"version": "0.5.10"
|
|
48
50
|
}
|
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.10';
|
|
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';
|