Change: Mira's converse route learns which surface a turn arrived on and behaves accordingly (RFD-AGT-013 T2j-b, D16/F8, CC-32). On a channel (WhatsApp, email) she signposts cross-surface references ("In the app you asked …") and paraphrases restricted content instead of quoting it — a short paraphrase naming the topic plus "open the app", never a verbatim span and never silence. Behind the prompt sits a brain-side fail-closed guard: a channel turn body containing a restricted span verbatim (or blank over restricted context) is replaced with a static signpost fallback before it leaves the brain. A deterministic offline eval fires the REAL converse path (scripted model → real prompt → real guard) and gates it all in CI.
| Field | Shape | Why |
|---|---|---|
MiraConverseInput.channel_origin | str = "app", token grammar [a-z][a-z0-9_]{0,31} (functions-side vocabulary, garbage fails closed) | the surface THIS turn arrived on; activates the cross-surface contract |
MiraMessage.channel_origin | str | None | per-history-turn surface (the Story-2875 chat_turn.channel_origin stamp); None = pre-2875 turn, rendered untagged — no fabricated origin |
ContextBundle.contains_restricted / restricted_excerpts() | property + list of verbatim spans | surfaces the sensitivity gate's restricted: true stamps (Story-2853 vocabulary) in the same shape the functions gate consumes |
Surface: whatsapp. ← deterministic line, STABLE prefix (cache-safe)
Matter metadata: …
<<MATTER_FRAME_v5>> … ← unchanged sandwich
history suffix: counsel (app): what did we file? ← per-turn surface tags when known
mira: the reply affidavit ← untagged when origin unknown
CONVERSE_PROMPT_VERSION = "mira.converse.v3" (prompt change = cache event +
replay stamp). The surface line sits in the stable prefix, so the cache prefix stays
byte-identical across a session's turns — a session lives on one surface.Runs on every turn before turn.completed. The turn body is what the channel
delivery rail sends (deltas reach only the app thread, where restricted is allowed), so
gating the body gates the channel:
| Delivered body (channel turn, restricted context) | Guard |
|---|---|
| safe paraphrase, no span | passes untouched |
| contains a restricted span verbatim (case/whitespace-mangled included — functions-gate normal form) | replaced with the static signpost fallback + operator log mira_converse_restricted_reveal_blocked |
| blank over restricted context (silence) | replaced with the fallback |
| app-surface turn | never gated — restricted is app-allowed |
Documented ceiling (ponytail comment in code): whole-span containment only — a bare figure
out of a span is prompt-taught and eval-graded (protected_terms), not
code-caught.
Each candidate in evals/datasets/signposting.yaml scripts the MODEL's raw
output; the harness fires the real build_converse_prompt → stream_converse →
guard_channel_restricted seam and grades the DELIVERED body:
| Invariant | Check |
|---|---|
| no verbatim reveal | no restricted span contained under the functions-gate normal form (a scripted leaking model must come out as the fallback — guard_fired) |
| no partial reveal | no protected_terms figure/name present — the grader is stricter than the span-guard |
| signposted | at least one signpost marker present ("open the app" / "in the app" / "on whatsapp") |
| not silent | every must_mention topic present on model-authored bodies; a blank body fails closed to the fallback |
Four cases / eleven scripted candidates, both directions.
tests/test_signposting_eval.py runs every candidate through the real path as the
blocking CI gate, and proves the suite RED-capable: with the guard neutered, the leak
candidate's delivered body carries the span and the invariants go red — the eval measures
the fence, not nothing.
uv run ruff check agents/mira # All checks passed uv run --package mira pytest agents/mira -q # 243 passed (13 new) uv run pytest tests/ -q # repo governance suite — 254 passed uv run python agents/mira/evals/run.py signposting # CC-32 set passed (real converse path) uv run python agents/mira/evals/run.py adversarial # 14 cases held (unchanged, still green) uv run python agents/mira/evals/run.py privilege # ER-26 set passed (unchanged)
channel_origin on the invoke envelope for WhatsApp-originated launches (T2b-a,
functions lane) and the restricted classification itself (Story-2853 sensitivity gate).
Until they ship, every envelope defaults honestly to channel_origin="app" and
the contract is dormant — no behavior change on the live app path.