Skip to content

Commit a29ec4e

Browse files
solomonneasclaude
andauthored
test(harness): validate Hermes and Antigravity runtimes (#450)
* test(harness): validate hermes and antigravity runtimes via version probe Replace the antigravity installation proxy (bare command_candidates availability on a gui surface that could never execute) with direct runtime evidence: gui/desktop fixtures that declare a CLI-companion binary block now run the same sandboxed --version probe, and the observed/nonzero receipts carry a parsed version line and a platform field. Hermes already used the cli path; both surfaces now have tests pinning the acceptance contract: - binary available: version probe records vendor version + platform - binary missing: externally_blocked with exact reason binary_not_found - a present home/config directory alone never counts as conformance Safety guards are unchanged: external_only desktop/gui surfaces still short-circuit, gui fixtures without a binary block stay limited to availability-only evidence, the exact [--version] gate still blocks unsafe args before spawn, and the sandbox keeps minimal env, temp HOME, bounded output, and redaction. Closes #343 Co-authored-by: Claude <noreply@anthropic.com> * fix(harness): align probe candidates and version parsing per Greptile review P1: a desktop/GUI install whose CLI companion is present under a declared availability candidate instead of the primary binary command produced a contradictory receipt (availability available, version probe binary_not_found). The version probe now resolves whichever declared candidate is present, so both signals describe the same runtime command. P2: the parsed version was the first nonblank output line, so a warning or banner printed before the version was reported as version evidence. The receipt now reports the first line carrying a dotted version number and null when no such line exists. Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dbef8a0 commit a29ec4e

3 files changed

Lines changed: 244 additions & 9 deletions

File tree

docs/research/fixtures/harness-contract.v1/antigravity.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"schema": "harness-contract.v1",
33
"harness": {"id": "antigravity", "surface": "gui"},
4+
"binary": {"command": "agy", "version_args": ["--version"]},
45
"availability": {"command_candidates": ["agy", "antigravity"]},
56
"capabilities": [
67
{"id": "instructions", "claim": "Skills codelab exists, but no local binary was available.", "provenance": "documented", "support_state": "externally_blocked", "implementation_layers": ["unknown"], "evidence": [{"kind": "vendor_documentation", "reference": "https://codelabs.developers.google.com/getting-started-with-antigravity-skills"}], "tested_version": null, "platform": "linux", "scope": "externally blocked"},

tests/test_harness_conformance_probe.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,197 @@ def test_antigravity_is_gui_surface_with_availability_only_candidates(probe, sch
399399
assert result["version_probe"]["state"] == "skipped"
400400

401401

402+
def _fake_version_process(exit_code: int = 0) -> mock.Mock:
403+
process = mock.Mock()
404+
process.poll.return_value = exit_code
405+
process.pid = 6161
406+
return process
407+
408+
409+
def test_hermes_version_probe_records_version_and_platform_receipt(probe, schema, fixtures) -> None:
410+
fixture = next(item for item in fixtures if item["harness"]["id"] == "hermes")
411+
with (
412+
mock.patch.object(probe.shutil, "which", return_value="/fake/bin/hermes"),
413+
mock.patch.object(probe, "_popen_probe_process", return_value=_fake_version_process()),
414+
mock.patch.object(
415+
probe,
416+
"_collect_bounded_output",
417+
return_value=(b"hermes 0.3.1\n", False, None),
418+
),
419+
):
420+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
421+
version_probe = result["version_probe"]
422+
assert version_probe["state"] == "observed"
423+
assert version_probe["command"] == "hermes"
424+
assert version_probe["version"] == "hermes 0.3.1"
425+
assert version_probe["platform"] == sys.platform
426+
assert "hermes 0.3.1" in version_probe["output"]
427+
428+
429+
def test_hermes_missing_binary_stays_externally_blocked_binary_not_found(probe, schema, fixtures) -> None:
430+
fixture = next(item for item in fixtures if item["harness"]["id"] == "hermes")
431+
with mock.patch.object(probe.shutil, "which", return_value=None):
432+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
433+
assert result["availability"]["state"] == "externally_blocked"
434+
assert result["availability"]["reason"] == "binary_not_found"
435+
assert result["version_probe"]["state"] == "externally_blocked"
436+
assert result["version_probe"]["reason"] == "binary_not_found"
437+
438+
439+
def test_hermes_home_directory_only_never_counts_as_runtime_conformance(
440+
probe, schema, fixtures, monkeypatch, tmp_path: Path
441+
) -> None:
442+
fixture = next(item for item in fixtures if item["harness"]["id"] == "hermes")
443+
home = tmp_path / "home"
444+
(home / ".hermes").mkdir(parents=True)
445+
(home / ".config" / "hermes").mkdir(parents=True)
446+
monkeypatch.setenv("HOME", str(home))
447+
with mock.patch.object(probe.shutil, "which", return_value=None):
448+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
449+
for section in (result["availability"], result["version_probe"]):
450+
assert section["state"] == "externally_blocked"
451+
assert section["reason"] == "binary_not_found"
452+
453+
454+
def test_antigravity_version_probe_records_version_and_platform_receipt(probe, schema, fixtures) -> None:
455+
fixture = next(item for item in fixtures if item["harness"]["id"] == "antigravity")
456+
assert fixture["binary"] == {"command": "agy", "version_args": ["--version"]}
457+
with (
458+
mock.patch.object(
459+
probe.shutil,
460+
"which",
461+
side_effect=lambda name: "/fake/bin/agy" if name == "agy" else None,
462+
),
463+
mock.patch.object(probe, "_popen_probe_process", return_value=_fake_version_process()),
464+
mock.patch.object(
465+
probe,
466+
"_collect_bounded_output",
467+
return_value=(b"agy 1.4.0\n", False, None),
468+
),
469+
):
470+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
471+
version_probe = result["version_probe"]
472+
assert version_probe["state"] == "observed"
473+
assert version_probe["command"] == "agy"
474+
assert version_probe["version"] == "agy 1.4.0"
475+
assert version_probe["platform"] == sys.platform
476+
assert "agy 1.4.0" in version_probe["output"]
477+
478+
479+
def test_antigravity_version_probe_falls_back_to_declared_availability_candidates(probe, schema, fixtures) -> None:
480+
# Greptile P1 on PR 450: an install with only the `antigravity` launcher
481+
# must not report availability "available" alongside a blocked version
482+
# probe; the probe resolves whichever declared candidate is present.
483+
fixture = next(item for item in fixtures if item["harness"]["id"] == "antigravity")
484+
with (
485+
mock.patch.object(
486+
probe.shutil,
487+
"which",
488+
side_effect=lambda name: "/fake/bin/antigravity" if name == "antigravity" else None,
489+
),
490+
mock.patch.object(probe, "_popen_probe_process", return_value=_fake_version_process()),
491+
mock.patch.object(
492+
probe,
493+
"_collect_bounded_output",
494+
return_value=(b"antigravity 2.0.1\n", False, None),
495+
),
496+
):
497+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
498+
assert result["availability"]["state"] == "available"
499+
version_probe = result["version_probe"]
500+
assert version_probe["state"] == "observed"
501+
assert version_probe["command"] == "antigravity"
502+
assert version_probe["version"] == "antigravity 2.0.1"
503+
assert version_probe["platform"] == sys.platform
504+
505+
506+
def test_version_receipt_skips_banner_lines_without_version_numbers(probe, schema, fixtures) -> None:
507+
# Greptile P2 on PR 450: a warning or banner printed before the version
508+
# must not be reported as the version evidence.
509+
fixture = next(item for item in fixtures if item["harness"]["id"] == "hermes")
510+
bannered_output = b"WARNING: deprecated config key detected\nBuild channel: stable\nhermes 0.3.1\n"
511+
with (
512+
mock.patch.object(probe.shutil, "which", return_value="/fake/bin/hermes"),
513+
mock.patch.object(probe, "_popen_probe_process", return_value=_fake_version_process()),
514+
mock.patch.object(
515+
probe,
516+
"_collect_bounded_output",
517+
return_value=(bannered_output, False, None),
518+
),
519+
):
520+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
521+
version_probe = result["version_probe"]
522+
assert version_probe["state"] == "observed"
523+
assert version_probe["version"] == "hermes 0.3.1"
524+
525+
526+
def test_version_receipt_reports_none_when_no_version_line_present(probe, schema, fixtures) -> None:
527+
fixture = next(item for item in fixtures if item["harness"]["id"] == "hermes")
528+
with (
529+
mock.patch.object(probe.shutil, "which", return_value="/fake/bin/hermes"),
530+
mock.patch.object(probe, "_popen_probe_process", return_value=_fake_version_process()),
531+
mock.patch.object(
532+
probe,
533+
"_collect_bounded_output",
534+
return_value=(b"build channel: stable\n", False, None),
535+
),
536+
):
537+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
538+
version_probe = result["version_probe"]
539+
assert version_probe["state"] == "observed"
540+
assert version_probe["version"] is None
541+
542+
543+
def test_antigravity_missing_binary_stays_externally_blocked_binary_not_found(probe, schema, fixtures) -> None:
544+
fixture = next(item for item in fixtures if item["harness"]["id"] == "antigravity")
545+
with mock.patch.object(probe.shutil, "which", return_value=None):
546+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
547+
assert result["availability"]["state"] == "externally_blocked"
548+
assert result["availability"]["reason"] == "binary_not_found"
549+
assert result["version_probe"]["state"] == "externally_blocked"
550+
assert result["version_probe"]["reason"] == "binary_not_found"
551+
552+
553+
def test_antigravity_home_directory_only_never_counts_as_runtime_conformance(
554+
probe, schema, fixtures, monkeypatch, tmp_path: Path
555+
) -> None:
556+
fixture = next(item for item in fixtures if item["harness"]["id"] == "antigravity")
557+
home = tmp_path / "home"
558+
(home / ".antigravity").mkdir(parents=True)
559+
(home / ".config" / "antigravity").mkdir(parents=True)
560+
monkeypatch.setenv("HOME", str(home))
561+
with mock.patch.object(probe.shutil, "which", return_value=None):
562+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
563+
for section in (result["availability"], result["version_probe"]):
564+
assert section["state"] == "externally_blocked"
565+
assert section["reason"] == "binary_not_found"
566+
567+
568+
def test_antigravity_version_args_gate_still_blocks_unsafe_args(probe, fixtures) -> None:
569+
fixture = json.loads(json.dumps(next(item for item in fixtures if item["harness"]["id"] == "antigravity")))
570+
fixture["binary"]["version_args"] = ["--full"]
571+
with mock.patch.object(probe, "_popen_probe_process") as popen:
572+
result = probe.run_version_probe(fixture, timeout_seconds=1.0)
573+
assert result["state"] == "externally_blocked"
574+
assert result["reason"] == "unsafe_version_arguments"
575+
popen.assert_not_called()
576+
577+
578+
def test_gui_surface_without_binary_declaration_remains_version_limited(probe, schema) -> None:
579+
fixture = {
580+
"schema": "harness-contract.v1",
581+
"harness": {"id": "gui-without-binary", "surface": "gui"},
582+
"availability": {"command_candidates": ["example-gui"]},
583+
"capabilities": _minimal_capabilities(),
584+
"deep_probes": _minimal_deep_probes(),
585+
}
586+
with mock.patch.object(probe, "_popen_probe_process") as popen:
587+
result = probe.probe_fixture(fixture, schema, run_version=True, timeout_seconds=1.0)
588+
assert result["version_probe"]["state"] == "externally_blocked"
589+
assert result["version_probe"]["reason"] == "version_execution_limited_to_cli_surface"
590+
popen.assert_not_called()
591+
592+
402593
def test_deep_probes_are_returned_not_executed(probe, schema, fixtures) -> None:
403594
result = probe.probe_fixture(fixtures[0], schema, run_version=False, timeout_seconds=1.0)
404595
assert result["deep_probes"] == fixtures[0]["deep_probes"]

tools/harness_conformance_probe.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
AUTHORIZATION_HEADER = re.compile(r"(?i)(Authorization\s*:\s*).+")
4949
BEARER_TOKEN = re.compile(r"(?i)\bBearer\s+([A-Za-z0-9._~+/=-]+)\b")
5050
JSON_QUOTED_CREDENTIAL = re.compile(r'(?i)("(?:api[_-]?key|token|secret|password)"\s*:\s*")[^"]+(")')
51+
VERSION_LINE = re.compile(r"\d+\.\d+(?:\.\d+)?(?:[-+~][0-9A-Za-z][0-9A-Za-z.-]*)?")
5152
OUTPUT_CAP_BYTES = 65536
5253
DEFAULT_TIMEOUT_SECONDS = 10.0
5354

@@ -83,6 +84,21 @@ def redact_text(value: str, *home_dirs: str | None) -> str:
8384
return redacted
8485

8586

87+
def _extract_version_line(output: str) -> str | None:
88+
"""Return the first output line carrying a dotted version number.
89+
90+
Vendors sometimes print warnings or banners before the version on
91+
``--version``; a bare first-nonblank-line parse would report that banner as
92+
the version. Requiring a dotted numeric component skips banner lines while
93+
still accepting common formats (``0.3.1``, ``1.4.0-rc.1``, ``2.0+build5``).
94+
"""
95+
for line in output.splitlines():
96+
stripped = line.strip()
97+
if stripped and VERSION_LINE.search(stripped):
98+
return stripped
99+
return None
100+
101+
86102
def load_schema(schema_path: Path) -> dict[str, Any]:
87103
with schema_path.open(encoding="utf-8") as handle:
88104
return json.load(handle)
@@ -369,7 +385,15 @@ def _collect_bounded_output(
369385

370386

371387
def run_version_probe(fixture: dict[str, Any], *, timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS) -> dict[str, Any]:
372-
"""Execute only ``<bare-command> --version`` inside an isolated sandbox."""
388+
"""Execute only ``<bare-command> --version`` inside an isolated sandbox.
389+
390+
CLI surfaces execute their declared ``binary.command``. Desktop/GUI surfaces
391+
may declare a CLI-companion ``binary`` block (for example Antigravity's
392+
``agy``); when present, the same sandboxed version probe runs so runtime
393+
conformance comes from direct vendor-binary evidence rather than an
394+
installation proxy. Non-CLI fixtures without a ``binary`` block stay
395+
limited to availability-only evidence.
396+
"""
373397
timeout_seconds = _positive_finite_timeout(timeout_seconds)
374398
harness = fixture.get("harness", {})
375399
harness_id = harness.get("id", "unknown")
@@ -382,16 +406,18 @@ def run_version_probe(fixture: dict[str, Any], *, timeout_seconds: float = DEFAU
382406
"reason": "desktop_or_gui_surface",
383407
}
384408

385-
if surface != "cli":
409+
binary = fixture.get("binary", {})
410+
if not isinstance(binary, dict):
411+
binary = {}
412+
command = binary.get("command")
413+
version_args = binary.get("version_args")
414+
if surface != "cli" and not isinstance(command, str):
386415
return {
387416
"harness_id": harness_id,
388417
"state": "externally_blocked",
389418
"reason": "version_execution_limited_to_cli_surface",
390419
}
391420

392-
binary = fixture.get("binary", {})
393-
command = binary.get("command")
394-
version_args = binary.get("version_args")
395421
if not isinstance(command, str) or not _is_safe_command_name(command):
396422
return {
397423
"harness_id": harness_id,
@@ -408,6 +434,20 @@ def run_version_probe(fixture: dict[str, Any], *, timeout_seconds: float = DEFAU
408434
}
409435

410436
executable = shutil.which(command)
437+
probed_command = command
438+
if executable is None and surface != "cli":
439+
# A desktop/GUI install may ship its CLI companion under one of the
440+
# declared availability candidates instead of the primary binary
441+
# command. Probe whichever candidate is present so the availability
442+
# signal and the version receipt never disagree.
443+
for candidate in _command_candidates(fixture):
444+
if candidate == command or not _is_safe_command_name(candidate):
445+
continue
446+
resolved = shutil.which(candidate)
447+
if resolved is not None:
448+
executable = resolved
449+
probed_command = candidate
450+
break
411451
if executable is None:
412452
return {
413453
"harness_id": harness_id,
@@ -458,30 +498,33 @@ def run_version_probe(fixture: dict[str, Any], *, timeout_seconds: float = DEFAU
458498
"harness_id": harness_id,
459499
"state": "externally_blocked",
460500
"reason": "TimeoutExpired",
461-
"command": command,
501+
"command": probed_command,
462502
}
463503
if overflow:
464504
return {
465505
"harness_id": harness_id,
466506
"state": "externally_blocked",
467507
"reason": "output_overflow",
468-
"command": command,
508+
"command": probed_command,
469509
}
470510

471511
output = redact_text(output_bytes.decode("utf-8", errors="replace"), str(home_dir))
472512
if return_code == 0:
473513
return {
474514
"harness_id": harness_id,
475515
"state": "observed",
476-
"command": command,
516+
"command": probed_command,
477517
"exit_code": return_code,
518+
"platform": sys.platform,
519+
"version": _extract_version_line(output),
478520
"output": output,
479521
}
480522
return {
481523
"harness_id": harness_id,
482524
"state": "nonzero_exit",
483-
"command": command,
525+
"command": probed_command,
484526
"exit_code": return_code,
527+
"platform": sys.platform,
485528
"output": output,
486529
}
487530

0 commit comments

Comments
 (0)