mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(ccr): wrap proactive expansion injection in XML attribution tag (#1398)
## Description In multi-agent threads, Headroom injects the proactive context expansion block directly into the latest non-frozen user turn's first text block as plain bracketed text. When that turn contains `<peer_turn from="AgentX">...</peer_turn>` markup, the injected block lands adjacent to agent-attributed regions with no machine-readable boundary. LLMs, loggers, and attribution parsers cannot distinguish Headroom-injected context from content attributed to AgentX, causing misattribution or treatment of the block as user-authored prompt injection. Root cause: `format_expansions_for_context` in `headroom/headroom/ccr/context_tracker.py` (~line 550) returns plain text bounded only by human-readable brackets (`[Proactive Context Expansion...]` / `[End Proactive Expansion]`). No XML wrapper is added at the injection site either. This PR wraps the entire return value of `format_expansions_for_context` in `<headroom_proactive_expansion>` tags. The existing brackets are preserved inside for human readability; the outer tag gives downstream consumers a provenance boundary consistent with the `<peer_turn>` XML convention used in multi-agent turns. Closes #503 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/headroom/ccr/context_tracker.py`: restructured the tail of `format_expansions_for_context` to wrap the joined parts in `<headroom_proactive_expansion>...</headroom_proactive_expansion>`. Inner brackets are unchanged. Empty-input early return is unchanged. Payload body is sanitized to escape any stray `</headroom_proactive_expansion>` close tag in expansion content, preventing wrapper boundary ambiguity. - `tests/test_ccr_context_tracker.py`: added XML wrapper assertions to existing formatter tests; new standalone tests for wrapper structure, full injection chain identifiability, and close-tag escape robustness. - `CHANGELOG.md`: entry under `[Unreleased]` for the injection format change. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_context_tracker.py -x -q`) - [x] Linting passes (`uv run ruff check .`) - [ ] Type checking passes (`uv run mypy headroom`) — N/A: single-expression change, no new types - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run pytest tests/test_ccr_context_tracker.py -x -q 41 passed in 2.41s ``` ## Real Behavior Proof - Environment: local, Python 3.11+, `uv sync --extra dev` - Exact command / steps: `uv run python -c "from headroom.ccr.context_tracker import ContextTracker; t = ContextTracker(); r = t.format_expansions_for_context([{'hash':'h1','type':'full','content':'ctx','item_count':1,'reason':'r'}]); print(r.startswith('<headroom_proactive_expansion>'))"` → `True` on head, `False` on base; `uv run pytest tests/test_ccr_context_tracker.py -x -q` → 41 passed - Observed result: return value now starts with `<headroom_proactive_expansion>` and ends with `</headroom_proactive_expansion>`; inner `[Proactive Context Expansion...]` and `[End Proactive Expansion]` brackets are present and not duplicated - Not tested: live multi-agent thread rendering with Anthropic API; downstream attribution parser behavior in production ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes The injection site (`AnthropicHandlerMixin._append_context_to_latest_non_frozen_user_turn` in `anthropic.py`) is unchanged. Existing tests that check for `"[Proactive Context Expansion" in formatted` continue to pass since the brackets are preserved inside the XML wrapper. The tag name `headroom_proactive_expansion` uses underscores (not hyphens) to match the `snake_case` convention used in the repo's other XML-like constructs. To prevent a stray `</headroom_proactive_expansion>` inside expansion content (e.g., code snippets) from breaking the wrapper boundary, the body is sanitized to `<\/headroom_proactive_expansion>` before wrapping; a test covers this edge case. --------- Co-authored-by: JD Davis <mxjerrett@gmail.com>
This commit is contained in:
parent
8d6c175d60
commit
cabf666b34
3 changed files with 89 additions and 3 deletions
|
|
@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
<<<<<<< pr/503-proactive-expansion-xml-tag
|
||||
### Fixed
|
||||
- Proactive expansion blocks injected into user turns are now wrapped in
|
||||
`<headroom_proactive_expansion>` XML tags, giving downstream consumers
|
||||
(LLMs, loggers, attribution parsers) a machine-readable provenance
|
||||
boundary and preventing misattribution in multi-agent threads.
|
||||
|
||||
=======
|
||||
>>>>>>> main
|
||||
### Changed
|
||||
|
||||
* **telemetry:** anonymous usage telemetry is now **opt-in** (off by default) instead of opt-out. Nothing is collected or sent unless you set `HEADROOM_TELEMETRY=on` or pass `--telemetry` to `headroom proxy` / `headroom install apply`. `is_telemetry_enabled()` is fail-closed — only explicit on-values (`on`/`true`/`1`/`yes`/`enable`/`enabled`) enable it; unset, empty, or unrecognized values stay disabled. The existing `--no-telemetry` flag and `HEADROOM_TELEMETRY=off` remain accepted for back-compat, and install manifests now write the `HEADROOM_TELEMETRY` value explicitly so generated deployments are unambiguous.
|
||||
|
|
|
|||
|
|
@ -587,9 +587,11 @@ class ContextTracker:
|
|||
else:
|
||||
parts.append(str(exp["content"]))
|
||||
|
||||
parts.append("\n[End Proactive Expansion]")
|
||||
|
||||
return "\n".join(parts)
|
||||
parts.append("[End Proactive Expansion]")
|
||||
body = "\n".join(parts)
|
||||
# Escape any stray close tag in payload to prevent wrapper boundary forgery
|
||||
body = body.replace("</headroom_proactive_expansion>", "<\\/headroom_proactive_expansion>")
|
||||
return f"<headroom_proactive_expansion>\n{body}\n</headroom_proactive_expansion>"
|
||||
|
||||
def get_tracked_hashes(self) -> list[str]:
|
||||
"""Get list of currently tracked hashes."""
|
||||
|
|
|
|||
|
|
@ -546,6 +546,8 @@ class TestExpansionFormatting:
|
|||
assert "[Proactive Context Expansion" in formatted
|
||||
assert "Expanded from earlier" in formatted
|
||||
assert '[{"id": 1}, {"id": 2}]' in formatted
|
||||
assert formatted.startswith("<headroom_proactive_expansion>\n")
|
||||
assert formatted.endswith("\n</headroom_proactive_expansion>")
|
||||
|
||||
def test_format_search_expansion(self):
|
||||
"""Format search expansion for LLM context."""
|
||||
|
|
@ -565,6 +567,8 @@ class TestExpansionFormatting:
|
|||
formatted = tracker.format_expansions_for_context(expansions)
|
||||
|
||||
assert "Search results for 'authentication'" in formatted
|
||||
assert formatted.startswith("<headroom_proactive_expansion>\n")
|
||||
assert formatted.endswith("\n</headroom_proactive_expansion>")
|
||||
|
||||
def test_format_empty_expansions(self):
|
||||
"""Empty expansions return empty string."""
|
||||
|
|
@ -574,6 +578,77 @@ class TestExpansionFormatting:
|
|||
|
||||
assert formatted == ""
|
||||
|
||||
def test_format_expansion_xml_wrapper(self):
|
||||
"""Expansion output is wrapped in machine-readable XML provenance tag."""
|
||||
tracker = ContextTracker()
|
||||
expansions = [
|
||||
{
|
||||
"hash": "h1",
|
||||
"type": "full",
|
||||
"content": "expanded content",
|
||||
"item_count": 1,
|
||||
"reason": "high relevance",
|
||||
}
|
||||
]
|
||||
result = tracker.format_expansions_for_context(expansions)
|
||||
assert result.startswith("<headroom_proactive_expansion>\n")
|
||||
assert result.endswith("\n</headroom_proactive_expansion>")
|
||||
assert "[Proactive Context Expansion" in result
|
||||
assert result.count("[End Proactive Expansion]") == 1
|
||||
|
||||
def test_proactive_expansion_identifiable_after_injection(self):
|
||||
"""Injected expansion carries XML provenance tag after full injection chain."""
|
||||
from headroom.ccr.context_tracker import ContextTracker
|
||||
from headroom.proxy.handlers.anthropic import AnthropicHandlerMixin
|
||||
|
||||
tracker = ContextTracker()
|
||||
expansions = [
|
||||
{
|
||||
"hash": "h1",
|
||||
"type": "full",
|
||||
"content": "expanded context",
|
||||
"item_count": 1,
|
||||
"reason": "high relevance",
|
||||
}
|
||||
]
|
||||
expansion_text = tracker.format_expansions_for_context(expansions)
|
||||
|
||||
# Simulate a user turn that contains peer_turn markup
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "<peer_turn from='AgentX'>some content</peer_turn>"}
|
||||
],
|
||||
}
|
||||
]
|
||||
result = AnthropicHandlerMixin._append_context_to_latest_non_frozen_user_turn(
|
||||
messages, expansion_text, frozen_message_count=0
|
||||
)
|
||||
injected = result[0]["content"][0]["text"]
|
||||
# Headroom-injected content is identifiable by XML tag, distinct from peer content
|
||||
assert "<headroom_proactive_expansion>" in injected
|
||||
assert "</headroom_proactive_expansion>" in injected
|
||||
assert "<peer_turn from='AgentX'>" in injected # peer content unchanged
|
||||
|
||||
def test_format_expansion_xml_close_tag_in_payload_escaped(self):
|
||||
"""Payload containing the XML close tag is escaped to keep wrapper boundaries intact."""
|
||||
tracker = ContextTracker()
|
||||
expansions = [
|
||||
{
|
||||
"hash": "h1",
|
||||
"type": "full",
|
||||
"content": "return '</headroom_proactive_expansion>'",
|
||||
"item_count": 1,
|
||||
"reason": "high relevance",
|
||||
}
|
||||
]
|
||||
result = tracker.format_expansions_for_context(expansions)
|
||||
assert result.startswith("<headroom_proactive_expansion>\n")
|
||||
assert result.endswith("\n</headroom_proactive_expansion>")
|
||||
assert result.count("<headroom_proactive_expansion>") == 1
|
||||
assert result.count("</headroom_proactive_expansion>") == 1
|
||||
|
||||
|
||||
class TestGlobalTracker:
|
||||
"""Test global tracker singleton."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue