Since Story-1741 the Dock brain can answer a data question from an authorized read. This Story adds a
sixth job: on a "show my matters as a table" ask the brain can answer with a rich app component
instead of prose. It does this by emitting a RenderSelection —
{component_id, query_ref, view_fields} — that names a catalog component and the read whose
result to render, and nothing else. The broker authors the RenderDataFrame
(functions#1793): it re-executes the bound read under the caller's RLS (B3 re-auth at bind) and stamps the rows.
This is a NEW render intent sibling to the existing answer / navigate / set_view / propose_operation
channels — not a rewrite.
agent_contracts.render.RenderSelection — the agents-repo mirror of the
functions#1793 contract, alongside agent_invoke / agent_event_stream / enums.
The brain ships as a self-contained AgentCore CodeZip and cannot import the repo root at runtime, so it
restates the shape locally and a parity test fences the restatement against the canonical field
shape — the same custody discipline runtime_axes uses for the enums, so the brain copy can't drift
from #1793.// counsel on the matters page: "show my open matters as a table" // surface declares component "matters_table"; caller holds matters:read { next_message_md: "Here's your open matters as a table.", render_selection: { component_id:"matters_table", query_ref:"open_matters", // the read to render — opaque ref view_fields:["title","status"] }, // row fields to surface (no rows!) is_terminal: true } // → broker binds the RenderDataFrame, renders to the app
| Area | Where | What it does |
|---|---|---|
| Shared contract custody | agent_contracts/render.py new | The canonical RenderSelection for the agents repo — the mirror of the functions#1793 contract (component_id/query_ref non-empty, view_fields list, extra="forbid"), exported from agent_contracts alongside the invoke / event-stream / enum contracts. |
| Brain restatement | agents/atlas/models.py | RenderSelection restated for the self-contained CodeZip (cannot import the repo root at runtime), fenced by a parity test against the canonical shape. AtlasDockOutput gains a flattened render_component_id/render_query_ref/render_view_fields channel; AtlasDockTurn gains render_selection (terminal). |
| The render-component catalog | agents/atlas/dock_render.py new | The brain's catalog — ONE component today (matters_table, matters:read), a flat definition not a registry (YAGNI until a second lands). available_render_component_ids is the capability gate: offered only when the surface declares it (current_context['render_components']) AND the role permits it. view_fields are NOT brain-validated — the broker projects them server-side (functions#1793 _project). |
| The Dock brain | agents/atlas/dock_chat.py | _assemble_render_turn builds the selection (blank query_ref → deterministic text fallback); render_component_id joins the one-action-per-turn guard; the output-validator _dock_render_offered DETERMINISTICALLY strips an un-offered component (a simple allow-set membership check) and keeps the text answer — no ModelRetry. Deps carry the offered-component allow-set. |
| The Dock prompt | agents/atlas/prompts/dock_chat.py | Sixth job: SELECT a component, give a query_ref, never emit rows/counts/SQL. Renders AVAILABLE RENDER COMPONENTS; empty ⇒ answer in text. |
| Runtime wiring | agents/atlas/runtime.py | _dock_turn_event puts render_selection on the agent_event_stream.v1 wire (typed dict, SELECTION ONLY) so the broker can bind it. |
| Closed capability universe | agents/atlas/command_vocabulary.py · evals/surface_operator_* | render is registered as the 6th IntentClass and a SurfaceFamily, with en/hi/Hinglish phrasings + success/refusal twins (cross-firm render is the red-team adversary). It now inherits the §8 refusal matrix, 100% refusal coverage, bilingual parity, and red-team independence like every other intent — no ungated escape hatch. |
| Evals + offline gates | agents/atlas/evals/dock_*.py · tests/test_dock_*.py | Live cases: valid render on a matters query, text fallback on an unsupported component, permission refusal. A render-fallback gate fails any non-render case that emits a selection. A parity test imports the canonical contract and fails on any field-shape drift. Offline + FunctionModel wiring tests run in CI without a key. |
Two conditions, both enforced in code:
current_context['render_components'] (the render twin of view_fields); a component the
page can't host is never offered.required_permission — the SAME
permission the server re-resolves before the read runs. granted_permissions is orientation only;
the server re-resolves real authority at bind, so a forged permission widens nothing.ModelRetry, no retry, no
model failure (RFD-AGT-004 AC). Never a blank or invalid selection: if the model left no words, a non-blank
bilingual (en + hi) default backstops it.render_selection is None, so the
red-team / refusal twins fail if a selection leaks — the render twin of the navigation check.# 1. offline gates + the real PromptedOutput wiring (no key — runs in CI) cd agents/atlas && pytest tests/test_dock_chat.py tests/test_dock_evals.py -q # passed # 2. repo gate — full atlas suite + lint cd agents/atlas && pytest tests -q && ruff check . # 507 passed, clean # 3. golden Dock-evals LIVE against the real model (needs OPENROUTER_API_KEY) cd agents/atlas && python -m evals.dock_run
The FunctionModel tests prove the chain without an API key: the flat DTO is validated, the output-function
assembles a render-selection turn (terminal, no rows), an invented component deterministically falls back
to the text answer (no failure, no retry), the parity test fences the brain's RenderSelection
against the canonical contract, and the selection rides the wire as a typed dict with no data key.
The LIVE Dock-evals (DeepSeek v3.2 via OpenRouter) assert the render / text-fallback / permission-refusal
behaviours against the real model where the key is injected.
.tsx (003). This Story is the brain
side only — it emits the selection.