Skip to content

About

Local Rust CLI trust layer for humans, scripts, and LLMs.

Resources

Stars

70 stars

Watchers

0 watching

Forks

Repository files navigation

Switchboard

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.

The Problem

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

The Solution

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.

Why CLI First

  • 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

Status

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.

Install Today

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.

From Source

cargo install --path crates/switchboard-cli
switchboard --help

If you do not want a global install yet:

cargo run -p switchboard-cli -- --help

With Nix

nix build .#switchboard
./result/bin/switchboard --help

5-Minute Quickstart

Create 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 = true

Then try:

switchboard ns list
switchboard tools list
switchboard github.notifications.list --ns github.personal --json

For 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 = false

Google 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' --json

Each 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> --apply

Existing 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 --json

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

Authentication diagnostics

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 = 60

For 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"

How It Works

Every action follows the same rough shape:

  1. Resolve the namespace.
  2. Load auth and any namespace-scoped runtime state.
  3. Validate policy.
  4. Plan the action.
  5. Persist an operation record for writes.
  6. Require approval if policy says so.
  7. Execute.
  8. Audit the result.

The important bit is that writes are not just "run the command with more confidence."

They are first-class planned operations.

Real Workflows

GitHub Read

switchboard github.pull_request.read \
  --ns github.personal \
  --repo openai/codex \
  --number 1382 \
  --json

Google Calendar Draft, Approve, Apply

Draft 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 \
  --json

Then approve and apply:

switchboard op approve op_1234abcd --actor codex --note 'user approved'
switchboard op apply op_1234abcd --json

Aggregate Read Across Namespaces

switchboard google.calendar.list \
  --ns google.work \
  --ns google.personal \
  --json

Reads can fan out across namespaces. Writes stay single-namespace unless the caller explicitly asks otherwise.

Raw CLI Coverage

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.

LLM Usage Rules

If you are driving switchboard from an LLM:

  • prefer --json
  • pass --ns explicitly for writes
  • draft or plan writes first
  • repeat --ns only 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.

Prompt-Shaped Workflows

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.

Command Discovery

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 --pending

Config Resolution

switchboard looks for config in this order:

  1. --config <path>
  2. SWITCHBOARD_CONFIG
  3. ./switchboard.toml
  4. $XDG_CONFIG_HOME/switchboard/config.toml
  5. $HOME/.config/switchboard/config.toml

Non-Goals

  • 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

Docs

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

About

Local Rust CLI trust layer for humans, scripts, and LLMs.

Resources

Stars

70 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages