Agent-First PSQL v0.7.0: Read the Secret From the Config
v0.7.0 lets afpsql read a connection secret straight from the application's own JSON, TOML, YAML, or dotenv config file — `--dsn-secret-config FILE DOT_PATH` and its conninfo/password siblings — resolved once, in-process, with no `jq`/`yq` subprocess and no secret in argv or shell history. Configured secrets always render as `***` in runtime config output regardless of source, and `afpsql-readonly` is redefined as a PostgreSQL write guard rather than a host sandbox: it restores SQL files, config sources, SSH options, container runtimes, redirects, and skill management while still refusing writes.
An agent that needs to reach PostgreSQL usually has the credentials already —
sitting in the application’s config file, next to everything else the service
uses. Before v0.7.0, getting them into afpsql meant a detour through the shell:
--dsn-secret "$(yq .database.url config.yaml)", a jq subprocess, an exported
environment variable. Every one of those routes a live secret through argv or
shell history and spawns a process whose only job is to re-read a file afpsql
could read itself.
v0.7.0 removes the detour. It also sharpens what read-only means: a boundary around the database write, not a sandbox around the host.
Connect straight from the config file
Every connection secret slot gains a *-secret-config source — one per slot,
mutually exclusive with the direct and *-secret-env forms:
afpsql --dsn-secret-config config.yaml database.url_secret --sql 'select 1'
afpsql --conninfo-secret-config .env PG_CONNINFO --sql 'select 1'
afpsql --host db --user app --dbname app \
--password-secret-config secrets.toml postgres.password --sql 'select 1'
The argument is always two values — FILE DOT_PATH — never the --flag=FILE
form. afpsql reads the file once, at startup, in-process through the
Agent-First Data document layer: no jq, no yq, no command substitution, no
subprocess. JSON (.json), TOML (.toml), YAML (.yaml/.yml), and dotenv
(.env, .env.*, *.env) are detected by extension. Because the value is read
directly into the process, it never enters argv or a temporary environment
variable, so it never lands in shell history or ps output — and the config
echo and startup logs report only the source kind and the file/dot-path, never
the resolved value.
The value is read verbatim
A connection secret is usually a DSN, so v0.7.0 is deliberate about what it does
— and does not do — to the string. The document layer returns the value
byte-for-byte: it does not URL-decode, does not expand $ in dotenv, does not
trim, and does not coerce a non-string into text. That means afpsql hands
PostgreSQL exactly what the file holds, so a real DSN should be percent-encoded
the way libpq expects — p%40ss for a @ in the password, %3A for a :.
The one thing to watch is your file format’s own string rules: a double-quoted
dotenv value or a TOML basic "..." string still processes escapes (\t
becomes a tab), so store a value that contains backslashes unquoted or in a
single-quoted '...' literal. afpsql adds no escaping of its own; it only reads.
Secrets stay redacted, whatever their source
Once a secret is configured, it should never reappear in output — and in v0.7.0
that is true regardless of where it came from. Runtime config output renders
dsn_secret, conninfo_secret, and password_secret as *** whether the
source was a direct flag, an environment variable, or a config file, across
JSON, YAML, and plain rendering:
{"kind":"result","result":{"code":"config","dsn_secret":"***","host":"db","user":"app","dbname":"app"},"trace":{"duration_ms":0}}
Startup logging follows the same rule: it may record that a secret came from a config file at a given path and dot-path, but never the secret itself.
Read-only is a database boundary, not a host sandbox
afpsql-readonly — the ordinary read-only entrypoint — used to blur two
different jobs: refusing database writes, and locking down the host. v0.7.0
draws the line where it belongs. The ordinary readonly profile is now defined as
a PostgreSQL write guard: it enforces the read-only transaction boundary and
nothing more.
In practice that means a read-only agent gets back the capabilities it legitimately needs — running SQL from a file, naming arbitrary explicit secret environment variables, resolving the new config secret sources, passing SSH options, choosing a custom container runtime, redirecting output, and managing its own skill install — while every one of those still runs inside the read-only permission boundary. Reads are allowed; writes are refused. The boundary is about the database, not the filesystem.
For deployments that do need the host-level lockdown, that layer still exists, separately: administrator-locked readonly profiles remain the fixed-target, fixed-transport deployment surface, pinning afpsql to one database over one boundary. v0.7.0 keeps the strict option for operators and hands the everyday one back to agents.
Upgrading
afpsql 0.7.0 is a drop-in upgrade for existing read/write usage — the connection
flags, output envelopes, and permission model are unchanged. The additions are
the three *-secret-config sources and the widened ordinary-readonly
capability set. If you relied on afpsql-readonly to block file or SSH access,
move that enforcement to an administrator-locked profile, which still pins target
and transport.