Read-only, three independent ways
Deleting any one of these still leaves a working product. They are not redundant belts on the same braces — each fails differently.1
Your own credential
The credential you create grants
SELECT and nothing else. TeamDuo generates the
command; you run it. This is the only layer outside TeamDuo’s code, and the only one
that survives a total compromise of it.2
The execution envelope
PostgreSQL —
SET TRANSACTION READ ONLY plus a statement timeout. The server itself
refuses a write.BigQuery — no legacy SQL, a job timeout, and a cap on bytes billed.MongoDB — weaker, and worth saying so: there is no read-only transaction. What
stands in its place is that the driver issues only aggregate, and the stage allowlist
contains neither $out nor $merge. That rests on TeamDuo’s code being right in a way
the other two don’t, which is why layer 1 matters most there.3
The exposure gate
Layers 1 and 2 stop writes, but would happily read a table you never agreed to share.
This is the layer that enforces exposure:
- PostgreSQL — the query is parsed with the real PostgreSQL grammar.
- BigQuery — the decision comes from a dry run, so the referenced tables and the cost come from BigQuery’s own planner.
- MongoDB — a structured pipeline is walked stage by stage. There is no string to parse.
Denials say what was denied
A refusal always names what was blocked — “tablepublic.salaries is not exposed”, “this
query would scan 42 GB, the limit is 1 GB”. An agent that is told why corrects itself; an
agent given a generic error retries blind.
The one honest gap
A BigQuery dry run reports which tables a query reads, never which columns. Column level exposure is therefore not enforceable on BigQuery, and TeamDuo refuses a column-restricted BigQuery table outright rather than approximating the rule. It generates aCREATE VIEW for your own project instead, where BigQuery enforces it.
An approximation would be worse than an absence, because only the approximation gets
mistaken for enforcement.