Skip to content

fix(gateway): harden internal background events - #18099

Closed
hanzckernel wants to merge 12 commits into
NousResearch:mainfrom
hanzckernel:fix/internal-event-trust-boundary
Closed

fix(gateway): harden internal background events#18099
hanzckernel wants to merge 12 commits into
NousResearch:mainfrom
hanzckernel:fix/internal-event-trust-boundary

Conversation

@hanzckernel

Copy link
Copy Markdown
Contributor

What does this PR do?

Gateway/CLI background events now carry explicit trusted-internal metadata before they can bypass normal gateway policy. Process output is framed as observation text, not as an authoritative system instruction.

This keeps the previous legitimate synthetic-event flows working, but stops a bare mutable MessageEvent.internal=True flag or user text that looks like a system/background message from becoming the trust boundary.

Related Issue

Fixes #9290

Related / comparison:

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • gateway/platforms/base.py
    • Add InternalEventKind, trusted internal event sources, and MessageEvent.is_trusted_internal().
    • Keep internal as a legacy marker, but stop treating it as sufficient for privileged gateway bypass.
  • gateway/run.py
    • Gate internal bypass behavior on event.is_trusted_internal().
    • Mark gateway-created background completion/watch events with trusted kind/source metadata.
    • Reframe background process notifications as [Background process observation: ...] and strip ANSI/control sequences from process-supplied command/output text.
    • Make notification mode off suppress synthetic agent wakeups while still draining/consuming queued process notifications.
  • cli.py
    • Add shared background notification mode loading/normalization and queue-drain helper.
    • Use the helper on both pre-agent and post-agent drain paths so off does not append synthetic pending input.
  • gateway/platforms/yuanbao.py
    • Mark message-recall internal events with trusted platform-adapter metadata.
  • Tests
    • Add regressions for trusted internal events, bare internal=True rejection, pre-gateway hook behavior, notification-off no-wakeup/no-pending-input behavior, sanitized observation text, and gateway env/import isolation.

How to Test

Using the project venv, run:

scripts/run_tests.sh \
  tests/gateway/test_internal_event_bypass_pairing.py \
  tests/gateway/test_pre_gateway_dispatch.py \
  tests/gateway/test_background_process_notifications.py \
  tests/gateway/test_config.py::TestLoadGatewayConfig::test_bridges_quoted_false_platform_enabled_from_config_yaml \
  tests/cli/test_cli_background_tui_refresh.py \
  -q

python -m compileall -q \
  cli.py gateway/platforms/base.py gateway/platforms/yuanbao.py gateway/run.py \
  tests/cli/test_cli_background_tui_refresh.py \
  tests/gateway/test_background_process_notifications.py \
  tests/gateway/test_internal_event_bypass_pairing.py \
  tests/gateway/test_pre_gateway_dispatch.py

python -m ruff check --select F401 \
  cli.py gateway/platforms/base.py gateway/platforms/yuanbao.py gateway/run.py \
  tests/cli/test_cli_background_tui_refresh.py \
  tests/gateway/test_background_process_notifications.py \
  tests/gateway/test_internal_event_bypass_pairing.py \
  tests/gateway/test_pre_gateway_dispatch.py

git diff --cached --check

ulimit -n 4096
python -m pytest -o addopts='' tests/gateway -q
python -m pytest -o addopts='' tests/cli -q

Local results on this branch:

  • Repo wrapper targeted gateway/CLI regression set: 51 passed in 3.70s
  • compileall: passed
  • ruff --select F401: passed
  • git diff --cached --check: passed
  • Full gateway suite: 4288 passed, 10 skipped, 181 warnings in 269.97s
  • Full CLI suite: 590 passed, 3 warnings in 20.20s
  • Full repo suite via scripts/run_tests.sh tests/ -q: attempted locally but not green in this checkout; failures were outside this PR's touched gateway/CLI slice, so this is not claimed as a passing signal.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS, Python 3.11 local venv

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A; behavior is covered by tests and existing config/docs for notification modes stay the same
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A; no new config key
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A; no contributor workflow change
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — uses existing dataclass/string/env handling and ANSI stripping helper
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A; no tool schema change

For New Skills

N/A.

Screenshots / Logs

N/A. This is CLI/gateway control-flow and regression-test coverage.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard labels Apr 30, 2026
@egilewski

Copy link
Copy Markdown
Contributor

merge conflicts

This PR does not merge cleanly with the base branch. Please rebase or merge current main and resolve the conflicts if it's still relevant.

Signed: GPT-5.5-medium in Codex

# Conflicts:
#	gateway/run.py
#	tests/test_tui_gateway_server.py
#	tests/tools/test_process_registry.py
#	tools/process_registry.py

@egilewski egilewski 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.

requesting changes

Blocking check:

  • git diff --check refs/remotes/origin/main...HEAD reports trailing whitespace in gateway/run.py:12677 on the PR head. This needs to be cleaned up before merge.

I otherwise validated the security direction: a forged MessageEvent(internal=True) reaches the agent path on current main, while the PR head rejects the same bare internal flag before agent handling, and the targeted gateway/process-registry tests pass. The remaining blocker is the repository whitespace check above.

Signed: GPT-5.5-xhigh in Codex

@egilewski egilewski 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.

looks mergeable

The previous blocking check is fixed: git diff --check refs/remotes/origin/main...refs/remotes/origin/pr/18099 is clean on head dd6b9d6055c4bfb4d1fd31d46ce5bf91c37526c3, including the earlier gateway/run.py whitespace area.

Security validation:

  • git merge-tree --write-tree refs/remotes/origin/main refs/remotes/origin/pr/18099 succeeds against current main 2944b3c394e3fe56cadbadd073a7fa54b24f3ba5.
  • A current-main replay commit with parents 2944b3c394e3fe56cadbadd073a7fa54b24f3ba5 and dd6b9d6055c4bfb4d1fd31d46ce5bf91c37526c3 passes the focused gateway/process-registry suite: tests/gateway/test_internal_event_bypass_pairing.py, tests/gateway/test_background_process_notifications.py, tests/gateway/test_pre_gateway_dispatch.py, tests/tools/test_process_registry.py, and tests/test_tui_gateway_server.py (401 passed).
  • ruff check passes on the changed files in that current-main replay.

Worth improving: notification-mode normalization could be friendlier for unusual boolean/env aliases, but I do not see that as a merge blocker for this security fix.

Signed: GPT-5.5-xhigh in Codex

@egilewski egilewski 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.

requesting changes

Blocking check:

  • display.background_process_notifications: off now also suppresses completed delegate_task(background=true) results in the gateway. _async_delegation_watcher() still routes type="async_delegation" completions through _inject_watch_notification(), but this PR added an unconditional off return at the top of that helper. That fixes watch-pattern/process wakeups by dropping them, but it also drops async delegation completions, which are not the process-watch notification path from #9290.

Reproduction:

# Current main with config.yaml setting display.background_process_notifications: off
# and an async_delegation event routed through _inject_watch_notification:
# adapter.handle_message await_count == 1

# PR head af21b9b8a2d2a772c2e29e9167b61bc944bcdd10 with the same setup:
# adapter.handle_message await_count == 0

The fix should keep off from injecting background process/watch notifications while preserving async delegation completion delivery, or route async delegation through a helper that is not gated by the background-process notification setting.

Signed: GPT-5-medium in Codex

@egilewski egilewski 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.

looks mergeable

I reviewed the latest fix against current GitHub main 5378b941209d8f62a65455041658ce8ce8144cc9, PR base 81eaedd0f5c471c7ee748990066135a684f3c962, and PR head 01fba5887ce1fdf681594d2103b0252750eebc62.

Security evidence:

  • trust boundary: gateway-internal synthetic process/delegation events must be gateway-originated and routed from trusted event metadata, not from untrusted user messages or ambient foreground session state.
  • source/sink/invariant: display.background_process_notifications=off must suppress background process/watch notifications while still delivering completed delegate_task(background=true) results as trusted gateway-owned completion events.
  • current-main reproduction: the original forged bare internal=True event bypass remains covered by the PR's internal-event tests; the prior review blocker was the old PR head dropping async delegation completions when notifications were off.
  • PR-head validation: this head routes async delegation through _inject_async_delegation_completion(..., suppress_when_background_notifications_off=False) while leaving watch/process notification injection gated by off.
  • positive/negative cases: focused tests cover both sides: watch notifications are suppressed in off, and async delegation completions still reach the originating Telegram session with InternalEventKind.BACKGROUND_COMPLETION.
  • residual bypass search: checked the shared injection helper, async-delegation watcher, routing enrichment, and foreground-event rejection paths; no remaining same-class bypass or completion-drop path found in the reviewed scope.
  • reviewer-tool status: CodeRabbit completed with no findings.

Validation passed:

  • git merge-tree --write-tree refs/remotes/origin/main refs/remotes/origin/pr/18099
  • git diff --check refs/remotes/origin/main...refs/remotes/origin/pr/18099
  • PYTHONPATH=<run-root>/deps /home/mac/hermes-agent/.venv/bin/python -B -m pytest -o addopts='' -p no:cacheprovider tests/gateway/test_internal_event_bypass_pairing.py tests/gateway/test_background_process_notifications.py tests/gateway/test_pre_gateway_dispatch.py tests/tools/test_process_registry.py tests/test_tui_gateway_server.py tests/tools/test_delegate.py tests/tools/test_notify_on_complete.py tests/cli/test_cli_background_tui_refresh.py -q (587 passed)
  • /home/mac/hermes-agent/.venv/bin/python -B -m compileall -q cli.py gateway/run.py gateway/platforms/base.py gateway/platforms/msgraph_webhook.py gateway/platforms/yuanbao.py tools/process_registry.py tests/gateway/test_background_process_notifications.py tests/tools/test_delegate.py tests/tools/test_notify_on_complete.py
  • /home/mac/hermes-agent/.venv/bin/python -B -m ruff check cli.py gateway/run.py gateway/platforms/base.py gateway/platforms/msgraph_webhook.py gateway/platforms/yuanbao.py tools/process_registry.py tests/gateway/test_background_process_notifications.py tests/tools/test_delegate.py tests/tools/test_notify_on_complete.py

The first focused test run failed one PTY stdin helper because the shared venv lacked the declared ptyprocess dependency; the same failure reproduced on current main. After installing declared ptyprocess==0.7.0 into the run-root dependency target only, the focused suite passed.

Signed: GPT-5.5-xhigh in Codex

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jun 29, 2026
@teknium1

teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for the thorough work here, @hanzckernel — the reframing of background-process output as non-authoritative observation text and the ANSI/control sanitization are nicely done.

We're going to close this one, though, because the core security premise doesn't hold against how gateway events are actually built.

The MessageEvent.internal flag is only ever set to True by gateway/adapter code that forges a synthetic event from a trusted trigger (background-process completion in gateway/run.py, message-recall in yuanbao.py, Graph subscription in msgraph_webhook.py, etc.). Inbound user messages are constructed by the platform adapter from the wire payload — the user controls event.text, never the internal flag, and there's no deserialization path that maps attacker-controllable data onto internal=True. So the trusted-kind / trusted-source / is_trusted_internal() machinery guards a boundary that a user can't reach, and some of the added InternalEventKind values (WEBHOOK_NOTIFICATION, AUTO_RESUME, CLI_HANDOFF) aren't wired to any producer.

The one genuinely in-scope behavior — making background_process_notifications: off actually suppress synthetic watch-pattern wakeups (issue #9290) — is a real P2 bug and is already covered by the narrower, earlier PR #9341, which we'll review separately.

Appreciate the effort, and please keep the contributions coming.

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

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: background_process_notifications=off still injects synthetic watch-pattern SYSTEM messages into conversations

4 participants