fix(security): single-source the invisible-char set; skip reply-precheck on captionless media#6426
Merged
Merged
Conversation
…echeck on captionless media Follow-ups to the #6141 prompt-injection hardening, from a post-merge review. Dedupe the invisible/format code-point set to one source of truth. The scanner (`librefang-skills::verify`), the runtime injection guard, the prompt-builder sanitizer, and the kernel prompt-context sanitizer each carried their own copy of the same 46 code points behind a "keep in sync" comment with nothing enforcing it, so a future edit to one and not the others would silently reopen the scanner bypass in the un-updated location. The canonical set now lives in `librefang_types::text::INVISIBLE_FORMAT_CHARS`; the three char-only copies alias it directly (drift is now impossible for them), and the skills labeled `(char, &str)` table — which needs a per-code-point label for its warning message — is guarded by a new equality test that fails the build if it diverges from the shared const. Skip the billed reply-precheck LLM call for captionless media. In the media dispatch gate, `group_policy=all` media ran `classify_reply_intent` on `extracted_user_text(...).unwrap_or_default()`, which is an empty string for a caption-less image/voice/video — a pointless billed classification on empty input. It now runs the precheck only when there is non-empty text (captioned media keeps parity with the text path) and otherwise proceeds, matching how bare media reached the agent before the media gate existed. This also reconciles the #6141 PR description, whose "Notes" claimed reply-precheck was deliberately not replicated onto the media path while the merged code did replicate it. Verification: `cargo clippy --all-targets -- -D warnings` green on librefang-types / -runtime / -kernel / -skills / -channels; the librefang-types text test, the librefang-skills invisible-char tests (incl. the new sync test), and the librefang-channels media / reply-intent tests pass.
…nless-media precheck fix
houko
enabled auto-merge (squash)
July 10, 2026 04:52
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.
Post-merge review follow-ups to #6141 (the prompt-injection / channel-security hardening cluster). Two findings surfaced while reviewing that PR's code; both live in the same files/domain, so they land together.
1. Invisible-char set: four copies → one source of truth (maintainability / defense-in-depth)
#6141 stripped the same set of 46 invisible/format code points in four places —
librefang-skills::verify::INVISIBLE_CHARS,librefang-runtime::injection_guard::INVISIBLE_CHARS,librefang-runtime::prompt_builder::INVISIBLE_PROMPT_CHARS, and an inline const inlibrefang-kernel::kernel::prompt_context— each behind a "keep in sync" comment with nothing enforcing it. The four were identical at merge, but a future edit to one and not the others silently reopens the scanner bypass in the un-updated location.librefang_types::text::INVISIBLE_FORMAT_CHARS— the single source of truth.librefang-skills::verifykeeps its own(char, &str)table (it emits a per-code-point label in the "Invisible unicode" warning), now guarded byverify::tests::invisible_chars_match_shared_source, which fails the build if the char set diverges from the shared const.librefang-types.2. Skip the billed reply-precheck LLM call for captionless media (wasted billed call + stale PR note)
In
media_dispatch_allowed(channels bridge),group_policy=allmedia ranclassify_reply_intentonextracted_user_text(...).unwrap_or_default()— an empty string for a caption-less image/voice/video, i.e. a billed LLM classification on empty input.group_policy=allis opt-in to respond broadly).Verification
cargo clippy -p librefang-types -p librefang-runtime -p librefang-kernel -p librefang-skills -p librefang-channels --all-targets -- -D warnings— clean.cargo test -p librefang-types text::— pass.cargo test -p librefang-skills invisible—invisible_chars_match_shared_source,test_scan_prompt_invisible_unicode,test_scan_prompt_invisible_unicode_bypasspass.cargo test -p librefang-channelsmedia / reply-intent tests (extracted_user_text_prefers_caption_over_placeholder,group_media_uses_individual_principal_and_group_policy_gate,classify_reply_intent_tests::*) pass.Not done
extracted_user_textcaptionless→Nonetest; a full end-to-end drive ofmedia_dispatch_allowedneeds aChannelAdaptermock that does not exist in the channels test module (the same limitation fix(security): close channel media RBAC bypass and audit findings #6141 documented for its own media test).