Extensions — concepts
SHERU is extensible along two independent axes. Extensions add function — data sources, commands, menu items, openers, launchers. Themes add presentation — the entire look of the UI. The two never mix: an extension has no pixels to clash with a theme, and a theme has no logic. This page is the concept map for the function axis: the nouns you use and the layering underneath them.
Two orthogonal axes: function ⊥ presentation#
The function axis and the presentation axis are fully decoupled, so any theme renders any set of installed extensions:
FUNCTION axis PRESENTATION axis
┌──────────────────────────┐ ┌──────────────────────────┐
│ Extension │ │ Theme │
│ = manifest + code │ @sheru-app/ui │ = tokens + styling │
│ contributes data │ ◄──seam────► │ renders the headless │
│ + registers commands │ (headless parts) │ contract; no logic │
└──────────────────────────┘ └──────────────────────────┘
what is there how it looksThe seam between them is the closed @sheru-app/ui headless contract — a governed vocabulary of semantic UI primitives plus design tokens. Extensions emit only semantic structure (declarative data + addressable logic); themes own 100% of how those things look. Because an extension never produces HTML, CSS, or pixels, installing one can never make a theme "only fit part of the UI."
A theme is therefore pure data, never code (tokens + a scoped CSS string + chrome config + declarative glyph data), sandbox-by-construction because there is nothing to run. The token contract is versioned ("1.0"), and the token schema is organized into roughly a dozen semantic groups (identity surfaces & text, chrome, bevel, controls, inputs, toolbar, sidebar, file list, modal dialogs, marquee, terminal, scrollbar, focus & motion) totaling on the order of ~80 tokens. The theme's icon field is the declarative glyph spec (iconGlyphs / FileIconData / ControlGlyphData) — data, not a React component table. For the full presentation-axis design see themes. Themes are explicitly not a contribution point of an extension; they are their own axis and their own marketplace.
The two user-facing nouns: Extension and Connector#
The user perceives exactly two nouns. Everything else on this page is internal and never surfaced as product vocabulary.
Extension — the distribution unit#
An Extension is a VSCode-style distribution unit: a directory with one manifest.json plus an optional JS/TS entry module and assets. It is a data package — it has no executable of its own. It is a container of contributions, not a feature; one extension can fill several contribution points. Built-in capabilities ship as built-in extensions, identical in shape to third-party ones. Its identity is the extensionId (surfaced as NodeInfo.extensionId), and it is what a developer ships.
Connector — one contribution point#
A Connector is one contribution point — and the most prominent — defined as "a connection to a third-party data source," projected as a browsable file tree the file browser can list, read, and (capability permitting) act on. It is what a user adds: adding it mints a node the file browser operates. Which node an extension can mint follows its provider class, and each class has a fixed capability ceiling:
| provider class | the user's term | backing | runtime | ceiling |
|---|---|---|---|---|
| service projection | service connector | out-of-process sidecar (GitHub, Notion, Linear) | host-gated, credentialed (trusted) | read-only projection, forever |
| fs-provider | device | out-of-process sidecar (adb — Android devices) | host-gated, credentialed (trusted) | faithful filesystem — may declare write / delete / paste, a terminal, processes, and a clipboard, clamped to the class ceiling |
| fs-engine (built-in, not an extension) | file connector | rclone preset (S3-compatible object storage, WebDAV, SMB, SFTP, FTP) | in-process, trusted | read + upload; in-place writes where the backend supports it (SFTP) |
The two classes an extension ships are service projection and fs-provider; fs-engine is built into the app. The class colors the icon and sets the ceiling; nothing else branches on it. There is no "mount" or "drive" in the API — every class presents identically as a node, distinguished only by the services it serves and its icon.
The local disk is not a connector (a connector is third-party). It is the built-in local node (
provider = "builtin-local", full write), shown as plain furniture ("This Mac", Home, Locations), never under the Connections group.
Why Extension ≠ instance (the lifecycle boundary)#
These name different things, and the distinction is load-bearing for the API — it is why the extension-host machinery coexists with the node lifecycle (this is not drift):
- An extension is the package a developer ships. Identity =
extensionId. Machinery isExtension*(discovered by the host, loaded bysheru-extension-runner). - The instance is the configured node a user adds. Lifecycle is
core.addNode/removeNode/validateNode+ thenodes.changedevent.
One extension can back many instances (two GitHub accounts, three buckets, N enumerated devices), each its own node id. The rule: the package is an extension; the user-added instance is a node. "Connector" survives as the user-facing word for connecting one.
The layering: Contribution Point → Contribution → Command#
Underneath the two user-facing nouns, the model layers three internal primitives. Only the last carries logic.
Command — the logic primitive#
A Command is an addressable unit of logic: a stable id plus a handler. It is the only thing that carries logic, and it is also the agent-tool surface. A command is invoked from many entry points — a context-menu item, a keybinding, a launcher, the agent — all routed to one handler by id.
A command must be addressable-by-id, never an inline closure, because the things that trigger it are declarative data rendered by native AppKit, by the WebView engine, or marshaled across a process boundary — none of which can carry a JS closure. Indirection through an id is what makes one handler reachable from every surface.
Contribution — the declarative data#
A Contribution is a pure, serializable descriptor placed into a typed contribution point. It carries no logic; instead it references a command. A context-menu item is a contribution that points at a command id; a connector is a contribution that declares how to back a Source. This is deliberately VSCode's contributes model.
Contribution Point — the typed slot#
A Contribution Point is a typed slot a Contribution goes into:
| point | contributes | owner |
|---|---|---|
nodes |
a node provider (provider class = service | fs-provider) |
core |
commands |
addressable logic (id + handler); also the agent-tool surface | core |
menus |
context-menu item → references a command, gated by a when predicate |
core |
keybindings |
key chord → command |
core |
openers |
file match (ext / uti / mime) → command that opens it |
core |
launchers |
double-click match (.app / uti) → command |
core |
sidebarItems |
a pinned row → target (command | connector source | view) |
core |
panels |
a bottom-panel view (gains a view field) |
advanced |
themes is not in this table — it is the separate presentation axis.
Trust model#
Privilege follows the contribution point, not the extension. There are three trust tiers:
- 2a — trusted-builtin, in-core. The rclone
fs-engineprovider, the built-in local node, and the federation client. First-party, no process boundary. - 2b — out-of-process sidecar. The extension-shipped classes (service projection and fs-provider) and any future credentialed logic extension. Scrubbed environment, manifest allow-lists (
execTools/secrets/allowHttp) enforced host-side, drop-kills-child lifecycle. A service projection is forced read-only regardless of child claims; an fs-provider's declared services are clamped to its class ceiling. This is the tier the extension runner serves. - 2c — in-process / declarative. Themes (output-only style sinks) and view/command handlers. The OS is reachable only through the typed protocol, via a capability-narrowed
ExtensionContext.
A service projection is read-only by contract: a manifest that claims write or paste capability is rejected, and it can only act through host-mediated command callbacks, never as a filesystem write. An fs-provider may serve real writes — but only up to its class ceiling, and the actual mutation still runs through a host-gated tool (e.g. adb), never with any privilege beyond its granted execTools.
The runner is explicitly not a sandbox. sheru-extension-runner loads arbitrary code (await import(entry)), so a local extension keeps full host access like any installed CLI. Therefore extensions are trusted (first-party / vetted); the exec / secret / http callbacks are the sanctioned interface and review surface, not an enforced security boundary. Untrusted third-party logic would require an actual sandbox.
What a manifest looks like#
An extension ships a manifest.json (validated against the published schema) describing the node it serves. A service-projection manifest (GitHub):
{
"id": "github",
"name": "GitHub",
"class": "service",
"icon": "github",
"runtime": { "type": "sheru-ts", "entry": "extension.js" },
"execTools": ["gh"]
}Required fields are id, name, class, runtime. The id matches ^[a-z0-9-]+$ and must equal the directory name; class is "service" (a read-only projection; the legacy spelling "connector" is still accepted) or "fs-provider" (a faithful filesystem); runtime.type is const "sheru-ts" with runtime.entry the module the runner imports. execTools / secrets are the host.exec / host.secret allow-lists, allowHttp defaults false, and schemaVersion defaults to the current schema version (2) — the core requires an exact match. An fs-provider additionally declares a services block (its fs write/delete/paste bits, a terminal command template, a proc block, and device nodes.enumerate); a service projection omits it. See the manifest reference.
The extension model is designed around a multi-contribution manifest: an extension can grow beyond a single node into a contributes bag with keys nodes / commands / menus / keybindings / openers / launchers / sidebarItems, command ids namespaced by the extension id. For the full picture see the authoring guide and the manifest reference.
What you write as an author#
An author touches exactly two things: the entry module's default export, and the host-gated ExtensionContext handed to every handler. The id is not in the definition — it lives once in the manifest, and the runner supplies it. The node is written as handler groups keyed by service:
// index.ts — built on @sheru-app/extension-sdk
import { defineExtension } from "@sheru-app/extension-sdk";
import { commands, invokeCommand, listPage, read } from "./github";
let repos: string[] = [];
export default defineExtension({
node: {
init(params) {
const list = (params.params as { repos?: unknown }).repos;
repos = Array.isArray(list) ? list.filter((x): x is string => typeof x === "string") : [];
},
fs: {
list: (params, ctx) => listPage(params, repos, ctx),
read: (params, ctx) => read(params.path, params.maxBytes, ctx),
},
commands: {
list: (params) => ({ commands: commands(params.path) }),
invoke: (params, ctx) => invokeCommand(params.command, params.path, params.args, ctx),
},
},
});The legacy flat connector: { listPage, read, commands, invokeCommand } shape is still accepted for a read-only service projection and is mapped onto node.fs / node.commands; new code writes node.
Only fs.list and fs.read are required; the fs mutation verbs, commands, terminal, proc, clipboard, and nodes groups are optional and declared per manifest. node.describe is fully synthesized (id from the manifest, protocol version) — what the node serves comes from the validated manifest, so there is no capability block to author. Handlers reach the host only through ctx:
// the host-gated SPI — every handler receives this
export function makeGh(ctx: ExtensionContext): Gh {
return async (args: string[]) => {
const r = await ctx.execBytes("gh", args); // only binaries in the manifest's execTools
const stdout = new TextDecoder().decode(r.stdout);
if (r.code !== 0) { /* throw with decoded stderr */ }
return JSON.parse(stdout);
};
}ctx.execBytes(bin, args, { stdin? }) runs a manifest-allowed binary; ctx.httpBytes(method, url, { headers?, body? }) performs host-mediated HTTP (only if allowHttp); ctx.secret(key) fetches a manifest-declared secret. The raw ctx.exec / ctx.http variants mirror the base64 wire exactly. See the authoring guide.
How the host runs an extension#
An extension is data, never its own executable. The host runs one signed binary, sheru-extension-runner. The host spawns it with a scrubbed environment, pointed at the extension's directory. The runner then:
- reads the directory's
manifest.json(the single source of truth for the id +runtime.entry; the host has already validated it); - imports the entry module's default export;
- checks the default export carries a
node(or the legacyconnector), otherwise exits with code2; - calls
runExtension(id, def), which owns the stdio loop.
// the SDK's stdio loop — authors never call this; the runner does
runExtension(id: string, def: ExtensionDefinition): Promise<void>The id always comes from the manifest, never the extension code — which is why per-extension code-signing collapses to one binary. The sub-protocol (EXTENSION_PROTOCOL = 2, exact-match) is line-delimited JSON over one shared stdio pipe, multiplexed by request id: downward requests (core → runner) are service-namespaced — node.describe, fs.* (list / read plus an fs-provider's mutation verbs), commands.list / commands.invoke, terminal.describe, proc.*, clipboard.*, nodes.enumerate; upward callbacks (runner → core) are host.exec / host.http / host.secret — the sanctioned host-gated door, gated by the manifest's execTools / secrets / allowHttp. An fs-provider may also emit a fire-and-forget { kind: "event", name: "nodes.changed" } when its device set changes. stderr is a free-form log drain. For the full wire and lifecycle, see the runner reference.
Where to go next#
- Manifest reference — every field, the node schema, and the
contributesbag. - Authoring guide —
defineExtension, the handler set, theExtensionContextSPI, and the path-scheme projection pattern. - Runner —
sheru-extension-runner, the stdio sub-protocol, and the trust boundary in detail. - Themes — the orthogonal presentation axis.
- Changelog — release notes for the extension system.