Commit Graph

1227 Commits

Author SHA1 Message Date
Peter Steinberger c340bc09f0 feat(buzz): support flat automatic replies and typing
Expose opt-in replyToMode off while retaining threaded replies by default. Keep incoming thread/session identity and explicit tool or CLI targets intact. Resolves #120339. Thanks to @Alfridus1 for the report.
2026-08-27 14:53:18 -07:00
Peter Steinberger 7b000dba9d feat(reef): operator-configurable sharing rules for the guard (#131167)
* feat(reef): operator-configurable sharing rules for the guard

Adds channels.reef.guard.rules with capped outbound/inbound free-text
policy. Rules ride the trusted instruction side of the guard call only,
may tighten decisions or explicitly allow otherwise-review cases, and can
never override the deny floor or deterministic checks. The rules text is
hashed into the effective policy version so audit rows and pending review
approvals bind to the exact policy in force.

* fix(reef): full rules digest in policy identity; align blank-rule validation

ClawSweeper review fixes: use the untruncated sha256 rules digest in the
effective policy version so approvalDigest stays collision-resistant, and
express identical non-blank (\S) rule validation in the zod schema, the
manifest JSON Schemas, and the generated channel metadata instead of a
trim-transform mismatch.
2026-08-27 14:37:17 -07:00
Peter Steinberger 3424633360 fix(twitch): retain native sender identity in execution audits (#130730)
* fix(twitch): retain native sender identity in execution audits

Forward the registered channel runtime and actual account identity, keeping native participants distinct from role authorization. Normalize the all role at the Twitch policy producer and preserve unknown and disabled-collection paths.

Cover actual registered-plugin ingress and native reply delivery, and drain asynchronous monitor test work before cleanup. Related to #122863; no shared authorization or storage change.

* test(twitch): track the registered context builder in inventory

* test(twitch): honor host and plugin contract boundaries
2026-08-27 13:45:33 -07:00
Peter Steinberger cad34e612f feat(channels): add Matrix and LINE join introductions (#131128)
Introduce on admitted bot joins using the existing sealed no-tools turn and durable room dedupe. Matrix uses room metadata and readable history; LINE uses group names and explicitly unavailable history.

Mark Matrix bridge membership provenance without changing existing event delivery, invite handling, or auto-join. Exclude startup snapshots and already-joined profile updates, including startup connection recovery.

Simplify snapshot budget accounting while preserving prompt bytes, metadata order, and oldest-first message dropping. Add channel config, docs, and regression coverage.
2026-08-27 12:52:55 -07:00
Peter Steinberger 5e924a574c fix(feishu): split oversized Markdown cards without losing content or receipts (#131106) 2026-08-27 11:34:50 -07:00
gaoanze888 47882f48cb fix(discord): preserve stringified components (#128498)
Preserve documented Discord Components V2 when MCP clients serialize component objects, and keep cross-channel schema discovery account-neutral.

Co-authored-by: gaoanze888 <214786078+gaoanze888@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-27 16:02:33 +05:30
Peter Steinberger fdf3cd69ce fix(googlechat): surface unsupported and unprocessed attachments (#130870) 2026-08-27 03:31:49 -07:00
Peter Steinberger 46ca7d1f27 fix(slack): record reasons for failed and omitted attachments (#130857)
* 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
2026-08-27 02:46:05 -07:00
Peter Steinberger bdd8f33f5d fix(telegram): surface media failures in the agent-facing body (#130849) 2026-08-27 02:18:26 -07:00
Peter Steinberger 873e1c85fa fix: preserve channel answers after unavailable approvals (#130624)
* 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
2026-08-27 01:50:36 -07:00
Markus Hartung 8c293c1ae1 fix(matrix): show recovery key availability in text status (#128051)
Keep raw recovery keys in explicitly opted-in JSON output. Add a safe text
hint in the CLI-owned result renderer, remove duplicated stored-key output,
and cover normal, verbose, missing-key and JSON command behavior.

Thanks @hartmark for the original fix and real-account reproduction.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 21:38:32 -07:00
Peter Steinberger 63f7df85bb feat(channels): post a grounded introduction when the bot joins a group room (#130103)
* 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
2026-08-26 21:02:26 -07:00
Ayaan Zaidi 1cb914d6bf fix(telegram): make ask_user controls native and reliable (#130262)
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>
2026-08-27 08:14:39 +05:30
Peter Steinberger 7f05472938 feat(buzz): retain bounded passive context for accepted turns (#130509)
* feat(buzz): retain bounded passive context for accepted turns

Add opt-in connection-owned room and thread context using the shared history window. Preserve sender admission, current roster filtering, UTF-8 bounds, and snapshot consumption without passive inference, session recording, or typing. Related: #129599.

* docs(config): align baseline with landed Feishu sticker sets
2026-08-26 19:36:05 -07:00
Peter Steinberger de70b00d95 feat(buzz): bound bot-to-bot room conversations (#130488)
Use the shared per-Gateway bot-pair budget with the latest received signed room roles. Preserve existing sender and mention admission and human traffic. Release note: bound repeated Buzz bot exchanges without adding a channel-specific policy or persistent state.
2026-08-26 17:58:41 -07:00
Peter Steinberger a0d6467948 feat(feishu): find received stickers by configured keywords (#130498)
* feat(feishu): search configured received stickers by keyword

Bind curated sticker sets to the selected bot app ID and reuse the existing sticker action gate and native sender. Enforce matching scalar bounds in runtime and generated config schemas, with bounded search output and explicit truncation. Completes the configurable-set request in #114578.

* refactor(feishu): keep sticker schemas with config owner

* build(feishu): regenerate catalog metadata after channel schema update
2026-08-26 17:37:05 -07:00
Ayaan Gazali 1f2e99832b fix(channels): bundled channels reject the documented responsePrefix override (#118148)
* fix(channels): bundled channels reject the documented responsePrefix override

* fix(feishu): pass channel and account to the reply prefix resolver

The ordinary reply dispatcher built the prefix context without channel or
accountId, so channels.feishu.responsePrefix and its account override fell
through to the global value. The comment dispatcher already passed them.

* fix(channels): accept twitch's root responsePrefix and type ClickClack's

Twitch validates as a two-branch union with both branches closed, so
responsePrefix declared only on the account shape left
channels.twitch.responsePrefix rejected whenever accounts is present. Also
declares responsePrefix on ClickClackAccountConfig so typed callers can express
what its schema already accepts.

* fix(clickclack): apply the configured responsePrefix in model mode

Model mode sends the completion straight to ClickClack instead of going through
the agent reply pipeline, so the documented responsePrefix parsed and then did
nothing. Resolve it through the public reply-pipeline seam so template
variables like {model} render from the completion's own provider and model.

* fix(clickclack): do not double the response prefix in model mode

systemPrompt is operator-owned, so a model can be instructed to emit the
configured prefix itself and the unconditional concatenation then sent it
twice. Match the shared reply normalizer's startsWith guard.

* test(clickclack): resolve inbound access through the real resolver

Main now requires access.channelIngress, which the hand-built fixture did not
provide, so the model-mode messages never dispatched. Drop the fixture and let
handleClickClackInbound resolve access from a DM-shaped message, matching the
sibling bot-loop tests.

* test(channels): reject a key missing from any composed branch

A union alternative is a configuration mode an operator can pick, so a key
present in only one mode is unusable in the other even though the union still
validates. Treat any rejecting alternative as a rejection, and walk allOf when
collecting account schemas.

* fix(channels): offer responsePrefix only where the reply path applies it

Applying the prefix to an outbound reply is per-channel wiring, not a shared
step. Buzz, Nostr, QA Channel, Raft, Reef and SMS never read it, so accepting
the key there validated a setting their delivery path ignores. Anchor the
contract to the channels that consume the prefix.

* fix(channels): complete shared response prefix delivery

Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>

* docs(config): refresh response prefix baseline

* test(tlon): use canonical prefix fixture helpers

* test(channels): prove Twitch reply prefix delivery

Exercise schema admission and the real shared reply dispatcher at the Twitch native send boundary, replacing redundant schema-only coverage. Keep Tlon fixtures on canonical imports and recording options.

* docs(channels): clarify explicit message prefix behavior

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 17:08:28 -07:00
Peter Steinberger e9dc4b9d1a fix: Buzz bots recover relay stalls and reject revoked senders (#130467)
* fix(buzz): recover stalled sessions and revalidate room admission

* test: correct Buzz relay fixture types

Keep release notes in the PR body and leave the release-owned changelog unchanged.

* test: isolate Buzz relay mock exports

* test: clarify Buzz admission and output lifetimes
2026-08-26 16:47:58 -07:00
Peter Steinberger 1f60d9eca2 feat(feishu): let agents resend received stickers (#130374)
* feat(feishu): resend received stickers without losing topic routing

Add opt-in agent sticker sending using received file keys, preserve inbound keys, and stop unsupported sticker resource downloads. Reuse keyed-media delivery and canonical reply selection; inherit source topics only within the same destination and account. Related: #114578.

* fix(feishu): preserve topic mode for prepared implicit replies

* docs(feishu): preserve account action gates when enabling stickers
2026-08-26 15:01:37 -07:00
Marvinthebored ce4a680544 feat(models): add configurable model selection scopes (#127813)
* feat: make model persistence scope explicit

* fix: keep unauthorized model directives as plain text

An unauthorized sender's inline directives are cleared to plain text before
they are acted on, but the persistent write target was derived from the
directives as they arrived. An unauthorized `/model <provider/model> -a` or
`-g` therefore reached the owner-authority error return instead of the
plain-text path every other directive takes.

Derive the target, the authority flag, and the persist flag after the
clearing. Authorized senders skip the clearing, so their behavior is
unchanged; the only closure that captures the target is invoked well after
the new declaration site.

Covers both persistent flags with a regression test that fails on the prior
head with "Agent and global model defaults require owner authority or
operator.admin scope." and passes here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(models): make model selection scope configurable

Preserve current model-selection defaults when the optional scope preference is unset. Add explicit session, agent, and global overrides without broadening config-write authority.

Co-authored-by: Marvinthebored <peter@lindsey.jp>

* test(models): simplify scope expectation fixtures

---------

Co-authored-by: Marvinthebored <peter@lindsey.jp>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 14:31:56 -07:00
Peter Steinberger 1448da315c fix(discord): prevent voice replies from cutting off during playback (#130346)
* fix(discord): prevent voice replies from cutting off during playback

Preserve realtime PCM through normal backpressure and release queued speech only after the Discord player finishes. Tolerate brief provider stalls, remove the STT/TTS completion deadline, and settle playback immediately on terminal session shutdown.

* test(discord): remove unused playback test bindings
2026-08-26 13:29:41 -07:00
Peter Steinberger ad1e946c7a feat(a2a): add A2A v1.0 agent-interop channel plugin (#130008)
* feat(a2a): add A2A v1.0 agent-interop channel plugin

Adds `a2a` as a bundled, default-off channel plugin so A2A-compliant agents
(Hermes Agent, LangChain, CrewAI, Google ADK) can discover an OpenClaw gateway
and send it tasks, and so OpenClaw can address configured peers.

Inbound HTTP (`/.well-known/agent-card.json`, `/a2a/v1`) runs through the normal
channel ingress path, so peer messages inherit allowlist admission, provenance
framing, and session routing. The agent reply returns through the channel
delivery callback and resolves the A2A task with its artifact, correlated per
(peer, contextId) FIFO so concurrent sends cannot cross-talk.

Wire format follows the canonical v1.0.0 spec sources (a2aproject/A2A
specification/a2a.proto): PascalCase SendMessage/GetTask/CancelTask,
supportedInterfaces[] rather than the 0.3-era top-level url/protocolVersion,
member-presence Part oneof, TASK_STATE_*/ROLE_* enums, no Task.kind. The 0.3
dotted method names are accepted as explicit compat aliases because shipped
Hermes-generation peers still send them.

Security: per-peer bearer tokens compared in constant time, per-peer sliding
window rate limit, 1 MiB body cap, 64 KiB inbound text cap, tasks scoped to
their owning peer, outbound redirect: "error", and no inbound-controlled target
URLs. With no peers configured the channel stays unconfigured and registers no
routes.

Live-verified against a real gateway on an isolated state dir with a mock
provider: 31/31 proofs, including the agent reply round-tripping into the task
artifact. That run caught two defects fixed here:

- The agent card read `cfg.agents.list` directly, so gateways configured with
  the canonical `agents.entries` roster advertised zero skills. Now uses
  listAgentIds/resolveAgentConfig, which read either roster shape.
- `returnImmediately` dispatches inherited the HTTP request's released work-
  admission root and every async task failed as GatewayDrainingError. Now
  reserves an independent root via runDetachedWebhookWork, matching sms/line/zalo.

* fix(a2a): route outbound sends through the SSRF guard and land setup metadata

CI on the first push surfaced four real defects that local `check:changed` did
not cover (it runs no oxlint or typecheck lane):

- Outbound peer sends used raw `fetch()`, tripping the channel/plugin
  no-raw-fetch boundary. They now go through `fetchWithSsrFGuard` with
  `maxRedirects: 0`, so A2A egress carries the same SSRF policy as every other
  plugin call and a redirect cannot re-target a delivered task.
- The channel declared an empty setup contract while its package manifest
  declared none, so the bundled-channel shape contract failed. The wizard now
  collects `advertisedUrl` plus a first peer name/token pair, which is the
  minimum that leaves A2A actually configured (it stays unconfigured until one
  peer credential exists), and the package manifest mirrors that metadata.
- Six lint errors in the test files: base-to-string on `BodyInit` bodies, an
  unused mock, and a shadowed `options` parameter. Request bodies now go through
  a typed reader that asserts the serialized-string contract before parsing.
- A card assertion referenced `capabilities` without declaring it on the local
  response type, failing test typecheck.

Outbound redirect handling moves from `redirect: "error"` to the guard's
`manual` inspection; the adapter tests assert the new shape.

* fix(a2a): drop route replaceExisting, accept configWrites, register runtime api

CI after the interop run surfaced four more gates:

- `registerPluginHttpRoute` used `replaceExisting: true`, tripping the
  GHSA-RQP8-Q22P-5J9Q rule: a duplicate path can silently replace another
  account's handler. A2A owns fixed global paths on a single account, so a
  duplicate registration means a stale or conflicting owner. It now fails loudly
  through `throwOnFailure` instead.
- The channel schema rejected `channels.a2a.configWrites`, which every bundled
  channel must accept; added to the zod schema, the manifest schema, and the
  config type, then regenerated the channel metadata.
- The new `input` assertion in the setup adapter needed a `// SAFETY:` line for
  the assertion ratchet.
- `extensions/a2a/runtime-api.ts` needed registering in the runtime-api
  classification list, like every other bundled channel barrel.

Also repairs a pre-existing incomplete `vi.mock` factory for `../infra/fs-safe.js`
in the agent-delete suites. Both pass in isolation, but the shared-worker lane
lets a sibling importer reach `readLocalFileSafely`, which the partial factory
never exported; adding this channel changed shard composition and surfaced it.
The factory now spreads the real module and overrides only `movePathToTrash`.

* chore(a2a): refresh config baseline for the configWrites surface

* chore(tests): adopt main's fs-safe mock repair for the agent-delete suites

Main landed the same importOriginal spread for the shared-worker mock-defeat
while this branch carried an equivalent fix; take main's version verbatim so the
two do not conflict.

* fix(a2a): isolate peer sessions and stop faking task cancellation

Addresses both ClawSweeper P1 findings.

Peer sessions: inbound routing never passed `dmScope`, so it fell back to
`session.dmScope ?? "main"` and every authenticated peer converged on
`agent:main:main` under default config. Untrusted remote content was joining the
operator's own session, and peers shared conversation history. A2A now pins
`per-account-channel-peer`; the peer id already embeds the A2A contextId, so each
peer+context pair gets its own session regardless of global session config.

Cancellation: `CancelTask` marked the task `TASK_STATE_CANCELED` and discarded
the late reply, but the dispatched agent run kept going and could still use
tools. The peer was told the work stopped when it had not. There is no
plugin-facing seam to abort a live run, so cancellation is refused with -32004
instead of acknowledged, and the store's cancel path plus its canceled-delivery
tombstone are deleted rather than left as a lying terminal state. Documented as
an explicit limitation.

Live-verified on a real gateway: 34/34 proofs, including three distinct
`agent:main:a2a:default:direct:<peer>:<context>` sessions with zero traffic in
`agent:main:main`, and both cancel spellings refused while the task stays
WORKING. Re-ran the official a2a-sdk 1.1.2 Docker interop afterwards: 9/9.
2026-08-26 08:04:53 -07:00
Peter Steinberger ea78c2488b feat(msteams): native Adaptive Card approve/deny for exec and plugin approvals (#129997)
* feat(msteams): deliver native Adaptive Card approvals

Exec and gateway plugin approvals now render as Adaptive Cards in Microsoft
Teams with token-bound approve/deny actions, mirroring the Google Chat card
pattern. Card submits are intercepted before message-text serialization,
authorized against channels.msteams.allowFrom/defaultTo AAD object IDs via
the existing approval auth, claimed once, resolved over the gateway, and the
card is updated in place to its terminal state. Native delivery gates on the
top-level approvals.exec/approvals.plugin forwarding config; the /approve
text fallback remains.

* chore(msteams): shrink assertion-safety baseline after send.ts cast removal

* chore(msteams): record approval-native adapter seam in chained-assertion ledger

* fix(msteams): surface a text approval fallback when card delivery fails

When the native route suppressed the local text prompt, a failed Adaptive
Card send only logged, leaving the pending approval invisible. On delivery
error, send a plain-text /approve prompt to the planned target so the
operator always has a visible approval path. Addresses the ClawSweeper P1
on #129997 channel-locally; #130040 tracks the shared-boundary fix.
2026-08-26 02:56:22 -07:00
Peter Steinberger 6a6635abb3 feat(daemon): support Bun 1.4 managed services (#129593)
* feat(daemon): support Bun 1.4 managed runtimes

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

* fix(daemon): enforce Bun SQLite safety

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

* fix(daemon): preserve lightweight Bun startup

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

* docs: show Bun runtime opt-in commands

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-26 00:45:43 -07:00
Peter Steinberger 4426f8d69d fix(channels): honor supported account reply-threading policies (#129679)
* fix(channels): honor supported account reply-threading policies

Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>

* fix(channels): document valid account reply policies

Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>

---------

Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>
2026-08-25 21:09:15 -07:00
Peter Steinberger 6f61184d1e fix(telegram): support polls lasting up to seven days (#129461) 2026-08-25 17:36:11 -07:00
Peter Steinberger 9b2831afd5 feat(buzz): authorize different senders in each room (#129655)
* feat(buzz): support room-scoped sender authorization

* fix(buzz): synchronize generated official channel catalog
2026-08-25 16:52:42 -07:00
Ayaan Gazali 834fc445cc docs(channels): scope implicitMentions overrides to the channels that read them (#119320)
* docs(channels): scope implicitMentions overrides to the channels that read them

Only Mattermost, Slack, and Tlon call resolveChannelImplicitMentions, and they
are the only channels whose schemas accept the key. The other documented
producers pass no policy, so allowedImplicitMentionKinds stays undefined and
every produced fact counts as a mention.

* docs(channels): correct implicitMentions override wording

QQBot is a listed reply-to-bot producer with a passthrough schema, so the key
is accepted and ignored there rather than rejected. Also drops 'yet', which
implied a planned rollout while #80234 is still an open product decision.

* docs: limit implicit mention claims to bundled channels

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-24 00:13:35 -07:00
Peter Steinberger b136df2307 feat(channels): matrix custom emote discovery, discord guild-emoji cache (#128518)
Matrix gains emoji-list backed by MSC2545 image packs: room state
im.ponies.room_emotes (any state key) plus the personal
im.ponies.user_emotes account-data pack, sticker-only entries excluded,
identifiers are literal shortcodes (Matrix reactions send the annotation
key as-is; the mxc URL is exposed alongside). Discord emoji-list results
now ride the lifecycle-owned bounded entity cache, invalidated by
GuildEmojisUpdate; the non-privileged GuildExpressions intent is added so
that dispatch actually arrives. Google Chat custom-emoji listing is
documented as unavailable: customEmojis.list requires user auth while the
plugin is a chat.bot service-account channel.
2026-08-23 23:00:22 -07:00
Ayaan Gazali 4f8f050f5b docs(whatsapp): document the canonical ack reaction keys (#118161)
* docs(channels): record the WhatsApp ack-reaction exceptions

The shared reference promises channel and account ackReaction overrides plus an
identity fallback for every channel, but only Discord, Matrix, Slack, and
Telegram accept those keys, and WhatsApp sends no acknowledgment at all when
messages.ackReaction is unset. Also notes that group activation always bypasses
the group-mentions mention check.

* docs(whatsapp): qualify the ack-reaction exception

The eligibility path reads the account-aware channels.whatsapp.reactionLevel
before the message settings, so "only" applies to the emoji and scope rather
than to the whole decision.

* docs(whatsapp): describe supported ack reaction overrides precisely

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-23 21:31:00 -07:00
Peter Steinberger 2aa5eee34e feat(channels): custom emoji discovery via emoji-list across Discord, Slack, Telegram (#128435)
* 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.
2026-08-23 20:39:06 -07:00
Vincent Koc b6798cbb10 docs(slack): use canonical postAs key (#128448) 2026-08-23 18:37:49 -07:00
Sarah Fortune 67bcea131e feat(slack): add compact commentary progress (#126480)
* feat(slack): add compact progress style

* chore(slack): protect compact progress contract

* chore(slack): clarify compact progress comment

* chore(slack): document compact task card behavior

* chore(slack): document compact draft card behavior

* chore(slack): document compact progress config contract

* fix(slack): enforce compact commentary-only progress

* chore(slack): strengthen compact style guard

* fix(slack): type compact progress config

* fix(slack): scope compact plan suppression

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-08-20 15:02:33 -07:00
Pavan Kumar Gondhi dff0f41e4a fix(imessage): reject ambiguous outbound recipients (#126564)
* fix(imessage): reject ambiguous outbound targets

* fix(imessage): preserve tel URI identities

* fix(imessage): explain target qualification

* test(imessage): keep target proof plugin-local

* docs(imessage): explain qualified contact targets
2026-08-20 16:16:18 +05:30
Markus Hartung 4970ed2357 fix(matrix): accept Room v12 IDs without server suffix (#123931)
* fix(matrix): recognize room version 12 room IDs (no :server suffix)

Room version 12 (MSC4291) dropped the trailing ":server" from room
IDs -- they are now a hash of the create event. Every place in the
Matrix plugin that treated "!" + ":" as the signature of an
already-resolved room ID silently discarded valid v12 room IDs as
unresolved instead of using them directly:

- channels.matrix.rooms config resolution (config.ts) dropped the
  entry entirely, so group rooms could never pass the groupPolicy
  "allowlist" gate on a v12 homeserver -- messages were dropped with
  no reply and no default-level log line, since the only trace is a
  verbose-only debug log.
- The invite auto-join allowlist validator and the interactive
  group-room setup resolver in onboarding.ts had the same check
  duplicated, so a user typing a v12 room ID during setup would be
  told it was invalid.
- session-route.ts's per-room DM recipientSessionExact check had the
  same gap for room-kind sends.

Runtime auto-join (auto-join.ts) already only checked for the "!"
sigil, which is why joining a v12 room worked while responding in it
did not -- this made the bug hard to spot from the join path alone.

Fix: add a single canonical isMatrixRoomId predicate next to the
existing isMatrixQualifiedUserId in target-ids.ts (user IDs and
aliases still require ":server" per spec; only room IDs changed) and
reuse it at all four sites instead of repeating the stale check.

Confirmed live against a real Room v12 homeserver (Conduit): the
server's own /joined_rooms response returns bare "!<hash>" room IDs
with no colon.

* docs(matrix): document suffixless room version 12 room IDs

Room version 12 (MSC4291) dropped the ":server" suffix from room
IDs. Document that the suffixless "!room" form is accepted anywhere
the docs previously only showed "!room:server", matching the
target-ids.ts fix landed in this same PR.

* fix(matrix): update stale Room v12 guidance text

* docs(matrix): accept suffixless Room v12 IDs in the group-policy guide

docs/channels/matrix.md already documents that channels.matrix.groups
accepts the suffixless !room form on room version 12+, but the
group-policy guide (docs/channels/groups.md) still only listed
!room:server, contradicting the channel doc an operator on a v12
homeserver would actually be following.

Addresses the ClawSweeper P2 finding on PR #123931.

* fix(matrix): advertise suffixless Room v12 IDs in onboarding placeholders

The invite auto-join and group-room setup prompts' placeholder text
still showed only `!roomId:server`, even though the retry note,
validation, and unresolved-room diagnostic already accept and describe
the suffixless `!roomId` form on room version 12+. An operator on a
v12 homeserver would see their homeserver's own room IDs contradicted
by the very placeholder guiding them through setup.

Updated both placeholders to list the suffixless form alongside the
existing examples, matching the phrasing already used in
docs/channels/matrix.md and the invite retry note. Added
configureRoomsAccess/roomsAllowlist options to the shared
createMatrixUpdateKeepCredentialsPrompter test harness (mirroring the
existing inviteAutoJoin option) so the group-room setup flow can be
exercised without duplicating the base prompter setup, then added
focused tests asserting the exact placeholder text for both prompts.
Verified both new tests fail against the pre-fix placeholders and pass
after.

Addresses the two ClawSweeper P2 findings on PR #123931.

* test(matrix): restore only allowlisted environment keys

* fix(matrix): reject empty room identifiers

---------

Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-08-19 16:12:05 -07:00
Peter Steinberger 97557ec3f5 fix(widgets): route show_widget through Discord Activities (#126294)
* refactor(widgets): unify Discord presentation

* fix(discord): keep incomplete Activity routes private

* fix(discord): require usable Activity accounts

* docs(discord): clarify hidden Activity routes
2026-08-19 05:41:37 -07:00
Peter Steinberger 554dfbe0a2 feat(discord): auto-join occupied voice rooms (#125974)
* feat(discord): auto-join occupied voice rooms

Add opt-in voice.autoJoin[].whenOccupied residency so Discord voice bots join for the first human and leave when the room becomes empty while preserving existing always-on, manual, transcript, and follow-user behavior.\n\nCloses #125973

* test(discord): isolate process runtime mocks

Use stable hoisted runtime-env mocks so isolate=false Discord test ordering cannot turn sleepWithAbort back into an unmocked function.

* fix(discord): defer unknown voice occupancy

Treat memberless voice states as unresolved instead of human so bot-only rooms cannot trigger occupancy-managed auto-join. Add cache-to-listener and manager regressions.\n\nCloses #125973

* test: isolate shared module mocks

Replace ineffective non-isolated module spies with stable hoisted mocks and a child-process SQLite connection-reuse probe so gateway and Discord shards are order-independent.

* test(gateway): inline connection reuse probe

Keep the child-process SQLite ownership probe in its owning Vitest file so Knip sees the full test surface without weakening process isolation or the original order regression.
2026-08-19 00:08:54 -07:00
Josh Avant aea7ee7088 fix(matrix): keep user ID authorization case-sensitive (#125432)
* fix(matrix): preserve case-sensitive user IDs

* docs(matrix): clarify exact user ID casing

* fix(matrix): preserve exact IDs during resolution

* docs(matrix): clarify approval target casing
2026-08-17 18:08:25 -07:00
Pavan Kumar Gondhi 6e026c2fe3 fix(gateway): reject unattributable loopback proxy traffic [AI] (#119950)
* fix(gateway): bind auth limits to ingress attribution

* fix(gateway): close remaining ingress auth gaps

* fix(gateway): carry attribution into new ingress paths

* fix(gateway): close ingress ownership gaps

* fix(gateway): complete proxy ingress hardening

* fix(gateway): stabilize managed Tailscale ingress

* fix(gateway): make Tailscale cleanup ownership-safe

Refuse reset-on-exit publication until Tailscale exposes an atomic owner-bound cleanup operation, and migrate legacy configs with Doctor.

* fix(gateway): finish ingress ownership repair

* fix(gateway): own managed Tailscale route lifetime

Run managed Serve and Funnel routes as foreground claims tied to the Gateway lifecycle. Retire named Service config through Doctor because Tailscale Services cannot run in foreground mode.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): align Tailscale consumers and build guards

Remove the retired named-service config from Telegram Mini App URL resolution and register the lifecycle worker as an explicit production entry.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): preserve retired Tailscale inputs

Keep Funnel enabled when removing an ignored named-Service setting and accept the legacy positive reset flag as a no-op now that managed routes always follow Gateway lifetime.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): preserve Tailscale route diagnostics

Prefer the actionable foreground CLI failure captured during timeout cleanup, and cover the original delayed-failure ordering.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): reconcile Tailscale ingress with main

Preserve current ingress ownership contracts after the rebase, retire the obsolete device-auth migration check, validate route-owner IPC, and move Tailscale auth coverage onto the managed listener.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* fix(gateway): finish ingress rebase coverage

Unify the rebased net imports and let module-reset WebSocket tests prepare attribution through the same fresh module instance as the handler.

Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>

* test(gateway): align run-loop server fixture

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-08-16 21:01:20 -07:00
Peter Steinberger 7349177ce3 feat: main-session agent-wide visibility + session.groupScope routing (#124965)
* feat: add main session group routing

* docs: explain main session routing scopes

* fix: align memory session visibility

* test(qa): cover main-scoped group bindings

* fix(sessions): preserve binding-scoped outbound routes

* fix(routing): preserve explicit outbound owners

* fix(sessions): recognize global main visibility

* chore(ci): prune assertion safety baseline
2026-08-16 19:57:18 -07:00
Peter Steinberger d8ebe85c24 refactor(line): replace nine-marker prompt DSL with typed rich messages (#124755)
* refactor(line): replace nine-marker prompt DSL with typed rich messages

Delete the LINE plugin's double-bracket marker language (quick_replies,
location, confirm, buttons, media_player, event, agenda, device,
appletv_remote) and its parser. Portable interactions now flow through the
existing presentation-block seam (renderPresentation, matching Discord and
Feishu); LINE-specific cards ride closed channelData.line schemas mapped to
the existing Flex renderers. Prompt section shrinks to four capability
lines and explicitly de-fangs marker text. Removes the stale
assertion-safety baseline entry for the deleted parser.

Production LOC net -69, tests net -433. Suite: 510/510 green.

* fix(line): declare rich message schema dependency

* fix(line): satisfy rich message type checks

* docs(line): mark card fragments as partial
2026-08-16 12:45:39 -07:00
Sarah Fortune dbc2356abc feat(slack): make presence event guidance configurable (#123875)
* feat(slack): make presence event guidance configurable

* chore(config): regenerate channel metadata

* fix(slack): keep presence prompt SDK compatible

* chore(config): refresh config baseline

* fix(slack): cap presence event prompts

* chore(config): refresh Slack prompt metadata

* fix(slack): bound queued presence wake prompts

* fix(slack): inherit presence prompt for named accounts

* fix(slack): keep prompt limit at config boundary

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-08-15 12:02:19 -07:00
Sarah Fortune f2e5d2054d feat(slack): include observed away duration in presence events (#123805)
* feat(slack): include observed away duration in presence events

* style(slack): format presence observation type

* test(slack): type presence enqueue mock

* fix(slack): preserve presence event prompting

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-08-14 22:46:00 +00:00
Pavan Kumar Gondhi d5e89de906 fix(synology-chat): deliver attachments without forwarding source URLs [AI] (#119941)
* fix(synology-chat): host outbound attachments

* fix(synology-chat): isolate hosted media routes

* fix(synology-chat): harden hosted media limits

* fix(synology-chat): inspect hosted media content

* fix(synology-chat): reject ambiguous media routes

* fix(synology-chat): retain indeterminate media capabilities

* fix(synology-chat): mask public callback URLs

* fix(synology-chat): align callback sensitivity metadata

* fix(synology-chat): reserve media capability query keys

* fix(synology-chat): release rejected staged media

* fix(synology-chat): preserve ambiguous media handoffs

* fix(synology-chat): scan complete active media preamble

* fix(synology-chat): report validated attachment readiness

* fix(synology-chat): bound public media capability probes

* fix(synology-chat): bound hosted media responses

* test(synology-chat): model response headers read-only

* fix(synology-chat): bound hosted media reads

* fix(synology-chat): bound hosted media delivery

* fix(synology-chat): retain media through active serves

* fix(plugin-sdk): lease hosted media readers atomically

* fix(synology-chat): close hosted media review gaps

* test(synology-chat): clean up hosted media state

* fix(system-agent): canonicalize sensitive config paths

* fix(system-agent): honor runtime config sensitivity hints

* fix(config): inherit sensitive metadata

* fix(synology-chat): close latest review findings

* test(system-agent): keep config recovery checks lint-clean

* fix(system-agent): redact structured config secrets

* fix(synology-chat): harden active-content sniffing

* fix(security): close hosted media review gaps

* fix(security): close remaining hosted media review findings

* fix(system-agent): narrow dynamic owner ids safely

* fix(system-agent): narrow dynamic channel ids safely

* test(channels): isolate hosted media proofs

* fix(security): close config and hosted media review gaps

* test(plugin-sdk): split outbound media retention coverage

* test(plugin-sdk): isolate capacity store fixtures

* test(system-agent): assert config secrecy invariant
2026-08-14 17:05:20 -05:00
Josh Avant 97a53a9b35 feat: audit admitted channel participant identity (#122863)
* feat: audit admitted channel participant identity

* fix: preserve Telegram identity through thread recovery

* fix: signal held gateway process groups

* fix: keep audit evidence passive in collect routing

* fix: validate copied channel participant evidence

* fix: bind channel participant evidence to host ingress

* fix: honor Telegram proof credential roles

* fix: restart held Telegram proof through gateway

* fix: repair channel identity CI regressions

* test(matrix): bind thread routing owner

* fix: preserve direct DM SDK compatibility

* fix: bind channel provenance at host runtime

* test(feishu): provide channel context builder

* fix: defer record-bound channel runtime resolution

* fix: keep channel admission evidence core-private

* fix(audit): bind channel admission to plugin lifecycle

* fix(audit): bind ingress provenance to final context

* refactor(audit): split admission scope keys

* test(queue): cover combined metadata carriers

* refactor(audit): keep lifecycle helpers private

* fix(queue): preserve combined turn authority

* test(channels): provide ingress context builders

* test(channels): align integrated CI fixtures

* test(clickclack): resolve model-loop ingress

* docs: preserve channel participant evidence invariant
2026-08-14 08:57:01 -05:00
Peter Lee ed08d1352b fix(telegram): reject webhook and health route collisions (#119268)
Reject reserved /healthz webhook configurations before registration, preventing silent Telegram update loss. Warn through Doctor and document recovery.

Co-authored-by: Peter Lee <22994703+xialonglee@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-14 12:37:31 +00:00
Peter Steinberger ee258beba6 fix(reef): diagnose relay protocol skew (#123498) 2026-08-13 22:47:53 -07:00
Peter Steinberger 8e0f464a5f fix(gateway): suppress ambient channel auto-enable (#123174) 2026-08-13 11:20:40 -07:00
Peter Steinberger 11b3bf374c feat(slack): unify the native progress turn into one streamed message (#122976)
* feat(slack): unify the native progress turn into one streamed message

Native progress mode now streams the whole turn into a single Slack message:
narration flows as markdown_text chunks interleaved with plan/task chunks,
task rows carry file-path details and +N/-N output, the terminal task links
the session via url_source, and the final answer lands through stopStream in
the same message. Media/oversized/error finals keep their normal-delivery
fallbacks.

Deletes the finished-card receipt collapse (the '\u{1F6E0} N tool calls · ⏱ Ns' edit)
outright: the card now stays as its finished self.

Live-verified on a real workspace: exactly one bot message per turn.

* fix(slack): serialize native stream updates and keep append-only rendered text monotonic

Overlapping progress updates (compositor render, narration payload, final)
computed their narration delta before awaiting the network and committed
state only afterwards, so concurrent updates re-appended identical
narration into the streamed message (each status line landed 3x live).
A single ordering chain now makes each update's compute -> append -> commit
atomic.

applyAppendOnlyStreamUpdate also replaced the accumulated rendered text
with the incoming cumulative partial once an appended chunk had diverged
rendered from source, dropping content the sink already displayed; rendered
now only ever extends.

* fix(slack): stop refreshing thread status once a turn has visible output

Slack clears the assistant thread status as soon as the app puts anything
in the thread, and renders its own rotating agent-working row ("Generating
response...", "Finding answers...") for every status write after that -- it
ignores the app-supplied string. The typing keepalive re-set the status
every 3s for up to 60s, so each turn painted a duplicate status row under
the streamed card or progress message.

The status write is now gated on the turn having visible output, which the
dispatcher already tracks (delivered reply, committed preview, or posted
draft message). The first status still fires before any output, so slow
turns keep their indicator, and the typing reaction is tracked separately
so a suppressed status write still cleans up its reaction.

* fix(slack): let the plan card own the status line instead of echoing it

The status headline and plan explanation fed both the streamed narration
markdown and the plan card title, so every headline rendered twice: once as
static text and once in the card that keeps updating it in place. Narration
now carries only authored commentary and reasoning, and a preamble payload
whose text the card title already shows is not streamed again.

* feat(slack): make the native agent card the default progress surface

Slack's native plan/task card was opt-in behind streaming.progress
.nativeTaskCards while the Block Kit session card shipped as the default.
The native surface is the better product on every axis we can measure --
one streamed message instead of three artifacts, live task rows with file
paths and diff counts, and Slack's own agent chrome -- so it becomes the
default and an explicit false selects the Block Kit card instead.

The session link is now emitted only when it can actually work: the
operator set gateway.publicOrigin and left the Control UI enabled.
Installations with no externally reachable Gateway get no link rather than
a dead one.

The progress card still only appears for turns that do real work; the
existing compositor start gate keeps plain question-and-answer turns
card-free.

* fix(slack): finish the final inside a buffered native stream

A short narration leaves the SDK session un-flushed, so `delivered` stays
false until `stop` makes its first network call. Requiring delivery before
finishing in-stream sent the final through normal delivery and then
finalized the stream anyway, producing exactly the second message this path
exists to prevent. Stop-time rejection already falls back via
SlackStreamNotDeliveredError, so a live session is enough.

Addresses the ClawSweeper P1/P2 finding on this PR.

* refactor(slack): collapse duplicate streaming surfaces and drop dead code

Cleanup pass over the progress/streaming neighborhood, all verified unused
by exhaustive reference search:

- Deleted buildSlackProgressStreamStartChunks/UpdateChunks: byte-identical
  pass-throughs to the same builder, plus the render-module branch that
  chose between them. One exported builder now.
- Collapsed slackStreaming.draftMode, a lossless restatement of the mode it
  was derived from, and its outbound mapper; nine comparisons now read the
  mode directly. Inbound legacy parsing stays for doctor migration.
- Dropped stopSlackStream's text parameter, the draft stream's stop() member
  and onMessageSent hook, a redundant nativeStreaming argument, four dead
  members on the progress runtime, and two single-expression wrappers.
- Deduped the native card title, which was computed twice per render.

Production LOC for the whole PR drops from +216 to +114.

* chore(config): regenerate bundled channel metadata for the Slack card default

The generated metadata still carried the old opt-in help text and
default-false description for streaming.progress.nativeTaskCards, so
config UI and diagnostics would publish stale guidance.

* fix(slack): un-export the now-internal legacy draft-mode type

Collapsing draftMode removed the type's only external consumer, so knip
flagged it as an unused export. Doctor migration still parses these legacy
values inbound, so the type stays module-local.
2026-08-13 11:19:32 -07:00
Sarah Fortune 3f731db90c fix(slack): accept org-wide user IDs in Enterprise Grid policies (#122934)
* test(slack): accept org-wide enterprise users

* fix(slack): allow org-wide enterprise users

* style(slack): format enterprise user policy

* test(slack): preserve enterprise user identity scope

* fix(slack): preserve enterprise user identity scope

* fix(slack): import workspace identity normalizer

* fix(slack): normalize enterprise owner ids

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-08-12 21:01:05 -07:00