> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vinmake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Guardrails and refusals

> Deterministic scope gates the model cannot talk its way past, refusals that name who does the thing instead, and the sandbox an untrusted document is read inside.

# Guardrails and refusals

Four of the five agents carry a **scope gate**: a set of pure functions over the message text, with no I/O and no model call.

That design choice is the guardrail.
A gate implemented as a system-prompt instruction is a suggestion the model weighs against everything else in the context.
A gate implemented as a regular expression over the message is a decision the model never participates in.

Two consequences follow:

* **A refusal is reproducible.** The same message always gets the same answer, and "the model decided not to today" is not a possible outcome.
* **It is impossible to prompt-inject.** There is no instruction the message can carry that changes what the gate computes, because the gate does not read instructions.

## What each gate decides

| Agent      | Gate       | Decides                                                                                |
| ---------- | ---------- | -------------------------------------------------------------------------------------- |
| Clerk      | `scope.py` | attachment means document, text means chat. Then: is the question in the intake lane?  |
| Auditor    | `scope.py` | in the assurance lane or out. Then: **which check families** does the question target? |
| Accountant | `scope.py` | in lane or out. Then: close, adjustment, or statutory?                                 |
| Typewriter | `scope.py` | in lane or out. Then: prose means compose, a list means tabulate                       |
| Bookkeeper | -          | no chat lane. It is driven by a launch, not a conversation                             |

Every gate is a **denylist that short-circuits out**, with everything else in.
An unrecognised in-lane message gets a scoped answer, not a refusal.

Greetings and "what can you do?" are **in** scope on the Clerk.
A capability question is a legitimate use of an agent, and refusing it is the kind of unhelpfulness that makes people stop using a tool.

## A refusal names who does it instead

```
That's outside what the Auditor does. I'm the read-only assurance agent for VinMake's book
of record: I recompute the ledger's hash chain, check that the trial balance nets to zero
and every entry balances, confirm nothing sits in a closed period, reconcile intake records
against their GL entries, and flag rows stuck mid-pipeline.

I never post, fix, or change anything - that's by design. To post a correction or a new
entry, ...
```

Three things that decline does, deliberately:

1. **states its lane positively**, so the user learns what to ask instead of only what not to;
2. **says the refusal is by design**, so it does not read as a failure or a permissions bug;
3. **hands the request on**, naming the thing that does do it.

The Typewriter's approve-refusal takes that furthest: the honest answer is that **nobody in the software** approves - approving is a human decision - and saying so is more useful than a generic redirect.

## Proximity, not adjacency

The sharpest lesson in this part of the system, and it cost two agents a real defect.

> **A scope-gate denylist must match verb and noun by PROXIMITY, not adjacency.**

"Classify this supplier invoice and file it" names a **concrete document type**, not the literal word "document".
An adjacency-only pattern let it straight through into a full audit sweep on the Auditor, and a close run on the Accountant.

Both gates now match with a bounded gap between the verb and the noun.

<Note>
  One deliberate omission: the Accountant's classify-verb noun list leaves out "statement", because financial **statements** are its own lane.
  A denylist that is right in general and wrong for the agent's own vocabulary refuses the thing the agent exists to do.
</Note>

## Enumerated families never short-circuit

The Auditor's family router is a **union**, not a first-match.
A question naming three families runs three.

And an explicit "full check" wins outright over the enumeration.

<Info>
  That last rule is deliberate, and the trade-off is worth stating: for a **read-only** agent, running one family too many costs a database read, while running one too few **answers half the question**.
  So an explicit full-sweep request can never be narrowed by a keyword the enumeration happened to miss.

  It keys on a **generic** assurance noun (check, audit, sweep, scan, review), never a family noun, so a narrow question like "any chain breaks?" still routes to the hash chain alone.
  Shipped; see [the Auditor](/greatbook/agents/auditor).
</Info>

## The output check does not nuke a handoff

The Clerk's chat path runs a drift guard over its own reply.

It uses the **off-topic** patterns only, not the cross-domain ones.
Otherwise a reply that names Bookkeeper terms **in order to hand the request off** gets suppressed for mentioning them, and the user gets nothing instead of a redirect.

## An untrusted document gets no filesystem

A turn carrying document text is a turn carrying **content from outside the system**, and it is treated that way.

<Warning>
  **Never grant a permission-bypass mode on an extraction or chat turn, and never allow an unscoped file read.**
  Both were once true at the same time, which meant an extraction turn had a general shell.

  The options for an untrusted-document turn are built by a dedicated helper that pins file reads to **the one staged document**, through a pre-tool hook.

  And an empty allowed-tools list does **not** mean "no tools" - it is additive, and under a bypass mode the model still had shell, read and write.
  That is the specific trap the helper exists to close.
</Warning>

## The human-task gate: ambiguity means no task

Not every turn is work.
The human worklist at **Audit → Actions** is an operator's to-do list, and a task row must exist for real work only.
The top-level **Actions** surface is different: it records agent runs whether they changed anything or failed before a workspace existed.

The rules fire in an order that resolves ambiguity toward **not** creating a row:

| Rule                                                     | Verdict                                                          |
| -------------------------------------------------------- | ---------------------------------------------------------------- |
| an attachment                                            | **always** an action - handing over a document *is* the request  |
| empty text                                               | never                                                            |
| a polite wrapper, stripped, then judged as an imperative | "can you process this invoice?" is a **request**, not a question |
| an informational question                                | **never**, even when it names a document                         |
| an imperative work verb aimed at a finance artifact      | an action                                                        |
| everything else                                          | conversation                                                     |

The informational rule fires **before** the imperative one, deliberately.
A user whose genuine request is read as chat gets an answer and can restate it.
A user whose greeting mints a task loses trust in the tab.

## Where it is enforced

| Concern                              | Code                                                                           |
| ------------------------------------ | ------------------------------------------------------------------------------ |
| the per-agent gates                  | `agents/{clerk,auditor,accountant,typewriter}/scope.py`                        |
| the Actions gate                     | `agents/common/action_required.py`                                             |
| the document sandbox                 | `data_entry/model/sdk_extractor.py` - `doc_sandbox_options`, `no_tool_options` |
| the boxed tool surface               | `agents/common/tools.py`                                                       |
| the browser-drivable graph allowlist | GreatBook web repository, the passthrough guard                                |

## The tests that would fail if it broke

Each agent's `test_scope.py`, plus `test_doc_sandbox.py` for the sandbox options and the pre-tool hook.

The gates are pure functions, which is exactly why they are cheap to test exhaustively: a scope test is a table of messages and expected verdicts, with no model and no database.

<Note>
  **One honest note.**
  The Auditor's gate is known to let one write-request phrasing through instead of declining it.
  That is a scope-gate miss, not a control failure - the Auditor has no write channel, so the worst outcome is a read-only sweep in response to a request it should have declined.
  It is recorded rather than left for a reader to find.
</Note>

## Related

* [The agent org](/greatbook/agents/index) - what each agent's lane is
* [The Auditor](/greatbook/agents/auditor) - the family router in full
* [Where the business logic lives](/greatbook/agents/sop-vs-code) - the other structural boundary
* [Two gates, three people](/greatbook/capabilities/maker-checker) - the human guardrails
