Plexus is the CortexKit module that lets agents act on external services (GitHub, Linear, Notion, Exa and others) without ever holding a credential. It runs as a subc-supervised daemon (ck-plexus). Every agent-requested connector action passes through GovernanceEngine before credentialed dispatch. Connector metadata discovery, periodic health probes, and operator-authorized GitHub handle import/probe/bind validation are setup and maintenance exceptions: they may contact vendors with credentials outside the action-governance pipeline. These exceptions do not authorize ordinary agent actions, and their setup/health/control checks and available audit records are separate from invocation decision/dispatch audit pairs. Credentials stay in the claustrum vault and are resolved per call.
Catalog. Each vendor is a reviewed static manifest in catalog/*.jsonc: transport, auth mode, actions with risk and severity, poll shapes and optional reconciliation read-backs. Manifests are parsed with deny_unknown_fields, so a field an older binary does not know refuses boot. At startup the catalog reconciler ingests them into the store, and live vendor discovery pins each action's input schema by hash. A changed input schema (including a description nested inside that schema) quarantines that action until an operator reviews and re-pins it. The vendor's top-level tool title and description are not part of this pin. Re-ingest updates reviewed policy on existing REST rows without overwriting operator status or schema-pin decisions. See the manifest model for implemented controls and deprecated metadata.
Authority. Authority narrows at every step: manifest → binding ticket ceiling → connection → grant → invoke. Operators mint it out of band with ck-plexus-admin over the privileged ManagementSurface (Principal::Direct only). Grant kinds: invoke, connection_admin, poll, events_consume. An empty selector list authorizes nothing, and * must be explicit. Grants are until-revoked unless given an expiry. A revocation deletes the grant, snapshots it in grant_revocations, and first appends it to a journal file beside the database, so restoring an older store cannot resurrect it.
Governance pipeline (GovernanceEngine). A tool call goes RESERVE → REVALIDATE → DISPATCH:
- The invoke boundary refuses undeclared identity-token fields (
assertion,agent_assertion). Governance refuses credential-named argument keys recursively for every caller, including internal calls. Agent identity comes only from the daemon's route scope stamp;agent_idattributes the call, whiledelegates: truerequests agent authority andowner_authorized: trueconfirms that the daemon allows the scope owner to delegate it. With consent cards configured, an unscoped call cannot raise a cardless approval. - Authorize: connection health, catalog status, a matching grant, policy (
allow/ask_first/block), severity (S3/S4 need a decision plane; none is configured in production, so they are refused), and resource filters. Write a 60-second reservation that pins the generations of every row it read. - Revalidate at claim time. Any generation that moved aborts the call.
- Resolve the credential from the vault (credential-free manifests dispatch anonymously), dispatch, report the outcome, and write the audit row.
A write whose outcome is unknown and whose action declares a read-back class is queued for reconciliation. It is resolved by a governed read under the acting caller's current grants and never re-sent.
There is no production decision-plane implementation wired into the daemon. S3/S4 actions are refused, not silently downgraded to ordinary approvals. The read-back machinery exists, but no shipped manifest currently declares a reconciliation class: vendor-specific read arguments and response matching still need review.
Facade tools. web_search searches public web pages. code_search searches public GitHub code through grep.app. Both are Pure reads governed by module grants, connector home, rate limits and query audits. Code search caps replies at 20 file matches, 100 lines per file and 32 KiB of rendered text; private code and pinned versions must be read from disk.
Both record the stamp's agent on audit rows but authorize only the carrying module's grant, not an agent grant. Web search also uses it in its per-caller rate key. Flow calls are carried only by Basal with a flow_id scope and cannot request consent (consent_unavailable).
Plexus serves tool-provider/v1's tool.catalog with per-tool capability tags from the advertised facade definitions.
github_react (Mutating, classic and flow only, tag plexus:github.react/v1) adds one reaction to an issue or pull request as the acting agent's own bot. It accepts only repository, kind, target.number, reaction and request_nonce, builds the request github's bot_request would receive for that reaction and runs it through that same path, so a flow can be given reactions without every other bot write. Its tool.catalog entry alone carries emits_events: false: a reaction leaves the issue's updated_at, and so its event key, unchanged, and the relay admits no reaction webhooks.
Connector home. Outside effects (polls, bot writes, invoke dispatch and approved-request execution) require Entorhinal's role.read {role: "connector_home"} to confirm known: true, held_here: true, stale: false. The exact peer id is entorhinal; reads run at boot and every 30 seconds. Transport errors, malformed replies and any stale answer retain the last confirmed decision; before one arrives, effects are refused. claim_integrity_failed revokes admission immediately. Health's connectorHome reports state, epoch, source, holder name and integrity code. connector-home status reads the role; taking it belongs to ck roles take connector_home, never Plexus. Rollout: pending, run at deploy: deploy, then Ufuk takes the role within the minute; effects remain refused until the confirmed read lands.
Transports. Shipped manifests can use RestTransport or McpRemoteTransport. The local McpRouteTransport adapter is test infrastructure, not a supported catalog transport; manifest validation refuses mcp_stdio. Agents see only sanitized error text. Full vendor diagnostics go to the operator log.
Triggers. A scheduler task ticks every second and polls due subscriptions sequentially using sync_token, ordered_timestamp, scalar_diff, sorted_head, ci_runs, or a registered probe. Failures back off exponentially, and five in a row trip a resumable circuit breaker. Events are deduplicated on (connection, subscription, vendor id, payload hash), so an edited item arrives as a new event, and coverage holes are recorded as gap rows. A per-poll ledger enforces examined == inserted + dropped with a closed set of drop reasons. Consumers read and ack events through events_consume grants; events op:list returns at most 500 events by default (limit up to 1000) and a next_after cursor to page on while more remain. Subscriptions can be tagged with a project and a delivery agent.
Probes. plexus-probe runs author-supplied JavaScript in a QuickJS/WASI guest on Wasmtime (32 MiB, 5 s epoch deadline), and it can call only the read actions it declared.
Health. health.check answers from cached state. It reports degraded when active subscriptions exist but nothing has completed within three times the longest cadence, measured on the awake clock so host sleep does not count.
| Area | Location |
|---|---|
| Manifest model and validation | crates/plexus-core/src/manifest.rs |
| Store, migrations, retention | crates/plexus-core/src/storage.rs |
| Governance pipeline | crates/plexus-core/src/governance.rs |
| Catalog ingest and discovery | crates/plexus-core/src/catalog.rs |
| Daemon scope identity and rechecks | crates/plexus-core/src/scope.rs |
| Poller and scheduler | crates/plexus-core/src/poller.rs, scheduler.rs |
| Uncertain-write reconciliation | crates/plexus-core/src/reconciliation.rs |
| GitHub bot facade, import, reads | crates/plexus-core/src/github/ |
| GitHub handle store | crates/plexus-core/src/store/github/ |
| Transports | crates/plexus-core/src/{rest_api,mcp_remote,mcp_route}.rs |
| Wire daemon, ceremony, health | crates/plexus-module/src/ |
| Operator CLI | crates/plexus-module/src/bin/ck-plexus-admin.rs |
| Probe sandbox | crates/plexus-probe/ |
| Conformance and drift harness | crates/plexus-conformance/ |
- Fail closed. A missing ceiling, grant, principal or decision plane, or a hash mismatch, is a denial, never a default. Unknown severity is S4. An action plexus cannot classify is refused.
- Action mediation. Every agent-requested connector action, including poller reads, enrichment calls and reconciliation read-backs, passes through
GovernanceEngine. The setup and maintenance exceptions above do not grant action authority. - Audit is append-only during ordinary operation. Runtime audit rows are immutable, and ordinary retention cannot delete rows within the seven-day SQL floor; the default retention window is thirty days (
PLEXUS_AUDIT_RETENTION_DAYS). Migration 40 was a one-time historical exception: for eligible revoked connections it temporarily removed the deletion triggers, purged associated audit and grant-revocation rows, and restored the triggers. Runtime connection deletion leaves both audit evidence and revocation snapshots intact; audit retention removes only sufficiently old audit rows, while revocation snapshots are otherwise retained indefinitely. Each governed call writes decision and dispatch rows correlated byrequest_id. - Pull events are a shared work queue. The connection owner and overlapping
events_consumegrantees share acknowledgements, not independent broadcast cursors. Acknowledgement retains payloads and does not affect outbox publication. Large requests commit idempotently in chunks of at most 500 ids and may be retried in full after partial or ambiguous failure. - Migrations are version-guarded, not necessarily additive or reversible. They can rebuild tables or delete data; migration 40's purge cannot be undone by a binary rollback. An older binary serves a newer store with a warning, which is not proof of compatibility. Deploy binary before catalog, and roll back catalog before binary. Rehearse every migration on a
.backupcopy of the live store (examples/migration_rehearsal.rs); destructive migrations need explicit operator approval and a retained backup. - Credentials are opaque. Connection bindings contain only non-secret vault credential references, or NULL for credential-free vendors. Plexus never persists bearer handles or a binding encryption key; vendor secrets are resolved by id at dispatch, discovery and health probing. Secret bytes never appear in
Debugoutput, logs or agent-visible errors. A 403 is a permission refusal and never invalidates a credential; only a 401 is reported to the vault. - Diagnostics go through
diag!(<level> <component>: ...), and a bareeprintln!does not compile in the daemon crates. Logs land in~/.local/share/cortexkit/plexus/logs/.
GitHub App bot requests go through plexus. The github tool has three operations: bot_request (governed writes with ownership pre-reads, or recording an issue an agent has seen), bindings.read (one handle connection), and bindings.read_for_agent (one registry agent's handles). Both binding reads expose only rows whose handle connection the caller has an active module invoke grant on. Repository-bound issue and CI reads use the separate github_read tool; the former github/read operation is no longer accepted.
bot_request optionally accepts expect: {app_handle_id, repo_binding_generation} beside request and request_nonce. Both fields are required when present (string handle, non-negative integer generation; no extra keys). After resolving the acting agent's bound bot and replaying any terminal receipt, plexus compares the expectation to the authority baseline it will pin, before any receipt claim or GitHub contact. A mismatch refuses stale_authority with dimension expected_handle or expected_binding and transmission: not_sent. The expectation joins a separate fingerprint domain, so changing it on the same nonce refuses nonce_payload_mismatch; calls without it retain their existing fingerprint and reply bytes. Existing baseline rechecks and uncached per-write scope descriptions remain mandatory: a cached open route never proves that a session is still live.
An optional sibling run_scope: {owner, ref, epoch} adds a per-write liveness precondition, never authority. All three members are required (string owner and ref, non-negative integer epoch, no extra keys); malformed fields refuse invalid_request by name with transmission: not_sent. Before every unsent write, the same daemon reader must report the run live at that epoch with a structured parent exactly matching the route's stamped owner, ref and epoch. Plexus's clock must also precede the live stamp's optional expires_at_ms. Run failures carry scope_role: run; transmission: not_sent is included only when no write leg was sent and omitted after any earlier write. Refusal codes are scope_ended for an ended, replaced, expired or synced-absent run, run_scope_not_child for a different parent, scope_not_synced while the owner has not synced, and scope_unverifiable for unavailable or incomplete replies, including a missing parent. Not-synced and unverifiable refusals release the claim for explicit retries; legs_sent honestly lists earlier writes that remain sent. Route-scope refusals gain only scope_role: route. Unlike expect, run_scope stays out of the fingerprint, so terminal same-nonce receipts replay their recorded outcome regardless of changes to the run constraint. Without it, existing behavior and fingerprint bytes remain unchanged apart from the route-refusal role.
On a reserved:basal flow route stamped with agent_id, github_read issue, comment, search and CI reads see only repositories currently bound to that agent's enabled GitHub bot. Every repository in an issue search must pass this check before grant checks or reservations; excluded repositories refuse repository_not_visible with transmission: not_sent. Allowed repositories still require all existing module grants and policies. Agentless global flows retain Basal's grant-based reach, workspace flows retain their audience fence, and non-flow callers retain their existing access.
Agent bot reads. github_read's github.repo.get (a GET relative to /repos/{owner}/{name}, path and media type validated by plexus), github.search.code and github.search.issues (fenced with an appended repo: qualifier), and AFT's byte-range github.actions.job_log (no max_bytes) are sent with the stamped agent's own bot. The agent is a head scope's agent_id or a worker scope's on_behalf_of_agent (stamped because plexus provides scope-on-behalf-of/v1); the latter is attribution only, recorded in its own audit column, never the acting agent and never write authority. Anything else refuses agent_identity_required, and a repository not bound to that agent's enabled bot refuses repository_not_visible. Each read is a governed action on the bot's handle connection under the calling module's grant. To upgrade imported handles, preview ck-plexus-admin handle-grant --handle <id> --kind read --carrier reserved:aft --actions github.repo.get,github.search.code,github.search.issues,github.actions.job.get,github.actions.job_log, or replace --handle <id> with --all-handles for every enabled handle. --apply uses one operator confirmation and one audited transaction to seed missing reviewed catalog rows (AddMissing, never reactivating disabled or quarantined rows), widen each ceiling by exactly the listed reads, and extend or create the carrier's module grant. Preview writes nothing and rerunning is a no-op; write grants and policies are unchanged. Every HTTP request is charged first to the bot installation's read budget (store/github_bot_reads.rs): 3,500 reads in any trailing hour, and none while GitHub last reported under 1,000 remaining before its reset, so writes always keep headroom; writes are never budgeted. A refused read sends nothing (read_budget_exhausted with retry_after_ms). Reply and refusal shapes are pinned by the vectors in crates/plexus-module/tests/fixtures/github-read-bot/.
- Handles. One row per App (
github_app_handle), keyed on its vault recordgithub_app:<slug>. The installation id is informational. Each handle has its owngithub-handle-<slug>connection and is bound to its agent's repository (github_repo_binding); a bot only ever speaks as its own agent, and a write to another agent's repository refusesagent_repository_conflict. - Stamp identity. A bot write requires an authorized delegating scope naming an agent. Otherwise it refuses
delegating_scope_requiredbefore handle lookup or credential access, with no fallback. The agent's bound bot is resolved from the store, and its scope is rechecked before each write. - Two grant layers. The calling module needs a module grant (agent_id null) on the handle connection. The agent needs an agent grant, which belongs to the agent whichever module carries its stamped call. Neither layer substitutes for the other.
- The request. The facade maps each request kind to catalog actions, builds the GitHub arguments from a per-kind allowlist (never the caller's fields), runs its ownership pre-reads (edits touch only the bot's own comments and issues), and dispatches each leg through
GovernanceEngine. The token comes from the vault'scredential.get_scopedat dispatch; the bytes are opaque. - Never twice. A receipt keyed on (agent, request nonce) replays a finished outcome, reports a write in flight, and leaves a write whose outcome is unknown unsent. Its reconciliation result is
unreconcilable, because GitHub comment read-backs cannot be matched to the write. - Setup. Operators import handles from an inventory with
github.handle.import(dry run by default, one transaction) and prove each withgithub.handle.probe, which lists the installation's repositories with the handle's own token. An unproven handle refuses.
Identity-token arguments have been retired with no compatibility mode; old callers are refused by field name, never silently ignored. Deploy this tool-contract cutover in a restart window so route bindings and fetched definitions refresh together. Audit storage remains additive: token-only claim columns stay present and new calls leave them NULL.
The obligations each seam of the facade must meet are listed in docs/design/github-writes-governance/facade-integration.md.