agctl 0.1.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.1.0/.claude/agents/docs-watcher.md +94 -0
- agctl-0.1.0/.gitignore +22 -0
- agctl-0.1.0/CLAUDE.md +13 -0
- agctl-0.1.0/LICENSE +21 -0
- agctl-0.1.0/PKG-INFO +414 -0
- agctl-0.1.0/README.md +370 -0
- agctl-0.1.0/agctl/__init__.py +1 -0
- agctl-0.1.0/agctl/assertion_registry.py +243 -0
- agctl-0.1.0/agctl/assertions.py +135 -0
- agctl-0.1.0/agctl/cli.py +163 -0
- agctl-0.1.0/agctl/clients/__init__.py +0 -0
- agctl-0.1.0/agctl/clients/db_client.py +94 -0
- agctl-0.1.0/agctl/clients/db_driver_protocol.py +27 -0
- agctl-0.1.0/agctl/clients/db_drivers/__init__.py +0 -0
- agctl-0.1.0/agctl/clients/db_drivers/postgresql.py +92 -0
- agctl-0.1.0/agctl/clients/http_client.py +116 -0
- agctl-0.1.0/agctl/clients/kafka_client.py +398 -0
- agctl-0.1.0/agctl/command.py +74 -0
- agctl-0.1.0/agctl/commands/__init__.py +1 -0
- agctl-0.1.0/agctl/commands/check_commands.py +115 -0
- agctl-0.1.0/agctl/commands/config_commands.py +175 -0
- agctl-0.1.0/agctl/commands/db_commands.py +329 -0
- agctl-0.1.0/agctl/commands/discover_commands.py +372 -0
- agctl-0.1.0/agctl/commands/http_commands.py +534 -0
- agctl-0.1.0/agctl/commands/kafka_commands.py +485 -0
- agctl-0.1.0/agctl/config/__init__.py +5 -0
- agctl-0.1.0/agctl/config/loader.py +108 -0
- agctl-0.1.0/agctl/config/models.py +104 -0
- agctl-0.1.0/agctl/config/resolver.py +84 -0
- agctl-0.1.0/agctl/config/validator.py +86 -0
- agctl-0.1.0/agctl/errors.py +46 -0
- agctl-0.1.0/agctl/output.py +25 -0
- agctl-0.1.0/agctl/params.py +29 -0
- agctl-0.1.0/agctl/plugin_protocol.py +42 -0
- agctl-0.1.0/agctl/resolution.py +77 -0
- agctl-0.1.0/docs/ARCHITECTURE.md +601 -0
- agctl-0.1.0/docs/DESIGN.md +1771 -0
- agctl-0.1.0/pyproject.toml +69 -0
- agctl-0.1.0/tests/__init__.py +0 -0
- agctl-0.1.0/tests/fixtures/agctl.yaml +117 -0
- agctl-0.1.0/tests/integration/__init__.py +0 -0
- agctl-0.1.0/tests/integration/conftest.py +280 -0
- agctl-0.1.0/tests/integration/test_db_commands.py +61 -0
- agctl-0.1.0/tests/integration/test_http_commands.py +75 -0
- agctl-0.1.0/tests/integration/test_kafka_commands.py +91 -0
- agctl-0.1.0/tests/unit/__init__.py +0 -0
- agctl-0.1.0/tests/unit/test_assertion_registry.py +221 -0
- agctl-0.1.0/tests/unit/test_assertions.py +205 -0
- agctl-0.1.0/tests/unit/test_check_commands.py +246 -0
- agctl-0.1.0/tests/unit/test_cli.py +137 -0
- agctl-0.1.0/tests/unit/test_command.py +97 -0
- agctl-0.1.0/tests/unit/test_db_client.py +116 -0
- agctl-0.1.0/tests/unit/test_db_commands.py +567 -0
- agctl-0.1.0/tests/unit/test_discover_command.py +235 -0
- agctl-0.1.0/tests/unit/test_discovery.py +47 -0
- agctl-0.1.0/tests/unit/test_errors.py +80 -0
- agctl-0.1.0/tests/unit/test_http_client.py +128 -0
- agctl-0.1.0/tests/unit/test_http_commands.py +257 -0
- agctl-0.1.0/tests/unit/test_http_ping.py +520 -0
- agctl-0.1.0/tests/unit/test_interpolation.py +41 -0
- agctl-0.1.0/tests/unit/test_kafka_client.py +597 -0
- agctl-0.1.0/tests/unit/test_kafka_commands.py +952 -0
- agctl-0.1.0/tests/unit/test_loader.py +151 -0
- agctl-0.1.0/tests/unit/test_models.py +64 -0
- agctl-0.1.0/tests/unit/test_output.py +35 -0
- agctl-0.1.0/tests/unit/test_params.py +33 -0
- agctl-0.1.0/tests/unit/test_plugins.py +235 -0
- agctl-0.1.0/tests/unit/test_postgresql_driver.py +183 -0
- agctl-0.1.0/tests/unit/test_resolution.py +141 -0
- agctl-0.1.0/tests/unit/test_resolver.py +70 -0
- agctl-0.1.0/tests/unit/test_smoke.py +4 -0
- agctl-0.1.0/tests/unit/test_validator.py +173 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docs-watcher
|
|
3
|
+
description: Review code/config changes and decide whether DESIGN.md or ARCHITECTURE.md need syncing at their respective altitudes.
|
|
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 configuration changes, then decide whether the project documentation needs updating.
|
|
11
|
+
|
|
12
|
+
## The Two Documents and Their Altitudes
|
|
13
|
+
|
|
14
|
+
You are responsible for two documents that live at **different abstraction altitudes**. Preserving this distinction is your core responsibility.
|
|
15
|
+
|
|
16
|
+
### `/Users/dmitry/Desktop/CursorProjects/agenttest/docs/DESIGN.md`
|
|
17
|
+
**Altitude:** WHAT and WHY — design-level, user-facing contract.
|
|
18
|
+
|
|
19
|
+
Contains:
|
|
20
|
+
- Goals and non-goals (§1)
|
|
21
|
+
- Configuration schema — what fields exist, what they mean (§2)
|
|
22
|
+
- CLI command surface — flags, arguments, behavior (§3)
|
|
23
|
+
- Output schema — JSON structure, error types (§4)
|
|
24
|
+
- Config resolution order (§5)
|
|
25
|
+
- Extension contracts — what plugins/assertions can do (§9)
|
|
26
|
+
- Roadmap/future work (§10)
|
|
27
|
+
|
|
28
|
+
**What does NOT belong here:** Implementation mechanics, module layouts, internal data flows, how the code actually works.
|
|
29
|
+
|
|
30
|
+
### `/Users/dmitry/Desktop/CursorProjects/agenttest/docs/ARCHITECTURE.md`
|
|
31
|
+
**Altitude:** HOW — implementation-level, as-built source of truth.
|
|
32
|
+
|
|
33
|
+
Contains:
|
|
34
|
+
- Module & layer map — file tree, responsibility per module (§3)
|
|
35
|
+
- Request lifecycle — step-by-step runtime flow (§4)
|
|
36
|
+
- Configuration pipeline — how load/validate/resolve works (§5)
|
|
37
|
+
- Transport/client internals — lazy imports, exception mappings (§8)
|
|
38
|
+
- Testing architecture — unit vs integration, fixtures (§12)
|
|
39
|
+
- Design-vs-implementation deltas — where code diverges from DESIGN (§14)
|
|
40
|
+
|
|
41
|
+
**What does NOT belong here:** User-facing behavior changes that are spec-level, not implementation-level.
|
|
42
|
+
|
|
43
|
+
## Your Decision Process
|
|
44
|
+
|
|
45
|
+
For every code/config change, you MUST:
|
|
46
|
+
|
|
47
|
+
1. **Read what changed** — Use `git status` and `git diff` (against the appropriate base) to understand what materially changed in behavior or structure.
|
|
48
|
+
|
|
49
|
+
2. **Classify the change:**
|
|
50
|
+
- **(a) User-facing behavior/contract change** — new/changed CLI flags, config schema fields, output schema, error types, extension contracts.
|
|
51
|
+
- **(b) Internal structural/architectural change** — module layout, runtime flow, internal mechanisms, packaging, testing architecture.
|
|
52
|
+
- **(c) Trivial/cosmetic/refactor-with-no-behavior-change** — test additions, formatting, behavior-preserving refactors.
|
|
53
|
+
|
|
54
|
+
3. **For each doc, ask:** Does this change fall within this doc's SCOPE **and** ALTITUDE?
|
|
55
|
+
- DESIGN.md: only user-facing contract changes (type a).
|
|
56
|
+
- ARCHITECTURE.md: internal structural changes (type b).
|
|
57
|
+
|
|
58
|
+
4. **Decide:**
|
|
59
|
+
- If the change belongs in a doc AT ITS ALTITUDE and is IMPORTANT → update that doc, matching its existing style, terseness, and detail level exactly. Edit only the relevant lines; do not expand the section.
|
|
60
|
+
- If the change has NO doc at this granularity (e.g., a pure internal helper, a test addition, a behavior-preserving refactor), OR is trivial, OR sits below the doc's altitude → **DO NOT update. A correct no-op is better than a speculative edit.**
|
|
61
|
+
|
|
62
|
+
5. **Default to leaving docs untouched.** When unsure, do not edit — and say so.
|
|
63
|
+
|
|
64
|
+
## Your Rules
|
|
65
|
+
|
|
66
|
+
1. **NEVER change a doc's altitude.** Do not inject low-level implementation detail into DESIGN.md. Do not strip detail from ARCHITECTURE.md.
|
|
67
|
+
|
|
68
|
+
2. **NEVER invent new sections.** If a change has no natural home in an existing section, it does not belong in that doc.
|
|
69
|
+
|
|
70
|
+
3. **Match existing style exactly.** When you do edit, preserve the doc's voice, terseness, and level of detail. Do not expand a section just because you can.
|
|
71
|
+
|
|
72
|
+
4. **Report transparently.** ALWAYS end by reporting:
|
|
73
|
+
- What you reviewed
|
|
74
|
+
- What you changed (one-line reason per change)
|
|
75
|
+
- What you deliberately did NOT change (and why)
|
|
76
|
+
|
|
77
|
+
5. **Git is your source of truth.** Use `git diff` to see what actually changed. Do not speculate from file names alone.
|
|
78
|
+
|
|
79
|
+
## Example Workflow
|
|
80
|
+
|
|
81
|
+
1. Run `git status` to see what files changed.
|
|
82
|
+
2. Run `git diff <base> -- <files>` to read the actual changes.
|
|
83
|
+
3. Classify each change per step 2 above.
|
|
84
|
+
4. For each doc, ask the scope+altitude question.
|
|
85
|
+
5. Make edits ONLY when the answer is "yes, at this altitude, and important."
|
|
86
|
+
6. Report your findings.
|
|
87
|
+
|
|
88
|
+
## What You Do NOT Do
|
|
89
|
+
|
|
90
|
+
- Do NOT update docs for test additions or test-only changes.
|
|
91
|
+
- Do NOT update docs for cosmetic refactorings (renames, formatting) that preserve behavior.
|
|
92
|
+
- Do NOT update docs for internal helpers that aren't user-visible.
|
|
93
|
+
- Do NOT "cover" a change by inventing a new section.
|
|
94
|
+
- Do NOT silently edit — always report what you did and why.
|
agctl-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# macOS
|
|
2
|
+
.DS_Store
|
|
3
|
+
|
|
4
|
+
# Claude Code local state (project agents/ and shared settings.json ARE tracked)
|
|
5
|
+
.claude/settings.local.json
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*.egg-info/
|
|
11
|
+
.eggs/
|
|
12
|
+
build/
|
|
13
|
+
dist/
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
|
|
20
|
+
# env files (keep .env.example tracked)
|
|
21
|
+
.env
|
|
22
|
+
.env.local
|
agctl-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# agctl
|
|
2
|
+
|
|
3
|
+
Agentic CLI interface for system under test.
|
|
4
|
+
|
|
5
|
+
## Docs
|
|
6
|
+
|
|
7
|
+
[docs/](./docs) - documentation
|
|
8
|
+
- [DESIGN.md](./docs/DESIGN.md) - project design doc (intent & spec)
|
|
9
|
+
- [ARCHITECTURE.md](./docs/ARCHITECTURE.md) - as-built architecture (source of truth: module layout, runtime flow, extension points)
|
|
10
|
+
|
|
11
|
+
## Docs Sync
|
|
12
|
+
|
|
13
|
+
After completing any task that changes code or config, invoke the `docs-watcher` subagent to check whether DESIGN.md or ARCHITECTURE.md need syncing. The subagent preserves each doc's altitude: DESIGN.md captures user-facing contract (WHAT/WHY), ARCHITECTURE.md captures as-built implementation (HOW). A correct no-op is better than a speculative edit.
|
agctl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry Teryaev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
agctl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agctl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-facing CLI harness for testing distributed systems
|
|
5
|
+
Project-URL: Homepage, https://github.com/HumanBean17/agctl
|
|
6
|
+
Project-URL: Repository, https://github.com/HumanBean17/agctl
|
|
7
|
+
Project-URL: Issues, https://github.com/HumanBean17/agctl/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/HumanBean17/agctl/releases
|
|
9
|
+
Author-email: Dmitry Teryaev <doudmitry@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,cli,database,distributed-systems,http,kafka,testing
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: click>=8.1
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Provides-Extra: db
|
|
27
|
+
Requires-Dist: jq>=1.6; extra == 'db'
|
|
28
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'db'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: http
|
|
32
|
+
Requires-Dist: httpx>=0.27; extra == 'http'
|
|
33
|
+
Provides-Extra: integration
|
|
34
|
+
Requires-Dist: confluent-kafka>=2.4; extra == 'integration'
|
|
35
|
+
Requires-Dist: httpx>=0.27; extra == 'integration'
|
|
36
|
+
Requires-Dist: jq>=1.6; extra == 'integration'
|
|
37
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'integration'
|
|
38
|
+
Requires-Dist: pytest>=8.0; extra == 'integration'
|
|
39
|
+
Requires-Dist: testcontainers; extra == 'integration'
|
|
40
|
+
Provides-Extra: kafka
|
|
41
|
+
Requires-Dist: confluent-kafka>=2.4; extra == 'kafka'
|
|
42
|
+
Requires-Dist: jq>=1.6; extra == 'kafka'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# agctl
|
|
46
|
+
|
|
47
|
+
> Agent-facing CLI harness for testing distributed systems.
|
|
48
|
+
|
|
49
|
+
`agctl` (alias: `agt`) is a small, system-agnostic command-line tool that an AI
|
|
50
|
+
coding agent drives to verify a running system. It talks **HTTP**, **Kafka**, and
|
|
51
|
+
**databases**, and gives the agent one consistent contract for all of them: every
|
|
52
|
+
invocation prints exactly **one JSON object** on stdout and exits with a
|
|
53
|
+
deterministic code (`0` success, `1` assertion failed, `2` config/tool/env error).
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
$ agctl http call create-order --param customer_id=cust-42 --param sku=WIDGET-001
|
|
57
|
+
{"ok": true, "command": "http.call", "result": {"status_code": 201, ...}, "error": null, "duration_ms": 87}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Why it exists
|
|
61
|
+
|
|
62
|
+
Agents need **deterministic, machine-readable feedback** to know whether a change
|
|
63
|
+
worked. Raw `curl` output, prose logs, and non-zero exit codes from shell glue are
|
|
64
|
+
noisy and ambiguous — an agent can't reliably tell "the feature is broken" from
|
|
65
|
+
"my command had a typo."
|
|
66
|
+
|
|
67
|
+
`agctl` closes that gap. It is a **harness** built specifically to be driven by an
|
|
68
|
+
agent (humans and CI can use it too):
|
|
69
|
+
|
|
70
|
+
- **One object, one code.** A single JSON envelope + a strict exit code, on every
|
|
71
|
+
command, across every protocol. Parse `ok`/`result`/`error` and move on.
|
|
72
|
+
- **Composable, narrow commands.** The agent chains them instead of relying on a
|
|
73
|
+
monolithic "run scenario" command: send a request → assert a Kafka event →
|
|
74
|
+
assert a DB row.
|
|
75
|
+
- **System-agnostic.** The tool ships with zero knowledge of your project. All
|
|
76
|
+
endpoints, topics, connections, SQL, and request templates live in an
|
|
77
|
+
`agctl.yaml` you commit to your repo.
|
|
78
|
+
- **Fail loudly.** A wrong assertion always exits `1`. There is no silent
|
|
79
|
+
false-positive — the worst possible failure mode for an agent harness.
|
|
80
|
+
- **Discovery, not dumps.** A three-level `discover` command lets the agent learn
|
|
81
|
+
what your system offers *without* loading your entire config into its context.
|
|
82
|
+
|
|
83
|
+
Design intent and the full spec live in [`docs/DESIGN.md`](./docs/DESIGN.md); the
|
|
84
|
+
as-built architecture (module layout, runtime flow, extension points) is the
|
|
85
|
+
source of truth in [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md).
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Setup
|
|
90
|
+
|
|
91
|
+
**Requirements:** Python ≥ 3.11.
|
|
92
|
+
|
|
93
|
+
Install `agctl` into your project (this repo uses `uv` — `pip` works too):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# with uv (recommended — this repo ships a uv.lock)
|
|
97
|
+
uv pip install -e .
|
|
98
|
+
|
|
99
|
+
# or with pip
|
|
100
|
+
pip install -e .
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The core install pulls only `click`, `pyyaml`, and `pydantic`. Protocol libraries
|
|
104
|
+
are **optional extras** — install only what your system needs. Heavy libs are
|
|
105
|
+
lazy-imported, so a missing extra fails with a clear `ConfigError` (exit 2) rather
|
|
106
|
+
than a crash:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install -e ".[http]" # http call / request / ping / check ready
|
|
110
|
+
pip install -e ".[kafka]" # kafka produce / consume / assert
|
|
111
|
+
pip install -e ".[db]" # db query / assert
|
|
112
|
+
pip install -e ".[http,kafka,db]" # everything (typical)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Verify the install — both binary names work:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
agctl --help
|
|
119
|
+
agt --help
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Drop a config file named `agctl.yaml` at your repo root (or anywhere up the
|
|
123
|
+
directory tree). Confirm it loads and validates:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
agctl config validate
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then orient yourself — this is the first command an agent runs in a session:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
agctl discover
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## CLI abilities
|
|
138
|
+
|
|
139
|
+
`--config <path>` is a global flag on every command (otherwise `agctl.yaml` is
|
|
140
|
+
auto-discovered from the current directory upward).
|
|
141
|
+
|
|
142
|
+
| Group | Command | What it does |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| **`http`** | `call <template>` | Execute a named HTTP template from config |
|
|
145
|
+
| | `request` | Free-form request (escape hatch; `--service --method --path`) |
|
|
146
|
+
| | `ping <template>` | Repeat a request on an interval — stream NDJSON (session keepalive) |
|
|
147
|
+
| **`kafka`** | `produce` | Publish one message (`--topic --message`) |
|
|
148
|
+
| | `consume` | Read a topic; return up to `--expect-count` matches within `--timeout` |
|
|
149
|
+
| | `assert` | Fail (exit 1) unless a matching message arrives within `--timeout`. Modes: `--contains`, `--match <jq>`, `--pattern <name>` (combinable) |
|
|
150
|
+
| **`db`** | `query` | Run `--template` or free-form `--sql`; return all rows |
|
|
151
|
+
| | `assert` | Assert `--expect-rows N`, or `--expect-value --path <jq> --equals <v>` on the first row |
|
|
152
|
+
| **`check`** | `ready` | Hit `health_path` for one (`--service`) or all services; 2xx = ready |
|
|
153
|
+
| **`config`** | `validate` | Validate schema, env vars, cross-references, version |
|
|
154
|
+
| | `show` | Dump fully-resolved config as JSON (secrets masked) |
|
|
155
|
+
| **`discover`** | *(top-level)* | Three levels: summary → `--category` → `--name`; plus `--search` |
|
|
156
|
+
|
|
157
|
+
**Composing commands** — the core pattern is *send, then assert*:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# 1. Trigger an action
|
|
161
|
+
agctl http call create-order --param customer_id=cust-42 --param sku=WIDGET-001
|
|
162
|
+
|
|
163
|
+
# 2. Assert the downstream Kafka event arrived (reliable by default — see notes)
|
|
164
|
+
agctl kafka assert --topic orders.created --contains '{"customer_id": "cust-42"}' --timeout 10
|
|
165
|
+
|
|
166
|
+
# 3. Assert the DB reflects the new state
|
|
167
|
+
agctl db assert --template find-order --param orderId=ord-789 --expect-value \
|
|
168
|
+
--path ".status" --equals "PENDING"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Exit codes** (the contract the agent relies on):
|
|
172
|
+
|
|
173
|
+
| Code | Meaning |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `0` | Success; all assertions passed |
|
|
176
|
+
| `1` | An assertion was evaluated and **failed** — the system is not in the expected state |
|
|
177
|
+
| `2` | Tool/config/env error — not an assertion result; fix the invocation or environment |
|
|
178
|
+
|
|
179
|
+
> **Send-then-assert is reliable by default.** `kafka consume`/`assert` seek each
|
|
180
|
+
> partition to `now - --lookback` (default `= --timeout`) and read forward — they
|
|
181
|
+
> do **not** subscribe at "latest" — so an event published a moment before the
|
|
182
|
+
> command starts still falls inside the window.
|
|
183
|
+
|
|
184
|
+
See [`docs/DESIGN.md` §3](./docs/DESIGN.md) for the complete flag reference and
|
|
185
|
+
[`docs/DESIGN.md` §11](./docs/DESIGN.md) for end-to-end agentic workflow examples.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Configuration
|
|
190
|
+
|
|
191
|
+
`agctl` loads one `agctl.yaml` per invocation. Resolution order (highest first):
|
|
192
|
+
|
|
193
|
+
1. `--config <path>` — if given, *only* this file is loaded.
|
|
194
|
+
2. `AGCTL_CONFIG` — env var pointing at the config file.
|
|
195
|
+
3. **Walk-up discovery** — searches from the current directory upward for
|
|
196
|
+
`agctl.yaml`, stopping at the first `.git` or the filesystem root.
|
|
197
|
+
4. **`${ENV_VAR}` interpolation** in YAML string values (after parsing).
|
|
198
|
+
5. **`AGCTL_<SECTION>__<KEY>` overrides** — highest precedence; applied last.
|
|
199
|
+
|
|
200
|
+
If no file is found, it exits `2` with a `ConfigError`.
|
|
201
|
+
|
|
202
|
+
### Env-var interpolation (in any string value)
|
|
203
|
+
|
|
204
|
+
| Syntax | Behavior |
|
|
205
|
+
|---|---|
|
|
206
|
+
| `${VAR}` | **Required.** Missing → `ConfigError` (exit 2), never a silent empty string. |
|
|
207
|
+
| `${VAR:-default}` | Optional with a literal default. |
|
|
208
|
+
| `${VAR:-}` | Optional; missing → empty string. |
|
|
209
|
+
|
|
210
|
+
Three substitution syntaxes exist — don't conflate them:
|
|
211
|
+
|
|
212
|
+
- `${VAR}` — environment, resolved at config load.
|
|
213
|
+
- `{name}` — HTTP path/body & Kafka-pattern placeholders, filled at call time from `--param key=value`.
|
|
214
|
+
- `:name` — JDBC-style SQL params (templates and free-form `--sql`), bound at execute time.
|
|
215
|
+
|
|
216
|
+
### Env-var overrides
|
|
217
|
+
|
|
218
|
+
`AGCTL_<SECTION>__<KEY>=value` (double underscore separates path segments;
|
|
219
|
+
uppercase each segment; hyphens → `_` within a segment). Applied **after**
|
|
220
|
+
interpolation, with highest precedence:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
AGCTL_DEFAULTS__TIMEOUT_SECONDS=30
|
|
224
|
+
AGCTL_KAFKA__DEFAULT_CONSUMER_GROUP=ci-consumer
|
|
225
|
+
AGCTL_DATABASE__CONNECTIONS__MAIN_DB__HOST=localhost
|
|
226
|
+
AGCTL_DATABASE__CONNECTIONS__MAIN_DB__PASSWORD=supersecret
|
|
227
|
+
AGCTL_SERVICES__ORDER_SERVICE__BASE_URL=http://order-svc:8080
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Complete, copy-paste-ready config
|
|
231
|
+
|
|
232
|
+
Below is a full `agctl.yaml` with **concrete localhost values and no required env
|
|
233
|
+
vars** — drop it at your repo root and `agctl config validate` passes as-is. (The
|
|
234
|
+
production version is the same file with secrets/hosts moved into `${...}` and
|
|
235
|
+
sourced from a `.env` — see the note after it.)
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
# agctl.yaml
|
|
239
|
+
# Version tracks the agctl MAJOR version only (currently "1").
|
|
240
|
+
version: "1"
|
|
241
|
+
|
|
242
|
+
# --- services: named HTTP base URLs for services under test -----------------
|
|
243
|
+
services:
|
|
244
|
+
order-service:
|
|
245
|
+
base_url: "http://localhost:8081"
|
|
246
|
+
health_path: "/actuator/health" # used by `agctl check ready`
|
|
247
|
+
timeout_seconds: 10 # optional; overrides defaults.timeout_seconds
|
|
248
|
+
|
|
249
|
+
payment-service:
|
|
250
|
+
base_url: "http://localhost:8082"
|
|
251
|
+
health_path: "/health"
|
|
252
|
+
timeout_seconds: 15
|
|
253
|
+
|
|
254
|
+
# --- kafka: broker config --------------------------------------------------
|
|
255
|
+
kafka:
|
|
256
|
+
brokers:
|
|
257
|
+
- "localhost:9092"
|
|
258
|
+
default_consumer_group: "agctl-consumer"
|
|
259
|
+
schema_registry_url: "" # optional; omit/leave empty if unused
|
|
260
|
+
timeout_seconds: 30 # default consume/assert timeout
|
|
261
|
+
|
|
262
|
+
# Optional TLS/mTLS — uncomment for brokers that require SSL. Setting ANY
|
|
263
|
+
# field to a non-empty value enables TLS (security.protocol defaults to "SSL").
|
|
264
|
+
# ca_location is optional: unset → librdkafka uses the system trust store
|
|
265
|
+
# (fine for publicly-trusted brokers; pin a CA for private-PKI brokers).
|
|
266
|
+
# Hostname verification stays ON unless endpoint_identification_algorithm: "none".
|
|
267
|
+
# ssl:
|
|
268
|
+
# ca_location: ""
|
|
269
|
+
# certificate_location: "" # path to client cert (mTLS)
|
|
270
|
+
# key_location: "" # path to client private key (mTLS)
|
|
271
|
+
# key_password: "" # optional private-key password
|
|
272
|
+
# # endpoint_identification_algorithm: "none" # disable hostname verification
|
|
273
|
+
# # security_protocol: "SSL" # default; set SASL_SSL when adding SASL
|
|
274
|
+
|
|
275
|
+
# patterns: named Kafka filters, analogous to HTTP templates.
|
|
276
|
+
# topic: Kafka topic
|
|
277
|
+
# match: jq boolean predicate over each message value;
|
|
278
|
+
# supports {placeholder} substitution via --param at assert time
|
|
279
|
+
patterns:
|
|
280
|
+
order-created:
|
|
281
|
+
description: "An ORDER_CREATED event for a specific order"
|
|
282
|
+
topic: orders.created
|
|
283
|
+
match: '.eventType == "ORDER_CREATED" and .payload.orderId == "{orderId}"'
|
|
284
|
+
|
|
285
|
+
payment-failed:
|
|
286
|
+
description: "Any PAYMENT_FAILED event regardless of order"
|
|
287
|
+
topic: payments.events
|
|
288
|
+
match: '.eventType == "PAYMENT_FAILED"'
|
|
289
|
+
|
|
290
|
+
# --- database: named connection profiles and SQL templates ------------------
|
|
291
|
+
database:
|
|
292
|
+
connections:
|
|
293
|
+
main-db:
|
|
294
|
+
type: postgresql # extensible via plugins (entry point agctl.db_drivers)
|
|
295
|
+
host: "localhost"
|
|
296
|
+
port: 5432
|
|
297
|
+
dbname: "app"
|
|
298
|
+
user: "app"
|
|
299
|
+
password: "app"
|
|
300
|
+
default: true # used when --connection is omitted
|
|
301
|
+
|
|
302
|
+
analytics-db:
|
|
303
|
+
type: postgresql
|
|
304
|
+
host: "localhost"
|
|
305
|
+
port: 5432
|
|
306
|
+
dbname: "analytics"
|
|
307
|
+
user: "analytics"
|
|
308
|
+
password: "analytics"
|
|
309
|
+
|
|
310
|
+
# templates: named SQL queries. `connection` is optional (falls back to
|
|
311
|
+
# defaults.database_connection). Use :paramName named params (JDBC-style).
|
|
312
|
+
templates:
|
|
313
|
+
find-order:
|
|
314
|
+
description: "Fetch a single order by ID"
|
|
315
|
+
connection: main-db
|
|
316
|
+
sql: "SELECT id, status, total_cents, created_at FROM orders WHERE id = :orderId"
|
|
317
|
+
|
|
318
|
+
orders-by-status:
|
|
319
|
+
description: "List orders in a given status, optionally filtered by customer"
|
|
320
|
+
connection: main-db
|
|
321
|
+
sql: "SELECT id, status FROM orders WHERE status = :status AND customer_id = :customerId"
|
|
322
|
+
|
|
323
|
+
count-failed-payments:
|
|
324
|
+
description: "Count failed payments after a given timestamp"
|
|
325
|
+
connection: main-db
|
|
326
|
+
sql: "SELECT COUNT(*) AS cnt FROM payments WHERE status = 'FAILED' AND created_at > :since"
|
|
327
|
+
|
|
328
|
+
# --- templates: named HTTP request templates --------------------------------
|
|
329
|
+
templates:
|
|
330
|
+
create-order:
|
|
331
|
+
description: "Submit a new order for a customer"
|
|
332
|
+
method: POST
|
|
333
|
+
service: order-service
|
|
334
|
+
path: "/api/v1/orders"
|
|
335
|
+
headers:
|
|
336
|
+
Content-Type: "application/json"
|
|
337
|
+
X-Request-Source: "agctl"
|
|
338
|
+
body:
|
|
339
|
+
customer_id: "{customer_id}"
|
|
340
|
+
items:
|
|
341
|
+
- sku: "{sku}"
|
|
342
|
+
quantity: 1
|
|
343
|
+
|
|
344
|
+
get-order:
|
|
345
|
+
description: "Fetch a single order by ID"
|
|
346
|
+
method: GET
|
|
347
|
+
service: order-service
|
|
348
|
+
path: "/api/v1/orders/{order_id}"
|
|
349
|
+
|
|
350
|
+
charge-payment:
|
|
351
|
+
description: "Trigger payment charge for an order"
|
|
352
|
+
method: POST
|
|
353
|
+
service: payment-service
|
|
354
|
+
path: "/api/v1/payments"
|
|
355
|
+
headers:
|
|
356
|
+
Content-Type: "application/json"
|
|
357
|
+
Authorization: "Bearer ${PAYMENT_SERVICE_TOKEN}" # required env var
|
|
358
|
+
body:
|
|
359
|
+
order_id: "{order_id}"
|
|
360
|
+
amount_cents: "{amount_cents}"
|
|
361
|
+
|
|
362
|
+
get-payment-status:
|
|
363
|
+
description: "Fetch payment status by order ID"
|
|
364
|
+
method: GET
|
|
365
|
+
service: payment-service
|
|
366
|
+
path: "/api/v1/payments/{order_id}/status"
|
|
367
|
+
|
|
368
|
+
# --- defaults: project-wide fallbacks --------------------------------------
|
|
369
|
+
defaults:
|
|
370
|
+
timeout_seconds: 10
|
|
371
|
+
database_connection: main-db
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
> **Note:** `charge-payment` references `${PAYMENT_SERVICE_TOKEN}` (a *required*
|
|
375
|
+
> env var), so `config validate` will report it as unresolved until you export it.
|
|
376
|
+
> Either `export PAYMENT_SERVICE_TOKEN=...` or remove that template for a clean
|
|
377
|
+
> baseline.
|
|
378
|
+
|
|
379
|
+
**Moving to environment-driven config** — replace the concrete values above with
|
|
380
|
+
`${...}` and source them from a `.env` (agctl resolves them at load time):
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
# .env — never commit real secrets
|
|
384
|
+
ORDER_SERVICE_URL=http://order-svc:8081
|
|
385
|
+
PAYMENT_SERVICE_URL=http://payment-svc:8082
|
|
386
|
+
KAFKA_BROKER=kafka:9092
|
|
387
|
+
DB_HOST=postgres
|
|
388
|
+
DB_NAME=app
|
|
389
|
+
DB_USER=app
|
|
390
|
+
DB_PASSWORD=change-me
|
|
391
|
+
PAYMENT_SERVICE_TOKEN=change-me
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
# agctl.yaml (snippet) — same file, env-interpolated
|
|
396
|
+
services:
|
|
397
|
+
order-service:
|
|
398
|
+
base_url: "${ORDER_SERVICE_URL}"
|
|
399
|
+
database:
|
|
400
|
+
connections:
|
|
401
|
+
main-db:
|
|
402
|
+
host: "${DB_HOST}"
|
|
403
|
+
dbname: "${DB_NAME}"
|
|
404
|
+
password: "${DB_PASSWORD}"
|
|
405
|
+
port: "${DB_PORT:-5432}" # optional-with-default form
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Validate before committing, and use `config show` to inspect the resolved result
|
|
409
|
+
(secrets are masked; pass `--unmask` only in trusted environments):
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
agctl config validate
|
|
413
|
+
agctl config show
|
|
414
|
+
```
|