A plugin is one JSON file (*.genisys-plugin.json) holding a manifest and the source of one ES module.
{
"format": "genisys-plugin@1",
"manifest": {
"id": "word-tools", // ^[a-z][a-z0-9-]{1,40}$
"name": "Word tools", // 1–60 chars
"version": "1.0.0", // semver x.y.z
"description": "Counts and cleans words.",
"tools": [{
"name": "word_count", // ^[a-z][a-z0-9_]{1,40}$, unique in the plugin
"description": "Count the words in a text.",
"input": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] },
"risk": "read" // "read" | "act" (act = changes something outside Genisys)
}],
"permissions": { "hosts": ["api.example.com"] }, // https hosts ctx.fetch may reach; [] for none
"skills": [{ "name": "tone-check", "description": "…", "instructions": "Markdown instructions" }]
},
"code": "export default { tools: { async word_count(input, ctx) { return { words: input.text.split(/\\s+/).filter(Boolean).length }; } } };"
}App connections and automation actions
The optional manifest.app adds a REST connection to the existing Developer console, Connections catalog and Automations executor. Open Settings → Developer → Open developer console → App integrations → New app. Fill in the connection, upload a PNG logo and declare actions. A connection-only file uses tools: [], skills: [], permissions: { hosts: [] } and code: "export default {};"; the app actions execute through the authorized HTTP adapter. Plugins can retain ordinary sandbox tools and skills alongside their app declaration.
{
"baseUrl": "https://api.example.com",
"auth": "header:X-Api-Key",
"logo": "",
"secretLabel": "Scoped API key",
"keyHelp": "Create an account-scoped key in your app settings.",
"docs": "https://example.com/docs",
"actions": [{
"id": "list_items",
"name": "List items",
"description": "Read items visible to the connected account.",
"method": "GET",
"path": "/items/{collection}",
"inputs": [{ "id": "collection", "label": "Collection ID", "in": "path", "required": true }]
}]
}baseUrl is a fixed public HTTPS origin without credentials, a port, path or query. The existing guarded fetcher enforces DNS, redirect and private-address restrictions. auth is bearer or a dedicated header:<name>; reserved headers are refused. Users enter keys through Connections into the existing vault. Saving a key reports unchecked until an actual provider verification exists; declarations do not supply OAuth enrollment.
An app declares 1–30 uniquely named actions. IDs match ^[a-z][a-z0-9_]{1,40}$; methods are GET/POST/PUT/PATCH/DELETE. Paths are relative, without queries or traversal, and each {field} requires a corresponding required path input. Inputs use id, label and in (path, query or body), with optional required, type (number/integer/boolean), long, json, list, row, name (body only) and placeholder. Non-GET bodies default to JSON; body: "form" is available in the manifest. GET has no body. Mutations always require action permission; GET can opt into risk: "act". Arbitrary headers, credentials, executable schemas and custom executors cannot be registered through this declaration.
The logo is empty or an inline PNG up to 64 KiB and 512 × 512 pixels. Remote pixels and SVG are refused. The same logo appears in Connections and workflow palettes/nodes, including nested actions and retry wrappers; bundled integrations retain their existing brand assets.
Only enabled, immutable production releases publish app connections/actions. Drafts and previews do not. Connection identity is developer.<plugin-id>.v<major>-<minor>-<patch> and action identity appends .<action-id>. A new version requires a new account binding; existing keys and saved workflow steps cannot silently acquire a changed endpoint or action. Rollback restores the prior binding. Disable/remove withdraws routing while retaining connection records and keys; old saved steps fail unavailable until their version is restored or edited. At most 200 production app connections are admitted; a refused promotion changes no release. Registration adds metadata, not permission, funding or provider acceptance. Local-only runs cannot send through these external actions. Import/export retains genisys-plugin@1 and legacy files without app remain compatible.
The module
export default { tools: { <name>: async (input, ctx) => output } } — one async function per manifest tool. The output must be JSON-serialisable (at most 100 KB as JSON). ctx:
ctx.log(...values)— appears in the console's logs (at most 200 lines per call).ctx.fetch(url, { method, headers, body })→{ status, headers, text }— HTTPS only, only topermissions.hosts; the user grants each host once. Performed by Genisys through its SSRF-guarded fetcher; the plugin itself has no network.ctx.stage—"test" | "preview" | "production".
Sandbox
Code runs in an isolated page with no Node, no file system, no network, no storage that outlives the call, and no access to Genisys; only ctx crosses the boundary. Each call has a 10 s limit. Calling a plugin tool from an agent is an integration action with target plugin:<id>, so it is asked for (or granted) like any other tool.
A released plugin's skills are offered to agents as <plugin-id>:<skill-name>: named in the project brief, listed by skill_list and loaded with skill_read, following the same stage rules as its tools. Skills are instructions only; they grant no permissions.
Stages
A plugin has an editable draft (stage test) and up to two frozen releases: a preview version and a production version, served at the same time.
test— the draft. Runs in the console's test runner and for test API keys.preview— promoting the draft freezes its version (which must be new and higher than every frozen one) and serves it to agents in the chosen preview projects.production— promoting preview copies that version to production: agents in all other projects and live API keys. Production keeps serving the old version until you promote a new one; rollback points production at any earlier frozen version. Disabling a plugin withdraws it from everyone but the console.
Harness protocol (desktop sandbox and web playground)
The host loads the harness page, then calls __genisys.start(callId, tool, inputJson, stage) and polls __genisys.poll(callId) → { state: "running" } | { state: "fetch", request } | { state: "done", output, logs } | { state: "error", message, logs }; a fetch is answered with __genisys.resolve(callId, responseJson). In an iframe the same calls travel as postMessage({ genisys: "start" | "poll" | "resolve", … }) and replies as { genisys: "state", callId, … }.