Agent-First PSQL v0.13.0: Read-Only Is a Transaction Mode
afpsql-readonly let `;COMMIT` through, accepted ProxyJump hops that could reopen command execution, and checked a pipe session's container runtime without the environment it would connect with. All three are closed, and the docs now say plainly what a read-only transaction does not stop. Plus explicit connection fields that beat environment DSNs, secrets kept out of parse errors, and a reconciled storage inspection report.
afpsql-readonly is the entrypoint you hand an agent when it should look and
not touch. It runs every statement inside a read-only transaction and keeps the
connection path to a fixed set of options. This release closes three ways
around that, and is honest about what a read-only transaction never promised.
Three ways around readonly, closed
PostgreSQL drops empty statements, so ;COMMIT is a single COMMIT and passes
the one-statement limit. The readonly classifier stopped at the ;, found no
keyword, and let it through. A pipe client could end the real transaction
behind afpsql’s back, or, with prepared transactions enabled, leave a
PREPARE TRANSACTION holding its locks. Leading ; and vertical tabs are now
skipped like whitespace, and SET transaction_read_only counts as transaction
control too.
ProxyJump is on the readonly SSH allowlist, but OpenSSH runs a jump through a
shell-built ProxyCommand, and older OpenSSH releases pass the host through
unchecked. An allowed ProxyJump could reopen the command execution the
allowlist keeps out. Each hop must now be [user@]host[:port], with IPv6 in
brackets, and may not start with -.
The container runtime variables (AFPSQL_CONTAINER_*_RUNTIME) are merged when
afpsql connects. CLI mode merged them while parsing, so the readonly policy saw
them, but a session added by a pipe config request was checked without them
and then ran the custom runtime anyway. The policy now judges the configuration
as it will connect. A pinned profile never reads them.
What a read-only transaction does not stop
A read-only transaction stops writes to tables. It does not stop a privileged
role from running CLUSTER, REINDEX, ANALYZE or LOCK TABLE, from
COPY ... TO a file or a program, from lo_export, from terminating other
backends, or from writing through dblink_exec. The docs now say so: readonly
is a transaction mode, not a privilege boundary. If the role must not do those
things, give it a role that cannot.
The target you named
An explicit --host, --port, --user or --dbname was silently discarded
whenever AFPSQL_DSN_SECRET or AFPSQL_CONNINFO_SECRET was set, so afpsql
connected to the environment’s target instead of the one you named. Explicit
fields now override the environment’s connection string, and an explicit
connection string wins over either environment form.
The environment is read more carefully too:
- an invalid port in a selected environment variable is rejected instead of falling through;
- when empty
AFPSQL_*variables leave libpq’s ownPG*variables in charge, afpsql reports those fallbacks rather than hiding them; - percent-encoded TLS options in a DSN are decoded before the TLS policy checks
them, so an escaped
sslmodecannot slip past it.
Secrets stay out of parse errors
A connection string is a secret as a whole. The parser’s diagnostics could quote an unknown option or a malformed value, and with it a password. Parse errors now carry only afpsql’s own text and a hint about where to look.
Storage inspection
afpsql inspect storage returns one report of where a database’s bytes are:
the database size before and after, every relation’s forks counted once, the
files in the database’s directories grouped by kind, PGroonga’s database-wide
files, and a reconciliation that says how much is accounted for and how much is
not. Every query is read-only and reads no application rows, so it runs through
afpsql-readonly and locked profiles too.
Each section reports complete, partial or unknown with its reason, and a
missing basis comes out as null rather than a made-up zero. A failed initial
connection names its cause: connect_failed, invalid_request or
storage_inspection_failed.
Smaller things
- The container bridge writes all socket data to stdout even when a write is short.
- The test fixtures and CI run PostgreSQL 18.6, and the container bridge image is on Ubuntu 26.04.
Breaking changes
afpsql-readonlyrefuses;COMMIT-style statements andSET transaction_read_only.afpsql-readonlyaccepts only host-shapedProxyJumphops.- Explicit connection fields and connection strings override environment DSNs. Unset the environment variable if you relied on it winning.
- An invalid environment port is an error.
- Agent-First UI 0.9.0 and Agent-First Data 0.36.1 are the pinned dependencies, and Rust 1.99 is the minimum.
Getting it
$ brew install agentfirstkit/tap/afpsql
$ cargo install agent-first-psql