Skip to content

fix(security): single-source the invisible-char set; skip reply-precheck on captionless media#6426

Merged
houko merged 2 commits into
mainfrom
fix/6141-invisible-sync-media-precheck
Jul 10, 2026
Merged

fix(security): single-source the invisible-char set; skip reply-precheck on captionless media#6426
houko merged 2 commits into
mainfrom
fix/6141-invisible-sync-media-precheck

Conversation

@houko

@houko houko commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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 in librefang-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.

  • New canonical const librefang_types::text::INVISIBLE_FORMAT_CHARS — the single source of truth.
  • The three char-only copies now alias it directly, so drift is structurally impossible for them.
  • librefang-skills::verify keeps its own (char, &str) table (it emits a per-code-point label in the "Invisible unicode" warning), now guarded by verify::tests::invisible_chars_match_shared_source, which fails the build if the char set diverges from the shared const.
  • No new dependency: all three consumer crates already depend on 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=all media ran classify_reply_intent on extracted_user_text(...).unwrap_or_default() — an empty string for a caption-less image/voice/video, i.e. a billed LLM classification on empty input.

  • The precheck now runs only when there is non-empty text; captioned media keeps parity with the text path, captionless media proceeds without the LLM call (matching how bare media reached the agent before the media gate existed, and group_policy=all is opt-in to respond broadly).
  • Reconciles fix(security): close channel media RBAC bypass and audit findings #6141's own PR description: its "Notes" claimed reply-precheck was deliberately not replicated onto the media path, but the merged code (its second commit, "close media-gate parity gaps") did replicate it. The code comment now states the parity decision explicitly.

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 invisibleinvisible_chars_match_shared_source, test_scan_prompt_invisible_unicode, test_scan_prompt_invisible_unicode_bypass pass.
  • cargo test -p librefang-channels media / 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

  • Behavioral coverage of the captionless-media skip is by inspection plus the existing extracted_user_text captionless→None test; a full end-to-end drive of media_dispatch_allowed needs a ChannelAdapter mock 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).

…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.
@github-actions github-actions Bot added size/L 250-999 lines changed area/channels Messaging channel adapters area/skills Skill system and FangHub marketplace area/runtime Agent loop, LLM drivers, WASM sandbox area/kernel Core kernel (scheduling, RBAC, workflows) labels Jul 10, 2026
@github-actions github-actions Bot added the area/docs Documentation and guides label Jul 10, 2026
@houko
houko enabled auto-merge (squash) July 10, 2026 04:52
@houko
houko merged commit 9fdb665 into main Jul 10, 2026
34 checks passed
@houko
houko deleted the fix/6141-invisible-sync-media-precheck branch July 10, 2026 05:04
This was referenced Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/channels Messaging channel adapters area/docs Documentation and guides area/kernel Core kernel (scheduling, RBAC, workflows) area/runtime Agent loop, LLM drivers, WASM sandbox area/skills Skill system and FangHub marketplace size/L 250-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant