feat(channels): port command-policy and message coalescing to sidecar channels#5931
Merged
Conversation
… channels Ports two per-channel overrides that the in-process Telegram adapter had but the sidecar architecture dropped (#5241), so sidecar operators can replicate them again (closes #5841). disable_commands / allowed_commands / blocked_commands command policy is re-exposed on `[[sidecar_channels]]` as `command_policy` (allow / disable / allowlist / blocklist) plus `allowed_commands` / `blocked_commands`. A rejected command is forwarded to the agent as plain text, never answered with an "unknown command" error — matching the #2063 behaviour public-facing bots rely on. message_coalesce_window_ms re-exposes the inbound debounce window (#4441): messages from one sender arriving within the window are buffered and dispatched as a single batched context, so a user forwarding several messages in rapid succession produces one agent invocation instead of racing concurrent ones on the same session. Implementation reuses the gating the bridge already has. `SidecarAdapter` builds a `ChannelOverrides` from its `[[sidecar_channels]]` block and exposes it via a new `ChannelAdapter::channel_overrides()` default trait method; the bridge prefers that per-instance override over the kernel-level channel-type lookup, which cannot tell two sidecars sharing a `channel_type` (e.g. two Telegram bots) apart. `command_policy` maps onto the existing `is_command_allowed` boolean/list fields and `message_coalesce_window_ms` onto the debouncer's `message_debounce_ms`, so no Python sidecar change is needed — gating happens uniformly kernel-side for every adapter type. Tests: - librefang-types: serde defaults are backward-compatible (allow / coalescing-off); disable / allowlist / blocklist parse. - librefang-channels: `overrides_from_sidecar_config` maps each policy variant and the coalesce window correctly, returns None for a plain config, and the adapter surfaces the built overrides. Verified in the sanctioned Docker dev image (named volumes, host target untouched): - cargo test -p librefang-types -p librefang-channels --lib (1333 pass) - cargo clippy -p librefang-types -p librefang-channels --all-targets -- -D warnings (clean) - cargo fmt -p librefang-types -p librefang-channels -- --check (clean) The kernel-config golden fixture was updated by hand (the regen test lives in librefang-api, whose build hits a pre-existing aarch64-Linux `libc::SYS_getpgrp` break in librefang-runtime unrelated to this change; the golden assertion is currently `#[ignore]`d on main for separate pre-existing drift, so CI does not gate on it yet).
`command_policy = "allowlist"` is an explicit default-deny intent, but with an empty (omitted or `[]`) `allowed_commands` the sidecar produced `ChannelOverrides { allowed_commands: [] }`.
The bridge's `is_command_allowed` treats an empty allowlist as "no allowlist configured" and falls through to the empty-blocklist branch, which allows everything — so selecting allowlist silently became fail-open, the exact public-bot scenario the policy is meant to lock down.
Map an empty allowlist onto `disable_commands = true` (the highest-precedence deny `is_command_allowed` honours) so it denies all commands.
A non-empty allowlist still allows only its listed commands; `disable` and `blocklist` are unchanged.
An empty blocklist keeps its intuitive "block nothing, allow all" meaning but now logs a WARN since it usually signals a forgotten list.
Tests:
- sidecar unit test asserts allowlist + empty list yields `disable_commands == true`.
- new bridge integration test builds the override through the real `SidecarAdapter` and drives `/agent coder` through the live dispatch path, asserting the command is gated (forwarded to the agent as the plain text `/agent coder`, router unchanged) rather than honoured.
Verified via the repo Docker dev image: `cargo test -p librefang-channels` (479 lib + 28 integration + 4 conformance + 3 doctests, all pass) and `cargo clippy -p librefang-channels --all-targets -- -D warnings` clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports two per-channel overrides that the in-process Telegram adapter had but the sidecar architecture dropped (#5241), so sidecar operators can replicate them again.
Closes #5841.
1. Command policy —
command_policyon[[sidecar_channels]]Re-exposes the in-process
disable_commands/allowed_commands/blocked_commandsgating (#2063) on sidecar channels:A rejected command is forwarded to the agent as plain text, never answered with an "unknown command" error — preserving the #2063 behaviour public-facing / customer-service bots rely on.
A non-technical end user who types
/newor/rebootno longer triggers a session reset.2. Inbound coalescing —
message_coalesce_window_msRe-exposes the inbound debounce window (#4441):
Messages from one sender arriving within the window are buffered and dispatched as a single batched context, so a user forwarding several messages in rapid succession produces one agent invocation instead of racing concurrent ones on the same session (the "Sorry, something went wrong" regression in the issue).
Implementation
Reuses the gating the bridge already has — the inbound
is_command_allowedcheck (bridge.rs) and theMessageDebouncer.The only gap was a config path to feed them for sidecars, since
KernelBridgeAdapter::channel_overrides()returnsNonefor sidecar channels.SidecarChannelConfiggainscommand_policy: SidecarCommandPolicy,allowed_commands,blocked_commands,message_coalesce_window_ms— all#[serde(default)], backward-compatible.SidecarAdapterbuilds aChannelOverridesfrom its block (overrides_from_sidecar_config) and exposes it via a newChannelAdapter::channel_overrides()default trait method (defaultNone).channel_overrides(channel_type, …)lookup at all three resolution sites (debouncer setup instart_adapter,dispatch_message, and the coalesced-media flush path). The kernel lookup is keyed only bychannel_typeand cannot distinguish two sidecars sharing achannel_type(e.g. two Telegram bots); a per-adapter override can.command_policymaps onto the existingis_command_allowedboolean/list fields;message_coalesce_window_msmaps onto the debouncer'smessage_debounce_ms. Gating happens uniformly kernel-side, so no Python sidecar change is needed — the adapter still emits the command, the bridge forwards it as text when policy rejects it.Tests
librefang-types(config/types.rs): serde defaults are backward-compatible (allow policy, coalescing off);disable/allowlist/blocklistblocks parse with their command lists.librefang-channels(sidecar.rs):overrides_from_sidecar_configmaps each policy variant and the coalesce window correctly, returnsNonefor a plain config (so the kernel fallback still applies), andSidecarAdapter::channel_overrides()surfaces the built overrides.Verification
Run in the sanctioned Docker dev image (
Dockerfile.rust-dev, named volumes — hosttarget/untouched):cargo test -p librefang-types -p librefang-channels --lib— 1333 pass (6 new in channels, 3 new in types)cargo clippy -p librefang-types -p librefang-channels --all-targets -- -D warnings— cleancargo fmt -p librefang-types -p librefang-channels -- --check— cleanNotes / deferred
kernel_config_schema.golden.json) was updated by hand to mirror the deterministic schemars output (newcommand_policy/allowed_commands/blocked_commands/message_coalesce_window_msproperties + theSidecarCommandPolicydefinition). The automated regen test lives inlibrefang-api, whose build currently hits a pre-existing aarch64-Linuxlibc::SYS_getpgrpbreak inlibrefang-runtime(unrelated to this change;SYS_getpgrpis x86-only). The golden assertionkernel_config_schema_matches_golden_fixtureis itself#[ignore]d onmainfor separate pre-existing drift, so CI does not gate on the fixture yet — but please re-runcargo test -p librefang-api --test config_schema_golden -- --ignored regenerate_golden --nocaptureon an x86_64 host to confirm a byte-match if convenient.[Unreleased]section exists onmainand the attribution guard expects(@user)); add one on merge.