* 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
* feat(channels): add identifier authentication kernel
* fix(channels): preserve exact identity provenance
* fix(channels): fail strict dynamic groups closed
* fix(channels): bind plugin allowlists to exact identity fields
Move the Twitch role normalizers into the kernel branch and keep Discord
name and tag entries on their corresponding identity fields. Repair the
same mismatch for Slack name slugs and IRC nick@host entries.
Add regressions at the channel authorization boundaries.
Related: #123782
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
* refactor(channels): resolve identifier authentication at intake
Keep raw SDK inputs optional and carry required authentication through
normalized kernel subjects, entries, and state. Resolve static claims at
intake and floor omitted fields in a supplied per-message map to unverified.
Remove the deprecated internal policy wrapper, downstream strength
fallbacks, and unused type exports. Cover the partial-map floor, static
claims, and mutable alias behavior, and document the map contract.
Related: #123782
* fix(channels): retain affected admission contributions
Preserve an authentication effect when other admission contributions were
not evaluated. Prefer any affected result, then any evaluated result,
before checking for uniformly unevaluated contributions.
Cover the mixed case through the existing redacted receipt flow.
Related: #123782
* test(twitch): await monitor delivery before resetting mocks
Make the ingress fixture drain accepted deliveries on stop, matching the
real monitor lifecycle. Await those deliveries directly so prefix and
reply assertions cannot race or leak completions into the next case.
Related: #123782
* fix(feishu): match ambiguous allow entries under both id fields
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(slack): record reasons for failed and omitted attachments
Keep failed and capped files visible in agent context with bounded, redacted reasons and unified unavailable attachment accounting. Log final failures once while preserving download limits, URL refresh retries, authentication, and HTML rejection.
* test(slack): align forwarded-image assertion with unified unavailable notice
* fix(agents): preserve replies after unavailable approvals
Keep setup notices durable without suppressing recovered answers or terminal errors. Preserve genuine pending-approval deduplication and align native approval setup guidance. Fixes#130584.
* fix(discord): restore named-account research skill discovery
Gate the Discord skill on channel configuration instead of a root token. Make clawtributor research portable across archive and native history readers, and apply requested time windows to conversation references rather than PR creation dates.
* fix(agents): order tool-result delivery before assistant events
* fix(slack): correct native approval enablement guidance
* test(qa): support context-aware model fixtures
* test(scripts): publish ready process IDs atomically
* feat(channels): introduce bots when they join group rooms
* feat(channels): add Discord and Telegram join introductions
* fix(channels): isolate untrusted evidence and select allowed join targets
* refactor(channels): scope joinIntro to implementing channels
* fix(channels): keep a delivered join introduction settled when its durable commit fails
* feat(channels): read more room history and document join introductions in detail
* chore(config): regenerate bundled channel metadata after rebase
Make ask_user use native Telegram controls when one single-select question can be represented directly, while keeping multi-select and batch questions on the text fallback. Add native Other-to-ForceReply input, preserve Gateway-owned option ordering across Telegram, Slack, and Discord, and prevent later progress from overwriting the delivered question.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Reuse the existing receipt aggregation owner for Slack action fanout and preserve every accepted message ID across media, presentation segments, and nested text chunks. Retain single-message shape, legacy scalar fields, thread placement, and question-card identity.
Fixes#130604
* fix(outbound): terminalize definitive channel rejections
* refactor(outbound): rethrow unclassified Slack rejections by identity
The Slack send boundary replaced every non-Error rejection with a synthetic
Error before classifying. That changed the propagated value at all four send
call sites, contradicting the stated contract that unlisted rejections keep
their previous path, and forced a compensating one-level `cause` walk in
`isSlackInvalidBlocksError` so the downstream `invalid_blocks` fallback could
still match. The guard's second operand was also dead: `isRecord` accepts Error
instances, so `!(err instanceof Error) || !isRecord(err)` never reached its
right side for a plain object.
Classify off the raw value and rethrow unclassified rejections by identity;
the `cause`-walking compensator and its test go away with it. Distill the
Telegram migration classifier's three-state result object into a
message-or-nothing, and document the description-first and 52-bit id contracts
inline.
Production surface for the PR drops from +71/-11 to +62/-8.
* test(agents): remove clock-tick race from workspace bootstrap ctime coverage
The in-place-edit case added in #127769 assumes restoring mtime leaves ctime as
the only changed stat field, and that it therefore differs. Linux and macOS
stamp ctime from a coarse per-tick clock, so an edit landing in the same tick as
the cached stat leaves ctimeMs equal: the ctime-only scenario never occurs, the
cache correctly serves its entry, and the assertion fails. Measured 199/200
identical ctimes in a tight loop; the test failed 1 in 8 local runs and broke
checks-node-compact-large-14 on CI.
Re-touch until the kernel advances ctimeMs, then assert mtimeMs and size are
unchanged so the scenario is provably ctime-only. Stripping ctimeMs from the
cache identity still fails the test.
* fix(slack): keep post-dispatch upload completion rejections ambiguous
PlatformMessageNotDispatchedError is a provider assertion that no
recipient-visible send began, and its contract says never use it after an
ambiguous send. files.completeUploadExternal runs after onPlatformSendDispatch
and is the one-time share operation, so a rejection there cannot prove the file
was never shared however definitive its code reads.
Drop the permanent-rejection classifier from that call and keep it on the
pre-dispatch calls only (chat.postMessage, files.getUploadURLExternal,
resolveChannelId). The upload test that pinned the old behavior asserted
onPlatformSendDispatch had already fired, which is exactly the condition that
forbids the claim; it now pins ambiguity instead.
Also widens the workspace bootstrap ctime wait to a 1s deadline and reshapes it
as a while loop, so a coarser filesystem tick cannot exhaust the bound.
* test(slack): prove permanent rejection recovery
* fix(test): stabilize Slack channel action routing
* fix(test): retain channel parity for precise targets
* refactor(outbound): drop unrelated test-routing changes
* fix(telegram): require Bot API error code for migration rejection
* test: repair Telegram tuples and preserve Slack test routing
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(approvals): typed approval scope summaries on channel cards
Approval owners can attach a closed ApprovalScope union (message-send,
payment, external-post) describing an action's blast radius. The gateway
sanitizes it once at the producer boundary, the core view model renders a
Scope metadata row so Slack/Discord/Google Chat cards show it unchanged,
shared text builders cover Telegram/WhatsApp/Signal/iMessage/Matrix, and
the durable presentation carries it additively for operator surfaces.
Scope is display-only, never authorization; missing scope keeps today's
cards.
* fix(approvals): emit native ApprovalScope union and clamp recipient previews
Name the three scope variants as registered protocol schemas so the Swift
generator emits the ApprovalScope discriminated union the presentation
structs reference, and commit the regenerated GatewayModels.swift. Clamp
recipient previews to the declared recipientCount at the sanitize boundary
so a count of 1 with 2 previews can no longer render inconsistently.
Addresses both ClawSweeper findings on #130116.
* refactor(approvals): extract text sanitizer to break the exec-approvals import cycle
check:architecture flagged approval-scope joining the exec-approvals SCC
through exec-approval-command-display. Move the self-contained display
sanitizer into a leaf module (exec-approval-text-sanitize) with no
exec-approvals imports and migrate all sanitize importers; command-display
keeps only the payload-typed command/preview resolver.
* chore(plugin-sdk): ratchet public surface budgets down after sanitizer extraction
The approval display sanitizers left the publicly reachable SDK graph when
they moved to the exec-approval-text-sanitize leaf: exports 4343 -> 4338,
callable exports 2582 -> 2578. Shrink-only budget pin.
* chore(control-ui): vendor Space Grotesk and Lora webfont subsets
Adds the woff2 faces the Absolutely theme paints in, plus the stylesheet that
declares them. Both families are SIL Open Font License 1.1 and ship unmodified,
with their upstream license text beside the files as the license requires.
Latin and latin-ext subsets only, 184 KB total. Serving these from the gateway
rather than a font CDN keeps font-src 'self' intact, avoids a third-party
request on every load, and works on an offline or LAN-only gateway.
The stylesheet is a static asset rather than a bundled import: it is linked at
runtime only while a theme that declares faces is active, so it stays out of
the startup CSS budget and out of every other theme's load.
* feat(control-ui): add the Absolutely theme with self-hosted typography
Adds a fourth built-in Control UI theme: terracotta clay on warm graphite in
dark mode, burnt clay on ivory in light mode, with its own typography — Space
Grotesk for chrome and Lora for chat prose.
Typography is theme-owned rather than global. A new --font-chat token defaults
to --font-body, so every other theme is byte-for-byte unchanged, and the face
declarations live in a static stylesheet that the theme applier links only
while a theme that declares fonts is active. Themes without declared faces
issue no font request at all.
The faces themselves are vendored in the preceding commit.
Also fixes two defects found while building it:
- Theme cards overflowed their border on long labels: the grid's 1fr label
track floors at min-content, so a long name pushed the trailing column past
the card edge. The track is now minmax(0, 1fr) with an ellipsis, which also
covers imported tweakcn labels. The selected-state checkmark that made the
overflow visible is gone; the accent border, tint, and ring already carry
selection, and aria-pressed owns the semantics.
- index.html's first-paint bootstrap only knew claw/knot/dash, so any new theme
flashed the default palette before the app corrected it. It now resolves the
new theme and has a matching pre-paint background, with mount-fallback
coverage for both modes.
Startup JS grows 198 B for the theme wiring, appearance option, locale
strings, and font loader, which fits the existing ratchet tolerance, so no
budget baseline moves. Startup CSS stays within its 45 KiB ceiling, which is
why the faces are a linked static asset rather than a bundled import.
* style(control-ui): format the vendored font stylesheet
The generated @font-face block kept its upstream single-line unicode-range
declarations, which oxfmt wraps. Repo-wide `pnpm format:check` is clean.
* test(control-ui): type the chat runId in the typography e2e
Reuses the chat-flow requireRecord/requireString helpers instead of asserting
the params shape, so runId is a real string rather than an unknown coerced by
String() (oxlint no-base-to-string).
* fix(control-ui): resolve theme fonts against the configured UI mount
The theme font stylesheet and its faces used root-absolute URLs, so a gateway
served under a configured Control UI base path (gateway.controlUi.basePath,
for example /openclaw) fetched them from the wrong origin path. Both requests
404ed there and Absolutely silently fell back to system fonts while its palette
still applied, which reads as "the theme looks a bit off" rather than as a
broken asset.
The link href now goes through inferControlUiPublicAssetPath, the same helper
plugin art and provider icons already use, and the stylesheet's own url()
references are relative to it, so both levels follow the mount without the
loader knowing the base path. Adds an e2e case under /openclaw asserting the
resolved href and that the browser fetches below the mount; it fails on the
previous code with the root-absolute path.
Reported by ClawSweeper review on #129885.
* test(slack): stop racing the aborted read request in the Web API bound test
`bounds dedicated reads without timing out shared clients` asserted the mock
server received exactly two requests, but the read client aborts at 20ms and
the server records on arrival, so whether the aborted request lands first is a
race. It failed on a loaded CI runner with 1 received.
The bound is still asserted directly (read rejects, shared resolves); the count
now only requires the shared client's arrival, and checks its token so the
dedicated client is still proven not to have carried the call.
Unrelated to this PR's theme work; fixed here rather than left red.
* feat(channels): custom emoji discovery via emoji-list across Discord, Slack, Telegram
Make custom emojis discoverable by the agent. The message tool's emoji
param now documents custom-emoji syntax per channel (gate-aware, only
naming emoji-list when the action is actually advertised). Discord
emoji-list defaults guildId from the current conversation and returns
reaction-ready { name, identifier, animated? } entries; Slack returns
normalized shortcodes with aliasOf. Telegram gains emoji-list backed by
one canonical allowed-reactions owner (getChat available_reactions,
custom_emoji entries preserved), numeric custom-emoji reactions, and
replaces the dead 'reaction disallow list' error advice with a bounded
sample of the chat's allowed reactions.
* test(channels): expect telegram emoji-list provider-owned read gate in plugin shape contract
* test(telegram): prove emoji-list authority chain via mock-gateway e2e
Ephemeral gateway + mock Bot API + mock OpenAI provider: current-chat
emoji-list returns normalized standard and custom_emoji identifiers with
exactly one getChat call; a delegated cross-chat request is rejected with
the conversation-binding error and zero Bot API requests reference the
foreign chat.
Centralize untrusted audio transcript framing across channel plugins and keep Discord voice transcripts out of typed command provenance.
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>