Skip to main content

Workstation-local event bus and blocking wait/emit primitive: let any agent or script wait on or emit CI, pytest, Docker, and filesystem events without polling. GitHub Actions is source #1.

Project description

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

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, 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.

waitbus cross-harness demo — two different agent frameworks on one local bus; one agent fails and the peer reacts

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 with make hero; higher-quality MP4 at hero.mp4. For a gentler start, see the single-agent waitbus demo in Try it in 60 seconds.


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

uvx waitbus demo          # one command, no install needed

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.

waitbus single-agent demo — a blocking wait returns the moment a GitHub Actions workflow_run event lands, then prints the measured event-to-unblock latency

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 stats output. To wire waitbus against real GitHub webhook deliveries, follow the Quick start below.


Quick start

uv tool install waitbus          # install the package
waitbus init                     # one-time setup: state dirs, SQLite schema, scaffold files
waitbus install-credentials github-webhook-secret   # encrypt + stage HMAC secret via systemd-creds
waitbus install-systemd          # Linux: copy + enable the 8 systemd-user units
waitbus install-launchd          # macOS: copy + bootstrap the 4 LaunchAgent plists
waitbus read-events watch        # live tail of incoming events

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:

# pytest: wait for any session to finish (zero setup -- local source)
waitbus wait --source pytest --match 'fields.event_type="pytest_session"' --timeout 10m

# Docker: wait for a container to exit (zero setup -- local source)
waitbus wait --source docker --match 'fields.event_type="docker_container"' --timeout 30s

# Filesystem: wait for a watched path to change (zero setup -- local source)
waitbus wait --source fs --match 'fields.event_type="fs_change"' --timeout 5m

# GitHub CI: wait on a commit's terminal conclusion (needs webhook wiring -- see below)
waitbus wait --sha 7f3a1b2 --timeout 5m

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

%%{init: {'theme':'neutral','themeVariables':{'fontSize':'16px','fontFamily':'sans-serif'},'flowchart':{'nodeSpacing':28,'rankSpacing':46,'padding':14}}}%%
flowchart TB
    WH["<b>Webhooks</b><br/>GitHub · Alertmanager"]
    IP["<b>In-process</b><br/>pytest · docker · fs<br/>agents · plugins"]
    LS["<b>listener serve</b><br/>HMAC verify · etag-poll"]
    DB[("<b>SQLite event store</b><br/>seq PK<br/><code>INSERT&nbsp;OR&nbsp;IGNORE</code>")]
    BD["<b>broadcast daemon</b><br/>AF_UNIX&nbsp;fan-out<br/>doorbell&nbsp;wake · length-prefixed&nbsp;frames"]
    CLI["<b>CLI</b><br/><code>wait · on · top · read-events</code>"]
    MCP["<b>MCP server</b><br/><code>mcp serve</code>"]
    SDK["<b>Python SDK</b><br/><code>subscribe · wait_for</code>"]

    WH -->|webhook| LS --> DB
    IP -->|emit| DB
    DB -->|doorbell| BD
    BD --> CLI
    BD --> MCP
    BD --> SDK
    CLI -.->|since= replay| BD
    SDK -.->|since= replay| BD

    classDef src fill:#ffffff,stroke:#5a6672,stroke-width:1.5px,color:#1a1a1a
    classDef store fill:#f3ecdc,stroke:#a9772f,stroke-width:2px,color:#1a1a1a
    classDef core fill:#dbe7f3,stroke:#2f6690,stroke-width:2.5px,color:#1a1a1a
    classDef sink fill:#f4f6f8,stroke:#3d4752,stroke-width:1.5px,color:#1a1a1a
    class WH,IP,LS src
    class DB store
    class BD core
    class CLI,MCP,SDK sink

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 — no listener, 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

uv tool install waitbus              # or: pipx install waitbus
waitbus init                         # one-time setup: state dirs, SQLite schema, scaffold files
waitbus install-credentials github-webhook-secret   # encrypt + stage HMAC via systemd-creds
waitbus install-credentials alertmanager-hmac       # optional: alertmanager / watchdog HMAC
waitbus install-systemd              # Linux: copy + enable the 8 systemd-user units
waitbus install-launchd              # macOS: copy + bootstrap the 4 LaunchAgent plists

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:

Sub-command Purpose
waitbus init Bootstrap state dirs, SQLite schema, scaffold files
waitbus install-systemd Linux: copy + enable systemd-user units
waitbus install-launchd macOS: copy + bootstrap LaunchAgent plists
waitbus install-credentials Encrypt a credential via systemd-creds and stage it for LoadCredentialEncrypted=
waitbus doctor Validate install (paths, binaries, credential store, units, /metrics)
waitbus status Operational dashboard: DB row count, daemon liveness
waitbus verify-plugin Validate .claude-plugin/plugin.json
waitbus listener serve HTTP webhook receiver (loopback :9000)
waitbus broadcast serve AF_UNIX broadcast hub daemon
waitbus etag-poll run GitHub API ETag poll worker
waitbus mcp serve MCP server (stdio) re-emitting broadcast events as notifications
waitbus read-events watch Subscribe to broadcast bus, stream events live
waitbus read-events list Print recent events from the local cache
waitbus pr-monitor tick Roll job events into per-PR state
waitbus watchdog-check run Ingestion-silence detector

And, depending on platform, either 8 systemd-user units in ~/.config/systemd/user/ (Linux):

waitbus-listener.service    waitbus-broadcast.socket
waitbus-broadcast.service   waitbus-etag-poll.service
waitbus-etag-poll.timer     waitbus-watchdog.service
waitbus-watchdog.timer      waitbus-forward@.service

…or 4 LaunchAgent plists in ~/Library/LaunchAgents/ (macOS):

dev.waitbus.listener.plist     dev.waitbus.broadcast.plist
dev.waitbus.etag-poll.plist    dev.waitbus.watchdog.plist

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

from waitbus import subscribe, wait_for, EventFrame

# Block until one matching event arrives (returns the frame, or None on timeout).
frame: EventFrame | None = wait_for('fields.event_type="pytest_session"',
                                    source="pytest", timeout=600)
if frame is not None:
    print(frame.event_type, frame.fields)

# Or stream every matching event as it lands.
for frame in subscribe(source="docker"):
    print(frame.event_type, frame.fields)

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://spec.modelcontextprotocol.io/). What a given client can do with it depends on which MCP features that client implements — so here is the actual capability split:

  • Pull — any MCP client that supports Tools. Call waitbus tools to query status and to block-wait for events; tail_events is a bounded long-poll, so even waiting needs no polling loop. This is the broadly portable path.
  • Push — clients that support resource subscription. waitbus emits the spec-standard notifications/resources/updated for waitbus://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 — the published client matrices track Tools / Resources / Prompts, not a notification-surfacing column.
  • Inline render — Claude Code. waitbus also emits an Anthropic-private notifications/claude/channel that 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 — 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:

{
  "mcpServers": {
    "waitbus": {
      "command": "uvx",
      "args": ["--from", "waitbus", "waitbus", "mcp", "serve"]
    }
  }
}
Client MCP config location (verify against the client's current docs)
Claude Code ~/.claude/.mcp.json (or per-project ./.mcp.json)
Claude Desktop ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows)
Cursor ~/.cursor/mcp.json (or .cursor/mcp.json per project)
Cline (VS Code) ~/.config/Cline/cline_mcp_settings.json (Linux), or per-workspace .vscode/cline_mcp_settings.json
Codex CLI (OpenAI) ~/.codex/config.toml (under a [mcp_servers.waitbus] table — Codex uses TOML, not JSON)
Gemini CLI (Google) ~/.gemini/settings.json (under the mcpServers key)
Continue ~/.continue/config.json (under the mcpServers key)

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 the waitbus wait CLI instead.

uv must be on PATH (curl -LsSf https://astral.sh/uv/install.sh | sh). See platform support.

As a Claude Code plugin

claude --plugin-dir ~/.local/share/waitbus/plugin

Or use the marketplace entry once it is published to the MCP Registry (see CHANGELOG.md for current status). The plugin enables /waitbus slash commands and the waitbus skill inside any Claude Code session.


Usage

Reading events

# Live tail — one line per event; monitoring-friendly cadence
waitbus read-events watch

# Query the last hour in JSON (pass extra flags after the sub-command)
waitbus read-events list --since 1h --json

# Roll job events into per-PR state (watch PRs 7 and 9)
waitbus pr-monitor tick --pr 7 --pr 9

Key features

  • Sub-second workflow_job failure detection. Catches matrix-cell failures within sub-second of GitHub delivering the webhook (wall-clock from webhook receipt, not from the CI failure itself) — no waiting for the parent workflow_run.completed to arrive. Individual job failures surface immediately even when other matrix cells are still running.

  • SQLite event store with idempotent writes. INSERT OR IGNORE on event_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_STREAM broadcast 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 (where SOCK_SEQPACKET is unavailable). Non-blocking sockets with per-subscriber lag counters; slow subscribers are closed after LAG_LIMIT consecutive drops and reconnect via EOF-triggered cursor reset.

  • HMAC-SHA256 webhook signature verification. Secret staged once via waitbus install-credentials github-webhook-secret (encrypted by systemd-creds, decrypted by systemd into $CREDENTIALS_DIRECTORY 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.txt using If-None-Match to avoid burning GitHub API rate limits. Coexists with webhook-driven repos.

  • Prometheus metrics endpoint. http://127.0.0.1:9000/metrics exposes 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

[prometheus]
owner = "my-org"
repo  = "my-repo"

Environment variable overrides:

Variable Purpose
WAITBUS_PROM_OWNER Override Prometheus alert label: owner
WAITBUS_PROM_REPO Override Prometheus alert label: repo
WAITBUS_STATE_DIR Override the state directory (SQLite DB, scaffolds, cursors). Default resolved via platformdirs.
WAITBUS_RUNTIME_DIR Override the runtime directory (broadcast + doorbell AF_UNIX sockets). Default resolved via platformdirs.

State directory resolution

Path resolution lives in waitbus._paths and follows the same first-hit-wins precedence as the rest of the config chain:

  1. WAITBUS_STATE_DIR / WAITBUS_RUNTIME_DIR env vars (operator-controlled).
  2. platformdirs.PlatformDirs("waitbus") per-platform defaults:
    • Linux: state at ~/.local/state/waitbus/ (honours XDG_STATE_HOME); sockets at /run/user/$UID/waitbus/ (honours XDG_RUNTIME_DIR).
    • macOS: state at ~/Library/Application Support/waitbus/; sockets at a stable tempfile.gettempdir()-derived path (platformdirs' macOS user_runtime_dir is Apple-evictable cache, unsuitable for AF_UNIX sockets).

The shipped systemd units use StateDirectory=waitbus and RuntimeDirectory=waitbus, so on Linux systemd creates the directories with 0700 ownership before the daemons start — no operator mkdir required.

Credentials (staged via waitbus install-credentials <name>):

Credential name Required Purpose
github-webhook-secret Yes HMAC secret for GitHub webhook signature verification
alertmanager-hmac No HMAC secret for Alertmanager / watchdog webhook verification
broadcast-token No Optional bearer token for broadcast subscriber auth

Each credential is encrypted by systemd-creds encrypt --name=<name> and written to /etc/credstore.encrypted/waitbus.<name>.cred. systemd decrypts it into $CREDENTIALS_DIRECTORY/<name> at unit-start; the daemon reads the plaintext from there. waitbus doctor reports whether each encrypted file is present without decrypting it. It exits 1 if any required piece 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:

gh attestation verify $(python -c "import waitbus; print(waitbus.__file__)") \
  --repo astrogilda/waitbus

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

# Start the core daemons
systemctl --user start waitbus-listener.service
systemctl --user start waitbus-broadcast.socket

# Start the background timers
systemctl --user start waitbus-etag-poll.timer
systemctl --user start waitbus-watchdog.timer

# Health check — exits 1 on any missing required config or stopped unit
waitbus doctor

# Watch webhook deliveries arrive in real time
waitbus read-events watch

Enable units to start automatically on login:

systemctl --user enable waitbus-listener.service waitbus-broadcast.socket \
    waitbus-etag-poll.timer waitbus-watchdog.timer

Rotate the webhook HMAC secret:

# Encrypt the new value with systemd-creds and overwrite the staged file.
openssl rand -hex 32 | waitbus install-credentials github-webhook-secret
# Then update the GitHub repository webhook secret and Alertmanager config.
systemctl --user restart waitbus-listener.service

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:

  1. Keep the user manager alive across logouts. Run once:

    loginctl enable-linger <username>
    

    Without this, the systemd user manager and all its units stop when the last interactive session ends.

  2. No keyring unlock required. Credentials are stored as systemd-creds-encrypted files under /etc/credstore.encrypted/ and decrypted by systemd at unit-start using a host-bound key (TPM2 when available, /var/lib/systemd/credential.secret otherwise). There is no D-Bus session requirement and no pam_gnome_keyring.so dependency, 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:

systemctl --user is-active waitbus-listener

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 wiring. pytest, Docker, and filesystem events are produced on the box itself -- once the daemons are running (Quick start above), waitbus wait --source pytest|docker|fs works with zero per-repo setup. 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 \
    --repo=<owner>/<repo> \
    --events=workflow_run,workflow_job \
    --url=http://127.0.0.1:9000/webhook \
    --secret="<the same plaintext value you piped to install-credentials>"

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 waitbus-forward@.service template), no operator action is needed: the unit declares LoadCredentialEncrypted=github-webhook-secret:... and reads the decrypted value from $CREDENTIALS_DIRECTORY/github-webhook-secret at unit-start.

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

Component Linux + systemd macOS + launchd Notes
Python listener Yes Yes Loopback-bound, stdlib only
SQLite event store Yes Yes Single-file DB, no server
Broadcast daemon Yes Yes AF_UNIX SOCK_STREAM + length-prefix framing
Doorbell primitive os.eventfd SOCK_STREAM ping Inline-dispatched at the bind site
Peer-credential UID check SO_PEERCRED getpeereid() via ctypes LOCAL_PEERPID deliberately not used (see SECURITY.md)
Install entry waitbus install-systemd waitbus install-launchd Each refuses to run on the other platform
Process supervisor systemd-user units launchd LaunchAgents 8 systemd units, 4 launchd plists, all driven via waitbus
MCP server Yes Yes stdio MCP — runs in any spec-compliant client
Claude Code plugin Yes Yes Plugin manifest in .claude-plugin/

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

waitbus-0.1.1.tar.gz (749.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

waitbus-0.1.1-py3-none-any.whl (406.1 kB view details)

Uploaded Python 3

File details

Details for the file waitbus-0.1.1.tar.gz.

File metadata

  • Download URL: waitbus-0.1.1.tar.gz
  • Upload date:
  • Size: 749.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for waitbus-0.1.1.tar.gz
Algorithm Hash digest
SHA256 9c4174205a22186d61b637ef7c17234e83d44cfdc8dc161e08164cde7d6144ac
MD5 412051b2b9f2ceec13d3187f6cb5c6a8
BLAKE2b-256 b25c96c15f4b13dd998d29d8f8b615cf5370d6edc8e46aedfb71f2d61f2929da

See more details on using hashes here.

Provenance

The following attestation bundles were made for waitbus-0.1.1.tar.gz:

Publisher: release.yml on astrogilda/waitbus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file waitbus-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: waitbus-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 406.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for waitbus-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6652edc3191b244fb67215ccf1918267854f08b2f560e1f50ccc4307ddd34d1a
MD5 f033e17b8417344af400b1aa216e22a1
BLAKE2b-256 651e43db4c41b81ca542702adcffd685b9d06e2cf7a27871bf8e9006cfd29c23

See more details on using hashes here.

Provenance

The following attestation bundles were made for waitbus-0.1.1-py3-none-any.whl:

Publisher: release.yml on astrogilda/waitbus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page