Agent-First PSQL v0.12.0: The Host You Named
A refused SSH setting used to fall through to a direct connection, reaching whatever answered on the local port instead of the database behind SSH. A lost transaction used to drop its queued statements into autocommit. Both now stop where they are. Plus a streamed one-shot query that could deadlock, a pipe that could stop reading stdin, and SSH options cut to an allowlist.
The previous release was about a database telling you something did not happen when it did. This one is about afpsql doing something you did not ask for — connecting somewhere else, or running a statement outside the transaction it was written for — and about the places where it could simply stop moving.
The host you named, or nothing
When a connection goes over SSH, afpsql decides first whether it needs the SSH stdio bridge. If that decision failed — because an SSH setting was refused — the error was dropped and the attempt fell through to the ordinary direct connect. With the host and port from the DSN. On the local machine.
So a target that was supposed to be a database behind a jump host could instead
reach whatever happened to answer on localhost:5432: a developer’s own
PostgreSQL, a tunnel left over from yesterday, another project’s container. The
query ran, against the wrong database, and nothing said so.
A refused SSH setting now ends the connection attempt with a connection error.
The SSH destination, the container-over-SSH path, kubectl pod names and the
remote runtime word all go through the same validation, so none of them can
smuggle an option in as a leading -.
The bridge also stops accepting connection options it cannot honour. It carries
the wire protocol over the ssh child’s stdin and stdout, so no local socket is
ever opened, and connect_timeout, tcp_user_timeout, the keepalives family,
target_session_attrs and load_balance_hosts configured nothing.
connect_timeout=5 looked set and bounded nothing. They are rejected by name
now, before the bridge starts.
SSH options are an allowlist
Explicit SSH options used to be passed through, which made the ordinary
entrypoint a way to hand ssh a ProxyCommand or an identity path. They are
limited to ConnectionAttempts, ConnectTimeout, Port, ProxyJump,
ServerAliveCountMax, ServerAliveInterval and TCPKeepAlive. Everything else
belongs in ~/.ssh/config, where SSH already knows how to read it.
Custom container runtimes follow the same reasoning: the ordinary entrypoint uses a fixed set, and only an administrator-locked profile can select another.
A lost transaction stays lost
An agent sends begin, then several statements, then commit — often all at
once, without waiting for each answer. If the connection died inside that
transaction, the session cleared its transaction state and went back to
autocommit. The statements still in the queue then ran, one by one, each
committing on its own, on a fresh connection. Half a transaction, applied.
The same happened when begin itself failed: the writes queued behind it ran
in autocommit.
A failed begin or a lost transaction now leaves the session in a failed
transaction state. Every query on that session is rejected without reaching
PostgreSQL, and a new begin is refused, until rollback settles it. A
commit settles it too, reporting transaction_lost_rolled_back. Both that
code and commit_outcome_unknown — a write whose fate needs checking before
anything else happens — are now listed in the reference’s error codes, where
they had been missing.
Never rerun a write for its output
A truncated UPDATE … RETURNING has already committed. The skill and reference
used to suggest rerunning with --stream-rows to get the full result, which for
a write means doing it again. They now say plainly: choose --stream-rows
before the first write when the full result matters, and after truncation use a
separate SELECT — which may not be able to reconstruct the old values, and
that is the honest answer.
The atomic write example also opens its session read-write, which the example previously forgot.
Not stopping
A streamed one-shot query could deadlock. The CLI ran the query to
completion and only then drained its output. With --stream-rows, the query
sends batches into a bounded channel; once it filled, the query waited for the
drain and the drain waited for the query. Both now run together.
The pipe could stop reading stdin. Admitting work to a session waited for
space in that session’s worker queue, so a busy session blocked the reader —
including the cancel that would have freed it. Each queue now has 64 slots and
a full one rejects new work immediately with a retryable invalid_request;
cancellation and shutdown are always read.
A configuration patch reserves room in every affected queue before it changes
anything, so it applies to all of them or none. An accepted begin keeps the
configuration its permission check saw, even if a later patch changes the
session before it runs.
Shutdown is bounded. EOF and close share one five-second deadline across
draining workers and closing database clients. Work still waiting at that
point is cancelled locally. The close acknowledgement does not claim that an
unfinished write did or did not happen — that still needs checking against the
database.
A cancel is sent once. A successful cancel token used to be followed by a
pg_cancel_backend fallback connection anyway. The fallback is now only for
when the token fails. A driver whose server stops answering mid-query is also
released on close instead of being held open waiting for a reply that will not
come.
Smaller things
The reference and design docs named --*-secret-env and --*-secret-config
flags that no longer exist. They describe what the CLI actually accepts:
--dsn, --conninfo and --password, each a literal, env:NAME, or
file:PATH#DOT_PATH.
The test script gained a minimal mode that builds without default features
and checks both binaries’ help and version discovery, and integration tests can
run against a private native PostgreSQL cluster instead of a container.
Breaking changes
- Explicit SSH options are limited to an allowlist. Put anything else in
~/.ssh/config. - Custom container runtimes require an administrator-locked profile.
- The SSH bridge rejects
connect_timeout,tcp_user_timeout,keepalives*,target_session_attrsandload_balance_hostsinstead of ignoring them. - A failed
beginor lost transaction blocks the session untilrollback. Wait for thebeginresult before sending a transaction’s writes. - A full session queue rejects new work with a retryable
invalid_request. - Agent-First UI 0.6.1 and Agent-First Data 0.35.0 are the pinned dependencies.
Getting it
$ brew install agentfirstkit/tap/afpsql
$ cargo install agent-first-psql