Skip to content
covey

The in-house ticket system has a REST API. Calls return issues, take comments, and want a key in the X-API-Key header. GitLab, Jira and GitHub have plugins. For this one there is none, and connecting it to an agent leaves two obvious options: write Go and wait for the next release, or drop the key into the agent's sandbox and hope.

A 2025 Hacker News post with 808 points called the Model Context Protocol an accidentally universal plugin system. Every plugin system brings the same question with it: what can the foreign thing reach once it runs?

Three of the four kinds need no new build

In covey every target system is a plugin, and a plugin reaches an installation in four kinds. A builtin is a Go subpackage and needs a build. A custom is a JSON manifest that a generic REST engine interprets. An mcp is the configuration of a reachable MCP server. A wasm is a WebAssembly module, which is to say real code.

All four satisfy the same interface, and the last three install at runtime.

Compiling one in is the exception now, and the spec lists four reasons for it, three of them still carrying built-ins today. The first is a protocol that is not JSON over HTTP, such as email over IMAP and SMTP, or SharePoint over Graph. The second is an auth flow beyond a static header, such as OAuth2 for Teams. The third is materialising files into the sandbox, which is what the GitLab and GitHub checkouts do.

The fourth used to be real computation, and nobody claims it any more (Zammad, VulnDB and Kubernetes have been catalogue entries as WebAssembly since 0.6.0).

Back to the helpdesk: it speaks JSON over HTTP with a static header.

So it gets a manifest.

Paths and scopes sit in the manifest, the address comes from the organisation

The file gives the auth, the actions, the fields of an inbound event and the connection test each a block of its own: auth, actions, webhook, probe. For the helpdesk, auth carries the header X-API-Key with the format {token}. Under it sit two actions, get_issue on GET /issues/{issue_id} and comment on POST /issues/{issue_id}/comments. If the helpdesk never calls covey itself, the manifest leaves the webhook block out, and the router answers calls to its webhook entrance with a 404.

Parse takes the file fail-closed: unknown fields are refused, the name has to match ^[a-z][a-z0-9_-]{1,31}$, and at least one action has to be there (a webhook block with fields but no id_field is refused as well). Allowed methods are GET, POST, PUT, PATCH and DELETE. Any scope the scopes block does not declare aborts the parse.

The most important rule is the shortest: every path has to start with /. A manifest therefore cannot name a host. helpdesk_url brokers the address and helpdesk_token the key, and both belong to the organisation. A hostile entry can at worst call unintended endpoints inside the agreed system, where the guard rails and the action subjects apply.

In the agent it is one line in ACCESS.md: - system: helpdesk scope: read,comment. Those words only narrow anything if the manifest declares them under scopes, and covey plugin lint tells the author so before anybody installs it.

The module brings the logic, the host adds the token

Sometimes one call is not enough. Whoever has to compute a value, or read a file out of the checkout, writes ordinary Go and compiles it with GOOS=wasip1 GOARCH=wasm.

The runtime lacks sockets and a mounted filesystem. Linear memory stops at 64 MiB, one invocation at 60 seconds, one action at 64 requests. Every call gets a fresh instance, so nothing survives from the agent before.

Credentials it never sees. Instead the module names a path, the host adds the base URL and the token, and an auth header the module sets itself is skipped. A module for the helpdesk would name paths like /issues/{issue_id}, and the host would put address and key in front. Reaching a foreign host is allowed where the module declared it in Describe (an operator sees the name before installing). Such a call goes over https, and the brokered token stays at home; a redirect that leaves the origin is refused.

What comes out of the catalogue is checked against a digest

Manifests, MCP configurations and modules travel through a catalogue. One JSON file sits behind the address in COVEY_MARKETPLACE_URL. The same mechanism works against GitLab raw, an S3 bucket, an internal nginx or a file:// path, which is how company-internal plugins get distributed too.

Before storing anything, covey checks the sha256 from the entry. If it does not match, the error names both values. Force-push, retag or delete the foreign repository, and what gets installed is still the version that was checked. A helpdesk manifest from a company-internal catalogue then goes through the same Parse as a manual upload.

A person decides before the first call

Under a report about a malicious VSCode extension, a reader described the usual trust model in one line: "install it, it runs, you trust its scope implicitly." Covey answers that differently. Installed from the catalogue, a plugin lands in the database with enabled=false, so installing and activating stay two steps. New versions are never pulled in automatically; they show up as a note, a diff and a button. Upload the helpdesk manifest by hand instead and it is active at once, since covey treats an upload as an admin setting up a system.

One exception takes a second decision: an MCP configuration names its own endpoint. covey plugin lint prints the line installing this opens the egress to with the address, and the release runs through the egress allowlist.

Back to all posts