Skip to content

Reference: authorization ​

Two blocks in config.yaml decide who may do what. grants says what each actor holds; restrict says what is closed unless granted. Unlisted agents and code repos are open through human baselines. Credentials and jobs need explicit grants on every deployment. One policy table (authorize(actor, action, resource)) reads the grants on every surface — Slack, CLI, HTTP, MCP, schedules — so nothing here can be bypassed by choosing a different way to ask. Enforcement is at run time against the resolved agent or repo, after directives, thread stickiness, and every config layer.

grants ​

yaml
grants:
  slack:U0123ADMIN:                 # an admin, spelled out
    actions: all
    channels: all
    repos: all
  slack:U0456DEV:
    actions: [agent:run:coding, repo:write]
    repos: [acme/payments]
  access:svc:ops-bot:               # an Access service token, by its common_name
    actions: [runs:read, runs:write, friction:read]
    channels: all
  http:ci:                          # an ingress token's subject over /ingress …
    actions: [dispatch, runs:read]
    channels: [http:ops]
  mcp:ci:                           # … and the same subject over MCP
    actions: [dispatch, runs:read]
    channels: [mcp:ops]

Keyed by platform-namespaced actor id. Four axes, each a list of names or the explicit word all. Absent actions, channels and repos are empty; omitted codeRepos inherits repos. all is never a default.

AxisNamesDecides
actions<group>:read / <group>:write / <group>:exec for every command group (runs, friction, repo, config, memory, mcp, schedule, deploy, env, setup, help), agent:run:<name>, dispatch, deploy:writewhich commands and agents the actor may run
channelschannel ids (slack:C…, http:<name>, mcp:<name>)whose runs the actor may read beyond the public ones
reposowner/name slugs (case-insensitive)explicit repository memory ownership; also code access when codeRepos is omitted
codeReposowner/name slugs (case-insensitive), or allcode access independent of repo memory ownership; an explicit empty list grants no code access beyond the human baseline

Baselines — what an id holds listed or not ​

Actor idBaselineA grants entry …
slack:U… (a Slack user)the open chat commands (help/config/repo/friction/memory/mcp/schedule reads, memory:write, mcp:write) plus unrestricted code repos and agent:run:<name> for every agent not under restrict.agentsadds to the baseline
access:<sub> (a browser signed in through Access)every group's read, unrestricted agents and code repos, plus memory:write and mcp:write for its own scope (the web chat makes a signed-in browser a chat user)adds to the baseline
access:svc:<common_name> (an Access service token)nothingis exactly what it holds
http:<subject> / mcp:<subject> (an ingress token)nothingis exactly what it holds
schedule:<name> (a cron firing)what the schedule registry declares for itreplaces the declaration
cli:localeverything— (the local operator is always an admin)

Everyone on a surface — <ns>:* ​

yaml
grants:
  access:*:                         # everyone signed in through Cloudflare Access: the org, granted once
    actions: all
    channels: all
    repos: all
  slack:*:                          # every workspace member the bot hears
    actions: [runs:read]

A key slack:*, http:*, mcp:* or access:* is a surface entry; mcp:personal:* is a separate family for browser-approved personal credentials: the same four axes, held by every actor that authenticated on that surface. Who may authenticate there is decided elsewhere (Access admits the org, Slack the workspace, the token maps the credentials), so the set is one an operator already trusts. An actor's grants are the union of its own entry (or its baseline) and its surface entry — a person listed for extra rights keeps what everyone holds, and a personal entry never narrows the surface entry. access:* is browser sign-ins only: an access:svc: service token is a named credential and holds exactly its own entry. A surface entry is not an actor.

Never a baseline, held only by a grant (or all): config:write (config set/clear/instructions channel, a channel's MCP servers), repo:write (repo onboard/offboard/reconfigure/rebuild, friction propose, forgetting shared memories, org-wide MCP servers), every runs:* action, every *:exec, dispatch, deploy:write (the restart, the crash injection, and a probe bearer for the model proxy, POST /admin/model-proxy/bearer), trace:read (the bot's span log, GET /admin/trace/log). No entry with actions: all means nobody is an admin — the fail-closed default.

Shared permission guidance says "ask an admin" without listing or mentioning people. This applies to config summaries, permission errors, access refusals, repository onboarding and Ship preflight.

Validation ​

The load fails, naming the entry and field, on an unknown id prefix (slack:, http:, mcp:, access:, schedule: are the vocabulary), a misspelled all, an unknown axis, or a block that is not a mapping. * is a whole surface or the mcp:personal:* family: a partial subject (slack:U*) and schedule:*, access:svc:*, agent:*, cli:* are refused by name — schedules and service tokens are individually named identities, an agent derives its grants from its principal, the CLI holds everything. Nothing is ever widened to recover from a typo. grants and restrict are the only authorization keys config.yaml has: any other top-level key — permissions, a misspelling — is unknown and fails the load by name.

restrict ​

yaml
restrict:
  agents: [coding, ship]            # run only for actors granted agent:run:<name> (or `all`)
  repos: [acme/payments]            # requires compiled code access (codeRepos, or the repos fallback)

Both lists are optional and independent. An agent or code repo not listed is open through human namespace baselines; credentials and jobs always need explicit grants; a listed code repo requires compiled code access (codeRepos when present, otherwise repos, with additive human/surface grants and separate delegation intersections) — admins pass through all. Restricting never takes a grant away from anyone, and granting never restricts anyone else: the lock and the key are separate keys, so a config reads exactly as it is enforced.

  • restrict.agents must name registered agents (general, research, review, coding, ship); an unknown name fails the load rather than restricting nothing silently.
  • restrict.repos must be owner/name slugs; comparison is case-insensitive on both sides.
  • Agent restriction is enforced in dispatch() against the resolved agent, so agent:coding in a message or config set me --agent coding cannot bypass it; a refused user gets the run of the agent they may use, with a reply naming who to ask. config show lists the restricted agents unavailable to the caller.
  • Repo restriction is enforced before an executor is created (a coding run against a resident) and before a GitHub write (github_issue_create); the refusal names the repo.

Ingress tokens are credentials, not grants ​

SWITCHBOARD_INGRESS_TOKENS is { "<token>": { "subject": "<name>", "channel"?: "<name>", "email"?: "<address>" } }. A token identifies — it resolves to the actor http:<subject> over /ingress and mcp:<subject> over MCP — and what that actor may do is its grants entry and nothing else. Starting a run (POST /ingress, the MCP dispatch tool) needs the dispatch action; running a registry command as text or as an MCP tool needs that command's action. A token whose subject has no grants entry can do nothing. channel is where the token's dispatches are recorded (http:<channel>), not a right: to read a channel's runs, name it under channels. email names the person the token belongs to: their runs are then requested by them — the runs index, "show mine" and the costs page say their name, and the record keeps the token as authenticatedAs — while what the run may do stays exactly the token's grants entry (spec item 15); the same variable on the CLI is SWITCHBOARD_CLI_EMAIL. subject, channel and email are the whole entry; any other field (scopes, say) is ignored, so nothing in the token map can widen what the grants entry says.

See also ​

Migrating scoped credentials ​

Before deploying the scope fix, grant each HTTP/MCP/service or schedule actor the agent actions and repos it is intended to use. dispatch admits a request at ingress; it does not grant an agent or repository. For example, a review credential needs actions: [dispatch, agent:run:review] and codeRepos: [acme/payments]. Leave repos absent to preserve its lack of repo memory rights. Use codeRepos: all for an operator intended to use every code repo; name each intended agent explicitly, so future agents stay closed. A credential bound to a person keeps this ceiling, including when the person is an admin. Registry-only, plan-runner and deployment credentials keep their existing command grants.

Human open code access is compiled independently of explicit repo memory ownership. Neither a human open default nor a delegated human identity grants a credential additional repo memory access. Code and memory grants intersect separately through nested delegation.