agent-switchboard 0.1.0__tar.gz

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.
Files changed (39) hide show
  1. agent_switchboard-0.1.0/.dockerignore +9 -0
  2. agent_switchboard-0.1.0/.github/workflows/ci.yml +72 -0
  3. agent_switchboard-0.1.0/.github/workflows/publish.yml +46 -0
  4. agent_switchboard-0.1.0/.gitignore +13 -0
  5. agent_switchboard-0.1.0/CONTRIBUTING.md +78 -0
  6. agent_switchboard-0.1.0/Dockerfile +40 -0
  7. agent_switchboard-0.1.0/LICENSE +21 -0
  8. agent_switchboard-0.1.0/PKG-INFO +354 -0
  9. agent_switchboard-0.1.0/README.md +314 -0
  10. agent_switchboard-0.1.0/docker-compose.yml +19 -0
  11. agent_switchboard-0.1.0/docs/api.md +178 -0
  12. agent_switchboard-0.1.0/docs/claude-code.md +253 -0
  13. agent_switchboard-0.1.0/docs/codex-cli.md +127 -0
  14. agent_switchboard-0.1.0/docs/concepts.md +158 -0
  15. agent_switchboard-0.1.0/docs/deployment.md +319 -0
  16. agent_switchboard-0.1.0/docs/encryption.md +531 -0
  17. agent_switchboard-0.1.0/docs/managed-hub.md +318 -0
  18. agent_switchboard-0.1.0/docs/quickstart.md +192 -0
  19. agent_switchboard-0.1.0/examples/coordinated_worker.py +155 -0
  20. agent_switchboard-0.1.0/pyproject.toml +77 -0
  21. agent_switchboard-0.1.0/src/switchboard/__init__.py +86 -0
  22. agent_switchboard-0.1.0/src/switchboard/auth.py +209 -0
  23. agent_switchboard-0.1.0/src/switchboard/cli.py +1033 -0
  24. agent_switchboard-0.1.0/src/switchboard/client.py +762 -0
  25. agent_switchboard-0.1.0/src/switchboard/config.py +156 -0
  26. agent_switchboard-0.1.0/src/switchboard/crypto.py +332 -0
  27. agent_switchboard-0.1.0/src/switchboard/mcp_server.py +628 -0
  28. agent_switchboard-0.1.0/src/switchboard/notify.py +67 -0
  29. agent_switchboard-0.1.0/src/switchboard/server.py +639 -0
  30. agent_switchboard-0.1.0/src/switchboard/store.py +918 -0
  31. agent_switchboard-0.1.0/tests/test_api.py +355 -0
  32. agent_switchboard-0.1.0/tests/test_auth.py +416 -0
  33. agent_switchboard-0.1.0/tests/test_cli_init.py +259 -0
  34. agent_switchboard-0.1.0/tests/test_cli_register_key.py +139 -0
  35. agent_switchboard-0.1.0/tests/test_cli_serve.py +83 -0
  36. agent_switchboard-0.1.0/tests/test_crypto.py +693 -0
  37. agent_switchboard-0.1.0/tests/test_mcp.py +355 -0
  38. agent_switchboard-0.1.0/tests/test_notify.py +234 -0
  39. agent_switchboard-0.1.0/tests/test_store.py +425 -0
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ .uv-cache/
3
+ .git/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ __pycache__/
7
+ *.db
8
+ dist/
9
+ build/
@@ -0,0 +1,72 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: pip install -e '.[dev]'
21
+ - run: ruff check .
22
+ - run: pytest -q
23
+
24
+ # The client, CLI and MCP bridge must install and work with httpx alone.
25
+ # This job fails if a server-only import leaks into them.
26
+ minimal-install:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.12"
33
+ - run: pip install -e .
34
+ - name: import client, CLI and MCP bridge without server or crypto extras
35
+ run: |
36
+ python -c "import switchboard, switchboard.cli, switchboard.mcp_server, switchboard.crypto"
37
+ python - <<'PY'
38
+ import importlib.util as u
39
+ leaked = [p for p in ("fastapi", "uvicorn", "starlette", "cryptography")
40
+ if u.find_spec(p)]
41
+ assert not leaked, f"optional deps leaked into the base install: {leaked}"
42
+ # crypto.py must still import without cryptography installed, so the
43
+ # error a user gets is an actionable message rather than ImportError
44
+ # at the top of an unrelated module.
45
+ import switchboard.crypto as c
46
+ assert c.AVAILABLE is False
47
+ try:
48
+ c.WorkspaceCipher.from_key("a" * 44, "w")
49
+ except c.CryptoError as exc:
50
+ assert "crypto extra" in str(exc), exc
51
+ else:
52
+ raise AssertionError("expected a CryptoError naming the extra")
53
+ PY
54
+ - run: switchboard --version
55
+ - name: the MCP bridge must answer initialize with no hub running
56
+ run: |
57
+ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
58
+ | switchboard-mcp | grep -q '"serverInfo"'
59
+
60
+ docker:
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+ - run: docker build -t agent-switchboard:ci .
65
+ - name: hub answers /health
66
+ run: |
67
+ docker run -d --name swb -p 8787:8787 -e SWITCHBOARD_TOKEN=ci agent-switchboard:ci
68
+ for i in $(seq 1 30); do
69
+ curl -sf http://127.0.0.1:8787/health && break || sleep 1
70
+ done
71
+ curl -sf http://127.0.0.1:8787/health | grep -q '"ok":true'
72
+ docker rm -f swb
@@ -0,0 +1,46 @@
1
+ name: Publish to PyPI
2
+
3
+ # Runs on a GitHub Release being published. Uses PyPI Trusted Publishing
4
+ # (OIDC) rather than a stored API token — nothing secret lives in this repo
5
+ # or in GitHub Actions secrets. The trust relationship (this repo + this
6
+ # workflow filename + the `pypi` environment) is configured once on PyPI's
7
+ # side: https://pypi.org/manage/account/publishing/
8
+ #
9
+ # To cut a release: bump `version` in pyproject.toml, merge, then
10
+ # `gh release create vX.Y.Z --generate-notes` (tag must match the version).
11
+
12
+ on:
13
+ release:
14
+ types: [published]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ build:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+ - run: python -m pip install --upgrade build
28
+ - run: python -m build
29
+ - run: python -m pip install --upgrade twine && python -m twine check dist/*
30
+ - uses: actions/upload-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+
35
+ publish:
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment: pypi
39
+ permissions:
40
+ id-token: write # required for Trusted Publishing
41
+ steps:
42
+ - uses: actions/download-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .uv-cache/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ *.db
11
+ *.db-wal
12
+ *.db-shm
13
+ .env
@@ -0,0 +1,78 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ python -m venv .venv && source .venv/bin/activate
7
+ pip install -e '.[dev]'
8
+ pytest -q
9
+ ruff check .
10
+ ```
11
+
12
+ That is the whole toolchain. There is no build step, no code generation, and
13
+ no service to run for the tests — they spin the app up in-process.
14
+
15
+ ## Layout
16
+
17
+ | File | What lives there |
18
+ |---|---|
19
+ | `store.py` | SQLite storage. All concurrency correctness is here. |
20
+ | `server.py` | FastAPI app: wire schemas, serialization, HTTP semantics. |
21
+ | `client.py` | Sync + async HTTP clients, and identity detection. |
22
+ | `cli.py` | The `switchboard` command. |
23
+ | `mcp_server.py` | MCP stdio bridge — speaks JSON-RPC directly, no SDK. |
24
+ | `config.py` | Env-driven settings and the TTL defaults/ceilings. |
25
+
26
+ The dependency direction is one-way: `store` knows nothing about HTTP,
27
+ `server` knows nothing about the CLI, and `cli`/`mcp_server` both go through
28
+ `client`. Keep it that way.
29
+
30
+ ## Things to preserve
31
+
32
+ **The client, CLI and MCP bridge must depend only on `httpx`.** An agent
33
+ should be able to join a hub without installing FastAPI. CI has a job that
34
+ fails if a server import leaks into them.
35
+
36
+ **The MCP bridge must not take an SDK dependency.** It implements the stdio
37
+ protocol directly so it cannot break when an SDK renames its API between
38
+ majors — which is exactly what happened between `mcp` 1.x and 2.0. If you add
39
+ a protocol method, add a test asserting its wire shape.
40
+
41
+ **Every record must expire.** A new table needs an `expires_at`, a read filter
42
+ on it, and an entry in `sweep()`. Correctness must not depend on the sweeper
43
+ having run — reads filter expiry themselves, and there is a test that says so.
44
+
45
+ **Read-then-write must happen inside `_tx()`.** That is `BEGIN IMMEDIATE`, and
46
+ it is the only reason two agents cannot both win the same lease. If you find
47
+ yourself reading a row and then writing based on what you read, it belongs in
48
+ one transaction.
49
+
50
+ ## Testing
51
+
52
+ Tests use synthetic timestamps (`now=1000.0`) rather than sleeping, so expiry
53
+ behaviour is tested exactly and instantly. Pass `now=` explicitly to every
54
+ store call in a test that cares about time — including the assertions, since
55
+ the reads default to wall-clock.
56
+
57
+ `test_store.py::test_concurrent_acquire_yields_exactly_one_winner` is the one
58
+ test that must never be weakened. It runs twelve real threads through a
59
+ barrier at one resource and asserts exactly one winner. If it becomes flaky,
60
+ something is wrong with the locking, not with the test.
61
+
62
+ ## Pull requests
63
+
64
+ - Run `ruff check .` and `pytest -q` before pushing.
65
+ - Add a test for anything behavioural. The suite is fast; there is no reason
66
+ not to.
67
+ - If you change the HTTP surface, update `docs/api.md` in the same PR.
68
+ - If you add or change an MCP tool, update its description — the description
69
+ is the entire interface an agent has to the tool, so it needs to say *when*
70
+ to use it, not just what it does.
71
+
72
+ ## Design questions
73
+
74
+ Open an issue before building anything that adds a fifth primitive. Four is a
75
+ deliberate ceiling: presence, leases, messages, blackboard. Most proposals
76
+ turn out to be one of those with a different name, and the ones that aren't
77
+ are usually asking Switchboard to be a queue, a log, or a scheduler — all
78
+ things it deliberately is not. See [docs/concepts.md](docs/concepts.md).
@@ -0,0 +1,40 @@
1
+ # Switchboard hub — one process, one SQLite file.
2
+ FROM python:3.12-slim AS build
3
+
4
+ WORKDIR /src
5
+ COPY pyproject.toml README.md LICENSE ./
6
+ COPY src ./src
7
+ RUN pip install --no-cache-dir --upgrade pip build \
8
+ && python -m build --wheel --outdir /dist
9
+
10
+ FROM python:3.12-slim
11
+
12
+ # The hub holds no source and no credentials, but there is no reason to run
13
+ # it as root either.
14
+ RUN useradd --create-home --uid 10001 switchboard \
15
+ && mkdir -p /data && chown switchboard:switchboard /data
16
+
17
+ COPY --from=build /dist/*.whl /tmp/
18
+ # The wheel path is resolved into a variable first: `/tmp/*.whl[server]` would
19
+ # be read by the shell as a glob with a [...] character class, which matches
20
+ # nothing and gets passed to pip verbatim.
21
+ RUN wheel="$(ls /tmp/*.whl)" \
22
+ && pip install --no-cache-dir "${wheel}[server]" \
23
+ && rm /tmp/*.whl
24
+
25
+ USER switchboard
26
+ WORKDIR /data
27
+
28
+ ENV SWITCHBOARD_DB=/data/switchboard.db \
29
+ PYTHONUNBUFFERED=1
30
+
31
+ EXPOSE 8787
32
+ VOLUME ["/data"]
33
+
34
+ # Kept to one physical line: a backslash-continued CMD inside a quoted Python
35
+ # one-liner is easy to break silently, and a healthcheck that always fails
36
+ # takes the container down.
37
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
38
+ CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8787/health', timeout=3)"]
39
+
40
+ ENTRYPOINT ["switchboard", "serve", "--host", "0.0.0.0", "--port", "8787"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Switchboard contributors
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.
@@ -0,0 +1,354 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-switchboard
3
+ Version: 0.1.0
4
+ Summary: An ephemeral orchestration hub for AI coding agents: presence, leases, messaging and a shared blackboard, all with TTLs.
5
+ Project-URL: Homepage, https://github.com/gald33/switchboard
6
+ Project-URL: Issues, https://github.com/gald33/switchboard/issues
7
+ Author: Switchboard contributors
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,claude,coordination,mcp,orchestration
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: httpx>=0.27
20
+ Provides-Extra: all
21
+ Requires-Dist: cryptography>=42.0; extra == 'all'
22
+ Requires-Dist: fastapi>=0.110; extra == 'all'
23
+ Requires-Dist: pydantic>=2.6; extra == 'all'
24
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'all'
25
+ Provides-Extra: crypto
26
+ Requires-Dist: cryptography>=42.0; extra == 'crypto'
27
+ Provides-Extra: dev
28
+ Requires-Dist: cryptography>=42.0; extra == 'dev'
29
+ Requires-Dist: fastapi>=0.110; extra == 'dev'
30
+ Requires-Dist: pydantic>=2.6; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4; extra == 'dev'
34
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
35
+ Provides-Extra: server
36
+ Requires-Dist: fastapi>=0.110; extra == 'server'
37
+ Requires-Dist: pydantic>=2.6; extra == 'server'
38
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'server'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # Switchboard
42
+
43
+ **An ephemeral orchestration hub for AI coding agents.**
44
+
45
+ When several coding agents work the same repo — one on your laptop, one in a
46
+ cloud session, one in CI — they need to coordinate. Today they mostly do it
47
+ through pull request bodies and review comments. That works, but it is the
48
+ wrong medium for most of what they have to say:
49
+
50
+ - **It's permanent.** "I'm taking the migration file, don't touch it for the
51
+ next 20 minutes" is true for 20 minutes and then it is litter in your
52
+ repository history, forever.
53
+ - **It's slow.** A PR comment is a message with a latency floor measured in
54
+ whole review cycles.
55
+ - **It can't say "now".** There is no way to ask *who else is awake right now*,
56
+ and no way to hold a claim that releases itself when you crash.
57
+
58
+ Switchboard is a small hub the agents talk to instead. Everything in it
59
+ **expires on its own**, because coordination state is not a record — it should
60
+ live exactly as long as the work does.
61
+
62
+ ```
63
+ local agent ─┐
64
+ cloud agent ─┼─► switchboard hub ─► SQLite (everything TTL'd)
65
+ CI agent ─┘ ▲
66
+ │ MCP tools / CLI / REST
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Four primitives
72
+
73
+ | | What it's for | Default TTL |
74
+ |---|---|---|
75
+ | **Presence** | Who is working right now, on what branch, on what task | 2 min |
76
+ | **Leases** | Exclusive claim on a resource key — *expires instead of leaking* | 15 min |
77
+ | **Messages** | Channel pub/sub with per-agent read cursors | 1 hour |
78
+ | **Blackboard** | Shared key/value scratch space for handoffs too big for a message | 24 hours |
79
+
80
+ That's the whole model. Direct messages aren't a fifth concept — a DM to agent
81
+ `bob` is just a message on channel `@bob`.
82
+
83
+ ### Why leases expire
84
+
85
+ This is the part worth dwelling on. A conventional "claim" — a row in a table,
86
+ a lock file, a label on an issue — is acquired explicitly and released
87
+ explicitly. The release is the half that gets dropped, because nothing ever
88
+ *asks* an agent whether it is still working. A session crashes, or merges and
89
+ moves on, and its claim sits there holding a piece of work hostage until a
90
+ human notices.
91
+
92
+ A Switchboard lease is acquired explicitly and released **by running out**.
93
+ Agents renew what they hold as a side effect of their heartbeat, so a live
94
+ agent keeps its claims and a dead one gives them up within a minute or two,
95
+ with nobody having to remember anything.
96
+
97
+ ---
98
+
99
+ ## Install
100
+
101
+ Not yet on PyPI — install straight from GitHub for now:
102
+
103
+ ```bash
104
+ # Agent side: client + CLI only (one dependency, httpx)
105
+ pip install "agent-switchboard @ git+https://github.com/gald33/switchboard.git"
106
+
107
+ # Hub side: also the server
108
+ pip install "agent-switchboard[server] @ git+https://github.com/gald33/switchboard.git"
109
+
110
+ # With end-to-end encryption
111
+ pip install "agent-switchboard[crypto] @ git+https://github.com/gald33/switchboard.git"
112
+
113
+ # With the MCP bridge for Claude Code / any MCP client
114
+ pip install "agent-switchboard[all] @ git+https://github.com/gald33/switchboard.git"
115
+ ```
116
+
117
+ Pin a commit or tag instead of tracking `main` once you depend on this for
118
+ real — append it to the URL: `git+https://github.com/gald33/switchboard.git@v0.1.0`.
119
+
120
+ ## The fast path
121
+
122
+ From the root of a repo:
123
+
124
+ ```bash
125
+ pip install "agent-switchboard[all] @ git+https://github.com/gald33/switchboard.git"
126
+ switchboard init
127
+ ```
128
+
129
+ That's one command instead of hand-editing three files: it writes `.mcp.json`,
130
+ adds the `SessionStart`/`Stop` lifecycle hooks to `.claude/settings.json`,
131
+ appends a coordination section to `CLAUDE.md`, and — if you didn't already
132
+ point it at a hub — generates a dev token into a gitignored `.env` so
133
+ `docker compose up -d` just works. It merges into whatever is already in
134
+ those files and is safe to run again. See
135
+ [Use it from Claude Code](#use-it-from-claude-code) below for what it wires
136
+ up and why, or skip straight to `switchboard init --help`.
137
+
138
+ ### Getting local, cloud, and CI agents onto the same hub
139
+
140
+ `switchboard init` with no `--url` defaults to a hub on `127.0.0.1:8787` — a
141
+ local dev instance reachable only from the machine it runs on. That's fine
142
+ for two terminals on your laptop, but a cloud Claude Code session or a CI
143
+ runner pointed at that same default would each spin up their *own* local
144
+ hub and never see each other, even though the workspace name (inferred from
145
+ your git remote) matches perfectly. Matching workspace names only matter
146
+ once everyone is actually talking to the same hub.
147
+
148
+ To get local + cloud + CI coordinating with each other:
149
+
150
+ 1. Deploy one hub somewhere all three can reach it — see
151
+ [Deployment](docs/deployment.md) for Docker, systemd, and TLS.
152
+ 2. Run `switchboard init --url https://your-hub` in the repo and commit the
153
+ `.mcp.json` it writes. Every clone of the repo — laptop, cloud session,
154
+ CI checkout — now points at the same URL and workspace with no further
155
+ config, because that file is part of the repo.
156
+ 3. Set `SWITCHBOARD_TOKEN` in each environment separately: your shell
157
+ profile locally, your cloud environment's secrets, your CI provider's
158
+ secrets store. `init` deliberately never writes the token into a
159
+ committed file, so this one step doesn't get automated away — it's the
160
+ one thing each environment has to be told on its own.
161
+
162
+ ## Run a hub
163
+
164
+ ```bash
165
+ export SWITCHBOARD_TOKEN="$(python -c 'import secrets;print(secrets.token_urlsafe(32))')"
166
+ switchboard serve --host 0.0.0.0 --port 8787 --db ./switchboard.db
167
+ ```
168
+
169
+ Or with Docker — no published image yet, so build it from a clone
170
+ (`docker-compose.yml` does the same thing):
171
+
172
+ ```bash
173
+ git clone https://github.com/gald33/switchboard.git && cd switchboard
174
+ docker build -t agent-switchboard .
175
+ docker run -p 8787:8787 -e SWITCHBOARD_TOKEN=secret -v swb:/data agent-switchboard
176
+ ```
177
+
178
+ The hub is one process and one SQLite file. It holds no source code and no
179
+ credentials — only who is awake and what they are saying to each other — so it
180
+ is cheap to run and cheap to lose.
181
+
182
+ ## Point agents at it
183
+
184
+ ```bash
185
+ export SWITCHBOARD_URL=https://hub.example.com
186
+ export SWITCHBOARD_TOKEN=secret
187
+ export SWITCHBOARD_WORKSPACE=my-org/my-repo
188
+ ```
189
+
190
+ ```bash
191
+ switchboard whoami # identity inferred from git + host
192
+ switchboard agents # who else is awake
193
+ switchboard claim db/migrations -m "adding 0142"
194
+ switchboard say build "migrations are mine for ~15m"
195
+ switchboard inbox --wait 25 # long-poll for messages
196
+ switchboard release db/migrations
197
+ ```
198
+
199
+ ## Use it from Claude Code
200
+
201
+ Add the MCP server (`.mcp.json` in your repo, or `claude mcp add`):
202
+
203
+ ```json
204
+ {
205
+ "mcpServers": {
206
+ "switchboard": {
207
+ "command": "switchboard-mcp",
208
+ "env": {
209
+ "SWITCHBOARD_URL": "https://hub.example.com",
210
+ "SWITCHBOARD_TOKEN": "secret",
211
+ "SWITCHBOARD_WORKSPACE": "my-org/my-repo"
212
+ }
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ Agents then get these as native tools:
219
+
220
+ | Tool | Does |
221
+ |---|---|
222
+ | `whoami` / `roster` | this agent's identity; who else is awake |
223
+ | `claim` / `release` / `claims` | take, drop and inspect leases |
224
+ | `say` / `dm` / `inbox` / `history` | channel and direct messaging |
225
+ | `board_set` / `board_get` / `board_list` | shared scratch space |
226
+ | `checkin` | heartbeat + renew leases + drain inbox, in one call |
227
+
228
+ `checkin` is the one that matters most in practice: a single tool call that
229
+ keeps the agent alive, renews everything it holds, and hands back anything
230
+ other agents said since last time.
231
+
232
+ See [`docs/claude-code.md`](docs/claude-code.md) for the full setup including a
233
+ `SessionStart` hook that registers the agent automatically and a `Stop` hook
234
+ that releases its leases.
235
+
236
+ The same MCP server works from any MCP-speaking coding agent — see
237
+ [`docs/codex-cli.md`](docs/codex-cli.md) for Codex CLI, which has an
238
+ equivalent `config.toml`-based hook system.
239
+
240
+ ## Use it from Python
241
+
242
+ ```python
243
+ from switchboard import Client, LeaseHeld, detect_identity
244
+
245
+ me = detect_identity()
246
+ with Client(agent_id=me.agent_id) as hub:
247
+ hub.register(name=me.name, kind=me.kind, branch=me.branch, channels=["build"])
248
+
249
+ try:
250
+ hub.acquire("db/migrations", note="adding 0142", ttl=900)
251
+ except LeaseHeld as exc:
252
+ print(f"{exc.holder} has it for another {exc.expires_in}s — doing something else")
253
+ else:
254
+ hub.post("build", "migrations are mine for ~15m")
255
+ ...
256
+ hub.release("db/migrations")
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Documentation
262
+
263
+ - [Quickstart](docs/quickstart.md) — hub up and two agents talking, in five minutes
264
+ - [Concepts](docs/concepts.md) — the model, the TTL rules, and what Switchboard deliberately is *not*
265
+ - [Claude Code setup](docs/claude-code.md) — MCP config, hooks, and prompt guidance
266
+ - [Codex CLI setup](docs/codex-cli.md) — same idea, `config.toml`-based hooks
267
+ - [Deployment](docs/deployment.md) — Docker, systemd, TLS, backups
268
+ - [HTTP API](docs/api.md) — every endpoint
269
+ - [End-to-end encryption](docs/encryption.md) — run a hub that cannot read its own traffic
270
+ - [Managed hubs](docs/managed-hub.md) — running one *for other people*: multi-tenancy, what actually runs out first, and how congestion should degrade
271
+
272
+ ## Encrypt it, and the hub can't read it either
273
+
274
+ A hub only ever needs to *route* and *compare*, never to read. So it doesn't
275
+ have to:
276
+
277
+ ```bash
278
+ switchboard keygen # prints a key, plus an opaque workspace name to pair with it
279
+ ```
280
+
281
+ Set both on every agent in the workspace. The key never reaches the hub. The
282
+ workspace name *does* — it is the routing key and cannot be encrypted — which
283
+ is why `keygen` hands you an opaque one rather than letting `acme/billing`
284
+ become the most descriptive string the hub holds.
285
+
286
+ Message bodies, blackboard values, lease notes, branch names and task
287
+ descriptions are sealed with AES-256-GCM before they leave the agent. Channel
288
+ names, lease resources and agent ids become opaque tokens the hub can still
289
+ compare for equality — which is all it needs to deliver a message or exclude a
290
+ second lease holder.
291
+
292
+ Costs 1.1µs to encrypt and 0.7µs to decrypt a message, against a ~1000µs
293
+ network round trip. Everything else — `claim`, `inbox`, `checkin`, the MCP
294
+ tools — behaves exactly as before.
295
+
296
+ What the hub stores once you do this:
297
+
298
+ ```
299
+ channel = mYkpn3DkU7rhr_Qjk_objQ
300
+ body = {"$swb":1,"n":"ARyT0f4DEQnXsJ2C","c":"4yTX0Vd1QuD_5Y38U7gkkZ5A…"}
301
+ ```
302
+
303
+ The hub needs **no changes and no configuration** to support this — it cannot
304
+ tell an encrypted workspace from a plaintext one, so it cannot be
305
+ misconfigured into weakening one. Plaintext is padded to size buckets before sealing, so message *length* does
306
+ not leak either. What remains is timing, volume, and which opaque tokens are
307
+ equal — [what that reveals, and what hiding more would cost](docs/encryption.md).
308
+
309
+ ## Sharing a hub between teams that don't trust each other
310
+
311
+ By default a hub has one token and every caller may use every workspace —
312
+ workspaces are a *namespace*, for keeping one team's coordination out of
313
+ another's way. That is the right shape for a hub your own agents share, and it
314
+ is what you get if you change nothing.
315
+
316
+ If a hub is shared by parties that shouldn't see each other's traffic,
317
+ workspaces become a *boundary* instead. Give `create_app` a resolver that maps
318
+ each key to the workspaces it may touch:
319
+
320
+ ```python
321
+ from switchboard import Principal, StaticKeyResolver
322
+ from switchboard.server import create_app # server extra; not at the package root
323
+
324
+ app = create_app(resolver=StaticKeyResolver({
325
+ "key-acme": Principal(key_id="acme", workspaces=frozenset({"acme/app"})),
326
+ "key-globex": Principal(key_id="globex", workspaces=frozenset({"globex/api"})),
327
+ "key-ops": Principal(key_id="ops", workspaces=None), # unrestricted
328
+ }))
329
+ ```
330
+
331
+ Every workspace-bearing endpoint then returns 403 outside a key's scope,
332
+ enforced in one shared dependency rather than per-handler — see
333
+ [docs/managed-hub.md](docs/managed-hub.md). Clients need no changes.
334
+
335
+ ## What this is not
336
+
337
+ - **Not a queue.** No delivery guarantees, no retries, no dead-letter. Messages
338
+ expire whether or not anyone read them. If you need work to survive, put it
339
+ in your issue tracker.
340
+ - **Not an audit log.** It forgets on purpose. Decisions that should outlive the
341
+ work still belong in a commit message, a PR, or a doc.
342
+ - **Not confidential from your own agents.** One key per workspace: everyone in
343
+ it reads everything in it. The encryption keeps out the hub and other
344
+ tenants, not your own colleagues.
345
+ - **Not an identity system.** Keys scope *which workspaces* a caller may touch;
346
+ within a workspace, agents are assumed to trust each other, because they
347
+ already share a codebase. Agent ids tell agents apart, they don't keep them
348
+ apart.
349
+ - **Not a scheduler.** It will tell you a resource is taken. It will not decide
350
+ who should have taken it.
351
+
352
+ ## License
353
+
354
+ MIT — see [LICENSE](LICENSE).