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.
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.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 }
| Area | Where | What it does |
|---|---|---|
| Typed contracts | agents/atlas/models.py | New 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 catalog | agents/atlas/dock_tools.py new | The 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 brain | agents/atlas/dock_chat.py | The 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 prompt | agents/atlas/prompts/dock_chat.py | Removed 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 wiring | agents/atlas/runtime.py | build_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 gates | agents/atlas/evals/dock_*.py · tests/test_dock_*.py | Live 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. |
Two independent safety rails, both enforced in code, not just prose:
_dock_tool_is_granted rejects (via ModelRetry) any
tool_intents whose name is not in this turn's granted allow-set. The brain can only propose a
tool it was shown; the server still re-resolves real authority before executing (RFC-APB-002 §C.3), so
a forged permission widens nothing.# 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.
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.