Skip to content

Orgabot is in private alpha. Request access, or join our Discord.

← All documentation
The organization

Connectors and tools

How a role reaches an outside service: GitHub Apps with short-lived scoped tokens, adopted MCP servers, brokered tool runs, and the three places a permission gets clamped.

A connector is the governed path by which a role reaches an outside tool, never your personal login.

orgabot connector add <org> <provider> [--scope read] [--base-url u] [--category c]
orgabot connector list | show <org> | revoke <org> <connector>
orgabot role connector grant <org> <role> <connector> [--scope read] [--repos a,b]
                          [--permissions k=v] [--expires ISO] [--security-review]
orgabot role tool grant|revoke <org> <role> <tool>

Secrets arrive through ORGABOT_CONNECTOR_SECRET or stdin and are stored in this platform's protected credential store, injected only at execution and redacted from logs and from the model itself. Which store that is depends on where Orgabot runs, and orgabot secrets status will tell you:

PlatformBackendWhat protects it
macOSLogin keychainEncrypted at rest under your login
LinuxSecret Service (GNOME Keyring, KWallet)Encrypted at rest under your login session
WindowsDPAPICiphertext keyed to your user account
Any, opt-inYour own cloud secret managerYour cloud account's IAM
FallbackA mode-0600 fileNothing. It is plaintext

Only the handle is ever recorded in Orgabot's own state. The material lives in the store.

The fallback is the one to watch. It is reached when the protected store on this platform is not available, and it holds material in the clear, so a backup or a sync client carries your credentials with it. Orgabot does not silently accept that: when a protected store was expected and is unavailable, new writes are refused rather than downgraded to plaintext, and orgabot secrets status says so. Repair the store and run orgabot secrets migrate to move the material in.

Remote custody is opt-in and never implicit. Set ORGABOT_SECRET_REMOTE=gcp_secret_manager:<project>[:<prefix>] and Orgabot resolves every handle through that secret manager using your ambient cloud identity. It fails closed: a provider it cannot reach is an error, never a quiet fall back to a different store holding different credentials. Moving existing material into it is a separate, explicit step that names both ends and every handle:

orgabot secrets migrate --from keychain --to remote secret_abc... secret_def...

Nothing is ever removed from the source, and a handle the target already holds is skipped rather than overwritten.

Available connectors

Connectors → Available in the dashboard lists every connector kind Orgabot ships as a card: what it is, which capability interfaces it provides (SourceControl, IssueProvider, MemorySink, IncidentReporter, ContextSource, OutputSink), whether it is connected, and Connect / Disconnect. Connected instances are nested under their kind, each with its scopes and its role-grant count.

Connect opens the kind's own flow in place instead of sending you to Security → Approvals & Access: the GitHub App form for github-app, a base-URL-and-credential form for a stored-secret kind, adoption for local-mcp, and the exact CLI command for a kind that keeps its own connection (Firebase).

Two things the library deliberately does not change:

  • It is not a new authority path. Connect posts to the same endpoint orgabot connector add and the Access workspace use, and Disconnect runs the same revoke: every role grant that depends on the connector is revoked and a circuit breaker opens. Connecting still grants nothing on its own.
  • Connected is not usable. A connector no role has been granted is connected but unusable, and the card says exactly that rather than letting the green pill imply an agent can act through it. Granting it is a separate, approved step (a connector grant needs CISO review). A kind that offers no scope at all (Firebase, which only observes) is labeled observation-only instead, because for it "no role grant" is the design and not a gap.

A connector is connected into one organization, so with more than one organization in scope the library asks you to select one rather than guessing.

Suggested connections on a role

A role that declares a requisite slot (a kind of tool it needs, not a vendor) shows Suggested connections chips on its detail page. A chip opens the flow that vendor actually connects through, and it says which one before you click it:

  • Give access to X when the organization already has X connected. There is no credential to paste, so the chip opens the guided grant for this role. That grant still goes to the CISO for review and applies only once approved.
  • Connect X for GitHub, which opens the GitHub App setup rather than asking for a key. GitHub has no pasteable credential.
  • Connect X for a token-based vendor with nothing connected yet, which opens the credential form. The form now names what to paste and which base URL Orgabot will call.
  • X already granted when this role already reaches that connection. Change or revoke it from the role's Can reach list.

A chip grants nothing by itself, in any of those cases.

Removal previews

Every removal on the dashboard previews its impact before it applies, the same way grants already do. The confirm for a revoke, disconnect, role removal, duty removal, or overlay removal states what is removed (which role loses which capability over which resources), what is still in place by another route (a pack-carried grant of the same tool, a separate connector of the same provider, the untouched connector ceiling), and what breaks, naming any mission still underway under the affected role. The two revoke shapes deliberately read differently: revoking one role's grant leaves the connector connected with every other role grant untouched, while Disconnect revokes every dependent role grant at once and opens a circuit breaker. A removal the governed path will refuse, such as removing the Owner seat or the organization's current authority anchor, says so in the preview instead of refusing after the click. The preview is read-only and computed from the same effective-access projection the grant path enforces; the removal itself still goes through the same audited revoke endpoints.

Per-connector logs

Each card carries a Logs panel: everything that connector produced, in one place, instead of spread across the broker audit, a mission transcript, and a thrown error. Entries are structured, so you can filter them by connection and by event:

EventWhat produced it
connect_attemptA connect was requested. Recorded before the outcome is known, so a connect that fails still leaves a trace.
connect_resultThe connect landed, as a new connection or a reconnect of an existing one.
disconnectThe connector was revoked, with how many role grants went with it and which circuit breaker opened.
token_mintA short-lived scoped token was minted for a connection, with the role, the expiry, and the permissions requested.
provisioner_stepOne step inside a provisioner, e.g. asking GitHub for an installation token against the live permission ceiling.
observationA read-only fact a connector observed, such as an App Hosting rollout state.
errorAnything the above refused or failed at, with the reason.

The same view from your terminal:

orgabot connector logs <kind> [--instance <id>] [--event <event>] [--limit <n>]

Three properties worth knowing before you rely on it:

  • Secrets are scrubbed before an entry is stored, never on the way out, so the file on disk (~/.orgabot/connectorLogs.json, owner-only) is the clean artifact. Both halves run: the broker's own redaction over known secret shapes (token=…, Bearer …, ghp_…), and a rule that blanks the value of any field whose name sounds like a credential, whatever it happens to contain. A private key, a minted token, and a stored bearer secret are all on the scrub list at the moment they are written.
  • The store is capped, at 500 entries overall and 100 per connector kind, oldest dropped first. The per-kind cap is what keeps a chatty connector from evicting a quiet one's history. So this is recent history, not the whole of it, and it is not an audit log: the authoritative record of who authorized what is the organization ledger and orgabot timeline.
  • A log entry carries no authority. It observes; it never grants, revokes, or authorizes. An error in the log does not make a connector unusable, and a clean log does not make one usable: that is still the role grant, and only the role grant.

GitHub as an App, not as you

Orgabot connects GitHub as a GitHub App. Each role grant gets short-lived (about one hour), repository-and-permission-scoped authority. The private key never leaves its trust domain:

  • in local auto-created and bring-your-own modes, the key stays in your protected credential store and the local broker mints the token;
  • in an isolated hosted deployment, the published orgabot-connect App key stays in a separate shared trust project. The broker checks the durable tenant claim, performs the bounded GitHub request, revokes the token, and returns neither key nor token to the tenant or worker.

One Orgabot organization can connect multiple GitHub accounts. Local-only and bring-your-own operation remain first-class and do not depend on the published App service.

orgabot connector connect github-app <org> --app-id <id> --installation <id> [--repos a,b]

Every gh read on behalf of a mission routes repo → installation through the connector App, fail-closed, with no ambient fallback.

Fail-closed delivery

A role with no GitHub grant cannot deliver through your ambient gh. Its mission holds its committed work until you grant access. Falling back would silently re-attribute the action to your personal identity and usually widen authority, which is exactly what the role boundary exists to prevent.

Permissions are clamped in three places

Granting a permission on GitHub is not enough for it to reach the token. It has to survive all three:

  1. the App's own permissions;
  2. the connector's ceiling;
  3. the per-role grant (and a sensitive grant needs CISO review before it exists at all).

A read of PR checks, for example, needs both checks:read and actions:read. And authentication is not authorization: a fine-grained token that can search a repo may still be unable to open a pull request.

The per-grant narrowing is visible, and it holds a mission early

--permissions narrows a grant inside its scope, so two roles can hold --scope write on the same connector and mint very different tokens: one at the App's full ceiling, one narrowed to issues=write by the Issues only access profile. That difference decides whether a role can deliver, so it is stated everywhere it is used:

  • the routing roster the model reasons over renders it, e.g. source_control scope=write permissions=issues:write (cannot push the mission's branch or open the delivery pull request ...), so a code change is not routed to a role that can only file issues;
  • the role brief a worker receives says the same thing, instead of the bare "Source control, write" it used to claim;
  • a mission that will change a repository holds at launch, before any worker runs, when its accountable role's grant cannot mint contents:write and pull_requests:write. The hold names the missing permissions and the orgabot role connector grant … --permissions command that widens them, or suggests routing to a role that already holds them.

A push that GitHub refuses despite all that reports the permission clamp rather than installation coverage. GitHub answers a token lacking repository contents with 404 Repository not found, the same answer it gives for an installation that does not cover the repo, so the hold now distinguishes the two instead of always sending you to fix an installation that may be perfectly correct.

There is no ambient escape hatch on the delivery path. --no-org exists on orgabot mission, not on orgabot ship: delivering an org-owned role's work under your own credentials would re-attribute the push and widen authority, which is what the role boundary exists to prevent.

Bing Webmaster Tools, read-only

Bing Webmaster Tools is an API-key connector for SEO and search analytics: impressions and clicks, query and page stats, crawl issues, link counts, and keyword data for a site your Bing account has already verified.

ORGABOT_CONNECTOR_SECRET=<your Bing Webmaster API key> orgabot connector add <org> bing-webmaster
orgabot role connector grant <org> <role> conn_<org>_bing-webmaster --scope read
orgabot tool run bing-webmaster GetUserSites
orgabot tool run bing-webmaster GetRankAndTrafficStats --site https://example.com

Get the key from Bing Webmaster Tools under Settings → API access → API key. It arrives the same way every other connector secret does, through ORGABOT_CONNECTOR_SECRET or stdin, never as a command argument, and the connector record holds only its handle. Connecting it grants nothing on its own: a role reaches it only through the connector grant above.

Four things are deliberate:

  • It is read-only, and the scope says so. read is the only scope this connector can grant, and its one tool, bing-webmaster, admits only Bing's Get* operations. Bing's write operations (URL submission, blocking a URL, submitting a feed) are refused before any request is made. Admitting one later would be a deliberate widening: a write scope plus a separate write-shaped tool, not something the read tool grows into.
  • The credential is an API key, not a bearer token. Bing authenticates with the key as an apikey query parameter, so the connector declares the api_key credential shape rather than pretending it is an Authorization: Bearer credential.
  • Because the key rides in the URL, no URL is ever printed. Every URL this tool surfaces in an error is masked, and every response body and error message is scrubbed of the key in both its raw and percent-encoded spellings.
  • No base URL is asked for. The API root is fixed at ssl.bing.com, which is also the host the connectivity preflight (orgabot connectivity) reports on for this connector.

MCP servers as connectors

Canonical registry and migration

Orgabot has one installation-wide MCP registry. Entries are deduplicated by a transport fingerprint (server name is descriptive; the fingerprint is the identity), so importing the same server from Claude and Codex does not create two connections. Claude and Codex configuration files are import evidence for Orgabot, but they remain normal harness configuration for direct Claude/Codex and iTerm use. Keep them by default. Orgabot reads them to plan or import a definition, but never treats either CLI config as the authority for an org attachment or role access. Project ownership is unchanged: a root project still owns its child projects and repository-wide records; importing an MCP definition does not move project ownership.

The dedupe boundary is deliberate: the same canonical transport is one server for the whole installation. Within one organization, equivalent unresolved source observations share one pending connection profile, so two CLI sightings and one connection profile produce one organization attachment, not one card per source CLI. The migration consolidates older source-scoped attachments, preserving the attachment that owns grants or OAuth state and retiring the duplicates without restoring revoked grants. Distinct organizations remain separate attachments, and distinct account or credential profiles remain separate connections even when they serve the same transport. A fresh successful enumeration or reconnect is validated evidence and updates the canonical server's inventory; it does not create a second server or widen a grant.

Use the read-only plan before applying anything:

orgabot connector mcp-migrate --dry-run          # preview legacy-record migration; no mutation
orgabot connector mcp-migrate --apply             # migrate legacy records; mutates registry bindings
orgabot connector mcp-plan <org>                  # preview CLI-source imports; no mutation

Retain both outputs. The plan is the proposed source-to-fingerprint mapping; the apply output is the receipt naming created, reused, skipped, or refused entries. Stop if a source is unexpected, a fingerprint changes, or a refusal would require guessing. Migration does not widen grants. A source import attaches only when you explicitly select it; it never grants a role access. Attach and grant explicitly, then retain the organization and role readbacks:

orgabot connector mcp-import <org> <server-name> [--enumerate] # mutates the org attachment
orgabot role connector grant <org> <role> <connector> --scope read  # mutates role authority
orgabot connector list <org>                                  # read-back; retain it

Manual configuration is the fallback when no CLI source exists:

orgabot connector mcp-configure <org> <name> --url https://server.example/mcp
orgabot connector mcp-configure <org> <name> --command /absolute/server --args-json '["--safe-flag"]'

Bearer custody uses mcp-authorize --token-env; stdio environment custody uses mcp-authorize-env --bind. OAuth connects from the organization's Connections card in the dashboard. The Capability Registry is read-only. Orgabot stores protected-secret handles, not Claude/Codex credentials. Retain the reconnect receipt and secrets status; stop if custody is not protected or the handle cannot be resolved.

After migration, keep intentional Claude/Codex source entries so the MCPs remain available directly from those harnesses. Verify the imported Orgabot definition, inventory, custody, grants, and one real source-independent call separately:

orgabot connector mcp-status                 # read-only last-known reachability
orgabot connector enumerate <org> <id>       # fresh validated inventory; updates canonical server
orgabot doctor                               # read-only consistency and migration checks
orgabot tool run-as <org> <role> <granted-read-only-mcp-tool>

For a canonical attachment, connector enumerate resolves only the Orgabot connection profile, performs a native initialize plus tools/list, and persists that result into the canonical server. It does not rediscover or use a Claude/Codex configuration. A successful connector check uses the same native path and also refreshes canonical inventory. Legacy attachments keep the older source-assisted enumeration path until they are migrated.

The routine tool run-as check does not require changing the harness config. Retain the status, doctor, connector/role readbacks, and tool receipt. If you want stronger independence evidence, temporarily disable one harness entry, repeat the check, and restore it afterward. Stop if the granted read-only call fails. Proceed only when the canonical fingerprint, inventory count, custody badge, role grants, and call all match the intended record.

The older discover and adopt names remain compatibility aliases. Prefer the canonical commands for new work:

orgabot connector mcp-plan <org>
orgabot connector mcp-import <org> <server-name> [--category c] [--enumerate]

Import stores no source credential. It may use the source CLI to observe an inventory during import, but subsequent invocation goes through Orgabot's broker and Orgabot-owned custody. Grants clamp the recorded inventory. The orgabot organization mcp-eagerness <org> [auto|suggest|off] knob tunes suggestions only; it grants nothing.

Harness-bundled MCP servers

Discovery can also see MCP launchers that are implementation details of an authoring harness, for example a binary inside a Codex or ChatGPT .app bundle. That is useful inventory, but it is not proof of a portable connector. Orgabot labels these entries source-linked and names the source harness. It does not ask for a credential when the launcher declares none, and it does not promise that Claude, an API worker, or a remote agent can run a private Codex binary.

Use three different states deliberately:

  • Managed by Orgabot means Orgabot owns a stable URL or independently launchable command plus any required secret handles. Compatible agents can use it through the broker according to role grants.
  • Source-linked means the MCP implementation belongs to the local harness installation. Orgabot may record it and the source harness may expose it, but it is available only where that harness and bundle exist. Treat this as a harness capability, not as cross-agent portability.
  • Reconnect required means the definition is portable but Orgabot still needs its own bearer, OAuth, or stdio environment custody.

Do not copy private harness credentials or app-bundle binaries into Orgabot. If the provider publishes a stable MCP URL or standalone launcher, configure or OAuth-connect that canonical definition instead. A future harness bridge may pass source-linked calls through the originating session, but that bridge must remain a distinct runtime route; it must not make a harness-local observation look broker-ready.

local-mcp is a connector kind, not one server: an org can adopt many, and they all share that provider id. So every operator surface names the adopted server and adds the kind as a short note, said once. The role's "Can reach" card reads analytics-mcp-highwire with local MCP server beside it; orgabot connector list, orgabot organization show, and the risk and health lines read analytics-mcp-highwire (local MCP server). Only a grant whose connector record can no longer be read falls back to the bare local-mcp kind, because a missing record is not a licence to guess which server it covered.

A server name comes from a config file Orgabot does not control, so it is shown only when it stays within the safe name charset (A-Z a-z 0-9 . _ -) and 64 characters. A name outside that is refused rather than trimmed to fit, and its grant is labeled with the bare local-mcp kind: a name carrying newlines or terminal escapes could otherwise forge a line on the very display that tells you which role can reach what. Rename the server in its own config if you want it named here.

Everything adoption and granting records is visible in one place: System → Capability Registry in the dashboard shows each adopted server, the roles holding grants on it, the tools each grant actually materializes after clamping, and the doctor findings with their fix commands. It is read-only. See The Capability Registry tab.

Capability discovery is deliberately executor-independent: tools discovered from any agent CLI are exposed to all workers, and role grants are the only axis of variation. "Works with one vendor's CLI, others later" is treated as a defect.

A server that needs authentication

Canonical remote servers may use bearer credentials or standards-based OAuth. Orgabot does not copy a credential out of Claude or Codex. Reconnect it into the organization-owned profile instead:

  • For a bearer token, place it in a temporary environment variable and run orgabot connector mcp-authorize <org> <connector-id> --token-env <NAME>.
  • For OAuth, use Connect OAuth on the organization's Connections card. Orgabot performs protected-resource and authorization-server discovery, requires PKCE S256, binds authorization and refresh to the canonical MCP resource, and keeps durable credentials behind secret-store handles.
  • For a stdio server, bind each required target variable with orgabot connector mcp-authorize-env <org> <connector-id> --bind TARGET=SOURCE_ENV.

Until custody is connected, role grants remain recorded but materialize no usable tools. orgabot doctor, the Connections card, and the read-only Capability Registry show the reconnect requirement. Reconnecting never grants or widens role access.

Legacy, unmigrated connectors retain their old source-CLI authentication path. That path exists only for compatibility and is not the authority model for a canonical profile. Migrate and reconnect so Orgabot no longer depends on it; the harness entry may still remain for direct harness use.

What a worker is told it holds

A mission brief announces only the MCP tools that worker can actually reach, which can be narrower than the role's grants:

  • Claude receives the role-filtered tool names and calls them through the Orgabot broker shim; the provider process receives no MCP credential.
  • Codex CLI uses the same broker shim. The direct API worker instead emits a bounded MCP call request that Orgabot rechecks against the live role grant before invoking.
  • Grok operate mode uses the broker shim already permitted by its governed shell route. Grok edit-only mode announces no MCP tools because it has no usable broker channel; Orgabot does not widen shell authority just to add one.

Nothing here changes what is allowed: grants are rechecked at the broker on every call. It changes what is disclosed, so a worker is never handed a list of tools it has no way to invoke.

Running a tool

orgabot tool run <tool> [args...] [--org <org> --role <role>]
orgabot tool run-as <org> <role> <tool> [tool args...]

run-as mints a single-use side token through the trusted store and drives the shim under an explicit identity. It is refused inside a worker session: a worker cannot elevate itself by calling the operator-side entrypoint.

The command broker

All mission shell goes through the CommandBroker: it refuses destructive patterns, redacts secrets, and enforces egress policy. In operate mode the gate additionally enforces the role's live tool and connector grants, fail-closed, writing a denial to the audit log.

The optional egress proxy narrows the network itself:

orgabot proxy [--project | --allow host1,host2 --network restricted|open|none]

When something is missing, ask rather than improvise

The sanctioned response to a missing capability is to hold the exact sub-goal and ask for the smallest sufficient capability, naming the role, connector, repository, resource boundary, scope, and expiry. Never invent credentials, never bypass the broker, and never broaden the request to avoid a second round trip. After the governed change lands, verify with a read-only check first, then resume the same objective.

A denial does not go straight to you. Orgabot walks a fixed escalation order and only the last rung is human: first it asks whether an existing grant can be requested inside the approval-autonomy envelope (a CISO agent can clear those without you), then whether another role already holds the capability and the sub-goal can be handed off, and only then does it surface the block. A direct tool grant is deliberately outside that envelope and is reserved to you: no agent may clear one for itself or for another role.

Request Grant: unblocking a capability gap from the dashboard

When the block that reaches you is a missing grant, the block card carries a Request Grant button. Everything on it comes from the recorded gap, not from the worker's prose, so the card names the organization, the role, and the tool before you click:

  • A tool grant takes effect immediately. You clicking the button is the approval (capability.tool.grant is human-reserved). The grant lands, the resolved blocker is cleared, and a mission still parked on its capability ask resumes without a second trip to answer it.
  • A connector grant does not. It says "This will queue a request for security review. It grants nothing until the CISO approves it," reports the queued request id afterwards, and leaves the mission held. A button that quietly queued while you believed you had granted would be a lie about your own authority, so the difference is stated before the click, never after.

The card also states the blast radius before you click, because a role tool grant is not narrowed to the project that hit the denial: it covers that role across every project it works on, and it does not expire. Revoke it with orgabot role tool revoke <org> <role> <tool>. One grant covers a role, not a mission, so the card lists every blocked mission the single click unblocks. Missions blocked under a different role are not included: granting to one role unblocks nothing for another.

The button refuses rather than improvising. An unknown tool id (one that does not resolve in the governed-tool registry), a role that cannot be resolved from the mission's own attribution, or a connector-backed tool with no connected instance all produce an explanation and grant nothing. It never falls back to a broader role to make itself work.

Two things it deliberately will not do for you:

  • It will not relaunch a mission that has already ended. A capability hold records no resume descriptor, so nothing durable says whether that mission ran with shell access, off which base branch, or under independent security review. Relaunching it would hand you back a mission wearing the same id at weaker authority: a security remediation could come back with the CISO gate silently gone. So the grant lands, and the result names the mission and the orgabot organization mission … command to relaunch it yourself, with the flags it needs.
  • It will not answer an unrelated question. Only a pending ask that actually names the gap or the denied tool is answered, and a --secret credential prompt never is. Your real prompt stays on screen.

The equivalent from your terminal, shown on the card so the path is always auditable:

orgabot role tool grant <org> <role> <tool>
orgabot role connector grant <org> <role> <connector>   # queues for CISO review

Choosing a GitHub path

Two callers, two orders:

An interactive assistant session (a human's terminal, acting as that human): a connected GitHub MCP tool first, then the gh CLI. MCP goes first because it does not consume the account's gh GraphQL budget, which real missions depend on.

A mission: the connector App assigned to its org and role, always. MCP then gh only for a mission with no configured connector. A role-owned mission with a configured connector does not fall back: it holds and surfaces the block.

Regardless of caller: REST and GraphQL have separate rate limits, so exhausting one leaves the other usable. gh pr create is GraphQL; gh api repos/O/R/pulls -X POST is REST: same identity, same authority, and a strictly smaller change than switching connectors. On exhaustion, stop retrying the same command and report the failing path with GitHub's reset time.

Can an MCP server actually be reached?

orgabot connector mcp-status

Which plane a connector lives on is a design fact and does not change from minute to minute. Whether it can be reached right now is a different question, and it is the one you have when work stops. A connector badged local is telling the truth about its plane while the broker serving it is down, and nothing in that badge says so.

Five answers, and the last two are deliberately not the same:

StateMeaning
localRuns on this host and this broker reaches it directly
broker_reachableRuns elsewhere; this broker reached it. Available only while this broker runs
cloud_readyServed by the always-on cloud broker, and reached
unavailableProbed, and it did not answer
unknownNot probed, or the probe itself failed

`unknown` is never folded into `unavailable`. "It is down" and "I did not look" send you to different places, and only the first is a fault in the server. It is also never folded into a reachable state, which is the direction that would matter.

Some connectors report unknown for a reason that is a configuration fact rather than a liveness one, and the detail says so: a transport whose stored URL carries a [redacted] placeholder where discovery removed a credential, or a stdio server whose credential comes from environment values Orgabot deliberately never kept. Those are the same two configurations that make a real tool call fail closed, so the probe does not pretend otherwise by opening a socket to them.

An unreachable server that no role holds a grant on raises nothing. That is tidy state, not an incident, and paging for it teaches you to dismiss the kind - which is how the real one gets dismissed too. When a role does hold one, an incident appears on the Incidents view naming the roles that are blocked.

A mission holds on a capability nothing has confirmed. Before any worker starts, the launch path reads this observation for the connectors the mission's accountable role holds a grant on. unavailable and unknown both hold: a plane nothing confirmed is not a plane a mission may start against. The mission stops with its branch and worktree intact, and the hold names the server, the role, and the grant (never a transport URL or a credential handle).

Four holds, because the fixes differ:

HoldWhat it meansWhat clears it
unreachableProbed, and it did not answerStart the server again, or revoke the grant
unprobeableA configuration fact: a [redacted] transport, or a stdio credential Orgabot never keptRe-adopt the connector, or revoke the grant. Restarting the server will not help
not observedNo pass has measured itLet the availability pass run (it runs from the dashboard)
staleThe last observation is older than ten minutesLet a fresh pass run

Staleness is a decision, not an accident: an observation older than ten minutes is evidence that the plane was up then, not that it is up now, so it reads as unknown and holds.

A stale or unobserved hold does not say the server was unreachable, because nothing observed that. Only the first row above is a server that was probed and did not answer; the other rows are a configuration fact, an absent observation, and an old one. The hold code stays mcp_capability_unreachable for compatibility, and what you read names the actual blocker.

Who owns the wait follows the same distinction. orgabot session <mission> --json reports a wait contract for the hold: stale and not-observed are owned by Orgabot and report "selfResolving": true, with the next availability pass as the condition that ends the wait, while unreachable and unprobeable are owned by you and report "selfResolving": false - no pass brings a stopped server back, and no pass ever clears an unprobeable connector.

A held mission resumes on its own. Once a later availability pass observes every capability it was waiting on reachable, the dashboard relaunches the same mission, under the same id, organization, and role, through the ordinary mission path - so every gate still applies. Resuming is idempotent: a second pass over the same mission does nothing. No launch or resume opens a transport; both read the stored observation, exactly as orgabot connector mcp-status does.

The probe lists; it never invokes a tool. A stdio server is checked structurally

  • can it be launched under the same policy the real spawn applies - rather than

started, because a health check must not run your tools as a side effect.

Running an agent-authored connector's actions

An installed connector package declares actions with a classification: read, write, destructive, administrative, or financially_sensitive. Two of them run today.

orgabot organization department extension runtime grant <org> <draft> write \
  --reason "month-end corrections" --until 2026-09-01T00:00:00Z
orgabot organization department extension runtime invoke <org> <draft> reports.update \
  --body '{"reportId":"r-1"}' --key close-2026-07
orgabot organization department extension runtime list <org>

Installing, binding, and activating a package confer nothing at runtime. An action runs only under a grant that names its class, has not expired, and covers that action. Issuing one takes the same gate granting a role a connector takes: owner trust, a security review, and an approval. Every grant expires; there is no open-ended form.

`destructive`, `administrative`, and `financially_sensitive` actions are declared and refused, by name, with the reason. Each needs something this stage does not have: a reversal story, its own approval class, or a spend authority. Running them under the write rules would be governance in appearance only.

A write must carry an idempotency key. It travels to the provider as Idempotency-Key, so a retry is recognizable as the same write rather than a second one.

Every invocation revalidates everything: the package fingerprint against the one the credential was connected for, the connector's status, provider, and host, the credential itself, and the grant. A connector that was fine an hour ago is not evidence that it is fine now.

Egress is the exact host from the connector's own base URL, never the package's, so a package cannot widen its own reach. An egress refusal surfaces as itself rather than as the provider being unreachable.

A write's body is validated against the declared request schema before anything is sent: a missing declared field, or one the schema does not declare, is refused. The response is validated too, and a 2xx satisfying none of the declared fields is recorded as a schema mismatch, not a success. Then it is dropped: the record keeps the status and which declared fields came back, never a value and never the credential.

Deactivating an extension stops its actions. Deactivation deliberately leaves the connector and its grants alone, so the runtime checks the activation itself.

A non-read whose request left is recorded as may have changed provider state, including one that returned an error, and including one that got no reply at all. A 500 from a write endpoint does not mean nothing happened, and a timeout on a POST is the most likely case where it did.

A refused invocation is recorded too. For the one surface that can change a provider's state, the refused attempts are the ones worth finding later.

Moving an extension to a different connector

A provider changes its host, its API version, or the shape of what it returns. replace moves an installed extension onto the new connector and migrates the declared schemas, and it tells you first what that costs.

# What would change, computed on a copy. Writes nothing.
orgabot organization department extension replace preview <org> <draft> \
  --base-url https://books.example.com/v1

# Apply it. --approve is required whenever anything widens, a schema changes,
# or the credential does not carry.
orgabot organization department extension replace apply <org> <draft> \
  --base-url https://books.example.com/v1 --approve

# Re-run the fixtures against the package as it now stands.
orgabot organization department extension replace retest <org> <draft> suite.json

# Undo, restoring the previous connector and schemas verbatim.
# --approve is required when it will revoke a grant or a credential handoff.
orgabot organization department extension replace rollback <org> <replacement-id> --approve

orgabot organization department extension replace list <org>

Some things are worth knowing before you run it.

The credential does not follow a connector that became a different thing. If the host, the base URL, the capability category, or a provider scope the old package did not declare changed, the credential is not copied and the runtime grants are revoked. Not marked stale: absent. The extension cannot run until you supply a credential and re-issue access through the paths that govern those. An extension that kept working across a provider swap would mean the approval that authorized the first provider silently authorized the second.

An omitted flag means unchanged. --category and --provider-scopes are read from the package as it stands when you leave them off, so a plain --base-url move does not quietly clear them, and a package declaring several categories keeps all of them. Both take a comma-separated list.

A base URL is compared whole. Moving /v1 to /v2 on the same host is a difference, because the runtime requires the connector instance's base URL to equal the package's. It breaks the credential carry for the same reason a host move does.

The migration comes from a file. --migration steps.json takes a list of {kind, schemaId, field, toField?, reason, sensitive?} steps.

A migration is declarative. Fields are renamed, dropped, or added, each with a reason, and every drop is reported. Orgabot does not run migration code a package supplied: an extension package is inert JSON, and it stays that way. Sensitivity follows a field through a rename, and an added field can be marked sensitive so it is redacted from the start.

Every replacement, carrying or not, leaves the package owing a retest, so run replace retest before expecting it to be invocable again.

A schema declares LEAF field paths, in dotted form (data.id). A schema declaring a container name where the body nests below it is refused at invocation and by the fixtures alike; migrate it to the leaf paths with replace apply --base-url <unchanged> --migration steps.json.

A rollback is a move too. It restores the previous package byte for byte, and it revokes the runtime grants and supersedes the credential handoff exactly as the replacement did. A grant minted for the connector you are moving away from must not survive to authorize writes against the one you are moving back to. Withdraw a grant directly with orgabot organization department extension runtime revoke <org> <grant-id> --reason R.

A rollback owes no retest, because it restores the fixture record with the bytes: both are byte-identical to what passed at install.

A replaced package owes a retest. A draft is otherwise immutable, and its recorded fixture evidence describes the package as it was, so the runtime refuses to invoke until replace retest runs the suite against the package as it now stands. Only a pass clears it.

Signed packages, publishers, and revocation

A package can carry a signature. What that signature settles is who published it, and nothing else.

# Name an identity whose signatures this organization recognizes.
orgabot organization department extension publisher register <org> publisher.json
orgabot organization department extension publisher list <org>

# Stop something being installed or run, permanently.
orgabot organization department extension package revoke <org> publisher acme-tools \
  --reason "signing key was exposed in a public repository"

# A key revocation names both, because key ids are unique only per publisher.
orgabot organization department extension package revoke <org> key acme-tools:key-1 \
  --reason "this key alone was exposed"
orgabot organization department extension package revocations <org>

# Installed packages a later revocation says may no longer run.
orgabot organization department extension package quarantined <org>

Security → Package Signing in the dashboard is the same eight governed endpoints on a page: the quarantine listing first, because it answers the question you arrive with - which installed packages did this revocation just disable - then the recognized publishers, then every revocation, then a read-only signature check you can run before installing. Every write there posts to the endpoint the CLI calls and decides nothing in the browser. A failed load keeps the lists that were already on screen and reports the error beside them, because an empty revocation or quarantine list is a false all-clear.

publisher.json names the publisher and its public keys, each with a validity window. Registration refuses a private key and tells you to rotate the pair, and it refuses to redefine a key id: rotating means adding the new key and letting the old one expire, so a package signed under the old key still verifies as what it was.

Pass the signature at install:

orgabot organization department extension install-draft <org> package.json fixtures.json \
  --signature signature.json

A signed package is imported, not organization-authored. Its trust state is imported_unreviewed. A replacement keeps the recorded publisher and key, so a revocation issued later still reaches the package, but that record is not a claim the signature still covers the bytes: a replaced package owes a retest before it can run at all.

A verified signature does not make a package trusted. It stays imported_unreviewed. Signing infrastructure is expensive, which makes it tempting to let it count for something, and the something it would count for is the review nobody did. The signature answers a different question: is this the package that publisher published, unchanged since.

Verification runs before validation. A package whose bytes changed after signing is refused as tampered rather than as invalid, because the second answer sends you looking for a bug in a package that is not the package its publisher published.

An unknown publisher is refused, and that is not a prompt to add one. Nothing discovers a publisher. You decide which identities your organization recognizes.

Key validity is judged at install time, not signing time. An expired key does not keep authorizing installs because the signature is old.

A package may not declare executable content. An extension package is inert declarative JSON. Signing does not become the route by which one carries code: a code-shaped field at any depth is refused, signed or not, and a package nested deeper than the scan reads is refused rather than passed.

# Check a signature before committing to an install. Reads only.
orgabot organization department extension package verify <org> package.json signature.json

# The one removal there is: an unreadable record, which protects nothing.
orgabot organization department extension package revocation-remove <org> <revocation-id>

On a multi-organization install, a corrupt row whose organization cannot be read is deliberately not assigned by guesswork. After inspecting the archived row, acknowledge that ambiguity explicitly:

orgabot organization department extension package revocation-remove <org> <revocation-id> --acknowledge-ambiguous-owner

An unreadable revocation record HOLDS everything. A row the store cannot decode cannot say what it revokes, so nothing is cleared against it: every quarantine-gated path refuses until you repair or remove it. Skipping it would mean the corruption of one row silently resumed what you revoked. Removing it is the way out, and it does not restore what it may have revoked: record that revocation again, deliberately. A readable revocation cannot be removed at all - being permanent is what makes it worth anything. The same applies to a publisher record: it is kept and marked rather than dropped, and it verifies nothing while marked. It still OCCUPIES its id, so you repair it by registering that publisher again - which replaces the broken row rather than adding a second one - and you can still revoke it, which is the state where you most need to. If its own id could not be read, publisher list shows a synthesized one no re-registration can match; remove that row with orgabot organization department extension publisher remove <org> <publisher-id> and register the publisher again. When a revocation names the material that row holds, register the publisher first, under a NEW key pair - the new row then carries the revoked material as retired, and the placeholder can be removed. Removing it before there is somewhere for that material to live is refused, because the projection is the only thing stopping a revoked key from being registered again under another id. A readable publisher is revoked, never removed.

An unsigned install cannot shed a revocation your organization has seen. A package whose extension id you have already installed under a signature is refused when that publisher or key is revoked, even if the new copy arrives with no signature at all: omitting one is not a way around the revocation. This is history-based, and that is its limit. An unsigned package carrying an extension id you have never installed signed names no publisher, so there is nothing for a revocation to match, and a revoked publisher shipping under a NEW id installs like any other unsigned package. Require signatures if that matters to you: an unsigned install records no provenance, and the trust state says so.

Retired key material does not come back. Repairing an unreadable publisher row replaces it, and any key material the repair does not re-supply is retired. Retired material is refused to every publisher under every key id, so a leaked key cannot return through a repair that quietly drops it. A publisher who needs a working key rotates to a new pair.

A package's own `trust` field grants nothing. For an unsigned install it is recorded as the package declared it, and nothing in Orgabot reads it to decide what a package may do; the signature, the publisher registry, and the revocations are what carry weight.

A revocation that can never match is refused. The scope must be one of the three, a package subject must be a sha256: digest, and a key subject must name its publisher. Recording one that matches nothing gives you audit evidence of a revocation and no protection from it.

A quarantined package does not run, and cannot be given more. Invocation, smoke verification, credential completion, activation, a version transition, an access request and its approval, runtime grants, and connector replacement and rollback all refuse a package a revocation covers, so revoking a compromised publisher stops its connector rather than only listing it.

Rotating a key means a new key PAIR. Re-registering the same public key material under a new id is refused: it would defeat the revocation of the old id outright.

A rollback is refused if either package is revoked - the one being replaced and the one it would restore.

Department blueprints run the same admission checks (executable content, digest revocation) but carry no signature, so they are revocable by DIGEST alone; publisher- and key-scoped revocation reach connector drafts. An installed blueprint a digest revocation covers appears in package quarantined alongside connector drafts.

Ids may not contain a colon, because a key revocation names <publisherId>:<keyId> and a colon in either half makes the subject ambiguous. An existing key's validity window cannot be edited either: add a new key id, or revoke the old one. Editing in place would rewrite the meaning of every signature already verified under it.

Revocation is present tense. It says what may run now. It does not rewrite the install that happened before it, because at the time that install was authorized, and an install record that changed retroactively would destroy the only account of what your organization actually did. Quarantine is computed from the revocations on file, so it appears in package quarantined and leaves the install record untouched. A revoked package is also refused when it arrives with no signature at all.