Atlas learns to answer from the data

The Dock brain stops refusing data questions and starts answering them — from a real authorized read, never on-screen guesswork.
Story-1741 · Epic-1646 (DEF-4 fix · Architecture B · part 2 of 2) · branch issue-1741-atlas-answer-brain · 2026-06-25

Until now the Dock brain (Story-1664) could do exactly two things: orient ("what is this page?") and navigate ("show my clients"). It was hard-prompted to refuse any data question — ask "how many matters for Acme?" and counsel got "I cannot read the data on this page" (UAT Run-2 DEF-4). That refusal string was not hardcoded; it was the model paraphrasing an explicit instruction in the Dock system prompt. This Story (the pramaan-agents half of Architecture B) wires the brain to answer: it emits a server-plane query_matters/global_search tool intent, the broker runs it under the caller's authz + RLS (part 1, functions#1739), and the result comes back so the brain answers from the authorized rows — and only those.

Two boundaries shape every line: (1) The brain never touches firm data — it proposes a typed AtlasServerToolIntent (plane=server) and the broker executes it under RLS; the brain stays stateless with no DB, no egress beyond the model. (2) Answer ONLY from the authorized result, never fabricate. If a permitted tool covers the ask, emit the intent; if not, refuse gracefully and offer to navigate. A turn may do exactly ONE of: answer, navigate, or fetch.

The multi-step turn shape

A data question is no longer single-shot. It is a small loop the broker drives across invokes — the brain emits an intent, the server runs it, the brain answers from the result:

// STEP 1 — counsel: "how many matters for Acme Corp?"  (no result yet)
//   brain emits a server-tool intent; this turn is NOT terminal
{ next_message_md: "Let me check your matters for Acme…",
  tool_intents: [ { name:"query_matters", plane:"server", params:{ client_id:"acme" } } ],
  is_terminal: false }            // → broker runs it under authz/RLS, then re-invokes

// STEP 2 — broker re-invokes with the AUTHORIZED result fed back
//   tool_results: [{ tool:"query_matters", outcome:"executed", result:{ count:4, … } }]
{ next_message_md: "Acme Corp has 4 matters.",   // the count comes from the result, not the screen
  tool_intents: [],
  is_terminal: true }

What to review

AreaWhereWhat it does
Typed contractsagents/atlas/models.pyNew AtlasServerToolIntent (server-plane, mirrors the functions AgentToolIntent) + DockToolResult. AtlasDockOutput gains a flattened tool_name/tool_params channel; AtlasDockTurn gains tool_intents and is no longer forced terminal. DockChatContext gains granted_permissions + current_context + tool_results.
The server-tool catalogagents/atlas/dock_tools.py newThe brain's STATIC mirror of the broker's READ tools (query_matters, global_search — both matters:read), gated per turn by the caller's granted_permissions. assert_tool_is_granted is the no-invented-tool guarantee (twin of route_intent's no-invented-key).
The Dock brainagents/atlas/dock_chat.pyThe output-function builds a tool intent (is_terminal=False) or a navigation/answer; a one-action-per-turn guard; a second output-validator rejecting any ungranted tool. Deps now carry the granted tool allow-set.
The Dock promptagents/atlas/prompts/dock_chat.pyRemoved the refusal instruction (the DEF-4 source). Added: emit a tool intent to fetch data; answer ONLY from AUTHORIZED TOOL RESULTS; never fabricate; refuse gracefully when no permitted tool fits. Renders AVAILABLE TOOLS + AUTHORIZED TOOL RESULTS.
Runtime wiringagents/atlas/runtime.pybuild_dock_context reads the new fields and lifts current_context['tool_results'] up (the one pass-through dict the merged contract carries). tool_intents ride the wire so the broker executes them.
Evals + offline gatesagents/atlas/evals/dock_*.py · tests/test_dock_*.pyLive cases: count→intent, count→answer-from-result, RLS zero-rows, refuse-without-tool. A no-fabrication gate (must_not_mention) runs on every intent. Offline + FunctionModel wiring tests run in CI without a key.

The no-fabrication / no-invented-tool guarantees

Two independent safety rails, both enforced in code, not just prose:

How to verify

# 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   # 54 passed

# 2. repo gate — full atlas suite + lint
cd agents/atlas && pytest tests -q && ruff check .                            # 463 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 full chain without an API key: the flat DTO is validated, the output-function assembles a tool-intent turn (not terminal) or an answer turn, an invented tool is rejected until retries exhaust, and build_dock_context reads the granted catalog + merges nested tool results. The LIVE Dock-evals (DeepSeek v3.2 via OpenRouter) run where the key is injected and assert the count-from-result + no-fabrication + RLS-zero behaviours against the real model.

Seam for DEF-6 (spec#1753): set_view + propose_operation slot in as additional DockServerToolSpec entries in dock_tools.py, gated by their own required_permission — the emission path, the validator, and the prompt rendering are unchanged.