Skip to content

test(relay): fail CI when a new send_message action skips the egress guard - #105731

Closed
benbarclay wants to merge 1 commit into
mainfrom
feat/relay-egress-surface-guard
Closed

benbarclay wants to merge 1 commit into
mainfrom
feat/relay-egress-surface-guard

Conversation

@benbarclay

@benbarclay benbarclay commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Why

#99220 authorizes a relay destination with a guard call at each entry point that takes a target — today action="send" and action="react"/"unreact". That shape has a failure mode no per-site test can see:

A new action with a target and no guard call is a silent bypass — every existing test still passes, because none of them know the new action exists.

This is the class-of-defect risk the removed decline latch was partly meant to cover. A CI tripwire is the cheaper answer: it catches the missing guard at review time rather than inventing turn-lifetime state to catch it at runtime.

What is actually new here

I mutation-checked this against the pre-existing test_send_message_relay_target_authz.py to avoid claiming credit it already earns. Being precise:

Mutation Existing file This file
Remove send-lane guard KILLED KILLED (duplicate)
Remove react-lane guard KILLED KILLED (duplicate)
Add a new unguarded action SURVIVES KILLED
New action misclassified read-only SURVIVES KILLED

Only the bottom two rows are new coverage. The existing suite already protects the two guard sites that exist today; it cannot protect a site that does not exist yet.

How it works

The action set is derived from SEND_MESSAGE_SCHEMA — the tool's own declaration of what a model may ask for — and each action is driven against an unattested relay destination, asserting the guard refused and nothing reached the wire. Two classification lists are hand-written; the action set is derived, so a newly declared action belongs to neither and fails.

The obvious escape hatch is closed: silencing the tripwire by classifying a new sending action as read-only also fails, because read-only actions are driven and asserted to perform no outbound act. Verified by adding a real unguarded broadcast handler and misclassifying it — test_read_only_actions_really_do_not_reach_a_destination[broadcast] fails.

Not a grep over source text. AGENTS.md bans tests that read .py files, and the reasoning applies here exactly: a regex passes when a call site is mis-wired and fails on a correct refactor. Every assertion runs the real send_message_tool entry point.

Liveness

A guard test that refuses everything is worthless. Making the guard refuse even attested destinations fails test_the_guard_admits_an_attested_destination — without that row, deleting the tool body would satisfy every other assertion in the file.

One fixture note

The send lane resolves platform config before the guard, so a fixture that only stubbed attestation made send fail with "Platform 'discord' is not configured" and never reach the check under test. The test caught itself passing for the wrong reason; the assertion requiring the refusal be the guard's is what exposed it.

409 passed across the relay + tool suites via scripts/run_tests.sh. Test-only; no production change.

Follow-up to #99220.

…guard

#99220 authorizes a relay destination with a guard call at each entry point
that takes a `target` — today `send` and `react`/`unreact`. That shape has a
failure mode no per-site test can see: a NEW action with a `target` and no
guard call is a silent bypass, and every existing test still passes because
none of them know it exists.

This derives the action set from SEND_MESSAGE_SCHEMA — the tool's own
declaration of what a model may ask for — and drives each one against an
unattested relay destination, asserting both that the guard refused it AND
that nothing reached the wire. A new action lands in neither classification
list and fails the coverage test, which is the signal to decide whether it
names a destination.

Deliberately NOT a grep over source text: AGENTS.md bans tests that read .py
files, and rightly — a regex passes when a call site is mis-wired and fails
on a correct refactor. Everything here runs the real send_message_tool entry
point and asserts on observed behaviour.

Proven falsifiable, four mutations, all killed:
- add "broadcast" to the schema enum → coverage test fails (the tripwire)
- remove the send-lane guard        → refusal test fails
- remove the react-lane guard       → refusal test fails
- make the guard refuse everything  → liveness control fails

That last one matters: without it, deleting the tool body would satisfy every
other assertion in the file.

Two fixture notes worth keeping, both found by the assertions rather than by
reading: the `send` lane resolves platform config BEFORE the guard, so a
fixture that only stubs attestation makes `send` fail with "platform not
configured" and never reach the check (the test caught itself passing for the
wrong reason); and the read-only allow-list is proven, not trusted — `list`
is driven and asserted to perform no outbound act.

Follow-up to #99220. 409 passed across the relay + tool suites.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on b94e0eb — test(relay): fail CI when a new send_message action skips th

❌ Job failures

Python lints / Windows footguns (blocking) · View job

Job Python lints / Windows footguns (blocking) failed.


⚠️ Warnings

CI timings · View report · View job

Wall time 6m50s vs 5m13s (+31.0%). 5 job(s) slower, 8 faster, 1 unchanged.

  • Detect affected areas: -33.0s
  • Python lints / ruff enforcement (blocking): -32.0s
  • OS-specific tests / Windows-only tests: +20.0s
  • Profile artifact check / Reject profile archives: -4.0s
  • OSV scan / Scan lockfiles / osv-scan: +4.0s

OSV vulnerability scan · View job

28 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

@alt-glitch alt-glitch added type/test Test coverage or test infrastructure P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets comp/gateway Gateway runner, session dispatch, delivery labels Sep 8, 2026
@benbarclay

Copy link
Copy Markdown
Contributor Author

Closing. Mutation-checked this against the pre-existing tests/tools/test_send_message_relay_target_authz.py after opening it, and the value is materially less than the PR body claimed:

  • Two of the four mutation rows are duplicate coverage. Removing either guard site (send, react) is already KILLED by the existing file. Only the "new unguarded action" rows were new.
  • It fails CI on a real rule. SEND_MESSAGE_SCHEMA is a plugin-compat pointer and in-tree code is forbidden from importing it (scripts/check_compat_pointers.py).
  • The data source is deleted on 2026-09-14. That symbol lives inside the PLUGIN-COMPAT block; the only reader anywhere in the repo was this test. Fixing the import does not help because the symbol itself is going away.

The premise was also weaker than stated: send_message is deliberately not an agent-callable model tool (tools/send_message_tool.py:22), so "the model names an arbitrary target through a new action" is not the live threat the body described. The guard still matters for cron delivery, hermes send, the kanban notifier and the MCP server — but that is narrower.

A durable version would derive the action set from a dispatch table in send_message_tool() (the thing production actually dispatches through) rather than from a shim. That is worth doing only if the "new outbound lane ships unguarded" risk resurfaces; it needs a production change to a file that just finished a long review cycle, so it is not worth it on speculation.

No production code was touched by this PR. Guard coverage on main is unchanged (#99220, 866332bfb5).

@benbarclay benbarclay closed this Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have type/test Test coverage or test infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants