feat(messaging): add Signal adapter via signal-cli JSON-RPC daemon#347
feat(messaging): add Signal adapter via signal-cli JSON-RPC daemon#347ibhagwan wants to merge 17 commits intospacedriveapp:mainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds first-class Signal support: a new Signal messaging adapter, config/toml schema and permissions, binding/group ACLs, prompt fragments, target parsing/normalization, tooling/agent wiring to propagate per-message adapter context, startup/watcher hot-reload integration, and API/binding extensions. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Woah! Thank you, huge. |
|
Ty @jamiepine for this wonderful project, although lesser known ATM I believe this one has huge potential, I've been personally testing every decent project out there and so far I like this one and IronClaw the most. Btw, this isn't my first signal support PR, I've added the same to ZeroClaw, IronClaw, NullClaw and Hermes-agent: |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/secrets/store.rs (1)
1103-1119:⚠️ Potential issue | 🟠 MajorSignal secret registration is still incomplete.
This only helps for whatever
SignalConfig::secret_fields()exposes, and that implementation currently declareshttp_urlbut notaccount. As a result,SIGNAL_ACCOUNTandSIGNAL_<INSTANCE>_ACCOUNTwill still auto-categorize as Tool secrets and can leak into worker env vars.🧩 Required follow-up in
src/config/types.rsfn secret_fields() -> &'static [SecretField] { - &[SecretField { - toml_key: "http_url", - secret_name: "SIGNAL_HTTP_URL", - instance_pattern: Some(InstancePattern { - platform_prefix: "SIGNAL", - field_suffix: "HTTP_URL", - }), - }] + &[ + SecretField { + toml_key: "http_url", + secret_name: "SIGNAL_HTTP_URL", + instance_pattern: Some(InstancePattern { + platform_prefix: "SIGNAL", + field_suffix: "HTTP_URL", + }), + }, + SecretField { + toml_key: "account", + secret_name: "SIGNAL_ACCOUNT", + instance_pattern: Some(InstancePattern { + platform_prefix: "SIGNAL", + field_suffix: "ACCOUNT", + }), + }, + ] }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/secrets/store.rs` around lines 1103 - 1119, Signal secret registration misses the Signal account key: update SignalConfig::secret_fields (in the SignalConfig implementation in src/config/types.rs) to include the account field alongside http_url so SIGNAL_ACCOUNT and SIGNAL_<INSTANCE>_ACCOUNT are treated as secret; modify the SignalConfig struct/impl to return the account secret name (matching naming used elsewhere for instance-scoped secrets) in secret_fields so the secrets store registers it correctly.src/agent/channel.rs (1)
1789-1800:⚠️ Potential issue | 🟠 MajorThis still drops named-instance context before it reaches
SendMessageTool.
self.current_adapter()only derives fromsource_adapter/conversation_id, and both are populated from the base source (message.source). A conversation running onsignal:supporttherefore still passes justsignalhere, so explicit Signal sends can escape onto the default instance instead of the active one. Capture and preferInboundMessage.adapterwhen establishing the channel adapter context.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agent/channel.rs` around lines 1789 - 1800, The code currently passes self.current_adapter() into add_channel_tools which is derived from source_adapter/conversation_id and can lose named-instance context; change the adapter argument to prefer the InboundMessage.adapter (or equivalent inbound message field available in this scope) when present, falling back to self.current_adapter() only if InboundMessage.adapter is None, so the channel context reaching SendMessageTool preserves the explicit inbound adapter instance.
🧹 Nitpick comments (3)
src/agent/channel_history.rs (1)
320-328: Minor: Double space whensender_contextis empty.For non-Signal adapters (Slack, Discord, Telegram, Twitch),
sender_contextis not set, resulting in an empty string. The format string" {sender_context} "then produces consecutive spaces in the output (e.g.,"Alice [timestamp]: text").Consider conditionally including the leading space only when
sender_contextis non-empty:🔧 Proposed fix
let sender_context = message .metadata .get("sender_context") .and_then(|v| v.as_str()) - .unwrap_or(""); + .filter(|s| !s.is_empty()) + .map(|s| format!(" {s}")) + .unwrap_or_default(); format!( - "{display_name}{bot_tag}{reply_context} {sender_context} [{timestamp_text}]: {text_content}" + "{display_name}{bot_tag}{reply_context}{sender_context} [{timestamp_text}]: {text_content}" )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agent/channel_history.rs` around lines 320 - 328, The formatted string currently always inserts a space before and after sender_context, causing a double space when sender_context is empty; update the code around the retrieval of sender_context (the variable named sender_context and the format! call that builds the final line) to conditionally include the leading space only when sender_context is non-empty (e.g., compute a sender_context_prefixed that is either "" or " "+sender_context and use that in the format string) so the output has no extra spaces for non-Signal adapters.src/api/bindings.rs (1)
35-60: Keep the update endpoint symmetric with the new group permission fields.
CreateBindingRequestnow acceptsgroup_ids/group_allowed_users, butUpdateBindingRequestbelow still cannot modify them. That leaves API clients with delete-and-recreate as the only way to change group-based binding permissions on an existing record.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/bindings.rs` around lines 35 - 60, UpdateBindingRequest is missing the new group permission fields present on CreateBindingRequest, preventing updates to group-based permissions; add group_ids: Vec<String> with #[serde(default)] and group_allowed_users: Vec<String> with #[serde(default)] to the UpdateBindingRequest definition (use the same types and serde defaults as CreateBindingRequest) so the update endpoint can modify group_ids and group_allowed_users symmetrically with creation.src/config/load.rs (1)
2031-2031: Renamestosignal_configin this loader block.This closure is long enough that the single-letter binding makes the field mapping harder to scan than it needs to be.
As per coding guidelines, "Use non-abbreviated variable names in Rust code:
queuenotq,messagenotmsg,channelnotch; common abbreviations likeconfigare acceptable".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/load.rs` at line 2031, The closure mapping toml.messaging.signal currently binds its argument as a single-letter `s`, which reduces readability; rename that binding to `signal_config` in the closure used to build `signal:` (i.e., change `toml.messaging.signal.and_then(|s| { ... }` to `and_then(|signal_config| { ... }`) and update all references inside the closure (field mappings and method calls) to use `signal_config` so the intent is clearer while preserving behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/agent/channel.rs`:
- Around line 2134-2139: The send of the user-visible error via
self.response_tx.send(OutboundResponse::Text(error_msg)).await currently ignores
the Result with `let _ =`, which silently discards failures; update the code in
channel.rs around the error path where error_msg is built to handle the send
result explicitly—either call .ok() to mark it as best-effort
(self.response_tx.send(...).await.ok()) or log the error when send returns Err
(e.g., capture the Result and call a logger with context including error_msg and
the send Err); reference the send call, response_tx, OutboundResponse::Text, and
error_msg when making the change.
In `@src/config/permissions.rs`:
- Around line 339-343: SignalPermissions currently mixes DM and group allowlists
causing group wildcards to grant DM access; preserve them separately by keeping
dm_allowed_users as the canonical DM allowlist and
group_filter/group_allowed_users as the group-scoped allowlist. Update the code
paths that set ["*"] for group IDs (the logic around group_filter and any places
that assign to dm_allowed_users when encountering a group wildcard) so they only
mutate group_filter or group_allowed_users and never overwrite or merge into
dm_allowed_users; likewise remove any folding of group_allowed_users into
dm_allowed_users in the code that consolidates permissions (the blocks
referenced around the current group handling and the consolidation sections),
and ensure authorization checks use dm_allowed_users for direct messages and
group_filter/group_allowed_users for group messages only.
- Around line 388-428: The group_filter construction currently returns
Some(all_group_ids) even when all_group_ids is empty; change it so that after
aggregating and filtering seed_group_ids and signal_bindings (using
is_valid_base64) you return None when all_group_ids.is_empty() to represent the
"block all" case; keep the existing early returns for wildcard ("*") that
produce Self with group_filter Some(vec!["*"]) and dm_allowed_users vec!["*"],
but replace the final Some(all_group_ids) with None when no valid IDs remain so
consumers don’t receive Some(vec![]); update the block where group_filter is set
(the let group_filter = { ... } scope) to perform this empty-check before
returning.
In `@src/config/types.rs`:
- Around line 1330-1339: Binding::matches currently ignores Signal-specific
metadata so resolve_agent_for_message will incorrectly select the first
channel="signal" binding; update Binding::matches to inspect the message's
Signal fields (e.g., signal_chat_type, signal_group_id, sender_id) and enforce
the new binding fields: check that signal_chat_type (or equivalent) matches the
binding, that for group chats signal_group_id is allowed by group_ids or
sender_id is allowed by group_allowed_users, and that for DMs sender_id is
allowed by dm_allowed_users; also respect require_mention and channel_ids when
matching Signal messages so resolve_agent_for_message can correctly route to the
appropriate agent.
- Around line 2341-2374: The SignalConfig treats account as sensitive in Debug
but SystemSecrets.secret_fields only includes http_url; add a SecretField for
the account inside the SignalConfig impl of SystemSecrets (update
secret_fields() to return both SecretField entries) using toml_key "account", a
sensible secret_name like "SIGNAL_ACCOUNT", and the same InstancePattern style
used for http_url (platform_prefix "SIGNAL", field_suffix "ACCOUNT") so the
account/phone number is resolved as a secret consistently with Debug redaction.
In `@src/config/watcher.rs`:
- Around line 233-239: The reload only replaces a local Arc (new_perms) into an
ephemeral ArcSwap when signal_permissions was None or when creating a named
instance, so adapters started after boot keep an unreachable permissions handle
and won't get updates; change the logic to allocate and store a shared
ArcSwap<SignalPermissions> that lives in the watcher's state and hand clones of
that ArcSwap (or Arc<ArcSwap<...>>) to any hot-started Signal adapters instead
of creating a fresh, untracked Arc each time. Specifically, where
signal_permissions is initialized or a named Signal instance is created (the
places that call SignalPermissions::from_config / perms.store now and the
named-instance startup blocks), create or reuse a single ArcSwap container held
in the watcher and update that container on reload (call store with the new
Arc<SignalPermissions>) so all adapters receive updates.
In `@src/conversation/worker_transcript.rs`:
- Around line 451-458: The change currently maps UserContent::Text to
TranscriptStep::Action which causes user messages to be labeled as agent text;
instead, update the branch that matches rig::message::UserContent::Text in
worker_transcript.rs to emit TranscriptStep::UserText (preserving the same inner
text payload) rather than TranscriptStep::Action so worker_inspect.rs will
render it as "**User:**". Locate the match arm handling UserContent::Text and
replace the Action emission with the UserText variant, keeping the text
cloning/empty-and-system checks intact.
In `@src/messaging/signal.rs`:
- Around line 581-585: The tracing::info calls (e.g., the one using
sender_display, text.len(), is_group and the other similar calls around the
file) are logging raw Signal identifiers and URLs; update those log statements
to redact or obfuscate sensitive fields instead of printing them raw: replace
sender_display with a redacted/hashed form (e.g., mask or hash the phone/UUID)
and replace any http_url or full URLs with a redacted or sanitized version (keep
only domain or a fixed placeholder), and ensure any helper used
(redact_identifier/redact_url or similar) is applied consistently to the
tracing::info calls at this site and the other occurrences you flagged (around
lines ~677-680 and ~899-900) so no PII/embedded credentials are emitted.
- Around line 287-295: The current logic checks response_body.is_empty() before
validating the HTTP status, allowing non-success statuses with empty bodies to
be treated as Ok(None); update the control flow in the function that reads
response_body and status so the status.is_success() check runs before returning
on an empty body (i.e., if !status.is_success() keep the existing truncated_body
+ anyhow::bail! behavior even when response_body.is_empty()), ensuring failed
send/sendTyping calls propagate an error rather than Ok(None).
- Around line 480-485: The sender resolution currently only checks
envelope.source_number and envelope.source and thus returns None for
privacy-mode envelopes; update the chain to also consider envelope.source_uuid
(e.g., prefer envelope.source_number, then envelope.source, then
envelope.source_uuid) before mapping to String so the new UUID allowlist path
works; reference the existing sender binding and the envelope.source_number /
envelope.source / envelope.source_uuid fields and ensure the final
.map(String::from)? remains.
- Around line 397-405: The code currently joins untrusted `filename` into
`self.tmp_dir`, allowing path traversal or nested paths; fix by extracting and
sanitizing only the final path component before joining: use
Path::new(&filename).file_name().and_then(|s|
s.to_str()).unwrap_or("attachment") (or fallback to the UUID alone) then
optionally replace or strip unsafe characters to produce `safe_name`, build
`unique_name` with Uuid::new_v4() and `safe_name`, and use that when creating
`tmp_path` (still `self.tmp_dir.join(&unique_name)`); ensure you handle the
fallback case so `tmp_path` cannot escape `tmp_dir` and avoid relying on
intermediate directories from the original filename.
- Around line 821-889: The cancel sender is being dropped immediately which
closes cancel_rx and causes the spawned typing task (created in the block using
cancel_tx/cancel_rx and inserted into self.typing_tasks) to exit immediately;
instead of drop(cancel_tx), retain and store the sender so stop_typing() can
call send() to signal cancellation. Modify the storage for typing tasks (e.g.,
replace the current map entry of just the JoinHandle inserted by
typing_tasks.insert(conversation_id, handle) with a small struct or tuple
containing both the JoinHandle and the cancel_tx Sender) or add a separate map
for cancel senders, ensure you insert the cancel_tx alongside the handle, and
remove the explicit drop(cancel_tx); update stop_typing() to use the stored
cancel_tx to signal cancellation before/without aborting the handle.
In `@src/prompts/engine.rs`:
- Around line 476-479: The match in PromptEngine::new() selects
"adapters/signal" for the "signal" branch, but PromptEngine::new() only
registers the "adapters/email" template so "adapters/signal" falls through to
the default None; update PromptEngine::new() (or the environment/template
registration code it calls) to register the "adapters/signal" template alongside
"adapters/email" so that the match on template_name ("adapters/signal") finds a
registered template and returns the proper adapter-specific guidance.
In `@src/tools/send_message_to_another_channel.rs`:
- Around line 249-284: The current hyphen check in parse_explicit_signal_target
misclassifies ordinary hyphenated names as Signal UUIDs; update the hyphen
branch to only accept a true UUID shape (or require the explicit "signal:uuid:"
prefix) before calling parse_delivery_target. Concretely, in
parse_explicit_signal_target replace the loose trimmed.contains('-') &&
trimmed.len() > 8 check with a strict UUID validation (e.g., test with a UUID
regex or attempt to parse as a UUID) and only then call
parse_delivery_target(&format!("signal:uuid:{trimmed}")), otherwise fall through
to channel lookup.
- Around line 141-166: The code currently unconditionally replaces
explicit_target.adapter with self.current_adapter, which causes explicit Signal
targets (e.g., parse_explicit_signal_target returning "signal:+1555...") to be
sent via the wrong adapter; change the logic in the explicit-target branch so
you only assign self.current_adapter to explicit_target.adapter when the parsed
adapter is empty/unspecified (i.e., do not overwrite when
parse_explicit_signal_target returned a concrete adapter like "signal" or
"signal:..."); update the block around parse_explicit_signal_target,
explicit_target.adapter, and the subsequent messaging_manager.broadcast call to
use the parsed adapter unless it was empty.
---
Outside diff comments:
In `@src/agent/channel.rs`:
- Around line 1789-1800: The code currently passes self.current_adapter() into
add_channel_tools which is derived from source_adapter/conversation_id and can
lose named-instance context; change the adapter argument to prefer the
InboundMessage.adapter (or equivalent inbound message field available in this
scope) when present, falling back to self.current_adapter() only if
InboundMessage.adapter is None, so the channel context reaching SendMessageTool
preserves the explicit inbound adapter instance.
In `@src/secrets/store.rs`:
- Around line 1103-1119: Signal secret registration misses the Signal account
key: update SignalConfig::secret_fields (in the SignalConfig implementation in
src/config/types.rs) to include the account field alongside http_url so
SIGNAL_ACCOUNT and SIGNAL_<INSTANCE>_ACCOUNT are treated as secret; modify the
SignalConfig struct/impl to return the account secret name (matching naming used
elsewhere for instance-scoped secrets) in secret_fields so the secrets store
registers it correctly.
---
Nitpick comments:
In `@src/agent/channel_history.rs`:
- Around line 320-328: The formatted string currently always inserts a space
before and after sender_context, causing a double space when sender_context is
empty; update the code around the retrieval of sender_context (the variable
named sender_context and the format! call that builds the final line) to
conditionally include the leading space only when sender_context is non-empty
(e.g., compute a sender_context_prefixed that is either "" or " "+sender_context
and use that in the format string) so the output has no extra spaces for
non-Signal adapters.
In `@src/api/bindings.rs`:
- Around line 35-60: UpdateBindingRequest is missing the new group permission
fields present on CreateBindingRequest, preventing updates to group-based
permissions; add group_ids: Vec<String> with #[serde(default)] and
group_allowed_users: Vec<String> with #[serde(default)] to the
UpdateBindingRequest definition (use the same types and serde defaults as
CreateBindingRequest) so the update endpoint can modify group_ids and
group_allowed_users symmetrically with creation.
In `@src/config/load.rs`:
- Line 2031: The closure mapping toml.messaging.signal currently binds its
argument as a single-letter `s`, which reduces readability; rename that binding
to `signal_config` in the closure used to build `signal:` (i.e., change
`toml.messaging.signal.and_then(|s| { ... }` to `and_then(|signal_config| { ...
}`) and update all references inside the closure (field mappings and method
calls) to use `signal_config` so the intent is clearer while preserving
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b7ebd32c-d659-4f3e-b98b-298d5a3dd4f0
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lock,!**/*.lockCargo.tomlis excluded by!**/*.toml
📒 Files selected for processing (23)
prompts/en/adapters/signal.md.j2prompts/en/tools/send_message_description.md.j2src/agent/channel.rssrc/agent/channel_history.rssrc/api/bindings.rssrc/config.rssrc/config/load.rssrc/config/permissions.rssrc/config/toml_schema.rssrc/config/types.rssrc/config/watcher.rssrc/conversation/worker_transcript.rssrc/llm/model.rssrc/main.rssrc/messaging.rssrc/messaging/signal.rssrc/messaging/target.rssrc/prompts/engine.rssrc/prompts/text.rssrc/secrets/scrub.rssrc/secrets/store.rssrc/tools.rssrc/tools/send_message_to_another_channel.rs
|
This needs some work, I'll change it to draft until I respond to all the review comments |
This is wonderful to hear, thank you. I'm putting a load of effort into making this the best I can. Hoping soon it will pick up traction wise, but so far the contributions have been amazing and we're moving quickly to stability. If you want to chat please join the Discord and/or DM me anytime! |
90cc30d to
b7b7218
Compare
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified All changes pass cargo check.
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified **Low** - agent/channel_history.rs: Condition space before sender_context on non-empty value to prevent double spacing All changes pass cargo check.
e741a28 to
b2509ac
Compare
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified **Low:** - agent/channel_history.rs: Condition space before sender_context on non-empty value to prevent double spacing - api/bindings.rs: Add group_ids and group_allowed_users to UpdateBindingRequest for symmetric updates All changes pass cargo check.
b2509ac to
22b4717
Compare
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified - agent/channel.rs: Add adapter parameter to run_agent_turn, prefer InboundMessage.adapter to preserve named-instance context - api/bindings.rs: Add group_ids and group_allowed_users to UpdateBindingRequest for symmetric updates **Low:** - agent/channel_history.rs: Condition space before sender_context on non-empty value to prevent double spacing All changes pass cargo check.
22b4717 to
dd419d7
Compare
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified - agent/channel.rs: Add adapter parameter to run_agent_turn, prefer InboundMessage.adapter to preserve named-instance context - api/bindings.rs: Add group_ids and group_allowed_users to UpdateBindingRequest for symmetric updates **Low:** - agent/channel_history.rs: Condition space before sender_context on non-empty value to prevent double spacing All changes pass cargo check.
aba09a9 to
0d2bf8f
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
1 similar comment
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/agent/channel.rs (1)
1325-1333:⚠️ Potential issue | 🟠 MajorPreserve the concrete adapter on batched turns.
Line 1332 still passes
self.current_adapter(), which is derived from the channel/source and can lose the per-message adapter instance. The single-message path at Lines 1625-1628 now prefersmessage.adapter, but the coalesced path does not, so a batched Signal conversation on a non-default instance can still send explicitsignal:*targets through the wrong adapter inSendMessageTool.Suggested fix
- let (result, skip_flag, replied_flag, _) = self + let adapter = messages + .iter() + .find_map(|message| message.adapter.as_deref()) + .or_else(|| self.current_adapter()); + let (result, skip_flag, replied_flag, _) = self .run_agent_turn( &combined_text, &system_prompt, &conversation_id, attachment_parts, false, // not a retrigger - self.current_adapter(), + adapter, ) .await?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agent/channel.rs` around lines 1325 - 1333, The batched/coalesced turn is still calling run_agent_turn(..., self.current_adapter()) which can lose a per-message adapter; change the coalesced-path call site to preserve and pass the concrete adapter instance used for those messages (the same adapter you use for single-message handling via message.adapter) into run_agent_turn (or into the coalescing routine) so SendMessageTool receives the correct adapter for signal:* targets; update the coalescing logic that builds combined_text/attachment_parts to carry the chosen adapter and pass that adapter instead of self.current_adapter().
♻️ Duplicate comments (4)
src/config/watcher.rs (1)
233-239:⚠️ Potential issue | 🟠 MajorHot-started Signal adapters still get untracked permission handles.
When Signal is enabled after boot, or when a named instance starts here, this code allocates a fresh
ArcSwapand passes it straight into the adapter. Later reloads only updatesignal_permissions, so those adapters keep stale DM/group filters until restart. The watcher needs one shared, stored handle per live Signal adapter.Also applies to: 541-547, 562-574
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/watcher.rs` around lines 233 - 239, The current watcher creates a fresh Arc/ArcSwap and hands it directly to newly started Signal adapters (via the code path that calls SignalPermissions::from_config and perms.store(...)), so adapters started after boot keep their own Arc and never see subsequent reloads; instead ensure the watcher maintains one shared ArcSwap<SignalPermissions> per live Signal adapter and always pass a clone of that shared ArcSwap handle into adapter constructors. Concretely: stop allocating a new ArcSwap for each adapter start, centralize the ArcSwap (the existing signal_permissions variable) so adapters receive signal_permissions.clone() (or an Arc clone of the ArcSwap/holder) and update SignalPermissions::from_config -> perms.store(Arc::new(new_perms)) on reloads; apply the same change where Signal adapters are instantiated (the branches that touch signal_permissions, SignalPermissions::from_config, and perms.store) so adapters observe reloads instead of keeping stale filters.src/tools/send_message_to_another_channel.rs (1)
147-156:⚠️ Potential issue | 🟠 MajorDon't overwrite explicitly selected Signal instances.
This still rewrites any explicit Signal target onto
current_adapteras long as the current conversation came from Signal.signal:work:+1555...sent from asignal:personalthread will go out on the wrong account. Only the bare shorthand forms should inherit the current adapter.Suggested guard
- if let Some(current_adapter) = self - .current_adapter - .as_ref() - .filter(|adapter| adapter.starts_with("signal")) - { - explicit_target.adapter = current_adapter.clone(); - } + if !args.target.trim_start().starts_with("signal:") + && let Some(current_adapter) = self + .current_adapter + .as_ref() + .filter(|adapter| adapter.starts_with("signal")) + { + explicit_target.adapter = current_adapter.clone(); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/send_message_to_another_channel.rs` around lines 147 - 156, The code currently overwrites any Signal target with self.current_adapter when the conversation is from Signal; change this so only bare shorthand targets inherit the current adapter. In the block that handles parse_explicit_signal_target(&args.target) (and sets explicit_target.adapter), first detect whether the caller provided an explicit adapter (e.g., args.target contains an adapter prefix like "signal:" or parse_explicit_signal_target produced a non-empty adapter); only if the target is a bare shorthand (no adapter specified) assign explicit_target.adapter = current_adapter.clone() — otherwise leave the explicit adapter untouched. Use the existing symbols parse_explicit_signal_target, explicit_target.adapter, args.target and self.current_adapter to implement the guard.src/messaging/signal.rs (2)
559-579:⚠️ Potential issue | 🔴 CriticalKeep DM and group sender scopes separate.
This re-merges
dm_allowed_usersinto group authorization. Withdm_allowed_users = ["*"], every sender in an allowed group passes even whengroup_allowed_usersis empty, so the new group-specific allowlist never actually narrows access.🔒 Proposed fix
- let all_group_users: Vec<&String> = permissions - .dm_allowed_users - .iter() - .chain(permissions.group_allowed_users.iter()) - .collect(); - - let sender_allowed = if all_group_users.is_empty() { + let sender_allowed = if permissions.group_allowed_users.is_empty() { false // Empty = block all - } else if all_group_users.iter().any(|u| u.as_str() == "*") { + } else if permissions + .group_allowed_users + .iter() + .any(|user| user.as_str() == "*") + { true // Wildcard = allow all } else { - all_group_users.iter().any(|allowed| { - allowed.as_str() == sender + permissions.group_allowed_users.iter().any(|allowed| { + allowed.as_str() == sender.as_str() || envelope .source_uuid .as_deref()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/signal.rs` around lines 559 - 579, The group authorization logic incorrectly merges dm_allowed_users into the group allowlist; update the check so it uses only permissions.group_allowed_users (remove dm_allowed_users from all_group_users) when computing sender_allowed for groups, keeping dm_allowed_users separate for DM checks; preserve the semantics for empty (deny all), wildcard "*" (allow all), and matching by sender or envelope.source_uuid in the group_allowed_users-based logic (referencing dm_allowed_users, group_allowed_users, all_group_users, sender_allowed, and envelope.source_uuid to locate the change).
105-111:⚠️ Potential issue | 🟠 Major
redact_url()still exposes URL userinfo.Keeping the first three
/-separated components preservesuser:pass@host:port, so a credentialedhttp_urlstill lands in logs. Parse the URL and emit onlyscheme://host[:port], or a fixed placeholder if parsing fails.🛡️ Proposed fix
fn redact_url(url: &str) -> String { - url.split('/') - .take(3) // scheme://domain - .collect::<Vec<_>>() - .join("/") - .to_string() + match reqwest::Url::parse(url) { + Ok(parsed) => match parsed.host_str() { + Some(host) => { + let mut redacted = format!("{}://{}", parsed.scheme(), host); + if let Some(port) = parsed.port() { + redacted.push_str(&format!(":{port}")); + } + redacted + } + None => "[redacted-url]".to_string(), + }, + Err(_) => "[redacted-url]".to_string(), + } }As per coding guidelines, "Compliance/privacy risks (PII retention, logging sensitive data -- like emails and other user identifiers, GDPR/CCPA violations)" are major issues.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/signal.rs` around lines 105 - 111, The current redact_url function still leaks URL userinfo because it naively keeps the first three slash-separated components; update redact_url to properly parse the input URL (using a URL parser) and reconstruct only the scheme and host (including port if present) in the form "scheme://host[:port]", explicitly omitting any userinfo, and return a fixed placeholder (e.g., "redacted://") if parsing fails or the scheme/host are missing; reference the redact_url function to locate and replace the naive split/join logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/api/bindings.rs`:
- Around line 157-166: The update_binding() path currently ignores the new
Signal permission fields so updates never persist group_ids and
group_allowed_users; modify update_binding() to copy these fields from the
incoming binding DTO into the in-memory binding (assign binding.group_ids =
dto.group_ids and binding.group_allowed_users = dto.group_allowed_users) and
ensure the updated config is written back to TOML using the same persistence
path used elsewhere (the existing save/write config routine in this module) so
changes to group_ids and group_allowed_users are persisted; keep types as
Vec<String> to match toml_schema.rs and respect serde defaults.
In `@src/config/permissions.rs`:
- Around line 397-419: The wildcard-handling branches inside the permission
assembly prematurely return a new Self (seen in the branches that set
group_filter to Some(vec!["*".to_string()])) which prevents merging the rest of
the allowlists (dm_allowed_users and group_allowed_users) from signal_bindings
and seed lists; change those branches to set only the relevant field (e.g., set
group_filter = Some(vec!["*".to_string()]) or dm_allowed_users =
vec!["*".to_string()]) and continue processing instead of returning, then after
iterating all signal_bindings build and return the final Self using the merged
dm_allowed_users, group_allowed_users, and group_filter so wildcards finalize
only their own field while allowing other lists to be combined (refer to
symbols: all_group_ids, signal_bindings, group_filter, dm_allowed_users,
group_allowed_users).
In `@src/config/types.rs`:
- Around line 1504-1511: Signal DM handling in SignalConfig is inverted: update
the block that checks self.dm_allowed_users for DMs so that when
dm_allowed_users is empty DMs are ignored, and when non-empty only listed
sender_ids are allowed. Specifically, in the Signal DM branch (SignalConfig /
method handling message routing that currently references self.dm_allowed_users
and message.sender_id) change the condition to return false if
self.dm_allowed_users.is_empty() or if
!self.dm_allowed_users.contains(&message.sender_id), so DMs are blocked when the
list is empty and otherwise only allowed for contained sender_ids.
In `@src/messaging/signal.rs`:
- Around line 887-897: The typing-indicator loop currently only handles
transport errors from client.post(...).send().await and keeps retrying on HTTP
4xx/5xx; change the block in the typing task that calls
client.post(&rpc_url).timeout(RPC_REQUEST_TIMEOUT).header(...).json(&body).send().await
to capture the Response on success, check response.status().is_success(), and if
not successful log the status and response body (or error) and break the
loop—follow the same status-checking pattern used by the rpc_request() function
so server-side failures stop the typing loop instead of retrying.
In `@src/messaging/target.rs`:
- Around line 335-374: parse_signal_target_parts currently handles e164 and
phone branches itself, causing inconsistent parsing vs the shared normalizer;
update parse_signal_target_parts to call the existing normalize_signal_target()
for any phone/e164 branches (both default and named-adapter arms) and use its
returned canonical phone string (or None to reject) when building
BroadcastTarget, keeping the adapter construction logic (adapter: "signal" or
format!("signal:{instance}")) and the uuid/group handling unchanged; this
ensures parse_signal_target_parts mirrors parse_delivery_target() behavior and
accepts bare numeric forms that normalize_signal_target accepts.
In `@src/tools/send_message_to_another_channel.rs`:
- Around line 288-291: The bare-digit branch (checking trimmed.len() >= 7 && all
digits) preempts ChannelStore lookup and wrongly treats numeric channel IDs as
Signal numbers; change the logic so that you only call
crate::messaging::target::parse_delivery_target for Signal when the input
explicitly indicates Signal (e.g., original input starts with "signal:" or an
explicit "+"/international prefix), otherwise fall through to the ChannelStore
resolution path. Update the condition around the trimmed check (and any use of
trimmed) to require an explicit signal indicator, and ensure ChannelStore lookup
remains the default resolution for plain numeric strings.
---
Outside diff comments:
In `@src/agent/channel.rs`:
- Around line 1325-1333: The batched/coalesced turn is still calling
run_agent_turn(..., self.current_adapter()) which can lose a per-message
adapter; change the coalesced-path call site to preserve and pass the concrete
adapter instance used for those messages (the same adapter you use for
single-message handling via message.adapter) into run_agent_turn (or into the
coalescing routine) so SendMessageTool receives the correct adapter for signal:*
targets; update the coalescing logic that builds combined_text/attachment_parts
to carry the chosen adapter and pass that adapter instead of
self.current_adapter().
---
Duplicate comments:
In `@src/config/watcher.rs`:
- Around line 233-239: The current watcher creates a fresh Arc/ArcSwap and hands
it directly to newly started Signal adapters (via the code path that calls
SignalPermissions::from_config and perms.store(...)), so adapters started after
boot keep their own Arc and never see subsequent reloads; instead ensure the
watcher maintains one shared ArcSwap<SignalPermissions> per live Signal adapter
and always pass a clone of that shared ArcSwap handle into adapter constructors.
Concretely: stop allocating a new ArcSwap for each adapter start, centralize the
ArcSwap (the existing signal_permissions variable) so adapters receive
signal_permissions.clone() (or an Arc clone of the ArcSwap/holder) and update
SignalPermissions::from_config -> perms.store(Arc::new(new_perms)) on reloads;
apply the same change where Signal adapters are instantiated (the branches that
touch signal_permissions, SignalPermissions::from_config, and perms.store) so
adapters observe reloads instead of keeping stale filters.
In `@src/messaging/signal.rs`:
- Around line 559-579: The group authorization logic incorrectly merges
dm_allowed_users into the group allowlist; update the check so it uses only
permissions.group_allowed_users (remove dm_allowed_users from all_group_users)
when computing sender_allowed for groups, keeping dm_allowed_users separate for
DM checks; preserve the semantics for empty (deny all), wildcard "*" (allow
all), and matching by sender or envelope.source_uuid in the
group_allowed_users-based logic (referencing dm_allowed_users,
group_allowed_users, all_group_users, sender_allowed, and envelope.source_uuid
to locate the change).
- Around line 105-111: The current redact_url function still leaks URL userinfo
because it naively keeps the first three slash-separated components; update
redact_url to properly parse the input URL (using a URL parser) and reconstruct
only the scheme and host (including port if present) in the form
"scheme://host[:port]", explicitly omitting any userinfo, and return a fixed
placeholder (e.g., "redacted://") if parsing fails or the scheme/host are
missing; reference the redact_url function to locate and replace the naive
split/join logic.
In `@src/tools/send_message_to_another_channel.rs`:
- Around line 147-156: The code currently overwrites any Signal target with
self.current_adapter when the conversation is from Signal; change this so only
bare shorthand targets inherit the current adapter. In the block that handles
parse_explicit_signal_target(&args.target) (and sets explicit_target.adapter),
first detect whether the caller provided an explicit adapter (e.g., args.target
contains an adapter prefix like "signal:" or parse_explicit_signal_target
produced a non-empty adapter); only if the target is a bare shorthand (no
adapter specified) assign explicit_target.adapter = current_adapter.clone() —
otherwise leave the explicit adapter untouched. Use the existing symbols
parse_explicit_signal_target, explicit_target.adapter, args.target and
self.current_adapter to implement the guard.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3a757d08-c636-441b-a7e1-bc51616e900f
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lock,!**/*.lockCargo.tomlis excluded by!**/*.toml
📒 Files selected for processing (23)
prompts/en/adapters/signal.md.j2prompts/en/tools/send_message_description.md.j2src/agent/channel.rssrc/agent/channel_history.rssrc/api/bindings.rssrc/config.rssrc/config/load.rssrc/config/permissions.rssrc/config/toml_schema.rssrc/config/types.rssrc/config/watcher.rssrc/conversation/worker_transcript.rssrc/llm/model.rssrc/main.rssrc/messaging.rssrc/messaging/signal.rssrc/messaging/target.rssrc/prompts/engine.rssrc/prompts/text.rssrc/secrets/scrub.rssrc/secrets/store.rssrc/tools.rssrc/tools/send_message_to_another_channel.rs
🚧 Files skipped from review as they are similar to previous changes (6)
- src/agent/channel_history.rs
- src/config/load.rs
- src/prompts/engine.rs
- src/secrets/store.rs
- src/conversation/worker_transcript.rs
- src/tools.rs
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/agent/channel.rs (1)
1325-1333:⚠️ Potential issue | 🟠 MajorPreserve the full runtime adapter key for named instances.
These call sites still fall back to
self.current_adapter(), but that value is derived frommessage.source/ the firstconversation_idsegment, so a named adapter likesignal:supportgets collapsed tosignal. Batched turns and synthetic retriggers will then register channel tools against the default adapter and can send follow-up messages through the wrong account. Please store/propagatemessage.adapteras the canonical per-channel adapter key instead of reconstructing it fromsource.Also applies to: 1625-1637
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agent/channel.rs` around lines 1325 - 1333, The call is using self.current_adapter() which collapses named adapters (e.g., "signal:support") to their transport only; instead propagate and use the full per-channel adapter key stored on the message (message.adapter) when invoking run_agent_turn and any other places that reconstruct adapters (also at the other call site around run_agent_turn lines ~1625-1637). Replace usages of self.current_adapter() for per-message operations with the message.adapter (or a function that returns the full adapter key from the Message struct) so named instances are preserved for registering channel tools and sending follow-ups.
♻️ Duplicate comments (10)
src/messaging/target.rs (2)
335-374:⚠️ Potential issue | 🟠 MajorReuse
normalize_signal_target()inparse_signal_target_parts().This helper still special-cases
e164/phone forms instead of normalizing them through the shared Signal normalizer. That leaves it inconsistent withparse_delivery_target():signal:e164:1234567890becomes1234567890here instead of canonical+1234567890, and bare numeric forms accepted bynormalize_signal_target()are still rejected on this path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/target.rs` around lines 335 - 374, parse_signal_target_parts currently special-cases e164/phone forms instead of reusing normalize_signal_target, causing inconsistent normalization; update parse_signal_target_parts to call normalize_signal_target for branches that handle phone/e164 or single-phone inputs (both default adapter and named adapter cases) and only construct BroadcastTarget when normalize_signal_target returns Some(normalized_target), using adapter "signal" or format!("signal:{instance}") as before; remove the duplicated phone/e164 normalization logic so numeric-only inputs accepted by normalize_signal_target are handled consistently.
104-122:⚠️ Potential issue | 🟠 MajorKeep the named Signal adapter when resolving stored channels.
This branch reparses metadata as
signal:{signal_target}, which hard-codes the default adapter. Sincesrc/messaging/signal.rs:1329-1333only persists the recipient undersignal_target, any channel whose ID encodes a named adapter (for examplesignal:<instance>:...) will resolve back tosignalhere and outbound sends can jump to the wrong daemon/account. Use the adapter encoded inchannel.idfor adapter selection and metadata only for the recipient payload.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/target.rs` around lines 104 - 122, When resolving Signal channels in this branch, do not hard-code the adapter as "signal" by calling parse_delivery_target with "signal:{signal_target}"; instead extract the adapter name from channel.id (the first part before the first ':') and combine that adapter with the recipient from platform_meta["signal_target"] so adapter selection uses the channel.id while the metadata supplies only the recipient payload; update the logic around parse_delivery_target and parse_signal_target_parts (and the parts extraction from channel.id) so that when signal_target exists you call parse_delivery_target with "<adapter>:<signal_target>" (adapter taken from channel.id) and otherwise continue to parse parts from channel.id via parse_signal_target_parts.src/tools/send_message_to_another_channel.rs (2)
288-290:⚠️ Potential issue | 🟠 MajorPlain numeric targets still hijack channel IDs.
This parser runs before
ChannelStore::find_by_name(), so"1234567890"will still be coerced intosignal:+1234567890instead of resolving a numeric channel ID. Require an explicit Signal marker here (signal:or+) and leave bare digits for normal channel lookup.Minimal fix
- // Bare phone number (7+ digits) - if trimmed.len() >= 7 && trimmed.chars().all(|c| c.is_ascii_digit()) { - return crate::messaging::target::parse_delivery_target(&format!("signal:+{trimmed}")); - } - // Group ID format: group:xxx (might be passed directly)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/send_message_to_another_channel.rs` around lines 288 - 290, The current bare-digit check in send_message_to_another_channel.rs incorrectly converts plain numeric strings to a Signal delivery target; change the logic so only explicit Signal markers are accepted: only call parse_delivery_target for inputs that either start with "signal:" or start with '+' followed by digits (e.g., "+1234567"); leave purely numeric trimmed strings untouched so ChannelStore::find_by_name() can resolve numeric channel IDs. Ensure you still construct the "signal:+{trimmed_digits}" form when handling a leading '+' case and keep parse_delivery_target usage for the explicit-signal branch.
150-156:⚠️ Potential issue | 🟠 MajorDon't overwrite explicitly named Signal adapters.
If
parse_explicit_signal_target()resolves something likesignal:work:+1555, this branch still replaces it withself.current_adapterwhenever the current conversation is on Signal. That sends through the wrong Signal account. Only inherit the current adapter for generic targets that resolved to the default"signal"adapter.Suggested guard
- if let Some(current_adapter) = self - .current_adapter - .as_ref() - .filter(|adapter| adapter.starts_with("signal")) - { - explicit_target.adapter = current_adapter.clone(); + if explicit_target.adapter == "signal" { + if let Some(current_adapter) = self + .current_adapter + .as_ref() + .filter(|adapter| adapter.starts_with("signal")) + { + explicit_target.adapter = current_adapter.clone(); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/send_message_to_another_channel.rs` around lines 150 - 156, The current logic overwrites explicitly resolved Signal adapters (e.g., "signal:work:+1555") by checking only that self.current_adapter starts_with("signal"); instead, only inherit the current adapter when the parsed target was the generic "signal". Change the guard in the block that sets explicit_target.adapter to ensure explicit_target.adapter == "signal" (and still optionally verify self.current_adapter.starts_with("signal")) before assigning self.current_adapter.clone(); reference the symbols explicit_target.adapter and self.current_adapter to locate and update the condition within the send_message_to_another_channel logic.src/messaging/signal.rs (2)
887-897:⚠️ Potential issue | 🟠 MajorStop the typing loop on HTTP failures too.
This branch only handles transport errors.
reqwest::send().awaitstill returnsOk(Response)for 4xx/5xx responses, so the typing task keeps retrying every 4 seconds even when Signal is rejecting the request.rpc_request()already handles statuses correctly.In reqwest, does RequestBuilder::send().await return Err for HTTP 4xx/5xx, or do callers need to inspect Response::status() / call error_for_status() themselves?Minimal fix
- if let Err(error) = client - .post(&rpc_url) - .timeout(RPC_REQUEST_TIMEOUT) - .header("Content-Type", "application/json") - .json(&body) - .send() - .await - { - tracing::debug!(%error, "failed to send signal typing indicator"); - break; - } + match client + .post(&rpc_url) + .timeout(RPC_REQUEST_TIMEOUT) + .header("Content-Type", "application/json") + .json(&body) + .send() + .await + { + Ok(response) if response.status().is_success() => {} + Ok(response) => { + tracing::debug!( + status = %response.status(), + "failed to send signal typing indicator" + ); + break; + } + Err(error) => { + tracing::debug!(%error, "failed to send signal typing indicator"); + break; + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/signal.rs` around lines 887 - 897, The current send() branch only treats transport errors as failures, but reqwest::RequestBuilder::send().await returns Ok(Response) for HTTP 4xx/5xx so the typing loop keeps retrying; after awaiting client.post(&rpc_url)...send().await, inspect the Response and treat non-success statuses as errors (e.g., call response.error_for_status() or check !response.status().is_success()) and log the error and break the loop similarly to the Err branch; update the block around client.post(&rpc_url)...timeout(RPC_REQUEST_TIMEOUT)...json(&body).send().await to handle and break on non-2xx responses (same behavior as rpc_request()).
105-112:⚠️ Potential issue | 🟠 Major
redact_url()still leaks embedded credentials.
https://user:pass@host/pathcurrently becomeshttps://user:pass@host, so the health-check debug log can still expose daemon credentials. Rebuild the redacted value from scheme/host/port instead of splitting on/.Suggested fix
fn redact_url(url: &str) -> String { - url.split('/') - .take(3) // scheme://domain - .collect::<Vec<_>>() - .join("/") - .to_string() + match reqwest::Url::parse(url) { + Ok(parsed) => { + let scheme = parsed.scheme(); + let host = parsed.host_str().unwrap_or("[invalid]"); + match parsed.port() { + Some(port) => format!("{scheme}://{host}:{port}"), + None => format!("{scheme}://{host}"), + } + } + Err(_) => "[invalid-url]".to_string(), + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/signal.rs` around lines 105 - 112, redact_url currently leaks embedded credentials by splitting on '/'; update redact_url to parse the input with a URL parser (e.g., Url::parse) and reconstruct the redacted string using the scheme and host (and port if present) only, explicitly omitting username/password and path/query/fragment; locate the function redact_url and replace the split/join logic with parsing and building "scheme://host" (including ":port" when Url::port_or_known_default indicates a non-default port) to ensure credentials are never included.src/config/watcher.rs (1)
233-239:⚠️ Potential issue | 🟠 MajorKeep Signal permissions on a shared
ArcSwap.When
signal_permissionsisNoneor a named instance starts here, these branches allocate a freshArcSwapand pass it only to the new adapter. The watcher never stores that handle, so later reloads stop updating that adapter's DM/group filters.Also applies to: 541-546, 572-574
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/watcher.rs` around lines 233 - 239, The watcher currently creates a fresh ArcSwap for signal permissions and only gives it to the new adapter, so later reloads don't update that adapter; change the logic around signal_permissions and SignalPermissions::from_config so that when you create a new ArcSwap (the ArcSwap holding SignalPermissions produced by SignalPermissions::from_config) you also store that ArcSwap into the shared watcher state (the signal_permissions Option) instead of only passing it to the adapter; ensure the code path that calls perms.store(Arc::new(new_perms)) always operates on the ArcSwap instance that is kept in signal_permissions (create-and-insert into signal_permissions when it was None, and reuse the existing ArcSwap when Some) so subsequent reloads update all adapters.src/api/bindings.rs (1)
157-166:⚠️ Potential issue | 🟠 MajorWire the new Signal group fields through
update_binding().These fields are still dead on the update path.
create_binding()andlist_bindings()handlegroup_ids/group_allowed_users, butupdate_binding()never writes or clears them, so Signal binding edits silently no-op.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/bindings.rs` around lines 157 - 166, The Signal-specific fields group_ids and group_allowed_users are not persisted in update_binding(), so edits to those properties silently no-op; modify update_binding() to read the incoming request's group_ids and group_allowed_users (respecting empty-to-clear semantics) and write them to the stored Binding record just like create_binding() and list_bindings() do, ensuring you handle serde/default-empty Vecs and retain require_mention/dm_allowed_users behavior consistent with the create path.src/config/types.rs (1)
1504-1510:⚠️ Potential issue | 🟠 MajorAn empty Signal DM allowlist still matches every DM.
This still behaves as allow-all when
dm_allowed_usersis empty, which contradictsSignalConfig.dm_allowed_usersand the adapter-level permission checks. It also only comparesmessage.sender_id, so a binding written with the sender's phone number won't match onceSignalAdapternormalizedsender_idto the UUID.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/types.rs` around lines 1504 - 1510, The current DM allowlist logic treats an empty dm_allowed_users as allow-all and only compares message.sender_id (a normalized UUID), which lets mismatched binding entries (e.g., phone numbers) slip through; change the logic so an empty self.dm_allowed_users results in denying the DM (return false) and, when checking membership, compare using the same normalized identifier as SignalAdapter produces (or compare both the normalized UUID and the original raw phone identifier used by bindings) so that contains(&message.sender_id) actually matches real binding entries; update the check around self.dm_allowed_users and the membership test that references message.sender_id to use the normalized id(s).src/config/permissions.rs (1)
393-419:⚠️ Potential issue | 🟠 MajorWildcard branches shouldn't short-circuit the rest of permission merging.
Each
return Self { ... }here exits before the remaining binding-derived allowlists are collected. For example,group_ids = ["*"]skips laterdm_allowed_users/group_allowed_users, anddm_allowed_users = ["*"]drops the configured group-user list. Set the wildcard for that field, then keep merging the others.Also applies to: 449-504
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/permissions.rs` around lines 393 - 419, The current wildcard handling in the group_filter/dm_allowed_users/group_allowed_users logic returns early (using return Self { ... }) when encountering "*" in seed_group_ids or binding.group_ids or dm_allowed_users, which prevents subsequent merging of allowlists; instead, modify the logic in the functions that build group_filter, dm_allowed_users, and group_allowed_users (look for usages of seed_group_ids, signal_bindings, and binding.group_ids / binding.dm_allowed_users) to set the respective field to a wildcard marker (e.g., set group_filter to Some(vec!["*".to_string())] or mark dm_allowed_users/group_allowed_users as wildcard) and then continue processing remaining bindings so other allowlists are still collected and merged; apply the same fix to the other identical block around the 449-504 region so wildcards no longer short-circuit merging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/config/types.rs`:
- Around line 1481-1502: The group-matching logic currently treats
self.group_ids and self.group_allowed_users as alternatives; change it so each
non-empty selector is required (cumulative narrowing): for the "group" branch in
the matching function, compute group_allowed only if self.group_ids is non-empty
and return false if that check fails, and likewise compute sender_allowed only
if self.group_allowed_users is non-empty and return false if that fails; use
message.metadata.get("signal_group_id") and message.sender_id as the inputs and
ensure both non-empty selectors must pass instead of using a single OR check.
In `@src/tools/send_message_to_another_channel.rs`:
- Around line 355-366: The tests use a non-canonical fake UUID so
parse_explicit_signal_target returns None; update the fixtures to use a real
canonical UUID string (for example "123e4567-e89b-12d3-a456-426655440000").
Change the parses_signal_bare_uuid test to call parse_explicit_signal_target
with that bare UUID and assert target.adapter == "signal" and target.target ==
"uuid:123e4567-e89b-12d3-a456-426655440000", and also update
parses_signal_uuid_prefixed to use
"signal:uuid:123e4567-e89b-12d3-a456-426655440000" so both tests exercise the
canonical-UUID parsing path in parse_explicit_signal_target.
---
Outside diff comments:
In `@src/agent/channel.rs`:
- Around line 1325-1333: The call is using self.current_adapter() which
collapses named adapters (e.g., "signal:support") to their transport only;
instead propagate and use the full per-channel adapter key stored on the message
(message.adapter) when invoking run_agent_turn and any other places that
reconstruct adapters (also at the other call site around run_agent_turn lines
~1625-1637). Replace usages of self.current_adapter() for per-message operations
with the message.adapter (or a function that returns the full adapter key from
the Message struct) so named instances are preserved for registering channel
tools and sending follow-ups.
---
Duplicate comments:
In `@src/api/bindings.rs`:
- Around line 157-166: The Signal-specific fields group_ids and
group_allowed_users are not persisted in update_binding(), so edits to those
properties silently no-op; modify update_binding() to read the incoming
request's group_ids and group_allowed_users (respecting empty-to-clear
semantics) and write them to the stored Binding record just like
create_binding() and list_bindings() do, ensuring you handle serde/default-empty
Vecs and retain require_mention/dm_allowed_users behavior consistent with the
create path.
In `@src/config/permissions.rs`:
- Around line 393-419: The current wildcard handling in the
group_filter/dm_allowed_users/group_allowed_users logic returns early (using
return Self { ... }) when encountering "*" in seed_group_ids or
binding.group_ids or dm_allowed_users, which prevents subsequent merging of
allowlists; instead, modify the logic in the functions that build group_filter,
dm_allowed_users, and group_allowed_users (look for usages of seed_group_ids,
signal_bindings, and binding.group_ids / binding.dm_allowed_users) to set the
respective field to a wildcard marker (e.g., set group_filter to
Some(vec!["*".to_string())] or mark dm_allowed_users/group_allowed_users as
wildcard) and then continue processing remaining bindings so other allowlists
are still collected and merged; apply the same fix to the other identical block
around the 449-504 region so wildcards no longer short-circuit merging.
In `@src/config/types.rs`:
- Around line 1504-1510: The current DM allowlist logic treats an empty
dm_allowed_users as allow-all and only compares message.sender_id (a normalized
UUID), which lets mismatched binding entries (e.g., phone numbers) slip through;
change the logic so an empty self.dm_allowed_users results in denying the DM
(return false) and, when checking membership, compare using the same normalized
identifier as SignalAdapter produces (or compare both the normalized UUID and
the original raw phone identifier used by bindings) so that
contains(&message.sender_id) actually matches real binding entries; update the
check around self.dm_allowed_users and the membership test that references
message.sender_id to use the normalized id(s).
In `@src/config/watcher.rs`:
- Around line 233-239: The watcher currently creates a fresh ArcSwap for signal
permissions and only gives it to the new adapter, so later reloads don't update
that adapter; change the logic around signal_permissions and
SignalPermissions::from_config so that when you create a new ArcSwap (the
ArcSwap holding SignalPermissions produced by SignalPermissions::from_config)
you also store that ArcSwap into the shared watcher state (the
signal_permissions Option) instead of only passing it to the adapter; ensure the
code path that calls perms.store(Arc::new(new_perms)) always operates on the
ArcSwap instance that is kept in signal_permissions (create-and-insert into
signal_permissions when it was None, and reuse the existing ArcSwap when Some)
so subsequent reloads update all adapters.
In `@src/messaging/signal.rs`:
- Around line 887-897: The current send() branch only treats transport errors as
failures, but reqwest::RequestBuilder::send().await returns Ok(Response) for
HTTP 4xx/5xx so the typing loop keeps retrying; after awaiting
client.post(&rpc_url)...send().await, inspect the Response and treat non-success
statuses as errors (e.g., call response.error_for_status() or check
!response.status().is_success()) and log the error and break the loop similarly
to the Err branch; update the block around
client.post(&rpc_url)...timeout(RPC_REQUEST_TIMEOUT)...json(&body).send().await
to handle and break on non-2xx responses (same behavior as rpc_request()).
- Around line 105-112: redact_url currently leaks embedded credentials by
splitting on '/'; update redact_url to parse the input with a URL parser (e.g.,
Url::parse) and reconstruct the redacted string using the scheme and host (and
port if present) only, explicitly omitting username/password and
path/query/fragment; locate the function redact_url and replace the split/join
logic with parsing and building "scheme://host" (including ":port" when
Url::port_or_known_default indicates a non-default port) to ensure credentials
are never included.
In `@src/messaging/target.rs`:
- Around line 335-374: parse_signal_target_parts currently special-cases
e164/phone forms instead of reusing normalize_signal_target, causing
inconsistent normalization; update parse_signal_target_parts to call
normalize_signal_target for branches that handle phone/e164 or single-phone
inputs (both default adapter and named adapter cases) and only construct
BroadcastTarget when normalize_signal_target returns Some(normalized_target),
using adapter "signal" or format!("signal:{instance}") as before; remove the
duplicated phone/e164 normalization logic so numeric-only inputs accepted by
normalize_signal_target are handled consistently.
- Around line 104-122: When resolving Signal channels in this branch, do not
hard-code the adapter as "signal" by calling parse_delivery_target with
"signal:{signal_target}"; instead extract the adapter name from channel.id (the
first part before the first ':') and combine that adapter with the recipient
from platform_meta["signal_target"] so adapter selection uses the channel.id
while the metadata supplies only the recipient payload; update the logic around
parse_delivery_target and parse_signal_target_parts (and the parts extraction
from channel.id) so that when signal_target exists you call
parse_delivery_target with "<adapter>:<signal_target>" (adapter taken from
channel.id) and otherwise continue to parse parts from channel.id via
parse_signal_target_parts.
In `@src/tools/send_message_to_another_channel.rs`:
- Around line 288-290: The current bare-digit check in
send_message_to_another_channel.rs incorrectly converts plain numeric strings to
a Signal delivery target; change the logic so only explicit Signal markers are
accepted: only call parse_delivery_target for inputs that either start with
"signal:" or start with '+' followed by digits (e.g., "+1234567"); leave purely
numeric trimmed strings untouched so ChannelStore::find_by_name() can resolve
numeric channel IDs. Ensure you still construct the "signal:+{trimmed_digits}"
form when handling a leading '+' case and keep parse_delivery_target usage for
the explicit-signal branch.
- Around line 150-156: The current logic overwrites explicitly resolved Signal
adapters (e.g., "signal:work:+1555") by checking only that self.current_adapter
starts_with("signal"); instead, only inherit the current adapter when the parsed
target was the generic "signal". Change the guard in the block that sets
explicit_target.adapter to ensure explicit_target.adapter == "signal" (and still
optionally verify self.current_adapter.starts_with("signal")) before assigning
self.current_adapter.clone(); reference the symbols explicit_target.adapter and
self.current_adapter to locate and update the condition within the
send_message_to_another_channel logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: da324a68-d864-43e9-b033-6679c394fc6b
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lock,!**/*.lockCargo.tomlis excluded by!**/*.toml
📒 Files selected for processing (23)
prompts/en/adapters/signal.md.j2prompts/en/tools/send_message_description.md.j2src/agent/channel.rssrc/agent/channel_history.rssrc/api/bindings.rssrc/config.rssrc/config/load.rssrc/config/permissions.rssrc/config/toml_schema.rssrc/config/types.rssrc/config/watcher.rssrc/conversation/worker_transcript.rssrc/llm/model.rssrc/main.rssrc/messaging.rssrc/messaging/signal.rssrc/messaging/target.rssrc/prompts/engine.rssrc/prompts/text.rssrc/secrets/scrub.rssrc/secrets/store.rssrc/tools.rssrc/tools/send_message_to_another_channel.rs
🚧 Files skipped from review as they are similar to previous changes (10)
- src/tools.rs
- src/llm/model.rs
- src/prompts/text.rs
- src/agent/channel_history.rs
- src/messaging.rs
- src/conversation/worker_transcript.rs
- src/secrets/store.rs
- src/secrets/scrub.rs
- prompts/en/adapters/signal.md.j2
- prompts/en/tools/send_message_description.md.j2
0d2bf8f to
aa8342e
Compare
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified - agent/channel.rs: Add adapter parameter to run_agent_turn, prefer InboundMessage.adapter to preserve named-instance context - api/bindings.rs: Add group_ids and group_allowed_users to UpdateBindingRequest for symmetric updates **Low:** - agent/channel_history.rs: Condition space before sender_context on non-empty value to prevent double spacing All changes pass cargo check.
09cc301 to
83b8f72
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/api/bindings.rs (1)
448-478:⚠️ Potential issue | 🟠 MajorRefresh the live Signal permission snapshot after these writes.
These new Signal-specific binding fields are persisted, but the post-write reload path in this file still only updates the Discord/Slack permission snapshots. A create/update through the API can therefore leave the running Signal adapter on stale DM/group filters until some other reload path runs. Mirror the Signal permission refresh here as well, and in the delete reload path for consistency.
Also applies to: 815-849
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/bindings.rs` around lines 448 - 478, The new Signal-specific binding fields (e.g., updates to binding_table via request.group_ids, request.channel_ids, request.dm_allowed_users, request.group_allowed_users, request.require_mention) are persisted but the post-write reload currently only refreshes Discord/Slack snapshots; update the create/update code path to also call the Signal permission snapshot refresh (same routine used elsewhere to reload Signal DM/group filters) immediately after writing these binding_table changes, and mirror the same Signal-refresh call in the delete reload path handling deletions so Signal adapter state is consistently updated after create/update/delete operations.
♻️ Duplicate comments (3)
src/main.rs (1)
1489-1525:⚠️ Potential issue | 🟠 MajorRetain a shared permissions handle for every Signal instance.
Only the default adapter’s
signal_permissionsis kept alive and passed intospawn_file_watcher(). Each named Signal instance gets a brand-newArcSwaphere and then drops the last external reference, so reloadingdm_allowed_users,group_ids, orgroup_allowed_userswill not update already-running named Signal adapters. This needs a runtime-keyed registry ofArc<ArcSwap<SignalPermissions>>values that both startup and hot-start paths reuse.Also applies to: 2995-3048
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 1489 - 1525, The code currently creates per-instance Signal permission ArcSwap values and drops external references so hot-reloads don't update running named Signal adapters; fix this by introducing a runtime-keyed registry (e.g., HashMap<String, Arc<ArcSwap<SignalPermissions>>> or a concurrent registry) that stores and reuses Arc<ArcSwap<SignalPermissions>> for every Signal instance name, ensure initialize_agents looks up/creates entries in this registry instead of creating throwaway ArcSwaps, and pass the shared Arc<ArcSwap<SignalPermissions>> handles (not ephemeral values) into spawn_file_watcher and any adapter startup paths so both initial startup and hot-start reuse the same permission handles (refer to symbols: signal_permissions, initialize_agents, spawn_file_watcher, SignalPermissions, ArcSwap).src/messaging/signal.rs (1)
913-925:⚠️ Potential issue | 🟠 MajorAvoid logging raw typing-error bodies.
This
warn!path logs the full RPC response body, which can echo Signal phone numbers, UUIDs, or account details from the daemon. For a transient typing failure, keep the warning to status only, or move a truncated/redacted body todebug.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/signal.rs` around lines 913 - 925, The warn path in the response handling for the Signal typing indicator currently logs the full response body (body_text) which may contain sensitive data; change the tracing::warn call in this block to only include the HTTP status (remove %body_text) and keep the "signal typing indicator request failed" message, and if you still want the body for debugging, emit a truncated or redacted version at debug level using tracing::debug with a safe truncation/redaction helper (referencing the same response, body_text variable and the existing warn message string to locate the block).src/config/types.rs (1)
1529-1550:⚠️ Potential issue | 🟠 MajorKeep
group_idsandgroup_allowed_userscumulative.This still matches a Signal group binding when either selector passes. A binding with both fields set currently accepts anyone in an allowed group or an allowed sender in any group, which widens routing instead of narrowing it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/types.rs` around lines 1529 - 1550, The group-matching logic in the chat_type "group" branch incorrectly allows either group_ids OR group_allowed_users to pass independently; change it so that when both self.group_ids and self.group_allowed_users are non-empty both conditions must be true, otherwise require whichever list is configured. Concretely, in the match arm handling Some("group") (around the group_allowed and sender_allowed calculations), replace the final check (currently if !group_allowed && !sender_allowed { return false; }) with: if both lists are non-empty require (group_allowed && sender_allowed) else if only group_ids is non-empty require group_allowed else if only group_allowed_users is non-empty require sender_allowed; return false when the required condition is not met.
🧹 Nitpick comments (1)
src/api/projects.rs (1)
856-856: Use a descriptive closure parameter name.The sorting refactor is correct and idiomatic, but the closure parameter
bshould use a non-abbreviated name likeentryfor clarity.As per coding guidelines: "Use non-abbreviated variable names in Rust code:
queuenotq,messagenotmsg,channelnotch".Suggested fix
- entries.sort_by_key(|b| std::cmp::Reverse(b.bytes)); + entries.sort_by_key(|entry| std::cmp::Reverse(entry.bytes));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/api/projects.rs` at line 856, Rename the closure parameter in the sort_by_key call to a non-abbreviated name for clarity: change the closure used on entries.sort_by_key(|b| std::cmp::Reverse(b.bytes)) to use a descriptive parameter like entry (e.g., entries.sort_by_key(|entry| std::cmp::Reverse(entry.bytes))) so the purpose of the variable is clear when referencing the entry.bytes field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/config/permissions.rs`:
- Around line 464-487: The config-sourced allowlists (seed_dm_allowed_users and
seed_group_allowed_users) are not trimmed before use, causing mismatches with
binding-derived trimmed entries; update the initialization of dm_users and
group_users to clone and normalize by trimming each entry and skipping empty
strings (e.g., map/iter trim + filter) so dm_wildcard and group_wildcard
detection and subsequent contains checks use trimmed values consistently with
the binding handling in the loop over signal_bindings.
In `@src/config/types.rs`:
- Around line 1542-1559: The current checks against dm_allowed_users and
group_allowed_users only test message.sender_id (which may be populated with
sourceUuid) and thus miss entries configured as phone numbers; update the
permission checks in the DM and group branches to consider both identities by
checking membership against message.sender_id, message.signal_source_number and
message.signal_source_uuid (or whichever fields hold the E164 number and UUID on
the Message struct). Concretely, replace the single contains(&message.sender_id)
tests used in the group_allowed_users / dm_allowed_users logic with a combined
predicate that returns true if any of the three identifiers are contained,
ensuring both phone-number and UUID-configured allowed users are matched.
In `@src/messaging/target.rs`:
- Around line 112-125: The code incorrectly infers a named adapter from
channel.id part count and returns an unnormalized signal_target; change the
logic in the function that builds BroadcastTarget so it (1) determines adapter
from the actual shape/prefix of signal_target (accepting exactly "uuid:...",
"group:...", or phone "+..."), (2) normalizes bare UUID reply targets by
prepending "uuid:" where needed, and (3) stops using channel.id.split count to
choose a named adapter; update the BroadcastTarget construction to use the
normalized target and adapter derived from signal_target so
SignalAdapter::broadcast will accept the value.
In `@src/tools/send_message_to_another_channel.rs`:
- Around line 111-117: The code treats any args.target starting with "signal:"
as an explicit adapter target, which incorrectly bypasses the current named
Signal adapter; instead call parse_signal_target_parts(&args.target) and use its
returned adapter value to decide: only consider it an explicit instance target
when adapter is Some(x) and x != "signal" (i.e., a named adapter like
"signal:work"); update the conditional that now uses
args.target.starts_with("signal:") to check the parsed adapter accordingly, and
make the same change in the other occurrence around the 147-160 block so
unscoped forms like signal:uuid:... still route to the current_adapter when set.
---
Outside diff comments:
In `@src/api/bindings.rs`:
- Around line 448-478: The new Signal-specific binding fields (e.g., updates to
binding_table via request.group_ids, request.channel_ids,
request.dm_allowed_users, request.group_allowed_users, request.require_mention)
are persisted but the post-write reload currently only refreshes Discord/Slack
snapshots; update the create/update code path to also call the Signal permission
snapshot refresh (same routine used elsewhere to reload Signal DM/group filters)
immediately after writing these binding_table changes, and mirror the same
Signal-refresh call in the delete reload path handling deletions so Signal
adapter state is consistently updated after create/update/delete operations.
---
Duplicate comments:
In `@src/config/types.rs`:
- Around line 1529-1550: The group-matching logic in the chat_type "group"
branch incorrectly allows either group_ids OR group_allowed_users to pass
independently; change it so that when both self.group_ids and
self.group_allowed_users are non-empty both conditions must be true, otherwise
require whichever list is configured. Concretely, in the match arm handling
Some("group") (around the group_allowed and sender_allowed calculations),
replace the final check (currently if !group_allowed && !sender_allowed { return
false; }) with: if both lists are non-empty require (group_allowed &&
sender_allowed) else if only group_ids is non-empty require group_allowed else
if only group_allowed_users is non-empty require sender_allowed; return false
when the required condition is not met.
In `@src/main.rs`:
- Around line 1489-1525: The code currently creates per-instance Signal
permission ArcSwap values and drops external references so hot-reloads don't
update running named Signal adapters; fix this by introducing a runtime-keyed
registry (e.g., HashMap<String, Arc<ArcSwap<SignalPermissions>>> or a concurrent
registry) that stores and reuses Arc<ArcSwap<SignalPermissions>> for every
Signal instance name, ensure initialize_agents looks up/creates entries in this
registry instead of creating throwaway ArcSwaps, and pass the shared
Arc<ArcSwap<SignalPermissions>> handles (not ephemeral values) into
spawn_file_watcher and any adapter startup paths so both initial startup and
hot-start reuse the same permission handles (refer to symbols:
signal_permissions, initialize_agents, spawn_file_watcher, SignalPermissions,
ArcSwap).
In `@src/messaging/signal.rs`:
- Around line 913-925: The warn path in the response handling for the Signal
typing indicator currently logs the full response body (body_text) which may
contain sensitive data; change the tracing::warn call in this block to only
include the HTTP status (remove %body_text) and keep the "signal typing
indicator request failed" message, and if you still want the body for debugging,
emit a truncated or redacted version at debug level using tracing::debug with a
safe truncation/redaction helper (referencing the same response, body_text
variable and the existing warn message string to locate the block).
---
Nitpick comments:
In `@src/api/projects.rs`:
- Line 856: Rename the closure parameter in the sort_by_key call to a
non-abbreviated name for clarity: change the closure used on
entries.sort_by_key(|b| std::cmp::Reverse(b.bytes)) to use a descriptive
parameter like entry (e.g., entries.sort_by_key(|entry|
std::cmp::Reverse(entry.bytes))) so the purpose of the variable is clear when
referencing the entry.bytes field.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0083118d-6db4-48e5-8100-cd8591396949
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lock,!**/*.lockCargo.tomlis excluded by!**/*.toml
📒 Files selected for processing (25)
prompts/en/adapters/signal.md.j2prompts/en/tools/send_message_description.md.j2src/agent/channel.rssrc/agent/channel_history.rssrc/api/bindings.rssrc/api/projects.rssrc/config.rssrc/config/load.rssrc/config/permissions.rssrc/config/toml_schema.rssrc/config/types.rssrc/config/watcher.rssrc/conversation/worker_transcript.rssrc/llm/model.rssrc/main.rssrc/messaging.rssrc/messaging/signal.rssrc/messaging/target.rssrc/prompts/engine.rssrc/prompts/text.rssrc/secrets/scrub.rssrc/secrets/store.rssrc/tools.rssrc/tools/send_message_to_another_channel.rstests/context_dump.rs
🚧 Files skipped from review as they are similar to previous changes (10)
- src/prompts/engine.rs
- src/secrets/scrub.rs
- src/tools.rs
- src/agent/channel_history.rs
- src/prompts/text.rs
- src/llm/model.rs
- prompts/en/tools/send_message_description.md.j2
- src/secrets/store.rs
- src/conversation/worker_transcript.rs
- prompts/en/adapters/signal.md.j2
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/messaging/signal.rs (1)
654-660:⚠️ Potential issue | 🟠 MajorCanonicalize UUID DM conversation IDs.
UUID-backed DMs currently become
signal:{uuid}. The shared target resolver rebuilds tracked Signal DMs fromsignal:uuid:{uuid}/signal:+..., so sending to a tracked UUID-only Signal conversation will fail once it goes through channel resolution instead of the direct reply path. The regression test below should be updated to lock the canonical shape in.🛠️ Suggested fix
let base_conversation_id = if let Some(gid) = group_id { format!("signal:group:{gid}") - } else { - // Use UUID if available (stable), fall back to phone number. - let identifier = envelope.source_uuid.as_deref().unwrap_or(&sender); - format!("signal:{identifier}") + } else if let Some(uuid) = envelope.source_uuid.as_deref() { + format!("signal:uuid:{uuid}") + } else { + format!("signal:{sender}") }; ... - assert_eq!(msg.conversation_id, "signal:uuid-1234"); + assert_eq!(msg.conversation_id, "signal:uuid:uuid-1234");Also applies to: 1843-1862
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/messaging/signal.rs` around lines 654 - 660, The DM conversation ID must canonicalize UUID-backed Signal targets to the form "signal:uuid:{uuid}" so channel resolution matches tracked IDs; update the base_conversation_id construction in signal.rs (the block that sets base_conversation_id, using group_id, envelope.source_uuid and sender) to format as "signal:uuid:{uuid}" when envelope.source_uuid is Some, otherwise fall back to "signal:{sender}" (or the phone-number form), and update the related regression test to assert the canonical "signal:uuid:{uuid}" shape for UUID DMs.
🧹 Nitpick comments (2)
src/config/types.rs (2)
2471-2484: Consider redacting phone numbers in Debug output for Signal allowed-users lists.Unlike other adapters where
dm_allowed_userscontains platform-specific IDs (Discord snowflakes, Slack user IDs, etc.), Signal's lists can contain E.164 phone numbers per the documentation. Phone numbers are generally considered more sensitive PII.This is consistent with the existing adapter pattern, so flagging as optional. If you decide to redact, a simple
&"[REDACTED]"likeaccountwould suffice.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/types.rs` around lines 2471 - 2484, The Debug impl for SignalInstanceConfig currently prints dm_allowed_users and group_allowed_users which may contain E.164 phone numbers; update the impl for SignalInstanceConfig (the std::fmt::Debug implementation) to redact those lists by replacing the .field calls that reference dm_allowed_users and group_allowed_users with a literal placeholder (e.g., &"[REDACTED]") similar to how account and http_url are redacted so phone numbers are not emitted in debug output.
1537-1542: Avoid string allocation in group ID comparison.The
contains(&gid.to_string())creates a newStringon each comparison. Use iterator-based comparison to avoid allocation.♻️ Suggested improvement
- } else { - message - .metadata - .get("signal_group_id") - .and_then(|v| v.as_str()) - .is_some_and(|gid| self.group_ids.contains(&gid.to_string())) - }; + } else { + message + .metadata + .get("signal_group_id") + .and_then(|v| v.as_str()) + .is_some_and(|gid| self.group_ids.iter().any(|id| id == gid)) + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/types.rs` around lines 1537 - 1542, The comparison currently allocates with contains(&gid.to_string()); change it to avoid allocation by comparing the &str `gid` against the stored Strings directly, e.g. replace the contains call with an iterator check like `self.group_ids.iter().any(|id| id == gid)` so the closure `|gid| ...` uses `self.group_ids.iter().any(|id| id == gid)` instead of `contains(&gid.to_string())`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/messaging/target.rs`:
- Around line 339-353: The helper extract_signal_adapter_from_channel_id
currently mis-parses named Signal channels like "signal:{instance}:uuid:{...}"
because splitting on ':' yields the third component as "uuid" or "group" (not
"uuid:{...}"); update extract_signal_adapter_from_channel_id to detect the
pattern when parts[0] == "signal" and parts[2] is "uuid" or "group" (or parts[2]
starts_with '+' / starts_with "uuid:" / "group:" to be robust) and return the
named adapter format!("signal:{instance}") in those cases; otherwise fall back
to the default "signal". Ensure you reference the function name
extract_signal_adapter_from_channel_id and the match arm handling ["signal",
instance, target_part, ..] when making the change.
In `@src/tools/send_message_to_another_channel.rs`:
- Around line 165-168: The tracing::info calls that log adapter and
broadcast_target (e.g., the one with adapter = %explicit_target.adapter,
broadcast_target = %explicit_target.target, "message sent via explicit target")
are leaking Signal recipient identifiers; update those log sites to redact or
omit the broadcast_target when explicit_target.adapter indicates Signal (or when
adapter == "signal"), e.g., replace broadcast_target with a masked value (like
"[REDACTED]" or a hash) or drop the field entirely for Signal sends; apply the
same change to the analogous tracing::info call around the other branch
mentioned (the similar block at lines ~235-238) so no successful Signal send
logs include raw recipient PII.
---
Duplicate comments:
In `@src/messaging/signal.rs`:
- Around line 654-660: The DM conversation ID must canonicalize UUID-backed
Signal targets to the form "signal:uuid:{uuid}" so channel resolution matches
tracked IDs; update the base_conversation_id construction in signal.rs (the
block that sets base_conversation_id, using group_id, envelope.source_uuid and
sender) to format as "signal:uuid:{uuid}" when envelope.source_uuid is Some,
otherwise fall back to "signal:{sender}" (or the phone-number form), and update
the related regression test to assert the canonical "signal:uuid:{uuid}" shape
for UUID DMs.
---
Nitpick comments:
In `@src/config/types.rs`:
- Around line 2471-2484: The Debug impl for SignalInstanceConfig currently
prints dm_allowed_users and group_allowed_users which may contain E.164 phone
numbers; update the impl for SignalInstanceConfig (the std::fmt::Debug
implementation) to redact those lists by replacing the .field calls that
reference dm_allowed_users and group_allowed_users with a literal placeholder
(e.g., &"[REDACTED]") similar to how account and http_url are redacted so phone
numbers are not emitted in debug output.
- Around line 1537-1542: The comparison currently allocates with
contains(&gid.to_string()); change it to avoid allocation by comparing the &str
`gid` against the stored Strings directly, e.g. replace the contains call with
an iterator check like `self.group_ids.iter().any(|id| id == gid)` so the
closure `|gid| ...` uses `self.group_ids.iter().any(|id| id == gid)` instead of
`contains(&gid.to_string())`.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f7c2af63-65a7-4f67-9198-2d59bdd02fee
📒 Files selected for processing (6)
src/api/projects.rssrc/config/permissions.rssrc/config/types.rssrc/messaging/signal.rssrc/messaging/target.rssrc/tools/send_message_to_another_channel.rs
|
@jamiepine, addressed everything by coderabbit/tembo, fmt, clippy and signal tests pass (some local test failures unrelated to the code, I believe will pass in the CI), I think this is ready |
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified - agent/channel.rs: Add adapter parameter to run_agent_turn, prefer InboundMessage.adapter to preserve named-instance context - api/bindings.rs: Add group_ids and group_allowed_users to UpdateBindingRequest for symmetric updates **Low:** - agent/channel_history.rs: Condition space before sender_context on non-empty value to prevent double spacing All changes pass cargo check.
71323f6 to
74d1e53
Compare
When an LLM call fails (e.g., provider error like StepFun's 'Unrecognized chat message'), the channel was only logging the error and sending nothing to the user. This led to confusing silent failures where the user received no response and had to check logs to understand what happened. Changes: - src/agent/channel.rs: Modified error handler to send error to user - Formats error message and sends via response_tx - User now sees: 'I encountered an error: ...' - Still logs full error for debugging This ensures users always receive feedback when something goes wrong, even if it's an internal/provider error rather than a channel bug.
Implements Signal messaging support using the signal-cli daemon HTTP API,
following the existing adapter architecture (Telegram, Discord, Slack, Twitch).
- Inbound: SSE stream with automatic reconnection and exponential backoff
(2s → 60s), UTF-8 chunk boundary handling, buffer overflow protection.
- Outbound: JSON-RPC send calls. DM recipients must be a JSON array.
- Typing indicators: JSON-RPC sendTyping with ~5s expiry.
- Attachments: Temp files in {instance_dir}/tmp/, auto-cleaned after send.
- Streaming: Not supported (Signal can't edit messages).
- Permissions: DM allowlist + group filter (None = block all groups).
Config types in types.rs, toml_schema.rs, load.rs. SignalPermissions in
permissions.rs with from_config/from_instance_config. Hot-reload support in
watcher.rs. 23 unit tests.
Add to config.toml:
```toml
[messaging.signal]
enabled = true
http_url = "http://127.0.0.1:8686"
account = "+1234567890"
dm_allowed_users = ["+0987654321"]
group_ids = ["<uuid1>", "<uuid2>"]
group_allowed_users = ["+5566778899", "+1122334455"]
ignore_stories = true
[[messaging.signal.instances]]
name = "work"
enabled = true
http_url = "http://127.0.0.1:8687"
account = "+1122334455"
dm_allowed_users = ["+5566778899"]
group_ids = ["<uuid1>", "<uuid2>"]
group_allowed_users = ["+1122334455"]
```
Requires signal-cli daemon running: `signal-cli daemon --http`
Closes spacedriveapp#310
…eapp#347) Fixes critical issues identified in code review: **Critical:** - conversation/worker_transcript.rs: Restore TranscriptStep::UserText emission to prevent user messages being mislabeled as agent text - config/permissions.rs: Separate DM and group permissions to prevent group wildcards from granting DM access - messaging/signal.rs: Move HTTP status check before empty body check to properly fail on non-2xx responses - messaging/signal.rs: Sanitize attachment filenames to prevent path traversal vulnerabilities - messaging/signal.rs: Add source_uuid fallback for sender resolution in privacy-mode envelopes **Medium:** - agent/channel.rs: Use .ok() instead of let _ = for best-effort error channel sends - config/types.rs: Add SecretField for Signal account to treat phone number as sensitive - prompts/engine.rs: Register adapters/signal template for proper Signal-specific guidance - tools/send_message_to_another_channel.rs: Only overwrite adapter when explicit target has no adapter specified - agent/channel.rs: Add adapter parameter to run_agent_turn, prefer InboundMessage.adapter to preserve named-instance context - api/bindings.rs: Add group_ids and group_allowed_users to UpdateBindingRequest for symmetric updates **Low:** - agent/channel_history.rs: Condition space before sender_context on non-empty value to prevent double spacing All changes pass cargo check.
Adds Signal-specific routing logic to Binding::matches in src/config/types.rs: - Check signal_chat_type metadata (group, dm, or private) - For group chats: validate against group_ids list OR check if sender is in group_allowed_users - For DMs: validate sender against dm_allowed_users list - Unknown chat types fail to match (return false) This fixes the issue where resolve_agent_for_message would incorrectly select the first signal binding regardless of group/DM permissions, enabling proper multi-agent routing based on Signal group membership and user allowlists.
...the fallback path. If signal_target metadata is missing, this branch only recognizes signal:uuid:*, signal:group:*, and signal:+*. Supported bare forms like signal:abc-123-def and signal:1234567890 then resolve to None even though normalize_signal_target() accepts them.
..onto whatever adapter the current conversation uses. Once parse_explicit_signal_target() returns a Signal target, this branch overwrites its adapter with current_adapter unconditionally. From a Telegram/Discord/Slack conversation, signal:+1555... would therefore be broadcast through the wrong adapter instead of Signal.
Replaces loose hyphen-based UUID detection with strict UUID format validation using uuid::Uuid::parse_str to prevent misclassifying hyphenated names as Signal UUIDs. Before: 'my-channel-name' would be treated as a Signal UUID After: Only valid 8-4-4-4-12 hexadecimal UUIDs are accepted The is_valid_uuid helper ensures proper UUID parsing before constructing the signal:uuid: target prefix.
Adds PII redaction helpers to prevent sensitive Signal identifiers from being logged in plain text: - redact_identifier(): Masks phone numbers and UUIDs, keeping only first/last 4 characters (e.g., '+123****8901' or 'abcd****wxyz') - redact_url(): Keeps only scheme and domain, removing path/query Applied to all tracing::info calls in the Signal adapter: - Group ID logging in permission rejection messages - Sender ID logging in DM/group rejection messages - Sender display in authorized message logging - Account (phone number) in connection logging - Health check URL in debug logging This prevents phone numbers, UUIDs, group IDs, and any embedded credentials in URLs from appearing in application logs.
Adds the broadcast method to SignalAdapter to enable SendMessageTool to send explicit Signal targets. The broadcast method parses target strings in uuid:xxx, group:xxx, or +xxx format into RecipientTarget and delegates to send_text/send_file. Handles Text, RichMessage, File, ThreadReply, Ephemeral, and ScheduledMessage response types. Unsupported types are logged and dropped. This fixes the no-op behavior where MessagingManager::broadcast would silently succeed without actually sending Signal messages. fix(signal): handle named adapters in resolve_broadcast_target Updates resolve_broadcast_target to properly parse Signal conversation IDs that include named adapter instances. For default adapter (signal): - signal:uuid:xxx -> adapter: signal, target: uuid:xxx - signal:group:xxx -> adapter: signal, target: group:xxx - signal:+xxx -> adapter: signal, target: +xxx For named adapters (signal:instance): - signal:instance:uuid:xxx -> adapter: signal:instance, target: uuid:xxx - signal:instance:group:xxx -> adapter: signal:instance, target: group:xxx - signal:instance:+xxx -> adapter: signal:instance, target: +xxx Also uses parse_delivery_target for signal_target metadata to ensure consistent handling with explicit target parsing. This fixes send_message_to_another_channel failing to resolve Signal channels created via named adapters.
…g issues This commit fixes six critical issues in Signal messaging handling: **1. Fix wildcard permission handling (src/config/permissions.rs)** - Wildcard handling was prematurely returning before processing all signal_bindings, preventing dm_allowed_users and group_allowed_users from bindings from being merged into the final permissions. - Changed wildcard branches to set flags and continue processing instead of early return, then build final Self after iterating all bindings. - Now wildcards finalize only their own field while allowing other lists to be combined from multiple bindings. **2. Fix inverted DM logic (src/config/types.rs)** - Signal DM handling was inverted: when dm_allowed_users was empty, DMs were being allowed (logic only checked non-empty && !contains). - Changed condition to block DMs when dm_allowed_users is empty OR when sender is not in the list. Now DMs are blocked when list is empty and only allowed for contained sender_ids. **3. Fix typing indicator status check (src/messaging/signal.rs)** - Typing indicator loop only handled transport errors and kept retrying on HTTP 4xx/5xx responses. - Changed to capture Response on success, check status().is_success(), and break loop on server failures. Follows same pattern as rpc_request(). **4. Fix parse_signal_target_parts consistency (src/messaging/target.rs)** - Function was handling e164 and phone branches directly instead of using the shared normalize_signal_target() function. - Updated phone/e164 branches to call normalize_signal_target() and use its returned canonical phone string, ensuring consistent parsing and acceptance of bare numeric forms. **5. Fix send_message_to_another_channel Signal detection (src/tools/send_message_to_another_channel.rs)** - Bare digit branch was preempting ChannelStore lookup and wrongly treating numeric channel IDs as Signal numbers. - Changed logic to require explicit Signal indicator (signal: prefix) for bare numeric strings, ensuring ChannelStore lookup remains the default resolution path for plain numeric strings. **6. Fix coalesced batch adapter preservation (src/agent/channel.rs)** - Batched turn was calling run_agent_turn with self.current_adapter() which could lose per-message adapter (e.g., signal:instance). - Changed to extract adapter from messages (preferring message.adapter) and pass that adapter to run_agent_turn, matching single-message handling behavior.
Update parses_signal_uuid_prefixed and parses_signal_bare_uuid tests in send_message_to_another_channel.rs to use valid canonical UUID strings (123e4567-e89b-12d3-a456-426655440000) instead of non-canonical fake UUIDs. This ensures the tests properly exercise the is_valid_uuid() validation logic which uses uuid::Uuid::parse_str() requiring the standard 8-4-4-4-12 hexadecimal format. Previous tests used invalid abc-123-def format which would fail actual UUID validation but were being parsed by parse_signal_target_parts without validation. Using real canonical UUIDs ensures the tests accurately test the intended code paths.
…lidation
Fixes the following issues identified in code review:
1. channel.rs: Use message.adapter instead of message.source for source_adapter
- Channel now prefers message.adapter (full adapter string like "signal:work")
- Falls back to message.source only when adapter is not present
- Batch adapter extraction now uses stored source_adapter instead of current_adapter()
- This preserves per-message adapter info for Signal named instances
2. permissions.rs: Apply same validation to seed_group_ids as bindings
- seed_group_ids now go through the same validation pipeline as signal_bindings
- Validates: trim, empty check, wildcard handling, base64 format
- Ensures both sources follow identical rules for group filter construction
3. signal.rs: Fix redact_url to properly strip userinfo
- Replaced naive split("/") parsing with proper userinfo detection
- Now correctly extracts scheme://host (or scheme://host:port)
- Discards username:password@ patterns that could leak credentials
4. signal.rs: Add source_uuid to sender extraction fallback chain
- UUID-only privacy-mode envelopes now preserve sender identity
- Fallback order: source_number -> source_uuid -> source -> "unknown"
5. target.rs: Preserve named adapter when using signal_target metadata
- When channel.id contains a named instance (e.g., "signal:work:uuid:xxx")
- And signal_target metadata exists, the adapter portion is now preserved
- Only the target portion is replaced from metadata
6. send_message_to_another_channel.rs: Respect explicit adapter in target
- Only overwrites parsed adapter when target lacks "signal:" prefix
- Prevents retargeting explicit named adapters like "signal:personal:..."
- Maintains backward compatibility for bare UUID/phone inputs
All changes pass cargo fmt, cargo check, and preflight/gate-pr checks.
fix(signal): address review feedback on permissions, routing, and logging
This commit addresses multiple issues identified in review:
1. **config/permissions.rs**: Trim seed allowed users lists
- Process seed_dm_allowed_users and seed_group_allowed_users with same
trimming/filtering as binding entries for consistency
- Map/trim each entry and filter empty strings before wildcard detection
2. **messaging/target.rs**: Fix Signal broadcast target resolution
- Add extract_signal_adapter_from_channel_id() helper
- Parse adapter from signal_target metadata format instead of relying
on channel.id.split() count (which was error-prone)
- Handle uuid:, group:, and +phone formats correctly
3. **tools/send_message_to_another_channel.rs**: Fix Signal target routing
- Only named adapters (signal:work:...) are treated as explicit targets
- Default signal:... targets now fall through to current_adapter
- Added fallback to send via current adapter when channel not found
4. **messaging/signal.rs**: Remove sensitive data from logs
- Removed body_text from warn! log (potential PII/sensitive data)
- Log truncated body (200 chars max) at debug level only
- Keep status code in warn log for troubleshooting
5. **api/projects.rs**: Rename variable for clarity
- Changed |b| to |entry| in sort_by_key closure
Signal PII leaks and conversation ID canonicalization
1. Redact Signal recipient PII in send_message_to_another_channel.rs logs
- Mask broadcast_target as "[REDACTED]" when adapter is Signal
- Applied to both explicit target and current adapter code paths
2. Redact dm_allowed_users and group_allowed_users in SignalInstanceConfig Debug
- Phone numbers in allowed user lists are now redacted in debug output
- Consistent with existing redaction of account and http_url
3. Avoid allocation in group_ids check (config/types.rs:1541)
- Replace contains(&gid.to_string()) with iter().any(|id| id == gid)
- Eliminates unnecessary string allocation during permission checks
4. Canonicalize Signal DM conversation ID to "signal:uuid:{uuid}" format
- UUID-backed targets now use explicit prefix for consistent channel resolution
- Falls back to "signal:{sender}" when UUID unavailable
- Updated regression test to assert canonical format
Also fixed pre-existing clippy warning:
- Replace metadata.get(...).is_none() with !metadata.contains_key(...)
74d1e53 to
5a3f77b
Compare
Implements Signal messaging support using the signal-cli daemon HTTP API,
following the existing adapter architecture (Telegram, Discord, Slack, Twitch).
(2s → 60s), UTF-8 chunk boundary handling, buffer overflow protection.
Config types in types.rs, toml_schema.rs, load.rs. SignalPermissions in
permissions.rs with from_config/from_instance_config. Hot-reload support in
watcher.rs. 23 unit tests.
Add to config.toml:
Requires signal-cli daemon running:
signal-cli daemon --httpCloses #310
Note
Summary: Complete Signal adapter implementation (1953 lines) following the existing messaging adapter pattern. Adds SSE-based inbound stream with exponential backoff reconnection, JSON-RPC outbound messaging, typing indicators, file attachment handling, and fine-grained permission controls (DM allowlist + group filters). Configuration schema extended for Signal with multi-instance support. Includes 23 unit tests covering JSON parsing, permissions, and SSE edge cases. No changes to core agent or Rig integration—purely additive messaging module.
Written by Tembo for commit 83b8f72. This will update automatically on new commits.