* refactor(plugin-sdk): delete the heavy runtime-doctor barrel
Nothing may pull the state-db/kysely graph through a doctor barrel anymore.
The barrel's remaining heavy exports move to two narrow private-local
subpaths, each with a single purpose:
- doctor-repair-runtime: install-path diagnosis, plugin config removal, and
state-database schema detect/repair (matrix doctor, voice-call lazy import)
- plugin-state-store-runtime: the sync keyed-store factory. It stays out of
plugin-state-runtime because hot channel entrypoints import that at module
load and opening a store pulls the state-database graph.
Doctor closures also stop pulling ssrf-runtime (fetch-guard + gateway net)
for two legacy private-network helpers that live in the lighter ssrf-policy
subpath: mattermost, nextcloud-talk, tlon, matrix.
The closure guard now forbids the two new heavy subpaths instead of the
deleted barrel, so the invariant keeps being enforced where it still applies.
* perf(doctor): keep heavy graphs out of every doctor closure
Doctor enumeration cold-loads each declaring plugin's contract closure, so
one heavy import in a closure is paid by the whole sweep. Four barrels were
still dragging unrelated graphs in for trivial helpers; each is repaired at
the leaf rather than by caching downstream:
- Legacy private-network config migration moves to a config leaf. It only
reshapes records, but lived beside the SSRF runtime (DNS, proxy, logging),
costing mattermost ~2.7s. ssrf-policy re-exports it, surface unchanged.
- Streaming config readers move to a leaf. They read two config keys, but
streaming.ts also formats tool aggregates, pulling tool-display/logging/
acp-core; that cost slack ~2.3s.
- signal took the channel-secret barrel for isRecord; the canonical plugin
record guard is string-coerce-runtime (root AGENTS.md).
- llm-task took the provider-model barrel for parseModelRef, now a narrow
model-ref-parse subpath.
Full doctor enumeration of all 42 declaring plugins, built mode:
legacy config rules 6668ms -> 1265ms, state migrations 184ms -> 127ms.
No plugin remains an outlier; the slowest is now ~380ms against a ~200ms floor.
Public export surfaces of every touched SDK subpath are byte-identical
(verified by diffing built module exports before/after); the API baseline
hashes move only because re-exported declarations emit differently.
The closure guard gains rules for each repaired barrel so the invariant
holds for future closures.
* fix(release): exclude new private-local declarations from the published package
Same pack-path rule as c41da3759f: private-local subpaths ship without d.ts.
* fix(doctor): repair the closure guard violations that break main
The landed guard fails on main: three closures import heavy barrels for one
symbol each. Two more surfaced once the guard learned about the provider-model
barrel. Each gets a narrow subpath at the leaf:
- telegram sent-message-cache + state-migrations took the session-store barrel
(session accessor + state-db) for resolveStorePath -> session-store-paths
- discord thread-bindings.state took the channel-outbound barrel (reply
pipeline + channel registry) for one identity write -> outbound-echo-runtime
- discord model-picker took the provider-model barrel for normalizeProviderId,
which model-ref-parse now exposes beside parseModelRef
The guard also stops walking artifacts of plugins whose manifest declares no
doctor surface. Such a declaration gates the artifact off every enumeration
path exactly as resolvePluginDoctorContracts does, so its closure cost is never
paid; anthropic ("doctorContract": {}) was being held to a cost it cannot
incur. Absent declarations still load eagerly and stay enforced.
Side effect worth naming: discord's built doctor contract now loads again.
On main both discord and telegram fail to require in packaged builds (an
ESM-only transitive dep) and silently lose their repairs; this restores
discord and takes enumerated legacy config rules from 87 to 99. Telegram's
built artifact still pulls execa through dist chunking - a build-level defect
with a different owner, filed as follow-up.
LLM Task (plugin)
Adds an optional agent tool llm-task for running JSON-only LLM tasks
(drafting, summarizing, classifying) with optional JSON Schema validation.
Designed to be called from workflow engines (for example, Lobster via
openclaw.invoke --each) without adding new OpenClaw code per workflow.
Enable
- Enable the plugin:
{
"plugins": {
"entries": {
"llm-task": { "enabled": true }
}
}
}
- Allowlist the tool (it is registered with
optional: true):
{
"agents": {
"list": [
{
"id": "main",
"tools": { "allow": ["llm-task"] }
}
]
}
}
Config (optional)
{
"plugins": {
"entries": {
"llm-task": {
"enabled": true,
"llm": {
"allowModelOverride": true,
"allowedCompletionModels": ["openai/gpt-5.6-sol"],
"allowAuthProfileOverride": true
},
"config": {
"defaultProvider": "openai",
"defaultModel": "gpt-5.6-sol",
"defaultAuthProfileId": "main",
"maxTokens": 800,
"timeoutMs": 30000
}
}
}
}
}
The host-owned llm policy authorizes model/profile overrides. Its
allowedCompletionModels restricts every completion, including the resolved agent
default. Run openclaw doctor --fix once for entries created by older releases;
Doctor grants the shipped selection permissions and moves legacy
config.allowedModels values into llm.allowedCompletionModels without widening them.
Tool API
Parameters
prompt(string, required)input(any, optional)schema(object, optional JSON Schema)provider(string, optional)model(string, optional)thinking(string, optional)authProfileId(string, optional)temperature(number, optional)maxTokens(number, optional)timeoutMs(number, optional)
Output
Returns details.json containing the parsed JSON (and validates against
schema when provided).
Notes
- The tool is JSON-only and instructs the model to output only JSON (no code fences, no commentary).
- No tools are exposed to the model for this run.
- Side effects should be handled outside this tool (for example, approvals in Lobster) before calling tools that send messages/emails.
Bundled extension note
This extension depends on OpenClaw internal modules (the embedded agent runner).
It is intended to ship as a bundled OpenClaw extension (like lobster) and
be enabled via plugins.entries + tool allowlists.
It is not currently designed to be copied into
~/.openclaw/extensions as a standalone plugin directory.