
Waitbus
io.github.astrogilda由 GitHub 帳號 astrogilda 發布v0.2.0更新於 Oct 25, 2026
Workstation-local event bus: agents wait on or emit CI/pytest/Docker/filesystem events, no polling.
概覽
一個本機事件匯流排 MCP 伺服器,讓助理不必輪詢就能等待、查詢與發出 CI、pytest、Docker、檔案系統與告警事件。
- 可以做什麼
- 查詢本機 SQLite 事件庫中的狀態與近期事件,並依 README 所述指令把工作事件彙整為各 PR 的狀態。跨來源阻塞等待符合的事件,支援 all-of/first-of 組合條件,並以有界長輪詢取代輪詢迴圈。向匯流排發出事件,喚醒本機其他代理、指令碼與 CI 工作。對支援資源訂閱的用戶端推送更新通知,並為 Claude Code 提供內嵌通道通知。在條件成立的瞬間執行指令,事件值以環境變數而非參數傳入。
- 適用情境
- 適合同一台機器上同時執行多個代理、指令碼、測試或容器,且需要互相得知完成與失敗的情況;也適合指令碼需要阻塞到測試、建置、CI 與容器結束全部完成的情況。若只等待單一 GitHub 儲存庫的 CI 或單純的檔案變更,使用更簡單的工具即可。
- 執行需求
- 以本機 stdio 程序執行,通常透過 uvx 啟動,因此 PATH 中需有 uv,並需要 Python 3.11 以上。支援 Linux(systemd 使用者服務)與 macOS(launchd),僅限桌面端。事件送達需要廣播守護程序及選用的來源監看程式在執行。GitHub webhook 監聽器與 Alertmanager 的選用 HMAC 金鑰透過 install-credentials 寫入;不需要帳號或雲端服務。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Waitbus,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
waitbus: the workstation-local, cross-harness status bus. D-Bus for agents, with a replay log and a wait primitive
[CI] [Python 3.11+] [License: MIT] [Ask DeepWiki] [Context7]
One wait verb that blocks on, and queries across, every local source at once. When any agent, CI job, test, or container on your machine finishes or fails, every other tool on the box hears it: your Claude Code, your Cursor, your scripts, your CI. Clients that support server notifications get a push; the rest get one blocking waitbus wait. No waitbus cloud, no account, no telemetry; all processing stays on your machine.
Below: a Pydantic AI agent and a LangGraph agent, two different frameworks running as two separate OS processes, both wait on one local waitbus bus. One fails; the peer on the other framework, plus a live waitbus top view, react to the single failure broadcast.
Real frameworks and real waitbus subscribe/emit; the agents' LLMs are deterministic fakes and the failure is an injected event, so the clip runs fully offline. Reproduce the claim (not just the recording): run uvx waitbus demo for the gentler single-agent walk-through (no clone, no install), or, for the exact two-framework proof shown above, clone the repo and run uv run --group agent-recipes python -m examples.hero_swarm (Linux-verified; prints PROVEN: two DIFFERENT frameworks woke on ONE peer's failure). Higher-quality MP4 at hero.mp4. Maintainers re-render this recording from docs/demo/.waitbus-demo/ with make hero (dev-only; needs vhs, ttyd, tmux, and JetBrains Mono).
Try it now, no install:
uvx waitbus demoboots a real local bus and returns the instant an event lands, fully offline (~8 s).
waitbus is the workstation-local, cross-harness status bus that lets the tools on one machine hear each other. It does three things: ingest events, broadcast them, and replay them.
Ingest: a blocking wait/emit primitive lets any agent or script
(across Cursor, Claude Code, and any tool on the box) wait on, or emit, events
from five built-in sources (GitHub Actions CI, pytest sessions, Docker
container lifecycle, filesystem changes, and Prometheus Alertmanager) plus any
plugin source registered under the waitbus.sources.v1 entry-point group.
Broadcast: events land in SQLite the moment they arrive, and a broadcast
daemon fans each row to every connected consumer within about a
millisecond on the local socket (measured; see benchmarks/BENCHMARKING.md). Your
agent blocks on waitbus wait (including across sources at once with
--all-of/--first-of) with zero agent-side polling and idle CPU until the thing it
cares about happens, and waitbus on <predicate> -- <command> runs a command
the moment it does (in the CLI process, not the daemon, via execve with no
shell; event values arrive as namespaced WAITBUS_* environment variables and
a $WAITBUS_EVENT_FILE, never as argv, so event content cannot inject flags
or overwrite PATH). Because every agent on the box shares that one bus, it
doubles as a same-machine coordination backplane: one agent emits, the others
wake.
Replay: a durable replay log (since=) means a consumer that was
offline catches up instead of missing the moment, and waitbus events analyze
queries what the bus stored. It is the local opt-in
event-broadcast model the OS already uses (D-Bus signals, inotify, journald),
with the replay log and a wait predicate added on top. (GitHub Actions was
the first source wired, which is why the examples lead with CI, but waitbus is
not CI-only.) The dependency stack is lean: typer, msgspec, platformdirs,
pydantic-settings, prometheus_client, stamina, and the mcp SDK, with no
heavy TUI or crypto dependency. The daemon idles at roughly 40 MB RSS measured
(p50 on the benchmark host, most of it the CPython interpreter baseline) over
a SQLite event store and an AF_UNIX SOCK_STREAM broadcast bus with
length-prefix framing. Runs on Linux (systemd-user) and macOS (launchd).
Who this is for
This is for you if…
- You run a heterogeneous agent fleet (Claude Code and Cursor and background scripts) on one workstation and want them to hear each other's finishes and failures.
- You wait across sources. "Block until the tests pass and the build is
done and CI is green," in one predicate, in a script
(
waitbus wait --all-of ...). Nothing else expresses this. - You want zero-LLM-token, zero-poll waiting as a daily habit across many local things, and you'll run a daemon to get it.
- You care that the daemon on your box is trustworthy: reproducible builds, offline, no cloud, no account, no telemetry.
This is NOT for you if…
- You wait on one GitHub repo's CI and nothing else. Use
gh run watch. It's zero-setup, and for that single job we don't beat it by enough to justify a daemon. - You only need to react to a file change. Use
inotifywait/entr. They're far faster on raw filesystem latency (our own benchmark says so) and need no daemon. - You want cloud, cross-machine, or team coordination. Out of scope for the local core by design.
- You want addressed agent-to-agent messaging as the product (threads,
inboxes, routing). The headline here is broadcast source-ingestion; the
request()/respond()facet exists, but a dedicated message queue serves that center of gravity better.
Try it in 60 seconds
waitbus demo allocates a temporary state directory and boots the
broadcast daemon, then runs two phases. Phase 1, the point: an
agent blocks on waitbus wait (the same egress engine the real command
uses) with zero polling and idle CPU; the moment a github
workflow_run event lands, the wait returns and the demo prints the
real measured event-to-unblock latency. Phase 2, breadth: the same
primitive delivers every source. pytest_session, docker_container,
and fs_change events fan out to a live subscriber. Nothing on your
machine outside the temporary directory is touched.
A recorded MP4 + GIF walkthrough lives at
docs/demo/.waitbus-demo/demo.mp4 and demo.gif
(the tape script is demo.tape). Re-rendering is reproducible
via make demo from that directory (requires
VHS ≥ 0.10.0, ttyd, and
ffmpeg); the Makefile enforces the VHS version floor and refuses
to render against an older binary that could silently change tape
semantics.
The four events the demo emits are synthesized in-process. There is no real HTTP listener, no real pytest run, no real Docker daemon, no real watchdog. A banner before each emit makes this explicit, mirroring the
waitbus statsoutput. To wire waitbus against real GitHub webhook deliveries, follow the Quick start below.
Quick start
Prove the primitive in ~10 seconds -- one foreground daemon, one event, no service install. Install the CLI and bootstrap state:
Then, in two terminals, watch a blocking wait return the instant an emit
lands on the bus:
That round trip -- block, emit, unblock, with zero polling -- is the whole primitive. Everything below wires it to real sources and, optionally, runs the daemons permanently under your service manager.
Run the fleet permanently (optional)
install-systemd is Linux-only; install-launchd is macOS-only.
Each command refuses to run on the other platform and points at the
right one.
Once events are flowing, block any script or agent on the next
matching event with waitbus wait -- any source, any field, exit code
carries the verdict. The wait side is always zero-setup; the event it
waits for still has to be produced by something (an opt-in emitter or a source
watcher), so each example below notes what must be emitting for it to return:
If you have used watchexec or entr to
re-run a command when a file changes, waitbus wait is the same idea with a wider
net: it blocks on a file change too, but it can also wait on a CI run, a pytest
session, a container exit, or another agent's event, and hands the verdict back
through the exit code. The bus underneath also
fans every one of those events out to every other tool on the box.
Architecture
Rendered from docs/architecture.mmd (a static image is used so the diagram renders on PyPI and other non-Mermaid surfaces, not only on GitHub). Regenerate with mmdc -i docs/architecture.mmd -o docs/architecture.png.
Two ingress classes land events in the same store: remote webhooks (GitHub,
Alertmanager) arrive over HTTPS and pass through waitbus listener serve, while
in-process sources (pytest, docker, fs, plugins, and any agent) call the
public emit() API to write the row directly, with no listener and no network hop.
An ETag polling fallback runs on a 45-second timer for repos you cannot receive
webhooks for (upstreams, forks). Every path writes through the same
INSERT OR IGNORE, so redeliveries are idempotent, and every row rings the
broadcast doorbell so subscribers wake within about a millisecond regardless of which
ingress produced it.
How waitbus relates to MCP Tasks (task=True) and to the local agent-to-agent
buses (agent-message-queue, claude-code-inter-session): see
docs/COMPETITIVE_LANDSCAPE.md.
Installation
As a Python CLI tool
Each command is idempotent. Re-running install-credentials <name> rotates
the credential (re-configure the GitHub webhook and Alertmanager after
rotating, then systemctl --user restart waitbus-listener.service).
After install you have one console script on PATH: waitbus. All
daemon entry-points and admin commands are reachable via sub-commands:
And, depending on platform, either 8 systemd-user units in ~/.config/systemd/user/ (Linux):
…or 4 LaunchAgent plists in ~/Library/LaunchAgents/ (macOS):
The macOS plists carry __BIN_DIR__ / __LOG_DIR__ / __RUNTIME_DIR__
placeholders that waitbus install-launchd resolves to the
operator's actual paths before writing. Agents are loaded via
launchctl bootstrap gui/$UID <plist> (the modern replacement for
the deprecated launchctl load -w).
As a Python library
Agents can also message each other on the same bus -- one sends a request and blocks for the correlated reply, the other answers from its inbox:
See docs/AGENT_MESSAGING.md for the request/reply
contract, the inbox stream, and the same-UID trust model.
The stable public API is emit, subscribe, asubscribe, wait_for,
request, respond, EventFrame, and the plugin hooks register_source,
register_condition, register_evaluator (full signatures in
docs/CONSUMER_API.md). To produce events, the
waitbus emit CLI is the simplest path; see examples/emitters/ for the
emit() Python form. Everything under a leading underscore (waitbus._db,
waitbus._paths, …) is a private internal and may change between releases.
Runtime dependencies (the full mandatory set, same as the lean stack
above): typer (umbrella CLI), msgspec (wire framing), platformdirs
(state and runtime path resolution), pydantic-settings (config
loader), prometheus_client (metrics), stamina (retry policy), and
the mcp SDK (MCP server). The two consumer-facing extras
(waitbus[analyze], waitbus[fs]) pull duckdb / watchdog lazily and
are the only optional packages. All other internal modules are pure
stdlib: no network at import time, no C extensions, safe to import
in any Python 3.11+ environment. The package root logger uses
NullHandler so library consumers never see leaked log records.
As an MCP server (PyPI + uvx)
waitbus mcp serve is a stdio MCP server (JSON-RPC 2.0 over the spec at
https://modelcontextprotocol.io/specification). What a given client can do with it
depends on which MCP features that client implements, so here is the actual
capability split:
- Pull, for any MCP client that supports Tools. Call waitbus tools to query
status and to block-wait for events;
tail_eventsis a bounded long-poll, so even waiting needs no polling loop. This is the broadly portable path. - Push, for clients that support resource subscription. waitbus emits the
spec-standard
notifications/resources/updatedforwaitbus://repo/...subscribers. Whether your client surfaces that push (rather than only calling tools) depends on the client; most coding-agent MCP clients today are pull-only, and the published client matrices track Tools / Resources / Prompts, not a notification-surfacing column. - Inline render, for Claude Code. waitbus also emits an Anthropic-private
notifications/claude/channelthat Claude Code renders inline; other clients ignore the unknown method harmlessly per JSON-RPC 2.0.
Verified with: an executable test exercises the path end-to-end, not just
"speaks the spec": Claude Code (the MCP emit path, tests/test_mcp_e2e.py);
and, via the public Python SDK (waitbus.wait_for / subscribe), a
Pydantic AI and a LangGraph agent (tests/test_agent_integration_*.py);
plus the language snippets and the bash waitbus wait wrapper. Any other
stdio-MCP client can use the pull path above, but that is config, not a tested
end-to-end guarantee.
The command is always uvx --from waitbus waitbus mcp serve; only WHERE each client
stores its MCP config differs. Those locations and formats move across client
versions, so treat the table below as a starting point and check your client's
current MCP docs:
Aider is intentionally absent: as of 2026-05 Aider ships no native MCP client (tracked upstream in aider-AI/aider FR #4506; the only MCP path is the separate AiderDesk companion app). Point Aider-style or non-MCP agents at waitbus through the Python SDK (
waitbus.wait_for) or thewaitbus waitCLI instead.
uv must be on PATH (curl -LsSf https://astral.sh/uv/install.sh | sh).
See platform support.
As a Claude Code plugin
Until a marketplace / MCP-registry entry is published, load the plugin from a
local checkout of this repository. The manifest at .claude-plugin/plugin.json
references the repo-root .mcp.json, so --plugin-dir must point at the
checkout root (not a subdirectory):
--dangerously-load-development-channels is required to load a plugin from a
local directory that is not a published marketplace entry (the same invocation
documented in CONTRIBUTING.md). Once the marketplace
entry is published to the MCP Registry (see CHANGELOG.md for current status),
install it from there instead. Either way the plugin enables /waitbus slash
commands and the waitbus skill inside any Claude Code session.
Usage
Reading events
Key features
-
Sub-second
workflow_jobfailure detection. Catches matrix-cell failures within sub-second of GitHub delivering the webhook (wall-clock from webhook receipt, not from the CI failure itself), with no waiting for the parentworkflow_run.completedto arrive. Individual job failures surface immediately even when other matrix cells are still running. -
SQLite event store with idempotent writes.
INSERT OR IGNOREonevent_id(ULID, assigned at ingest time). Webhook redeliveries and ETag-poll duplicates are silently discarded; the DB stays consistent without a dedup queue. -
AF_UNIX
SOCK_STREAMbroadcast bus with length-prefix framing. Each wire frame is a 4-byte big-endian length followed by the JSON payload, portable across Linux and macOS (whereSOCK_SEQPACKETis unavailable). Non-blocking sockets with per-subscriber lag counters; slow subscribers are closed afterLAG_LIMITconsecutive drops and reconnect via EOF-triggered cursor reset. -
HMAC-SHA256 webhook signature verification. Secret staged once via
waitbus install-credentials github-webhook-secretinto a0600secrets.jsonunder the user state directory, read at unit-start. Missing or invalid signatures are rejected with 401 before the payload is read. -
SIGTERM drain guarantee. The broadcast daemon stops accepting new subscribers on SIGTERM, drains all in-flight frames to connected subscribers, closes them with EOF, unlinks its socket files, and exits 0. No events are dropped on
systemctl restart waitbus-broadcast. -
ETag polling fallback. A 45-second timer polls repos listed in
watched_repos.txtusingIf-None-Matchto avoid burning GitHub API rate limits. Coexists with webhook-driven repos. -
Prometheus metrics endpoint.
http://127.0.0.1:9000/metricsexposes ingestion-event and ingestion-error counters in Prometheus text format. The watchdog timer detects and records ingestion silence.
Configuration
Config file (optional): ~/.config/waitbus/config.toml
Environment variable overrides:
State directory resolution
Path resolution lives in waitbus._paths and follows the same
first-hit-wins precedence as the rest of the config chain:
WAITBUS_STATE_DIR/WAITBUS_RUNTIME_DIRenv vars (operator-controlled).platformdirs.PlatformDirs("waitbus")per-platform defaults:- Linux: state at
~/.local/state/waitbus/(honoursXDG_STATE_HOME); sockets at/run/user/$UID/waitbus/(honoursXDG_RUNTIME_DIR). - macOS: state at
~/Library/Application Support/waitbus/; sockets at a stabletempfile.gettempdir()-derived path (platformdirs' macOSuser_runtime_diris Apple-evictable cache, unsuitable for AF_UNIX sockets).
- Linux: state at
The shipped systemd units use StateDirectory=waitbus and
RuntimeDirectory=waitbus, so on Linux systemd creates the
directories with 0700 ownership before the daemons start, so no
operator mkdir is required.
Secrets (staged via waitbus install-credentials <name>):
The broadcast bus itself needs no secret: it is an AF_UNIX socket guarded by
the kernel's same-UID peer-credential check, so there is no subscriber token.
Secrets are stored in a single 0600-mode secrets.json under the user
state directory (<state_dir>/secrets.json); install-credentials reads the
value from --file or stdin and merges it in with an atomic replace, never
exposing it on the command line. At-rest protection of the file is delegated
to the host's full-disk encryption (FileVault / LUKS); see
SECURITY.md. waitbus doctor reports whether each required
key is present without printing its value, and exits 1 if a piece needed by
an enabled unit is missing.
Security
See SECURITY.md for the full HMAC threat model, platform assumptions, and vulnerability reporting process.
Same-UID trust model. The broadcast bus has no inter-process
confidentiality: any process running as your user can read every event on the
bus and can emit events under any sender name. waitbus is a single-user
workstation tool, not a security boundary between processes you run. Peer
authentication is by kernel-verified UID (SO_PEERCRED), so a different user
cannot connect, but same-user isolation is out of scope by design.
To verify the release artifacts (requires the gh CLI, network access, and a
published GitHub release that carries attestations -- so this is not a
run-it-offline-in-a-clone check):
See SECURITY.md for the full provenance verification procedure including SLSA, PEP 740, and SBOM checks.
Support
For bug reports, feature requests, and questions, see SUPPORT.md; it lists the security, abuse/conduct, and general channels and sets single-maintainer, best-effort expectations. Security vulnerabilities go through the private process in SECURITY.md, never a public issue.
Operating
Enable units to start automatically on login:
Rotate the webhook HMAC secret:
Headless server (SSH-only / no graphical session)
On a headless box without a graphical login session, two additional pieces are required for the systemd-user instance to run continuously even when no interactive session is open:
-
Keep the user manager alive across logouts. Run once:
Without this, the systemd user manager and all its units stop when the last interactive session ends.
-
No keyring unlock required. Secrets live in a
0600secrets.jsonunder the user state directory, read with a plain file read, no keyring, no D-Bus session requirement, and nopam_gnome_keyring.sodependency, so the daemon stack runs unmodified on a fully headless box.
Verifying the linger setting works. Open a fresh SSH session (do NOT start the units manually first) and run:
The result should be active. If the unit is not active without a manual
systemctl --user start, the linger setting is not in effect.
XDG_RUNTIME_DIR on linger-enabled hosts. When linger is enabled,
systemd sets XDG_RUNTIME_DIR=/run/user/$(id -u) automatically and creates
the broadcast socket path beneath it. No operator action is required; the
path is available from the first systemctl --user call in any session.
Wiring up repositories
Local sources need no per-repo wiring. pytest, Docker, and filesystem
events are produced on the box itself, so there is no webhook forwarder to set
up. Once the daemons are running (Quick start above) and the relevant producer
is emitting -- the pytest opt-in emitter, or the waitbus source docker /
waitbus source fs watchers -- waitbus wait --source pytest|docker|fs needs
zero per-repo configuration. The two subsections below are only for GitHub CI,
which arrives over a webhook and therefore needs a forwarder.
Event-driven (preferred for repos you own):
gh webhook forward creates an ephemeral GitHub-side webhook and tunnels
deliveries to the local listener. The secret must match the plaintext you
staged via waitbus install-credentials github-webhook-secret.
For a systemd-supervised forwarder (the shipped [email protected]
template), no operator action is needed: the unit reads the
github-webhook-secret key from secrets.json (via $STATE_DIRECTORY) at
start and passes it to gh webhook forward --secret.
ETag polling (repos you can read but cannot receive webhooks for):
Append owner/repo lines to watched_repos.txt under the resolved
state directory (waitbus doctor prints the path; on Linux without
WAITBUS_STATE_DIR overrides, that is ~/.local/state/waitbus/watched_repos.txt).
The 45-second timer picks them up automatically.
For SSH-only boxes, see Headless server
for the loginctl enable-linger requirement.
Platform support
Both platforms run the full daemon stack. Linux uses systemd-user units
with socket activation; macOS uses launchd LaunchAgents with manual
socket bind. The peer-credential UID check, doorbell primitive, and
install entry-point each dispatch inline at the leaf call site; there
is no IPCBackend-style abstraction layer, since those three leaf
sites are the only platform-divergent code.
Details in SECURITY.md.
Native Windows is not supported. waitbus's trust model rests on an
AF_UNIX socket plus a SO_PEERCRED same-UID peer check, which has no
Win32 equivalent. Windows users run waitbus under WSL2, which is a real
Linux kernel: pip install waitbus (or uv tool install waitbus) inside a
WSL2 distribution works exactly as it does on native Linux. See
docs/ARCHITECTURE.md for the platform rationale.
Zero default egress, no telemetry. The broadcast daemon, every
subscriber (waitbus wait, waitbus read-events, waitbus mcp serve), and
the emit() path make no outbound network calls; nothing is phoned
home. The only network surface is the optional inbound GitHub webhook
listener (loopback 127.0.0.1:9000, HMAC-verified), plus local OS keyring
access for credential storage. Outbound traffic happens only when you
explicitly opt into the ETag polling fallback (which calls the GitHub API
for repos you list). See SECURITY.md for the full network
surface.
Contributing
See .github/CONTRIBUTING.md for the per-surface dev guide, loading the plugin locally, version sync, and release flow.
License
waitbus is released under the MIT License. See LICENSE for the full text.
More
- CHANGELOG.md: version history and migration notes
- SECURITY.md: HMAC threat model, platform assumptions, reporting process
- .github/CONTRIBUTING.md: per-surface dev guide, loading the plugin locally
- docs/emitters/recipes.md: shell and docker emit recipes, plus the CloudEvents envelope mapping for external producers
- docs/emitters/claude-code-hook.md: emit a bus event on Claude Code session lifecycle hooks
來源:README.md,提交 12482fc
工具
0版本歷史
1- v0.2.0最新Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分