Configuration and Validation

View as Markdown

Plugin configuration is an operator contract. Keep it portable JSON, make each setting’s effect clear, and reject ambiguous input before Relay changes the runtime. Provider clients, callbacks, file handles, and resolved secret values belong in implementation state rather than in the configuration document.

The runtime group is independent of the request-policy group. When either runtime setting is enabled, the examples install a small tool-execution wrapper that emits marks or manages an isolated stack. Disabling request rewriting, including with requests.break_chain, does not suppress those runtime operations.

The Two Configuration Files Have Different Jobs

FilePurpose
plugins.tomlRuntime configuration. It contains component kinds, enabled state, component-local config, validation policy, and references to discoverable manifests. Relay can layer discovered files with binding-provided configuration.
relay-plugin.tomlPackage manifest for one discoverable native library or grpc-v1 worker. It declares identity, compatibility, entrypoint, optional JSON Schema, and integrity metadata. It does not replace the component configuration in plugins.toml.

The binding APIs use the same canonical document shape as plugins.toml: a document version, components, and policy. Each component has a kind, an enabled flag, and a component-local JSON object. Keys stay snake_case in Rust, Python, Node.js, JSON, and TOML even though Node.js API method names use camelCase.

The following configuration activates every shared feature group and the native-only executor control:

1version = 1
2
3[[components]]
4kind = "documentation-plugin"
5enabled = true
6
7[components.config]
8tag = "documentation"
9
10[components.config.observe]
11enabled = true
12redact_keys = ["secret"]
13
14[components.config.requests]
15enabled = true
16mode = "enforce"
17blocked_tools = ["dangerous_tool"]
18blocked_models = ["restricted-model"]
19header_name = "x-nemo-relay-plugin"
20header_value = "documentation"
21priority = 20
22break_chain = false
23
24[components.config.execution]
25enabled = true
26priority = 30
27emit_pending_marks = true
28
29[components.config.runtime]
30emit_marks = true
31emit_isolated_scope = true
32
33# Include this group only for a native typed plugin.
34[components.config.executor]
35worker_threads = 2
36
37[policy]
38unknown_component = "error"
39unknown_field = "warn"
40unsupported_value = "error"

The top-level policy controls document validation that Relay owns. A custom plugin’s validate hook is responsible for unknown fields, types, ranges, enums, and cross-field rules inside its own config. Built-in plugins can additionally receive host policy overrides. Do not assume the top-level unknown_field choice automatically inspects an arbitrary third-party JSON object; implement and test that behavior in the plugin.

Publish the Component Schema with the Package

The following strict schema is used by the Python worker and native examples. The native example adds an executor object because only the typed native SDK owns a Tokio executor. additionalProperties: false is repeated inside each object so a misspelled control fails at the exact nesting level where it appears.

1{
2 "$schema": "https://json-schema.org/draft/2020-12/schema",
3 "title": "Documentation Plugin Configuration",
4 "type": "object",
5 "additionalProperties": false,
6 "properties": {
7 "tag": { "type": "string", "minLength": 1, "default": "documentation" },
8 "observe": {
9 "type": "object",
10 "additionalProperties": false,
11 "properties": {
12 "enabled": { "type": "boolean", "default": true },
13 "redact_keys": {
14 "type": "array",
15 "items": { "type": "string" },
16 "default": ["secret"]
17 }
18 }
19 },
20 "requests": {
21 "type": "object",
22 "additionalProperties": false,
23 "properties": {
24 "enabled": { "type": "boolean", "default": true },
25 "mode": {
26 "type": "string",
27 "enum": ["observe", "enforce"],
28 "default": "enforce"
29 },
30 "blocked_tools": {
31 "type": "array",
32 "items": { "type": "string" },
33 "default": ["dangerous_tool"]
34 },
35 "blocked_models": {
36 "type": "array",
37 "items": { "type": "string" },
38 "default": ["restricted-model"]
39 },
40 "header_name": {
41 "type": "string",
42 "minLength": 1,
43 "default": "x-nemo-relay-plugin"
44 },
45 "header_value": {
46 "type": "string",
47 "minLength": 1,
48 "default": "documentation"
49 },
50 "priority": { "type": "integer", "default": 20 },
51 "break_chain": { "type": "boolean", "default": false }
52 }
53 },
54 "execution": {
55 "type": "object",
56 "additionalProperties": false,
57 "properties": {
58 "enabled": { "type": "boolean", "default": true },
59 "priority": { "type": "integer", "default": 30 },
60 "emit_pending_marks": { "type": "boolean", "default": true }
61 }
62 },
63 "runtime": {
64 "type": "object",
65 "additionalProperties": false,
66 "properties": {
67 "emit_marks": { "type": "boolean", "default": true },
68 "emit_isolated_scope": { "type": "boolean", "default": true }
69 }
70 }
71 }
72}

Defaults in a schema describe the intended value to tools and readers; JSON Schema does not insert them into the component object. The implementation must merge the same defaults before it validates and registers behavior. Keeping one checked default value beside each typed configuration structure prevents the schema, validator, and runtime from silently interpreting omitted fields differently.

The Rust worker intentionally uses a permissive schema with additionalProperties: true, then reports unknown keys from its validation hook as warnings. This demonstrates a warning-based migration policy. Do not copy the strict schema into that worker unless unknown keys should become activation errors; the schema and validator must express the same operator contract.

Write Diagnostics for the Operator

Validation should be deterministic and free of registration or lasting I/O. A diagnostic contains a level, stable code, component identity when known, field path when known, and a sentence explaining how to correct the value. Use a warning when activation remains safe and the operator should review the choice. Use an error when the plugin cannot install the promised behavior.

The following diagnostic identifies an unsupported request-policy value and tells the operator which field to correct:

1{
2 "level": "error",
3 "code": "documentation-plugin.unsupported_mode",
4 "component": "documentation-plugin",
5 "field": "requests.mode",
6 "message": "requests.mode must be either observe or enforce"
7}

A checked-in JSON Schema gives editors and package validation the same first line of defense, but it does not replace the validation hook. The hook still owns semantic rules such as requiring at least one blocked target in enforce mode, checking relationships between feature groups, or applying a deliberate unknown-field policy.

Store secret references rather than secret values. A plugin can define environment variable names, credential-provider references, or another deployment-specific lookup in its schema, then resolve the secret during registration. Validation can confirm that the reference is well-formed without printing or persisting the resolved value in a diagnostic or runtime report.

Validate Before Activation

Use the following sequence to prevent invalid configuration from changing runtime state:

  1. Construct the effective plugin document, including any discovered plugins.toml layers that production startup uses.
  2. Call the binding or CLI validation path before initialization. Treat the returned report as data and fail deployment when it contains error diagnostics.
  3. Test missing required fields, wrong types, unsupported enum values, unknown fields, and invalid cross-field combinations. Repeat one invalid case with enabled = false; disabled components are still validated.
  4. Initialize only after the effective report is acceptable, then inspect the activation report separately. An unknown enabled kind can still prevent initialization when a permissive policy reported it as a warning.
  5. Exercise one call for each enabled feature group so configuration controls are tied to observable behavior.

Success means invalid or disabled-invalid input produces stable diagnostics without any registration, while a valid document activates only the requested feature groups. The complete runtime discovery and layering rules remain in Plugin Configuration Files.