Metadata-Version: 2.5
Name: switchboard-connector
Version: 0.1.11
Summary: Switchboard: the local client (connector, harness adapters, mailbox) for the verifiable A2A directory
License-File: LICENSE.md
Requires-Python: >=3.12
Requires-Dist: a2a-sdk[fastapi,postgresql]==1.1.2
Requires-Dist: cryptography<51,>=45
Requires-Dist: httpx<1,>=0.27
Requires-Dist: mcp==1.29.0
Requires-Dist: platformdirs<5,>=4
Requires-Dist: provably-sdk<1,>=0.3.1
Requires-Dist: psycopg[binary]<4,>=3.1
Requires-Dist: pydantic<3,>=2
Requires-Dist: python-dotenv>=1.2.3
Requires-Dist: sourcerykit<2,>=1.2.0b2
Requires-Dist: structlog>=26.1.0
Requires-Dist: uvicorn<1,>=0.35
Requires-Dist: websockets<17,>=15
Description-Content-Type: text/markdown

# Switchboard

<div align="center">
  <a href="https://github.com/ProvablyAI/switchboard/actions/workflows/ci.yml">
    <img src="https://github.com/ProvablyAI/switchboard/actions/workflows/ci.yml/badge.svg" alt="CI" />
  </a>
  <a href="LICENSE.md">
    <img src="https://img.shields.io/badge/license-BSL%201.1-blue" alt="License: BSL 1.1" />
  </a>
</div>

Switchboard is a directory and an encrypted task mailbox for sharing selected
local agent sessions with permitted people in your workspace. Readable prompts,
answers and transcripts never leave the harness on the machine that ran them — the
hosted service routes opaque ciphertext between permitted devices and stores
nothing it can read.

Switchboard is in alpha. Calls between separate signed-in machines have completed
end to end. Treat every other path as pilot material.

## How it works

```mermaid
flowchart LR
    UI[Switchboard Web App<br/>or Desktop App]
    U[Caller asks a question] --> H[Caller harness<br/>any supported harness]
    H <-->|MCP over local stdio| L[Local MCP server]
    L <-->|local loopback| C[Caller Local Connector]
    UI -.->|local service health and sharing controls| L
    UI -.->|identity, pairing and sharing controls| M[Switchboard Network Backend<br/>directory, grants, opaque mailbox]
    C -->|signed encrypted envelope over HTTPS| M
    M -->|opaque ciphertext only| W[Owner Local Connector<br/>and mailbox worker]
    W -->|decrypt + recheck grant and policy| A[Local A2A task and harness adapter]
    A --> S[Owner source session<br/>or caller-isolated continuation]
    S --> A
    A -->|encrypted result follows the reverse path| H
```

| Component | What it does | Where |
|---|---|---|
| Switchboard Web App | Authenticated browser UI: directory, sharing, inbox, settings | [switchboard-control](https://github.com/ProvablyAI/switchboard-control) behind Cloudflare Access |
| Switchboard Desktop App | Companion app: onboarding, agents, contacts, wallet and service controls | `apps/desktop` (Tauri: Rust + React) |
| Switchboard Network Backend | Hosted control plane: identity, grants, opaque mailbox routing, credit ledger | [switchboard-control](https://github.com/ProvablyAI/switchboard-control), Docker + PostgreSQL |
| Local Connector | Discovers local harnesses, enforces the answer-only policy, runs the mailbox worker and local MCP server | `src/switchboard/local`, supervised per platform (LaunchAgent, systemd user service, Scheduled Task) |

### Local handle routing

`ask:<handle>` resolves against both the owner-local directory and callable
network shares. An owner-local match stays on the machine and runs through the
existing private A2A invocation; its source session is never published to the
hosted directory. A remote match keeps using the encrypted mailbox. Conflicting
local and remote matches are reported as ambiguous, and an incomplete directory
leaves an unmatched handle unknown rather than unavailable. Local handle discovery reads
the current mapping from the local API on each lookup.

## Getting started

### Open the dashboard

Your workspace administrator runs the Switchboard Network Backend and has the
dashboard URL. Sign in with a permitted account and continue to the workspace.
Workspace membership admits you to the directory; each agent's owner still
chooses who may call it.

### Set up a machine

The desktop app is the easy path: it checks for `uv`, installs the connector,
registers your harnesses, runs the Verified A2A sign-in, pairs the machine and
starts the resident service. No source checkout is involved. Download it from
[switchboard-releases](https://github.com/ProvablyAI/switchboard-releases/releases/latest);
after that it updates itself from its About page. To build it yourself instead:

```bash
cd apps/desktop && pnpm install
pnpm tauri build --config '{"bundle":{"createUpdaterArtifacts":false}}'   # then open the built app
```

Prefer the terminal? The same steps, on macOS and Linux alike:

```bash
uv tool install switchboard-connector               # installs the `switchboard` CLI
switchboard install --switchboard-url <url>          # harnesses, Verified A2A, resident service
switchboard pair --device-name "my machine"
```

The PyPI distribution is `switchboard-connector` (the name `switchboard` belongs
to an unrelated project); the command it installs is `switchboard`. Update it
later with `uv tool upgrade switchboard-connector`.

`install` sets up a LaunchAgent on macOS, a systemd user service on Linux and a
Scheduled Task on Windows; `switchboard start`, `stop`, `status` and `uninstall`
manage it afterwards. Only the pair sign-in and the Share approval are manual, by
design.

The control-plane address is set once per machine and stored locally; the
connector worker and the harness-launched MCP both resolve that single setting,
so they cannot point at different servers. The desktop onboarding offers
`https://switchboard-api.provably.ai` (or `SWITCHBOARD_API_URL`, if the app was launched
with it) under Advanced, checks that the server's `/health` answers before
installing anything, and stores it. From the terminal, pass
`switchboard install --switchboard-url <url>`. Without a stored address the
connector uses `SWITCHBOARD_API_URL`, and otherwise the same production default.

The approval bridge on `127.0.0.1:8766` answers the control plane and the
desktop app. If the Switchboard web app runs somewhere else, name it:

```bash
switchboard install --web-app-url https://app.example.com
```

Comma-separate more than one, say a deployed app and your own copy. It is
stored as `SWITCHBOARD_WEB_APP_URL`, matched as an exact origin, and refused at
startup unless it is a complete `https` address (`http` only for `localhost`). Unset, the bridge
accepts exactly who it accepts today.

### Linux notes

`switchboard install` writes `~/.config/systemd/user/switchboard.service`; run
`loginctl enable-linger "$USER"` to keep it running while you are logged out,
and `journalctl --user -u switchboard` to read its log. The device credential
and mailbox keys live in the macOS Keychain; on Linux they are `0600` files
under `~/.local/state/ProvablySwitchboard/`, protected by your disk encryption.

### Windows

Windows runs Switchboard natively: `switchboard install` registers a per-user
Scheduled Task that starts the connector at logon and restarts it if it fails,
needing no elevation. Credentials are files under your profile directory, where
Windows' own ACL keeps other standard users out. `switchboard start`, `stop`,
`status` and `uninstall` drive the task exactly as they drive the service
elsewhere.

Two caveats. Harness CLIs installed by npm or bun are `.cmd` shims, which
Switchboard runs through `cmd.exe` — this path gets less real-world use than the
Unix one, so report anything that misbehaves. And Hermes Desktop's automatic
gateway discovery needs `ps` and `lsof`, so on Windows set `HERMES_API_URL`
instead.

### Windows (WSL2)

WSL2 is the alternative, and the better-travelled one: it is the Linux path
above, with two requirements.

- **Enable systemd**, so `switchboard install` can manage the service. Add this
  to `/etc/wsl.conf`, then run `wsl --shutdown` from Windows:

  ```ini
  [boot]
  systemd=true
  ```

  Without it, `switchboard install` says so and you run `switchboard run` in the
  foreground instead.
- **Install your harness CLIs inside the same distribution.** Switchboard
  discovers sessions from the Linux home directory, so CLIs installed on Windows
  are invisible to it.

WSL forwards localhost, so the dashboard in a Windows browser still reaches the
approval bridge on `127.0.0.1:8766`.

### Platform support

| Platform | State |
|---|---|
| macOS | Supported: desktop app or CLI, LaunchAgent service, Keychain credentials |
| Linux | Supported: desktop app or CLI, systemd user service, file-backed credentials |
| Windows | Supported: desktop app or CLI, Scheduled Task service, file-backed credentials under the user profile. Less exercised than the others; WSL2 is the safe alternative |

### Self-host the control plane

Discovery failures are isolated per harness. Healthy sessions remain locally
usable, and partial directory updates use `/api/connector/heartbeat/partial`.
The control plane retains unavailable harnesses' agent identities and shares,
disables their remote availability, and restores availability after discovery
recovers. An actual removal from a successfully scanned harness still revokes
the source's shares. Settings and agent rows distinguish harness unavailability
from source removal; check the affected harness's connection or sign-in.

Deploy the matching control-plane support before this connector. Older servers
reject the partial endpoint; the connector never retries that snapshot against
the full endpoint. Completely empty discovery still preserves the last published
directory. Neither a partial scan nor its failure prevents healthy local routing.

The hosted control plane (identity, directory, mailbox routing, credit
ledger) lives in the separate
[switchboard-control](https://github.com/ProvablyAI/switchboard-control)
repo — see that repo's README for `docker compose up` self-hosting.

## Security model

Owner-local MCP listings deduplicate repeated session IDs within a harness,
keeping the first record in discovery order. Different sessions with the same
name remain distinct: pass the listed `session_id` to select one explicitly.

- The Network Backend routes and revokes deliveries; it never sees a prompt,
  an answer or a transcript. Envelopes are opaque ciphertext at rest.
- Sharing is one atomic action: the owner's fixed answer-only policy installs
  locally before a share can be created, and the mailbox worker rechecks the
  grant and policy before every invocation.
- Revocation blocks future delivery immediately. It does not rewrite either
  person's local transcript.
- Verified A2A (optional, QEDB-backed) adds route-and-record-integrity proofs,
  shown as green, orange or red marks on an answer.
- Private caller (opt-in per share) shows the owner a caller-chosen alias
  instead of an account identity. The alpha credit ledger settles each answer
  in one double-entry transaction.

## Development

Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/). Verified A2A runs
on `sourcerykit`, installed from PyPI with everything else.

```bash
uv sync
make check                      # pre-commit, full test suite, build — the CI gate
```

Common targets: `make lint`, `make format`, `make typecheck` (strict mypy on
`src`), `make test`.

```text
src/switchboard/
├── harness/          adapters for Codex, Hermes, Claude Code, OpenCode and OpenClaw
├── local/            the resident service: CLI, connector, approval bridge, policy
│   ├── owner_app/    the owner-local A2A app (task API, agent cards, state, web UI)
│   ├── mcp/          the harness-facing MCP server and its remote calls
│   ├── policy/       owner policy authoring and enforcement
│   └── skills/       the switchboard-ask skill installed into every harness
├── mailbox/          envelope crypto, client and worker, commitments, QEDB ledger
└── qedb/             A2A task store, task proofs and the A2A profile
apps/desktop/         Tauri desktop companion app
scripts/              MCP launcher and the verified-A2A smoke test
docs/                 harness registration and session publishing
tests/                mirrors src/switchboard
```

The hosted control plane (`control/`) lives in the separate
[switchboard-control](https://github.com/ProvablyAI/switchboard-control) repo
— users installing this repo never pull down that server-side code.

### Releases

The connector and the desktop app ship separately, each from its own tag, so a
fix to one never forces a version bump of the other. Bump the version first
and tag to match.

| What | Version in | Tag | Produces |
|---|---|---|---|
| Connector | `pyproject.toml` | `connector-v0.1.1` | A wheel and sdist, published to PyPI |
| Desktop app | `apps/desktop/src-tauri/tauri.conf.json` (keep `package.json` and `Cargo.toml` in step) | `desktop-v0.1.1` | A **draft** release in `switchboard-releases` |

```bash
git tag connector-v0.1.1 && git push origin connector-v0.1.1
git tag desktop-v0.1.1   && git push origin desktop-v0.1.1
```

Release the connector first when the desktop app depends on its changes.

**Connector.** The tag publishes straight to PyPI, with no draft to review.
Publishing uses trusted publishing through the `pypi` GitHub environment, so
there is no token in the repo: configure the publisher once on PyPI (project →
Publishing → GitHub, workflow `release-connector.yml`, environment `pypi`), and
add a required reviewer to that environment if you want a deliberate click
before anything goes out. Running the workflow from the Actions tab publishes
too.

**Desktop app.** Each OS builds on its own runner (Tauri cannot
cross-compile): `.dmg` for Apple silicon and Intel, `.deb`, `.rpm` and
`.AppImage`, `.msi` and `.exe`. A tag run first checks the tag against
`tauri.conf.json`, then creates one draft that every platform uploads to. A run
from the Actions tab (where you can pick one platform) only builds: the bundles
are attached to the run and no release is created.

Releases go to the public
[`ProvablyAI/switchboard-releases`](https://github.com/ProvablyAI/switchboard-releases)
repo, because the app's **Check for updates** (About page) downloads them
without credentials. Besides the installers, each release carries a signed
update bundle per platform and a `latest.json`. The app reads
`releases/latest/download/latest.json`, so users get the update only once you
publish the draft: check that every platform has its installer, its update
bundle with a `.sig`, and that `latest.json` is there.

One-time setup, already done for this repo:

- `ProvablyAI/switchboard-releases` exists, is public, and has a commit on
  `main` (release tags are created from it).
- `plugins.updater.pubkey` in `tauri.conf.json` holds the updater public key.
  The workflow refuses to build while it is the placeholder.
- Repo secrets: `TAURI_SIGNING_PRIVATE_KEY` and
  `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (the updater private key and its
  password, generated with `pnpm tauri signer generate`), and `RELEASES_TOKEN`
  (a fine-grained token with *Contents: read and write* on
  `switchboard-releases` only).
- macOS signing and notarization secrets: `APPLE_CERTIFICATE` (the Developer
  ID Application `.p12`, base64), `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD` (an app-specific password) and `APPLE_TEAM_ID`.

The private key signs every future update. If it is lost, installed apps can no
longer update and users must reinstall by hand.

The same About page checks the connector against PyPI; updating it runs
`uv tool install --force` and restarts the service.

The macOS app is signed and notarized with the Developer ID secrets above. The
Windows installer is not code-signed yet, so SmartScreen warns on it. (The
updater signature above is separate from both.)

## License

Business Source License 1.1 — see [LICENSE.md](LICENSE.md). You may not offer
the Software as a commercial hosted service without a commercial license from
Provably Technologies Ltd. On the change date the license converts to
GPL-3.0-or-later.
