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:
Rod Boev 2026-06-26 15:15:17 -04:00 committed by GitHub
parent 8d6c175d60
commit cabf666b34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 3 deletions

View file

@ -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.

View file

@ -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."""

View file

@ -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."""