//  AUTHORING

Writing a plugin

From an example that already runs, to an entry in the catalog.

What a plugin is

A plugin is a library cxagent did not ship, loaded into a session at run time. It can add tools the model may call, and it can add a block of guidance to the system prompt — saying what individual tool descriptions cannot, such as that positions are 1-based or which file types it serves. A plugin that declares no tools and only that block is a valid plugin too.

It declares all of this in a sidecar file that cxagent reads before it loads the assembly, so the load prompt can show what a plugin claims without running any of its code.

Two flavours: managed, a .NET assembly, portable across every platform cxagent runs on; and ABI, a native library, for a plugin your language cannot write managed — not portable, so it ships per platform.

Know what you are asking of a user. A plugin is not sandboxed: it runs in cxagent's own process and can do what that process can. The gate cxagent enforces is the decision to load it at all.

1. Start from the calculator

Both examples are a complete plugin in about a hundred lines, deliberately gating something absurd — adding two numbers — so the permission prompt is visible the first time you run it.

Copy one, rename it, and get it loading before you change what it does. Load it into a session with no config file to edit:

/plugin load my-plugin.dll

2. Declare your plugin contract

Your sidecar names the contract version it was written against:

{ "pluginContract": 2, "name": "my-plugin", ... }

The host compares this for exact equality. A contract-1 plugin is refused by a contract-2 host exactly as a contract-3 one would be, and that is deliberate: a plugin written for a different contract may omit something whose absence changes behaviour silently, so the host refuses rather than guessing.

Check it back from your own Load. A host older than your contract has never heard of it and reads your manifest with its own rules — a cxagent predating "dynamic" takes it for false and offers your tools ungated. Nothing fails, and the gate you declared is simply not there. So verify:

if (context.HostContract < 2)
    throw new NotSupportedException($"needs contract 2; this host speaks {context.HostContract}.");

3. Decide what needs gating

Each tool in your sidecar declares whether a call needs a human. There are three states:

gatedWhat it meansUse it when
trueevery call asks the tool can always do something the user would want to see first
falseno call asks the tool only ever reads, or only ever touches your own state
"dynamic"your code decides, per call, from the arguments the tool is dangerous only sometimes

"dynamic" is for when the arguments decide. A tool is not categorically safe or dangerous — a query that reads is not the same call as one that writes, and a lookup inside the working folder is not the same call as one outside it. That is exactly what the shipped csharp-lsp tools do: they run unasked inside the folder you started in, and ask before reading a file outside it. Declaring the tool true instead would make every lookup a prompt, which trains a user to click through them.

"dynamic" routes each call to a method that sees the arguments and returns either a gate with your own wording, or null for no prompt. Declaring "dynamic" without implementing IPluginGateSource fails the load: the sidecar promised a decision the assembly cannot make, and silently gating nothing is the wrong answer.

4. Publish a release with your artifact

Publish the built plugin as a release asset on your own repository, and publish a SHA256SUMS — or the hash of the artifact itself — alongside it. Your catalog entry pins that hash, and it is the only thing vouching for a file hosted somewhere this project does not control.

Version the plugin, and keep the version in the sidecar in step with the release that carries it. A catalog entry naming an asset no release carries is worse than no entry at all.

5. Submit it to the catalog

The catalog is a committed file in the cxagent repository. To add an entry, open an issue from the plugin submission template:

Submit a plugin →

It asks for the plugin's name, its repository, a one-line description, the contract it targets, the platforms it actually has downloads for, and its tools with each one's gating. Those are the fields the catalog carries, and they are what a reader judges before deciding to run your code — publisher and repository are claims, not facts a client can verify, which is why they must be true.

The steps for adding an entry by hand, and a worked example of each source shape, are in RELEASING.md.

The reference

The full contract — the lifecycle, the sidecar's every field, permission and settings, and the managed and ABI interfaces side by side — is at /docs/plugins.html.