Skip to content

fix(security): prevent metadata spoofing of internal job monitor flag - #1195

Merged
ilblackdragon merged 1 commit into
stagingfrom
fix/job-monitor-metadata-spoofing
Mar 15, 2026
Merged

ilblackdragon merged 1 commit into
stagingfrom
fix/job-monitor-metadata-spoofing

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Summary

  • Adds is_internal: bool field to IncomingMessage that can only be set via as_internal() by trusted in-process code — external channels cannot spoof it through metadata
  • Replaces the __internal_job_monitor metadata key check in the agent loop with message.is_internal, closing a security hole where WASM channel plugins could bypass the entire agent pipeline (hooks, safety checks, LLM processing)
  • Removes notify_metadata forwarding from dispatcher (monitor only needs channel/user/thread routing, not the full original metadata blob)
  • Removes unused __job_monitor_job_id metadata key from build_internal_metadata

Fixes security issue identified in review of #1173.

Test plan

  • test_external_metadata_cannot_spoof_internal_flag — verifies with_metadata({"__internal_job_monitor": true}) does NOT set is_internal
  • test_as_internal_sets_flag — verifies as_internal() correctly sets the flag
  • Existing test_monitor_forwards_assistant_messages updated to assert is_internal instead of metadata key
  • All 3016 tests pass (pre-push hook)
  • Zero clippy warnings

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 15, 2026 06:16
@github-actions github-actions Bot added scope: agent Agent core (agent loop, router, scheduler) scope: channel Channel infrastructure scope: tool/builtin Built-in tools size: M 50-199 changed lines scope: dependencies Dependency updates risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Mar 15, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request implements a crucial security enhancement by addressing a vulnerability where external channels could potentially bypass the agent's core processing pipeline by spoofing an internal job monitor flag. The changes introduce a dedicated, internally controlled is_internal flag for messages, ensuring that only trusted components can mark messages for special handling. This fortifies the system against unauthorized access to internal message flows and maintains the integrity of the agent's operational logic.

Highlights

  • Security Fix: Internal Flag Spoofing Prevention: Introduced an is_internal: bool field to IncomingMessage that can only be set by trusted in-process code via as_internal(), effectively preventing external channels from spoofing this critical flag through metadata.
  • Agent Loop Logic Update: Replaced the __internal_job_monitor metadata key check in the agent loop with the new message.is_internal flag, ensuring that internal messages bypass the normal user-input pipeline (LLM/tool processing) securely.
  • Metadata Forwarding Refinement: Removed notify_metadata forwarding from the dispatcher, now only forwarding essential routing information (channel, user, thread) to the job monitor, rather than the full original metadata blob.
  • Cleanup of Unused Metadata: Removed the unused __job_monitor_job_id metadata key from the build_internal_metadata function.
Changelog
  • Cargo.lock
    • Updated uds_windows dependency from version 1.2.0 to 1.2.1.
  • registry/channels/discord.json
    • Bumped the version of the Discord channel registry entry from 0.2.0 to 0.2.1.
  • registry/tools/github.json
    • Bumped the version of the GitHub tool registry entry from 0.2.0 to 0.2.1.
  • registry/tools/web-search.json
    • Bumped the version of the Web Search tool registry entry from 0.2.0 to 0.2.1.
  • src/agent/agent_loop.rs
    • Added a conditional check to directly forward messages marked as is_internal, bypassing the standard LLM/tool processing pipeline.
  • src/agent/dispatcher.rs
    • Modified the job_ctx.metadata to explicitly include only notify_channel, notify_user, and notify_thread_id for job monitoring, removing the full original metadata forwarding.
  • src/agent/job_monitor.rs
    • Defined a new JobMonitorRoute struct to encapsulate channel, user, and thread IDs for routing.
    • Updated the spawn_job_monitor function to accept a JobMonitorRoute parameter.
    • Modified the creation of IncomingMessage instances within the job monitor to use the provided JobMonitorRoute and mark messages as internal using as_internal().
    • Added new test cases (test_external_metadata_cannot_spoof_internal_flag and test_as_internal_sets_flag) to verify the secure handling of the is_internal flag.
  • src/channels/channel.rs
    • Added an is_internal: bool field to the IncomingMessage struct, defaulting to false.
    • Implemented an as_internal() method for IncomingMessage to securely set the is_internal flag.
    • Initialized the is_internal field to false in the IncomingMessage::new constructor.
  • src/tools/builtin/job.rs
    • Modified the call to crate::agent::job_monitor::spawn_job_monitor to pass a JobMonitorRoute derived from the current JobContext.
    • Introduced a new helper function monitor_route_from_ctx to extract routing information from the JobContext metadata.
  • tests/e2e_routine_heartbeat.rs
    • Explicitly initialized the new is_internal field to false in IncomingMessage instances within existing end-to-end tests.
Activity
  • A security issue was identified during the review of pull request Fix subagent monitor events being treated as user input #1173, prompting the development of this fix.
  • The core of the solution involves introducing a new is_internal flag within IncomingMessage to differentiate trusted internal messages from external ones.
  • The agent's message processing logic was updated to respect this new internal flag, ensuring secure bypass of the standard LLM/tool pipeline for internal communications.
  • Metadata handling for job monitoring was refined to prevent potential spoofing and ensure only necessary routing information is passed.
  • Comprehensive unit and integration tests were added and updated to validate the correct behavior of the is_internal flag and confirm the security fix.
  • All 3016 existing tests passed, and no Clippy warnings were introduced, indicating a high standard of code quality and stability.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a security fix to prevent metadata spoofing of an internal job monitor flag. It achieves this by adding an is_internal boolean field to IncomingMessage, which can only be set by trusted code via a new as_internal() method. The agent loop now correctly checks this flag to bypass the normal processing pipeline for internal messages. This is a robust solution to the security vulnerability. The implementation is clean, and the inclusion of new tests to verify the fix and prevent regressions is excellent.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Closes a security gap where external channel-provided metadata could spoof an “internal job monitor” message and bypass the normal agent pipeline by introducing an explicit internal flag on IncomingMessage and using it in the agent loop.

Changes:

  • Adds IncomingMessage.is_internal plus as_internal() and updates call sites/tests accordingly.
  • Reworks job monitor injection to use explicit route context (channel/user/thread) and marks injected messages as internal; agent loop now short-circuits on message.is_internal.
  • Bumps registry manifest versions to 0.2.1 and updates Cargo.lock.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/channels/channel.rs Adds is_internal to IncomingMessage and an as_internal() setter.
src/agent/agent_loop.rs Forwards internal messages directly, bypassing LLM/tool loop.
src/agent/job_monitor.rs Adds JobMonitorRoute, injects routed internal messages, updates/extends tests.
src/tools/builtin/job.rs Spawns job monitor with explicit route from JobContext metadata; skips if missing.
src/agent/dispatcher.rs Populates JobContext.metadata with notify route fields for monitoring.
tests/e2e_routine_heartbeat.rs Updates IncomingMessage struct literals to include is_internal.
registry/tools/web-search.json Version bump to 0.2.1.
registry/tools/github.json Version bump to 0.2.1.
registry/channels/discord.json Version bump to 0.2.1.
Cargo.lock Updates uds_windows to 1.2.1.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/channels/channel.rs Outdated
}

/// Mark this message as internal (bypasses user-input pipeline).
pub fn as_internal(mut self) -> Self {
Comment thread src/channels/channel.rs Outdated
Comment on lines 90 to 93
pub is_internal: bool,
}

impl IncomingMessage {
Comment thread src/agent/agent_loop.rs Outdated
Comment on lines +756 to +757
// The `is_internal` flag can only be set by trusted in-process code
// (not via metadata), so external channels cannot spoof it.
@ilblackdragon
ilblackdragon force-pushed the fix/job-monitor-metadata-spoofing branch from 6d5b3b3 to 80a0305 Compare March 15, 2026 07:09
zmanian
zmanian previously approved these changes Mar 15, 2026

@zmanian zmanian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: APPROVE

Well-executed security fix for a real vulnerability. WASM channel plugins could set a magic metadata key (__internal_job_monitor) to bypass the entire agent pipeline — hooks, safety checks, LLM processing.

The fix: Replaces metadata-based check with a typed is_internal: bool field on IncomingMessage that defaults to false and can only be set via as_internal(), called only from trusted in-process code (job_monitor.rs).

Bypass vectors checked — none found:

  • IncomingMessage doesn't derive Deserialize — no wire-level injection
  • WASM wrapper uses IncomingMessage::new() which defaults to false
  • with_metadata() doesn't touch is_internal
  • Struct literal sites verified: thread_ops.rs clones existing message, test sites set false

Additional defense in depth: Removes forwarding of full metadata blob to job monitor, replacing with typed JobMonitorRoute struct carrying only routing fields.

Minor suggestions (non-blocking):

  • is_internal is pub — consider pub(crate) or a comment noting it should only be set via as_internal()
  • Brief comment on why notify_user has a fallback but notify_channel doesn't in monitor_route_from_ctx

CI all green.

Copilot AI review requested due to automatic review settings March 15, 2026 20:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens internal message handling by replacing metadata-based “internal job monitor” detection with a non-spoofable IncomingMessage::is_internal flag, ensuring external channels/plugins can’t bypass the normal agent pipeline.

Changes:

  • Add is_internal to IncomingMessage and an as_internal() constructor-style setter for trusted in-process injection.
  • Update job monitor injection to route notifications back to the originating channel/user/thread and mark them internal.
  • Update agent loop to short-circuit internal messages (bypass LLM/tools/hooks pipeline) and adjust tests to use the new constructor APIs.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/e2e_routine_heartbeat.rs Switches test message construction to IncomingMessage::new(...).
src/tools/builtin/job.rs Adds route extraction from JobContext metadata and passes it into the job monitor spawn.
src/channels/channel.rs Introduces IncomingMessage.is_internal and as_internal(); initializes flag in new().
src/agent/job_monitor.rs Adds JobMonitorRoute, routes monitor output to real channel/user/thread, and marks injected messages internal; adds regression tests.
src/agent/dispatcher.rs Populates JobContext.metadata with notify routing keys for job-monitor/message-tool routing.
src/agent/agent_loop.rs Replaces metadata key check with message.is_internal and forwards internal content directly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/agent/dispatcher.rs
Comment on lines +147 to +149
"notify_channel": message.channel,
"notify_user": message.user_id,
"notify_thread_id": message.thread_id,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That Copilot comment is wrong. serde_json::json! doesn't move values — it calls serde_json::to_value()
which works with references via Serialize. Since String and Option implement Serialize, the
macro serializes from &message.channel etc. without moving.

@zmanian
zmanian enabled auto-merge (squash) March 15, 2026 20:50
The `__internal_job_monitor` metadata key that bypassed the entire
agent pipeline (hooks, safety checks, LLM processing) was spoofable
by external channels — WASM channel plugins could inject arbitrary
metadata including this key, causing attacker-controlled content to be
forwarded directly as assistant responses.

Replace the metadata-based check with a dedicated `is_internal` field
on `IncomingMessage` that can only be set via `into_internal()` by
trusted in-process code. Both the field and setter are `pub(crate)` to
prevent external crates from spoofing the flag. Also remove
`notify_metadata` forwarding (the monitor only needs channel/user/thread
routing) and the unused `__job_monitor_job_id` metadata key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ilblackdragon
ilblackdragon force-pushed the fix/job-monitor-metadata-spoofing branch from ddb9942 to 46a588e Compare March 15, 2026 21:24
@ilblackdragon
ilblackdragon disabled auto-merge March 15, 2026 21:32
@ilblackdragon
ilblackdragon merged commit bde0b77 into staging Mar 15, 2026
14 checks passed
@ilblackdragon
ilblackdragon deleted the fix/job-monitor-metadata-spoofing branch March 15, 2026 21:33
@ironclaw-ci ironclaw-ci Bot mentioned this pull request Mar 17, 2026
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
…nearai#1195)

The `__internal_job_monitor` metadata key that bypassed the entire
agent pipeline (hooks, safety checks, LLM processing) was spoofable
by external channels — WASM channel plugins could inject arbitrary
metadata including this key, causing attacker-controlled content to be
forwarded directly as assistant responses.

Replace the metadata-based check with a dedicated `is_internal` field
on `IncomingMessage` that can only be set via `into_internal()` by
trusted in-process code. Both the field and setter are `pub(crate)` to
prevent external crates from spoofing the flag. Also remove
`notify_metadata` forwarding (the monitor only needs channel/user/thread
routing) and the unused `__job_monitor_job_id` metadata key.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drchirag1991 pushed a commit to drchirag1991/ironclaw that referenced this pull request Apr 8, 2026
…nearai#1195)

The `__internal_job_monitor` metadata key that bypassed the entire
agent pipeline (hooks, safety checks, LLM processing) was spoofable
by external channels — WASM channel plugins could inject arbitrary
metadata including this key, causing attacker-controlled content to be
forwarded directly as assistant responses.

Replace the metadata-based check with a dedicated `is_internal` field
on `IncomingMessage` that can only be set via `into_internal()` by
trusted in-process code. Both the field and setter are `pub(crate)` to
prevent external crates from spoofing the flag. Also remove
`notify_metadata` forwarding (the monitor only needs channel/user/thread
routing) and the unused `__job_monitor_job_id` metadata key.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: agent Agent core (agent loop, router, scheduler) scope: channel Channel infrastructure scope: dependencies Dependency updates scope: tool/builtin Built-in tools size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants