Plugin Config (plugin.toml)
Every plugin has a plugin.toml in its root folder. It tells N.E.K.O what the package is, which Python class the host must load, and which optional capabilities it exposes.
Two different kinds of entry
[plugin].entry = "module.path:ClassName" is a host-loading entry point. It imports one NekoPluginBase class when the plugin process starts. Runtime entry IDs such as greet come from @plugin_entry(id="greet") or register_dynamic_entry(...); the Agent selects those IDs only after the plugin has loaded.
Below is a complete config for a fictional "Smart Notes" plugin. This plugin can search and create notes, has its own UI panel, supports multiple languages, and can be called by the AI agent.
Full example
[plugin]
id = "smart_notes"
name = "Smart Notes"
type = "plugin"
description = "Manage your notes: search, create, organize, with AI-powered classification."
short_description = "Note management with AI-powered organization."
keywords = ["note", "笔记", "memo", "record", "メモ"]
version = "1.2.0"
entry = "plugin.plugins.smart_notes:SmartNotesPlugin"
[plugin.author]
name = "Alice"
[plugin.sdk]
recommended = ">=0.1.0,<0.2.0"
supported = ">=0.1.0,<0.3.0"
[plugin.i18n]
default_locale = "zh-CN"
locales_dir = "i18n"
[plugin.store]
enabled = true
[plugin.ui]
enabled = true
[[plugin.ui.panel]]
id = "main"
title = "Smart Notes"
entry = "ui/panel.tsx"
context = "dashboard"
permissions = ["state:read", "action:call"]
[[plugin.ui.guide]]
id = "quickstart"
title = "User Guide"
entry = "docs/guide.md"
permissions = ["state:read"]
[plugin_runtime]
enabled = true
auto_start = true
[notes]
max_per_page = 20
auto_classify = trueSection by section
[plugin] — Who is this plugin
[plugin]
id = "smart_notes"
name = "Smart Notes"
version = "1.2.0"
entry = "plugin.plugins.smart_notes:SmartNotesPlugin"These four fields are required by the supported check and release workflow. id must match ^[A-Za-z0-9_-]+$ and must be unique. Legacy source discovery can still load some incomplete manifests or folder/ID mismatches, but package build and production installation require the declared ID, archive directory, executable destination, and entry package to stay aligned; no suffixed executable copy is created. entry must use module.path:ClassName and resolve to a NekoPluginBase subclass; a PluginRouter cannot be launched directly.
For a normal plugin, type = "plugin" is optional because it is the default. Use type = "adapter" only for an Adapter package. The removed extension type and [plugin.host] table are rejected.
Keep id stable across releases. Upgrades, reinstalls, and downgrades replace executable code while preserving runtime config, data, and cache; changing id creates a different plugin identity. Optional previous_ids only prevents an old and new identity from being installed together. It is not a runtime alias and does not migrate or delete old data. Any replacement requires explicit user confirmation.
description = "Manage your notes: search, create, organize, with AI-powered classification."
short_description = "Note management with AI-powered organization."
keywords = ["note", "笔记", "memo", "record", "メモ"]These fields affect Agent routing after the host has loaded the plugin:
description— Full description used in plugin metadata and fine-grained Agent assessment.short_description— Compact description used by coarse screening. If absent, the Agent may generate and cache one fromdescription.keywords— Regular-expression patterns. Matches are unioned into the Stage 1 candidate set; they do not bypass Stage 2 or guarantee execution.
Set passive = true for a listener/integration that must never participate in Agent dispatch. A non-passive plugin also needs at least one Agent-visible runtime entry before it can become a candidate.
The Agent's final Stage 2 decision returns a plugin_id and runtime entry_id. Both are checked against the candidates shown to that assessment; one corrective retry is allowed, then an invalid decision is rejected.
version = "1.2.0"Required by the check and release workflow. Used for version management and marketplace publishing.
[plugin.author] — Who wrote it
[plugin.author]
name = "Alice"Optional. Shown in Plugin Manager.
[plugin.sdk] — Which SDK version it's compatible with
[plugin.sdk]
recommended = ">=0.1.0,<0.2.0"
supported = ">=0.1.0,<0.3.0"Tells the host which plugin SDK versions this package supports. Values use Python packaging specifier syntax.
supported— Versions accepted without an untested exceptionrecommended— Best-tested range; being outside it produces a warninguntested— Additional allowed range that produces a warningconflicts— Explicitly rejected version ranges, even if another range matches
If supported is present, a host outside both supported and untested is not loaded. Invalid specifiers are also rejected.
[plugin_runtime] — How it runs
[plugin_runtime]
enabled = true
auto_start = true
priority = 0
timeout = 10
startup_failure = "warn"timeout- Number of seconds to wait for startup readiness; must satisfy0 < timeout <= 300. Omit it to use the system default.startup_failure- What to do iflifecycle.startupraises after the process is alive. Omit it to default towarn:warnkeeps the plugin running and marks startup as degraded,failaborts startup, andignoreonly logs the error.enabled— Set tofalseto temporarily disable without deleting filesauto_start— Whentrue, starts automatically with N.E.K.O; otherwise start manually from the panelpriority— Optional integer runtime ordering hint
Plugin Manager's independent auto-start switch controls the saved preference. By default, manual Start/Stop/Reload does not change that preference. Newly installed plugins remain pending auto-start approval until the independent switch is enabled; starting or reloading them manually never grants approval.
Setting NEKO_PLUGIN_SYNC_AUTO_START_ON_TOGGLE=1 opts into legacy preference synchronization: manual Start/Reload saves auto_start=true, and Stop saves auto_start=false. The switch's "manual start/stop does not change this" hint therefore describes the default mode. Even in legacy mode, a pending plugin still requires explicit approval through the independent auto-start switch.
[plugin.i18n] — Multi-language support
[plugin.i18n]
default_locale = "zh-CN"
locales_dir = "i18n"If your plugin needs multiple languages, create an i18n/ folder in your plugin directory with locale files:
i18n/
├── en.json
└── zh-CN.jsonDon't need i18n? Just don't include this section.
[plugin.store] — Persistent storage
[plugin.store]
enabled = trueWhen enabled, you can use self.store in code to save and retrieve data (key-value pairs) that persists across restarts.
Don't need storage? Just don't include this section (disabled by default).
[plugin.ui] — Custom UI
[plugin.ui]
enabled = true
[[plugin.ui.panel]]
id = "main"
title = "Smart Notes"
entry = "ui/panel.tsx"
context = "dashboard"
permissions = ["state:read", "action:call"]
[[plugin.ui.guide]]
id = "quickstart"
title = "User Guide"
entry = "docs/guide.md"
permissions = ["state:read"]If your plugin needs a custom interface in Plugin Manager:
panel— Interactive panel (written in TSX, can have buttons, tables, forms)guide— Read-only documentation (written in Markdown)
The file extension determines rendering: .tsx = interactive panel, .md = documentation.
Don't need UI? Just don't include this section. See Hosted UI for details.
Custom sections — Your business config
[notes]
max_per_page = 20
auto_classify = trueAdditional top-level sections are preserved as business config. Read them in code:
cfg = await self.config.dump()
notes_cfg = cfg.get("notes", {})
max_per_page = notes_cfg.get("max_per_page", 20)You can define as many custom sections as you want, named however you like.
Directory structure for this plugin
plugin/plugins/smart_notes/
├── plugin.toml ← the file above
├── __init__.py ← plugin code
├── i18n/ ← locale files (because [plugin.i18n] is configured)
│ ├── en.json
│ └── zh-CN.json
├── ui/ ← interactive panel (because [[plugin.ui.panel]] is configured)
│ └── panel.tsx
├── docs/ ← user guide (because [[plugin.ui.guide]] is configured)
│ └── guide.mdWritable state is separate from the source or installed executable directory:
<user data root>/plugins/smart_notes/
├── config/plugin.toml ← effective runtime configuration
├── data/ ← self.data_path()
└── cache/ ← self.cache_path()Only plugin.toml and the importable Python module named by [plugin].entry are required. The module does not have to be __init__.py, although that is the common layout. Installed package code remains separate from this writable state.
JSON Schema for the configuration panel
Place an optional config.schema.json beside the installed plugin's plugin.toml to describe field names, help text, and controls on the generic Configuration tab. No manifest setting or custom UI is required. Ship this file with the plugin, not in its writable runtime/profile directory. If your package uses an include allow-list, include this file.
For example, a schema for the [notes] section:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"notes": {
"type": "object",
"title": "Notes",
"properties": {
"max_per_page": {
"type": "integer",
"title": "Notes per page",
"description": "Maximum number of notes shown on a page.",
"x-title-i18n": { "en": "Notes per page", "zh-CN": "每页笔记数量" },
"x-description-i18n": { "en": "Maximum number of notes shown on a page.", "zh-CN": "每页最多显示多少条笔记。" },
"minimum": 1,
"maximum": 100,
"default": 20
},
"auto_classify": {
"type": "boolean",
"title": "Automatic classification",
"description": "Automatically organize new notes."
},
"sort_order": {
"type": "string",
"title": "Sort order",
"enum": ["newest", "oldest"]
}
}
}
}
}| Keyword | Form behavior |
|---|---|
properties | Describes object fields using the actual configuration structure. Existing undeclared fields remain editable. |
additionalProperties | An object schema describes dynamic keys absent from properties, including password controls and preview masking. Named properties take precedence. Boolean values supply no field annotations; this editor does not enforce key admission. |
title / description | Plain-text label and help text. The raw key remains visible as secondary information and is the fallback label. |
type | A single string, number, integer, boolean, object, or array selects the corresponding control. Without it, the editor infers the type from the current value. |
items | One child schema for array elements, including nested objects and arrays. |
enum | A nonempty list of strings, numbers, or booleans produces a dropdown and preserves value types. |
minimum / maximum | Numeric control bounds; integer controls accept only integers. |
maxLength | Maximum text input length. |
readOnly | Disables editing of the field and its child controls. |
String fields with writeOnly: true | Uses a password input with a reveal toggle and masks non-empty values in baseline hints, change summaries, and JSON data views. Real values are retained for saving; this is display masking, not encryption or access control. |
default | Initial value when explicitly adding a field or array item; not a runtime configuration default. |
x-title-i18n / x-description-i18n | Optional locale-to-text maps. Standard title and description remain strings. |
Translation fallback follows the panel's existing order: exact locale, primary language, en-US, en, the first nonempty map value, then title / description. The example only shows two languages; provide every language supported by your plugin when publishing.
This is a form presentation contract, not a complete JSON Schema validator or a server-side authorization/validation boundary. required, pattern, composition, $ref, boolean schemas, union types, and null controls are not supported by this form. The host never fetches $schema or $ref URLs. Plugins remain responsible for runtime validation; keep runtime defaults in plugin.toml / config.example.toml.
Opening the page never inserts schema defaults or writes a profile. Schema-only fields are shown but are only written after editing. Object merging and whole-array replacement retain existing behavior. The top-level plugin section remains protected and excluded from profile editing.
The file must be UTF-8 JSON with an object root ("type": "object"), at most 256 KiB and at most 32 levels of properties / items / additionalProperties nesting. Missing files retain the old editor. Invalid files or malformed supported keywords produce a warning and fall back to generic editing. Configuration queries return metadata separately as config_schema; it never becomes part of config or the profile.
