Skip to content

fix(channels): make the untrusted-turn tool ceiling configurable - #1219

Merged
penso merged 1 commit into
moltis-org:mainfrom
vikng-dev:group-tools-opt-in
Aug 20, 2026
Merged

fix(channels): make the untrusted-turn tool ceiling configurable#1219
penso merged 1 commit into
moltis-org:mainfrom
vikng-dev:group-tools-opt-in

Conversation

@vikng-dev

@vikng-dev vikng-dev commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

#1170 gave every turn that is not an operator in a proven direct chat a
hardcoded deny-all tool policy on top of the public audience ceiling. That
was right for /sh, but it also removed the three tools registered for the
public audience, and it made tool policy layers 4 and 5 unreachable in any
shared chat, which is the case they were added for in #677.

Two new per-account fields carry the ceiling instead of hardcoding it:

untrusted_audience = "public" | "trusted" (default "public")
untrusted_tools = "deny_all" | "policy" (default "deny_all")

The defaults reproduce the current behaviour exactly, so an account with no
configuration is unchanged. WhatsApp is the only channel that reads them so
far; the other eight fall back to the defaults, which is the unconfigured
behaviour.

/sh keeps its own gate, tied to the operator-direct-chat test and not to the
ceiling. It cannot follow the tool policy the way the agent's exec call does:
run_explicit_shell_command takes "exec" straight from the request registry,
and the [tools.policy] layers are applied in apply_runtime_tool_filters on
the agent-run path, which /sh returns before reaching. A guard that widened
with the ceiling would hand out an exec that no deny can take back.

_private_context stays off for every untrusted turn and is not configurable.
Owner memory, profile and project context describe the owner rather than the
conversation, so a room with other people in it never receives them.

🤖 Generated with Claude Code

@vikng-dev

Copy link
Copy Markdown
Contributor Author

Heads up for whoever merges: this and #1218 both insert a field into the same spot in crates/whatsapp/src/config.rs (right after group_allowlist), so whichever lands second will need a one-line textual rebase. They are independent otherwise.

@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes the untrusted-turn tool ceiling configurable per WhatsApp account while preserving fail-closed defaults and the independent /sh restriction.

  • Adds serialized audience and name-policy ceiling settings.
  • Applies account-specific ceilings during channel dispatch.
  • Keeps private context disabled for untrusted turns.
  • Adds focused behavior tests and WhatsApp reference documentation.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
crates/channels/src/config_view.rs Adds shared serialized enums and fail-closed default accessors for untrusted-turn ceilings.
crates/gateway/src/channel_events.rs Resolves account ceilings and applies the selected audience, tool policy, and private-context restrictions.
crates/gateway/src/channel_events/dispatch.rs Integrates account-specific ceilings while retaining the operator-direct-chat shell-command gate.
crates/gateway/src/channel_events/tests.rs Covers default equivalence, independent ceiling axes, and the lifted-ceiling request shape.
crates/whatsapp/src/config.rs Persists and exposes the two new per-account WhatsApp ceiling settings.
docs/src/channels.md Explains how untrusted ceilings interact with channel tool policies and the /sh restriction.
docs/src/whatsapp.md Adds the accepted values and defaults to the WhatsApp account reference.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Inbound channel turn] --> B{Operator in proven direct chat?}
  B -->|Yes| C[Use normal tool policy]
  B -->|No| D[Load account untrusted ceiling]
  D --> E[Apply audience ceiling]
  E --> F[Apply deny-all or configured policy]
  F --> G[Disable private context]
  B -->|No and /sh| H[Reject explicit shell command]
Loading

Reviews (2): Last reviewed commit: "fix(channels): make the untrusted-turn t..." | Re-trigger Greptile

Comment thread crates/channels/src/config_view.rs
Comment thread crates/whatsapp/src/config.rs
moltis-org#1170 gave every turn that is not an operator in a proven direct chat a
hardcoded deny-all tool policy on top of the public audience ceiling. That
was right for /sh, but it also removed the three tools registered for the
public audience, and it made tool policy layers 4 and 5 unreachable in any
shared chat, which is the case they were added for in moltis-org#677.

Two new per-account fields carry the ceiling instead of hardcoding it:

  untrusted_audience = "public" | "trusted"    (default "public")
  untrusted_tools    = "deny_all" | "policy"   (default "deny_all")

The defaults reproduce the current behaviour exactly, so an account with no
configuration is unchanged. WhatsApp is the only channel that reads them so
far; the other eight fall back to the defaults, which is the unconfigured
behaviour.

/sh keeps its own gate, tied to the operator-direct-chat test and not to the
ceiling. It cannot follow the tool policy the way the agent's exec call does:
run_explicit_shell_command takes "exec" straight from the request registry,
and the [tools.policy] layers are applied in apply_runtime_tool_filters on
the agent-run path, which /sh returns before reaching. A guard that widened
with the ceiling would hand out an exec that no deny can take back.

_private_context stays off for every untrusted turn and is not configurable.
Owner memory, profile and project context describe the owner rather than the
conversation, so a room with other people in it never receives them.

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

Copy link
Copy Markdown
Contributor Author

Correction to the original description, and a force-push that fixes it.

The first version of this PR gated /sh on the configured ceiling rather than on the operator-direct-chat test, and argued that this was safe because "run_explicit_shell_command resolves exec from the same request-scoped registry the agent uses, so a turn whose policy denies exec already cannot run /sh."

The first half of that is true and the second half is not. With untrusted_audience = "trusted" and untrusted_tools = "policy" no _tool_policy is injected, so:

  • resolve_request_tool_registry short-circuits on policy.is_none() && audience == Trusted and hands back the unfiltered shared registry.
  • The only policy check on that path, in send.rs, is request_tool_policy.as_ref().is_some_and(|p| !p.is_allowed("exec")), which is false for None and so passes.
  • The configured [tools.policy] layers never enter it at all: resolve_effective_policy has one caller, inside apply_runtime_tool_filters, and every caller of that is an agent-run path. /sh returns before reaching any of them.

Net effect in that configuration: any group member could run shell commands, and a deny = ["exec"] on the group would not have stopped them. That removed the only working gate and replaced it with one that does not exist.

/sh now keeps its original gate. The ceiling still does what this PR is for, which is making policy layers 4 and 5 reachable on the agent path in shared chats. Making /sh follow those layers would mean resolving the effective policy in send.rs before run_explicit_shell_command, in a different crate, and belongs in its own change.

Also in this push, from the review feedback:

  • docs/src/channels.md said per-channel policies "cannot enable tools for a guest, shared room, or unknown chat". This PR made that false, so it now describes both cases.
  • docs/src/whatsapp.md gains rows for both new fields.
  • A test pins the fact that the lifted ceiling leaves no request-side tool policy, which is the reason the /sh guard has to stay separate.
  • Noted in the description that WhatsApp is the only channel reading these fields so far; the other eight fall back to the defaults, which is the unconfigured behaviour. Wiring them is a separate change.

crates/config/src/template.rs is deliberately untouched: it carries no WhatsApp account block, only the channel name in an offered array, so there is nothing to extend without inventing an example section this PR did not open.

@vikng-dev

Copy link
Copy Markdown
Contributor Author

Updating the earlier merge-order note: this and #1218 now overlap in two files rather than one. Both insert a field after group_allowlist in crates/whatsapp/src/config.rs, and both append a row after otp_cooldown_secs in the configuration table in docs/src/whatsapp.md. Whichever lands second needs a two-hunk textual rebase, no semantic overlap. #1217 merges cleanly with both.

@penso

penso commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

@greptileai review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants