Sanitize and demarcate untrusted catalog and tool-result text in rendered prompt surfaces
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 9
- Forks
- 17
- Avg merge
- 21h 36m
- Merged PRs (30d)
- 22
Description
Summary
Add a deterministic sanitization and demarcation pass for externally sourced text (tool names, descriptions, tool-result summaries) before it is rendered into prompt surfaces such as ChoiceCard text and firewall summaries. Today this text is length-truncated but otherwise rendered verbatim.
Why this matters
Tool catalogs and tool results frequently come from third-party servers the agent operator does not control. Text from those sources ends up inside the prompts that drive routing and answering. A normalization pass (control-character stripping, Unicode confusable/bidi normalization, newline collapsing) plus clear demarcation of untrusted spans strengthens prompt-injection resistance for every gateway and adapter user, and gives the documented threat model (#396) an enforcement mechanism rather than guidance alone.
Current evidence
src/contextweaver/routing/cards.py—render_cards_text()interpolatescard.descriptiondirectly into the rendered line ([{idx}/{total}] {card.id} ({card.kind}) — {card.description}…); truncation is token-budget based (sentence-boundary truncation around lines 111–153) with no character-class filtering.src/contextweaver/adapters/mcp.py—mcp_tool_to_selectablestores upstreamname/descriptionverbatim; the module already documents that MCP annotations are unverified server-declared hints (lines 138–152).- Firewall summaries originate from raw tool output (
src/contextweaver/context/firewall.py) and are rendered inline into compiled prompts. - Open issues #428 (secret redaction) and #396 (threat-model docs) are adjacent but do not cover text normalization or untrusted-span demarcation.
Proposed implementation
- Add a
sanitize_untrusted_text()primitive (likely_utils.pyor a new small module): strip C0/C1 control characters (preserving\n/\twhere meaningful), apply NFKC normalization, remove bidi override and zero-width characters, collapse repeated newlines. - Apply it at the two trust boundaries: catalog ingest (adapters producing
SelectableItem) and firewall summary/fact production. Keep it idempotent and deterministic. - Add optional demarcation in rendering: wrap untrusted description spans in fixed delimiters (e.g.
<<desc>>…<<\/desc>>-style markers documented as inert), controlled by a render option so existing byte-stable outputs can be preserved until the next minor release. - Update
docs/gateway_spec.md§2 to state the sanitization guarantees for card content. - Coordinate with #428 so redaction and sanitization run as composable, ordered passes.
Example prompt, schema, or interface
clean = sanitize_untrusted_text(raw_description)
# - removes \x1b, (bidi override), zero-width joiners
# - NFKC-normalizes confusables
# - collapses 3+ newlines to 1
Acceptance criteria
- Control characters, bidi overrides, and zero-width characters never appear in rendered
ChoiceCardtext or firewall summaries. - Sanitization is deterministic and idempotent (
sanitize(sanitize(x)) == sanitize(x)). - A render option enables untrusted-span demarcation; default output remains unchanged until documented.
- Gateway spec documents the guarantees;
make ci(including scorecard and record-demos drift checks) passes.
Test and evaluation plan
- Unit tests with a fixture corpus of adversarial text shapes (control chars, bidi, confusables, oversized newline runs) — defensive fixtures only, no exploit walkthroughs.
- Golden render tests proving default output is unchanged for benign catalogs.
- Property test: idempotency and printable-character invariants (complements #440).
- Add sanitization cases to the smoke-eval fixtures.
Migration notes
Rendered prompt text changes for catalogs that contained the affected character classes; committed demo casts and scorecards may need regeneration (make record-demos-check, make scorecard). Flag the change in CHANGELOG.md. Demarcation markers are opt-in initially.
Risks and tradeoffs
NFKC normalization can alter legitimate non-Latin text; the pass must be conservative and configurable. Byte-stable rendering guarantees (cache-stable browse, §5 of the gateway spec) require the sanitizer to run before first-sighting freezing so frozen card content is already clean.
Suggested labels
security, prompt-engineering, area/routing, breaking-change
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with render_cards_text() in src/contextweaver/routing/cards.py and mcp_tool_to_selectable in src/contextweaver/adapters/mcp.py, then trace firewall summaries in src/contextweaver/context/firewall.py. Review docs/gateway_spec.md §2 and the proposed unit, golden-render, property, and smoke-eval tests. Done means the acceptance criteria pass and make ci, make record-demos-check, and make scorecard complete successfully.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend-api-design, documentation, security, testing
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100