Plugins
Subprocess-isolated extensions that give the agent new tools. Write them in any language.
What are plugins?
A plugin is a separate process that communicates with Salmex I/O over JSON-RPC 2.0 through stdin/stdout. Each plugin provides one or more tools the agent can call — typed, schema-validated operations that connect your agent to APIs, databases, services, and anything else you can write code for.
Key properties:
- Subprocess isolation — plugins run in their own OS process. A crash in a plugin never takes down Salmex I/O or other plugins.
- Language-agnostic — any language that can read stdin and write stdout works. Official examples exist for Go, Node.js, Python, Deno, and Bash.
- Explicit trust — discovered plugins are invisible to the agent until the owner explicitly approves them. Nothing auto-enables.
- Lazy by default — approved plugins start only when the agent first calls one of their tools, unless marked as eager in the manifest.
- Crash recovery — if a plugin crashes, Salmex I/O restarts it automatically with exponential backoff (up to 3 attempts).
The plugin system is available on all tiers, including free. Extensibility is not a paywall — paid tiers focus on multi-channel orchestration and managed infrastructure.
How plugins work
Lifecycle
Every plugin follows the same lifecycle:
- Discovery — Salmex I/O scans
<workspace>/.plugins/for directories containing aplugin.jsonmanifest. - Approval — the owner explicitly approves the plugin via the CLI, desktop app, or API. Until approved, the plugin's tools are hidden from the agent.
- Configuration — if the plugin declares a config schema, the owner provides values (API keys, preferences). Secrets are encrypted at rest.
- Initialize — when the plugin starts, Salmex I/O spawns the subprocess and performs a JSON-RPC handshake. The plugin receives its configuration and responds with its capabilities.
- Tool calls — the agent calls plugin tools like any other tool. Salmex I/O sends
tools/callrequests over stdin; the plugin responds on stdout. - Shutdown — when Salmex I/O stops or the plugin is revoked, a
shutdownnotification is sent and the process exits gracefully.
Protocol
The protocol is JSON-RPC 2.0 with Content-Length framing over stdio — the same transport family as LSP and MCP. Messages are structured, debuggable, and implementable in any language.
Content-Length: 128
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0","config":{}}}The host sends requests and notifications to the plugin's stdin. The plugin writes responses and notifications to stdout. All communication is multiplexed over a single connection.
Protocol methods
| Method | Direction | Purpose |
|---|---|---|
initialize | Host → Plugin | Handshake: exchange protocol version, config, and capabilities |
initialized | Host → Plugin | Host is ready; plugin can start emitting notifications |
tools/call | Host → Plugin | Invoke a tool by name with JSON arguments |
config/changed | Host → Plugin | User updated config; plugin can hot-reload |
shutdown | Host → Plugin | Graceful shutdown signal |
log.* | Plugin → Host | Plugin emits debug, info, warn, or error logs |
event.progress | Plugin → Host | Progress updates for long-running tools |
The manifest
Every plugin requires a plugin.json at its root. The manifest declares metadata, tools, and optional configuration.
{
"name": "hello-node",
"version": "0.1.0",
"description": "A minimal Node.js plugin",
"author": "Your Name",
"license": "MIT",
"protocolVersion": "1.0",
"interpreter": "node",
"main": "main.js",
"eager": false,
"capabilities": {
"tools": [
{
"name": "say_hello",
"description": "Greet someone by name",
"risk_level": "low",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The person to greet"
}
},
"required": [
"name"
]
}
}
]
}
}Key fields
| Field | Required | Description |
|---|---|---|
name | Yes | Plugin identifier. Lowercase, alphanumeric, hyphens allowed. |
version | Yes | Semantic version (e.g. 0.1.0). |
protocolVersion | Yes | Must be "1.0". |
interpreter | Yes | Runtime: node, python3, deno, bash, sh, or empty for compiled binaries. |
main | Yes | Relative path to the entrypoint file. |
capabilities.tools | Yes | Array of tool definitions with name, description, parameters (JSON Schema), and risk level. |
eager | No | If true, the plugin starts immediately after approval instead of on first tool call. |
config | No | Configuration schema for user-provided settings. Supports string, secret, boolean, integer, number, and select types. |
skills | No | If true, the host scans the plugin's .skills/ directory for markdown skill files. |
Risk levels
Each tool declares a risk level that determines how the judge handles it:
| Level | Behaviour | Example |
|---|---|---|
low | Auto-approved | Read-only lookups, search queries |
medium | User notified | Creating an issue, sending a message |
high | Requires explicit approval | Deploying code, deleting resources |
critical | Always requires approval | Merging a PR, dropping a database |
Supported runtimes
Plugins can be written in any language. The interpreter field in the manifest tells Salmex I/O how to start the process:
| Interpreter | Language | Entrypoint |
|---|---|---|
node | JavaScript | .js file |
python3 | Python | .py file |
deno | TypeScript | .ts file |
bash / sh | Shell | .sh file |
| (empty) | Compiled binary | Go, Rust, C, or any compiled executable |
All official hello-world examples use zero external dependencies — a single source file and a manifest. Complexity comes from your application logic, not framework overhead.
Plugin configuration
Plugins can declare a config schema in their manifest. Salmex I/O generates a settings form automatically from the schema — no custom UI needed.
{
"config": {
"properties": [
{
"key": "api_key",
"type": "secret",
"label": "API Key",
"description": "Your service API key",
"required": true
},
{
"key": "units",
"type": "select",
"label": "Units",
"default": "metric",
"options": [
{
"value": "metric",
"label": "Metric"
},
{
"value": "imperial",
"label": "Imperial"
}
]
}
]
}
}Supported field types: string, secret (encrypted at rest), boolean, integer, number, and select. Fields can be grouped into collapsible sections and support conditional visibility via the show_when property.
Plugins receive their decrypted configuration during the initialize handshake and again via config/changed notifications when the user updates settings.
Installing a plugin
Copy the plugin directory into <workspace>/.plugins/<plugin-name>/. The directory must contain a valid plugin.json.
When Salmex I/O discovers the plugin, approve it from the Plugins page in the desktop app. Nothing runs without your explicit consent.
If the plugin has required config fields, provide values on the plugin's configuration page before the first tool call.
The plugin's tools are now available to the agent. Ask your agent to use the tool or let it decide on its own based on the task.
Managing plugins
All plugin management happens in the Plugins section of the desktop app. You can approve, revoke, start, stop, and configure plugins from there. Individual tools from a plugin can be enabled or disabled without affecting the plugin itself — useful when you want to approve a plugin but restrict which tools the agent can access.
Security model
The plugin system is designed with defence in depth:
- Process isolation — each plugin is a separate OS process. It cannot access host memory or other plugins' state.
- Explicit approval — nothing runs without the owner's consent. Discovered plugins are invisible until approved.
- Manifest validation — manifests are validated at discovery time. Path traversal attempts, invalid interpreters, and malformed schemas are rejected.
- Tool namespacing — plugin tools are namespaced as
plugin-name/tool-name. A plugin cannot register tools that impersonate another plugin or a built-in tool. - Secret encryption — config fields of type
secretare encrypted at rest with AES-256-GCM and decrypted only when passed to the plugin. - Judge evaluation — plugin tools go through the same risk assessment as built-in tools. The judge evaluates every call before execution.
Examples and source code
The public salmexio/plugins repository contains reference implementations and examples:
| Plugin | Runtime | Description |
|---|---|---|
| hello-native | Go (compiled) | Single main.go, zero dependencies |
| hello-node | Node.js | Single main.js, no npm dependencies |
| hello-python | Python 3 | Single main.py, no pip dependencies |
| hello-deno | Deno | Single main.ts, no third-party imports |
| hello-bash | Bash | Single main.sh, standard unix tools only |
All hello plugins implement the same tool (say_hello) and config field (greeting). Pick the one that matches your preferred language and use it as a starting point.
The repository also contains design specs for more advanced patterns:
- github/ — secrets, config groups, conditional fields, and skills
- devops/ — dynamic tool registration, full risk spectrum, and log notifications
Building a plugin
The fastest way to start is to clone one of the hello plugins and modify it. Every hello plugin follows the same pattern:
- Read frames — parse Content-Length headers and JSON from stdin.
- Handle
initialize— extract config, respond with capabilities. - Handle
tools/call— dispatch by tool name, validate arguments, return results. - Handle notifications — respond to
shutdown(exit), ignore others.
Here is a minimal Node.js plugin:
const readline = require('readline');
let config = {};
function send(msg) {
const json = JSON.stringify(msg);
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
}
function handle(msg) {
if (msg.method === 'initialize') {
config = msg.params?.config || {};
send({
jsonrpc: '2.0', id: msg.id,
result: {
protocolVersion: '1.0',
capabilities: { tools: [{ name: 'say_hello', description: 'Greet someone', parameters: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } }] }
}
});
} else if (msg.method === 'tools/call') {
const greeting = config.greeting || 'Hello';
const name = msg.params?.arguments?.name || 'world';
send({
jsonrpc: '2.0', id: msg.id,
result: { content: [{ type: 'text', text: `${greeting}, ${name}!` }] }
});
} else if (msg.method === 'shutdown') {
process.exit(0);
}
}
// Read Content-Length framed messages from stdin
let buffer = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
buffer += chunk;
while (true) {
const headerEnd = buffer.indexOf('\r\n\r\n');
if (headerEnd === -1) break;
const header = buffer.slice(0, headerEnd);
const match = header.match(/Content-Length: (\d+)/);
if (!match) break;
const len = parseInt(match[1], 10);
const bodyStart = headerEnd + 4;
if (buffer.length < bodyStart + len) break;
const body = buffer.slice(bodyStart, bodyStart + len);
buffer = buffer.slice(bodyStart + len);
handle(JSON.parse(body));
}
});The salmexio/plugins repository includes complete examples with build scripts, deploy targets, and detailed READMEs for each supported runtime.
What's next
- Skills — markdown instructions that complement plugin tools
- Architecture — how plugins fit into the 8-layer system
- salmexio/plugins on GitHub — reference implementations, examples, and community plugins