Skip to content

Commit 8ed5d6e

Browse files
fix(pantry): clarify version policy rejection wording (#480) (#511)
Reword pantry health messages so dev/prerelease builds read as deliberate policy rejection instead of parse failure, while preserving the fixed invalid-version label and never echoing raw version strings. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e27dba8 commit 8ed5d6e

5 files changed

Lines changed: 81 additions & 23 deletions

File tree

docs/technical-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,7 @@ First-class station CLIs (always registered; not extras-gated):
13721372

13731373
`pantry` (alias `larder`) is the agent session auth sync station. Agent Pantry remains a process-boundary Go binary; Brigade never imports it.
13741374
`brigade add pantry` installs agentpantry via `go install github.com/escoffier-labs/agentpantry/cmd/agentpantry@latest` and prints the first-class operator path (setup plan, doctor, expiry-alert).
1375+
Before invoking any installed agentpantry surface, Brigade probes `agentpantry version --json` and accepts only released ASCII semver triples (optional leading `v`, no prerelease or build suffix). Dev builds, prerelease tags, and other non-triple strings are rejected by version policy, not because parsing failed. Brigade never echoes the raw rejected version string in `work brief`, doctor output, logs, or receipts; it surfaces a fixed policy message instead.
13751376
`brigade doctor` health-checks it with `agentpantry doctor --json --no-net` and keeps a compatibility fallback to `agentpantry status --json` for older binaries.
13761377
Like the memory satellites, agentpantry inspects host-global state, so its checks are advisory and never FAIL a workspace run: an unwired install (exit 2, no config) is a `WARN`, and setup problems are surfaced as advisory pantry health.
13771378
Use `brigade pantry status` and `brigade pantry doctor` for pantry-specific health with explicit `next` commands, `brigade pantry setup plan --role source|sink` to preview or write a reviewed setup plan, and `brigade pantry service plan --role source|sink` to preview or write service setup steps.

src/brigade/pantry_compat.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
probe, malformed JSON, non-string version, prerelease-shaped, or below-floor
1414
version all collapse to an incompatible :class:`VersionProbe` with a precise
1515
detail string.
16+
17+
**Version policy:** Brigade accepts only released ASCII semver triples (optional
18+
leading ``v``, no prerelease or build suffix). Dev builds, prerelease tags,
19+
unknown labels, and any other non-triple string are rejected deliberately, not
20+
because parsing failed. Those values collapse to the fixed
21+
:data:`_INVALID_VERSION_LABEL` in :attr:`VersionProbe.observed` so the raw
22+
version field never reaches ``work brief``, doctor output, logs, or receipts.
23+
The surfaced detail explains policy rejection instead of echoing the raw string.
1624
"""
1725

1826
from __future__ import annotations
@@ -97,6 +105,17 @@ def floor_label() -> str:
97105
return f"expected >= {major}.{minor}.{patch}"
98106

99107

108+
def released_floor_label() -> str:
109+
"""Return the released-build floor expectation, e.g. ``expected released >= 0.5.0``."""
110+
major, minor, patch = AGENTPANTRY_MIN_VERSION
111+
return f"expected released >= {major}.{minor}.{patch}"
112+
113+
114+
def _policy_rejected_detail() -> str:
115+
"""Detail string when a version string fails the released-semver policy."""
116+
return f"agentpantry build rejected by version policy (unreleased or non-semver build); {released_floor_label()}"
117+
118+
100119
def parse_version(value: object) -> Optional[Tuple[int, int, int]]:
101120
"""Parse a version value into an integer ``(major, minor, patch)`` triple.
102121
@@ -172,10 +191,16 @@ def probe_agentpantry_version() -> VersionProbe:
172191
parsed = parse_version(raw_version)
173192
if parsed is None:
174193
observed = _unparsable_observed_label(raw_version)
194+
if observed == _INVALID_VERSION_LABEL:
195+
detail = _policy_rejected_detail()
196+
elif observed == "missing":
197+
detail = f"agentpantry version field missing; {expected}"
198+
else:
199+
detail = f"agentpantry version field is not a string; {expected}"
175200
return VersionProbe(
176201
compatible=False,
177202
observed=observed,
178-
detail=f"agentpantry version unparsable ({observed}); {expected}",
203+
detail=detail,
179204
)
180205
observed = _format_triple(parsed)
181206
if parsed < AGENTPANTRY_MIN_VERSION:

tests/test_managed.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from brigade import managed
6+
from brigade import managed, pantry_compat
77
from brigade import station_manifest
88
from brigade import stations_cmd
99
from brigade.station import DoctorContext
@@ -296,9 +296,20 @@ def fake_run(args, **kw):
296296
ctx = DoctorContext(target=Path("/tmp/ws"), selection=None, harnesses=[])
297297
results = t.doctor(ctx)
298298
assert all(status != "FAIL" and status != "OK" for status, _, _ in results)
299-
assert any(
300-
status == "WARN" and "expected >= 0.5.0" in detail and observed in detail for status, _, detail in results
301-
), (version_value, observed, results)
299+
if observed == "invalid-version":
300+
assert any(
301+
status == "WARN"
302+
and "rejected by version policy" in detail
303+
and pantry_compat.released_floor_label() in detail
304+
for status, _, detail in results
305+
), (version_value, observed, results)
306+
else:
307+
assert any(
308+
status == "WARN"
309+
and "expected >= 0.5.0" in detail
310+
and (observed in detail if observed != "non-string" else "is not a string" in detail)
311+
for status, _, detail in results
312+
), (version_value, observed, results)
302313
# The raw invalid version field must not leak into any surfaced doctor detail.
303314
raw = "" if version_value is None else str(version_value)
304315
if raw:
@@ -321,7 +332,7 @@ def fake_run(args, **kw):
321332
results = t.doctor(ctx)
322333
assert all(status != "FAIL" and status != "OK" for status, _, _ in results)
323334
assert any(
324-
status == "WARN" and "invalid-version" in detail and "expected >= 0.5.0" in detail
335+
status == "WARN" and "rejected by version policy" in detail and pantry_compat.released_floor_label() in detail
325336
for status, _, detail in results
326337
), results
327338
assert all(secret not in detail for _, _, detail in results), results

tests/test_pantry_cmd.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22

3-
from brigade import center_cmd, pantry_cmd, work_cmd
3+
from brigade import center_cmd, pantry_cmd, pantry_compat, work_cmd
44

55

66
def test_pantry_status_reports_uninstalled(monkeypatch, tmp_path):
@@ -193,6 +193,24 @@ def test_work_brief_includes_pantry_health(monkeypatch, tmp_path):
193193
assert payload["pantry"]["summary"] == "pantry test summary"
194194

195195

196+
def test_work_brief_surfaces_pantry_policy_rejection_without_leak(monkeypatch, tmp_path, capsys):
197+
_pantry_installed(monkeypatch)
198+
199+
def fake_run(args, **kw):
200+
assert args == ["agentpantry", "version", "--json"]
201+
return pantry_cmd.proc.Result(code=0, stdout='{"version": "dev"}', stderr="")
202+
203+
monkeypatch.setattr(pantry_cmd.proc, "run", fake_run)
204+
205+
assert work_cmd.brief(target=tmp_path) == 0
206+
out = capsys.readouterr().out
207+
pantry_line = next(line for line in out.splitlines() if line.startswith("pantry: "))
208+
assert "agentpantry build rejected by version policy" in pantry_line
209+
assert pantry_compat.released_floor_label() in pantry_line
210+
assert "unparsable" not in pantry_line
211+
assert "dev" not in pantry_line
212+
213+
196214
def test_center_status_includes_pantry_health(monkeypatch, tmp_path):
197215
monkeypatch.setattr(
198216
pantry_cmd, "status_payload", lambda target: {"installed": False, "summary": "pantry center summary"}
@@ -301,8 +319,8 @@ def fake_run(args, **kw):
301319
# An unparsable version string collapses to the fixed sanitized label and
302320
# never leaks the raw value into the surfaced pantry payload.
303321
assert payload["version"] == "invalid-version"
304-
assert "expected >= 0.5.0" in payload["summary"]
305-
assert "invalid-version" in payload["summary"]
322+
assert "rejected by version policy" in payload["summary"]
323+
assert pantry_compat.released_floor_label() in payload["summary"]
306324
assert "dev" not in payload["summary"]
307325
assert "dev" not in payload["version"]
308326
assert calls == [["agentpantry", "version", "--json"]]
@@ -324,7 +342,7 @@ def fake_run(args, **kw):
324342
assert payload["version"] == "invalid-version"
325343
assert secret not in payload["version"]
326344
assert secret not in payload["summary"]
327-
assert "invalid-version" in payload["summary"]
345+
assert "rejected by version policy" in payload["summary"]
328346

329347

330348
def test_pantry_status_unhealthy_on_missing_version_field(monkeypatch, tmp_path):

tests/test_pantry_compat.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,27 +114,28 @@ def test_probe_incompatible_on_non_string_version(monkeypatch):
114114
assert probe.compatible is False
115115
assert probe.observed == "non-string"
116116
assert "expected >= 0.5.0" in probe.detail
117-
assert "non-string" in probe.detail
117+
assert "is not a string" in probe.detail
118118

119119

120120
@pytest.mark.parametrize("version", ["dev", "unknown", "0.5.0-dev", "0.5.0-rc.1", "0.5", ""])
121121
def test_probe_incompatible_on_unparsable_string_versions(monkeypatch, version):
122122
probe = _probe(monkeypatch, stdout=json.dumps({"version": version}))
123123
assert probe.compatible is False
124-
assert "expected >= 0.5.0" in probe.detail
124+
assert probe.observed == "invalid-version"
125+
assert "rejected by version policy" in probe.detail
126+
assert "unreleased or non-semver build" in probe.detail
127+
assert pantry_compat.released_floor_label() in probe.detail
128+
assert "unparsable" not in probe.detail
125129
# Any unparsable string collapses to the fixed sanitized label; the raw
126130
# invalid version field never reaches observed or detail.
127-
assert probe.observed == "invalid-version"
128-
# The raw value must not leak (skip the substring check for the empty
129-
# string, which is vacuously a substring of every string).
130131
if version:
131132
assert version not in probe.observed
132-
# detail always carries the fixed safe floor text, so a raw value that
133-
# overlaps the floor (e.g. "0.5" is a substring of "expected >= 0.5.0")
134-
# would make a bare substring assertion vacuously fail. Strip the fixed
135-
# floor text first; the raw invalid field must not appear in the
136-
# remainder, which still proves no raw content leaks.
137-
assert version not in probe.detail.replace(pantry_compat.floor_label(), "")
133+
# detail carries the fixed safe floor text, so a raw value that
134+
# overlaps the floor (e.g. "0.5" is a substring of "expected released
135+
# >= 0.5.0") would make a bare substring assertion vacuously fail.
136+
# Strip the fixed floor text first; the raw invalid field must not
137+
# appear in the remainder, which still proves no raw content leaks.
138+
assert version not in probe.detail.replace(pantry_compat.released_floor_label(), "")
138139

139140

140141
def test_probe_unparsable_secret_version_field_never_leaks(monkeypatch):
@@ -146,6 +147,7 @@ def test_probe_unparsable_secret_version_field_never_leaks(monkeypatch):
146147
assert probe.observed == "invalid-version"
147148
assert secret not in probe.observed
148149
assert secret not in probe.detail
150+
assert "rejected by version policy" in probe.detail
149151

150152

151153
def test_probe_unparsable_version_field_never_leaks_other_stdout(monkeypatch):
@@ -161,6 +163,7 @@ def test_probe_unparsable_version_field_never_leaks_other_stdout(monkeypatch):
161163
assert "/home/user/private" not in probe.detail
162164
assert "prerelease" not in probe.observed
163165
assert "prerelease" not in probe.detail
166+
assert "rejected by version policy" in probe.detail
164167

165168

166169
def test_probe_below_floor_still_exposes_normalized_semver(monkeypatch):
@@ -240,7 +243,7 @@ def test_probe_oversized_segment_collapses_to_invalid_version_label(monkeypatch)
240243
assert huge not in probe.detail
241244
assert raw_version not in probe.observed
242245
assert raw_version not in probe.detail
243-
assert "expected >= 0.5.0" in probe.detail
246+
assert pantry_compat.released_floor_label() in probe.detail
244247

245248

246249
def test_probe_non_ascii_digit_version_collapses_to_invalid_version_label(monkeypatch):
@@ -252,7 +255,7 @@ def test_probe_non_ascii_digit_version_collapses_to_invalid_version_label(monkey
252255
assert probe.observed == "invalid-version"
253256
assert raw_version not in probe.observed
254257
assert raw_version not in probe.detail
255-
assert "expected >= 0.5.0" in probe.detail
258+
assert pantry_compat.released_floor_label() in probe.detail
256259

257260

258261
def test_probe_oversized_segment_never_leaks_other_stdout(monkeypatch):

0 commit comments

Comments
 (0)