EDR 0027 Status: accepted Implementation: none
Be psql first, then be better than psql
`marque psql` accepts psql's flags, meta-commands and output formats so it can be aliased in place. Catalog introspection is a named statement class that runs under the role without approval, logged in aggregate.
TL;DR
alias psql='marque psql'
psql -h prod-primary -U settings_writer -c 'select count(*) from accounts'
marque psql is a client, emulating psql's interface rather than wrapping it: the same flags,
the same meta-commands, the same output formats, the same exit codes. It talks to Marque's API
directly and needs no local port — the loopback proxy
(EDR-0022) exists for the tools we cannot replace.
The decision this forces: \dt is a catalog query, and requiring an approval to list tables would
make the tool unusable. So catalog introspection becomes a named statement class — read-only,
restricted to an allowlist of catalog and information-schema relations, executed under the role's own
privileges, needing no marque, and logged in aggregate rather than one entry per keystroke.
Longer term the aim is to be better than psql, which Marque is unusually placed to do: it already parses every statement, knows the schema, and knows your scope. It can tell you a statement is outside your delegation before you press enter.
Context
Familiarity is the adoption lever. An operator who has typed psql for fifteen years has muscle
memory in their fingers, \d in their reflexes, and a .psqlrc they have tuned. Asking them to
learn a new client to do the same job is a tax on the routine work Marque most needs to capture —
and taxed routes get avoided, which is how a direct credential ends up in a password manager.
Aliasing is the strongest possible version of "keep your tools": nothing about the operator's habits changes, and the brokering is invisible until it needs not to be.
The awkward part is meta-commands. psql's \dt, \d, \l and \df are not protocol features —
they are client-side conveniences that expand into pg_catalog queries. Running them through the
approval machinery unchanged would mean an approval request to list tables, which is absurd; running
them outside the machinery would mean an unbrokered path to the database, which is the one thing this
system does not have. Neither is acceptable, so introspection needs to be a class with its own
stated rules rather than a special case somebody added.
Decision
psql compatibility
Flags, matching semantics: -c, -f, -h, -p, -U, -d, -A, -t, -F, -x, -q,
-e, -v, -X, --csv. -h accepts a Marque target name as well as a hostname, and -U accepts
a role name — so an aliased invocation reads naturally.
Meta-commands, first release: \?, \h, \q, \c, \l, \dt, \d, \d+, \dn, \dv,
\di, \df, \du, \x, \timing, \pset, \a, \t, \o, \i, \set, \unset, \watch,
\g.
Output formats: aligned, unaligned, expanded and CSV, with \pset behaving as expected. Exit
codes follow psql's: 0 success, 1 a client-side fatal error, 2 a connection error, 3 a script
error under ON_ERROR_STOP.
Refused, with a specific message rather than a confusing error:
| Refused | Why |
|---|---|
\copy, COPY |
bulk data movement is not brokered in the first release (EDR-0022) |
-1 / --single-transaction, BEGIN |
a marque authorises a statement set decided in advance |
\e, \!, and the |command argument form of \o / \g / \w |
shelling out from a brokering client is a surprising amount of authority. Stated as a capability — the client contains no code path that spawns a process — rather than as a list of meta-command names, because a list goes stale |
.psqlrc |
read only with -X semantics inverted: it is not read by default, because a file that silently issues statements at startup is the wrong default here. --rcfile opts in |
That last one is a deliberate deviation from psql and will surprise someone; it is called out in
--help rather than left to be discovered.
Catalog introspection as a statement class
A statement is introspection when all of the following hold:
- it is read-only, and inside the checkable grammar (EDR-0007) — with one exemption: its relations may be allowlisted catalog views, notwithstanding that record's base-table rule, which the allowlist itself discharges;
- every relation it reads is on the engine's allowlist of catalog and information-schema views;
- every function it calls is on the purity allowlist.
Introspection then:
- runs under the role, with no marque and no approval. But note what actually bounds it: on
PostgreSQL the role's privileges do not meaningfully restrict catalog reads — most of
pg_catalogis world-readable, so "the database decides what the catalog shows" does no work here. The reviewed allowlist is the control, not the role. Where a meta-command has aninformation_schemaequivalent, Marque prefers it, since those views are privilege-filtered; and where it does not, Marque conjoinshas_table_privilege/has_column_privilegeinto the query it composes, so the answer reflects what this role may actually see. - The class discloses object definitions, and column exclusion cannot fix that. Excluding
pg_proc.prosrcwas the original answer and it is insufficient: function bodies, view definitions, column defaults and check expressions are all reachable through definition-returning functions (pg_get_functiondef,pg_get_viewdef,pg_get_expr, andpg_get_function_sqlbodyfor theBEGIN ATOMICbodies that live inpg_proc.prosqlbodyrather thanprosrc) that a naive purity allowlist admits. The rule is a closure invariant, not a list: a function that can return the value of an excluded column is itself excluded from the purity allowlist for this class — otherwise every new definition-returning function reopens the channel. So those functions are excluded from the purity allowlist for this class, and the honest statement stands inSECURITY.md: introspection is a read channel over object definitions, and a deployment keeping secrets in a function body must treat it as one. - The
information_schemapreference is a deviation from psql, and is stated as one. Those views are privilege-filtered, which is why they are preferred — and it means\dtshows fewer objects than real psql, which lists tables the role cannot read. That is a compatibility difference the suite must assert deliberately rather than discover. - is logged in aggregate: a per-session rollup naming the principal, the target, the role, the
count and the distinct relations touched. One entry per
\dwould drown the logbook and make the entries that matter harder to find (EDR-0012). - is quota'd like everything else (ZFN-18).
The allowlist is relations, not schemas. "Anything in pg_catalog" would be wrong: that schema
contains functions and views that read files and system state, and an allowlist by schema would
inherit whatever an extension adds to it later. Extensions are the specific reason this is a list
that gets reviewed rather than a prefix match.
Introspection is not a privilege escalation path, and the test suite says so: a statement that joins an allowlisted catalog view to a user table is not introspection — it is an ordinary read against a user table, and it takes the ordinary path.
Already better than psql
Escalation is inline, in the first release. A referred statement shows the chain, who is being
waited on and a share link, and the prompt says when it clears
(EDR-0038); \urgent and \breakglass are
available mid-session (EDR-0037). This was listed below as a later
ambition until those records shipped it.
Better than psql, later
Direction rather than commitment, recorded so it is not re-litigated:
- Scope shown before you run. Marque knows your delegation and can parse as you type, so a statement heading outside your scope can say so before you press enter rather than after.
- Rehearsal on demand.
\rehearsereturns the measured row count without submitting anything. - Schema-aware completion that reflects what your role can actually reach, rather than everything.
None of this is in the first release, and none of it changes the security model — each is Marque surfacing what it already knows.
Consequences
Easier.
alias psql='marque psql'is close to a zero-friction migration for the people whose habits matter most.- Brokering becomes invisible for the routine case and loud only when it needs to be, which is the right shape for a control people should not resent.
- Introspection has a stated rule instead of accumulating as exceptions.
Harder.
- psql's surface is large and its behaviour is precise. Partial compatibility is worse than none, because an alias implies completeness: a missing meta-command shows up as a broken tool rather than an unimplemented feature. This is a long tail with a compatibility test suite behind it.
- The introspection allowlist is a security-relevant list that must be maintained, per engine, and reviewed when extensions change. A wrong entry is a read path nobody approved.
- Aggregate logging is a deliberate loss of detail. If someone enumerates a schema before an attack, the logbook has the rollup and not the sequence. That is the accepted trade for a usable logbook, and it is stated here so it is not discovered during an investigation.
- Refusing
.psqlrcby default deviates from the tool being emulated, which is exactly the kind of small surprise that erodes trust in an emulation.
New obligations.
- A compatibility suite runs real psql and
marque psqlagainst equivalent inputs and compares output, because "behaves like psql" is a claim that decays silently. - The introspection allowlist is reviewed with the purity allowlist (EDR-0007); they have the same failure mode and should not drift apart.
References
- EDR-0022 — the server-side emulation, for tools that cannot be replaced.
- EDR-0006 — why introspection is safe without an approval.
- ZFN-4 — the interface reached for during an incident should be the familiar one.
Changelog
- 2026-08-15: Accepted.
- 2026-08-16: Amended after the expert panel's should-fix pass: corrected the reasoning behind catalog introspection — on PostgreSQL the role does not bound catalog reads, so the reviewed allowlist is the control — made the allowlist column-aware, and restated the shell-out refusal as a capability.
- 2026-08-16: Amended after the second panel's should-fix pass: excluding
pg_proc.prosrcwas insufficient — definition-returning functions reach view bodies, defaults and check expressions — so those are excluded from the purity allowlist for this class and the read channel is stated in SECURITY.md. Also recorded that preferringinformation_schemamakes\dtshow fewer objects than real psql, which is a deliberate compatibility deviation. - 2026-08-16: Amended in the second panel's should-fix pass: named
prosqlbodyandpg_get_function_sqlbodyalongside their siblings, and restated the exclusion as a closure invariant — a function that can return the value of an excluded column is itself excluded — since a list goes stale. - 2026-08-16: Amended for the emergency paths and operator surfaces:
\\urgentand\\breakglassare available mid-session, and a referred statement shows the chain and a share link (EDR-0037, EDR-0038). - 2026-08-16: Terminology and staleness fix: moved inline escalation out of "later" — EDR-0038 ships it in the first release, so listing it as an ambition was stale the day that record landed.