Manifest (spindle.json)¶
Every extension needs a spindle.json at the repository root.
{
"version": "1.0.0",
"name": "My Extension",
"identifier": "my_extension",
"author": "Your Name",
"github": "https://github.com/you/my-extension",
"homepage": "https://example.com",
"description": "A brief description of what this extension does.",
"permissions": ["generation", "interceptor"],
"entry_backend": "dist/backend.js",
"entry_frontend": "dist/frontend.js",
"minimum_lumiverse_version": "0.1.0"
}
Fields¶
| Field | Required | Description |
|---|---|---|
version |
Yes | Semver version string |
name |
Yes | Human-readable display name |
identifier |
Yes | Unique ID. Lowercase letters, numbers, and underscores only. Must start with a letter. Pattern: /^[a-z][a-z0-9_]*$/ |
author |
Yes | Author name |
github |
Yes | GitHub repository URL |
homepage |
Yes | Extension homepage or docs URL |
description |
No | Short description shown in the Extensions panel |
permissions |
Yes | Array of gated permissions this extension requires (can be []) |
requested_capabilities |
No | Array of declared backend capabilities that suppress specific install-time scanner blocks. See Backend Capabilities |
entry_backend |
No | Path to backend entry. Default: "dist/backend.js" |
entry_frontend |
No | Path to frontend entry. Default: "dist/frontend.js" |
minimum_lumiverse_version |
No | Minimum Lumiverse version required |
storage_seed_files |
No | Files/directories to copy into extension storage on install (see below) |
interceptorTimeoutMs |
No | Per-extension override (in milliseconds) for how long the host will wait for this extension's interceptor to return. See Interceptor Timeout below |
Requested Capabilities¶
requested_capabilities is distinct from permissions. Permissions gate runtime API surfaces (spindle.generate, spindle.chats, etc.). Capabilities suppress install-time scanner blocks for code patterns the extension legitimately needs — usually because a bundled dependency (Zod, Handlebars) or your own helper trips a heuristic that's looking for malware-style obfuscation.
{
"requested_capabilities": ["dynamic_code_execution", "base64_decode"]
}
Available capabilities:
| Capability | Suppresses |
|---|---|
"dynamic_code_execution" |
The dynamic code execution block. Required when the bundled backend contains eval( or Function( / new Function( — including inside vendored libraries (Zod feature-detect probes), inside RegExp literals whose source mentions Function\s*\(, or as part of a sandboxed in-extension script runner |
"base64_decode" |
The base64 decoding block. Required when the bundled backend uses Buffer.from(value, "base64") — common for binary asset I/O and image helpers |
Only declare capabilities you actually need. A full description of each, plus the list of patterns that cannot be opted out of, is in Backend Capabilities.
Storage Seed Files¶
Seed files let you ship default data (config templates, databases, assets) that get copied into the extension's storage directory on install.
{
"storage_seed_files": [
{ "from": "defaults/config.json", "to": "config.json" },
{ "from": "assets/", "to": "assets/", "overwrite": false },
{ "from": "required-data.db", "required": true }
]
}
| Field | Type | Default | Description |
|---|---|---|---|
from |
string |
required | Source path relative to the extension repo root |
to |
string |
same as from |
Destination path relative to extension storage root |
overwrite |
boolean |
false |
If true, overwrite existing files on update |
required |
boolean |
false |
If true, fail installation when the source file is missing |
Interceptor Timeout¶
Extensions that register a pre-generation interceptor are bound by a wall-clock budget. The host resolves this budget per run, so both the manifest value and the user's Spindle setting take effect on the next generation without requiring the extension to re-register.
Resolution order (highest priority first):
interceptorTimeoutMsin your manifest- The user's
spindleSettings.interceptorTimeoutMssetting (configurable in the Spindle panel) - Default:
10000ms
All values are clamped to [1000, 300000] ms (1 second to 5 minutes).
{
"identifier": "my_retrieval_extension",
"permissions": ["interceptor"],
"interceptorTimeoutMs": 45000
}
Use this when your interceptor performs real work before the LLM call — multi-step retrieval, graph traversal, external API lookups, or controller-driven context assembly — and needs a larger budget than the 10 second default. See Interceptors → Timeout for the full behavior, including what happens when the budget is exceeded.