* feat(security): warn on inert mutable allowFrom entries
Report configured entries that cannot authorize with name matching disabled,
and preview lockout counts before disabling mutable matching. Distinguish
mutable aliases from exact identifiers with unproven ownership in audit text.
Reuse channel ingress declarations through one SDK classifier without changing
admission policy. Keep Discord audit findings redacted and exclude symbolic
access-group references from mutable-identifier warnings.
Refs #131099.
* test(security): keep entry-authentication audit coverage in its lanes
* fix(mattermost): anchor bot mention matching on username boundaries
The wake check used a bare substring test and the mention stripper used
\b word boundaries, but Mattermost usernames match ^[a-z0-9.\-_]+$
(server model/user.go), where "." and "-" are username characters. A
mention of a different user whose handle starts with the bot name
("@clawdia", "@claw.dia", "@claw-ops") or an embedded handle
("bob@claw") woke the bot and answered a message addressed to someone
else, and stripping ate the bot-name prefix out of the other user's
handle, delivering a mangled body to the model.
Anchor both sides of the mention pattern on the username character
class, and stop rewriting lines that contain no mention at all: the
whitespace collapse ran on every line, destroying code-block and table
alignment in messages that merely accompanied a mention.
Detection and stripping share one anchored pattern helper; the wake
decision now uses it instead of the lowercase substring check.
* fix(mattermost): align bot mentions with server punctuation rules
Share one upstream-accurate mention pattern across activation and normalization while preserving local, federated, and punctuation-adjacent behavior at the real posted-event boundary.
Co-authored-by: goffern <goffern@users.noreply.github.com>
---------
Co-authored-by: goffern <goffern@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(mattermost): keep server file name when the download fails
#129140 propagates the attachment file name into the model context, but
only on the successful-download path. The failed-download fallback
already calls /files/{id}/info and reads mime_type while discarding the
name the same response carries — and on that branch the name is the only
context the model gets about the attachment, since there is no local
file. Read it and forward it like the success path does; blank names
stay omitted.
* fix(mattermost): surface unavailable attachment filenames to agents
Preserve server-provided attachment names through the Mattermost-owned unavailable-media notice, safely bound model-visible metadata, and cover the actual posted-event dispatch boundary.
Co-authored-by: goffern <goffern@users.noreply.github.com>
---------
Co-authored-by: goffern <goffern@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The monitor reply path sent local (non-http) media without
requireMediaUpload, so a failed upload silently fell back to posting the
caption alone while still reporting a successful media send. The file
vanished with no visible error to the user.
The message-tool send paths in channel.ts already require upload for
non-http media; this aligns the reply path with that policy. Extract the
shared requiresMattermostMediaUpload predicate into the lightweight
normalize module (removing the duplicate in channel.ts) and apply it on
the reply path so an upload failure surfaces instead of dropping the
attachment.
Co-authored-by: goffern <goffern@users.noreply.github.com>
* fix(mattermost): react action rejects raw emoji glyphs
The react message action forwarded the caller's emoji verbatim (only
stripping wrapping colons), so a raw Unicode glyph such as a thumbs-up
character was sent as the `emoji_name`. Mattermost's reaction API accepts
only emoji short names, so the server rejects a raw glyph and the
reaction never appears.
Add a Mattermost-local normalizer that maps the common glyphs to their
short name (skin-tone modifiers and variation selectors stripped before
lookup) and leaves unknown emoji unchanged, mirroring the Slack plugin's
existing handling.
* test(mattermost): cover glyph normalization through the react action boundary
The normalizer previously had only direct helper coverage. Drive a raw
thumbs-up glyph through handleAction for both add and remove so the
boundary that serializes emoji_name is exercised: on pre-fix code the
add request body would carry the raw glyph and the remove URL would
embed it, failing the existing thumbsup request expectations.
* fix(mattermost): preserve inherited custom emoji names
* fix(mattermost): preserve lint-safe emoji decoration ranges
* fix(mattermost): preserve skin tones when normalizing glyph reactions
Stripping Fitzpatrick modifiers before lookup silently turned a toned
reaction such as a medium-tone thumbs-up into the untoned 👍.
Mattermost names toned system emoji `<base>_<tone>_skin_tone` (verified
against SystemEmojis for all five modifier-capable mapped bases), so
compose that name when the base glyph is mapped and tone-capable,
mirroring the Slack plugin's modifier handling. Stray modifiers on
non-modifier bases still resolve to the base name, and unknown glyphs
still pass through unchanged.
---------
Co-authored-by: goffern <goffern@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(channels): validate headless channel setup
* docs(channels): document headless provisioning
* fix(channels): repair setup metadata typing
* chore(channels): regenerate official channel catalog for env metadata
* fix(slack): keep mode-conditional env contract plugin-owned
Static --use-env declaration keeps only the unconditional SLACK_BOT_TOKEN;
socket-vs-HTTP conditional requirements (app token, signing secret) stay in
Slack's own setup validation so HTTP mode no longer demands an irrelevant
SLACK_APP_TOKEN.
* chore(sdk): regenerate api baselines and catalog after rebase
* fix(slack): align manifest env declaration with runtime contract
* chore(sdk): regenerate api baselines after rebase
* chore(sdk): regenerate api baselines after rebase
* chore(sdk): regenerate api baselines after rebase
* 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>
Keep raw commands, paths, and provider errors out of ordinary chat while preserving explicit raw diagnostics and structured admin history.
Default command progress is status-only; `/verbose full` and `commandText: "raw"` retain diagnostic detail.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* 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.
* 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.