PluginsCreate a Plugin

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 browser

Every 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:

  • apiVersion must be bahulam.plugin/1 (older shapes are ignored silently)
  • Agent files referenced by config.workspace or config.agents_from should use the backend-compatible apiVersion: agent.framework/v1 shape: metadata for identity, agent for prompt/model/runtime caps, and top-level tools for the allowlist. Older flat agent YAML is still accepted by the CLI for compatibility.
  • metadata.name must 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 — an AbortSignal you should honor for long-running work
  • options.pluginName — set for logging / attribution
  • options.state — the plugin’s Shared Blackboard handle (lazy — only opens the DB if you touch it)
  • output can 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