faz
Configuration

Permissions

How the permissions block in faz.yaml controls which queries succeed and which get blocked at the RBAC gate.

The permissions: block in faz.yaml is faz's primary access-control surface. It declares, per database and per table, which operations an AI assistant is allowed to perform. Every query is evaluated against it before execution.

This page covers the model, the seven access codes, the layered lookup, and the default behaviour you need to know about. The cheat-sheet matrix lives on Permission levels; the broader safety pipeline that calls into this gate is on How faz protects you.

The model

Permissions are a flat list. Each entry covers one database with a mandatory baseline access level and an optional per-table override map.

faz.yaml
permissions:
  - database: <database>          # connector name from `databases:` above
    access: R                     # baseline applies to every table by default
    tables:                       # optional overrides for specific tables
      <writable-table>: RW
      <audit-table>: W
      <append-table>: RWA

  - database: <other-database>    # second connector, same shape
    access: R

When a query arrives, faz extracts every table the query touches — including those reached through JOIN, MongoDB $lookup, _reindex, Cypher rebound labels, and similar. For each target, it does a layered lookup:

  1. Per-table override: is there an entry under tables: for this exact table? If so, use it.
  2. Per-database baseline: otherwise, use the database's access: value.
  3. Implicit fallback: if neither matches and the request is authenticated, an implicit R (read-only) is granted for that single target.

Any target that fails its check causes the whole query to block. For federated queries, the default is to silently strip the denied step and let the rest run; see Federated query behaviour below.

The access codes

Six codes are recognised: R, W, RW, RA, RWA, A. Each is a set of allowed operation classes.

  • R — Read only. Allows SELECT and EXPLAIN. The default when in doubt. An agent with R on a database can ask any read-shaped question against any table covered by that baseline, and nothing else.
  • W — Write only. Allows INSERT, UPDATE, and DELETE. Notably excludes SELECT. Useful for write-only audit tables where an agent should append rows but never read them.
  • RW — Read and write. Allows SELECT, EXPLAIN, INSERT, UPDATE, DELETE. The "trusted" baseline.
  • RA — Read and append. Allows SELECT, EXPLAIN, INSERT. No UPDATE, no DELETE. For append-only logs and event tables.
  • RWA — Read, write, append (no delete). Allows everything in RW except DELETE. For tables where rows can be added or amended but never removed.
  • A — Admin. Everything in RW, plus DDL: CREATE, DROP, ALTER, TRUNCATE, GRANT, REVOKE. The only level that lets DDL through.

A is the only level that lets DDL through. Every other level rejects CREATE, DROP, ALTER, and TRUNCATE at the RBAC gate, with a redundant block at the AST checker. Grant A only when an agent legitimately needs to mutate schema — for development databases, or briefly during a migration.

Per-table overrides

The tables: map under each database entry overrides the baseline for specific tables. Use it to tighten or loosen access.

permissions:
  - database: <database>          # connector name from `databases:` above
    access: R                     # baseline: read-only
    tables:
      <writable-table>: RW        # tighten/loosen as needed
      <audit-table>: W            # write-only — agent can append, can't read
      <ddl-table>: A              # admin — agent can DDL this one table

Overrides win over the baseline. Operations against <database>.<audit-table> are evaluated against W (no SELECT; INSERT/UPDATE/DELETE allowed). Operations against <database>.<writable-table> are evaluated against RW. Every other table in <database> falls through to the baseline R.

What happens to tables you didn't declare

This is the part that's easy to get wrong. If your permissions: block declares a database but an agent's query touches a table you didn't list under tables:, the baseline applies — that's expected.

What's less obvious: if the query touches a table whose database isn't declared in permissions: at all, the request is still authenticated, and an implicit R permission is added for that single target. Reads against undeclared databases pass. Writes against undeclared databases fail (since R doesn't allow them).

This default is permissive on reads. If you don't want an agent reading a database, don't connect it to faz, or declare the database in permissions: with an explicit tables: override that doesn't grant SELECT (set the table to W, which excludes reads — see Blocking a table from reads below).

An empty or missing permissions: block does not block all queries. Authenticated requests fall through to the implicit R default and reads succeed. Always declare an explicit permissions: entry for every database you've connected, even if you only want to reaffirm R.

Blocking a table from reads

There is no none access level. To make a specific table unreadable while leaving the rest of its database readable, set the table's override to W — write-only — which excludes SELECT:

permissions:
  - database: <database>
    access: R
    tables:
      <sensitive-table>: W      # blocks reads; allows writes

This is admittedly a footgun if your goal is "no access at all." If you genuinely want to make a table invisible, the safer pattern is to keep the database connection scoped at the SQL/RBAC level outside of faz — give the database user a role that doesn't have SELECT on the table. faz then can't see it either.

The wildcard

In the parsed permission rows, the per-database baseline is stored as a wildcard * table name. You'll see this in the audit log and in faz policy output:

Permission(database_uri='<database>', table_name='*',                access_level='R')
Permission(database_uri='<database>', table_name='<writable-table>', access_level='RW')

You don't write * in faz.yaml. The parser produces it from the access: baseline. Keep it in mind when reading faz policy output or grepping the audit log.

Federated query behaviour

For multi-database queries, the RBAC gate runs on every step independently. The default policy when some (but not all) steps are denied is PARTIAL: denied steps are silently stripped from the request and the remaining steps execute. The merged result simply has no rows from the denied step.

The alternative policy is BLOCK: any denial fails the whole request. That's not yet exposed via faz.yaml; the runtime default is PARTIAL.

If your safety posture requires "all-or-nothing" — for example, a federated query that joins customer data with PII must succeed completely or not at all — partial denial is the wrong behaviour. Track this issue: making the policy configurable per request is on the roadmap.

See Federated queries for the full step model.

What the gate cannot do

Worth being explicit about, because the boundary matters:

  • Row-level security. The gate is per-table. It does not enforce "agent X can read rows where tenant_id = 42." Pre-filter at the database role level (Postgres RLS, view-based isolation) and connect faz to the filtered view.
  • Column-level masking. The gate doesn't redact specific columns. Use a view that omits sensitive columns and connect that.
  • Rate limiting. Per-agent or per-user request rate isn't the gate's job; that's on the safety defaults (some keys are on the roadmap).
  • Identity. faz has no agent-identity concept. Every request that arrives is treated as the same caller; permissions are global to the faz instance, not per-agent. If you need per-agent isolation, run separate faz instances behind a router.

Reload behaviour

Permissions are loaded once at startup. Editing faz.yaml does not hot-reload — you need to restart faz serve (or your MCP client, which respawns the stdio server) to apply changes. faz policy is the safe way to confirm what's currently loaded without restarting.

On this page