Skills, plugins and the marketplace
Skill versus plugin, whether you need TypeScript, what is sandboxed, and what review and yanking reach.
Questions about changing what the office does: which of the two extension shapes to reach for, how much of it you can write without touching code, how far a plugin can reach once it is loaded, and what publishing to a marketplace actually promises. The sandbox answer is the important one, because it is the place where the intuitive assumption is wrong.
What is a skill, and how is it different from a plugin?
Should I write a skill or a plugin? A skill is one markdown file containing instructions that get rendered into an employee's prompt when a turn looks like it needs them. A plugin is a directory with a manifest that can contribute providers, models, skills, routing rules, role templates, pipelines, tool names and console panels — and, if it ships code, tools of its own.
Why
They differ in what they are allowed to change, not in how much effort they take. A skill changes behaviour within a role: it is knowledge, rendered under a heading into the prompt of a turn that was selected into it. It cannot add a capability, cannot register anything, and cannot affect routing. Fifteen skills ship in the repository, each one a decision procedure — a checklist, a shape for the output, a set of named anti-patterns — and none of them contains a line of code.
A plugin changes what the installation can do. The manifest's contributes object has exactly eight keys: providers, models, skills, roleTemplates, pipelines, routingRules, uiPanels and toolNames. Note which of those overlaps: a plugin can carry skills, so the two shapes nest rather than compete — the usual pattern for distributing knowledge to other people is a plugin whose contribution is a skill. Note also what is not in that list: settings is a top-level manifest field rather than a contribution key, and permissions are a separate set of ten that is not a contribution list at all.
The rule of thumb that follows: if the change is "this colleague should behave differently on this class of task", it is a skill and it belongs in a markdown file. If the change is "this installation should be able to use a different model, a different provider, a different pipeline or a panel that shows something none of the shipped panels show", it is a plugin. Writing a skill and Building a plugin cover each properly, and Plugin manifest is the field-by-field version.
Do I need to know TypeScript to extend it?
Can I extend dev3d without writing code? Yes, for most of what people want. A skill is a markdown file with a four-key front matter block, and a declarative plugin is a single JSON manifest with no entry module — data the host validates and acts on itself. The moment you want to register a tool or watch the event stream you are writing a JavaScript module.
Why
The dividing line is one field, and it is worth knowing precisely because everything else follows from it. A manifest that names no entry is declarative: the host reads it, validates it, and renders or applies the data. A manifest that names one is a code plugin: the host imports that module at boot and calls its activate function inside the orchestrator's own process. There is no third kind and no half-way house.
What that buys the non-programmer is more than it sounds. Declarative plugins can add a whole provider (base URL, protocol, and the name of the environment variable holding the key), add models to the catalogue, add skills, add role templates, add pipelines, add routing rules, and draw console panels from a closed set of six widget kinds. A hostile declarative plugin can waste console space and nothing else, because every string is length-capped and every list is row-capped by the validator. The released dev3d.cost-guard plugin is exactly this shape: two routing rules, one model, one skill, one role template, one pipeline and three settings, with no code at all.
Two things are genuinely out of reach without code, and both are honest limits rather than omissions. First, tools: a declarative plugin that asks for the tools permission without shipping code gets a warning, because it cannot register anything. Second, event subscriptions — following the run stream and reacting to it needs a module. The third shape of extension you might expect, running something in the browser, does not exist at all: panels are drawn from the closed widget set, so there is no plugin markup and no plugin script in the interface. Building a plugin shows both kinds side by side.
What can a plugin change, and what can it not?
How far does a plugin reach? It can add providers, models, skills, routing rules, role templates, pipelines, console panels, settings, and — with code — tools and event subscriptions. It cannot ship anything that runs in the browser, and it cannot force a model onto a role whose policy does not allow it.
Why
The contribution points are wired rather than advertised, which is a stronger statement than it sounds. Each one reaches the thing it names: a contributed provider appears in the registry without a restart because the registry rebuilds on every plugin change, a contributed model is merged into the catalogue the router chooses from, a contributed skill lands in the office catalogue where a floor can enable it like any other. Contributions are recomputed from scratch when the set of plugins changes rather than patched in place, which is why enabling or disabling a plugin takes effect at once instead of leaving a half-applied state behind.
The limit that surprises people is routing. A plugin can bias the router with a routingRules entry — and a rule that names a tier genuinely does move the walk, which is the opposite of what the project's own release notes claim. But hints only add score bonuses to candidates that already passed capability filtering; they cannot add a candidate. A rule that prefers a model the role is not allowed to use will not rescue it, and avoidModelIds is a score penalty rather than an exclusion, so an avoided model can still win on the other terms. There is also a scoping caveat worth reading twice: taskClass is optional, so a rule written without one applies to every task class — the widest possible blast radius, usually by accident.
What a plugin cannot do is reach the browser. A panel is drawn from six widget kinds — metric, keyValue, table, list, bars and note — with real caps on how much it may draw, and a panel that needs live data fetches an HTTP source on a refresh interval clamped between five seconds and an hour. That is a deliberate trade: it gives plugin authors a way to show something in the console and takes away any way to inject markup or script into it. Model routing has the scoring detail behind the routing caveats.
Is plugin code sandboxed?
Does dev3d confine what a plugin can do? No. A code plugin runs in the orchestrator's own process with the orchestrator's authority, so nothing confines it. The manifest's permission list is enforced for two specific capabilities — registering a tool needs tools, subscribing to the event stream needs events — and is a disclosure for the other eight; it is not a sandbox, and activate() can do plenty the host never mediates. Plugin-supplied tools are not confined by the host either — it forwards the workspace root to them as information rather than enforcing it.
Why
It is worth being exact, because "plugin" covers two very different things and only one of them is a risk. A declarative plugin is data. It cannot execute, and the validator's caps mean the worst a malicious one can do is waste console space. A code plugin is a module the host imports into the orchestrator process, where it can read and write files as the server user, open network connections, and see the same environment the orchestrator sees. There is no separate sandbox, no permission check at call time, and no capability boundary beyond what the operating system gives the Node process.
The workspace root deserves its own sentence, because the intuition here is wrong in a specific way. The host wraps a plugin tool so the engine sees an ordinary tool, and what it forwards is the run's workspace root, the plugin's settings, an abort signal and a logger. It does not resolve paths, does not reject escapes, and does not check that a returned path is inside the workspace. So the root is handed over as information rather than enforced as a boundary — which means "one choke point" describes the built-in tools and not the tool list an installation actually has once a plugin or an MCP server is connected. Within the built-ins the picture is more mixed than it used to be described anyway: eight of the fifteen take paths and call the workspace resolver, think and todo_write touch no path at all, the two web tools are guarded by address rather than by path, and run_shell and git are confined by nothing except their working directory, with the approval round trip as their only gate. An MCP tool's arguments go to the server's own process verbatim, which is why MCP tools are granted to nobody by default.
The practical rule: install code plugins only from somewhere you trust. That is not a hedge, it is the honest consequence of the design — a code plugin's reach is the orchestrator's reach, and the orchestrator holds your provider keys, your database and your workspace directories. The permission list still matters, but for a different reason than people assume: it is the promise the listing makes, and a plugin that understates it has broken that promise rather than defeated a check. This belongs next to the other confinement limits on Known gaps, and the terms state it as a condition of installing.
What does the marketplace review check?
Is there a human review, and what does it stop? Every version enters a review queue and is not installable until a person approves it — it is hidden from the catalog and from its own bundle URL in the meantime. A publisher an administrator has marked trusted skips the queue, and only an administrator can grant that.
Why
Split what happens into what a machine refuses and what a person decides, because the two lists are different and the difference is instructive. At upload, the checks are mechanical: a bundle over 32 MB or with more than 2048 entries, an archive containing an absolute path, a parent-directory segment, a link or a device node, anything that is not a readable .tar.gz, an apiVersion whose major is not 1, a plugin id that is not lowercase reverse-dns, an entry pointing outside the plugin directory, or a plugin.json that is not valid JSON. Those are refusals, not warnings, and the whole archive is rejected rather than partially salvaged.
What review adds is judgment about disclosure, because that is what a manifest cannot prove about itself. Three examples make the shape clear. A manifest that omits entry while the bundle ships code is refused as a misrepresentation — the presence of entry is what tells an installer that a plugin runs code at all. Permissions that understate what the code does are a policy violation, since the permission list is the consent record a stranger reads before enabling it. And a plugin that ships a credential — a key, a token, a base64 blob that decodes to one — is refused outright, because the host reads provider keys from its own environment and a plugin can name a variable but never carry a secret.
Be clear about where review stops, because the site's own policy is. The marketplace classifies a bundle by its manifest and cannot see code that no entry points at, so the honest-code rule is one a publisher is held to rather than one a scanner catches. The site cannot uninstall anything from anyone's host, inspect a running installation, or recover data a plugin already exfiltrated: it is a file server with a catalog, and its reach ends at the HTTP response. Reports come through one route, and a report about a malicious bundle is acted on first and discussed afterwards — the listing comes down and the publisher is asked about it second. Marketplace policy has the whole list of what is accepted and refused.
Can I publish a plugin privately?
Can I publish a plugin that only my organisation can install? Not through this marketplace. A published listing and every version you ship under it are publicly reachable once approved — there is no private listing, no per-account visibility and no unlisted mode. What you can do is keep a plugin off the marketplace entirely.
Why
The reason is structural rather than a missing toggle. A listed version is served from a bundle URL that any host can fetch once its checksum matches, and the catalog is generated from published listings with at least one published, non-yanked version and is served with an open CORS header, because a marketplace that is briefly stale is harmless and one that is unreachable is not. A private listing would have to break that, and the format has no field for it: the catalog entry carries a manifest, a download URL, a checksum, tags, a size and a readme — nothing about who may see it.
The alternative is the one that actually works, and it is simpler than publishing. A plugin is a directory with a manifest; nothing requires it to come from a marketplace. Dropping it into the directory the server scans as the bundled-plugins location is how the three shipped examples are loaded, and a marketplace-registered source is just another directory the host can fetch a catalog from. A host can also be pointed at a catalog you serve yourself — the format is a small JSON document, and any static host that can serve it is a marketplace as far as the host is concerned. The reason this is a real answer rather than a workaround is that the host's install gate is off by default and applies to marketplace bundles; a plugin you place locally is your own code in your own directory, and you are already trusting it.
What you give up by not publishing is the parts that publishing provides: the review that tells a stranger your plugin is honest, the checksum a host verifies before unpacking, the update offer computed from a newer version, and the distribution. If you are distributing inside one organisation, the last one is usually the reason you wanted a private listing in the first place — and a catalog on an internal host, or simply a shared directory, covers it. Publishing to the marketplace describes the public contract in full, which is also the contract you would be reimplementing.
How does a plugin update reach my installation?
Do plugin updates install themselves? No. Nothing polls a marketplace on its own: the operator asks, the host fetches each registered catalog, and an offer is computed only for plugins that are already installed, only when the marketplace's version is strictly newer. Upgrading is a separate action you take.
Why
The check itself is narrow by design. An offer requires that the plugin is installed, that the catalog entry declares the same plugin id, and that its version compares strictly newer than what is on disk. The comparison is on three numbers plus an optional pre-release tag, and the host's own source is explicit that it is not a semantic-versioning implementation: a missing component counts as zero, so 1.2 and 1.2.0 compare equal rather than one being invalid, and a pre-release sorts below the release it leads up to. The only question ever asked of it is "is this newer", which is why build metadata and the finer points of precedence are simply not part of the contract.
Two rules bound what an upgrade can do, and both are refusals with a message rather than silent behaviour. A marketplace bundle can never replace a plugin that ships with the office — the error says to update dev3d instead — because replacing it would leave the installed files and the loaded set disagreeing about what version ships there. And there is no downgrade and no pinning: nothing records "stay on this version", so declining an upgrade means not running it, and the console keeps offering it because offering is all it does. Rolling a bad version back therefore means removing the plugin and installing the older version as a fresh install, which is a different operation with different consequences.
The swap itself is ordered carefully, and the ordering is worth knowing if you ever debug one. On an upgrade the old plugin is unloaded before the new directory is moved into place, because its tools and event subscriptions belong to the code being replaced and leaving them registered would let an in-flight run keep calling into the version that has just been deleted. Only then is the target directory removed, the staged root renamed into position, and the new directory loaded. One platform limit applies after all of that: Node cannot unload an ES module, so disabling a code plugin withdraws its contributions and calls its deactivate hook but leaves the module resident until the process restarts. Publishing to the marketplace has the real refusal messages.
What happens to a version that gets yanked?
If a publisher withdraws a bad version, does it disappear from my installation? It disappears from the marketplace: a yanked version is dropped from the catalog and stops being served. It does not disappear from anyone who already installed it — there is no mechanism for that, which is the honest headline of versioning here.
Why
Yanking is a publisher's tool for withdrawing something broken, and it does exactly two things. It removes the version from the catalog, so no host is offered it as an update; and it stops the bundle being served from its download URL. Withdrawing the only approved version of a published listing is refused, because that would leave a published listing with nothing to install. The version row stays in the database rather than being deleted, because download history points at it.
What it cannot do is reach an installation. A host that already unpacked the version keeps running it: the catalog can stop serving it and the update check can stop offering it, and neither of those is an uninstall. The operator's real answer is to disable the plugin, which is a local action on a local machine, and the publisher's real answer is to ship a fixed version with a higher number as quickly as possible. This is a genuine limitation of the platform and it is documented as one rather than dressed up — Marketplace policy states it as the headline of its versioning section, and the practical consequence is that publishers should assume anything they publish may be in use the moment it appears.
Two adjacent facts are worth knowing while you are here. A report about a malicious bundle is acted on differently from a yank: the listing is removed from the catalog first and the publisher is asked about it second, and if that turns out to be a mistake a version can be restored — the asymmetry is deliberate, because the cost of a listing being briefly unavailable is small and the cost of a malicious bundle staying available is not. And the marketplace's own reach stops at the HTTP response: it cannot uninstall anything, inspect a running host, or recover data. Any policy claiming otherwise would be lying about the architecture.
Still unanswered?
- Contact — a plugin question, or a report about a listing that does something it does not disclose.
- Documentation — the whole manual, grouped by section.
- Building a plugin — declarative versus code, what a manifest may contribute, and the rules the host enforces.
- Writing a skill — front matter, the real per-turn selection algorithm, and why a skill does not get picked.
- Publishing to the marketplace — the bundle, the checksum and the catalog entry.
- Marketplace policy — what is accepted, refused, and what a publisher can and cannot take back.
Where to go next
Linked from
Did this page answer your question?