agent-suite-conformance 1.0.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.
@@ -0,0 +1,62 @@
1
+ # Operator config — holds real DSN hosts, project slugs, secret-backend refs.
2
+ # Never committed; a placeholder-only suite.env.example is committed instead.
3
+ suite.env
4
+ *.env
5
+ .env
6
+ secrets/
7
+
8
+ # Real deployment captures / component data — never committed
9
+ samples/
10
+ *.db
11
+ *.sqlite3
12
+ *.sqlite3-wal
13
+ *.sqlite3-shm
14
+
15
+ # Operator-specific convergence/proof evidence — never committed. The
16
+ # canonical release-evidence artifacts live under golden/ at the root
17
+ # (scrubbed, publication-clean). golden/local/ holds operator-specific
18
+ # proof logs that name real hostnames, user accounts, and infrastructure
19
+ # endpoints; per docs/publication-review.md these are publication-sensitive
20
+ # even when the canonical identifier denylist does not forbid them.
21
+ golden/local/
22
+
23
+ # Production operating-history exports — never committed. These contain real
24
+ # operational events and identifiers from the production store. A metadata
25
+ # manifest (README.md) records the bundle hash, counts, and verifier result;
26
+ # the full export belongs in access-controlled release evidence, not the public
27
+ # tree (Sol round-3 finding #3).
28
+ golden/operating-history/*.json
29
+
30
+ # Generated inventory snapshot — never committed. The inventory is a
31
+ # current-state read of the operator's checkouts + suite.env, so any
32
+ # committed snapshot is immediately self-stale (committing it changes the
33
+ # umbrella HEAD it just snapshotted). The canonical release-time artifact
34
+ # is generated by CI on tag push (see .github/workflows/release.yml when
35
+ # present) and attached to the release; local operators run
36
+ # `agent-suite inventory --json` for live inspection. See Sol round-2/3
37
+ # finding #2 and data/release-board.json WI-0.2 status_note.
38
+ data/candidate-inventory.json
39
+
40
+ # The generated lock is committed (it IS the release artifact); a local scratch is not
41
+ SUITE.local.lock
42
+
43
+ # Python
44
+ __pycache__/
45
+ *.py[cod]
46
+ .venv/
47
+ venv/
48
+ *.egg-info/
49
+ dist/
50
+ build/
51
+ .mypy_cache/
52
+ .ruff_cache/
53
+ .pytest_cache/
54
+ uv.lock
55
+
56
+ # Editors / OS
57
+ .DS_Store
58
+ *.swp
59
+ .venv/
60
+
61
+ # never commit the local denylist (identifier gate)
62
+ .identifiers-denylist.local
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-suite-conformance
3
+ Version: 1.0.0
4
+ Summary: CLI contract v1 conformance kit for the agent suite (Plan 018 WI-2 / Plan 019 B1).
5
+ License: MIT
6
+ Keywords: agent-suite,cli-contract,conformance,testing
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+
10
+ # agent-suite-conformance
11
+
12
+ The CLI contract v1 conformance kit for the agent suite (Plan 018 WI-2). One
13
+ centrally versioned, stdlib-only package that every suite
14
+ component depends on as a normal pinned version — so there is exactly one kit,
15
+ never per-repo copies to drift.
16
+
17
+ ## What it provides
18
+
19
+ `agent_suite.conformance` — success/error/usage/broken-pipe case runners and the
20
+ common error-envelope validator that hold each suite CLI to the contract in
21
+ `docs/cli-contract.md`:
22
+
23
+ - stdout under `--json` is exactly one JSON document (or documented NDJSON),
24
+ zero non-JSON bytes;
25
+ - documented error paths exit nonzero with the common error envelope;
26
+ - usage errors exit 2; broken pipe exits without a traceback;
27
+ - error output carries no secret material.
28
+
29
+ ## Use
30
+
31
+ ```toml
32
+ # in a component's dev/test dependencies
33
+ "agent-suite-conformance==1.0.0"
34
+ ```
35
+
36
+ ```python
37
+ from agent_suite.conformance import (
38
+ KIT_VERSION, CLI_CONTRACT_VERSION,
39
+ SuccessCase, ErrorCase, UsageCase,
40
+ run_success_case, run_error_case, run_usage_case,
41
+ validate_envelope,
42
+ )
43
+ ```
44
+
45
+ The kit discovers what to test from each component's CLI manifest
46
+ (`<tool> contract --json`); see the agent-suite CLI contract for the manifest
47
+ shape.
48
+
49
+ ## Provenance
50
+
51
+ Built from the single source of truth at `src/agent_suite/conformance/` in the
52
+ agent-suite repository. `version` here equals `agent_suite.conformance.KIT_VERSION`;
53
+ a guard test fails CI if they diverge.
@@ -0,0 +1,44 @@
1
+ # agent-suite-conformance
2
+
3
+ The CLI contract v1 conformance kit for the agent suite (Plan 018 WI-2). One
4
+ centrally versioned, stdlib-only package that every suite
5
+ component depends on as a normal pinned version — so there is exactly one kit,
6
+ never per-repo copies to drift.
7
+
8
+ ## What it provides
9
+
10
+ `agent_suite.conformance` — success/error/usage/broken-pipe case runners and the
11
+ common error-envelope validator that hold each suite CLI to the contract in
12
+ `docs/cli-contract.md`:
13
+
14
+ - stdout under `--json` is exactly one JSON document (or documented NDJSON),
15
+ zero non-JSON bytes;
16
+ - documented error paths exit nonzero with the common error envelope;
17
+ - usage errors exit 2; broken pipe exits without a traceback;
18
+ - error output carries no secret material.
19
+
20
+ ## Use
21
+
22
+ ```toml
23
+ # in a component's dev/test dependencies
24
+ "agent-suite-conformance==1.0.0"
25
+ ```
26
+
27
+ ```python
28
+ from agent_suite.conformance import (
29
+ KIT_VERSION, CLI_CONTRACT_VERSION,
30
+ SuccessCase, ErrorCase, UsageCase,
31
+ run_success_case, run_error_case, run_usage_case,
32
+ validate_envelope,
33
+ )
34
+ ```
35
+
36
+ The kit discovers what to test from each component's CLI manifest
37
+ (`<tool> contract --json`); see the agent-suite CLI contract for the manifest
38
+ shape.
39
+
40
+ ## Provenance
41
+
42
+ Built from the single source of truth at `src/agent_suite/conformance/` in the
43
+ agent-suite repository. `version` here equals `agent_suite.conformance.KIT_VERSION`;
44
+ a guard test fails CI if they diverge.
@@ -0,0 +1,47 @@
1
+ """CLI contract v1 conformance kit (Plan 018 WI-2).
2
+
3
+ One centrally versioned package, owned by agent-suite, consumed pinned by
4
+ every component — never copied, so there is exactly one kit to drift from
5
+ (the identifier-gate distribution model applied to conformance).
6
+
7
+ ``KIT_VERSION`` identifies the kit in recorded conformance results
8
+ (``data/cli-conformance.json``); ``CLI_CONTRACT_VERSION`` is the contract
9
+ revision the kit enforces (``docs/cli-contract.md``).
10
+ """
11
+
12
+ from agent_suite.conformance.envelope import (
13
+ build_envelope,
14
+ emit_error,
15
+ validate_envelope,
16
+ )
17
+ from agent_suite.conformance.kit import (
18
+ BrokenPipeCase,
19
+ ErrorCase,
20
+ Framing,
21
+ SuccessCase,
22
+ UsageCase,
23
+ run_broken_pipe_case,
24
+ run_error_case,
25
+ run_success_case,
26
+ run_usage_case,
27
+ )
28
+
29
+ KIT_VERSION = "1.0.0"
30
+ CLI_CONTRACT_VERSION = 1
31
+
32
+ __all__ = [
33
+ "KIT_VERSION",
34
+ "CLI_CONTRACT_VERSION",
35
+ "BrokenPipeCase",
36
+ "ErrorCase",
37
+ "Framing",
38
+ "SuccessCase",
39
+ "UsageCase",
40
+ "build_envelope",
41
+ "emit_error",
42
+ "run_broken_pipe_case",
43
+ "run_error_case",
44
+ "run_success_case",
45
+ "run_usage_case",
46
+ "validate_envelope",
47
+ ]
@@ -0,0 +1,114 @@
1
+ """The common CLI error envelope (CLI contract v1 §3).
2
+
3
+ ``build_envelope`` constructs the shape, ``validate_envelope`` checks a
4
+ parsed document against it (stdlib-only — the schema at
5
+ ``data/cli-error-envelope.schema.json`` is the normative statement; this
6
+ validator mirrors it for consumers that must stay dependency-free), and
7
+ ``emit_error`` is the helper a conforming CLI calls on every error path.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ import sys
15
+ from typing import Any
16
+
17
+ _CODE_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
18
+
19
+
20
+ def build_envelope(
21
+ code: str,
22
+ message: str,
23
+ *,
24
+ detail: str | None = None,
25
+ retryable: bool = False,
26
+ partial: dict[str, Any] | None = None,
27
+ ) -> dict[str, Any]:
28
+ """Construct a contract-v1 error envelope."""
29
+ if not _CODE_RE.match(code):
30
+ raise ValueError(f"error code {code!r} is not SCREAMING_SNAKE")
31
+ return {
32
+ "ok": False,
33
+ "error": {
34
+ "code": code,
35
+ "message": message,
36
+ "detail": detail,
37
+ "retryable": retryable,
38
+ "partial": partial,
39
+ },
40
+ }
41
+
42
+
43
+ def validate_envelope(document: Any) -> list[str]:
44
+ """Return contract violations for a parsed error document (empty = valid)."""
45
+ violations: list[str] = []
46
+ if not isinstance(document, dict):
47
+ return ["envelope is not a JSON object"]
48
+ if document.get("ok") is not False:
49
+ violations.append("envelope 'ok' is not false")
50
+ error = document.get("error")
51
+ if not isinstance(error, dict):
52
+ return violations + ["envelope 'error' is not an object"]
53
+ code = error.get("code")
54
+ if not isinstance(code, str) or not _CODE_RE.match(code):
55
+ violations.append(f"error 'code' {code!r} is not a SCREAMING_SNAKE string")
56
+ if not isinstance(error.get("message"), str):
57
+ violations.append("error 'message' is not a string")
58
+ if "detail" in error and not isinstance(error["detail"], (str, type(None))):
59
+ violations.append("error 'detail' is not a string or null")
60
+ if not isinstance(error.get("retryable"), bool):
61
+ violations.append("error 'retryable' is not a boolean")
62
+ partial = error.get("partial")
63
+ if partial is not None:
64
+ if not isinstance(partial, dict):
65
+ violations.append("error 'partial' is not an object or null")
66
+ else:
67
+ succeeded = partial.get("succeeded")
68
+ failed = partial.get("failed")
69
+ if not isinstance(succeeded, int) or succeeded < 0:
70
+ violations.append("partial 'succeeded' is not a non-negative integer")
71
+ if not isinstance(failed, int) or failed < 1:
72
+ violations.append("partial 'failed' is not a positive integer")
73
+ unknown = set(error) - {"code", "message", "detail", "retryable", "partial"}
74
+ if unknown:
75
+ violations.append(f"error object has unknown fields: {sorted(unknown)}")
76
+ unknown_top = set(document) - {"ok", "error"}
77
+ if unknown_top:
78
+ violations.append(f"envelope has unknown fields: {sorted(unknown_top)}")
79
+ return violations
80
+
81
+
82
+ def emit_error(
83
+ code: str,
84
+ message: str,
85
+ *,
86
+ detail: str | None = None,
87
+ retryable: bool = False,
88
+ partial: dict[str, Any] | None = None,
89
+ json_mode: bool = False,
90
+ ) -> int:
91
+ """Report an error per the contract and return the exit code (1).
92
+
93
+ Under ``--json`` the envelope is the single stdout document; otherwise
94
+ the message (and detail) go to stderr. Either way the caller returns
95
+ the value returned here — no path prints an error and exits 0.
96
+ """
97
+ if json_mode:
98
+ print(
99
+ json.dumps(
100
+ build_envelope(
101
+ code,
102
+ message,
103
+ detail=detail,
104
+ retryable=retryable,
105
+ partial=partial,
106
+ ),
107
+ indent=2,
108
+ )
109
+ )
110
+ else:
111
+ print(f"error: {message}", file=sys.stderr)
112
+ if detail:
113
+ print(f" {detail}", file=sys.stderr)
114
+ return 1
@@ -0,0 +1,215 @@
1
+ """Behavioral conformance checks for CLI contract v1 (Plan 018 WI-2).
2
+
3
+ Each check runs a real subprocess against the component's CLI and returns
4
+ a list of contract violations (empty = conformant). Components declare
5
+ cases (fixtures) and parameterize pytest over them; the checks themselves
6
+ live only here, centrally versioned.
7
+
8
+ The checks prove the contract, not the implementation: stream discipline
9
+ (§1), exit-code taxonomy (§2), the error envelope (§3), traceback and
10
+ broken-pipe robustness (§4), and secret redaction (§3). Output honesty
11
+ (§5) and manifest discovery (§6) are P1 — cases for them extend these
12
+ dataclasses rather than forking the kit.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import sys
21
+ from dataclasses import dataclass, field
22
+ from enum import Enum
23
+ from typing import Mapping
24
+
25
+ from agent_suite.conformance.envelope import validate_envelope
26
+
27
+ _TRACEBACK_MARKER = "Traceback (most recent call last)"
28
+
29
+ # Value planted in per-case env vars to prove error paths never echo
30
+ # secret material (contract §3 redaction). Cases list the env var *names*
31
+ # whose values must never surface; the kit plants this sentinel there.
32
+ SENTINEL_SECRET = "conformance-sentinel-3f9d2c"
33
+
34
+
35
+ class Framing(Enum):
36
+ DOCUMENT = "document"
37
+ NDJSON = "ndjson"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class SuccessCase:
42
+ """A verb invocation that must succeed with pure JSON stdout."""
43
+
44
+ name: str
45
+ argv: tuple[str, ...]
46
+ framing: Framing = Framing.DOCUMENT
47
+ env: Mapping[str, str] = field(default_factory=dict)
48
+ unset_env: tuple[str, ...] = ()
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ErrorCase:
53
+ """A documented failure that must exit nonzero with the envelope."""
54
+
55
+ name: str
56
+ argv: tuple[str, ...]
57
+ expect_code: str | None = None
58
+ json_mode: bool = True
59
+ env: Mapping[str, str] = field(default_factory=dict)
60
+ unset_env: tuple[str, ...] = ()
61
+ secret_env_names: tuple[str, ...] = ()
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class UsageCase:
66
+ """A malformed invocation that must exit 2 (argparse usage error)."""
67
+
68
+ name: str
69
+ argv: tuple[str, ...]
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class BrokenPipeCase:
74
+ """A verb whose stdout is closed early; it must not traceback."""
75
+
76
+ name: str
77
+ argv: tuple[str, ...]
78
+ env: Mapping[str, str] = field(default_factory=dict)
79
+
80
+
81
+ CASE_TIMEOUT: float = 60.0
82
+
83
+
84
+ def _run(
85
+ argv: tuple[str, ...],
86
+ env: Mapping[str, str],
87
+ unset_env: tuple[str, ...] = (),
88
+ timeout: float = CASE_TIMEOUT,
89
+ ) -> subprocess.CompletedProcess[str]:
90
+ merged = {**os.environ, "PYTHONIOENCODING": "utf-8", **env}
91
+ for name in unset_env:
92
+ merged.pop(name, None)
93
+ try:
94
+ return subprocess.run(
95
+ list(argv),
96
+ capture_output=True,
97
+ text=True,
98
+ check=False,
99
+ env=merged,
100
+ timeout=timeout,
101
+ )
102
+ except subprocess.TimeoutExpired:
103
+ return subprocess.CompletedProcess(
104
+ args=list(argv),
105
+ returncode=124,
106
+ stdout="",
107
+ stderr=f"conformance: timed out after {timeout}s",
108
+ )
109
+
110
+
111
+ def run_success_case(case: SuccessCase) -> list[str]:
112
+ """Contract §1 + §2: exit 0, stdout is pure JSON, no traceback."""
113
+ proc = _run(case.argv, case.env, case.unset_env)
114
+ violations: list[str] = []
115
+ if proc.returncode != 0:
116
+ violations.append(
117
+ f"exit {proc.returncode}, expected 0; stderr: {proc.stderr[-500:]!r}"
118
+ )
119
+ if _TRACEBACK_MARKER in proc.stderr:
120
+ violations.append("traceback on a documented success path")
121
+ stdout = proc.stdout.strip()
122
+ if not stdout:
123
+ violations.append("empty stdout on a JSON success path")
124
+ return violations
125
+ match case.framing:
126
+ case Framing.DOCUMENT:
127
+ try:
128
+ json.loads(stdout)
129
+ except json.JSONDecodeError as exc:
130
+ violations.append(
131
+ f"stdout is not a single JSON document ({exc}); "
132
+ f"first 200 bytes: {stdout[:200]!r}"
133
+ )
134
+ case Framing.NDJSON:
135
+ for lineno, line in enumerate(stdout.splitlines(), start=1):
136
+ if not line.strip():
137
+ continue
138
+ try:
139
+ json.loads(line)
140
+ except json.JSONDecodeError:
141
+ violations.append(
142
+ f"NDJSON line {lineno} is not JSON: {line[:200]!r}"
143
+ )
144
+ return violations
145
+
146
+
147
+ def run_error_case(case: ErrorCase) -> list[str]:
148
+ """Contract §2 + §3: nonzero exit, envelope on stdout, no leaks."""
149
+ env = dict(case.env)
150
+ for name in case.secret_env_names:
151
+ env[name] = SENTINEL_SECRET
152
+ proc = _run(case.argv, env, case.unset_env)
153
+ violations: list[str] = []
154
+ if proc.returncode == 0:
155
+ violations.append(
156
+ "exit 0 on a documented error path (the fail-open class); "
157
+ f"stdout: {proc.stdout[:200]!r} stderr: {proc.stderr[:200]!r}"
158
+ )
159
+ if proc.returncode == 2:
160
+ violations.append("exit 2 (usage) on an operational error path")
161
+ if _TRACEBACK_MARKER in proc.stderr or _TRACEBACK_MARKER in proc.stdout:
162
+ violations.append("traceback on a documented error path")
163
+ if case.secret_env_names and SENTINEL_SECRET in proc.stdout + proc.stderr:
164
+ violations.append("secret material leaked into error output")
165
+ if case.json_mode:
166
+ stdout = proc.stdout.strip()
167
+ if not stdout:
168
+ violations.append("no envelope on stdout for a --json error path")
169
+ return violations
170
+ try:
171
+ document = json.loads(stdout)
172
+ except json.JSONDecodeError:
173
+ violations.append(
174
+ f"--json error stdout is not JSON: {stdout[:200]!r}"
175
+ )
176
+ return violations
177
+ violations.extend(validate_envelope(document))
178
+ if case.expect_code is not None and not violations:
179
+ actual = document["error"]["code"]
180
+ if actual != case.expect_code:
181
+ violations.append(
182
+ f"error code {actual!r}, expected {case.expect_code!r}"
183
+ )
184
+ return violations
185
+
186
+
187
+ def run_usage_case(case: UsageCase) -> list[str]:
188
+ """Contract §2: malformed invocations exit 2, without traceback."""
189
+ proc = _run(case.argv, {})
190
+ violations: list[str] = []
191
+ if proc.returncode != 2:
192
+ violations.append(f"exit {proc.returncode}, expected 2 (usage error)")
193
+ if _TRACEBACK_MARKER in proc.stderr:
194
+ violations.append("traceback on a usage error")
195
+ return violations
196
+
197
+
198
+ def run_broken_pipe_case(case: BrokenPipeCase) -> list[str]:
199
+ """Contract §4: closing stdout early must not produce a traceback."""
200
+ if sys.platform == "win32":
201
+ return [] # SIGPIPE semantics don't apply; nothing to prove here.
202
+ with subprocess.Popen(
203
+ list(case.argv),
204
+ stdout=subprocess.PIPE,
205
+ stderr=subprocess.PIPE,
206
+ env={**os.environ, **case.env},
207
+ ) as proc:
208
+ assert proc.stdout is not None
209
+ proc.stdout.read(1)
210
+ proc.stdout.close()
211
+ stderr = proc.stderr.read() if proc.stderr else b""
212
+ proc.wait(timeout=120)
213
+ if _TRACEBACK_MARKER.encode() in stderr:
214
+ return ["traceback on broken stdout pipe"]
215
+ return []
@@ -0,0 +1,41 @@
1
+ # Standalone distribution for the CLI contract v1 conformance kit (Plan 019 B1).
2
+ #
3
+ # ONE source of truth: src/agent_suite/conformance/ in this repo. This project
4
+ # builds that subtree into a tiny, publishable wheel — agent-suite-conformance —
5
+ # so sibling components depend on it as a normal pinned version
6
+ # (`agent-suite-conformance==X.Y`) instead of a git+SHA direct reference (which
7
+ # PyPI forbids in uploaded metadata, and which broke regista's 0.5.3 publish).
8
+ #
9
+ # The wheel ships only agent_suite/conformance/ with no agent_suite/__init__.py,
10
+ # so `agent_suite` resolves as a PEP 420 namespace in a consumer that installs
11
+ # ONLY this package. agent-suite proper is never published, so there is no
12
+ # regular-vs-namespace collision in practice.
13
+ #
14
+ # VERSION MUST EQUAL agent_suite.conformance.KIT_VERSION. The guard test
15
+ # tests/test_conformance_packaging.py fails CI if they drift.
16
+
17
+ [project]
18
+ name = "agent-suite-conformance"
19
+ version = "1.0.0"
20
+ description = "CLI contract v1 conformance kit for the agent suite (Plan 018 WI-2 / Plan 019 B1)."
21
+ readme = "README.md"
22
+ requires-python = ">=3.12"
23
+ license = { text = "MIT" }
24
+ keywords = ["agent-suite", "conformance", "cli-contract", "testing"]
25
+ dependencies = [] # stdlib-only, by design
26
+ # No authors/urls: matches the published sibling (regista) and avoids emitting
27
+ # a placeholder or a link to the private agent-suite repo on a public PyPI page.
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ # Map the shared source subtree into the wheel as agent_suite/conformance/.
35
+ # No agent_suite/__init__.py is shipped -> PEP 420 namespace on the consumer.
36
+ force-include = { "../../src/agent_suite/conformance" = "agent_suite/conformance" }
37
+ # force-include alone defines the wheel contents; no auto-detected packages.
38
+ bypass-selection = true
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ force-include = { "../../src/agent_suite/conformance" = "agent_suite/conformance", "README.md" = "README.md" }