* 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.
* refactor(qqbot): remove bundled extension source
Mechanical deletion half of the #107295 squashed rebase; the catalog
repoint and host integration land in the follow-up commit.
Co-authored-by: sliverp <870080352@qq.com>
* refactor(qqbot): install plugin from Tencent package
Squashed rebase of #107295 onto current main. Repoints the official
external channel catalog at @tencent-connect/openclaw-qqbot@2.0.1 and
adapts onboarding, doctor migrations, secrets, build guards, and tests.
Documents the known limitation that the external package does not
support structured SecretRef clientSecret values; operators move those
to QQBOT_CLIENT_SECRET or clientSecretFile before upgrading.
Co-authored-by: sliverp <870080352@qq.com>
* fix(doctor): reuse shared hasOwnKey record helper
The rebased QQBot migration carried its own hasOwnKey export, colliding
with the one main now ships in legacy-config-record-shared.ts.
Co-authored-by: sliverp <870080352@qq.com>
* fix(plugins): carry catalog integrity through the update bridge
The externalized-bundled-plugin bridge dropped the official catalog's
expectedIntegrity pin, so bundled-user updates installed the external
npm package without integrity verification. The bridge now carries the
pin for the catalog's exact npm spec and both bridge install calls pass
it through; update-channel spec overrides intentionally skip the pin
since it only covers the pinned version.
Co-authored-by: sliverp <870080352@qq.com>
* chore(plugin-sdk): refresh per-entrypoint API baselines
The QQBot compat export and bundled-type removal shift 26 entrypoint
closure hashes in the new split baseline layout.
Co-authored-by: sliverp <870080352@qq.com>
* refactor(qqbot): drop helper reintroduced during rebase
Main's coercion consolidation added this file after the deletion
commit's base; its only consumers were the removed qqbot sources.
Co-authored-by: sliverp <870080352@qq.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): stop advertising trigger-gated fields when cron triggers are disabled
When cron.triggers.enabled is off (the documented default posture), the
cron tool still advertised job.trigger/patch.trigger, schedule.kind
"stream" with its stream-only fields, and payload.kind "script" — all of
which the scheduler unconditionally rejects. Schema-eager models attach
junk triggers to plain reminder requests, get rejected, and retry-loop
without ever creating the job.
Gate those surfaces on the resolved config so they are only advertised
when the runtime can actually accept them, and swap the trigger guidance
for a short unavailable notice so the model tells the user instead of
polling or silently degrading the request. Config-less construction
paths keep the full surface.
Related: #119455
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cron): restore config type import and regenerate schema contracts
Repairs rebase fallout and CI-contract consequences of the trigger gating:
- Re-import OpenClawConfig from config/types.openclaw.js; the rebase crossed
#121768 which removed the re-export from config/config.js, stranding
resolveCronTriggersEnabled's signature.
- Guard the destructured tool in the MCP gating test for
noUncheckedIndexedAccess.
- Collapse the hookContext merge object to one line: the added config
pass-through tipped openclaw-tools.ts over the max-lines ratchet (701/700).
- Regenerate prompt snapshots (cron description narrowed under
triggers-disabled harness config; ~3k chars per prompt) and the plugin-sdk
API baseline (CronToolOptions gained the optional config member, visible
via codex-mcp-projection).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(cron): align schema contracts with current main
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
Per-requester OAuth for HTTP MCP servers: in shared channels each trusted sender connects their own account. New config: mcp.servers.<name>.oauth.identity ("shared" default, byte-identical behavior) and gateway.publicOrigin (HTTPS except loopback) for the new GET /oauth/mcp/callback served on the Gateway HTTP server. Requester tokens are isolated per (channel, account, sender) in mcp_oauth_stores rows (no schema bump); callbacks correlate through a durable state-keyed one-time index with a 10-minute TTL; per-requester servers are fail-closed out of static/scheduled runtimes; unauthenticated senders get a sign-in link with a portable URL button; MCP config mutations clear requester credentials at the canonical owner. mcp status --json keeps the legacy authStatus fields additively. Proven by a two-user self-hosted-Executor E2E through the real gateway callback. Part of #122034.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(ui): align effort picker after model switches
Publish ordered per-model effort profiles from the Gateway and reconcile new-session effort overrides so labels, slider positions, and create payloads stay consistent.
* fix(ci): align model profile contracts
* feat(ui): show session-reset dividers and fix boundary markers on DB-backed sessions
/reset now leaves a durable 'Session reset' divider at the transcript
boundary in the Control UI. Root-cause fix underneath: the SQLite
transcript projection only selected message events, so compaction (and
now reset) markers never reached clients for DB-backed sessions; marker
synthesis now has one owner (session-transcript-message.ts) consumed by
both storage backends across full/recent/paged/by-id/anchor reads.
Additive __openclaw marker kind 'reset' documented in clients.md.
* fix(gateway): keep history readers out of the plugin SDK barrel and fix CI gates
Direct imports for the sqlite history readers (the session-accessor barrel
is SDK-reachable via session-transcript-lock-runtime); reset marker added
to the kept-tail chat.history expectation; lint naming fixes; marker tests
split into session-transcript-readers.markers.test.ts.
Attachment MIME gating and extraction dispatch used nine separate byte policies across five files; the allowlist could approve one MIME while dispatch observed another after a second sniff. @openclaw/media-core/attachment-classify now owns byte sniffing, text heuristics, extension mapping, and the seven attachment classes; the cache stores one classification that gates and dispatch share. Channel-declared MIME leads with the transport Content-Type as a byte-arbitrated hint, normalizeMimeType folds YAML/XML synonyms so existing configured allowlists keep matching, and input_file API callers keep declared-MIME precedence over cosmetic filenames. Net -135 production LOC.
Proof: 2,153 focused tests across media-core/media-understanding/media plus boundary-contract and deadcode-mirror suites; ClawSweeper local review clean (round 5, zero findings); live Telegram default-turn probe green.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Inbound image, audio, and video attachments could vanish with no recorded outcome and no model-visible explanation: attachments dropped by first-only selection, disabled or unconfigured capabilities, stage failures, URL-only images, and ACP-undelivered images all left the model unaware the media existed. Fixes#122044; completes the outcome custody started for documents in #122025.
Media capabilities now record one closed disposition per pre-truncation candidate at the decision site that owns the reason, with a memoized lazy native-vision probe (fires only when a marker could render; failure yields unknown and never alters outcomes; explicit image models never pay a catalog lookup). One late reader renders bounded, actionable markers from recorded facts only: native-vision and ACP-delivered images suppress per verified index, recorded failures always render, and document plus media markers share one five-marker budget with a reason-neutral overflow summary. The shipped SDK decision type stays additive (optional dispositions map).
Proof: 906 + 103 focused tests including nine adversarial-review regressions, ClawSweeper local review clean (round 9, zero findings, security cleared), live Telegram image drive showing ref preservation with no false marker.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(ui): hide unusable models from picker
* refactor(ui): remove stale model availability helper
* refactor(ui): simplify catalog state guards
* style: format provider catalog imports
* chore: refresh plugin SDK API baseline
* refactor(core): break provider catalog type cycle
* chore(protocol): refresh models list Swift output
* chore: refresh plugin SDK API baseline after rebase
* fix(gateway): preserve full catalog preload semantics
* fix(ui): keep model status within startup budget
* fix(ui): preserve provider status within startup budget
* fix(models): scope live catalog outcomes
* test(ui): expect agent-scoped model refresh
* test(ui): align model refresh e2e fixtures
Schema versions are global v6 and agent v17 (src/state/openclaw-state-db-contract.ts:6; src/state/openclaw-agent-db-contract.ts:22). State v5/v6 provenance comes from 7a7d6bb and 509a5f0, matching the current contract comments at src/state/openclaw-state-db-contract.ts:4.
QMD is retired and Doctor migrates its paths to builtin memory (src/commands/doctor/shared/legacy-config-migrations.runtime.retired-memory-qmd.ts:94). Device auth is SQLite-backed in TypeScript and Swift while Android remains SecurePrefs-backed (src/infra/device-auth-store.ts:115; apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthStore.swift:26; apps/android/app/src/main/java/ai/openclaw/app/gateway/DeviceAuthStore.kt:55).
Opt-in cache, payload, raw-stream, timeline, and macOS diagnostics remain JSONL artifacts (src/agents/cache-trace.ts:93; src/agents/anthropic-payload-log.ts:46; src/agents/embedded-agent-subscribe.raw-stream.ts:16; src/infra/diagnostics-timeline.ts:96; apps/macos/Sources/OpenClaw/DiagnosticsFileLog.swift:3).
The canonical agent schema currently has cache_entries but no VFS/tool/run artifact tables, and subagent attachments materialize in child workspaces (src/state/openclaw-agent-schema.sql:398; src/agents/subagents/spawn/subagent-attachments.ts:308). Current Doctor/session owners replace deleted prototype paths (src/infra/state-migrations.doctor.ts:66; src/config/sessions/session-accessor.sqlite-entry.ts:165; src/config/sessions/paths.ts:320).
Close the barrel gap (SessionEntryListScope), migrate the 14 production
type-bypass imports and the sessions-rewind raw-op imports through the
accessor barrel, and document session-accessor.sqlite-scope as the
sanctioned low-level entry point for doctor/migrations/infra. Net +8
production LOC: the barrel gains its missing surface. Part 3 (final) of
the session-accessor dual-layer collapse (#121316, #121536).
Inbound document attachments could vanish silently (DOCX and other Office files, read failures, disabled URL sources). File intake now classifies every attachment into a closed outcome union rendered through one exhaustiveness-checked path: unsupported formats, policy-rejected types, unreadable files, and disabled URL sources all produce bounded model-visible markers (max five per message plus one overflow summary), with strict MIME-token and URL-basename sanitization so attachment metadata cannot inject prompt text or leak signed-URL credentials. Media-stage outcome custody is tracked separately in #122044.
Proof: focused media suites (1855 tests), pre-fix regression, exact-head ci-gate green, ClawSweeper local review clean, and a live Telegram E2E showing the DOCX marker in the model payload.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
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>