sql-harness 0.3.2__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sql_harness-0.3.2 → sql_harness-0.3.4}/PKG-INFO +2 -2
- {sql_harness-0.3.2 → sql_harness-0.3.4}/SKILL.md +33 -9
- {sql_harness-0.3.2 → sql_harness-0.3.4}/docs/AGENTS.md +2 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/docs/README.md +1 -1
- {sql_harness-0.3.2 → sql_harness-0.3.4}/docs/connections-toml-example.toml +17 -2
- {sql_harness-0.3.2/skills/sql-harness/references → sql_harness-0.3.4/docs}/install.md +16 -3
- {sql_harness-0.3.2 → sql_harness-0.3.4}/docs/maintainer.md +16 -4
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/ssh/auth-and-tunnels.md +23 -8
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/transactions.md +1 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/pyproject.toml +2 -1
- {sql_harness-0.3.2 → sql_harness-0.3.4}/sql-harness +1 -1
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/analytics.py +6 -2
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/cli.py +98 -41
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/config.py +19 -2
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/drivers/__init__.py +19 -2
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/drivers/mysql.py +30 -2
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/drivers/postgres.py +33 -2
- sql_harness-0.3.4/src/sql_harness/drivers/readonly.py +265 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/drivers/redis.py +6 -1
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/drivers/sqlite.py +15 -2
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/drivers/ssh.py +73 -18
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/helpers.py +14 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/manager.py +5 -1
- sql_harness-0.3.4/src/sql_harness/output.py +309 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/run.py +14 -13
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/test_cli.py +237 -1
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/test_config.py +58 -0
- sql_harness-0.3.4/tests/unit/test_output.py +161 -0
- sql_harness-0.3.4/tests/unit/test_readonly.py +185 -0
- sql_harness-0.3.4/tests/unit/test_ssh.py +228 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/uv.lock +1 -1
- sql_harness-0.3.2/.claude-plugin/marketplace.json +0 -26
- sql_harness-0.3.2/.claude-plugin/plugin.json +0 -13
- sql_harness-0.3.2/docs/install.md +0 -145
- sql_harness-0.3.2/skills/sql-harness/README.txt +0 -8
- sql_harness-0.3.2/tests/unit/test_ssh.py +0 -54
- {sql_harness-0.3.2 → sql_harness-0.3.4}/.env.example +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/.gitignore +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/AGENTS.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/agent-workspace/README.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/agent-workspace/agent_helpers.example.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/agent-workspace/agent_helpers.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/agent-workspace/zones/example_pg/scripts/sh_demo_crud.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/agent-workspace/zones/example_pg/skills/sh_demo-schema.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/agent-workspace/zones/meta/skills/pool.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/agent-workspace/zones/meta/skills/workspace.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/docs/headless-notes.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/docs/standalone-install.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/aggregations-advanced.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/aggregations.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/encoding-and-charset.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/joins-advanced.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/joins.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/large-result-sets.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/migrations.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/mysql/json-columns.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/pooling.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/btree-indexes.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/indexes-and-explain.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/json-columns.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/migrations.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/plan-reading.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/slow-queries-joins.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/specialized-indexes.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/postgres/table-optimization.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/recursive-ctes-advanced.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/recursive-ctes.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/save-run-cycle.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/schema-introspection.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/ssh/docker-via-ssh.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/ssh/ssh.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/timeouts-and-cancellation.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/window-functions-advanced.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/window-functions.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/interaction-skills/zone-skill-auto-surface.md +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/__init__.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/agent_loader.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/src/sql_harness/paths.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/__init__.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/conftest.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/integration/__init__.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/integration/test_mysql.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/integration/test_pg_optimization.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/integration/test_pgexercises_practice.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/integration/test_postgres.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/integration/test_ssh.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/__init__.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/test_agent_loader.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/test_analytics.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/test_helpers.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/test_manager.py +0 -0
- {sql_harness-0.3.2 → sql_harness-0.3.4}/tests/unit/test_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sql-harness
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace.
|
|
5
5
|
Project-URL: Source, https://github.com/zhaoliuxue/much_bigpy/tree/master/lab/sql_harness
|
|
6
6
|
Project-URL: Issues, https://github.com/zhaoliuxue/much_bigpy/issues
|
|
@@ -37,7 +37,7 @@ uv sync
|
|
|
37
37
|
uv run sql-harness init
|
|
38
38
|
|
|
39
39
|
# 3. Add a connection
|
|
40
|
-
uv run sql-harness add local_pg --driver postgres --url 'postgresql://postgres:postgres@localhost:5432/postgres'
|
|
40
|
+
uv run sql-harness add --name local_pg --driver postgres --url 'postgresql://postgres:postgres@localhost:5432/postgres'
|
|
41
41
|
|
|
42
42
|
# 4. Test it
|
|
43
43
|
uv run sql-harness test local_pg
|
|
@@ -7,7 +7,7 @@ description: "Always use sql-harness for SQL, SSH, and database operations — q
|
|
|
7
7
|
|
|
8
8
|
A single-process SQL + SSH CLI for agents. 5 drivers (`postgres`, `mysql`, `sqlite`, `redis`, `ssh`) share one heredoc namespace. Connections live in plaintext in one TOML file.
|
|
9
9
|
|
|
10
|
-
For setup, read `install.md
|
|
10
|
+
For setup, read `install.md` (shipped next to this file; in a source checkout it's `docs/install.md`). For stuck-point mechanics, see `interaction-skills/` (cross-DB) and `interaction-skills/postgres/` (PG depth).
|
|
11
11
|
|
|
12
12
|
## Reach for sql-harness when...
|
|
13
13
|
|
|
@@ -26,9 +26,9 @@ For setup, read `install.md`. For stuck-point mechanics, see `interaction-skills
|
|
|
26
26
|
```bash
|
|
27
27
|
sql-harness --help
|
|
28
28
|
sql-harness list
|
|
29
|
-
sql-harness add <name> --driver postgres --url 'postgresql://user:pw@host/db'
|
|
30
|
-
sql-harness add <name> --driver mysql
|
|
31
|
-
sql-harness add <name> --driver ssh
|
|
29
|
+
sql-harness add --name <name> --driver postgres --url 'postgresql://user:pw@host/db'
|
|
30
|
+
sql-harness add --name <name> --driver mysql --url 'mysql+pymysql://user:pw@host/db'
|
|
31
|
+
sql-harness add --name <name> --driver ssh --url 'ssh://user@host:22?key=/path/id_ed25519'
|
|
32
32
|
sql-harness test <name>
|
|
33
33
|
|
|
34
34
|
sql-harness <<'PY'
|
|
@@ -48,7 +48,7 @@ PY
|
|
|
48
48
|
|
|
49
49
|
## Drivers & capabilities
|
|
50
50
|
|
|
51
|
-
Five drivers, one heredoc namespace. URL scheme must match the backend: MySQL requires `mysql+pymysql://`; PostgreSQL accepts `postgres://` / `postgresql://` (normalized to `postgresql+psycopg://`); SSH accepts `ssh://`, `ssh+password://`, `ssh+key://`.
|
|
51
|
+
Five drivers, one heredoc namespace. URL scheme must match the backend: MySQL requires `mysql+pymysql://`; PostgreSQL accepts `postgres://` / `postgresql://` (normalized to `postgresql+psycopg://`); SSH accepts `ssh://`, `ssh+password://`, `ssh+key://`. SSH password auth may come from the connection's standalone `password` key instead of the URL (precedence: URL > `password` > `$BH_SSH_PASSWORD`); a connection that sets a password uses password auth exclusively, ignoring local `~/.ssh` keys.
|
|
52
52
|
|
|
53
53
|
### Postgres (`postgres://`)
|
|
54
54
|
|
|
@@ -71,7 +71,7 @@ Standard SQLAlchemy + PyMySQL. JSON syntax differs from PG — see `interaction-
|
|
|
71
71
|
|
|
72
72
|
### SQLite (`sqlite:///path.db`)
|
|
73
73
|
|
|
74
|
-
Local dev / tests. All standard helpers work. Use `sql-harness add --driver sqlite --url 'sqlite:///<absolute-path>'` (SQLite needs absolute path).
|
|
74
|
+
Local dev / tests. All standard helpers work. Use `sql-harness add --name <name> --driver sqlite --url 'sqlite:///<absolute-path>'` (SQLite needs absolute path).
|
|
75
75
|
|
|
76
76
|
### Redis (`redis://host:port`)
|
|
77
77
|
|
|
@@ -103,7 +103,7 @@ See `interaction-skills/ssh/` for: `ssh.md` (helpers + CLI + gotchas), `docker-v
|
|
|
103
103
|
| `list_tables(schema=None)` | `list[str]` | Schema recon — always start here |
|
|
104
104
|
| `describe(table, schema=None)` | `list[dict]` | Columns + types — before writing SQL |
|
|
105
105
|
| `table(name, schema=None, limit=None)` | `list[dict]` | Quick peek: `SELECT * FROM x LIMIT n` |
|
|
106
|
-
| `with_transaction()` | context manager yielding `Connection` | ≥ 2 statements / DDL
|
|
106
|
+
| `with_transaction()` | context manager yielding `Connection` | ≥ 2 DML statements / atomicity (DDL: PG+SQLite only) |
|
|
107
107
|
| `run_sql_file(path)` | `list[dict]` | Multi-statement `.sql`, one transaction |
|
|
108
108
|
| `server_version()` | `str` | Round-trip liveness check |
|
|
109
109
|
| `connection_info()` | `dict` | Current workspace metadata |
|
|
@@ -159,7 +159,8 @@ Reached via `apply_skill("pool")` etc. from any active zone — `zones/meta/skil
|
|
|
159
159
|
|
|
160
160
|
- **Start with `list_tables` + `describe`, not `query`.** Before writing SQL on an unknown DB, run `list_tables(schema=None)` then `describe("table", schema=None)` to learn the columns. Skips the trial-and-error SELECT cycle.
|
|
161
161
|
- **Set `BH_SQL_ZONE_SKILLS=1` once** so every `use_workspace()` surfaces that zone's per-DSN skill docs as a hint list (10 filenames max). Then `apply_skill(name)` reads the body when relevant.
|
|
162
|
-
- **Default to `with_transaction()`** for any write with ≥ 2 statements (insert + update,
|
|
162
|
+
- **Default to `with_transaction()`** for any write with ≥ 2 statements (insert + update, update + delete, …). Automatic rollback on exception — **for DML**. DDL is a different story: PostgreSQL and SQLite roll back `CREATE`/`DROP`/`ALTER` inside a transaction, **MySQL does not** (it implicitly commits), so a `drop + create` migration that fails halfway leaves you with neither table. Run DDL migrations with a backup or a reversible plan, not on the strength of the transaction. See `interaction-skills/migrations.md`.
|
|
163
|
+
- **Transactions are only real on a transactional engine.** A MySQL table created on a server whose `@@default_storage_engine` is not InnoDB (e.g. MyISAM) ignores `ROLLBACK` *silently* — the write sticks and nothing warns you. Check with `SELECT @@default_storage_engine`, and write `ENGINE=InnoDB` explicitly if you need rollback.
|
|
163
164
|
- **For PG performance tasks, reach for the 8 PG-only helpers first** (`explain_analyze`, `slow_queries`, `unused_indexes`, `seq_scan_heavy`, `table_stats`, `index_usage_stats`, `missing_indexes_hint`). Don't hand-parse `pg_stat_*` SQL.
|
|
164
165
|
- **For remote tasks, reach for SSH workspace first.** Don't reach for raw `subprocess` + ssh-key files in agent code — `ssh_exec("systemctl restart myapp")` is one line.
|
|
165
166
|
- **For "insert returning the new id"**: psycopg supports `INSERT … RETURNING id`; MySQL has no `RETURNING` — use `execute(...)` and read `result.lastrowid`.
|
|
@@ -184,11 +185,34 @@ Reached via `apply_skill("pool")` etc. from any active zone — `zones/meta/skil
|
|
|
184
185
|
## Gotchas
|
|
185
186
|
|
|
186
187
|
- Driver label in TOML must match the URL scheme (`postgres` ⇄ `postgresql://`, `mysql` ⇄ `mysql+pymysql://`, `ssh` ⇄ `ssh://` / `ssh+password://` / `ssh+key://`).
|
|
187
|
-
- Passwords in TOML are plaintext; use `${env:VAR}` indirection for prod secrets.
|
|
188
|
+
- Passwords in TOML are plaintext; use `${env:VAR}` indirection for prod secrets. SSH accepts a standalone `password` key (keeps the URL clean); when one is set, local `~/.ssh` keys are ignored.
|
|
188
189
|
- `query()` is for SELECTs only. For INSERTs, use `execute()`.
|
|
189
190
|
- For tables > 10k rows, use `engine.connect().execution_options(stream_results=True)` and iterate manually — `query()` loads everything into memory.
|
|
190
191
|
- SSH SFTP subsystem may be disabled on the remote host. `ssh_upload/ssh_download` raise `RuntimeError("SFTP subsystem not available")`; fall back to `tar -czf - | ssh host tar -xzf -` via `ssh_exec`.
|
|
191
192
|
- Default `ssh_exec` timeout is 30 s. Long-running commands (e.g. `pg_dump`, `tar czf`) need explicit `timeout=`.
|
|
193
|
+
- **Long output is spilled to a file, not printed.** A failed run whose traceback is long, and any `ssh exec` / `ssh run-script` output past a few KB, are written to `$BH_SQL_HOME/tmp/output/` and replaced by a pointer line naming the path, line count and size. That is deliberate — read the file when you actually need it. `BH_SQL_TRACEBACK=1` forces the raw traceback to stderr instead. The full traceback always also lands in `$BH_SQL_HOME/sql-harness.log` (NDJSON), whatever the terminal shows.
|
|
194
|
+
|
|
195
|
+
## Read-only connections (`read_only = true`)
|
|
196
|
+
|
|
197
|
+
For a sensitive database, mark the connection read-only. Two rails are installed at the driver layer, both attached to the **engine** rather than to `query()`/`execute()` — so `conn.execute()`, `run_sql_file()` and any path that skips the helpers are covered too.
|
|
198
|
+
|
|
199
|
+
```toml
|
|
200
|
+
[[connections]]
|
|
201
|
+
name = "prod_ro"
|
|
202
|
+
driver = "mysql"
|
|
203
|
+
url = "mysql+pymysql://ro_user@host:3306/proddb"
|
|
204
|
+
read_only = true
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
sql-harness add --name prod_ro --driver mysql --url '...' --read-only
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
What it refuses: anything whose verb is not `SELECT`/`SHOW`/`DESCRIBE`/`EXPLAIN`/`TABLE`/`VALUES`/`PRAGMA`, plus transaction control. **Unrecognised statements are refused, not permitted** — that is why it is an allow-list: a deny-list misses `CALL`, `DO`, `LOAD DATA`, `HANDLER` and every future statement type. It also handles the dodges a naive keyword match falls for: `/*!50000 DROP */` (MySQL executes those), `SELECT 1; DROP TABLE t`, `WITH x AS (...) DELETE`, `EXPLAIN ANALYZE DELETE` (PostgreSQL executes it), and `#` — which is a comment in MySQL but the XOR operator in PostgreSQL, so treating it as a comment there would hide a write. On MySQL it additionally refuses `INTO OUTFILE`/`INTO DUMPFILE`/`FOR UPDATE`/`LOCK IN SHARE MODE`; on PostgreSQL, `SELECT ... INTO` (creates a table) and the `FOR ...` lock variants.
|
|
212
|
+
|
|
213
|
+
Where the backend has a session setting, it is applied on **every new pooled connection**: `SET SESSION TRANSACTION READ ONLY` (MySQL) / `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY` (PostgreSQL). That rail is enforced by the server, so it catches anything that slips past the statement guard (e.g. a stored procedure).
|
|
214
|
+
|
|
215
|
+
**This is a guard rail, not a security boundary.** Both rails are escapable from the same session — a caller can run `SET SESSION TRANSACTION READ WRITE` and carry on, and any client-side check is pattern matching rather than a parser. The only hard boundary is the database account: use `GRANT SELECT ON db.* TO ...` for a database you genuinely must not write to, and treat everything above as defence in depth on top of it. SQLite has no session rail at all; for a file database the real boundary is the URL (`sqlite:///file:path?mode=ro&uri=true`). SSH workspaces ignore the flag — a remote shell is not a statement stream.
|
|
192
216
|
|
|
193
217
|
## Domain / table skills
|
|
194
218
|
|
|
@@ -12,10 +12,12 @@ Core code lives in `lab/sql_harness/src/sql_harness/`:
|
|
|
12
12
|
- `config.py` — TOML schema + ConnectionConfig / ConnectionsConfig dataclasses
|
|
13
13
|
- `paths.py` — XDG-style state directory resolution
|
|
14
14
|
- `drivers/` — backend abstractions (postgres, mysql, redis stub, sqlite for tests)
|
|
15
|
+
- `drivers/readonly.py` — read-only statement guard shared by the SQL drivers: comment/literal-stripping, allow-list verb check, and `install_guard()` which attaches both rails to an engine. Read its module docstring before relying on it — it is a guard rail, not a security boundary
|
|
15
16
|
- `manager.py` — SqlHarness class: holds engines, workspaces, skills registry
|
|
16
17
|
- `helpers.py` — heredoc helpers (auto-imported)
|
|
17
18
|
- `agent_loader.py` — loads `$BH_SQL_AGENT_WORKSPACE/agent_helpers.py`
|
|
18
19
|
- `analytics.py` — event bus (observer) + JSON stats + NDJSON execution log: CLI call counts, doc-read tracking, command/error history; `BH_SQL_ANALYTICS` (default ON), view with `sql-harness stats`
|
|
20
|
+
- `output.py` — terminal output policy: UTF-8 stream setup, long-output spill to `$BH_SQL_HOME/tmp/output/`, concise error rendering; `BH_SQL_TRACEBACK=1` forces raw tracebacks
|
|
19
21
|
- `run.py` — heredoc entry; delegates to CLI otherwise
|
|
20
22
|
- `cli.py` — argparse subparser CLI (18 subcommands)
|
|
21
23
|
|
|
@@ -14,7 +14,7 @@ uv sync
|
|
|
14
14
|
uv run sql-harness init
|
|
15
15
|
|
|
16
16
|
# 3. Add a connection
|
|
17
|
-
uv run sql-harness add local_pg --driver postgres --url 'postgresql://postgres:postgres@localhost:5432/postgres'
|
|
17
|
+
uv run sql-harness add --name local_pg --driver postgres --url 'postgresql://postgres:postgres@localhost:5432/postgres'
|
|
18
18
|
|
|
19
19
|
# 4. Test it
|
|
20
20
|
uv run sql-harness test local_pg
|
|
@@ -18,7 +18,9 @@ echo = false
|
|
|
18
18
|
# Connections
|
|
19
19
|
# ---------------------------------------------------------------------------
|
|
20
20
|
# Each [[connections]] block defines a named profile. Drivers: postgres, mysql,
|
|
21
|
-
# redis (stub), sqlite. URL must match the driver.
|
|
21
|
+
# redis (stub), sqlite, ssh / ssh+password / ssh+key. URL must match the driver.
|
|
22
|
+
# Optional keys: description, password, pool_size, pool_recycle, pre_ping, echo,
|
|
23
|
+
# application_name. `${env:VAR}` is expanded in both `url` and `password`.
|
|
22
24
|
|
|
23
25
|
[[connections]]
|
|
24
26
|
name = "local_pg"
|
|
@@ -51,11 +53,24 @@ driver = "ssh"
|
|
|
51
53
|
url = "ssh://app@10.0.0.5:2222"
|
|
52
54
|
description = "Production application host (key auth via $BH_SSH_KEY or ~/.ssh/id_ed25519)"
|
|
53
55
|
|
|
56
|
+
# Password auth, credentials kept out of the URL (recommended). Each connection
|
|
57
|
+
# carries its own password, so switching workspaces needs no env fiddling.
|
|
58
|
+
# Precedence: URL-embedded password > this field > $BH_SSH_PASSWORD.
|
|
59
|
+
# Note: a connection with a password uses password auth exclusively — local
|
|
60
|
+
# ~/.ssh keys are not consulted.
|
|
61
|
+
[[connections]]
|
|
62
|
+
name = "iot_server"
|
|
63
|
+
driver = "ssh"
|
|
64
|
+
url = "ssh://113.44.193.72:22"
|
|
65
|
+
password = "${env:IOT_SSH_PASSWORD}"
|
|
66
|
+
description = "IoT host (password auth)"
|
|
67
|
+
|
|
68
|
+
# URL-embedded password also works (driver label may be ssh or ssh+password).
|
|
54
69
|
[[connections]]
|
|
55
70
|
name = "lab-server"
|
|
56
71
|
driver = "ssh+password"
|
|
57
72
|
url = "ssh+password://lab:lab@192.168.1.10:22"
|
|
58
|
-
description = "Lab box (password auth)"
|
|
73
|
+
description = "Lab box (password auth, password in URL)"
|
|
59
74
|
|
|
60
75
|
# ---------------------------------------------------------------------------
|
|
61
76
|
# Optional per-connection pool overrides (alternative syntax)
|
|
@@ -27,7 +27,8 @@ Writes `~/.config/sql-harness/connections.toml` (or `$BH_SQL_HOME/connections.to
|
|
|
27
27
|
## 3. Add your first connection
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
uv run sql-harness add
|
|
30
|
+
uv run sql-harness add \
|
|
31
|
+
--name my_pg \
|
|
31
32
|
--driver postgres \
|
|
32
33
|
--url 'postgresql://user:pw@localhost:5432/db' \
|
|
33
34
|
--description "My dev DB"
|
|
@@ -79,9 +80,21 @@ name = "prod_pg"
|
|
|
79
80
|
driver = "postgres"
|
|
80
81
|
url = "${env:PROD_PG_URL}" # env-var indirection
|
|
81
82
|
pool_size = 10
|
|
83
|
+
|
|
84
|
+
[[connections]]
|
|
85
|
+
name = "iot_server"
|
|
86
|
+
driver = "ssh"
|
|
87
|
+
url = "ssh://113.44.193.72:22"
|
|
88
|
+
password = "${env:IOT_SSH_PASSWORD}" # standalone secret — keeps the URL clean
|
|
82
89
|
```
|
|
83
90
|
|
|
84
|
-
`${env:VAR}` placeholders inside `url` are expanded at load time from the process environment — keep prod secrets out of the file.
|
|
91
|
+
`${env:VAR}` placeholders inside `url` and `password` are expanded at load time from the process environment — keep prod secrets out of the file.
|
|
92
|
+
|
|
93
|
+
Optional per-connection keys: `description`, `password`, `read_only`, `pool_size`, `pool_recycle`, `pre_ping`, `echo`, `application_name`.
|
|
94
|
+
|
|
95
|
+
`read_only = true` refuses writes on that connection — a statement allow-list on every statement the engine sends, plus the server-side session read-only setting where the backend has one. Use it for sensitive databases, but note it is a **guard rail, not a security boundary**: the session setting is escapable from the same session, and any client-side check is pattern matching. Pair it with a `SELECT`-only database account, which is the actual boundary. See `SKILL.md` → "Read-only connections".
|
|
96
|
+
|
|
97
|
+
SSH password precedence (highest first): password embedded in `url` > the `password` key > `$BH_SSH_PASSWORD`. A connection that sets a password uses password auth exclusively — local `~/.ssh` keys are not consulted (see `interaction-skills/ssh/auth-and-tunnels.md`).
|
|
85
98
|
|
|
86
99
|
## Configuration env vars
|
|
87
100
|
|
|
@@ -110,7 +123,7 @@ pool_size = 10
|
|
|
110
123
|
| `ssh` / `ssh+password` / `ssh+key` | remote shell + SFTP (paramiko) | `paramiko>=3.4` | ✅ |
|
|
111
124
|
|
|
112
125
|
To enable Redis: `uv add "redis>=5.0,<6"` then implement `drivers/redis.py`.
|
|
113
|
-
To add another SSH host: append a `[[connections]]` block with `driver = "ssh"` and an `ssh://...` URL.
|
|
126
|
+
To add another SSH host: append a `[[connections]]` block with `driver = "ssh"` and an `ssh://...` URL. Use the `password` key for password auth, or `?key=/abs/path` for a key outside the default locations. `sql-harness test <name>` probes SSH with a remote `echo` instead of `SELECT 1`.
|
|
114
127
|
|
|
115
128
|
## Troubleshooting
|
|
116
129
|
|
|
@@ -17,14 +17,26 @@ hand-synced copies. `pyproject.toml` `[tool.hatch.build.targets.wheel.force-incl
|
|
|
17
17
|
maps the repo docs into the wheel:
|
|
18
18
|
|
|
19
19
|
- `SKILL.md` → `sql_harness/_skills/SKILL.md`
|
|
20
|
+
- `docs/install.md` → `sql_harness/_skills/install.md`
|
|
20
21
|
- `interaction-skills` → `sql_harness/_skills/interaction-skills`
|
|
21
22
|
- `agent-workspace/zones/meta/skills` → `sql_harness/_skills/zones-meta-skills`
|
|
22
23
|
|
|
23
24
|
The root `SKILL.md` is the **single source of truth** (v0.3.0 consolidation —
|
|
24
25
|
the old `skills/sql-harness/SKILL.md` and `src/sql_harness/SKILL.md` copies
|
|
25
|
-
were removed
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
were removed; v0.3.4 removed the last hand-synced copy,
|
|
27
|
+
`skills/sql-harness/references/install.md`). After editing it, just
|
|
28
|
+
`uv build` again. `sql-harness skill` reads `sql_harness/_skills/SKILL.md`
|
|
29
|
+
from the installed package and falls back to the repo-root `SKILL.md` in a
|
|
30
|
+
source checkout.
|
|
31
|
+
|
|
32
|
+
`SKILL.md` references `install.md` as a sibling file — that only resolves
|
|
33
|
+
because `docs/install.md` is force-included into the bundle as
|
|
34
|
+
`_skills/install.md`. Dropping it from the force-include list breaks the
|
|
35
|
+
reference for every `skill install` user.
|
|
36
|
+
|
|
37
|
+
`skill install` needs a real (non-editable) wheel install to work: it reads
|
|
38
|
+
`sql_harness/_skills/`, which only exists after `uv build`. In a bare source
|
|
39
|
+
checkout `_bundled_skills_root()` returns `None` and the command exits 2.
|
|
28
40
|
|
|
29
41
|
## Build outputs
|
|
30
42
|
|
|
@@ -44,7 +56,7 @@ The wheel contains the package modules + the `_skills/` doc bundle
|
|
|
44
56
|
```bash
|
|
45
57
|
# standalone venv
|
|
46
58
|
uv sync
|
|
47
|
-
uv run pytest tests/unit -q #
|
|
59
|
+
uv run pytest tests/unit -q # 133 tests must pass
|
|
48
60
|
uv run sql-harness --help # CLI works
|
|
49
61
|
uv run sql-harness skill # SKILL.md emit works
|
|
50
62
|
```
|
|
@@ -16,15 +16,29 @@
|
|
|
16
16
|
|
|
17
17
|
### 1.1 三种 scheme 对照
|
|
18
18
|
|
|
19
|
-
`drivers/ssh.py
|
|
19
|
+
`drivers/ssh.py` 认这三种 URL 形式:
|
|
20
20
|
|
|
21
|
-
| URL 形式 | 何时用 |
|
|
21
|
+
| URL 形式 | 何时用 | 认证方式 |
|
|
22
22
|
|---|---|---|
|
|
23
|
-
| `ssh://user@host:port` | 默认
|
|
24
|
-
| `ssh+key://user@host:port?key=/abs/path` | key 不在默认路径 |
|
|
25
|
-
| `ssh+password://user:pw@host:port` | 跳板 /
|
|
23
|
+
| `ssh://user@host:port` | 默认 | 显式 key(`?key=`/`$BH_SSH_KEY`)→ key;否则配了密码 → 纯密码;都没有 → 自动找 `~/.ssh` key |
|
|
24
|
+
| `ssh+key://user@host:port?key=/abs/path` | key 不在默认路径 | key 认证,密码作后备 |
|
|
25
|
+
| `ssh+password://user[:pw]@host:port` | 跳板 / 临时主机 | 纯密码认证;密码可嵌 URL,也可放字段/env |
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
**密码来源优先级**(高 → 低):URL 内嵌 > 连接里的 `password` 字段 > `$BH_SSH_PASSWORD`。
|
|
28
|
+
|
|
29
|
+
**`password` 字段**(推荐——密码按连接存,切 workspace 不用动环境变量):
|
|
30
|
+
|
|
31
|
+
```toml
|
|
32
|
+
[[connections]]
|
|
33
|
+
name = "iot_server"
|
|
34
|
+
driver = "ssh"
|
|
35
|
+
url = "ssh://113.44.193.72:22"
|
|
36
|
+
password = "${env:IOT_SSH_PASSWORD}" # 也可直接写明文
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**认证策略**(`_open_client`):显式给 key(`?key=`/`$BH_SSH_KEY`)→ key 优先、密码兜底;没给 key 但配了密码 → **纯密码**,不翻 `~/.ssh`;都没有 → 自动发现 `~/.ssh/id_{ed25519,rsa,ecdsa}`。
|
|
40
|
+
|
|
41
|
+
> 第三条很关键:paramiko 开着 `look_for_keys` 会逐个试 `~/.ssh` 里的 key,只要里面有一把解析不了的(旧 DSA key 是常见雷),认证线程直接抛异常、**在密码之前**就中断握手。所以配了密码就不会被本地散装 key 劫持。
|
|
28
42
|
|
|
29
43
|
### 1.2 用哪个连接串
|
|
30
44
|
|
|
@@ -32,7 +46,7 @@
|
|
|
32
46
|
有 ed25519 key 在 ~/.ssh/ 且 authorized_keys 已加?
|
|
33
47
|
├─ 是 → ssh://user@host (默认 key path)
|
|
34
48
|
└─ 否:
|
|
35
|
-
|
|
49
|
+
有密码? → 连接里加 password = "..."(driver 用 ssh 即可)
|
|
36
50
|
key 在非默认路径? → ssh://user@host?key=/path/to/k
|
|
37
51
|
key 在 ssh-agent? → §1.3
|
|
38
52
|
```
|
|
@@ -131,7 +145,8 @@ sql-harness 不内置 SSH SOCKS 代理;本机 `psql`/`TablePlus` 直连躲在
|
|
|
131
145
|
## Gotchas
|
|
132
146
|
|
|
133
147
|
- **paramiko 不会自动用 ssh-agent**——见 §1.3。
|
|
134
|
-
-
|
|
148
|
+
- **密码优先级**:URL 内嵌 > `password` 字段 > `$BH_SSH_PASSWORD`。配了密码即纯密码认证,本地 key 不再参与(见 §1.1)。
|
|
149
|
+
- **服务端禁密码登录**:报 `Authentication failed` 时先确认服务端开了 `PasswordAuthentication`,别急着改本地配置。
|
|
135
150
|
- **堡垒机转发时 `ssh -fN` 在某些堡垒机受限**——`AllowTcpForwarding no`。失败就 §2.2 走 paramiko `request_port_forward`。
|
|
136
151
|
- **`request_port_forward` 返回的对象必须在 finally 里 cancel**,否则下次 `use_workspace` 复用 transport 报端口占用。
|
|
137
152
|
- **workspace disconnect 不自动清 tunnel**——硬规矩:tunnel 用 try/finally 包起来。
|
|
@@ -55,3 +55,4 @@ for attempt in range(3):
|
|
|
55
55
|
- `text("SELECT ...").execution_options(...)` is per-statement; transaction-level options belong on the `SET` line.
|
|
56
56
|
- Nested `with_transaction()` will create a SAVEPOINT (PostgreSQL) or fail (MySQL). Don't nest.
|
|
57
57
|
- A failed statement inside a transaction does NOT auto-rollback the whole transaction in some dialects — call `conn.rollback()` explicitly if you've swallowed the exception.
|
|
58
|
+
- **A non-transactional table ignores `ROLLBACK` silently.** On MySQL, check `SELECT @@default_storage_engine` before trusting a rollback: if it is not InnoDB, any table created without an explicit `ENGINE=InnoDB` accepts `BEGIN`/`ROLLBACK` and discards them — the row is still there afterwards and nothing errors. Reach for `with_transaction()` on such tables and you get the *appearance* of atomicity with none of the substance.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sql-harness"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.4"
|
|
4
4
|
description = "Single-process SQL + SSH CLI for LLM agents. PostgreSQL/MySQL/SSH via SQLAlchemy + paramiko. Plaintext credentials in one TOML file. Helpers auto-injected into the heredoc namespace."
|
|
5
5
|
readme = "docs/README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -50,6 +50,7 @@ packages = ["src/sql_harness"]
|
|
|
50
50
|
# shipping its SKILL.md in the wheel, but extended to the whole doc bundle.
|
|
51
51
|
# Paths are relative to this pyproject.toml (lab/sql_harness/).
|
|
52
52
|
"SKILL.md" = "sql_harness/_skills/SKILL.md"
|
|
53
|
+
"docs/install.md" = "sql_harness/_skills/install.md"
|
|
53
54
|
"interaction-skills" = "sql_harness/_skills/interaction-skills"
|
|
54
55
|
"agent-workspace/zones/meta/skills" = "sql_harness/_skills/zones-meta-skills"
|
|
55
56
|
|
|
@@ -16,7 +16,7 @@ set -euo pipefail
|
|
|
16
16
|
# browser-harness keeps the launcher at repo root; sql-harness's "repo root" is lab/sql_harness/).
|
|
17
17
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
18
|
ROOT="$SCRIPT_DIR"
|
|
19
|
-
SRC="$ROOT/src"
|
|
19
|
+
SRC="$ROOT/src"
|
|
20
20
|
|
|
21
21
|
# Isolate dev state.
|
|
22
22
|
export BH_SQL_HOME="$ROOT/.sql-harness-dev"
|
|
@@ -48,8 +48,12 @@ CLI_INVOKED = "cli.invoked" # payload: {"subcommand": str}
|
|
|
48
48
|
DOC_READ = "doc.read" # payload: {"doc": str, "via": str}
|
|
49
49
|
COMMAND_EXECUTED = "command.executed"
|
|
50
50
|
# payload: {"argv": list[str], "code": str|None, "ok": bool,
|
|
51
|
-
# "exit_code": int, "error": str|None}
|
|
52
|
-
#
|
|
51
|
+
# "exit_code": int, "error": str|None}
|
|
52
|
+
# argv = CLI args including the subcommand; [] in heredoc mode
|
|
53
|
+
# code = the heredoc body; None in CLI mode
|
|
54
|
+
# error = the FULL traceback when the run died on an uncaught exception,
|
|
55
|
+
# else None. stderr shows a compressed form (see output.py), so this
|
|
56
|
+
# log is the only complete channel — it keeps everything.
|
|
53
57
|
|
|
54
58
|
_OFF_VALUES = {"0", "off", "false", "no"}
|
|
55
59
|
DOC_READS_CAP = 500
|