dgenio / dgenio/contextweaver

Sanitize and demarcate untrusted catalog and tool-result text in rendered prompt surfaces

Open
#480 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area/routing breaking-change complexity:complex priority:medium prompt-engineering security
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.pyrender_cards_text() interpolates card.description directly 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.pymcp_tool_to_selectable stores upstream name/description verbatim; 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

  1. Add a sanitize_untrusted_text() primitive (likely _utils.py or a new small module): strip C0/C1 control characters (preserving \n/\t where meaningful), apply NFKC normalization, remove bidi override and zero-width characters, collapse repeated newlines.
  2. Apply it at the two trust boundaries: catalog ingest (adapters producing SelectableItem) and firewall summary/fact production. Keep it idempotent and deterministic.
  3. 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.
  4. Update docs/gateway_spec.md §2 to state the sanitization guarantees for card content.
  5. 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 ChoiceCard text 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.