Repository navigation
agent: the server-text fence is a rule, and the one door to the model - #157
Merged
Merged
Conversation
… model Re-audit of #149: 1. fenced_server_message drops by rule, not list: every character in General_Category Cc (tab, newline, CR become spaces), Cf, Co, Cn, Zl, Zp or Cs, the TAG block (ASCII smuggling), U+180E and the variation selectors. It maps by rule: any character whose NFKC form or UTS #39 confusable skeleton is a fence character, or whose Unicode name makes it a bare angle (a generated table, tools/fence_tables.rs from scripts/gen_angle_lookalikes.py: PRECEDES, the angle-bracket ornaments and presentation forms, arrowheads, ...), becomes ‹ › / or '. New deps: unicode-general-category (already in the graph), unicode-security. 2. fenced_server_message is the one place server text crosses into model-visible errors: a JSON-RPC error's message and data in tool_call_err; MCP Events' RpcError (from_json, from_service), fenced where it comes in with its code and structured data kept for the decisions made on them; rmcp's refresh errors carrying the authorization server's own text. 3. Pinned what #149 left untested: the insufficient-scope (and auth-required) challenge among a failed call's causes (describe_cause), the legacy server/discover rejection's body, the direct-HTTP stream's `$`-stripping (an e2e forge, over HTTP and stdio), and the dropped-id sequence seeded from the host's secret. 4. params_members no longer copies a server's own `$`-scalars into the host's genuine stand-in. Tests, each failing with its fix reverted (12 mutations): no_lookalike_or_invisible_spelling_ survives_the_fence (the auditor's full hostile set, plus private-use and unassigned), a_json_rpc_errors_message_and_data_are_fenced_in_the_tool_error, a_challenge_header_is_fenced_among_a_failed_calls_causes, a_servers_rpc_error_message_is_fenced_where_it_comes_in, a_refresh_failure_fences_the_authorization_servers_text, a_legacy_discover_rejections_body_is_fenced, a_servers_host_keys_forge_nothing_over_{http,stdio}, a_dropped_message_id_is_not_a_guessable_sequence, a_servers_dollar_keys_are_not_copied_into_the_stand_in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
UTS #39 maps Katakana ノ and Coptic Ⳇ to `/`, so a Japanese server message read "/ート". Letters are no longer folded to `/`; letters shaped like `<`, `>` or `"` (Canadian syllabics ᐸ/ᐳ) still are, and without a `<` the fence cannot be closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
jaredLunde
force-pushed
the
jared/fence-hardening
branch
from
October 7, 2026 15:06
c952224 to
76dada6
Compare
jaredLunde
enabled auto-merge (squash)
October 7, 2026 15:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes the items left by the re-audit of #149. Each fix has a test that fails without it. I checked this by reverting each fix in place and running its test: 12 reverts, all fail.
fenced_server_messagerelied on hand-kept lists of lookalike and invisible characters.‹ › / '. So does any character whose Unicode name makes it a bare angle: PRECEDES, the angle-bracket ornaments and presentation forms, arrowheads, and so on. Those aren't all confusable with<under UTS #39, so they come from a generated table:tools/fence_tables.rs, produced byscripts/gen_angle_lookalikes.py, which excludes compound relations such as≤and≮. New dependencies:unicode-general-category(already in the graph) andunicode-security.no_lookalike_or_invisible_spelling_survives_the_fenceruns the auditor's full hostile set (audit149/r2/zz_fence.rs) plus private-use and unassigned characters. Each case keeps exactly one real closing tag, with no</>inside the fence and no invisible, format, separator, TAG or variation-selector character left. Reverting the category drop, the angle table, or the skeleton check each fails it.fenced_server_messageis now the one place server text crosses into model-visible errors. Tool calls: a JSON-RPC error'smessageanddataintool_call_err. MCP Events:RpcError(from_json,from_service), fenced where it comes in; its code and structureddatastay as they are, for the decisions made on them. Token refresh: rmcp's refresh errors, which carry the authorization server's own text such aserror_description.a_json_rpc_errors_message_and_data_are_fenced_in_the_tool_error,a_servers_rpc_error_message_is_fenced_where_it_comes_in(from_jsonandfrom_service),a_refresh_failure_fences_the_authorization_servers_textdescribe_cause; Y5, the legacyserver/discoverrejection's body; Y10,$-stripping on direct-HTTP streams, using an end-to-end forge over HTTP and stdio (new fixture flagMCP_FIXTURE_FORGE_HOST_KEYS); Y11, the dropped-id sequence seeded from the host's secret.a_challenge_header_is_fenced_among_a_failed_calls_causes,a_legacy_discover_rejections_body_is_fenced,a_servers_host_keys_forge_nothing_over_{http,stdio},a_dropped_message_id_is_not_a_guessable_sequenceparams_memberscopied a server's own$-scalars into the host's genuine stand-in.$keys are no longer copied.a_servers_dollar_keys_are_not_copied_into_the_stand_inChecks
mcp_*suite, the serve suites,serve_harness_deadlines,tool_reactor_stall, and theagentandagent-corelib tests. The code-mode lib suite andtool_reactor_stallwith code-mode pass too (24 tests).cargo clippywith-D warningsis clean, both for the agent with code-mode and for the whole workspace. fmt and dprint are clean.check.pypasses 12/12 over HTTP and 12/12 over stdio.🤖 Generated with Claude Code
https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk