# ops/ — operator + agent query tooling Read-only query access to Thermograph data, uniform across environments. ## `dbq.sh` — Postgres, any environment ```sh infra/ops/dbq.sh dev "select count(*) from climate_history" infra/ops/dbq.sh prod -tA -c "select max(date) from climate_history" infra/ops/dbq.sh beta -c '\dt' echo "select 1" | infra/ops/dbq.sh prod -f - ``` Environments: `dev` (LAN compose stack on the dev machine), `beta` (75.119.132.91), `prod` (169.58.46.181). Extra args pass straight through to `psql` (`-tA`, `-x`, `--csv`, `-f`, …); stdin is forwarded. ### Why it execs into the container instead of using a connection string None of the databases are exposed over TCP — each listens only on its private docker network. Prod's is a Swarm **overlay** (`10.0.2.0/24`) that the host cannot route to, so `ssh -L` works for beta but is *impossible* for prod; publishing 5432 would mean new ufw rules plus a Swarm endpoint change on production. Running `psql` inside the db container works identically in all three environments with no ports, no tunnels, and no infra changes. The prod container name is a Swarm task name that changes on every redeploy, so it is resolved at call time via `docker ps --filter name=…`, never hardcoded. ### Safety Queries connect as **`thermograph_ro`** — a `NOSUPERUSER` role granted only `pg_read_all_data`. Read-only is enforced by Postgres, not by convention: ``` $ infra/ops/dbq.sh prod -c "create table t(i int)" ERROR: permission denied for schema public ``` The app's own `thermograph` role is a superuser and is deliberately **not** used by this tool. If the role is ever missing (fresh database), recreate it with: ```sql CREATE ROLE thermograph_ro LOGIN; GRANT CONNECT ON DATABASE thermograph TO thermograph_ro; GRANT pg_read_all_data TO thermograph_ro; ``` ## Iceberg (planned) **Implementation spec: [`ICEBERG-HANDOFF.md`](./ICEBERG-HANDOFF.md)** — the contract, environment constraints, acceptance criteria and anti-goals for the Iceberg worker. Iceberg lands as a sibling in this directory following the same three conventions, so the query surface stays uniform: 1. **Env-keyed dispatch** — `iceberg.sh ""`, same argument shape as `dbq.sh`. 2. **Run the engine where the data is reachable** — the catalog/warehouse dictates it (a Trino/DuckDB/pyiceberg invocation, containerised the same way), rather than exposing new network endpoints. 3. **A dedicated read-only identity** — the Iceberg equivalent of `thermograph_ro`, so the same "read-only enforced by the system, not by convention" property holds.