Policies — plain English, enforced math

Aggregate-only access, k-anonymity group suppression, and join limits — written the way you'd say them, enforced by query analysis.

Masking hides sensitive columns. Sometimes the requirement is stronger: agents should never see individuals at all — only statistics, only over groups big enough that nobody can be reverse-engineered. That’s the control data clean rooms sell (Google’s Ads Data Hub won’t return a row unless ≥50 users are behind it). DataCharter gives you the same class of control in one YAML block, written the way you’d say it:

policies:
  crm.customers:
    - aggregates only
    - groups of at least 10
    - no joins to payments

The sentences compile deterministically — no model, no ambiguity; an unrecognized sentence is a load error. Prefer explicit keys? Same meaning:

You can also edit these in the app: Govern → Studio. The visual editor writes the same policies:, pii:, and row_filters: blocks, and the YAML pane is the file that will be saved.

policies:
  crm.customers:
    aggregate_only: true
    min_group_size: 10
    no_joins_to: [payments]

What each rule enforces (agent surface only)

  • aggregates only — a query touching the relation must be a single plain aggregate SELECT (aggregate functions, GROUP BY welcome). Raw rows, DISTINCT, CTEs, and set operations are refused with an error that tells the agent how to comply. Analysis uses DuckDB’s own parser and fails closed: a query too odd to certify is refused, not waved through.
  • groups of at least k — k-anonymity. The query is rewritten so any group with fewer than k rows is suppressed from the result (the clean-room standard), and the result carries a warning saying so. Group counts are computed after row-level security, and when several policied relations are involved the strictest k wins.
  • no joins / no joins to a, b — the relation may not be queried together with other relations (deliberately broader than literal JOINs — a UNION counts too).

Agents aren’t left guessing: describe_table on a policied relation returns its rules ("policies": ["aggregates only", …]), so a well-behaved agent writes a conforming query on the first try. Refusals land in the flight recorder like any errored access. The human SQL editor is unaffected — policies govern the agent surface, like masking and row filters.

Try it

uvx datacharter serve examples/ecommerce
# the example's crm table carries: aggregates only · groups of at least 2

Ask the agent for “average customer count by region” — works. Ask it to “list customer emails” — refused, with the policy quoted back.

No LLM handy? Policies bind the agent surface, so your own SQL in the editor won’t trigger them — but any MCP client will. Point one at the workspace (datacharter mcp <dir>, config on the MCP page) and call the query tool with SELECT * FROM <policied relation>: you get the refusal text back, and the refusal itself lands in the flight recorder. The tour demo (datacharter init demo --demo --tour) ships a policied table and a seeded refusal to inspect.

Next: Prove it happened — audit →