Extensions

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).
Plugins are free

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:

  1. Discovery — Salmex I/O scans <workspace>/.plugins/ for directories containing a plugin.json manifest.
  2. 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.
  3. Configuration — if the plugin declares a config schema, the owner provides values (API keys, preferences). Secrets are encrypted at rest.
  4. 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.
  5. Tool calls — the agent calls plugin tools like any other tool. Salmex I/O sends tools/call requests over stdin; the plugin responds on stdout.
  6. Shutdown — when Salmex I/O stops or the plugin is revoked, a shutdown notification 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.

Wire format
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

MethodDirectionPurpose
initializeHost → PluginHandshake: exchange protocol version, config, and capabilities
initializedHost → PluginHost is ready; plugin can start emitting notifications
tools/callHost → PluginInvoke a tool by name with JSON arguments
config/changedHost → PluginUser updated config; plugin can hot-reload
shutdownHost → PluginGraceful shutdown signal
log.*Plugin → HostPlugin emits debug, info, warn, or error logs
event.progressPlugin → HostProgress updates for long-running tools

The manifest

Every plugin requires a plugin.json at its root. The manifest declares metadata, tools, and optional configuration.

plugin.json (minimal)
{
	"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

FieldRequiredDescription
nameYesPlugin identifier. Lowercase, alphanumeric, hyphens allowed.
versionYesSemantic version (e.g. 0.1.0).
protocolVersionYesMust be "1.0".
interpreterYesRuntime: node, python3, deno, bash, sh, or empty for compiled binaries.
mainYesRelative path to the entrypoint file.
capabilities.toolsYesArray of tool definitions with name, description, parameters (JSON Schema), and risk level.
eagerNoIf true, the plugin starts immediately after approval instead of on first tool call.
configNoConfiguration schema for user-provided settings. Supports string, secret, boolean, integer, number, and select types.
skillsNoIf 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:

LevelBehaviourExample
lowAuto-approvedRead-only lookups, search queries
mediumUser notifiedCreating an issue, sending a message
highRequires explicit approvalDeploying code, deleting resources
criticalAlways requires approvalMerging 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:

InterpreterLanguageEntrypoint
nodeJavaScript.js file
python3Python.py file
denoTypeScript.ts file
bash / shShell.sh file
(empty)Compiled binaryGo, 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 schema example
{
	"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

Place the plugin in your workspace

Copy the plugin directory into <workspace>/.plugins/<plugin-name>/. The directory must contain a valid plugin.json.

Approve the plugin

When Salmex I/O discovers the plugin, approve it from the Plugins page in the desktop app. Nothing runs without your explicit consent.

Configure (if required)

If the plugin has required config fields, provide values on the plugin's configuration page before the first tool call.

Use it

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 secret are 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:

PluginRuntimeDescription
hello-nativeGo (compiled)Single main.go, zero dependencies
hello-nodeNode.jsSingle main.js, no npm dependencies
hello-pythonPython 3Single main.py, no pip dependencies
hello-denoDenoSingle main.ts, no third-party imports
hello-bashBashSingle 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:

  1. Read frames — parse Content-Length headers and JSON from stdin.
  2. Handle initialize — extract config, respond with capabilities.
  3. Handle tools/call — dispatch by tool name, validate arguments, return results.
  4. Handle notifications — respond to shutdown (exit), ignore others.

Here is a minimal Node.js plugin:

main.js
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));
  }
});
Full examples with Makefiles

The salmexio/plugins repository includes complete examples with build scripts, deploy targets, and detailed READMEs for each supported runtime.


What's next