docs(plugins): document runtime config APIs

This commit is contained in:
Peter Steinberger
2026-04-27 12:17:06 +01:00
parent 7f3f108521
commit c59af3caf7
5 changed files with 76 additions and 6 deletions

View File

@@ -90,6 +90,43 @@ releases.
## How to migrate
<Steps>
<Step title="Migrate runtime config load/write helpers">
Bundled plugins should stop calling
`api.runtime.config.loadConfig()` and
`api.runtime.config.writeConfigFile(...)` directly. Prefer config that was
already passed into the active call path. Long-lived handlers that need the
current process snapshot can use `api.runtime.config.current()`. Long-lived
agent tools should use the tool context's `ctx.getRuntimeConfig()` inside
`execute` so a tool created before a config write still sees the refreshed
runtime config.
Config writes must go through the transactional helpers and choose an
after-write policy:
```typescript
await api.runtime.config.mutateConfigFile({
afterWrite: { mode: "auto" },
mutate(draft) {
draft.plugins ??= {};
},
});
```
Use `afterWrite: { mode: "restart", reason: "..." }` when the caller knows
the change requires a clean gateway restart, and
`afterWrite: { mode: "none", reason: "..." }` only when the caller owns the
follow-up and deliberately wants to suppress the reload planner.
Mutation results include a typed `followUp` summary for tests and logging;
the gateway remains responsible for applying or scheduling the restart.
`loadConfig` and `writeConfigFile` remain as deprecated compatibility
helpers for external plugins during the migration window. Bundled plugins
and repo runtime code are protected by scanner guardrails: new production
plugin usage fails outright, direct config writes fail, gateway server
methods must use the request runtime snapshot, and long-lived runtime
modules have zero allowed ambient `loadConfig()` calls.
</Step>
<Step title="Migrate Pi tool-result extensions to middleware">
Bundled plugins must replace Pi-only
`api.registerEmbeddedExtensionFactory(...)` tool-result handlers with

View File

@@ -25,6 +25,26 @@ register(api) {
}
```
## Config Loading And Writes
Prefer config that was already passed into the active call path, for example `api.config` during registration or a `cfg` argument on channel/provider callbacks. This keeps one process snapshot flowing through the work instead of reparsing config on hot paths.
Use `api.runtime.config.current()` only when a long-lived handler needs the current process snapshot and no config was passed to that function. The returned value is readonly; clone or use a mutation helper before editing.
Tool factories receive `ctx.runtimeConfig` plus `ctx.getRuntimeConfig()`. Use the getter inside a long-lived tool's `execute` callback when config can change after the tool definition was created.
Persist changes with `api.runtime.config.mutateConfigFile(...)` or `api.runtime.config.replaceConfigFile(...)`. Each write must choose an explicit `afterWrite` policy:
- `afterWrite: { mode: "auto" }` lets the gateway reload planner decide.
- `afterWrite: { mode: "restart", reason: "..." }` forces a clean restart when the writer knows hot reload is unsafe.
- `afterWrite: { mode: "none", reason: "..." }` suppresses automatic reload/restart only when the caller owns the follow-up.
The mutation helpers return `afterWrite` plus a typed `followUp` summary so callers can log or test whether they requested a restart. The gateway still owns when that restart actually happens.
`api.runtime.config.loadConfig()` and `api.runtime.config.writeConfigFile(...)` are deprecated compatibility helpers. Bundled plugins must not use them; the architecture guard fails if production plugin code calls them or imports those helpers from plugin SDK subpaths.
Internal OpenClaw runtime code has the same direction: load config once at the CLI, gateway, or process boundary, then pass that value through. Long-lived runtime modules have a zero-tolerance scanner for ambient `loadConfig()` calls; use a passed `cfg`, a request `context.getRuntimeConfig()`, or `getRuntimeConfig()` at an explicit process boundary.
## Runtime namespaces
<AccordionGroup>
@@ -284,13 +304,25 @@ register(api) {
</Accordion>
<Accordion title="api.runtime.config">
Config load and write.
Current runtime config snapshot and transactional config writes. Prefer
config that was already passed into the active call path; use
`current()` only when the handler needs the process snapshot directly.
```typescript
const cfg = await api.runtime.config.loadConfig();
await api.runtime.config.writeConfigFile(cfg);
const cfg = api.runtime.config.current();
await api.runtime.config.mutateConfigFile({
afterWrite: { mode: "auto" },
mutate(draft) {
draft.plugins ??= {};
},
});
```
`mutateConfigFile(...)` and `replaceConfigFile(...)` return a `followUp`
value, for example `{ mode: "restart", requiresRestart: true, reason }`,
which records the writer intent without taking restart control away from the
gateway.
</Accordion>
<Accordion title="api.runtime.system">
System-level utilities.

View File

@@ -178,8 +178,9 @@ const mockRuntime = {
// ... other mocks
},
config: {
loadConfig: vi.fn(),
writeConfigFile: vi.fn(),
current: vi.fn(() => ({}) as const),
mutateConfigFile: vi.fn(),
replaceConfigFile: vi.fn(),
},
// ... other namespaces
} as unknown as PluginRuntime;