switchboard is a local Rust CLI trust layer for humans, scripts, and LLMs.
It gives you a stable command contract across ugly real-world tools like GitHub, Google Workspace, and MyChart, with explicit namespaces, draft-first writes, approvals, audit logs, and raw provider passthrough when the curated layer is not enough.
Most agent setups are fine at reasoning and bad at authority boundaries.
The real pain is boring and operational:
- work and personal accounts get mixed together
- credentials end up smeared across prompts, shells, and random caches
- writes happen too easily and too opaquely
- audit trails are weak or nonexistent
- backend integrations churn, but your calling contract should not
- many useful CLIs do not support clean multi-login on one machine
That last one matters more than it sounds. Google Workspace CLI style tooling is a good example, upstream login and cache behavior are not really built around multiple isolated local identities. switchboard treats each namespace as its own authority domain, materializes credentials separately, and uses per-namespace state dirs so one login does not stomp another.
The product is not "AI does tasks."
The product is "automation with sane trust boundaries."
Keep the durable interface as a CLI.
Let models, humans, scripts, and cron all shell out to the same surface.
Make every command resolve to an explicit namespace, run through policy, produce structured output, and leave an audit trail.
Humans / Scripts / LLMs
|
v
switchboard CLI
|
v
namespace + policy + audit + undo
|
+--------+--------+
| | |
v v v
GitHub Google MyChart
That gives you a stable local contract even if the backend changes later from one CLI to another CLI or from a CLI to a direct API.
- CLIs compose well with humans, scripts, cron, and models
- CLIs are easy to inspect, test, replay, and debug
- CLIs survive ecosystem churn better than model-specific glue
- LLMs need exact commands and stable JSON more than they need another chat wrapper
- MCP can exist later as a shim, but it should not be the center of the design
This repo is real, but still in the "tighten the public surface" phase.
| Area | Status | Notes |
|---|---|---|
| Source install | Ready | Build from source or use Nix today |
| Prebuilt release binaries | Wired, awaiting first tag | cargo-dist release pipeline is configured for checksummed archives plus shell and PowerShell installers |
| GitHub curated reads | Executable | Notifications, bounded PR/issue context, exact-commit CI, search/read |
| GitHub curated writes | Mixed | Comment tools plan cleanly, apply path is still evolving |
| Google curated reads | Mixed | Hydrated mail search, thread context, mail read and calendar list execute; drive search is planning-only |
| Google curated writes | Mixed | Mail draft and calendar create/delete execute, mail send is planning-only |
| MyChart raw CLI tools | Executable | Namespace-scoped mychart-cli passthrough, including inventory-backed raw leaf commands |
| Phone calls | Implemented, live validation pending | Local Rust CLI, replaceable LiveKit worker, encrypted transcripts; see setup |
| Raw provider CLI tools | Executable | Namespace, policy, approval, and audit still apply |
This workspace also contains adjacent CLIs. The public polish and open-source hardening work is focused on switchboard first.
See results, recovery, and readback for the versioned outcome contract, uncertain writes, verification boundaries, resumable reads, and execution timings.
For fewer caller round trips, start with the concise CLI workflows:
hydrated mail search, bounded mail/PR/issue context, inline read batches with
automatic checkpoints, and bounded CI waits. Discovery returns a short executable
list by default; --full exposes the complete catalog and schemas. Execution
supports compact JSON and --fields projections without changing stored receipts.
Today, the honest install paths are source or Nix. The release pipeline for prebuilt binaries is configured, but the first public tagged release still needs to be cut.
cargo install --path crates/switchboard-cli
switchboard --helpIf you do not want a global install yet:
cargo run -p switchboard-cli -- --helpnix build .#switchboard
./result/bin/switchboard --helpCreate a switchboard.toml in the repo root or at $XDG_CONFIG_HOME/switchboard/config.toml.
Minimal GitHub example:
[auth.github_personal]
provider = "github"
kind = "gh_cli"
account = "jessfraz"
[namespace.github.personal]
provider = "github"
account = "jessfraz"
auth = "github_personal"
default_read = trueThen try:
switchboard ns list
switchboard tools list
switchboard github.notifications.list --ns github.personal --jsonFor Google, declare the accounts you want to use. Switchboard defaults to CLI-managed login and creates a separate state path for each namespace:
[namespace.google.work]
provider = "google"
account = "work@example.com"
default_read = true
auth_scope_profile = "workspace_admin"
[namespace.google.personal]
provider = "google"
account = "personal@example.com"
default_read = falseGoogle credentials always use the file backend. No
GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND, config-directory, or client-secret
environment prefixes are needed for routine calls. Saved Google credentials
contain what gws needs to renew its login, so CLI-managed namespaces do not
resolve 1Password secrets before each read.
switchboard doctor --ns google.personal
switchboard google.cli.read --ns google.personal --json -- auth status
switchboard google.mail.search --ns google.personal --query 'newer_than:1d' --jsonEach namespace still needs an initial Google login. Place its downloaded OAuth
client configuration, client_secret.json, in the namespace directory reported
by doctor. Create that directory if it does not exist and keep the file outside
version control. Then use the normal approval flow for login:
switchboard google.cli.write --ns google.personal --draft -- auth login
switchboard op approve <operation-id> --applyExisting namespace logins need no new client setup. Standard bare auth login
uses gws auth login --full --services for Drive, Sheets, Gmail, Calendar,
Docs, Slides, Tasks, and People, and verifies the configured auth account.
Caller-provided login options are preserved.
The pinned gws 0.22.5 cannot resolve all Admin Directory, Groups Settings, or
Cloud Identity scopes using its service registry. Bare login for the optional
workspace_admin profile therefore reports unsupported before launching
consent. Existing saved admin sessions remain usable. Standard login does not
claim Cloud Identity coverage. Use explicitly verified native login options or
a gws version with the required services when additional access is needed.
Use a read-only identity request to verify access, including the expected account, before parallel work:
switchboard auth check --ns google.personal --json
switchboard auth migration-preview --ns google.personal --verify --jsonauth check supports Google and GitHub. Google identity verification uses
Gmail getProfile and requires Gmail access. Ordinary commands use credential
caches and existing sessions first, without environment-variable setup.
Rejected cached 1Password credentials are invalidated only after a structured
authentication rejection, then re-resolved once. A cache miss can make one
desktop-unlock attempt, bounded to 60 seconds. Commands automatically share a
60-second recovery window per provider/account; concurrent callers wait for its
saved result, and restarting the CLI does not immediately prompt again.
Automation can optionally set SWITCHBOARD_RUN_ID to limit recovery to one
attempt per provider/account for an entire task instead. Browser consent and
account mismatches stop the provider. These commands do not send or modify
provider content.
Migration preview preserves configuration and the namespace state directory.
--verify checks the proposed saved Google CLI session before suggesting the
opt-in change. File presence alone is never reported as valid authentication.
Default Google state paths live under namespaces/<namespace> beside the
operation database. A local switchboard.toml therefore uses
.switchboard/namespaces/google.personal; global config uses its own state
directory. Explicit state_dir paths keep their existing meaning and contents.
Namespace names are escaped when necessary to keep directory names distinct.
To reuse a login from an existing configuration without resolving its OAuth
secrets on every call, retain its state_dir and select google_cli explicitly:
[auth.google_personal]
provider = "google"
kind = "google_cli"
account = "personal@example.com"
[namespace.google.personal]
provider = "google"
account = "personal@example.com"
auth = "google_personal"
state_dir = "~/.config/gws-personal"Remove the old client_id, client_secret, and refresh_token references from
that auth block when changing its kind. Existing google_oauth and
google_oauth_file configurations remain supported for explicit secret
injection. When auth is omitted, a matching auth.google_<name> block takes
precedence over the implicit CLI-managed default.
switchboard doctor --json reports the selected config, namespace state,
credential-file presence, cache health, and provider binary versions. It does
not resolve secrets, authenticate accounts, or create an operation database.
File presence is a diagnostic hint, not a claim that a login is valid.
1Password sessions and item lookups are cached automatically. Cache-write
failures produce a warning instead of silently causing repeated lookups. Auto
mode detects graphics access in the current macOS login session and uses
DISPLAY or WAYLAND_DISPLAY on Linux. It disables desktop authentication in
headless sessions and CI, while preserving the CLI's saved app-integration
setting when graphics are available or detection is inconclusive. Explicit
1Password environment settings take precedence over detection and Switchboard's
configured mode.
Cached tokens can be reused when no explicit authentication choice overrides
them. You can configure a different mode once in Switchboard:
[one_password]
auth_mode = "auto" # auto, desktop, session, or service_account
timeout_seconds = 60For unattended access, configure named service-account profiles and opt specific secrets into them. The token file contains only the service-account token; do not put the token itself in configuration, shell startup files, or generated Nix store files.
[one_password.profiles.automation]
token_file = "~/.config/agent-credentials/automation.token"
[secret.automation_api_token]
kind = "onepassword_item"
account = "example.1password.com"
vault = "vault-id"
item = "item-id"
field = "token"
auth_profile = "automation"The token must be a regular file owned by the current user with no group/other
permissions, inside an owner-only directory (normally file 0600, directory
0700). Symlink token files are rejected. Relative paths resolve against the
configuration directory; ~ expands to the home directory. Named profiles
require an explicit vault. Prefer vault and item IDs to avoid ambiguous names
and reduce API requests. The token selects the 1Password identity; account
remains reference metadata and is not passed as a desktop CLI account selector.
Profile lookups never fall back to desktop authentication. Switchboard clears
conflicting OP_* environment settings and injects the selected service token
only into its op child. Provider children receive their resolved credential,
without 1Password bootstrap tokens or sessions. Unprofiled references retain
their existing behavior.
Profile caches contain only the requested field and are separated by profile, token generation, vault, item, and field. Switchboard checks the token file before returning a cached value, so removing it or rotating its token prevents reuse of the previous generation. Disk entries expire after one hour. Remote service-account revocation alone does not retract previously cached provider credentials; remove the local token file as well and revoke the provider credential if access already issued must stop. Owner-only files protect against other users, not arbitrary code running as the same user.
Noninteractive 1Password calls and captured provider commands have bounded
waits. Interactive provider login keeps its terminal attached so you can
complete browser consent. Use doctor to inspect the effective setup before
adding environment overrides.
MyChart works the same way, except the upstream CLI wants a config file instead of a config dir. switchboard still treats the namespace state as isolated local authority, it just materializes MYCHART_CONFIG as a file path inside the namespace state directory and pins MYCHART_ACCOUNT so Epic does not wander off into the wrong patient account. For the normal case, state_dir is enough. You only need an explicit auth block if you want switchboard to inject extra MYCHART_* overrides from env, files, or 1Password. If you do add one, switchboard will pick up the default mychart_<namespace> auth ref automatically, so namespace.mychart.ucla naturally pairs with auth.mychart_ucla.
[namespace.mychart.ucla]
provider = "mychart"
account = "UCLA Health"
default_read = false
state_dir = "/Users/jessfraz/.config/mychart-ucla"Schwab follows the same CLI-managed pattern as MyChart. switchboard scopes the Schwab CLI state file under the namespace state_dir by materializing SCHWAB_CONFIG, and you can optionally attach an explicit schwab_cli auth block when you want client id or secret overrides injected from env, files, or 1Password. If you omit auth, switchboard will automatically look for auth.schwab_<namespace>.
[secret.schwab_personal_client_id]
kind = "onepassword_item"
account = "my.1password.com"
item = "schwab cli"
field = "username"
[secret.schwab_personal_client_secret]
kind = "onepassword_item"
account = "my.1password.com"
item = "schwab cli"
field = "credential"
[auth.schwab_personal]
provider = "schwab"
kind = "schwab_cli"
account = "jessfraz"
client_id = "schwab_personal_client_id"
client_secret = "schwab_personal_client_secret"
[namespace.schwab.personal]
provider = "schwab"
account = "jessfraz"
auth = "schwab_personal"
default_read = true
state_dir = "/Users/jessfraz/.config/schwab-personal"Every action follows the same rough shape:
- Resolve the namespace.
- Load auth and any namespace-scoped runtime state.
- Validate policy.
- Plan the action.
- Persist an operation record for writes.
- Require approval if policy says so.
- Execute.
- Audit the result.
The important bit is that writes are not just "run the command with more confidence."
They are first-class planned operations.
switchboard github.pull_request.read \
--ns github.personal \
--repo openai/codex \
--number 1382 \
--jsonDraft first:
switchboard google.calendar.create \
--ns google.work \
--title 'Budget review' \
--start '2026-03-30T15:00:00-07:00' \
--end '2026-03-30T15:30:00-07:00' \
--draft \
--jsonThen approve and apply:
switchboard op approve op_1234abcd --actor codex --note 'user approved'
switchboard op apply op_1234abcd --jsonswitchboard google.calendar.list \
--ns google.work \
--ns google.personal \
--jsonReads can fan out across namespaces. Writes stay single-namespace unless the caller explicitly asks otherwise.
The curated tools are the nice typed layer, not the limit.
switchboard also generates raw provider tool surfaces from committed CLI inventories, so provider breadth does not depend on hand-writing every wrapper first.
Examples:
switchboard google.cli.read --ns google.work --json -- calendar +agenda --format json --today
switchboard github.cli.write --ns github.personal --draft -- --repo owner/repo issue comment 123 --body 'needs tests'Everything before -- belongs to switchboard.
Everything after -- is forwarded to the provider CLI unchanged.
Namespace auth, policy, approval, and audit still apply.
If you are driving switchboard from an LLM:
- prefer
--json - pass
--nsexplicitly for writes - draft or plan writes first
- repeat
--nsonly for aggregate reads - treat raw tools as escape hatches, not defaults
- if namespace resolution is ambiguous, ask instead of guessing
This repo is being shaped for both humans and LLMs on purpose. The docs should work as rendered pages and as plain Markdown chunks.
These are the kinds of user requests switchboard is meant to support well:
- "You see the latest email from the car wash people, make me a calendar event with the details." The model should search mail, read the winning message, extract the event fields, and draft the calendar event before applying it.
- "Look up all the past emails I sent to book the dogs at the dog hotel, make a new appointment for these dates by sending them a similar email." The model should search sent mail, read the relevant history, extract the reusable pattern, and draft the outbound message before sending it.
- "Review my open PRs and draft a comment on the riskiest one." The model should search or list PRs, read the candidate PRs, inspect the risky one, and draft the comment instead of posting it directly.
The model does the reasoning. switchboard provides typed, namespace-aware primitives with audit, policy, and approval around them.
The longer versions of these prompt workflows live in docs/llms/patterns.md.
You should be able to discover the surface without reading Rust:
switchboard tools list
switchboard tools describe github.notifications.list
switchboard tools describe google.cli.read
switchboard audit list
switchboard op list --pendingswitchboard looks for config in this order:
--config <path>SWITCHBOARD_CONFIG./switchboard.toml$XDG_CONFIG_HOME/switchboard/config.toml$HOME/.config/switchboard/config.toml
- building another chat shell
- replacing Codex, Claude, or ChatGPT
- forcing everything through MCP
- pretending every provider has a clean API
- shipping a fake "universal everything bus" before the core is trustworthy
- LLM operator guide: docs/llms/getting-started.md
- LLM workflow patterns: docs/llms/patterns.md
- Generated reference catalog: docs/reference/README.md
- Structured tool catalog JSON: docs/reference/catalog.json
- Prebuilt binaries and package-manager installs still need the first public release tag
The repo should eventually be the best place to understand:
- the problem
- the solution
- how the trust boundary works
- how to install it
- how to call it from both shells and LLMs