Agent-First PSQL v0.7.0: Read the Secret From the Config

by Agent-First Kit Contributors

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 expectsp%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.