Authoring a Plexus extension
You are authoring an extension for a local Plexus instance. An extension is a runtime-registered connector: a manifest that declares a source and the capability entries it contributes. Installing it makes those capabilities discoverable — it does NOT grant access. A human still approves every install and issues every grant.
This page is the contract the authoring agent follows. The full spec is the extension spec.
1. Manifest shape

{
"manifest": "plexus-extension/0.1",
"source": "my-tool", // SourceId; seeds every entry id (<source>.<name>)
"label": "My tool",
"transport": "local-rest", // default transport for caps that don't override
"capabilities": [ /* ExtensionCapabilityDecl[] */ ],
"secrets": [ /* ExtensionSecretRef[] (optional) */ ],
"serviceHint": { /* how to locate a local service (optional) */ }
}Each ExtensionCapabilityDecl:
{
"name": "vault.write", // <noun>.<verb>; full id = <source>.<name>
"kind": "capability", // capability | skill | workflow
"label": "Write a vault note",
"describe": "Write/overwrite a note at {path}. Use when the user asks to save…",
"io": { "input": { "type": "object", "properties": { "path": {"type":"string"} } } },
"grants": ["write"], // verbs this cap requires: read | write | execute
"transport": "local-rest", // cli | local-rest | skill | workflow | stdio | ipc (no mcp)
"route": { /* transport routing — see §3 */ }
}A good describe is the agent-relevance signal — say WHAT it does, WHEN to use it, and name the inputs. Be specific; vague describes make the capability undiscoverable.
The id is <source>.<name> — do NOT repeat the source in name
The full capability id is built by prefixing source automatically. For source: "user-profile", name: "read" yields the id user-profile.read, while name: "user-profile.read" yields the doubled user-profile.user-profile.read — and it still validates, so the mistake is silent. Pick name as the unprefixed part: <noun>.<verb> when the source groups several nouns (vault.read, vault.write), or a bare <verb> for a single-purpose source (read).
2. EntryKinds
- capability — a callable backed by a transport (
cli/local-rest/ipc/stdio). - skill — pure markdown usage guidance, no transport.
body: { format:"markdown", markdown }. - workflow — composes existing entries via
members[](each must resolve once registered).
3. Per-transport route requirements
route is read ONLY by the owning transport, never by core. Per transport:
cli (the #2 RCE surface)
"route": {
"bin": "ls", // bare binary name — NO path, NO shell metacharacters
"args": ["{dir}"], // argv template; {placeholders} fill from io.input
"allowedBins": ["ls"] // user-confirmed allow-list (part of the approval surface)
}local-rest (the #3 SSRF / secret-redirect surface)
"route": {
"baseUrl": "http://127.0.0.1:27123", // loopback by default; a non-loopback host is opt-in and
// requires an explicit, user-confirmed `allowedHosts` entry
// (the approval surface) — see `transport-policy.ts`
"allowedHosts": ["127.0.0.1:27123"], // host allow-list (part of the approval surface)
"method": "PUT",
"pathTemplate": "/vault/{path}", // canonical URL path key (`path` is a legacy alias)
"secret": { "name": "vault-key", "attach": "bearer" } // references secrets[] by name
}The secret VALUE never appears in the manifest — it lives under ~/.plexus/secrets/<name> and the transport attaches it at dispatch.
skill / workflow
- skill: no
route; supplybody. - workflow: no
route; supplymembers[]referencing present entry ids. Cross-source attach (a skill/workflow reaching into a different source) is OFF by default — it is a prompt-injection channel and must be explicitly gated + human-confirmed.
4. Security surface (what the human approves)
When you install, the human sees exactly: the cli bins the extension may spawn, the non-loopback rest hosts it may reach, any cross-source skill attaches, the verbs each capability requires, and whether it is transport-backed. Keep the surface minimal — request only the bins/hosts/verbs you actually need.
5. Install flow
- Fetch this guide:
GET /admin/api/extensions/authoring-guide. - Draft the manifest as JSON.
- Preview (no commit):
POST /admin/api/extensions/previewwith{ manifest }. Readvalid/reasons[]; ifvalid:false, fix the manifest and re-preview. Show the human the returnedsurface(cli bins / rest hosts / cross-source / verbs). - Install (human approves):
POST /admin/api/extensionswith{ manifest }. The local user is the connection-key holder = the human approver, so this commits directly and auditssource.install. Response:{ ok, source, registered, revision, reason? }. - Remove:
DELETE /admin/api/extensions/:source.
Installed extensions persist across a gateway restart
An admin-installed extension is persisted to ~/.plexus/extensions.json and replayed on boot, so its capabilities come back after a restart without a re-install. DELETE/remove drops it from that durable store too.
CLI equivalents: plexus extension preview|add|list|remove.
6. Worked example — a local-rest "vault write" extension
{
"manifest": "plexus-extension/0.1",
"source": "my-vault",
"label": "My local vault",
"transport": "local-rest",
"secrets": [{ "name": "my-vault-key", "attach": "bearer" }],
"capabilities": [
{
"name": "notes.read",
"kind": "capability",
"label": "Read a note",
"describe": "Read the markdown of a note at {path}. Use to fetch existing note content.",
"io": { "input": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } },
"grants": ["read"],
"transport": "local-rest",
"route": {
"baseUrl": "http://127.0.0.1:27123",
"allowedHosts": ["127.0.0.1:27123"],
"method": "GET",
"pathTemplate": "/vault/{path}",
"secret": { "name": "my-vault-key", "attach": "bearer" }
}
},
{
"name": "notes.write",
"kind": "capability",
"label": "Write a note",
"describe": "Create or overwrite the note at {path} with {content}. Use when saving content the user dictated.",
"io": { "input": { "type": "object", "properties": { "path": { "type": "string" }, "content": { "type": "string" } }, "required": ["path", "content"] } },
"grants": ["write"],
"transport": "local-rest",
"route": {
"baseUrl": "http://127.0.0.1:27123",
"allowedHosts": ["127.0.0.1:27123"],
"method": "PUT",
"pathTemplate": "/vault/{path}",
"body": "{content}",
"secret": { "name": "my-vault-key", "attach": "bearer" }
}
},
{
"name": "notes.howto",
"kind": "skill",
"label": "How to use my-vault",
"describe": "Usage guidance for my-vault.notes.read / notes.write.",
"grants": [],
"transport": "skill",
"body": { "format": "markdown", "markdown": "# my-vault\nRead with `notes.read { path }`; write with `notes.write { path, content }`. Paths are relative to the vault root." }
}
]
}This extension is transport-backed (local-rest) and write-capable, so its approval surface lists restHosts: ["127.0.0.1:27123"] and the write verb on my-vault.notes.write — exactly what the human signs off on.
7. Best practices & self-check
A manifest that validates is not yet a good citizen. These practices make your extension trustworthy to the humans who approve it and useful to the agents who discover it.
7a. Implement the health check
A source SHOULD implement the per-source health protocol so the live availability of its capabilities is surfaced — both in the admin dashboard and to agents that discover it:
health(): Promise<{ status: "ok" | "degraded" | "unavailable" | "unknown", detail?: string }>ok— reachable and serving.degraded— up but impaired.unavailable— down/unreachable.- It is optional: a no-op is allowed and just reports
unknown. But implementing it lets agents route around an unavailable source instead of failing an invoke blind. - If
health()is absent, status is derived fromcheckRequirements()(e.g. missing binary / unreachable host) — and if that says nothing, it falls back to"unknown".
Health is reconciled with the source_unavailable invoke error (§7b): a source that reports unavailable should also fail invokes with source_unavailable, so discovery and dispatch agree.
7b. Return precise, semantic errors
When a capability fails, feed the calling agent a standard Plexus error code plus a clear, human-readable message/detail — never an opaque 500 or a vague string. A precise error lets the agent recover (retry, pick another source) or tell the user exactly what's wrong.
Use the standard codes: source_unavailable, transport_error, schema_validation_failed, grant_required (and the others in the spec).
// BAD — opaque, unactionable:
{ "error": "failed" }
// GOOD — semantic code + a message the agent (or user) can act on:
{ "code": "source_unavailable",
"message": "Obsidian REST API not reachable at 127.0.0.1:27124 — is the plugin running?" }7c. Self-check checklist (run before installing)
Before you POST /admin/api/extensions, tick each of these off:
- [ ] Manifest validates — run
plexus extension preview <manifest.json>and confirmvalid:true. Review the printed security surface (declared cli bins / rest hosts). - [ ] Transports are reachable & host-confined — loopback (
127.0.0.1/localhost) is allowed by default; a non-loopback host is opt-in and requires an explicit, user-confirmedallowedHostsentry (the approval surface) — seetransport-policy.ts. The local service is actually up. - [ ] Secrets referenced by name only — no secret values anywhere in the manifest.
- [ ] Capabilities are honest — each has a specific
describe(what/when/inputs) and an accurateioschema; you're not over-claiming what a cap does. - [ ] Health implemented (or deliberately skipped) — skipping
health()is fine, but make it a choice, not an oversight (§7a). - [ ] Errors are semantic — failures return a standard code + readable message, not a 500 or
{error:"failed"}(§7b).
8. Conformance checklist
- [ ]
manifestis"plexus-extension/0.1";sourceis a non-reserved id. - [ ] every cap has
name(<noun>.<verb>),kind,label, a specificdescribe,grants,transport. - [ ] cli caps: bare
bin+args+allowedBins. local-rest caps: loopbackbaseUrl+allowedHosts+ secret ref. - [ ] secrets referenced by NAME only (no values in the manifest).
- [ ] workflows reference present member ids; cross-source attach only if explicitly intended.
- [ ] previewed (
valid:true) before install; minimal cli-bins / rest-hosts / verbs surface.