#4668
fix: enforce message outbound policy
Summary
- add an authorization hook before
messagetool outbound dispatch - wire AgentLoop to enforce channel
allow_from/group_allow_fromfor cross-target sends - confine local media attachments to workspace/media roots even when workspace restriction is disabled
Fixes #4076
Verification
uv run pytest tests/tools/test_message_tool.py::test_message_tool_blocks_unauthorized_cross_target_send tests/tools/test_message_tool.py::test_message_tool_rejects_outside_absolute_media_paths_when_unrestricted tests/tools/test_message_tool.py::test_message_tool_resolves_mixed_media_paths tests/tools/test_message_tool.py::test_message_tool_records_media_deliveries tests/tools/test_message_tool.py::test_message_tool_tracks_turn_media_for_same_target tests/tools/test_message_tool.py::test_message_tool_allows_ws_explicit_when_matches_context tests/agent/test_loop_tool_context.py::test_message_tool_authorizer_uses_channel_allowlists -quv run ruff check nanobot/agent/tools/message.py nanobot/agent/loop.py tests/tools/test_message_tool.py tests/agent/test_loop_tool_context.py
Activity
8Review — fix: enforce message outbound policy
What this PR does: Two changes:
- Outbound authorization: Adds
outbound_authorizercallback toMessageTool. Called before dispatch with(channel, chat_id, default_channel, default_chat_id). If it returns a string, send is blocked with that error.AgentLoop._authorize_message_tool_targetimplements the policy: same-target allowed; otherwise must be in channel'sallow_fromorgroup_allow_from, or be"*"wildcard, or beis_approved(pairing system). - Media path confinement:
_resolve_mediano longer has an "absolute paths allowed when unrestricted" branch. All non-URL paths go throughresolve_workspace_pathwithaccess.allowed_root or workspaceandextra_allowed_dirs=[get_media_dir()]. Outside-workspace absolute paths are rejected even whenrestrict_to_workspace=False.
What's good
- Authorizer is a callback, not hardcoded.
MessageToolis agnostic to the policy;AgentLoopwires in the specific check. Testable in isolation (the test passes a custom_authorizefunction). - Same-target fast-path.
if channel == default_channel and chat_id == default_chat_id: return None— normal replies never hit the allowlist check. No perf concern for the common case. - **
"*"`` wildcard support.** Channels with"allow_from": ["*"]` allow any target. Backwards-compatible with open-policy deployments. is_approved(pairing) is respected. Explicitly paired chats can be messaged cross-target without being inallow_from. Reasonable design — pairing IS authorization.- Media path change is correctly breaking. The old
test_message_tool_passes_through_absolute_media_pathstest is renamed and inverted totest_message_tool_rejects_outside_absolute_media_paths_when_unrestricted. Existing media tests are updated to use workspace-relative ormedia_dirpaths. test_message_tool_authorizer_uses_channel_allowliststests all four branches: same-target, allow_from match, group_allow_from match, unconfigured channel, unauthorized target. Thorough.
Concerns / questions
- Breaking change for users who relied on unrestricted absolute media paths. A user with
restrict_to_workspace: falsewho legitimately attaches files from/var/data/reports/would now be blocked. The fix is correct from a security standpoint (the issue explicitly calls this out), but should be called out in release notes with a migration path (e.g., setmedia_dirto/var/data/reports/or use symlinks). _resolve_mediachange affects ALLMessageToolinstances, not just Dream or agent-driven ones. If any external code constructs aMessageTooldirectly (e.g., a plugin), it would now have stricter media path enforcement. Probably fine, but worth flagging.get_media_dir()is called at_resolve_mediatime, not at construction. If the media dir is configured dynamically (e.g., per-tenant), this is correct. If it's static, it could be cached. Minor perf.OutboundAuthorizertype alias is defined but the type isn't exported. External plugins that want to provide their own authorizer would need to importCallabledirectly. Consider exportingOutboundAuthorizerfrom the module.test_not_suppress_when_sent_to_different_targetintest_message_tool_suppress.pyis updated to callmt.set_outbound_authorizer(lambda *_args: None)— a permissive authorizer. This is necessary because the test sends to a different target, which would now be blocked by the default authorizer. Correct fix.- No test for the
"*"wildcard case. A test with"allow_from": ["*"]and a target not otherwise in the list would lock in the wildcard behavior.
Verdict
Approve. Correct security fix with good test coverage. The breaking change for unrestricted media paths is intentional and justified — but please add a release note / migration guide for affected users.
chengyongrureviewed4mo ago
Automated PR review by nanobot. This is not a human maintainer review or approval.
I found one authorization mismatch that leaves a common configured target blocked.
hamb1yreviewed4mo ago
hamb1yreviewed4mo ago
Review — fix: enforce message outbound policy
What this PR does: Two changes:
outbound_authorizercallback toMessageTool. Called before dispatch with(channel, chat_id, default_channel, default_chat_id). If it returns a string, send is blocked with that error.AgentLoop._authorize_message_tool_targetimplements the policy: same-target allowed; otherwise must be in channel'sallow_fromorgroup_allow_from, or be"*"wildcard, or beis_approved(pairing system)._resolve_mediano longer has an "absolute paths allowed when unrestricted" branch. All non-URL paths go throughresolve_workspace_pathwithaccess.allowed_root or workspaceandextra_allowed_dirs=[get_media_dir()]. Outside-workspace absolute paths are rejected even whenrestrict_to_workspace=False.What's good
MessageToolis agnostic to the policy;AgentLoopwires in the specific check. Testable in isolation (the test passes a custom_authorizefunction).if channel == default_channel and chat_id == default_chat_id: return None— normal replies never hit the allowlist check. No perf concern for the common case."*"`` wildcard support.** Channels with"allow_from": ["*"]` allow any target. Backwards-compatible with open-policy deployments.is_approved(pairing) is respected. Explicitly paired chats can be messaged cross-target without being inallow_from. Reasonable design — pairing IS authorization.test_message_tool_passes_through_absolute_media_pathstest is renamed and inverted totest_message_tool_rejects_outside_absolute_media_paths_when_unrestricted. Existing media tests are updated to use workspace-relative ormedia_dirpaths.test_message_tool_authorizer_uses_channel_allowliststests all four branches: same-target, allow_from match, group_allow_from match, unconfigured channel, unauthorized target. Thorough.Concerns / questions
restrict_to_workspace: falsewho legitimately attaches files from/var/data/reports/would now be blocked. The fix is correct from a security standpoint (the issue explicitly calls this out), but should be called out in release notes with a migration path (e.g., setmedia_dirto/var/data/reports/or use symlinks)._resolve_mediachange affects ALLMessageToolinstances, not just Dream or agent-driven ones. If any external code constructs aMessageTooldirectly (e.g., a plugin), it would now have stricter media path enforcement. Probably fine, but worth flagging.get_media_dir()is called at_resolve_mediatime, not at construction. If the media dir is configured dynamically (e.g., per-tenant), this is correct. If it's static, it could be cached. Minor perf.OutboundAuthorizertype alias is defined but the type isn't exported. External plugins that want to provide their own authorizer would need to importCallabledirectly. Consider exportingOutboundAuthorizerfrom the module.test_not_suppress_when_sent_to_different_targetintest_message_tool_suppress.pyis updated to callmt.set_outbound_authorizer(lambda *_args: None)— a permissive authorizer. This is necessary because the test sends to a different target, which would now be blocked by the default authorizer. Correct fix."*"wildcard case. A test with"allow_from": ["*"]and a target not otherwise in the list would lock in the wildcard behavior.Verdict
Approve. Correct security fix with good test coverage. The breaking change for unrestricted media paths is intentional and justified — but please add a release note / migration guide for affected users.