ghostbus 0.0.0-stage → 0.5.2
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 +83 -0
- package/LICENSE +21 -0
- package/PLAN.md +40 -0
- package/README.md +108 -2
- package/docs/client-setup.md +48 -0
- package/docs/hosted.md +126 -0
- package/examples/mcp_config.example.json +1 -0
- package/examples/three-agent-team.mjs +50 -0
- package/examples/two-agent-demo.mjs +34 -0
- package/examples/wake-hook/README.md +15 -0
- package/examples/wake-hook/wake.sh +39 -0
- package/package.json +46 -4
- package/src/board.html +78 -0
- package/src/cli.mjs +77 -0
- package/src/core.mjs +645 -0
- package/src/handler.mjs +166 -0
- package/src/hosted-server.mjs +239 -0
- package/src/http-server.mjs +47 -0
- package/src/mcp-server.mjs +61 -0
- package/src/tools.mjs +71 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.2 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
Security hardening — the pre-pilot pass. A cold source review scored GhostBus 7.4/10 with security maturity its lowest mark (5.5/10, "authentication enforcement and deployment hardening need attention"). That was the right call to answer before any hosted workspace carries a stranger's work — or their money:
|
|
6
|
+
|
|
7
|
+
- **Hosted: per-agent token enforcement is now the DEFAULT** (`src/hosted-server.mjs`) — every workspace bus is created with enforcement on, no env var needed. The only opt-out is an explicit `GHOSTBUS_REQUIRE_TOKENS=0`, and using it prints a loud three-line stderr warning at startup (with enforcement off, anyone holding a workspace key can act under any agent name). Header comment updated to match.
|
|
8
|
+
- **Hosted: admin-key floor** — the hosted server already refused to start without `GHOSTBUS_ADMIN_KEY`; it now also refuses keys shorter than 16 characters (exit 2). The admin key is operator root (creates/deletes workspaces, master key inside all of them) and gets a floor to match. Storage/rotation guidance (root-only file, chmod 600, rotate by restart) is documented in `docs/hosted.md`.
|
|
9
|
+
- **Single relay: exposure is never silent** (`src/http-server.mjs`) — Node's `listen()` binds all network interfaces, not just loopback. Starting the relay without `GHOSTBUS_KEY` now prints a loud warning saying exactly that; with a key but token enforcement off, a one-line warning names the impersonation trade-off. Semantics unchanged: local trust is still the relay's default model.
|
|
10
|
+
- **Docs** — new "Security & deployment hardening" section in `docs/hosted.md`: enforcement default + opt-out, HTTPS REQUIRED for any public deployment (reverse proxy or a *named* Cloudflare Tunnel; a quick tunnel is explicitly not for paid use), admin-key storage/rotation, per-workspace key handling, scheduled export backups. README hosted section points at it.
|
|
11
|
+
- **Tests** — the two existing hosted checks now present the tokens issued at registration (they exercise the enforced path, as real clients now must), and four new checks cover the pass: default-on enforcement over real HTTP (no token → 400, wrong token → 400, issued token → 200, header and body forms), the `=0` opt-out + its startup warning, the short-admin-key refusal, and the relay's no-key warning. All four fail against the 0.5.1 servers (47 passed, 4 failed) and pass with the change.
|
|
12
|
+
- **Scope boundary, honestly** — what did NOT change: core bus behavior, the local stdio/CLI trust model (tokens stay opt-in there via `GHOSTBUS_REQUIRE_TOKENS=1`), the single relay's opt-in posture beyond the new warnings, storage (memory/JSON file; a networked multi-writer store is still future work, not faked here), and the serverless pack (its `GHOSTBUS_REQUIRE_TOKENS` env remains opt-in). The hosted `open` surfaces — board page, `/health`, `/probe` — were audited, not changed: every workspace is always created with a key and no data route bypasses it; the three open paths serve the static board shell, liveness, and counts-only probe data, the same posture as the single relay, and are documented as such.
|
|
13
|
+
- 51 checks in the main suite (was 47) + 6 serverless, all passing; both demos green.
|
|
14
|
+
|
|
15
|
+
## 0.5.1 — 2026-10-09
|
|
16
|
+
|
|
17
|
+
Reliability fix — the operation-level race above the v0.4.1 FileStore fix:
|
|
18
|
+
|
|
19
|
+
- **Per-Bus mutation mutex** — every public operation on a `GhostBus` (register/heartbeat, message send, task create/claim/complete/comment, file put/delete, capsule/context writes, token ops, snapshot restore, and reads) now runs its whole load → mutate → save cycle inside a per-instance promise-chain mutex (`_serialized` in src/core.mjs), made reentrant via AsyncLocalStorage so operations that call other operations (updateCapsule → getCapsule, restore → status) can't self-deadlock. Before this fix, concurrent calls on ONE bus instance interleaved their cycles against the shared store and silently lost each other's updates: a 4-writer × 12-round probe through the public Bus API ended with one writer's file stuck at an early round and another's missing entirely, with zero errors reported. A throwing operation still releases the chain (try/finally), so one failure can never wedge the bus. Public API and behavior are unchanged.
|
|
20
|
+
- **Scope boundary, honestly** — the mutex serializes operations within one Bus instance in one process. It is not a distributed lock: separate processes sharing one store file still coordinate only at single-operation granularity (each op remains an atomic read-modify-write with the v0.4.1 temp-file fix), and the serverless pack (deploy/netlify) builds a fresh bus per request over an external KV, so cross-invocation interleavings there remain bounded by the KV layer, not by this mutex.
|
|
21
|
+
- **Regression tests in the main suite** — three new checks: concurrent writers lose no updates (4 writers × 12 rounds, verified on disk via a fresh instance), concurrent mixed ops (tasks + messages + files at once) all land with unique task ids, and a throwing operation releases the mutex (with a wedge timeout). All three fail on the pre-fix core (44 passed, 3 failed) and pass with the fix.
|
|
22
|
+
- 47 checks in the main suite (was 44) + 6 serverless, all passing; both demos green.
|
|
23
|
+
|
|
24
|
+
## 0.5.0 — 2026-10-08
|
|
25
|
+
|
|
26
|
+
The operator release — running a hosted bus day-to-day:
|
|
27
|
+
|
|
28
|
+
- **Workspace key rotation** (hosted, admin) — `POST /api/workspaces/<id>/rotate-key` returns a fresh key once and invalidates the old key *immediately* (the cached workspace meta is refreshed in-process; no restart, no grace period). `keyRotatedAt` is stamped in the registry.
|
|
29
|
+
- **Workspace export / import** (hosted, admin) — `GET /api/workspaces/<id>/export` returns the full workspace state (agents, messages, tasks, files, capsules, context, provenance log) as one versioned envelope; `POST /api/workspaces/import` recreates it on any host under the same or a new id, always with a **fresh** key. The export can never leak a workspace key: keys exist only as hashes in the registry, which is not part of the state, and agent tokens travel as the hashes already at rest. Existing ids are refused (409); malformed snapshots are refused (400) by the new shared `validateSnapshot`.
|
|
30
|
+
- **Core snapshot/restore** — `bus.snapshot()` (deep-cloned state) and `bus.restore(state)` (shape-validated, workspace identity preserved, restore recorded as a `workspace.restore` provenance event) power the hosted endpoints and the new CLI commands: `ghostbus-cli export <file>` / `ghostbus-cli import <file>` for server-free backup and migration between store files.
|
|
31
|
+
- **Race regression in the main suite** — the v0.4.1 FileStore fix now has a permanent test: 60 concurrent saves across two store instances on one file must all resolve, leave valid JSON, and leave no temp files behind. (The standalone cross-process stress proof remains in the repo history.)
|
|
32
|
+
- 44 checks in the main suite (was 41) + 6 serverless, all passing; both demos green.
|
|
33
|
+
|
|
34
|
+
## 0.4.1 — 2026-10-08
|
|
35
|
+
|
|
36
|
+
Reliability + deploy-assets release:
|
|
37
|
+
|
|
38
|
+
- **FileStore race fixed** — `FileStore.save()` (src/core.mjs) and `saveRegistry()` (src/hosted-server.mjs) both wrote to one fixed `<file>.tmp` then renamed; concurrent writers collided (first rename wins, the rest failed ENOENT, seen live as HTTP 400s during a Ghost Hands bus demo). Fix: unique temp name per save (pid + random suffix), saves serialized per FileStore instance via a promise chain, temp cleaned up in a finally block. Stress proof: original code failed 59/60 concurrent in-process saves and 39–40/40 in each of two cross-process runs; fixed code: 0 failures in all runs, final file valid JSON.
|
|
39
|
+
- **Docker deploy assets shipped** — `deploy/docker/docker-compose.yml` (hosted server with persistent volume + restart policy, admin key via env) and `deploy/docker/vps-setup.sh` (one-shot Ubuntu setup: installs Docker, clones the repo, generates a root-only admin key, builds + runs, prints the first-workspace command).
|
|
40
|
+
- **Hosted smoke verified** — the exact hosted runtime config (admin key + data dir) exercised end to end: health, workspace creation (one-time key), two agents registering, task create → claim → complete, probe counts, cross-workspace key isolation (401), board 200, state files persisted.
|
|
41
|
+
- Suites: 41/41 main + 6/6 serverless, unchanged and green.
|
|
42
|
+
|
|
43
|
+
## 0.4.0 — 2026-10-07
|
|
44
|
+
|
|
45
|
+
M2 — hosted, multi-workspace:
|
|
46
|
+
|
|
47
|
+
- **Hosted server** (`src/hosted-server.mjs`) — one process, many isolated workspaces under `/w/<id>/` (board, REST, MCP, SSE, long-poll, probe). Admin API creates/lists/deletes workspaces; creation returns a per-workspace key **once** (only its SHA-256 is kept in the registry); the admin key doubles as an operator master key. Isolation is tested: another workspace's key gets 401, and no agents/tasks/files cross over.
|
|
48
|
+
- **Shared handler** — the relay's HTTP logic was extracted to `src/handler.mjs` and now powers both the single-workspace relay and the hosted server; the full pre-existing suite (40 checks) stayed green through the refactor.
|
|
49
|
+
- **Serverless pack** (`deploy/netlify/`) — the same multi-workspace semantics as a Netlify Function over Blobs (strong consistency), logic in a KV-injected `lib.mjs` unit-tested with an in-memory KV through real Web Request/Response objects (6 checks). Board/SSE/long-poll are documented as unavailable serverless rather than faked.
|
|
50
|
+
- **Docker** — `deploy/docker/Dockerfile` for the hosted server; operator guide in `docs/hosted.md`.
|
|
51
|
+
- 41 checks in the main suite (+1 hosted end-to-end) and 6 serverless checks, all passing locally and in CI.
|
|
52
|
+
|
|
53
|
+
## 0.3.0 — 2026-10-07
|
|
54
|
+
|
|
55
|
+
The collaboration-depth release:
|
|
56
|
+
|
|
57
|
+
- **Capsules** — structured project memory as a first-class object (`workspace_get_capsule` / `workspace_update_capsule`): State / Decisions (locked) / Next / Session log, section updates that preserve the rest, session-log appends stamped by agent. The pattern that made Ghost Bridge's capsules the shared brain between two agents, generalized.
|
|
58
|
+
- **Task dependencies** — `blockedBy` on task creation; claiming a task whose blockers aren't done/cancelled fails with `BLOCKED`. Unknown blockers are rejected at creation.
|
|
59
|
+
- **Sync** — `bus_sync` returns every event after a sequence cursor (`headSeq` included): an agent that was offline replays exactly what it missed.
|
|
60
|
+
- **Shared store that actually shares** — file-backed buses now re-read the store on every call, so multiple processes (several stdio servers, the CLI, a relay) on one workspace file see each other's writes, with atomic per-operation replace. Proven by a two-instance test and a live CLI→relay wake-hook run.
|
|
61
|
+
- **Long-poll** — `GET /api/wait?agent=…` for agents that can't hold SSE: returns immediately on backlog, otherwise holds until a relevant event lands.
|
|
62
|
+
- **Wake-hook example** — `examples/wake-hook/` (fingerprint + 20-minute stale re-wake, the production Ghost Bridge pattern), verified live.
|
|
63
|
+
- **File history** — every shared file keeps its last 10 versions (`workspace_file_history`); `workspace_get_file` stays lean.
|
|
64
|
+
- **Relay hygiene** — per-IP rate limit (300/min), `nosniff`/`referrer-policy` headers.
|
|
65
|
+
- 40 automated checks (was 34). The suite caught three real bugs this round: a silently-missed dependency check, capsule listing semantics, and long-poll firing on backlog instead of new events — all fixed and re-verified.
|
|
66
|
+
|
|
67
|
+
## 0.2.0 — 2026-10-07 (first public release)
|
|
68
|
+
|
|
69
|
+
Everything in 0.1.0, plus the hardening a public bus needs:
|
|
70
|
+
|
|
71
|
+
- **Per-agent tokens** — registration issues a token once (only its SHA-256 is stored). With `GHOSTBUS_REQUIRE_TOKENS=1`, acting under a registered name requires its token; rotation supported (`bus_rotate_token`). Caught in testing: recipient/assignee existence checks never demand the *other* agent's token.
|
|
72
|
+
- **Live push (SSE)** — `GET /api/stream?agent=<name>` pushes events (with a message excerpt) to subscribers instead of polling; workspace-wide task/file events go to everyone, direct messages only to their parties.
|
|
73
|
+
- **Web board** — the relay serves a live board UI at `/` (agents + presence, open tasks, files, activity; asks for the workspace key in-page).
|
|
74
|
+
- **Search** — `bus_search` / `GET /api/search` across messages, tasks, and files.
|
|
75
|
+
- **Task comments** — discussion lives on the task (`bus_comment_task`).
|
|
76
|
+
- **Presence** — `bus_heartbeat`, online/away on agent listings; token hashes never exposed.
|
|
77
|
+
- **Terminal CLI** — `ghostbus-cli` drives any workspace from the shell (status, register, send, inbox, tasks, claim/complete, board, search, files).
|
|
78
|
+
- **Channels list**, **file delete**, docs (`docs/client-setup.md`: Claude/Cursor/Antigravity/Windsurf setup, token mode, wake-hook pattern), a three-agent team demo, and GitHub Actions CI (Node 20 + 22) and an npm Trusted-Publishing workflow.
|
|
79
|
+
- 34 automated checks: core, token enforcement, persistence, MCP stdio E2E, HTTP auth, board UI, SSE delivery, CLI.
|
|
80
|
+
|
|
81
|
+
## 0.1.0 — 2026-10-07 (M0, pre-release)
|
|
82
|
+
|
|
83
|
+
Core bus: agent registry, direct/broadcast/threaded messages with read receipts, claimable tasks (exclusive 15-minute leases, assignee guard, needs-approval gate), shared files/context, rendered board, provenance event log. 19 MCP tools over stdio; HTTP relay (REST + `/mcp`) with optional workspace key and a counts-only `/probe`. Zero dependencies.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ghost Developer Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PLAN.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# GhostBus — Agent-to-Agent Message Bus / Shared Workspace MCP
|
|
2
|
+
|
|
3
|
+
**Status:** PUBLISHED — v0.5.1 live Oct 9, 2026 (github.com/littlestjames82-sys/ghostbus). v0.5.1 is a reliability patch: a per-Bus mutation mutex in the core serializes every operation's load → mutate → save cycle, closing the silent lost-update race under concurrent writers on one bus instance — 47/47 + 6/6 with three new concurrency regression checks (all red on the pre-fix core). v0.5.0 is the operator release: hosted workspace key rotation (old key dies immediately), full-state export/import for backup + host migration (fresh key on import, keys never inside exports), core snapshot/restore + CLI export/import, and the FileStore race regression test in the main suite — 44/44 + 6/6. v0.4.0 added the hosted multi-workspace server + Docker + Netlify serverless pack (41/41 + 6/6 tests). v0.4.1 fixed the FileStore temp-file race (unique temp per save + serialized saves; stress 0 failures vs 59/60 failing before), shipped the docker-compose + one-shot VPS setup assets, and passed a full hosted smoke test. Still not deployed to a public host — that waits on Ryan's VPS choice. npm: name `ghostbus` verified available; publish rides the Trusted-Publishing workflow after Ryan's one-time npm setup (see LAUNCH.md).
|
|
4
|
+
**Origin:** Generalizes Ghost Bridge (Ryan's private Muse ↔ Antigravity relay, Level 3, live since Oct 6) into a standalone product any agents can join. Ghost Bridge stays private; GhostBus is the clean, general version — same proven patterns, no Ryan-specific context, no studio data in the box.
|
|
5
|
+
|
|
6
|
+
## The problem (one line)
|
|
7
|
+
Agents today work alone in separate windows. When two agents need to cooperate, a human copy-pastes between them. GhostBus is the room they meet in instead.
|
|
8
|
+
|
|
9
|
+
## What it is
|
|
10
|
+
A zero-dependency Node server that gives any MCP-capable agent (Claude, Cursor, Antigravity/Gemini, Windsurf, custom/LLM agents) one shared workspace:
|
|
11
|
+
|
|
12
|
+
- **Registry** — agents join with a name, role, and capabilities; everyone can see who's on the bus and when they were last active.
|
|
13
|
+
- **Message bus** — direct messages, broadcasts, channels, threads, read receipts (inbox marking).
|
|
14
|
+
- **Task handoffs** — create → (optional approval gate) → exclusive claim with a 15-minute lease → complete with a result. Double-claims fail loudly; expired leases return to the queue; assigned tasks can't be poached.
|
|
15
|
+
- **Shared workspace** — shared files (capsules/specs/deliverables), one shared context object, and a rendered Board (agents / open tasks / files / recent activity).
|
|
16
|
+
- **Provenance** — an append-only event log stamps every action with actor + time. Nothing is anonymous.
|
|
17
|
+
- **Two transports, one store** — MCP over stdio for local clients (shared file store = agents on one machine actually share state), plus an HTTP relay (REST + `/mcp`) with an optional workspace key for multi-machine/hosted setups. Counts-only `/probe` stays open so wake-hooks can poll cheaply.
|
|
18
|
+
|
|
19
|
+
## Why this fits Ghost Developer Studio
|
|
20
|
+
- It's the third leg of the agent-infra stool Ryan is already building: **Agent Seatbelt** (stop agents doing damage), **GhostGuard** (govern what agents may do), **GhostBus** (let agents work *together* — with the approval gate built in, so governance travels with collaboration).
|
|
21
|
+
- Proven in-house first: Ghost Bridge has run this exact pattern live for Ryan's own two agents. GhostBus is productizing something that already works, not a paper idea.
|
|
22
|
+
- Honest positioning vs. the standards world: Google's A2A protocol and MCP are complementary, not competitors — MCP is how an agent reaches tools; GhostBus is an MCP server, so it works with the clients people already run today, no new protocol adoption required.
|
|
23
|
+
|
|
24
|
+
## Design rules (carried from Ghost Bridge lessons)
|
|
25
|
+
1. Per-entity writes / atomic file replace — rapid read-modify-write on a single blob clobbered data under propagation lag in Bridge v1. GhostBus core is single-process serialized with atomic saves in M0; hosted multi-writer storage is an M2 problem, stated plainly.
|
|
26
|
+
2. Keys are hashed/compared in constant time; the relay stores no plaintext secrets beyond the optional env key.
|
|
27
|
+
3. Approval is a state, not a comment: a `needs-approval` task *cannot* be claimed until approved.
|
|
28
|
+
4. Every claim is a lease, not a lock forever — a dead agent can't strand a task.
|
|
29
|
+
5. Zero dependencies. Install = clone + `node`. Same rule that made Seatbelt easy to adopt.
|
|
30
|
+
|
|
31
|
+
## Milestones
|
|
32
|
+
- **M0 (DONE, Oct 7):** core + 19 MCP tools + stdio server + HTTP relay + file/memory stores, 23/23 tests (core, persistence, stdio E2E with two agents, HTTP E2E with auth), two-agent demo, README/PLAN/examples.
|
|
33
|
+
- **M1 (DONE, shipped in v0.2.0):** per-agent tokens with enforced mode + rotation, SSE event push, web board UI, search, task comments, heartbeat/presence, file delete, channels, terminal CLI, client-setup docs, three-agent demo, CI + npm Trusted-Publishing workflow. Still open from the original list: wake hooks as a shipped example (pattern documented in docs/client-setup.md).
|
|
34
|
+
- **M2 (DONE, shipped in v0.4.0; hardened in v0.4.1):** hosted multi-workspace server (registry, per-workspace keys, isolation, admin API, Docker) + Netlify/Blobs serverless pack with mock-tested logic. v0.4.1: FileStore race fixed, docker-compose + vps-setup.sh shipped, hosted smoke test passed. NOT yet deployed anywhere public — Ryan chose the Docker-on-VPS route Oct 8; a live deploy waits on his VPS pick (no VPS provisioned yet, no paid spend without his go). Remaining from the original list: A2A-protocol bridge adapter, only if demand shows up.
|
|
35
|
+
- **M3 / monetization (draft only):** open-source core stays MIT; paid hosted relay (per-workspace) is the GhostGuard/Seatbelt-shaped play. No pricing set — Ryan's call.
|
|
36
|
+
|
|
37
|
+
## Ryan's taps (batched)
|
|
38
|
+
1. ~~Name + publish~~ — DONE Oct 7 on his "Publish and build the shit out of it": name kept, GitHub live.
|
|
39
|
+
2. npm one-time setup (his only remaining tap): first manual `npm publish` from his machine *or* create the package + Trusted Publisher on npmjs.com (steps in LAUNCH.md) — then releases publish automatically from GitHub, token-free, exactly like ghost-seatbelt on PyPI.
|
|
40
|
+
3. Whether GhostBus replaces Ghost Bridge for his own Muse ↔ Antigravity traffic later (Bridge works; migration is optional).
|
package/README.md
CHANGED
|
@@ -1,3 +1,109 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GhostBus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**An agent-to-agent message bus and shared workspace, exposed as an MCP server.**
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
Zero dependencies. Node 18+. MIT.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install ghostbus
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
GhostBus is published on npm as [`ghostbus`](https://www.npmjs.com/package/ghostbus) (v0.5.0) — zero dependencies, Node 18+. You can also run it straight from a clone of this repository, as the quick start below shows.
|
|
16
|
+
|
|
17
|
+
## Status
|
|
18
|
+
|
|
19
|
+
Built and in use: 29 MCP tools across presence, messages, tasks, a shared workspace, and a provenance log; local stdio servers, an HTTP relay, and a hosted multi-workspace server are all included in this repo, along with a live web board, SSE event pushes, per-agent tokens, and a terminal CLI. See `CHANGELOG.md` for release history and `docs/` for the client setup and hosted-operator guides.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/littlestjames82-sys/ghostbus.git ghostbus && cd ghostbus
|
|
25
|
+
node examples/two-agent-demo.mjs # watch a planner and a builder cooperate
|
|
26
|
+
node test.mjs # 51 checks: core, tokens, capsules, dependencies, sync, persistence, concurrency, MCP stdio, HTTP, SSE, CLI, hosted security
|
|
27
|
+
node examples/three-agent-team.mjs # planner + builder + reviewer, no human relaying
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Connect an MCP client (local, stdio)
|
|
31
|
+
|
|
32
|
+
Add to your client's MCP config (see `examples/mcp_config.example.json`):
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"ghostbus": {
|
|
38
|
+
"command": "node",
|
|
39
|
+
"args": ["/path/to/ghostbus/src/mcp-server.mjs", "--store", "/path/to/shared/ghostbus-data.json"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Point **every** agent's client at the **same** `--store` file — that's what makes the workspace shared instead of private per client. Each agent calls `bus_register` with its own name first.
|
|
46
|
+
|
|
47
|
+
### Run a relay (multi-machine / hosted)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
GHOSTBUS_KEY=choose-a-long-secret node src/http-server.mjs --port 8377
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Host many workspaces
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
GHOSTBUS_ADMIN_KEY=pick-a-long-secret node src/hosted-server.mjs --port 8388
|
|
57
|
+
```
|
|
58
|
+
|
|
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
|
+
|
|
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`.
|
|
62
|
+
|
|
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
|
+
|
|
65
|
+
## The 29 tools
|
|
66
|
+
|
|
67
|
+
| Area | Tools |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Presence | `bus_status`, `bus_register`, `bus_agents`, `bus_heartbeat`, `bus_rotate_token` |
|
|
70
|
+
| Messages | `bus_send` (direct / broadcast / threaded), `bus_inbox` (read receipts), `bus_thread`, `bus_channels`, `bus_search`, `bus_sync` (offline catch-up by event cursor) |
|
|
71
|
+
| Tasks | `bus_create_task` (incl. `blockedBy` dependencies), `bus_list_tasks`, `bus_claim_task`, `bus_complete_task`, `bus_cancel_task`, `bus_approve_task`, `bus_comment_task` |
|
|
72
|
+
| Workspace | `workspace_put_file`, `workspace_get_file`, `workspace_list_files`, `workspace_delete_file`, `workspace_file_history`, `workspace_put_context`, `workspace_get_context`, `workspace_board`, `workspace_get_capsule`, `workspace_update_capsule` |
|
|
73
|
+
| Provenance | `bus_events` |
|
|
74
|
+
|
|
75
|
+
## How work flows
|
|
76
|
+
|
|
77
|
+
1. Agents `bus_register` with a role and capabilities.
|
|
78
|
+
2. One publishes shared context + a spec file, creates a task, and messages the assignee.
|
|
79
|
+
3. The assignee **claims** the task — exclusively, on a 15-minute lease. A second claim fails loudly; if the claimer dies, the lease expires and the task returns to the queue.
|
|
80
|
+
4. Sensitive tasks are created with `needsApproval: true` — they sit in `needs-approval` and *cannot* be claimed until another agent (or a human driving one) approves them. Tasks can also declare `blockedBy` dependencies and refuse claims until those finish.
|
|
81
|
+
5. Long-lived projects keep a **capsule** — State / Decisions (locked) / Next / Session log — that every agent updates as it works, so the next agent starts from the shared picture, not a cold prompt.
|
|
82
|
+
5. Completion stores a result on the task; every step lands in the provenance event log, and `workspace_board` renders the whole picture for a human at a glance.
|
|
83
|
+
|
|
84
|
+
## Design notes
|
|
85
|
+
|
|
86
|
+
- **One shared store, many processes.** State lives in one JSON store (file or memory). File-backed buses re-read the store on every call and replace it atomically per mutation, so several stdio servers, the CLI, and a relay can share one workspace file and see each other's writes (the per-operation race window is stated plainly: last writer wins a single colliding operation). A networked multi-writer database store is future work and is not faked here.
|
|
87
|
+
- **Auth that matches the threat model.** Local stdio needs none (it's your machine). The HTTP relay takes one workspace key (`GHOSTBUS_KEY`, constant-time compared), and per-agent tokens (issued once at registration, stored only as SHA-256) can be enforced with `GHOSTBUS_REQUIRE_TOKENS=1` so a stolen agent *name* can't impersonate it.
|
|
88
|
+
- **Governance travels with collaboration.** GhostBus pairs naturally with agent-governance tooling (approval gates here; policy enforcement belongs in a layer like [GhostGuard]) — the bus never silently executes anything; it only carries messages, tasks, and files between agents that choose to act.
|
|
89
|
+
|
|
90
|
+
## Origin
|
|
91
|
+
|
|
92
|
+
GhostBus generalizes a private relay that has run in production for one studio's own agents since Oct 2026 (task handoffs, shared project capsules, a live board). This is the clean, general version of that proven pattern. Built by Ghost Developer Studio.
|
|
93
|
+
|
|
94
|
+
## From Ghost Developer Studio
|
|
95
|
+
|
|
96
|
+
GhostBus is one part of the studio's agent-infrastructure family:
|
|
97
|
+
|
|
98
|
+
- **Visual roadmap board** — where every studio product stands, in one picture: [ghost-roadmaps](https://github.com/littlestjames82-sys/ghost-roadmaps)
|
|
99
|
+
- **GhostGuard** — the governance layer for autonomous systems: [ghostguard](https://github.com/littlestjames82-sys/ghostguard)
|
|
100
|
+
- **Ghost Bridge** — MCP bridge + self-hostable relay for handing tasks to a personal agent, with scoped keys and approvals: [ghost-bridge](https://github.com/littlestjames82-sys/ghost-bridge)
|
|
101
|
+
- **Ghost Hands** — governed, recorded, replayable agent hands for the web: [ghost-hands](https://github.com/littlestjames82-sys/ghost-hands)
|
|
102
|
+
- **Agent Seatbelt** — deterministic guardrails that gate what an AI coding agent does before it does it: [agent-seatbelt](https://github.com/littlestjames82-sys/agent-seatbelt)
|
|
103
|
+
- **Ghost Developer Studio storefront** — [storefront](https://github.com/littlestjames82-sys/storefront)
|
|
104
|
+
|
|
105
|
+
Built in public by Ryan Cotten / Ghost Developer Studio.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Connecting clients to GhostBus
|
|
2
|
+
|
|
3
|
+
Every agent connects through its own MCP client, but they all share **one store** — that's the whole point. Two ways:
|
|
4
|
+
|
|
5
|
+
## A. Local — stdio, one shared store file
|
|
6
|
+
|
|
7
|
+
Each client's MCP config launches `src/mcp-server.mjs` with the **same** `--store` path. Processes come and go; the workspace persists in that file.
|
|
8
|
+
|
|
9
|
+
**Claude (claude_desktop_config.json / .mcp.json), Cursor (~/.cursor/mcp.json), Windsurf, Antigravity (~/.gemini/config/mcp_config.json):**
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"ghostbus": {
|
|
15
|
+
"command": "node",
|
|
16
|
+
"args": ["/ABS/PATH/ghostbus/src/mcp-server.mjs", "--store", "/ABS/PATH/shared/ghostbus-data.json", "--name", "My Workspace"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then, in each agent's first message: *"Register on GhostBus as `<name>` with role `<role>`, then check your inbox."* The agent calls `bus_register` itself. Give every agent a **different name** — names are identities on the bus.
|
|
23
|
+
|
|
24
|
+
## B. Hosted — one HTTP relay
|
|
25
|
+
|
|
26
|
+
Run the relay once (any machine the agents can reach):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
GHOSTBUS_KEY=pick-a-long-secret node src/http-server.mjs --port 8377 --store ./ghostbus-data.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Humans watch the live board at `http://<host>:8377/` (it asks for the workspace key in-page).
|
|
33
|
+
- Agents that can't hold a stream can long-poll `GET /api/wait?agent=<name>` (returns on the next relevant event; pass `sinceSeq` to drain backlog). Agents that support remote MCP use `POST /mcp` with headers `x-bus-key: <key>` (and `x-agent-token: <token>` on token-enforced buses).
|
|
34
|
+
- Agents/scripts can use the REST mirror (`/api/*`) or the SSE stream (`GET /api/stream?agent=<name>`) to get pushed events instead of polling.
|
|
35
|
+
|
|
36
|
+
## Token-enforced mode (recommended for hosted)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
GHOSTBUS_REQUIRE_TOKENS=1 GHOSTBUS_KEY=... node src/http-server.mjs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `bus_register` returns a per-agent token **once**. Only its SHA-256 is stored.
|
|
43
|
+
- The agent passes it as the `token` argument on tool calls (or the `x-agent-token` header). A stolen name alone can't impersonate an agent.
|
|
44
|
+
- Lost token → `bus_rotate_token` with the current token; on a fully locked-out agent, the workspace owner resets by removing that agent's `tokenHash` from the store file while the servers are stopped.
|
|
45
|
+
|
|
46
|
+
## Waking idle agents
|
|
47
|
+
|
|
48
|
+
Stdio clients only act when their human/app prompts them. The relay's `GET /probe` returns counts only (`queued`, `needsApproval`, `agents`) with no auth — poll it cheaply from a cron/hook and nudge the right client when work appears. (This is the pattern GhostBus's predecessor ran in production.)
|
package/docs/hosted.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Running GhostBus hosted (multi-workspace)
|
|
2
|
+
|
|
3
|
+
One GhostBus process, many fully isolated workspaces — for a team, a studio running several clients, or a small hosted service. Each workspace has its own agents, tasks, files, capsules, board, and **its own key**.
|
|
4
|
+
|
|
5
|
+
## Option A — the hosted server (any machine / Docker)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
GHOSTBUS_ADMIN_KEY=pick-a-long-admin-secret node src/hosted-server.mjs --port 8388 --data-dir ./ghostbus-hosted
|
|
9
|
+
# or: docker build -t ghostbus-hosted -f deploy/docker/Dockerfile .
|
|
10
|
+
# docker run -p 8388:8388 -e GHOSTBUS_ADMIN_KEY=... -v ghostbus-data:/data ghostbus-hosted
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Data layout in the data dir: `registry.json` (workspace metadata + key **hashes**) and one `<id>.json` state file per workspace. Back up the directory and you've backed up everything.
|
|
14
|
+
|
|
15
|
+
### Operator flow
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# create a workspace (returns its key ONCE)
|
|
19
|
+
curl -X POST localhost:8388/api/workspaces -H 'x-bus-key: <admin>' \
|
|
20
|
+
-d '{"id":"acme", "name":"Acme Team"}'
|
|
21
|
+
# -> {"id":"acme","name":"Acme Team","key":"<64 hex>","keyNote":"..."}
|
|
22
|
+
curl localhost:8388/api/workspaces -H 'x-bus-key: <admin>' # list + counts
|
|
23
|
+
curl -X DELETE localhost:8388/api/workspaces/acme -H 'x-bus-key: <admin>'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Key rotation, backup, migration (v0.5.0)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# rotate a workspace's key — the new key is returned ONCE and the old key
|
|
30
|
+
# stops working immediately (no restart, no grace period)
|
|
31
|
+
curl -X POST localhost:8388/api/workspaces/acme/rotate-key -H 'x-bus-key: <admin>'
|
|
32
|
+
|
|
33
|
+
# export a workspace's full state (agents, messages, tasks, files, capsules,
|
|
34
|
+
# context, event log) as one JSON envelope — for backup or moving hosts.
|
|
35
|
+
# The export never contains the workspace key (only its hash exists, and
|
|
36
|
+
# hashes stay in the registry); agent tokens travel as hashes, as at rest.
|
|
37
|
+
curl localhost:8388/api/workspaces/acme/export -H 'x-bus-key: <admin>' > acme-backup.json
|
|
38
|
+
|
|
39
|
+
# import an export (on this host or a new one). The workspace is created
|
|
40
|
+
# with a FRESH key, returned once — the source key is never carried over.
|
|
41
|
+
# Importing over an existing workspace id is refused (409).
|
|
42
|
+
curl -X POST localhost:8388/api/workspaces/import -H 'x-bus-key: <admin>' \
|
|
43
|
+
-d @acme-backup.json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The same snapshot format works without a server: `ghostbus-cli export backup.json`
|
|
47
|
+
and `ghostbus-cli --store other.json import backup.json`. Restores are recorded
|
|
48
|
+
in the workspace's provenance log (`workspace.restore`).
|
|
49
|
+
|
|
50
|
+
### Agent flow
|
|
51
|
+
|
|
52
|
+
Everything from the single-workspace relay works under the prefix:
|
|
53
|
+
|
|
54
|
+
- Board: `https://host/w/acme/` (asks for the workspace key in-page)
|
|
55
|
+
- REST: `https://host/w/acme/api/*` with header `x-bus-key: <workspace key>`
|
|
56
|
+
- MCP: `POST https://host/w/acme/mcp` (same header; plus `x-agent-token` — per-agent token enforcement is ON by default in hosted mode, see below)
|
|
57
|
+
- Probe (open, counts only): `GET https://host/w/acme/probe`
|
|
58
|
+
- Push: SSE at `/w/acme/api/stream?agent=…`, long-poll at `/w/acme/api/wait?agent=…`
|
|
59
|
+
|
|
60
|
+
The admin key also works as a master key inside any workspace — treat it like root: operators only, never hand it to agents.
|
|
61
|
+
|
|
62
|
+
## Security & deployment hardening
|
|
63
|
+
|
|
64
|
+
The hosted server is the one GhostBus deployment that serves people other than
|
|
65
|
+
the operator, so its defaults are stricter than the local relay's (v0.5.2):
|
|
66
|
+
|
|
67
|
+
- **Per-agent tokens are enforced by default** in every workspace: an agent
|
|
68
|
+
registers once, receives its token once, and must present it
|
|
69
|
+
(`x-agent-token` header or `token` field) for every act. A stolen agent
|
|
70
|
+
*name* alone cannot impersonate it. `GHOSTBUS_REQUIRE_TOKENS=0` is the only
|
|
71
|
+
opt-out — the server prints a loud startup warning when it is used, because
|
|
72
|
+
with enforcement off, anyone holding a workspace key can act under any
|
|
73
|
+
agent name. Don't disable it for a public or paid deployment.
|
|
74
|
+
- **HTTPS is REQUIRED for any public deployment.** The server itself speaks
|
|
75
|
+
plain HTTP; terminate TLS in front of it — a reverse proxy with automatic
|
|
76
|
+
certificates (Caddy, nginx + certbot) or a **named** Cloudflare Tunnel with
|
|
77
|
+
a stable hostname. A Cloudflare *quick* tunnel (`trycloudflare.com`) is
|
|
78
|
+
fine for a demo and **not** for paid use: its URL changes on restart and it
|
|
79
|
+
carries no uptime expectation. Never expose the plain-HTTP port directly.
|
|
80
|
+
- **Admin key hygiene.** The admin key is operator root: it creates and
|
|
81
|
+
deletes workspaces, rotates their keys, exports their state, and works as a
|
|
82
|
+
master key inside every workspace. The server refuses to start with a key
|
|
83
|
+
shorter than 16 characters; use 32+ random hex. Store it in a root-only
|
|
84
|
+
file, never in the repo, a ticket, or a chat log:
|
|
85
|
+
```bash
|
|
86
|
+
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' > /root/ghostbus-admin-key
|
|
87
|
+
chmod 600 /root/ghostbus-admin-key
|
|
88
|
+
GHOSTBUS_ADMIN_KEY=$(cat /root/ghostbus-admin-key) node src/hosted-server.mjs
|
|
89
|
+
```
|
|
90
|
+
(The shipped `deploy/docker/vps-setup.sh` already generates it this way.)
|
|
91
|
+
To rotate the admin key, restart the server with a new one — existing
|
|
92
|
+
workspace keys are unaffected (they live in the registry as hashes, not
|
|
93
|
+
derived from the admin key).
|
|
94
|
+
- **Per-workspace keys.** Each workspace key is shown exactly once, at
|
|
95
|
+
creation (or import); only its SHA-256 is stored. Hand it to that
|
|
96
|
+
workspace's owner over a secure channel, never publish it, and if one
|
|
97
|
+
leaks, rotate it — `POST /api/workspaces/<id>/rotate-key` kills the old
|
|
98
|
+
key immediately (see above).
|
|
99
|
+
- **Backups are a schedule, not a feature.** The data dir *is* the state
|
|
100
|
+
(`registry.json` + one `<id>.json` per workspace), so back it up on a
|
|
101
|
+
timer, and per-workspace exports give you portable copies:
|
|
102
|
+
```bash
|
|
103
|
+
# nightly, e.g. from cron — plus a plain copy of the data dir
|
|
104
|
+
curl -s localhost:8388/api/workspaces/acme/export -H 'x-bus-key: <admin>' > backups/acme-$(date +%F).json
|
|
105
|
+
```
|
|
106
|
+
Drill a restore (delete + import) before you need one; a restore lands in
|
|
107
|
+
the workspace's provenance log as `workspace.restore`.
|
|
108
|
+
|
|
109
|
+
## Option B — serverless on Netlify (Blobs)
|
|
110
|
+
|
|
111
|
+
`deploy/netlify/` contains a Netlify Function implementing the same multi-workspace semantics over Netlify Blobs (strong consistency):
|
|
112
|
+
|
|
113
|
+
1. Create a Netlify site using `deploy/netlify/` as the site root (or copy `functions/` + `netlify.toml` into a site).
|
|
114
|
+
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.)
|
|
115
|
+
3. Add `@netlify/blobs` to that site's dependencies (`npm i @netlify/blobs` in the site root) and deploy.
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
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`.
|
|
120
|
+
|
|
121
|
+
## Isolation guarantees (both options)
|
|
122
|
+
|
|
123
|
+
- A workspace key opens exactly one workspace; another workspace's key gets 401.
|
|
124
|
+
- Agents, messages, tasks, files, capsules, and event logs never cross workspaces (separate stores, verified by test).
|
|
125
|
+
- Registry stores only SHA-256 hashes of workspace keys; a leaked registry does not leak keys.
|
|
126
|
+
- Deleting a workspace removes its registry entry and its state.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"_comment":"Add this entry to each agent client's MCP config (Claude, Cursor, Antigravity, Windsurf). Every client MUST point at the same --store file — that shared store is the workspace. Each agent then calls bus_register with its own name.","mcpServers":{"ghostbus":{"args":["/path/to/ghostbus/src/mcp-server.mjs","--store","/path/to/shared/ghostbus-data.json","--name","My Workspace"],"command":"node"}}}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Three-agent software team demo — planner, builder, reviewer cooperate on one
|
|
4
|
+
* bus with NO human relaying messages: a spec is published, work is claimed,
|
|
5
|
+
* reviewed with comments, fixed, approved, and a gated deploy is released.
|
|
6
|
+
* Run: node examples/three-agent-team.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { createBus, MemoryStore } from '../src/core.mjs';
|
|
9
|
+
|
|
10
|
+
const bus = await createBus(new MemoryStore(), { name: 'Team Demo' });
|
|
11
|
+
for (const [name, role, caps] of [
|
|
12
|
+
['planner', 'planning + product', ['specs', 'prioritization']],
|
|
13
|
+
['builder', 'implementation', ['node', 'api']],
|
|
14
|
+
['reviewer', 'code review + QA', ['review', 'tests']],
|
|
15
|
+
]) await bus.registerAgent({ name, role, capabilities: caps });
|
|
16
|
+
|
|
17
|
+
console.log('team on the bus:', (await bus.listAgents()).map(a => a.name).join(', '));
|
|
18
|
+
|
|
19
|
+
// Planner sets the shared picture
|
|
20
|
+
await bus.putContext({ by: 'planner', context: { project: 'widget-api', release: '0.1', freeze: 'Friday' } });
|
|
21
|
+
await bus.putFile({ by: 'planner', path: 'capsules/widget-api.md', text: '# Widget API\nGET /widgets returns [] initially.\nPOST /widgets {name} -> 201.' });
|
|
22
|
+
|
|
23
|
+
const build = await bus.createTask({ from: 'planner', title: 'Implement Widget API', body: 'Spec: capsules/widget-api.md', assignee: 'builder', priority: 'urgent' });
|
|
24
|
+
await bus.sendMessage({ from: 'planner', to: 'builder', body: `Task #${build.id} ready — spec in workspace.`, channel: 'dev' });
|
|
25
|
+
|
|
26
|
+
// Builder works it
|
|
27
|
+
await bus.claimTask(build.id, { by: 'builder' });
|
|
28
|
+
await bus.completeTask(build.id, { by: 'builder', result: 'Endpoints implemented; manual smoke passed.' });
|
|
29
|
+
|
|
30
|
+
// Reviewer reviews via comments + a follow-up task
|
|
31
|
+
const review = await bus.createTask({ from: 'reviewer', title: 'Review Widget API', body: 'Check status codes + empty-name handling.', assignee: 'reviewer' });
|
|
32
|
+
await bus.claimTask(review.id, { by: 'reviewer' });
|
|
33
|
+
await bus.addTaskComment(review.id, { by: 'reviewer', body: 'Found: POST with empty name returns 201, should be 400.' });
|
|
34
|
+
const fix = await bus.createTask({ from: 'reviewer', title: 'Fix: empty widget name must 400', assignee: 'builder' });
|
|
35
|
+
await bus.claimTask(fix.id, { by: 'builder' });
|
|
36
|
+
await bus.completeTask(fix.id, { by: 'builder', result: 'Validation added; empty name now 400.' });
|
|
37
|
+
await bus.completeTask(review.id, { by: 'reviewer', result: 'Re-checked after fix — clean.' });
|
|
38
|
+
|
|
39
|
+
// Gated deploy: builder proposes, planner approves
|
|
40
|
+
const deploy = await bus.createTask({ from: 'builder', title: 'Deploy widget-api 0.1', needsApproval: true });
|
|
41
|
+
console.log(`deploy gated: #${deploy.id} status=${deploy.status}`);
|
|
42
|
+
await bus.approveTask(deploy.id, { by: 'planner' });
|
|
43
|
+
await bus.claimTask(deploy.id, { by: 'builder' });
|
|
44
|
+
await bus.completeTask(deploy.id, { by: 'builder', result: 'Deployed.' });
|
|
45
|
+
await bus.sendMessage({ from: 'builder', to: '*', body: 'widget-api 0.1 is live. 🎉', channel: 'announce' });
|
|
46
|
+
|
|
47
|
+
const found = await bus.search('empty name');
|
|
48
|
+
console.log(`search "empty name" -> ${found.tasks.length} task(s), ${found.messages.length} message(s)`);
|
|
49
|
+
console.log('channels:', (await bus.listChannels()).map(c => `${c.channel}(${c.messages})`).join(' '));
|
|
50
|
+
console.log('\n' + await bus.board());
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Two-agent demo — planner and builder meet on one bus, no human in the middle
|
|
4
|
+
* except the approval gate. Run: node examples/two-agent-demo.mjs
|
|
5
|
+
* Uses an in-memory workspace and prints the shared board at the end.
|
|
6
|
+
*/
|
|
7
|
+
import { createBus, MemoryStore } from '../src/core.mjs';
|
|
8
|
+
|
|
9
|
+
const bus = await createBus(new MemoryStore(), { name: 'Demo Workspace' });
|
|
10
|
+
|
|
11
|
+
await bus.registerAgent({ name: 'planner', role: 'planning + review', capabilities: ['research', 'specs'] });
|
|
12
|
+
await bus.registerAgent({ name: 'builder', role: 'implementation', capabilities: ['node', 'tests'] });
|
|
13
|
+
|
|
14
|
+
await bus.putContext({ by: 'planner', context: { project: 'widget-api', stack: 'node', phase: 'build' } });
|
|
15
|
+
await bus.putFile({ by: 'planner', path: 'capsules/widget-api.md', text: '# Widget API\nGET /widgets, POST /widgets. JSON in/out.' });
|
|
16
|
+
|
|
17
|
+
const task = await bus.createTask({ from: 'planner', title: 'Implement Widget API', body: 'Spec: capsules/widget-api.md', assignee: 'builder', priority: 'urgent' });
|
|
18
|
+
await bus.sendMessage({ from: 'planner', to: 'builder', body: `Task #${task.id} is yours — spec is in the workspace.` });
|
|
19
|
+
|
|
20
|
+
const inbox = await bus.inbox('builder');
|
|
21
|
+
console.log('builder inbox:', inbox.map(m => `#${m.id} from ${m.from}: ${m.body}`).join(' | '));
|
|
22
|
+
|
|
23
|
+
await bus.claimTask(task.id, { by: 'builder' });
|
|
24
|
+
try { await bus.claimTask(task.id, { by: 'planner' }); } catch (e) { console.log('double-claim correctly refused:', e.message); }
|
|
25
|
+
|
|
26
|
+
await bus.completeTask(task.id, { by: 'builder', result: 'Widget API implemented; endpoints verified.' });
|
|
27
|
+
await bus.sendMessage({ from: 'builder', to: 'planner', body: 'Done — result is on the task.', threadId: inbox[0].id });
|
|
28
|
+
|
|
29
|
+
const deploy = await bus.createTask({ from: 'builder', title: 'Deploy widget-api to production', needsApproval: true });
|
|
30
|
+
console.log(`deploy task #${deploy.id} is gated: status=${deploy.status}`);
|
|
31
|
+
await bus.approveTask(deploy.id, { by: 'planner' });
|
|
32
|
+
console.log(`after planner approval: status=${(await bus.listTasks()).find(t => t.id === deploy.id).status}`);
|
|
33
|
+
|
|
34
|
+
console.log('\n' + await bus.board());
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Wake hook example
|
|
2
|
+
|
|
3
|
+
Agents connected over stdio only act when prompted, and polling an inbox burns tokens. The wake pattern fixes that: a cheap unauthenticated `GET /probe` (counts only) is polled by a tiny script, and the agent is only nudged when the **actionable set changes** — a new queued task appears, or something lands in `needs-approval`.
|
|
4
|
+
|
|
5
|
+
`wake.sh` implements it with the two lessons from production use:
|
|
6
|
+
|
|
7
|
+
1. **Fingerprint, don't just count polls.** The script stores the last actionable fingerprint (`queued + needsApproval` counts) and stays silent while it's unchanged.
|
|
8
|
+
2. **Re-wake a stale set once after 20 minutes.** If a worker crashed mid-task, the set never changes — without the re-wake, work would sit forever.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
GHOSTBUS_RELAY=http://127.0.0.1:8377 WAKE_CMD="./on-work.sh" ./examples/wake-hook/wake.sh
|
|
12
|
+
# cron: */2 * * * * GHOSTBUS_RELAY=... WAKE_CMD=... /path/to/wake.sh >> ~/ghostbus-wake.log 2>&1
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`on-work.sh` is yours: open your agent client with a prompt like *"Check your GhostBus inbox and the task queue; work what's yours; report results on the bus."* — over HTTP, agents can also skip the hook entirely and use the SSE stream (`/api/stream`) or long-poll (`/api/wait`), which push instead of poll.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# GhostBus wake hook — poll a relay's /probe and run a command when the
|
|
3
|
+
# actionable set CHANGES (new queued task, or a task lands in needs-approval).
|
|
4
|
+
# The same fingerprint pattern Ghost Bridge ran in production:
|
|
5
|
+
# - unchanged set -> silent, exit 0
|
|
6
|
+
# - same set >20 min -> re-wake once (crash safety: a worker may have died)
|
|
7
|
+
# - changed set -> run $WAKE_CMD with $GHOSTBUS_ACTIONABLE ids in env
|
|
8
|
+
#
|
|
9
|
+
# Usage (cron every 2 min, or a loop):
|
|
10
|
+
# GHOSTBUS_RELAY=http://127.0.0.1:8377 WAKE_CMD="./on-work.sh" ./wake.sh
|
|
11
|
+
# State: ~/.ghostbus-wake-state (fingerprint + timestamp). Stdout is for logs.
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
RELAY="${GHOSTBUS_RELAY:-http://127.0.0.1:8377}"
|
|
14
|
+
STATE="${GHOSTBUS_WAKE_STATE:-$HOME/.ghostbus-wake-state}"
|
|
15
|
+
NOW=$(date +%s)
|
|
16
|
+
|
|
17
|
+
PROBE=$(curl -fsS --max-time 10 "$RELAY/probe") || { echo "probe failed — relay down?"; exit 0; }
|
|
18
|
+
QUEUED=$(echo "$PROBE" | python3 -c "import json,sys; print(json.load(sys.stdin)['queued'])")
|
|
19
|
+
APPROVAL=$(echo "$PROBE" | python3 -c "import json,sys; print(json.load(sys.stdin)['needsApproval'])")
|
|
20
|
+
FPRINT="q${QUEUED}_a${APPROVAL}"
|
|
21
|
+
|
|
22
|
+
PREV=""; PREV_AT=0
|
|
23
|
+
[ -f "$STATE" ] && { read -r PREV PREV_AT < "$STATE" || true; }
|
|
24
|
+
|
|
25
|
+
if [ "$FPRINT" = "$PREV" ]; then
|
|
26
|
+
AGE=$(( NOW - ${PREV_AT:-0} ))
|
|
27
|
+
if [ "$QUEUED" -eq 0 ] && [ "$APPROVAL" -eq 0 ]; then exit 0; fi
|
|
28
|
+
if [ "$AGE" -lt 1200 ]; then exit 0; fi
|
|
29
|
+
echo "re-wake: same actionable set for ${AGE}s (crash safety)"
|
|
30
|
+
else
|
|
31
|
+
echo "wake: actionable set changed ($PREV -> $FPRINT)"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
echo "$FPRINT $NOW" > "$STATE"
|
|
35
|
+
if [ -n "${WAKE_CMD:-}" ]; then
|
|
36
|
+
GHOSTBUS_QUEUED="$QUEUED" GHOSTBUS_NEEDS_APPROVAL="$APPROVAL" bash -c "$WAKE_CMD"
|
|
37
|
+
else
|
|
38
|
+
echo "WAKE_CMD not set — would wake now (queued=$QUEUED needsApproval=$APPROVAL)"
|
|
39
|
+
fi
|