ADR-0070: Context formatting locked ordering and session nonce
Architecture decision record 0070 — locked context injection order (directive → history → memories by category → tools), XML-style ibex_memory delimiters, and per-assembly secrets.token_urlsafe nonce.
ADR-0070: Context formatting locked ordering and session nonce
- Status: Accepted
- Date: 2026-09-05
- Authors: IBEX Harness team
- Milestone: 3.5.C.5 Context Formatter
- Depends on: ADR-0069 (
PackedMemories/ScoredMemory), ADR-0038 (protoassembled_contextstring — wiring in 3.5.C.6)
Context
Track C needs a formatter that turns a resolved directive, recent messages, and packed memories into a single assembled_context string. Discovery (main @ post-C.4) found:
- No formatter module existed under
services/context. - Planning sketches incorrectly named ADR-0042 (self-hosted OpenAI-compatible adapter) as the amend target for ordering — unrelated.
- ADR-0038 covers the gRPC contract only and has no ordering/delimiter section; Phase 3.5 exit criteria cite it for indexing, not for formatter decisions.
- ARCHITECTURE.md “Format and inject” previously said History last (
Directive → Procedural → Declarative → Episodic → Tools → History), while phase index / goals / blog already locked history at position 2. - SECURITY.md §7.3 requires memories wrapped as data with a per-session nonce (e.g.
<ibex_memory nonce="...">).
Decision
1) Locked injection order
1. Directive (plain text + untrusted-data instruction naming the assembly nonce)
2. Conversation history (native `role: content` lines — not XML-wrapped)
3. Memories by category: procedural → factual → preference → behavioral → episodic
4. Tool schemas (optional; omitted when empty)History @ 2: mitigates “lost in the middle” — recent turns stay near the start where attention is reliable; putting history last buried it after memory blocks.
Unknown categories (if any) append after the five known buckets, sorted by (category, memory_id).
2) Module
services/context/app/formatter.py—ContextFormatter,FormatRequest,FormattedContext,CATEGORY_ORDER- Consumes one
FormatRequest(directive, recent messages,PackedMemories, optional tool schemas / injected nonce) - Does not orchestrate budget → retrieve → pack (3.5.C.6)
3) Per-assembly nonce + XML-escaped memory bodies
- Generate with Python stdlib
secrets.token_urlsafe(nbytes)(OS CSPRNG) — no home-grown crypto (SECURITY.md §6.4 / AGENTS §5.4). - Default
nbytes=16(~128 bits of entropy); envIBEX_CONTEXT_FORMATTER_NONCE_BYTES(allowed 1..64). - Scope: one nonce per
format()call (one assembly). Open tags carrynonce="{assembly}"; consumers must only trust tags whose nonce matches this assembly. - Boundary contract: memory markup is serialized only (never parsed) via
html.escapefor attribute values (quote=True) and body text. Untrusted content cannot forge</ibex_memory>or inject raw markup. The per-assembly nonce on the open tag authenticates genuine delimiters; escaping prevents close-tag / attribute breakout from memory bodies. Stdlibxml/defusedxmlare intentionally unused — XXE applies to parsers, not serializers, and SAST often false-positives on serialize-onlyElementTreeimports.
4) Doc supersession
This ADR supersedes the stale ARCHITECTURE.md “Format and inject” history-last ordering. ARCHITECTURE.md is updated to match this decision and links here. Do not amend ADR-0038 or ADR-0042.
Consequences
- Golden-output and nonce spoof unit tests gate regressions in CI (
context-test). - C.6 can map
FormattedContext.assembled_context→AssembleContextResponse.assembled_context. - Proxy / directive injection modes (ADR-0031) remain separate: this ADR formats the assembly string, not the proxy message-array inject strategy.
Alternatives considered
- Amend ADR-0042 — rejected: wrong document (Phase 2.5 self-hosted adapter).
- Amend ADR-0038 — rejected: mixes gRPC contract with formatting policy; 0038 has no ordering section.
- Nonce alone without body escaping — rejected for close-tag forgery: raw
</ibex_memory>in content could terminate the block; escaping is required in addition to the per-assembly open-tag nonce. - XML-wrap history — rejected: milestone success signal requires native role messages for trusted history.
xml.etree.ElementTree/defusedxmlfor markup — rejected for this module: we never parse XML (no XXE surface);defusedxmldoes not provide anElementbuilder API, and importing stdlibxmltrips serialize-only SAST noise. Preferhtml.escapeserialization.
Was this page helpful?
Last updated on