Skip to content

Releases: a7ex-turcan/rtfq

rtfq 0.7.5

Choose a tag to compare

@github-actions github-actions released this 30 Sep 07:51

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=Require failed 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 Mode no longer silently downgrades to plaintext. Found while fixing the above, and
    the more serious of the two. Npgsql's default is Prefer, 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

Choose a tag to compare

@github-actions github-actions released this 03 Sep 05:00

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_table no 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 where describe_source and describe_table disagreed on the cache age seconds apart: the
    confirming re-read leaves both fresh.
  • A missing table is request.table_unknown, not policy.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

  • refresh is 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 both describe_table's error and the explore_source prompt 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

Choose a tag to compare

@github-actions github-actions released this 24 Aug 14:47

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 carry assets/rtfq.png because 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 token now 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
    (--watch for 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

Choose a tag to compare

@github-actions github-actions released this 21 Aug 14:23

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. The rtfq serve startup 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

Choose a tag to compare

@github-actions github-actions released this 21 Aug 13:55

Field-reported: an approval-required source left an agent looping on pending with no reachable way to approve.

Added

  • rtfq approvals --watch stays 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
    pending response 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 answers pending until a person approves it outside
    the session, that the agent cannot approve it itself, and not to poll in a loop. propose_write says 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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 15:23

Added

  • Four MCP prompts - diagnose_slow_query, explore_source, investigate_record and propose_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 the prompts capability and answers prompts/list and
    prompts/get.

  • A failed read now says what to do about it. A timeout points at explain on 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's detail rather than
    folded into the message, so a client can show or suppress it without parsing prose.

  • A truncated query response now also names explain, 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.log returned tee'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 printing highest glibc symbol required: with nothing
    after it. The pipeline now sets pipefail, 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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 12:12

Added

  • writable_tables accepts 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_tables is 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 validate now warns per pattern (source.writable_wildcard) rather than letting a
    gate get wider quietly, and on anything that matters require_approval moves 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 because ExportPkcs8PrivateKey does not exist in 5.1, a Windows service
    cannot see your user environment, and a SAN must name what callers actually dial. scripts/README.md collects
    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.md gains 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.md states the one exception to secrets are referenced: server.tls.cert and server.tls.key take
    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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 08:04

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: true now 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 approvals lists what is waiting, with --approve ID --as NAME and --deny ID --as NAME to answer it.
    The decision and the approver land in the audit log beside the exact statement.
  • rtfq unlock SOURCE --write --ttl 15m and rtfq lock SOURCE. With require_unlock: true a 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_ttl in defaults: (10m), and the approval: config section, both validated by rtfq 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
    VACUUM on PostgreSQL; a gate that expensive gets switched off.
  • describe_table now reports writable truthfully — the intersection of source access, token grant and the
    write allow-list. It had been hard-coded to false since 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, that commit_write answers pending until 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 write does not open schema.
  • server.tls.cert and server.tls.key take 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_AGENT is now RTFQ_AGENT_SECRET in
    rtfq.dev.yaml, and RTFQ_READONLY_SECRET / RTFQ_WRITER_SECRET in rtfq.multi.yaml. The old names sat one
    character away from RTFQ_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 as cert: ${file:/etc/rtfq/tls.crt}, which is exactly the form
    that does not work. Corrected to paths, and the snippet now passes rtfq validate --production as written.
  • The README covers putting rtfq on 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

Choose a tag to compare

@github-actions github-actions released this 17 Aug 15:35

M3 — the write path, for PostgreSQL and SQL Server. See
ADR 0006.

Added

  • propose_write, commit_write, abort_write on 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_rows and require_approval per 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 requiring access: 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/DELETE are refused, and so are trivially-true predicates (WHERE 1=1, WHERE true,
    … OR true) — a WHERE-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 DELETE in 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_approval refuses 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. Enable READ_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 by statement_timeout and lock_timeout.

rtfq 0.3.0

Choose a tag to compare

@github-actions github-actions released this 17 Aug 08:51

M2 — SQL Server, MongoDB and HTTP adapters. The interface held; see
ADR 0005.

Added

  • SQL Server (kind: mssql) via Microsoft.Data.SqlClient, with a read guard on Microsoft's own ScriptDom,
    catalog introspection including row estimates and indexes, and SHOWPLAN_ALL for explain.
  • 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.CheckAsync returns 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-SQL EXEC/sp_executesql/xp_cmdshell (dynamic SQL the parse tree cannot see into), Mongo's $out and
    $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 libicu installed. Microsoft.Data.SqlClient refuses 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.