Create a Plugin
A plugin is a directory with a plugin.yaml manifest at the root plus
whatever tool modules, HTML views, and sub-agent definitions the manifest
references. Ship what you need, skip what you don’t — a plugin with only
tools is valid, a plugin with only a workspace view is valid.
Layout
~/.bahulam/plugins/options-terminal/
├── plugin.yaml ← manifest (required)
├── config/
│ ├── workspace.yaml ← entry agent prompt + tool allowlist
│ └── agents/ ← optional delegated subagents
├── lib/
│ └── bsm.mjs ← shared code, imported by tool modules
├── tools/
│ ├── price.mjs ← one module per tool
│ ├── implied-vol.mjs
│ └── chain.mjs
└── workspace/
└── terminal.html ← the panel shown in the browserEvery path in the manifest is resolved relative to the plugin directory.
The Manifest — plugin.yaml
apiVersion: bahulam.plugin/1
kind: Plugin
metadata:
name: options-terminal # unique across your installed plugins
version: 1.0.0
description: >
Black-Scholes-Merton pricing, IV solver, multi-leg strategy analytics,
and a live chain terminal.
author: your-name
repository: https://github.com/your-name/options-terminal
config:
# Client-side tools the agent can call
tools:
- name: opt_price
description: Price a European option with BSM and return full Greeks
tool: ./tools/price.mjs # the JS module that implements this tool
parameters: # JSON Schema, sent to the model
type: object
properties:
type: { type: string, enum: [call, put] }
spot: { type: number }
strike: { type: number }
days: { type: number }
iv: { type: number, description: "annualized vol as decimal" }
required: [type, spot, strike, iv]
# Entry/primary agent. Keep the prompt in config/workspace.yaml.
workspace: ./config/workspace.yaml
# Optional delegated sub-agents.
agents_from: ./config/agents/
# Panels contributed to the browser workspace
views:
- type: panel
name: Options Terminal
source: ./workspace/terminal.html# config/workspace.yaml
apiVersion: agent.framework/v1
kind: SingleAgent
metadata:
slug: options-analyst
name: Options Analyst
role: specialist
description: Derivatives analyst backed by the opt_* tools
agent:
max_iterations: 10
system_prompt: |
You are an options analyst. Never estimate prices, Greeks, or IVs
by hand — always call the opt_* tools.
tools: [opt_price, opt_implied_vol, opt_chain]Rules the loader enforces:
apiVersionmust bebahulam.plugin/1(older shapes are ignored silently)- Agent files referenced by
config.workspaceorconfig.agents_fromshould use the backend-compatibleapiVersion: agent.framework/v1shape:metadatafor identity,agentfor prompt/model/runtime caps, and top-leveltoolsfor the allowlist. Older flat agent YAML is still accepted by the CLI for compatibility. metadata.namemust be unique across search paths (later paths override earlier)- Tool names must match
^[A-Za-z_][A-Za-z0-9_-]{0,63}$and must not shadow a built-in (read_file,shell,edit_file, …); collisions are dropped at schema-validation time - Cap: 200 client tools per session
Tool Handlers
Every tool is an ES module that exports at minimum an async call(args)
function returning { success, output }:
// tools/price.mjs
import { bsm } from '../lib/bsm.mjs';
export const name = 'opt_price';
export const description = 'Black-Scholes-Merton price + Greeks';
export const inputSchema = { /* optional; falls back to manifest parameters */ };
export async function call(args, { signal, pluginName } = {}) {
try {
const T = args.days / 365;
const g = bsm(args.type, args.spot, args.strike, T, args.iv,
args.rate ?? 0.05, args.div_yield ?? 0);
return { success: true, output: {
price: g.price,
greeks: { delta: g.delta, gamma: g.gamma, vega_per_1pct: g.vega/100 },
}};
} catch (err) {
return { success: false, output: `opt_price: ${err.message}` };
}
}Handler contract, in full:
- Signature:
async (args, options) => ({ success, output }) options.signal— anAbortSignalyou should honor for long-running workoptions.pluginName— set for logging / attributionoptions.state— the plugin’s Shared Blackboard handle (lazy — only opens the DB if you touch it)outputcan be anything JSON-serializable; the agent sees the JSON representation- Throwing is fine — the executor catches it and returns
{ success: false, output: "Plugin tool error (name): <message>" } - No filesystem sandbox — handlers run in your CLI process with your permissions. Treat every plugin the same way you treat a shell command someone told you to run.
Not restricted to JavaScript. For Python/Go/Rust/remote services, see Bring your own tools via MCP — a plugin can declare MCP servers alongside JS handlers, all mixing into the same agent tool list.
Workspace Views
A view is a plain HTML file. The workspace serves the whole plugin directory
statically at /plugin-view/<plugin>/<path>, so relative imports of CSS, JS,
images, fonts, and WASM all work.
Each view opens as a central-panel tab next to your file tabs. The menubar gains a Plugins dropdown that lists every declared view, so you can reopen one after closing its tab.
Inside the iframe, call the CLI’s tools through the local API:
<script>
const token = new URLSearchParams(location.search).get('token');
async function tool(name, args) {
const res = await fetch('/api/tools/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Bahulam-Local-Token': token, // token is passed in the iframe URL
},
body: JSON.stringify({ name, args }),
});
const body = await res.json();
if (!res.ok || body.ok === false) throw new Error(body.error || 'failed');
const r = body.result;
if (r?.success === false) throw new Error(String(r.output));
return r?.output ?? r;
}
const g = await tool('opt_price', { type: 'call', spot: 100, strike: 100, days: 30, iv: 0.25 });
document.getElementById('price').textContent = g.price.toFixed(4);
</script>The iframe is sandboxed with allow-scripts allow-same-origin allow-forms.
That is enough to call the local API, run canvas/WebGL, and persist to
localStorage, but blocks top-level navigation, popups, and cross-origin
frames.
Any tool the CLI knows about — built-in, from a different plugin, or an MCP tool from this plugin’s declared servers — is callable from a view. That means one plugin can compose another, and views can drive the exact same tools the agent does.
Plugin Agents
The entry agent loaded from config.workspace, delegated agents loaded from
config.agents_from, and any inline config.agents entries are merged into
the same registry as .bahulam/agents/ files, but they keep
source_scope: plugin. That scope matters: plugin agents are available inside
the plugin workspace and can be run explicitly by slug, while autonomous
main-loop delegation only sees them after the slug is allowlisted in plugin
settings.
The agent’s tools: list is the contract for delegated execution. If
options-analyst declares tools: [opt_price, opt_implied_vol, opt_chain],
then the sub-agent can choose those tools during its run and the local CLI or
workspace executor calls the underlying handlers. The primary agent should
delegate to the plugin agent for domain work instead of calling those tools
directly, except when the user explicitly asks for a specific tool-level
operation.
Invoke a plugin agent directly from the REPL:
/run options-analyst "Price this spread and explain the risk"For autonomous delegation from the normal chat loop, add the plugin agent slug
to settings.plugins.agent_allowlist. If a plugin agent has the same slug as a
project agent, global agent, or built-in role, the local or built-in definition
wins and the plugin agent is skipped for that slug.
Agents appear in bahulam agents list. Workspace-scoped plugin agents may be
shown as discoverable but not runnable from the main loop until allowlisted.
Next
- Bring your own tools via MCP — Python, Go, Rust, remote services
- Shared Blackboard — persistent state, live UI updates
- Install & Manage
- Publish & Distribute