* refactor: consolidate coercion ownership
Centralize four canonical coercion helpers, migrate exact core and plugin duplicates through narrow Plugin SDK facades, and enforce declaration and plugin-normalization ownership boundaries.
The sweep adds eight focused SDK exports while deleting more production and tooling code than it adds. User-visible behavior is unchanged except for safer equivalent object and UI parsing at existing boundaries.
* fix: guard integer option ownership
Register resolveIntegerOption with the canonical function owner and extend the declaration-guard fixture so future local duplicates fail validation.
* fix: keep integer helpers on numeric facade
Remove the unshipped duplicate string-coerce exports and route every affected plugin consumer through the existing number-runtime contract.
* fix: point numeric coercion to number runtime
Make boundary and declaration diagnostics recommend the canonical numeric facade, with failing-before coverage for both guidance paths.
Unset heartbeat.target now resolves "owner": elected heartbeat notifications deliver to the operator's DM resolved from commands.ownerAllowFrom or the channel allowFrom (first concrete entry; wildcards and channel-scoped wildcards excluded; configured owners exhausted across channels before any channel-local fallback). Delivery requires the channel's own classifier to positively prove a direct destination — every bundled messaging plugin now ships an inferTargetChatType contract — and unproven or group-shaped destinations fail closed to the visible no-route state. The first implicitly-routed delivery carries a one-line self-explanation naming the target: "none" opt-out. Explicit target "last" remains as the follow-the-conversation opt-in. Refines the unreleased #121892 default before it ships; refs #121880.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The raw-command-detail streaming card test shipped in #121826 asserting
raw exec text under the default commandText mode, but the same commit
made "status" (tool label only) the documented default with raw as an
explicit opt-in. The test has failed since introduction; change-classified
CI never selected the feishu lane on main pushes. Opt the test into
streaming.progress.commandText: "raw" so it proves the documented path.
* perf(doctor): keep bundled doctor contract closures dependency-light
Doctor contract enumeration cold-loads each plugin's doctor-contract-api
closure via jiti, so a static value import of openclaw/plugin-sdk/runtime-doctor
pulled the state-db/kysely graph (~4.3s per closure) into
listPluginDoctorLegacyConfigRules / listPluginDoctorStateMigrationEntries.
- migrate all light doctor-contract closures (66 files) to the
dependency-light openclaw/plugin-sdk/runtime-doctor-migrations subpath
- voice-call: load detect/repairOpenClawStateDatabaseSchema* lazily inside
the migration bodies; keep only a type-only static runtime-doctor import
- matrix: split pure credential record shapes/normalizers into
credentials-state.ts so the doctor closure no longer imports the sync
plugin-state store through credentials-read
- guard: doctor-contract-closure-guard.test.ts now forbids static value
imports of runtime-doctor in closures alongside agent-runtime
* fix(matrix): keep credential revocation record type module-local
Knip production scan flags the export as consumer-less; the type is only
referenced by the exported union and revocation guard signature.
* perf(plugins): declare doctor contract surfaces
* perf(doctor): slim migration import closures
* perf(plugins): narrow doctor declaration record surface and wire owner-test lane
Registry records carry only the doctorContract declaration instead of the whole
parsed manifest, and check:changed now selects the src/plugins-owned declaration
honesty and closure-guard tests for extension module/manifest changes so
cross-lane drift cannot pass PR classification.
* fix(doctor): keep control-plane dist imports require-safe
Keep doctor and channel control-plane chunks off exec-class dependencies, and enforce native require(esm) loading during postbuild.
* chore(plugin-sdk): regenerate API baseline
* chore(plugin-sdk): sync export ordering
* fix(plugins): satisfy doctor contract CI boundaries
* perf(doctor): make qqbot doctor closure dependency-light
qqbot was the last plugin above 5s in doctor state-migration enumeration
(~8s under tsx/jiti). The cost was not the state-key builder (already a
leaf): its doctor closure value-imported the runtime-doctor SDK barrel,
whose plugin-state-store/state-db re-exports pull kysely (~330 modules),
plus security-runtime for one fileExists (~200 modules), all resolved
per-module by jiti during enumeration.
Split the migration-define helpers and light re-exports into a new
private-local plugin-sdk/runtime-doctor-migrations subpath; runtime-doctor
re-exports it so its public surface is byte-identical (API baseline hash
unchanged). qqbot's doctor-contract and state-migrations now import only
the light subpath, swapping fileExists for the equivalent async
legacyStateFileExists already in the closure.
qqbot enumeration: ~8.0s/531 modules -> ~0.25s/18 modules.
* chore(plugin-sdk): drop private-local subpath from API baseline
runtime-doctor-migrations is private-local-only; the baseline tracks public
modules, and the earlier line was generated before the classification.
* fix(plugins): register runtime-doctor-migrations boundary paths
The private-local subpath list feeds the extension package boundary map;
the shared paths config and xai's derived overrides must carry the same
entry or the boundary contract test fails.
* fix(feishu): drain all message reaction pages
* fix(feishu): preserve reaction actor and topic ownership
* fix(feishu): support all valid reaction pages
---------
Co-authored-by: Peter Steinberger <steipete@macos.shared>
* fix(feishu): guard Object.assign against prototype pollution in webhook payload merge
Object.assign merges JSON.parse webhook payload into an Object.create target
without filtering `__proto__`, `constructor`, or `prototype` keys. A
crafted webhook body like `{"__proto__": {"polluted": true}}` would set
the `__proto__` property on the envelope object, enabling prototype
pollution on the target.
Replace Object.assign with explicit iteration that skips blocked keys, and
use Object.create(null) so the envelope itself has no prototype to pollute.
Matches the isBlockedObjectKey pattern used in core config paths.
* fix(feishu): preserve webhook envelope when filtering payloads
* fix(feishu): preserve webhook envelope when filtering payloads
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(feishu): log message content JSON parse failures instead of silently swallowing
Replace formatErrorMessage(err) with safe metadata-only logging in
parseFeishuMessageContent to prevent potential message content leaks
through V8 JSON.parse error messages.
Changes:
- Remove formatErrorMessage import (security: V8 JSON.parse errors can
include input content in the message)
- Log only msgType and optional messageId (safe metadata) when parse
fails, never the exception message or raw content
- Add assertion that raw content is NOT present in the log output
- Pass messageId through to enable richer diagnostics
The raw content is still preserved as the function return value (existing
fallback behavior).
* fix(feishu): move parse-failure test into getMessageFeishu suite
The test 'logs a safe diagnostic (not raw content) when message content
is not valid JSON' was declared after the closing brace of
describe('getMessageFeishu'), so it did not inherit that suite's fixture
setup and reset hooks (beforeEach/afterAll). Move it inside the suite
so it benefits from the shared mock reset and cleanup.
Fixes ClawSweeper P2: 'Keep the parse-failure test inside the fetch suite'
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>