Connect+Documentation
AI plugins

Create an AI plugin

Author new tools the AI sidebar can use — the manifest, the capability api, and the write → validate → test → pack → publish workflow.

An AI plugin adds tools the Connect+ AI sidebar assistant can use — call an online API, transform data, (soon) read or act on the current page. A plugin is a small folder of JavaScript; the assistant loads its tools on demand and runs them in an isolated worker that can only reach what the plugin declared and the user approved.

This guide covers the manifest, the capability api, and the author workflow (write → validate → test → pack → publish). The types and validation come from the @arw/ai-plugin package, which mirrors the runtime exactly.


1. Anatomy

my-plugin/
  plugin.json     # the manifest — id, tools, permissions
  index.js        # the entry — one exported async function per tool

A minimal working example lives in docs/examples/ai-plugins/catfacts.

2. plugin.json

{
  "id": "catfacts",                 // lowercase, digits, hyphens (2–64)
  "name": "Cat Facts",
  "description": "Fetches a random cat fact.",
  "version": "1.0.0",
  "entry": "index.js",              // relative; .js / .mjs / .cjs, no ".."
  "permissions": {
    "network": ["catfact.ninja"],   // exact hostnames api.fetch may reach
    "page": "read",                 // "read" | "act" (page access — coming soon)
    "secrets": ["API_TOKEN"]        // UPPER_SNAKE names the user fills in
  },
  "tools": [
    {
      "name": "random_fact",        // bare identifier: a–z, 0–9, _
      "description": "Get a random cat fact.",
      "parameters": {               // JSON-Schema object; omit for a no-arg tool
        "type": "object",
        "properties": { "maxLength": { "type": "number" } }
      }
    }
  ]
}

Everything in permissions is a request, not a grant: the user sees exactly what a plugin asks for and approves it per profile. Declaring a host or secret grants nothing until they do.

3. The entry module

Export one async function per tool named in the manifest. Each receives (args, api) and returns a string (or any JSON-serialisable value, which is stringified for the assistant).

export async function random_fact(args, api) {
  const max = args.maxLength ?? 140;
  const res = await api.fetch('https://catfact.ninja/fact?max_length=' + max);
  return res.json().fact;
}

With TypeScript, import the types for full typing:

import type { Tool } from '@arw/ai-plugin';
export const random_fact: Tool<{ maxLength?: number }> = async (args, api) => {
  const res = await api.fetch(`https://catfact.ninja/fact?max_length=${args.maxLength ?? 140}`);
  return (res.json() as { fact: string }).fact;
};

4. The api your tool receives

MemberWhat it does
api.fetch(url, init?)HTTP to a host in permissions.network (http/https only). Anything else throws. Returns { status, headers, text(), json() }. Runs in the main process, bound to the profile session (its cookies/proxy/UA).
api.secret(name)The value the user entered for name (must be in permissions.secrets); undeclared throws. Never logged, never shown to the assistant.

api.page.* (read/act on the current tab) is coming and will require the page permission plus a per-site grant.

The plugin runs in a worker with no access to Node, the filesystem, the session, other plugins, or any host you didn't declare — the api is the whole surface.

5. Author workflow

Install the SDK/CLI:

npm install --save-dev @arw/ai-plugin
  • Validate the manifest + entry:
    npx aip validate ./my-plugin
  • Test a tool locally against a mock api (real fetch, --secret to supply one):
    npx aip dev ./my-plugin random_fact maxLength=80
    npx aip dev ./my-plugin notion_save db=Tasks --secret NOTION_TOKEN=secret_xxx
    Args are key=value, JSON-parsed (n=1, on=true, s=\"hi\").

Or drive it in your own tests:

import { devRun } from '@arw/ai-plugin';
import manifest from './my-plugin/plugin.json';
import * as mod from './my-plugin/index.js';

const out = await devRun(mod, manifest, 'random_fact', { maxLength: 80 }, {
  fetch: async () => ({ status: 200, headers: {}, body: '{"fact":"…"}' }),
});

6. Installing your plugin

  • Locally: AI sidebar → Permissions → Plugins → Install from folder… → pick your folder. Enable it, fill in any secrets, and ask the assistant to use it.
  • From the store (coming): pack + sign your folder into a signed .aip and publish it; users install with one click (Add to Connect). The desktop re-verifies the signature on install — the store is only distribution.

7. Trust

A .aip is signed. Signed by the ARW root key → first-party; signed by your author key → signed. The app re-verifies every package on install and refuses a tampered, unsigned, or key-rotated one.

On this page