agctl 0.2.0__tar.gz → 0.3.0__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.
- agctl-0.3.0/.claude/agents/docs-watcher.md +182 -0
- {agctl-0.2.0 → agctl-0.3.0}/.gitignore +3 -0
- agctl-0.3.0/.superpowers/sdd/hardening-report.md +208 -0
- agctl-0.3.0/.superpowers/sdd/progress.md +50 -0
- agctl-0.3.0/.superpowers/sdd/review-062a2b7..6877e76.diff +165 -0
- agctl-0.3.0/.superpowers/sdd/review-0ab08be..704335c.diff +192 -0
- agctl-0.3.0/.superpowers/sdd/review-61f60a9..062a2b7.diff +830 -0
- agctl-0.3.0/.superpowers/sdd/review-6388ab6..80ff01e.diff +103 -0
- agctl-0.3.0/.superpowers/sdd/review-6877e76..0ab08be.diff +661 -0
- agctl-0.3.0/.superpowers/sdd/review-704335c..fc66690.diff +383 -0
- agctl-0.3.0/.superpowers/sdd/review-80ff01e..61f60a9.diff +371 -0
- agctl-0.3.0/.superpowers/sdd/review-93570b1..e932d8d.diff +3387 -0
- agctl-0.3.0/.superpowers/sdd/task-1-brief.md +34 -0
- agctl-0.3.0/.superpowers/sdd/task-1-report.md +63 -0
- agctl-0.3.0/.superpowers/sdd/task-2-brief.md +57 -0
- agctl-0.3.0/.superpowers/sdd/task-2-report.md +142 -0
- agctl-0.3.0/.superpowers/sdd/task-3-brief.md +80 -0
- agctl-0.3.0/.superpowers/sdd/task-3-report.md +243 -0
- agctl-0.3.0/.superpowers/sdd/task-4-brief.md +44 -0
- agctl-0.3.0/.superpowers/sdd/task-4-report.md +79 -0
- agctl-0.3.0/.superpowers/sdd/task-5-brief.md +74 -0
- agctl-0.3.0/.superpowers/sdd/task-5-report.md +185 -0
- agctl-0.3.0/.superpowers/sdd/task-6-brief.md +57 -0
- agctl-0.3.0/.superpowers/sdd/task-6-report.md +138 -0
- agctl-0.3.0/.superpowers/sdd/task-7-brief.md +44 -0
- agctl-0.3.0/.superpowers/sdd/task-7-report.md +169 -0
- {agctl-0.2.0 → agctl-0.3.0}/PKG-INFO +5 -2
- {agctl-0.2.0 → agctl-0.3.0}/README.md +2 -1
- agctl-0.3.0/agctl/assertions.py +298 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/cli.py +2 -1
- {agctl-0.2.0 → agctl-0.3.0}/agctl/clients/db_client.py +30 -0
- agctl-0.3.0/agctl/clients/db_drivers/postgresql.py +584 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/config_commands.py +5 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/db_commands.py +191 -14
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/http_commands.py +140 -4
- {agctl-0.2.0 → agctl-0.3.0}/agctl/config/models.py +14 -1
- {agctl-0.2.0 → agctl-0.3.0}/agctl/config/validator.py +26 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/mock/engine.py +15 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/mock/http_server.py +8 -1
- agctl-0.3.0/agctl/mock/jq_precompile.py +70 -0
- {agctl-0.2.0 → agctl-0.3.0}/docs/ARCHITECTURE.md +105 -18
- {agctl-0.2.0 → agctl-0.3.0}/docs/DESIGN.md +97 -4
- agctl-0.3.0/docs/superpowers/plans/active/2026-07-04-agctl-http-jq.md +484 -0
- agctl-0.3.0/docs/superpowers/plans/archive/2026-07-04-db-schema-discovery.md +417 -0
- agctl-0.3.0/docs/superpowers/plans/archive/2026-07-04-test-runbook-skills.md +384 -0
- agctl-0.3.0/docs/superpowers/specs/active/2026-07-04-agctl-http-jq-design.md +452 -0
- agctl-0.3.0/docs/superpowers/specs/archive/2026-07-04-db-schema-discovery-design.md +409 -0
- agctl-0.3.0/docs/superpowers/specs/archive/2026-07-04-test-runbook-skills-design.md +232 -0
- {agctl-0.2.0 → agctl-0.3.0}/pyproject.toml +2 -1
- {agctl-0.2.0 → agctl-0.3.0}/skills/agctl/SKILL.md +120 -3
- {agctl-0.2.0 → agctl-0.3.0}/skills/agctl-config/SKILL.md +19 -1
- {agctl-0.2.0 → agctl-0.3.0}/skills/agctl-config/reference/db-template.md +2 -2
- {agctl-0.2.0 → agctl-0.3.0}/skills/agctl-config/reference/http-template.md +4 -1
- agctl-0.3.0/skills/agctl-config/reference/mocks.md +220 -0
- agctl-0.3.0/skills/agctl-run-test-runbook/SKILL.md +107 -0
- agctl-0.3.0/skills/agctl-run-test-runbook/reference/report-annotation.md +92 -0
- agctl-0.3.0/skills/agctl-write-test-runbook/SKILL.md +89 -0
- agctl-0.3.0/skills/agctl-write-test-runbook/reference/fixtures-heartbeat.md +37 -0
- agctl-0.3.0/skills/agctl-write-test-runbook/reference/fixtures-mock.md +66 -0
- agctl-0.3.0/skills/agctl-write-test-runbook/reference/runbook-template.md +65 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/integration/test_db_commands.py +167 -0
- agctl-0.3.0/tests/integration/test_http_commands.py +160 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/integration/test_mock_commands.py +150 -0
- agctl-0.3.0/tests/unit/test_assertions.py +579 -0
- agctl-0.3.0/tests/unit/test_config_commands.py +138 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_db_client.py +81 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_db_commands.py +336 -5
- agctl-0.3.0/tests/unit/test_http_commands.py +633 -0
- agctl-0.3.0/tests/unit/test_jq_precompile.py +284 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_mock_engine.py +206 -1
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_mock_http_server.py +243 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_mock_models.py +21 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_models.py +13 -0
- agctl-0.3.0/tests/unit/test_packaging.py +22 -0
- agctl-0.3.0/tests/unit/test_postgresql_driver.py +1154 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_resolver.py +10 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_validator.py +136 -0
- agctl-0.2.0/.claude/agents/docs-watcher.md +0 -94
- agctl-0.2.0/.superpowers/sdd/progress.md +0 -30
- agctl-0.2.0/.superpowers/sdd/task-1-brief.md +0 -53
- agctl-0.2.0/.superpowers/sdd/task-1-report.md +0 -187
- agctl-0.2.0/.superpowers/sdd/task-2-brief.md +0 -43
- agctl-0.2.0/.superpowers/sdd/task-2-report.md +0 -76
- agctl-0.2.0/.superpowers/sdd/task-3-brief.md +0 -46
- agctl-0.2.0/.superpowers/sdd/task-3-report.md +0 -118
- agctl-0.2.0/.superpowers/sdd/task-4-brief.md +0 -51
- agctl-0.2.0/.superpowers/sdd/task-4-report.md +0 -166
- agctl-0.2.0/.superpowers/sdd/task-5-brief.md +0 -52
- agctl-0.2.0/.superpowers/sdd/task-5-report.md +0 -389
- agctl-0.2.0/.superpowers/sdd/task-6-brief.md +0 -50
- agctl-0.2.0/.superpowers/sdd/task-6-report.md +0 -151
- agctl-0.2.0/.superpowers/sdd/task-7-brief.md +0 -51
- agctl-0.2.0/.superpowers/sdd/task-7-report.md +0 -486
- agctl-0.2.0/agctl/assertions.py +0 -135
- agctl-0.2.0/agctl/clients/db_drivers/postgresql.py +0 -142
- agctl-0.2.0/tests/integration/test_http_commands.py +0 -75
- agctl-0.2.0/tests/unit/test_assertions.py +0 -205
- agctl-0.2.0/tests/unit/test_http_commands.py +0 -257
- agctl-0.2.0/tests/unit/test_postgresql_driver.py +0 -352
- {agctl-0.2.0 → agctl-0.3.0}/.claude/skills/pypi-publish/SKILL.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.claude/skills/pypi-publish/reference.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/.gitignore +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-04c89be..994ff5e.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-04c89be..e125cce.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-5488ac3..f617e4d.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-5524aaa..47790df.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-6e0bc77..4cadfb6.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-6e0bc77..5524aaa.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-70bad42..d99cbdc.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-91271c7..b5bb878.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-9fabeda..5488ac3.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-b5bb878..1ec55c1.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-b5bb878..6e0bc77.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-e125cce..1fd899f.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-e125cce..70bad42.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-e942e2d..eff0aef.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/review-eff0aef..91271c7.diff +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/task-8-brief.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/task-8-report.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/task-9-brief.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/.superpowers/sdd/task-9-report.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/CLAUDE.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/LICENSE +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/assertion_registry.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/clients/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/clients/db_driver_protocol.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/clients/db_drivers/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/clients/http_client.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/clients/kafka_client.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/command.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/check_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/discover_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/kafka_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/commands/mock_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/config/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/config/loader.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/config/resolver.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/data/sample-config.yaml +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/errors.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/mock/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/mock/kafka_reactor.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/mock/routing.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/output.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/params.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/plugin_protocol.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/agctl/resolution.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/docs/superpowers/plans/archive/2026-07-03-db-write-execute.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/docs/superpowers/plans/archive/2026-07-03-mock-server.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/docs/superpowers/specs/archive/2026-07-03-agctl-mock-server-design.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/docs/superpowers/specs/archive/2026-07-03-db-write-execute-design.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/skills/agctl-config/reference/db-write-template.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/skills/agctl-config/reference/init-config.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/skills/agctl-config/reference/kafka-pattern.md +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/fixtures/agctl.yaml +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/integration/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/integration/conftest.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/integration/test_kafka_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/__init__.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_assertion_registry.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_check_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_cli.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_command.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_discover_command.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_discovery.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_errors.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_http_client.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_http_ping.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_interpolation.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_kafka_client.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_kafka_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_loader.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_mock_commands.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_mock_kafka_reactor.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_mock_routing.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_output.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_params.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_plugins.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_resolution.py +0 -0
- {agctl-0.2.0 → agctl-0.3.0}/tests/unit/test_smoke.py +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docs-watcher
|
|
3
|
+
description: Review code/config changes and keep all user-facing docs fresh — DESIGN.md (WHAT/WHY), ARCHITECTURE.md (HOW), and the consumer skills/ tree (operational CLI/config reference an agent follows).
|
|
4
|
+
tools: Read, Grep, Glob, Edit, Write, Bash
|
|
5
|
+
model: sonnet
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# docs-watcher Subagent
|
|
9
|
+
|
|
10
|
+
You are the `docs-watcher` subagent for the `agctl` project. Your job is to review code and
|
|
11
|
+
configuration changes, then decide which **user-facing markdown** needs updating to stay
|
|
12
|
+
fresh against the as-built code.
|
|
13
|
+
|
|
14
|
+
You own **three document families**, each at a different abstraction altitude. Preserving
|
|
15
|
+
the altitude of each — and catching the case where code shipped but a family was forgotten —
|
|
16
|
+
is your core responsibility. The mock-server feature shipped with DESIGN.md and
|
|
17
|
+
ARCHITECTURE.md synced but the `skills/` tree stale; that gap is exactly what you exist to
|
|
18
|
+
close.
|
|
19
|
+
|
|
20
|
+
## The Documents and Their Altitudes
|
|
21
|
+
|
|
22
|
+
### `docs/DESIGN.md`
|
|
23
|
+
**Altitude:** WHAT and WHY — design-level, user-facing contract.
|
|
24
|
+
|
|
25
|
+
Contains: goals/non-goals (§1); config schema — fields and meaning (§2); CLI command surface
|
|
26
|
+
— flags, args, behavior (§3); output schema — JSON structure, error types (§4); config
|
|
27
|
+
resolution order (§5); extension contracts (§9); roadmap/future work (§10).
|
|
28
|
+
|
|
29
|
+
**What does NOT belong here:** implementation mechanics, module layouts, internal data flows.
|
|
30
|
+
|
|
31
|
+
### `docs/ARCHITECTURE.md`
|
|
32
|
+
**Altitude:** HOW — implementation-level, as-built source of truth.
|
|
33
|
+
|
|
34
|
+
Contains: module & layer map (§3); request lifecycle (§4); config pipeline (§5); transport/
|
|
35
|
+
client internals incl. lazy imports and exception mappings (§8); testing architecture (§12);
|
|
36
|
+
design-vs-implementation deltas (§14).
|
|
37
|
+
|
|
38
|
+
**What does NOT belong here:** user-facing behavior changes that are spec-level, not
|
|
39
|
+
implementation-level.
|
|
40
|
+
|
|
41
|
+
### `skills/` (consumer skills — agents copy these into their own repos)
|
|
42
|
+
**Altitude:** OPERATIONAL — "what an agent needs to know to use agctl correctly **today**."
|
|
43
|
+
|
|
44
|
+
- **`skills/agctl/SKILL.md`** — *driving* the CLI: the command surface, flags, the one-JSON-
|
|
45
|
+
object-per-invocation contract (and its streaming exceptions), exit-code meanings, output
|
|
46
|
+
parsing, gotchas, command forms, recipes, and lifecycle protocols. **Stale here = an agent
|
|
47
|
+
issues a wrong command, misreads output, or misses a gotcha.**
|
|
48
|
+
- **`skills/agctl-config/SKILL.md` + `reference/*.md`** — *authoring* `agctl.yaml`: the config
|
|
49
|
+
contract (placeholder syntaxes, cross-refs, naming, verify-after), the mode table, the
|
|
50
|
+
structural checklist, and one `reference/<mode>.md` per section (http / kafka / db / db-write
|
|
51
|
+
/ mock / init). **Stale here = an agent writes invalid config or misses a schema/validation
|
|
52
|
+
rule.**
|
|
53
|
+
|
|
54
|
+
**What does NOT belong here:** design rationale (that's DESIGN) or internal mechanics
|
|
55
|
+
(that's ARCHITECTURE). Skills state the *operational surface* — what to type, what comes
|
|
56
|
+
back, what to watch for.
|
|
57
|
+
|
|
58
|
+
## Mapping a Change to Documents
|
|
59
|
+
|
|
60
|
+
A single change can land in several families. Update **every** family that applies, each at
|
|
61
|
+
its own altitude:
|
|
62
|
+
|
|
63
|
+
| Change | DESIGN.md | ARCHITECTURE.md | skills/agctl | skills/agctl-config |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| New/changed CLI command, flag, output shape, exit code, runtime behavior | §3 / §4 | §4 / §6 / §8 (if internal flow changes) | intent table, command forms, gotchas, recipes | — |
|
|
66
|
+
| New/changed config field, validation rule, placeholder semantics | §2 | §5 (pipeline) / §15 (limitations) | gotchas (if user-facing) | SKILL.md contract + structural checklist + matching `reference/<mode>.md` |
|
|
67
|
+
| New/changed `discover` category or item shape | §3 | — | discover section + category list | verify/discover notes |
|
|
68
|
+
| Internal module layout, runtime flow, packaging, test seams | (only if user-visible) | §3 / §4 / §8 / §12 / §14 | — | — |
|
|
69
|
+
| Pure refactor / test-only / cosmetic, no behavior change | — | — | — | — |
|
|
70
|
+
|
|
71
|
+
**Overlaps are the rule, not the exception.** A new command usually touches DESIGN §3 **and**
|
|
72
|
+
`skills/agctl`; a new config field usually touches DESIGN §2 **and** `skills/agctl-config`.
|
|
73
|
+
The mock feature touched all four. When in doubt, check each family.
|
|
74
|
+
|
|
75
|
+
## Your Decision Process
|
|
76
|
+
|
|
77
|
+
For every code/config change, you MUST:
|
|
78
|
+
|
|
79
|
+
1. **Read what changed** — `git status` and `git diff` (against the appropriate base) to see
|
|
80
|
+
what materially changed in behavior or structure.
|
|
81
|
+
|
|
82
|
+
2. **Classify the change:**
|
|
83
|
+
- **(a) User-facing behavior/contract change** — new/changed CLI flags, config schema
|
|
84
|
+
fields, output schema, error types, extension contracts, discover surface.
|
|
85
|
+
- **(b) Internal structural/architectural change** — module layout, runtime flow, internal
|
|
86
|
+
mechanisms, packaging, testing architecture.
|
|
87
|
+
- **(c) Trivial/cosmetic/refactor-with-no-behavior-change** — test additions, formatting,
|
|
88
|
+
behavior-preserving refactors.
|
|
89
|
+
|
|
90
|
+
3. **For each document family, ask:** does this change fall within this family's SCOPE **and**
|
|
91
|
+
ALTITUDE, **and** does it make the family's *current text* stale?
|
|
92
|
+
- DESIGN.md: user-facing contract changes (type a).
|
|
93
|
+
- ARCHITECTURE.md: internal structural changes (type b).
|
|
94
|
+
- skills/: the operational surface an agent relies on — usually the user-facing slice of
|
|
95
|
+
(a), occasionally the user-visible consequence of (b).
|
|
96
|
+
|
|
97
|
+
4. **Decide:**
|
|
98
|
+
- If the change belongs in a family AT ITS ALTITUDE and is IMPORTANT → update it, matching
|
|
99
|
+
the file's existing style, terseness, and structure exactly. Edit only the relevant
|
|
100
|
+
lines/rows/bullets; do not expand or restructure the section.
|
|
101
|
+
- If the change has no home at this granularity, is trivial, or sits below the family's
|
|
102
|
+
altitude → **DO NOT update. A correct no-op is better than a speculative edit.**
|
|
103
|
+
|
|
104
|
+
5. **Default to leaving docs untouched.** When unsure, do not edit — and say so.
|
|
105
|
+
|
|
106
|
+
## Skills Freshness — Specific Rules
|
|
107
|
+
|
|
108
|
+
These apply *in addition* to the general rules below:
|
|
109
|
+
|
|
110
|
+
1. **Reflect AS-BUILT reality, never aspirational specs.** If a design spec said "discover
|
|
111
|
+
will surface X" but the code deferred it, the skill must say X is **not** surfaced. A skill
|
|
112
|
+
that claims a feature works when the code deferred it is a silent false green — the worst
|
|
113
|
+
failure mode for a test tool's docs.
|
|
114
|
+
|
|
115
|
+
2. **State deferrals/limitations where an agent would otherwise assume support.** A behavior
|
|
116
|
+
the MVP doesn't cover must appear in the skill (gotcha, "not covered" note, or a
|
|
117
|
+
pointed-out absence) — not just in DESIGN §10. If `agctl discover` has no `mocks`
|
|
118
|
+
category, the skill says so.
|
|
119
|
+
|
|
120
|
+
3. **Edit surgically and preserve structure.** Each skill file has a fixed shape — the mode
|
|
121
|
+
table, the numbered gotchas, the command-forms block, the recipes, the structural
|
|
122
|
+
checklist. Add a row / line / bullet / checklist item in the right slot; do not renumber,
|
|
123
|
+
reorder, or rewrite.
|
|
124
|
+
|
|
125
|
+
4. **Keep cross-references intact.** The two skills point at each other (`agctl` ↔
|
|
126
|
+
`agctl-config`) and at `reference/<mode>.md` files. When you add a mode or command, wire
|
|
127
|
+
the cross-refs on both sides.
|
|
128
|
+
|
|
129
|
+
5. **Watch for facts repeated across files.** Exit-code meanings, the placeholder-syntax
|
|
130
|
+
table, the `discover` category list, and "streaming commands" appear in more than one
|
|
131
|
+
place. If one copy changes, check the others. (When `mock run` became the second streaming
|
|
132
|
+
command, "http ping is the only streaming command" became wrong in `skills/agctl`.)
|
|
133
|
+
|
|
134
|
+
6. **Skills are consumer artifacts, not repo internals.** They are copied verbatim into other
|
|
135
|
+
repos. Don't reference repo-internal paths, build commands, or test files from inside a
|
|
136
|
+
skill — only the `agctl` CLI surface and `agctl.yaml`.
|
|
137
|
+
|
|
138
|
+
## Your Rules
|
|
139
|
+
|
|
140
|
+
1. **NEVER change a document's altitude.** No implementation detail in DESIGN.md; no
|
|
141
|
+
operational how-to in ARCHITECTURE.md; no design rationale or internal mechanics in skills/.
|
|
142
|
+
|
|
143
|
+
2. **NEVER invent new sections.** If a change has no natural home in an existing section, it
|
|
144
|
+
does not belong in that document.
|
|
145
|
+
|
|
146
|
+
3. **Match existing style exactly.** Preserve each file's voice, terseness, table format, and
|
|
147
|
+
level of detail. Do not expand a section just because you can.
|
|
148
|
+
|
|
149
|
+
4. **Reflect the code, not the spec.** Specs under `docs/superpowers/specs/` are historical
|
|
150
|
+
design records — never edit them, and never let them override what the code actually does.
|
|
151
|
+
|
|
152
|
+
5. **Report transparently.** ALWAYS end by reporting:
|
|
153
|
+
- What you reviewed.
|
|
154
|
+
- What you changed (one-line reason per change, per family).
|
|
155
|
+
- What you deliberately did NOT change (and why) — including any family you checked and
|
|
156
|
+
found already fresh.
|
|
157
|
+
|
|
158
|
+
6. **Git is your source of truth.** Use `git diff` to see what actually changed. Do not
|
|
159
|
+
speculate from file names alone.
|
|
160
|
+
|
|
161
|
+
## Example Workflow
|
|
162
|
+
|
|
163
|
+
1. `git status` → see which files changed.
|
|
164
|
+
2. `git diff <base> -- <files>` → read the actual changes.
|
|
165
|
+
3. Classify each change (a / b / c).
|
|
166
|
+
4. For each family (DESIGN / ARCHITECTURE / skills-agctl / skills-agctl-config), ask the
|
|
167
|
+
scope+altitude+staleness question. Consult the mapping table above.
|
|
168
|
+
5. When a skill is in scope, read the relevant skill file to see whether its current text is
|
|
169
|
+
now stale (don't assume — verify the claim against the code before editing).
|
|
170
|
+
6. Make edits ONLY when the answer is "yes, at this altitude, important, and currently stale."
|
|
171
|
+
7. Report your findings across all families.
|
|
172
|
+
|
|
173
|
+
## What You Do NOT Do
|
|
174
|
+
|
|
175
|
+
- Do NOT update docs for test additions or test-only changes.
|
|
176
|
+
- Do NOT update docs for cosmetic refactorings (renames, formatting) that preserve behavior.
|
|
177
|
+
- Do NOT update docs for internal helpers that aren't user-visible.
|
|
178
|
+
- Do NOT "cover" a change by inventing a new section.
|
|
179
|
+
- Do NOT touch archived specs under `docs/superpowers/specs/` — they are frozen history.
|
|
180
|
+
- Do NOT sync the packaged `agctl/data/sample-config.yaml` to the README — that drift is
|
|
181
|
+
enforced by a test, not by you.
|
|
182
|
+
- Do NOT silently edit — always report what you did and why, per family.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Security Hardening: Redact Secrets from `ConnectionFailure` Detail in `connect()`
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Fixed a secret-leak finding from code review: `PostgreSQLDriver.connect()`
|
|
6
|
+
emitted the raw connection config (including `password` and any URL-embedded
|
|
7
|
+
credentials) into `ConnectionFailure.detail`, which `@envelope` writes to
|
|
8
|
+
stdout — exposing secrets to the agent and any captured transcript.
|
|
9
|
+
|
|
10
|
+
The fix adds a module-level `_redact_config` helper and applies it to the config
|
|
11
|
+
copy that enters the error detail. The original config (used for the actual
|
|
12
|
+
`psycopg.connect` call) is untouched.
|
|
13
|
+
|
|
14
|
+
## The Leak (Before)
|
|
15
|
+
|
|
16
|
+
`agctl/clients/db_drivers/postgresql.py` `connect()` raised:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
raise ConnectionFailure(
|
|
20
|
+
message=str(exc),
|
|
21
|
+
detail={"driver": "postgresql", "config": dict(config)},
|
|
22
|
+
) from exc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`config` is the raw connection dict (`type/host/port/dbname/user/password/url`).
|
|
26
|
+
`AgctlError.to_dict()` emits `detail` verbatim into the envelope's
|
|
27
|
+
`error.detail`, which `@envelope` writes to stdout. So an auth/connection
|
|
28
|
+
failure printed `error.detail.config.password` and any `user:pass@` segment
|
|
29
|
+
inside `config.url` to stdout.
|
|
30
|
+
|
|
31
|
+
The other `ConnectionFailure` raises in this file (`execute`/`execute_write`)
|
|
32
|
+
pass only `message=str(exc)` with no `detail` — they do NOT leak. Only
|
|
33
|
+
`connect()` needed the fix. (`describe_schema` does not exist on this driver.)
|
|
34
|
+
|
|
35
|
+
## The Fix (After)
|
|
36
|
+
|
|
37
|
+
Added `_redact_config(config: dict) -> dict` at module scope in
|
|
38
|
+
`agctl/clients/db_drivers/postgresql.py`. It returns a **copy** with:
|
|
39
|
+
|
|
40
|
+
1. **Secret-style keys** — any key whose name matches the substring pattern
|
|
41
|
+
`password|secret|token|key` (case-insensitive) — replaced by the fixed
|
|
42
|
+
sentinel `"***"`. This covers `password`, `key_password`, `api_token`,
|
|
43
|
+
`secret`, etc.
|
|
44
|
+
2. **URL userinfo** — a `url` string value with a leading `user:pass@` (or
|
|
45
|
+
`user@`) userinfo prefix has that prefix replaced by `"***"`:
|
|
46
|
+
`postgres://u:p4ss@h:5432/db` -> `postgres://***@h:5432/db`. Handles both
|
|
47
|
+
`postgres://` and `postgresql://` schemes. URLs without userinfo, and
|
|
48
|
+
non-string `url` values, pass through unchanged.
|
|
49
|
+
|
|
50
|
+
The original `config` is NOT mutated (the caller still needs the real values
|
|
51
|
+
for the actual connection attempt / retry).
|
|
52
|
+
|
|
53
|
+
Wired into `connect()`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
detail={"driver": "postgresql", "config": _redact_config(config)},
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Only `connect()` was touched. `execute` / `execute_write` were left as-is (they
|
|
60
|
+
don't leak).
|
|
61
|
+
|
|
62
|
+
## Helper's Home + Rationale
|
|
63
|
+
|
|
64
|
+
**Location:** module-level function `_redact_config` in
|
|
65
|
+
`agctl/clients/db_drivers/postgresql.py` (near its only caller).
|
|
66
|
+
|
|
67
|
+
**Why not a shared util (`agctl/utils.py` / `agctl/_redact.py`)?**
|
|
68
|
+
- No shared redaction util currently exists in the codebase (checked
|
|
69
|
+
`agctl/utils*`, `agctl/_*`, and grepped for `redact|sanitize|scrub` — no
|
|
70
|
+
hits).
|
|
71
|
+
- The helper's only caller is this driver's `connect()`. Co-locating it keeps
|
|
72
|
+
the diff minimal and the logic next to the call site that needs it.
|
|
73
|
+
- The Kafka leak check (below) found no other call site that puts secrets into
|
|
74
|
+
an error `detail`, so there is no second consumer to justify extraction.
|
|
75
|
+
|
|
76
|
+
If a second leak surface appears later (e.g. a future driver copies this
|
|
77
|
+
pattern), the helper can be promoted to a shared util at that point — the
|
|
78
|
+
function signature (`dict -> dict`) is already general.
|
|
79
|
+
|
|
80
|
+
## TDD Evidence
|
|
81
|
+
|
|
82
|
+
### RED (failing test, before the fix)
|
|
83
|
+
|
|
84
|
+
Command:
|
|
85
|
+
```
|
|
86
|
+
python3 -m pytest tests/unit/test_postgresql_driver.py::test_connect_failure_redacts_secrets_from_detail -q
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Output (tail):
|
|
90
|
+
```
|
|
91
|
+
> assert redacted_config["password"] == "***"
|
|
92
|
+
E AssertionError: assert 's3cret' == '***'
|
|
93
|
+
E
|
|
94
|
+
E - ***
|
|
95
|
+
E + s3cret
|
|
96
|
+
tests/unit/test_postgresql_driver.py:312: AssertionError
|
|
97
|
+
=========================== short test summary info ============================
|
|
98
|
+
FAILED tests/unit/test_postgresql_driver.py::test_connect_failure_redacts_secrets_from_detail
|
|
99
|
+
1 failed in 0.08s
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### GREEN (after the fix)
|
|
103
|
+
|
|
104
|
+
New test in isolation:
|
|
105
|
+
```
|
|
106
|
+
python3 -m pytest tests/unit/test_postgresql_driver.py::test_connect_failure_redacts_secrets_from_detail -q
|
|
107
|
+
. [100%]
|
|
108
|
+
1 passed in 0.03s
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Full driver file (confirms existing connection-failure tests still pass):
|
|
112
|
+
```
|
|
113
|
+
python3 -m pytest tests/unit/test_postgresql_driver.py -q
|
|
114
|
+
......................... [100%]
|
|
115
|
+
25 passed in 0.04s
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Full unit suite (no regressions):
|
|
119
|
+
```
|
|
120
|
+
python3 -m pytest tests/unit -q
|
|
121
|
+
........................................................................ [ 14%]
|
|
122
|
+
........................................................................ [ 28%]
|
|
123
|
+
........................................................................ [ 42%]
|
|
124
|
+
........................................................................ [ 57%]
|
|
125
|
+
........................................................................ [ 71%]
|
|
126
|
+
........................................................................ [ 85%]
|
|
127
|
+
........................................................................ [100%]
|
|
128
|
+
504 passed in 9.71s
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Output is pristine (no warnings, no skips, no errors).
|
|
132
|
+
|
|
133
|
+
The new test asserts:
|
|
134
|
+
- `detail["config"]["password"] == "***"` (literal redaction).
|
|
135
|
+
- `"s3cret" not in json.dumps(detail)` AND `"p4ss" not in json.dumps(detail)`
|
|
136
|
+
(strongest guarantee — covers url-embedded creds and any other secret-style
|
|
137
|
+
key).
|
|
138
|
+
- `url` userinfo scrubbed (`"p4ss"` and `"u:p4ss@"` absent; `"host:5432"`
|
|
139
|
+
survives).
|
|
140
|
+
- Non-secret keys (`type/host/port/dbname/user`) preserved unchanged.
|
|
141
|
+
- Original caller config untouched (`config["password"] == "s3cret"` and
|
|
142
|
+
`config["url"]` still raw after the call — redaction operates on a copy).
|
|
143
|
+
|
|
144
|
+
## Files Changed
|
|
145
|
+
|
|
146
|
+
- `agctl/clients/db_drivers/postgresql.py`
|
|
147
|
+
- Added `import re`.
|
|
148
|
+
- Added module-level constants `_SECRET_KEY_PATTERN`, `_URL_USERINFO_PATTERN`,
|
|
149
|
+
`_REDACTED_SENTINEL`.
|
|
150
|
+
- Added module-level function `_redact_config(config: dict) -> dict`.
|
|
151
|
+
- Changed the `connect()` `ConnectionFailure` detail from
|
|
152
|
+
`dict(config)` to `_redact_config(config)`.
|
|
153
|
+
- `tests/unit/test_postgresql_driver.py`
|
|
154
|
+
- Added `test_connect_failure_redacts_secrets_from_detail`.
|
|
155
|
+
|
|
156
|
+
Diffstat: `2 files changed, 98 insertions(+), 1 deletion(-)`.
|
|
157
|
+
|
|
158
|
+
## Kafka / Other-Driver Leak Check
|
|
159
|
+
|
|
160
|
+
Scanned all `detail={...}` literals across `agctl/` and every
|
|
161
|
+
`ConnectionFailure` raise in the Kafka client.
|
|
162
|
+
|
|
163
|
+
**Result: none found.**
|
|
164
|
+
|
|
165
|
+
- `agctl/clients/kafka_client.py` raises `ConnectionFailure(message=...)` with
|
|
166
|
+
NO `detail` dict at all (lines 154, 156, 160, 166-168, 399, 442-444, 447-449,
|
|
167
|
+
492, 509, 520, 527, 540). The only data embedded is broker addresses inside
|
|
168
|
+
the `message` string (e.g. line 443: `f"Kafka broker(s) {','.join(self._brokers)} unreachable: {exc}"`),
|
|
169
|
+
which are not secrets. `KafkaSSL.key_password` lives on the parsed config
|
|
170
|
+
object but is never placed into any error `detail`.
|
|
171
|
+
- The only other `detail={...}` literals in the codebase
|
|
172
|
+
(`agctl/config/resolver.py:94` -> `{"field": last}` and
|
|
173
|
+
`agctl/commands/config_commands.py:204` -> `{"resource": ...}`) carry no
|
|
174
|
+
secrets.
|
|
175
|
+
|
|
176
|
+
No follow-up items.
|
|
177
|
+
|
|
178
|
+
## Self-Review
|
|
179
|
+
|
|
180
|
+
- **COPY, not mutation:** `_redact_config` builds and returns a fresh dict; the
|
|
181
|
+
test explicitly asserts the caller's `config` is unchanged after the call.
|
|
182
|
+
The original `config` is still used verbatim for the `psycopg.connect` kwargs.
|
|
183
|
+
- **Strongest-guarantee assertion holds for both secret carriers:** the test
|
|
184
|
+
asserts `"s3cret" not in json.dumps(detail)` (password) AND
|
|
185
|
+
`"p4ss" not in json.dumps(detail)` (url-embedded creds). Both hold.
|
|
186
|
+
- **Non-secret keys preserved:** asserted for `type/host/port/dbname/user`.
|
|
187
|
+
- **Existing connection-failure tests still pass:** the full driver file
|
|
188
|
+
(25 tests, including `test_connect_raises_connection_failure_on_operational_error`
|
|
189
|
+
and `test_connect_with_url_raises_connection_failure_on_psycopg_error`) is
|
|
190
|
+
green; the full unit suite (504 tests) is green.
|
|
191
|
+
- **`execute`/`execute_write` untouched:** confirmed via the diff — the only
|
|
192
|
+
behavioral change is the one `detail=` line in `connect()`.
|
|
193
|
+
|
|
194
|
+
## Concerns
|
|
195
|
+
|
|
196
|
+
None.
|
|
197
|
+
|
|
198
|
+
- The URL-userinfo regex (`^(postgres(?:ql)?://)[^@/]+@`) requires an `@`
|
|
199
|
+
before the first `/` to match, so URLs without userinfo pass through
|
|
200
|
+
unchanged. It handles `postgres://`, `postgresql://`, `user:pass@`, and
|
|
201
|
+
`user@` (no password) forms. A pathologically malformed URL with an
|
|
202
|
+
unencoded `@` inside the password (e.g. `postgres://u:p@ss@h/db`) would
|
|
203
|
+
under-redact the password portion — but such URLs are invalid (the `@`
|
|
204
|
+
must be percent-encoded) and not a realistic concern for this fix's scope.
|
|
205
|
+
- The secret-key substring pattern matches `key`, which would also match a
|
|
206
|
+
hypothetical benign key like `keyboard_shortcut` — but no such key exists in
|
|
207
|
+
`DatabaseConnection`'s schema, and the broad match is intentional (the task
|
|
208
|
+
spec asks for substring match on `password|secret|token|key`).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SDD Progress Ledger — db-schema-discovery
|
|
2
|
+
|
|
3
|
+
Branch: feat/db-schema-discovery | Plan: docs/superpowers/plans/active/2026-07-04-db-schema-discovery.md
|
|
4
|
+
|
|
5
|
+
Task 1: complete (commits 6388ab6..80ff01e, review clean — ✅ spec, Approved)
|
|
6
|
+
Task 2: complete (commits 80ff01e..61f60a9, review clean — ✅ spec, Approved)
|
|
7
|
+
Minor (deferred to final review): push D6 system-schema/partition-leaf/relkind exclusions into SQL WHERE for the schema=None case (currently Python-only filter; correct + integration-tested, but pulls extra rows in production). Cosmetic: typo in task-2-report.md RED evidence path.
|
|
8
|
+
Note: Task 2 implementer self-invoked docs-watcher per CLAUDE.md (correct no-op, method unwired); not part of plan Task 2 but harmless.
|
|
9
|
+
Task 3: complete (commits 61f60a9..062a2b7, review clean — ✅ spec, Approved; opus review verified all load-bearing rules in code)
|
|
10
|
+
Minor (deferred to final review):
|
|
11
|
+
M1: silent attnum-drop in constraint resolution (postgresql.py:287-303,321-325) could misalign positional pairing in inconsistent catalog — can't happen in practice (dropping a column drops its constraints); loud failure safer than quiet truncation.
|
|
12
|
+
M2: no ORDER BY on pg_constraint query (FK/unique order deterministic-by-chance; add ORDER BY con.oid for stable output).
|
|
13
|
+
M3: redundant Python schema re-check at postgresql.py:99-100 (dead defensive code).
|
|
14
|
+
M4: Any import verified present (not a defect).
|
|
15
|
+
M5 (WATCH-ITEM for Task 6): conkey/confkey consumed as Python lists; if psycopg3 returns int2vector as a string, a parser is needed. Integration test will surface it.
|
|
16
|
+
Task 4: complete (commits 062a2b7..6877e76, review clean — ✅ spec, Approved; zero findings)
|
|
17
|
+
Task 5: complete (commits 6877e76..0ab08be, review clean — ✅ spec, Approved; opus review verified probe-before-connect structurally guaranteed + disambiguation + verbatim hints)
|
|
18
|
+
Minor (deferred to final review):
|
|
19
|
+
M1: db_commands.py:202,214-222 use .get() on match fields (m.get("schema")/match.get("comment")) where brief specifies subscript (m["schema"]). Functionally equivalent for well-formed driver output; subscript would fail-loudly (KeyError) on a malformed driver contract instead of silently emitting null. Style/robustness, not load-bearing.
|
|
20
|
+
Task 6: complete (commits 0ab08be..704335c, review clean — ✅ spec, Approved; live run PASS against Postgres 16 testcontainer)
|
|
21
|
+
M5 RETIRED: int2vector watch-item confirmed non-issue — psycopg3 returns conkey/confkey as Python int lists; live FK pairing assertions passed.
|
|
22
|
+
Minor (deferred to final review): redundant in-function `import json` at tests/integration/test_db_commands.py:60 (module already imports json).
|
|
23
|
+
Task 7: complete (commits 704335c..e932d8d via fix, review Approved after fix — ✅ spec)
|
|
24
|
+
Fixed in-loop: I1 (DESIGN.md primary_key rendered as {name,columns} object but code returns list[str]; fixed to ["id"]; SKILL.md:142 preventive clarification primary_key→primary_key:[col]). Controller-verified the fix directly (surgical, reviewer-specified, sibling scan clean) — no full re-review subagent.
|
|
25
|
+
Minor (deferred to final review): ARCHITECTURE.md:100-102 lifecycle phrasing "close runs on raises" slightly imprecise (close runs before the raises, in _probe_and_describe's finally) — accurate, leave.
|
|
26
|
+
|
|
27
|
+
ALL TASKS DONE. Branch tip = e932d8d.
|
|
28
|
+
DEFERRED MINOR FINDINGS FOR FINAL REVIEW TRIAGE:
|
|
29
|
+
- T2/M1: push D6 system-schema/partition-leaf/relkind exclusions into SQL WHERE for schema=None (currently Python-only; correct + integration-tested, but pulls extra rows in production).
|
|
30
|
+
- T2/M(report): cosmetic typo in task-2-report.md RED evidence path.
|
|
31
|
+
- T3/M1: silent attnum-drop in constraint resolution (postgresql.py:287-303,321-325) could misalign positional pairing in inconsistent catalog — can't happen in practice; loud failure safer.
|
|
32
|
+
- T3/M2: no ORDER BY on pg_constraint query (FK/unique order deterministic-by-chance; add ORDER BY con.oid for stable output).
|
|
33
|
+
- T3/M3: redundant Python schema re-check at postgresql.py:99-100 (dead defensive code).
|
|
34
|
+
- T5/M1: db_commands.py:202,214-222 use .get() on match fields where brief specifies subscript; subscript would fail-loudly on malformed driver contract.
|
|
35
|
+
- T6/M1: redundant in-function `import json` at tests/integration/test_db_commands.py:60.
|
|
36
|
+
- T7/M1: ARCHITECTURE.md:100-102 lifecycle phrasing (imprecise, accurate).
|
|
37
|
+
|
|
38
|
+
FINAL REVIEW: complete (93570b1..e932d8d). Verdict: Ready to merge — YES. Zero Critical/Important.
|
|
39
|
+
T3/M3 reclassified: NOT a trivial pre-merge cleanup. Fix subagent BLOCKED correctly — the Python schema re-check is load-bearing for the SQL-agnostic CatalogFakeCursor unit tests (scenarios 5/6 assert filtered results via it). Removing it needs a test-fixture refactor (fake honors n.nspname, or stage matching rows) → belongs in the follow-up cluster, not pre-merge. No commit made; tree clean.
|
|
40
|
+
Dropped (false premises): T6/M1 (file has no top-level import json — in-function is house style), T7/M1 (ARCH phrasing accurate).
|
|
41
|
+
FOLLOW-UP ISSUE (one cluster, touch postgresql.py once): T2/M1 push D6 filters into SQL WHERE (efficiency/defense-in-depth for schema=None on busy clusters) + T3/M3 delete dead Python re-check WITH CatalogFakeCursor fixture update + T3/M2 ORDER BY con.conname + T3/M1 loud attnum failure. Plus optional: T5/M1 .get()->subscript; multi-col FK in integration test.
|
|
42
|
+
Branch feat/db-schema-discovery tip = e932d8d. Merge-ready.
|
|
43
|
+
|
|
44
|
+
4-REVIEWER PARALLEL CODE REVIEW (security / correctness / architecture / tests, all opus):
|
|
45
|
+
Security: YES. 1 Important (PRE-EXISTING, not in diff): connect() ConnectionFailure detail={"config": dict(config)} leaks password to stdout via envelope (postgresql.py:57-61). Reachable today via db query/assert/execute; db schema's ungated nature broadens surface. → recommend SEPARATE hardening PR (redact password/secret*/token*/*key in detail.config). New code clean (perfect bind-param discipline, no identifier construction, no new secrets, lifecycle correct).
|
|
46
|
+
Correctness: YES (empirically verified live vs Postgres 16, incl. composite FK positional pairing + identity-vs-serial redaction). 0 Critical/Important. 2 future-proofing notes: int2vector caveat (conkey is int2[] OID 1005 → list, correct; pg_index.indkey would be a string), pg_description query missing AND classoid='pg_class'::regclass (safe via global OID alloc).
|
|
47
|
+
Architecture: YES. 0 Critical/Important. 5 Minor nits: duplicated capability-refusal error construction (getattr vs _conn_dict['type']); double-probe on happy path (defense-in-depth); Python-side catalog filtering (T2/M1); probe asymmetry vs execute_write (intentional, document why); _probe_and_describe close-timing docstring (T7/M1).
|
|
48
|
+
Tests: WITH FIXES. 1 Important: schema-filter unit-test fidelity gap — CatalogFakeCursor is SQL-agnostic so unit tests can't catch a broken SQL WHERE; Python re-check masks it; integration test backstops only when Postgres available (self-skipping). SAME root as T3/M3 → one coherent fix (fake honors bind params + remove dead Python re-check). 6 Minor coverage-strength suggestions.
|
|
49
|
+
|
|
50
|
+
AGGREGATE: 0 Critical across all 4. 2 Important (both pre-existing/follow-up, neither blocks this branch): password leak (separate PR), schema-filter fidelity (follow-up cluster already documented).
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Review package: 062a2b7..6877e76
|
|
2
|
+
|
|
3
|
+
## Commits
|
|
4
|
+
6877e76 feat(db): add DbClient.supports_describe_schema + describe_schema probe
|
|
5
|
+
|
|
6
|
+
## Files changed
|
|
7
|
+
agctl/clients/db_client.py | 30 +++++++++++++++++++
|
|
8
|
+
tests/unit/test_db_client.py | 71 ++++++++++++++++++++++++++++++++++++++++++++
|
|
9
|
+
2 files changed, 101 insertions(+)
|
|
10
|
+
|
|
11
|
+
## Diff
|
|
12
|
+
diff --git a/agctl/clients/db_client.py b/agctl/clients/db_client.py
|
|
13
|
+
index b985315..56ef21e 100644
|
|
14
|
+
--- a/agctl/clients/db_client.py
|
|
15
|
+
+++ b/agctl/clients/db_client.py
|
|
16
|
+
@@ -100,12 +100,42 @@ class DbClient:
|
|
17
|
+
The dict returned by the driver's ``execute_write`` method.
|
|
18
|
+
"""
|
|
19
|
+
execute_write_attr = getattr(self._driver, "execute_write", None)
|
|
20
|
+
if not callable(execute_write_attr):
|
|
21
|
+
raise ConfigError(
|
|
22
|
+
f"connection's driver ({self._conn_dict['type']}) does not support writes",
|
|
23
|
+
{"driver": self._conn_dict["type"]},
|
|
24
|
+
)
|
|
25
|
+
return self._driver.execute_write(sql, params)
|
|
26
|
+
|
|
27
|
+
+ def supports_describe_schema(self) -> bool:
|
|
28
|
+
+ """Return True if the driver offers a callable ``describe_schema``.
|
|
29
|
+
+
|
|
30
|
+
+ This is a **pre-connect**, side-effect-free probe: it does not call
|
|
31
|
+
+ ``describe_schema`` and does not require a connection. Callers
|
|
32
|
+
+ (``agctl db schema``) use it to fail fast when the selected driver
|
|
33
|
+
+ cannot introspect, without opening a connection.
|
|
34
|
+
+
|
|
35
|
+
+ Returns:
|
|
36
|
+
+ True if the driver has a callable ``describe_schema`` attribute.
|
|
37
|
+
+ """
|
|
38
|
+
+ return callable(getattr(self._driver, "describe_schema", None))
|
|
39
|
+
+
|
|
40
|
+
+ def describe_schema(self, table: str | None, schema: str | None) -> dict:
|
|
41
|
+
+ """Delegate to the driver's optional ``describe_schema`` capability.
|
|
42
|
+
+
|
|
43
|
+
+ Probes :meth:`supports_describe_schema`; raises ConfigError if the
|
|
44
|
+
+ driver lacks this optional capability. Otherwise delegates to the
|
|
45
|
+
+ driver and returns its dict unchanged.
|
|
46
|
+
+
|
|
47
|
+
+ Returns:
|
|
48
|
+
+ The dict returned by the driver's ``describe_schema`` method.
|
|
49
|
+
+ """
|
|
50
|
+
+ if not self.supports_describe_schema():
|
|
51
|
+
+ raise ConfigError(
|
|
52
|
+
+ f"connection's driver ({self._conn_dict['type']}) does not support schema discovery",
|
|
53
|
+
+ {"driver": self._conn_dict["type"]},
|
|
54
|
+
+ )
|
|
55
|
+
+ return self._driver.describe_schema(table, schema)
|
|
56
|
+
+
|
|
57
|
+
def close(self) -> None:
|
|
58
|
+
self._driver.close()
|
|
59
|
+
diff --git a/tests/unit/test_db_client.py b/tests/unit/test_db_client.py
|
|
60
|
+
index 9824992..03cfce3 100644
|
|
61
|
+
--- a/tests/unit/test_db_client.py
|
|
62
|
+
+++ b/tests/unit/test_db_client.py
|
|
63
|
+
@@ -40,20 +40,42 @@ class FakeDriverReadOnly(FakeDriver):
|
|
64
|
+
|
|
65
|
+
class FakeDriverWithNonCallableExecuteWrite(FakeDriver):
|
|
66
|
+
"""Fake driver with non-callable execute_write attribute."""
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
super().__init__()
|
|
70
|
+
# Set execute_write to a non-callable value
|
|
71
|
+
self.execute_write = "not a method"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
+class FakeDriverWithDescribeSchema(FakeDriver):
|
|
75
|
+
+ """Fake driver that supports schema discovery."""
|
|
76
|
+
+
|
|
77
|
+
+ SCHEMA = {
|
|
78
|
+
+ "tables": [
|
|
79
|
+
+ {"name": "users", "schema": "public", "columns": [{"name": "id"}]}
|
|
80
|
+
+ ]
|
|
81
|
+
+ }
|
|
82
|
+
+
|
|
83
|
+
+ def describe_schema(self, table, schema):
|
|
84
|
+
+ return self.SCHEMA
|
|
85
|
+
+
|
|
86
|
+
+
|
|
87
|
+
+class FakeDriverWithNonCallableDescribeSchema(FakeDriver):
|
|
88
|
+
+ """Fake driver with non-callable describe_schema attribute."""
|
|
89
|
+
+
|
|
90
|
+
+ def __init__(self):
|
|
91
|
+
+ super().__init__()
|
|
92
|
+
+ # Set describe_schema to a non-callable value
|
|
93
|
+
+ self.describe_schema = "not a method"
|
|
94
|
+
+
|
|
95
|
+
+
|
|
96
|
+
class FakeDriverSubclass(FakeDriver):
|
|
97
|
+
"""Distinct class so tests can assert which driver was selected."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestDbClientDirectInjection:
|
|
101
|
+
def test_execute_returns_driver_rows(self):
|
|
102
|
+
client = DbClient(
|
|
103
|
+
DatabaseConnection(type="postgresql", host="h"), driver=FakeDriver()
|
|
104
|
+
)
|
|
105
|
+
rows = client.execute("SELECT 1", {})
|
|
106
|
+
@@ -162,10 +184,59 @@ class TestExecuteWrite:
|
|
107
|
+
def test_driver_with_non_callable_execute_write_raises_config_error(self):
|
|
108
|
+
"""Driver with non-callable execute_write attribute: also raises ConfigError."""
|
|
109
|
+
client = DbClient(
|
|
110
|
+
DatabaseConnection(type="postgresql", host="h"),
|
|
111
|
+
driver=FakeDriverWithNonCallableExecuteWrite(),
|
|
112
|
+
)
|
|
113
|
+
with pytest.raises(ConfigError) as exc_info:
|
|
114
|
+
client.execute_write("INSERT INTO t (x) VALUES (1)", {"x": 1})
|
|
115
|
+
assert "does not support writes" in exc_info.value.message
|
|
116
|
+
assert exc_info.value.detail.get("driver") == "postgresql"
|
|
117
|
+
+
|
|
118
|
+
+
|
|
119
|
+
+class TestDescribeSchema:
|
|
120
|
+
+ """Tests for DbClient.supports_describe_schema + describe_schema probe."""
|
|
121
|
+
+
|
|
122
|
+
+ def test_driver_with_describe_schema_reports_support_and_delegates(self):
|
|
123
|
+
+ """Driver WITH callable describe_schema: probe True, delegation returns dict unchanged."""
|
|
124
|
+
+ client = DbClient(
|
|
125
|
+
+ DatabaseConnection(type="postgresql", host="h"),
|
|
126
|
+
+ driver=FakeDriverWithDescribeSchema(),
|
|
127
|
+
+ )
|
|
128
|
+
+ assert client.supports_describe_schema() is True
|
|
129
|
+
+ result = client.describe_schema(None, None)
|
|
130
|
+
+ assert result == FakeDriverWithDescribeSchema.SCHEMA
|
|
131
|
+
+
|
|
132
|
+
+ def test_driver_without_describe_schema_reports_no_support_and_raises(self):
|
|
133
|
+
+ """Driver WITHOUT describe_schema: probe False, raises ConfigError with driver type."""
|
|
134
|
+
+ client = DbClient(
|
|
135
|
+
+ DatabaseConnection(type="postgresql", host="h"),
|
|
136
|
+
+ driver=FakeDriverReadOnly(),
|
|
137
|
+
+ )
|
|
138
|
+
+ assert client.supports_describe_schema() is False
|
|
139
|
+
+ with pytest.raises(ConfigError) as exc_info:
|
|
140
|
+
+ client.describe_schema(None, None)
|
|
141
|
+
+ assert "does not support schema discovery" in exc_info.value.message
|
|
142
|
+
+ assert exc_info.value.detail.get("driver") == "postgresql"
|
|
143
|
+
+
|
|
144
|
+
+ def test_driver_with_non_callable_describe_schema_raises_config_error(self):
|
|
145
|
+
+ """Driver with non-callable describe_schema attribute: probe False, raises ConfigError."""
|
|
146
|
+
+ client = DbClient(
|
|
147
|
+
+ DatabaseConnection(type="postgresql", host="h"),
|
|
148
|
+
+ driver=FakeDriverWithNonCallableDescribeSchema(),
|
|
149
|
+
+ )
|
|
150
|
+
+ assert client.supports_describe_schema() is False
|
|
151
|
+
+ with pytest.raises(ConfigError) as exc_info:
|
|
152
|
+
+ client.describe_schema(None, None)
|
|
153
|
+
+ assert "does not support schema discovery" in exc_info.value.message
|
|
154
|
+
+ assert exc_info.value.detail.get("driver") == "postgresql"
|
|
155
|
+
+
|
|
156
|
+
+ def test_supports_describe_schema_does_not_open_connection(self):
|
|
157
|
+
+ """Pre-connect probe must be side-effect-free: no connection opened."""
|
|
158
|
+
+ fake = FakeDriverWithDescribeSchema()
|
|
159
|
+
+ client = DbClient(
|
|
160
|
+
+ DatabaseConnection(type="postgresql", host="h"),
|
|
161
|
+
+ driver=fake,
|
|
162
|
+
+ )
|
|
163
|
+
+ # Probe before any connect(); the driver must not have been connected.
|
|
164
|
+
+ assert client.supports_describe_schema() is True
|
|
165
|
+
+ assert fake.connected_with is None
|