Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · Plugin settings and internationalization

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

Plugin settings and internationalization

Write a plugin settings page

Use the existing host editor for ordinary plugin settings. Declare configSchema in the plugin manifest; the host renders it under Settings without a custom browser bundle.

{
  "configSchema": [
    {
      "key": "enabled",
      "label": "Enabled",
      "type": "boolean",
      "default": true
    }
  ]
}

Add label and hint translations in the plugin's i18n files, and read the validated plugin configuration through PluginContext. The declared default initializes the instance form; instance runtime consumers use the matching runtime default when no value is stored. Per-account runtime values already resolve manifest defaults through ctx.userConfig(). Use userConfigSchema for personal settings. For a custom plugin settings page, declare the browser contribution and use the shared configuration editor described in Browser UI. MCP is a real browser/settings consumer; Files demonstrates an ordinary manifest-driven form.

Configuration fields

configSchema describes instance-wide settings. userConfigSchema describes per-account settings. Both use the same field shape. PluginConfigField and PluginManifest are inferred with TypeBox Static from their runtime schemas in src/plugins/manifest.ts, rather than maintained as parallel interfaces. CONFIG_FIELD_TYPES is the literal tuple accepted by the field schema; project-panel placement and the user field retain their schema literals. The browser imports these types only, so schema validation adds no browser code.

retiredConfigKeys optionally names exact instance-config keys the plugin no longer uses. For example:

{
  "retiredConfigKeys": ["sec_voice", "voiceProvider", "stt", "sttModel", "tts", "ttsModel", "ttsVoice"]
}

Discord and Telegram use this list when moving voice notes to core Voice mode. On each enabled plugin load, after its manifest and entry are accepted and before register(ctx), only the authoritative daemon removes present listed keys through ConfigStore.update. The plugin receives the cleaned configuration immediately. Each removed key produces one core log entry with reporting: 'local-only', without its value. Reloads with nothing left to remove make no write or removal log. Runners never prune, even when the manifest declares the list. Omission or an empty list leaves storage untouched.

The list is limited to 64 unique, non-empty, trimmed strings of at most 128 characters. A listed key still declared in configSchema is a manifest error. Keys are literal properties, not paths or patterns. Unlisted values, undeclared values, secrets and other plugins' configuration are retained. Per-account configuration is unaffected: configSchema describes a form, not a storage allowlist. One cleanup scans the bounded list and writes the plugin slice once against the snapshot revision. A concurrent config change or a failed write fails that plugin load through its normal error path, preserving the newer configuration.

The supported field types are:

string, secret, boolean, number, textarea, rolePolicies, model, provider,
section, enum, multiSelect, code, prompt, json, embeddingModel,
destination, projects, plugins, tools, models, timezone, tokenList, user

mcpServers is retired in browser API 33. The manifest parser drops it from both config forms with the same unknown-type diagnostic as every unsupported field, preserving stored values and keeping older installed plugins loadable. The MCP plugin's server page, API, and p_mcp_servers store are the single supported management surface; shared generic editor primitives remain unchanged.

Every field has key, label, and type. Optional properties are hint, required, min, max, step, placeholder, display, browse, default, providerType, modelKind, options, language, help, risk, advanced, fullWidth, credential and visibleWhen.

Field-specific rules:

  • options supplies value and label pairs for enum and multiSelect. A multiSelect default is an array of unique declared option values, including an empty array. For example, declare weekday options with values sun, mon, tue, wed, thu, fri, sat and set default: ["mon", "tue", "wed", "thu", "fri"]; the meeting-confirm plugin uses this for calling days. Instance and account manifests reject scalar, duplicate or undeclared defaults during parsing, before installation or loading. An omitted default remains omitted. Instance defaults initialize the existing form; account defaults also resolve at runtime through ctx.userConfig(); they do not write stored settings, and the plugin owns the matching runtime fallback. Validation costs one option scan per selected value and one duplicate check.
  • language selects the editor mode for code.
  • providerType narrows a provider picker to one or more provider types.
  • model, embeddingModel and models fields hold a model exec. The host keeps the stored spelling canonical (<provider>/<model>) on both the instance-wide and the per-account form, so a plugin never has to read the legacy elowen: prefix. A models list is saved only if it is a valid personal model grant: * stands alone, and no entry contains a comma.
  • modelKind: "image" makes a model field use the image model catalog. Such a field is not an exec: its value is the bare id the image APIs take, and the host stores it exactly as it is.
  • For numbers, display.control is input or slider; display.unit and display.divisor affect presentation only. isStepAligned(value, base, step) in src/shared/pluginNumber.ts is the canonical finite-number alignment rule; callers validate the step as positive first and use the declared min, or zero, as the base. It accepts rounding error within eight scaled machine epsilons. Manifest defaults and slider maxima consume it; the instance and account API writers share it through src/api/routes/plugins/configValues.ts, preserving finite-number, min/max and exact field-error checks. The byte-identical web/lib/pluginNumber.ts copy serves draft validation and display conversion without a daemon runtime import. Omitted steps impose no alignment constraint. Evaluation is constant-time, does no I/O and does not clamp values or alter the caller's bounds or error messages.
  • Every enum field renders through the one anchored dropdown (ChoiceField, the enum case in web/modules/settings/PluginConfigEditor.tsx); display.control and risk do not select a different control, risk only adds its badge. Stored values stay unchanged. select is rejected on non-enum fields, and numeric display options are rejected on an enum select (parseManifest in src/plugins/manifest.ts).
  • display.placement: "pluginForm" explicitly assigns an editable field to the plugin's own form. The generic instance and account editors omit its row and any section left with no visible inputs. The field remains in the schema, config reads, validation and revision-safe writes. Omission keeps the generic editor. This is opt-in placement, never automatic duplicate detection or an authorization boundary. Sections cannot declare it. Teams uses it for rolePolicies, whose complete ordered and wildcard policy editor lives in People. Other intentional plugin-page, account and entity settings surfaces stay available and share the same controls.
  • browse: "directory" is valid only on tokenList in instance config.
  • A tokenList default must be an array of trimmed, non-empty, unique strings.
  • A timezone default must be a trimmed valid IANA timezone; an empty string is also accepted.
  • user picks one Elowen account and stores its username as a string; an empty string means none. The form shows the admin user directory (GET /users) as a dropdown with each account's avatar and name, and the config route refuses a username that does not exist at save time (one directory read per save of a schema that declares the type). A saved username whose account was later deleted stays shown and marked, and the plugin resolves it at runtime and owns the missing-account case. Because the directory is admin-only, the type is rejected in userConfigSchema. A core that predates the type drops the field with a diagnostic and keep the stored value. meeting-confirm uses it for the account its calls run as.
  • Numeric bounds, defaults, and steps are validated by the host.
  • visibleWhen displays a field only when another field equals the declared string, number, or boolean value.
  • Unknown field types are removed from the form with a warning. The stored value is preserved so a newer host can render it later. Other manifest errors reject the manifest.

Keep English labels, hints, descriptions, options, and browser strings in the manifest. They are the fallback when no translation exists.

Manifest patch and replacement validation

applyConfigPatch(schema, stored, values, brainModelGate?, destinationPlatforms?, usernames?, operation = 'patch') is the single host helper for manifest field validation and reset/secret rules. The real bulk caller supplies 'replace'; per-plugin/account PATCH callers use the default. Replacement starts with supplied values, preserves supplied undeclared keys and removes omitted keys. PATCH starts with stored values and preserves omitted keys. Explicit null clears optional overrides and secrets according to the existing required-field contract; a blank secret retains the stored secret in either mode. Replacement validates all supplied declared fields, while PATCH retains its existing unchanged-value contract. No caller adds a second schema validator. Without a supplied plugin slice neither route invokes this helper for that plugin.

Scalar configuration normalization

Import { normalizeConfigScalars } from 'elowen-plugin-shared/configScalars'. Its contract is normalizeConfigScalars(source: Readonly<Record<string, unknown>>, booleanDefaults: Readonly<Record<string, boolean>>, enumOptions: Readonly<Record<string, readonly [string, ...string[]]>>): Record<string, unknown>. Minimal use is normalizeConfigScalars(config, { showTools: true }, { mode: ['live', 'final'] }). It returns a shallow copy and processes only scalar overrides whose source value is not undefined. Missing/undefined values keep inheritance intact, valid booleans and declared enum strings remain unchanged, invalid present booleans select the supplied manifest default, and invalid present enum values select the first nonempty option. Unlisted values and nested references are retained. Callers provide the already-object source and their own domain defaults/options; this helper does not parse unknown input or own roles, token lists or platform policy. It has no IO or registration cost. Current validation is tests/shared/configScalars.test.ts; no production caller uses this helper yet.

Shared settings editor

UI API 51 publishes exact PluginConfigEditorProps: { name, detail, draft, mode?, layout? }, with mode setup | behavior | advanced | all | credentials. A host plugin detail carries credentials; then every mode except credentials omits those fields (see Credentials and the Access page). layout defaults to groups; use rows inside a caller-owned entity group to render only schema rows with the same localization, controls and draft. Filter that entity's schema in detail, but keep the complete schema in the shared draft so other fields and their revision are preserved. Optional model fields offer the empty choice; required model fields do not. Its minimal PluginConfigEditorDetail is { name, configSchema, secretsSet, secretsUnreadable?, i18n? }; secretsUnreadable names stored secrets the daemon can no longer decrypt, which the editor marks as unreadable instead of stored, keeping the Replace action. i18n maps locale to optional fields, then field key to optional label, hint, help and options keyed by option value. The editor owns locale lookup, English fallback and host risk translations. Do not pass localization callbacks. Supply the existing PluginConfigDraft, including resolveConflict('reload' | 'merge'); secrets still use explicit commitValue and are never returned as plaintext. An unresolved revision conflict blocks all writes, including autosave, retry, flush and queued explicit commits. Its remote revision is adopted only after the user chooses reload or merge. Every save a draft sends carries only the fields whose value changed since the last persisted snapshot, and a committed value goes out even when unchanged: a field the editor merely DISPLAYS — an account default the account inherits, a legacy value nobody touched — is never written back as an edit of its own. Conflict merge applies only fields changed locally since the persisted snapshot, preserving untouched remote updates and deletions. A draft with no unsaved edit adopts a newer revision and configuration that another editor of the same plugin stored (Settings → Access next to a plugin page, for example); a draft with an edit keeps its revision and meets the conflict choice on save. For personal settings, inject the existing personal-config transport as usePluginConfigDraft's save option and retain the host query key.

Titled groups with visible inputs start open and keep their existing plugin.<name>.<mode>.<section> fold key. Remembered states win and deep-link reveal still works. Headerless fields and intentional information cards do not fold; sections with only hidden fields produce no empty disclosure. Short field hint remains visible; detailed help uses HelpTip. Section hint and help are deduplicated and combined in the header HelpTip.

The existing Input accepts unit?: string: omission preserves ordinary input rendering; unit="" reserves the same numeric unit cell. A full-width numeric band is 18 rem: 13.5 rem input, 0.5 rem gap and 4 rem unit cell. The input shrinks on narrow rows. className, native attributes, events and ref target the actual input; a nonempty unit joins its accessible description without changing its label. Divisors only scale display; bounds, precision and step remain canonical. Explicit sliders remain sliders, with invalid saved values falling back to the unit-aware input.

components.LimitSliderRow publishes the existing limits-dialog row and exact LimitSliderRowProps: icon, label, valueLabel, scalar value/min/max/step and onChange, plus optional hint, note, children, ariaLabel, disabled, sliderClassName and siblingControl. Core limits dialogs and Chatbot limits share it; the generic schema slider is the host's SliderWithReading with an unbounded reading, so a long unit wraps under the track.

A minimal custom editor is:

const { PluginConfigEditor } = runtime.components;
const draft = runtime.hooks.usePluginConfigDraft(detail.name, detail);
return <PluginConfigEditor name={detail.name} detail={detail} draft={draft} mode="all" />;

Bundles using this signature, Input.unit or LimitSliderRow declare web.requiresApiVersion: 51 in the manifest and requiresApiVersion: 51 in registration. Schema presentation changes alone need no bundle minimum increase. Release the host before dependent bundles; no old callback adapter is retained. requiresSharedApi remains a separate helper contract. Rendering adds local field lookups and a schema scan, with no new request, store or endpoint.

Credentials and the Access page

Settings → Access is the one administrator page where every plugin's instance credentials are entered: API keys, tokens and passwords, plus the few plain fields they cannot work without, such as an account SID next to its auth token. A plugin appears there only by declaring it in its manifest; core knows no plugin names.

Every type: "secret" field of configSchema is a credential. Mark a non-secret companion with credential: true:

{
  "configSchema": [
    { "key": "sec_connection", "label": "Connection", "type": "section" },
    { "key": "accountSid", "label": "Account SID", "type": "string", "required": true, "credential": true },
    { "key": "authToken", "label": "Auth token", "type": "secret", "required": true },
    { "key": "region", "label": "Region", "type": "enum", "options": [{ "value": "us1", "label": "US" }] }
  ]
}

Where and when it acts:

  • The daemon derives the classification once, with credentialFieldKeys in src/shared/pluginCredentials.ts. GET /plugins and GET /plugins/:name return it as credentials: { keys, corrupt }: the credential keys in schema order and the secret keys the encrypted vault reports corrupt, read from the vault's corruption index without decrypting anything.
  • Settings → Access lists every enabled plugin with credentials as one foldable row, open by default and remembered per plugin: icon, name and status (connected, required details missing, reconnect needed, not set). Expanding it renders exactly those fields through the shared editor (mode="credentials"), grouped by their declared sections when they span more than one. Secrets keep the write-only commit on blur or Enter; companions autosave with the plugin's revision-safe draft through PATCH /plugins/:name/config.
  • Every other editor mode leaves the credential fields out, and a section left without inputs disappears. setup and all show an Access card instead, with the status and a link to /settings?cat=access&row=access.<plugin>, which opens that row expanded. This applies wherever the host detail is passed to the editor, including a plugin's own settings page; Microsoft Teams' workspace Settings tab is a real consumer.

What happens without a declaration: a plugin with no secret and no marker gets no row, its editor renders as before, and its listing entry has no credentials.

Limits: the marker is accepted only in configSchema, only as true, and only on non-secret editable fields; userConfigSchema rejects it, because personal credentials stay on the account form. Required credentials count toward the Access status, not toward the plugin detail's setup checklist. Storage, masking, revisions and restart behavior are those of ordinary config fields and secrets. Cores without this seam ignore the marker, so a plugin may declare it without raising requiresCore. Cost: one plugin detail read per credential row when Settings → Access opens; no new endpoint or store.

Real consumer: Twilio marks accountSid and apiKeySid next to its apiKeySecret and authToken.

i18n

Put optional locale files in i18n/<lang>.json. The loader builds a locale map from the files it can parse. A locale may contain description, label, userConfigLabel, and fields, where each field key may override label, hint, help, and option labels. Both instance configSchema and per-account userConfigSchema forms resolve fields.<key>.help in the current locale and fall back to the field's English manifest help when the override is absent. Long-form help stays behind the shared help affordance and is omitted when it is blank or repeats the localized one-line hint. For example, a Raynet username field can supply fields.username.help in i18n/cs.json; without it, the same form renders the manifest text. Resolution is a local field lookup and performs no additional request. Its alerts map overrides manifest alert templates by key. Its web object may override nav, account, user, project, settings, and the flat strings map. The English manifest and web metadata remain the source and fallback. Missing or malformed locale files do not stop the plugin; the affected text remains in English.

The manifest's top-level alerts map contains the plugin's English alert templates. Alert producers pass a title key, optional subtitle and body keys, optional facts (up to eight { labelKey, valueKey } template pairs shown as a labelled list), and tagged parameters through ctx.alerts. Numbers, { kind: 'bytes', value } and { kind: 'usd', value } parameters are formatted for the recipient's locale. The host validates every key and placeholder and renders the message for each recipient's locale when the bell or push payload is read; push receives the same text as plain lines.

The instance configuration API is administrator-only. The host exposes plugin listing and detail, live contribution information, configuration, enablement, logs, and data-management routes under the /plugins route family. The schema form reads a masked snapshot and its revision, then sends expectedRevision with the next patch. Omitted keys remain unchanged. null clears an optional non-secret value. A stale revision returns 409 with the canonical current snapshot. A successful instance config write is durable before the restart that applies it: the response is 202 with pending: true, plus a restart block when this daemon can restart itself.

Secret fields are write-only. The host returns only the names of secrets that are set. Omitted and empty ('') secret values keep the existing secret; an explicit null erases it, also for a required field, which then reads as missing setup. The core form never puts a secret in its autosaved snapshot: an unset secret saves on blur or Enter, a stored one shows only a Replace action, and replacing it with nothing sends that null. Escape cancels a replacement. This applies to configSchema and userConfigSchema alike.

For per-account settings, use userConfigSchema, userConfigLabel, and optionally userConfigPlacement: "account" | "pluginPage". Read values at runtime with ctx.userConfig(). It resolves each non-secret field from the account's own value, then the account default row under ACCOUNT_DEFAULTS_USER_ID = 0, then the manifest default. It returns null when the current turn is not acting as an account. A secret stays personal and never resolves from the default row or a manifest default; chatbot accounts skip the default row. This does not read the instance's plugins.config slice. For example, Raynet's useOwnCredentials resolves to its manifest's false unless an account default or an own value turns it on. Administrators edit these defaults through GET /users/defaults/plugins and PATCH /users/defaults/plugins/:name with { values, revision }; secrets are rejected. POST /users/defaults/apply with plugin:<name> clears human accounts' non-secret overrides and advances their revisions. GET /plugins/user-config reports effective values. These are synchronous small-blob reads, with no model cost or plugin reload. Per-account values use the same revision and patch rules. The host routes are GET /plugins/user-config and PATCH /plugins/:name/user-config. Secret values never leave the daemon.