Repository navigation
Releases: a7ex-turcan/rtfq
Release list
rtfq 0.7.5
Field-reported: PostgreSQL sources with SSL Mode=Require could not connect at all.
Fixed
- PostgreSQL sources can connect over TLS. Field-reported against three AWS Aurora sources: any DSN with
SSL Mode=Requirefailed client-side, before reaching the server, with "Transport security hasn't been
enabled". The adapter builds its data source with Npgsql's slim builder for NativeAOT, which leaves TLS out
unless it is opted into, and it never was. Not a regression: it was missing in every release from 0.1.0
through 0.7.4, and went unnoticed because no test ever ran against a TLS-enabled server. - A DSN with no
SSL Modeno longer silently downgrades to plaintext. Found while fixing the above, and
the more serious of the two. Npgsql's default isPrefer, and a client that cannot speak TLS "prefers" its
way straight to an unencrypted connection - so against a server offering TLS, every query and result crossed
the network in cleartext, with no error and no warning. It now negotiates TLS whenever the server offers it.
Enabling the capability forces nothing: SSL Mode=Disable still connects in plaintext, and certificate
verification is untouched - VerifyFull against an untrusted certificate is still refused at the handshake.
Every case is covered by a new suite against a real TLS-enabled PostgreSQL, and was run against the published
NativeAOT binary.
rtfq 0.7.4
Field-reported: describe_table served a stale cache and reported an existing table as missing, so an agent
verifying a migration concluded it had not run.
Fixed
describe_tableno longer reports an existing table as missing from a stale cache. Field-reported: a table
created after the cached snapshot was captured came back as "no table X in source", so an agent verifying a
migration concluded it had not run — it had. A miss against a stale snapshot is now confirmed against the
live source before it is reported absent; only a stale hit keeps serving-stale-then-refreshing behind the
response (ADR 0003), so probing missing names cannot stampede the catalog. This also dissolves the reported
divergence wheredescribe_sourceanddescribe_tabledisagreed on the cache age seconds apart: the
confirming re-read leaves both fresh.- A missing table is
request.table_unknown, notpolicy.source_unknown. Conflating the two let a present,
permitted source read as absent. The message is honest about what it knows: when the source was reachable it
says the schema is current; when it was not, it says the table may exist and could not be confirmed, rather
than asserting absence.
Added
refreshis an MCP tool. It re-reads a source's schema from the live database, for the moment right after
a migration when you need discovery to reflect it at once. It existed as a CLI command and an HTTP endpoint,
and bothdescribe_table's error and theexplore_sourceprompt already told agents to use it — so the
runtime was directing agents to a tool that did not exist, which is the API lying about what it can do.
Next up is M5: the Docker image, a quickstart timed on a clean machine, and the security posture
document written for whoever has to approve pointing this at production.
rtfq 0.7.3
Added
- An icon. Embedded in the Windows executable as a six-frame
.ico(16 through 256), so it shows in
Explorer, the taskbar and file properties. Set only for Windows targets: Linux and macOS binaries have nowhere
to put one, and setting the property unconditionally makes those publishes warn about a resource they cannot
use. The release bundles carryassets/rtfq.pngbecause the bundled README references it.
Changed
- RTFQ now expands as Remote Tool For Queries. Same four letters, safe on a shared screen, and a truer
description besides: reaching data sources on a machine you cannot otherwise get to is the whole pitch. no tokennow says where the value comes from. "pass --token or set RTFQ_TOKEN" is accurate and useless
in the case people actually hit — standing on the server box, where the config's secrets are exported and the
client variable is not, with nothing to suggest the two want the same string.
Documentation
- The approvals walkthrough gained the three things a field report showed were missing: that the agent cannot
approve its own write and no tool will ever let it; that nothing notifies you, with both answers
(--watchfor one operator, a webhook for a team); and when to turn approval on at all — production,
where a small in-bounds wrong write is what no structural gate catches, rather than a dev database, where
signing off every scratch write is how people end up removing the gate everywhere. - Says where to run
rtfq approvals --watch: it is a client, not a server component, so your own machine is
usually the right place. Several people can watch at once — the first answer wins and the request leaves
everyone else's queue.
rtfq 0.7.2
Changed
-
Server log lines carry a timestamp, in UTC and on one line:
2026-08-21 14:03:43.874 info: Microsoft.Hosting.Lifetime[14] Now listening on: ...UTC because the audit journal writes ISO-8601 UTC, and correlating a console line with a journal entry is
something done while an incident is running rather than at leisure. One line because a stamped two-line record
puts the time on one line and the message on the next, which defeats grep. Thertfq servestartup summary is
unchanged: it is a banner, not a log stream.
Next up is M5: the Docker image, a quickstart timed on a clean machine, and the security posture
document written for whoever has to approve pointing this at production.
rtfq 0.7.1
Field-reported: an approval-required source left an agent looping on pending with no reachable way to approve.
Added
rtfq approvals --watchstays open and reports requests as they arrive, prompting[a/d/s]when there is
a person at the keyboard and printing the commands when there is not. This is the answer to "nobody was
notified" for a single operator; a webhook provider remains the answer for a team. Skipping is a first-class
answer and the default for anything unrecognised, because the safe direction is always "not yet".
Fixed
- A proposal awaiting approval now says who has to do what. It carries the
approval_id, and the hint names
the exact command —rtfq approvals --approve <id> --as NAME— and states plainly that nobody has been
notified. The previous wording, "a human has been asked to approve this exact diff", was wrong twice over:
nobody had been asked, and the caller had no id to quote, so a change could only sit until it lapsed. Every
pendingresponse repeats it, since an agent polling is the one being asked "so what do I do?". commit_write's description never mentioned approval, so an agent was told to confirm the diff and commit
while the runtime did something else. It now says the call answerspendinguntil a person approves it outside
the session, that the agent cannot approve it itself, and not to poll in a loop.propose_writesays the same.
Reported from real use; it is the defect class CLAUDE.md added a working agreement about two releases ago.
Next up is M5: the Docker image, a quickstart timed on a clean machine, and the security posture
document written for whoever has to approve pointing this at production.
rtfq 0.7.0
Added
-
Four MCP prompts -
diagnose_slow_query,explore_source,investigate_recordandpropose_fix. These
are procedures, not capabilities: each is carried out through the existing tools, so everything they lead to
passes the same gates, and none asserts anything about your data.Prompts rather than a tenth tool, deliberately. A tool's description sits in the model's context all session -
which is why CLAUDE.md says adding one needs an argument - while a prompt is listed by name and its body only
fetched when it is run. The server now declares thepromptscapability and answersprompts/listand
prompts/get. -
A failed read now says what to do about it. A timeout points at
explainon the same statement, because
the plan is what distinguishes a missing index from a query that was always going to read the table, and an
agent that only learns "too slow" retries a variant and is slow again. A dialect error points at
describe_table, which answers a column-name question from cache. Carried in the error'sdetailrather than
folded into the message, so a client can show or suppress it without parsing prose. -
A truncated
queryresponse now also namesexplain, alongside what it already said about narrowing.
Nothing is added to responses that went fine.
Fixed
- CI reported success on a publish that failed.
docker run ... | tee publish.logreturnedtee's exit
code, so an AOT trim error passed the publish step; the two steps after it then ran against a binary that had
never been produced and passed as well, one of them printinghighest glibc symbol required:with nothing
after it. The pipeline now setspipefail, the glibc check asserts the binary exists and that objdump found
symbols, and a trim or AOT analysis error in RTFQ's own code is caught and named rather than surfacing as a
linker exit code.
rtfq 0.6.0
Added
-
writable_tablesaccepts patterns.dbo.*covers a schema;*covers everything. Previously the
allow-list was exact-match only, and the answer to "make this dev schema writable" was a hundred lines of
YAML — which is the kind of friction that ends with somebody removing the gate entirely. See
ADR 0008.Three things did not move:
deny_tablesis still evaluated first and still wins, an empty allow-list still
means nothing is writable, and matching is still ordinal and case-sensitive. An entry with no*matches
exactly as before, so existing configs are unaffected.The cost, stated plainly: a pattern covers tables that do not exist yet, so
dbo.*includes whatever is
created next month.rtfq validatenow warns per pattern (source.writable_wildcard) rather than letting a
gate get wider quietly, and on anything that mattersrequire_approvalmoves from advisable to load-bearing. -
scripts/windows/— PATH, environment, self-signed certificate and firewall rule, one script each. They
came out of a real deployment rather than being written to look tidy, so the comments carry the traps: the
certificate script needs PowerShell 7 becauseExportPkcs8PrivateKeydoes not exist in 5.1, a Windows service
cannot see your user environment, and a SAN must name what callers actually dial.scripts/README.mdcollects
those in one place. -
examples/rtfq.mssql.yaml— several SQL Server databases plus a PostgreSQL, the shape of a typical
internal estate, including two databases that share a host and are still two sources.
Documentation
CLAUDE.mdgains a working agreement: the API must never lie to an agent about what it can do. A stale
hint or a hard-coded capability flag is a defect of the same order as a broken gate, because an agent plans
from what discovery tells it. M4 shipped three, and all three passed the suite.CLAUDE.mdstates the one exception to secrets are referenced:server.tls.certandserver.tls.keytake
paths, and a diagnostic must never echo the value when they do not.
Next up is M5: the Docker image, a quickstart timed on a clean machine, and the security posture document
written for whoever has to approve pointing this at production.
rtfq 0.5.0
M4 — approval and the time-boxed unlock, the control CLAUDE.md calls the central defence against a
well-formed malicious write. See ADR 0007.
Added
- Human approval on a commit. A source with
require_approval: truenow queues its proposal for a person
instead of refusing it. The approver sees the statement and the affected rows as they are now, and nothing
else — there is no field in which an agent can supply a summary, because the case this gate exists for is an
agent persuaded by a poisoned row. rtfq approvalslists what is waiting, with--approve ID --as NAMEand--deny ID --as NAMEto answer it.
The decision and the approver land in the audit log beside the exact statement.rtfq unlock SOURCE --write --ttl 15mandrtfq lock SOURCE. Withrequire_unlock: truea source is
shut at runtime even where the config permits writing, until somebody deliberately opens it. TTL is clamped to
an hour, expiry is evaluated on read rather than swept, and a restart re-locks — not configurable.- A webhook approval provider, selected with
approval: {mode: webhook, endpoint: ...}. This is how a Slack
integration is built without Slack living in core: NativeAOT rules out loading plugin assemblies, so the
boundary is HTTP. Anything the endpoint says that is not a recognised verdict — a 500, a timeout, malformed
JSON, an unreachable host — is treated as not yet decided, never as approval. approval_ttlindefaults:(10m), and theapproval:config section, both validated byrtfq validate.
Changed
- An approval-required proposal holds no transaction open while a human decides. It runs, captures the diff,
and rolls straight back. Commit re-runs the statement and refuses unless the diff is identical to the one that
was approved. Holding a transaction across a human's attention span blocks readers on SQL Server and holds back
VACUUMon PostgreSQL; a gate that expensive gets switched off. describe_tablenow reportswritabletruthfully — the intersection of source access, token grant and the
write allow-list. It had been hard-coded tofalsesince before M3, telling agents they could not write to
tables they could.- The MCP hint on an approval-required proposal explained that commit would be refused "until an approver exists
(M4)". It now says nothing is held, thatcommit_writeanswerspendinguntil somebody decides, and when the
request lapses — the old text would have an agent abort instead of wait. - An HTTP source refusing a non-GET no longer says writes "arrive in M3". HTTP sources are read-only by design:
the write path needs a transaction that can capture before-images and roll back, and a request that has been
sent cannot be un-sent.
Security
- An approval binds to one change, not a shape of change. The fingerprint covers the statement, the affected
count and the before-images, so if the rows move while somebody is deciding, the commit is refused and rolled
back rather than applied to data nobody approved. - Seeing or answering the approval queue requires a token with write access to some source. A read-only agent has
no business in the queue that exists to police it. - Opening a source requires the access being opened, and an unlock for
writedoes not openschema. server.tls.certandserver.tls.keytake a path, unlike every other secret in the file. Writing
${file:...}there substituted the PEM and then reported it as a missing filename — printing a private key
into the terminal, and into whatever CI log or screenshot the error landed in. That case is now named rather
than echoed, and any over-long value in the diagnostic is truncated.
Changed — configuration
- The sample configs renamed their token secrets:
RTFQ_TOKEN_AGENTis nowRTFQ_AGENT_SECRETin
rtfq.dev.yaml, andRTFQ_READONLY_SECRET/RTFQ_WRITER_SECRETinrtfq.multi.yaml. The old names sat one
character away fromRTFQ_TOKEN, the client's own variable, while meaning something entirely different — one
is a secret the server's config resolves, the other is the bearer token a client presents. These are names
the sample chooses, not names RTFQ knows, so this affects nobody's deployment: if your config says
${env:RTFQ_TOKEN_AGENT}, it still works.
Documentation
CLAUDE.md's illustrative config wrote TLS ascert: ${file:/etc/rtfq/tls.crt}, which is exactly the form
that does not work. Corrected to paths, and the snippet now passesrtfq validate --productionas written.- The README covers putting
rtfqon your PATH on each platform, and reaching the server from another
machine: certificate, config, firewall, and the reasons not to point the port at the public internet. Every
command in both sections was run before it was written down.
rtfq 0.4.0
M3 — the write path, for PostgreSQL and SQL Server. See
ADR 0006.
Added
propose_write,commit_write,abort_writeon the HTTP API and as MCP tools. A proposal runs the
statement inside a transaction and stops: it reports the real number of rows changed and the rows as they were
beforehand, and nothing is saved until it is committed.- The four structural gates — source access, token grant, target allow-list, statement guard — each with its
own refusal code.writable_tables,deny_tables,max_affected_rowsandrequire_approvalper source. - Before-image journaling. The affected rows are captured inside the same transaction, at repeatable read so
the capture and the mutation see the same rows, and written to the audit log at propose time. - Additive schema changes per ADR 0002, gated at the
subcommand level and requiringaccess: schema.
Security
- The affected-row cap is enforced against the driver's real count from the uncommitted execution, never an
estimate. One row over rolls back and refuses with the count. - Unqualified
UPDATE/DELETEare refused, and so are trivially-true predicates (WHERE 1=1,WHERE true,
… OR true) — aWHERE-presence test is not enough. - Deny rules apply to everything a statement touches, so a write to an allowed table that reads a denied one
through a subquery is refused. - Handles are single-use, owned by their creator, capped at four open per source, and roll back on abort, on
expiry, and on shutdown. - A write nested inside another statement — a
DELETEin a CTE — is refused: it has no unambiguous target, so
the allow-list and the cap would be guessing.
Known gaps
- MongoDB and HTTP writes are refused. Mongo needs a replica set and its own adversarial suite; HTTP has no
transaction to leave open. require_approvalrefuses the commit rather than queuing it. There is no approver until M4.- On SQL Server an open proposal blocks readers of the affected rows, where PostgreSQL shows them the
pre-image. EnableREAD_COMMITTED_SNAPSHOT, and treat the handle TTL as an availability control there. - The unbounded-statement pre-check is still not implemented; an uncommitted runaway does its work before being
rolled back, bounded only bystatement_timeoutandlock_timeout.
rtfq 0.3.0
M2 — SQL Server, MongoDB and HTTP adapters. The interface held; see
ADR 0005.
Added
- SQL Server (
kind: mssql) viaMicrosoft.Data.SqlClient, with a read guard on Microsoft's own ScriptDom,
catalog introspection including row estimates and indexes, andSHOWPLAN_ALLforexplain. - MongoDB (
kind: mongodb). Statements are command documents —{"find": "orders", "filter": {…}}— which is
Mongo's own dialect. Schema is inferred from sampled documents and says so, reporting every observed type
for a field (total double|int32) rather than picking one. - HTTP APIs (
kind: http). Statements are request lines:GET /v1/invoices?status=open. Paths must be
explicitly allow-listed; an empty allow-list reaches nothing rather than everything. - A shared adapter conformance suite every adapter runs unmodified against a real containerised instance.
- Startup capability checks. Some declarations can only be tested against a live source, so
rtfq validate
stays offline and the server verifies at boot. A source that cannot support its declared access stops startup;
a source that is merely unreachable is a warning.
Changed
ISourceAdapter.CheckAsyncreturns capabilities instead of void, because MongoDB's transaction support depends
on deployment topology and cannot be known at construction.- Which kinds exist and what they can do moved into
AdapterCatalog; config validation asks the adapter layer
rather than keeping its own list. This was the one leak the interface audit found.
Security
- Guards for all three new dialects, each covering its own version of "not a write command, but catastrophic":
T-SQLEXEC/sp_executesql/xp_cmdshell(dynamic SQL the parse tree cannot see into), Mongo's$outand
$merge(aggregation stages that write a collection) and$where/$function(server-side JavaScript), and
for HTTP a wildcard path combined with a write method, which is now a config error. - All guards are verified in the published AOT binary against real servers, not only under the JIT.
Known gaps
- ⚠ Linux hosts now need
libicuinstalled.Microsoft.Data.SqlClientrefuses to run in invariant
globalization mode, so that setting had to be turned off. Windows and macOS are unaffected. - Linux artifacts now target glibc 2.34, so Debian 12, Ubuntu 22.04 and RHEL 9 are supported. M2's new
dependencies had briefly raised the floor to 2.38 — enough to exclude all three — because a binary links
against whatever glibc built it. Linux builds now happen inside an Ubuntu 22.04 container and CI asserts the
ceiling, since this is the kind of thing that drifts upward without anyone noticing. - The binary is now ~66 MB, up from 20 MB.
- Still reads only; the write path is M3.