Implement the harness contract and prove it against rein-aharness (GLAS-WP-0001-T02/T03/T04)
- src/glas_harness/contract.py: Rein ABC (start_session/dispatch_tool/
end_session), SandboxHandle/ToolCall/ToolResult, per docs/harness-contract.md.
- reins/rein_aharness.py: adapter around the rein-aharness CLI. Collapses
a whole `agent-harness run` into one dispatch_tool call for now (no
per-tool hooks yet — tracked in rein-aharness HARNESS-WP-0002-T03);
verifies success via new-commit detection, mirroring rein-aharness's
own success signal.
- gateway.py + cli.py: resolves a sand-boxer sandbox, runs one task
through a rein, tears the sandbox down.
- registry/reins/*.yaml (rein-aharness implemented, rein-openweights
planned) and profiles/harness.agent-dev{,-local}.yaml, pairing with
sand-boxer's profile.agent-dev and the new profile.bwrap-local.
Tested against a real local git repo + mocked SandboxManager/CLI
subprocess (10 tests, all passing). A real live run against the actual
Claude Code CLI is deliberately left for a human-triggered follow-up —
not executed autonomously since it spends real API credits/credentials.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
97a50fc780
commit
5681fce787
16 changed files with 556 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -180,3 +180,4 @@ cython_debug/
|
|||
.claude/*
|
||||
!.claude/rules/
|
||||
!.claude/rules/*.md
|
||||
.venv/
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
| task | GLAS-0001-T01 | todo | — | workplans/GLAS-0001-statehub-bootstrap.md |
|
||||
| task | GLAS-0001-T02 | todo | — | workplans/GLAS-0001-statehub-bootstrap.md |
|
||||
| task | GLAS-0001-T03 | todo | — | workplans/GLAS-0001-statehub-bootstrap.md |
|
||||
| task | GLAS-WP-0001-T01 | todo | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
|
||||
| task | GLAS-WP-0001-T01 | done | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
|
||||
| task | GLAS-WP-0001-T02 | todo | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
|
||||
| task | GLAS-WP-0001-T03 | todo | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
|
||||
| task | GLAS-WP-0001-T04 | todo | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
|
||||
|
|
|
|||
10
profiles/harness.agent-dev-local.yaml
Normal file
10
profiles/harness.agent-dev-local.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
id: harness.agent-dev-local
|
||||
version: "1.0.0"
|
||||
rein: rein-aharness
|
||||
sandbox_profile: profile.bwrap-local
|
||||
tool_profile: green-commit-only
|
||||
metadata:
|
||||
latency_class: fast
|
||||
# Pairs with sand-boxer's profile.bwrap-local (ext.bwrap, same-host,
|
||||
# no SSH hop) — for CI/local dev where the compose-ssh remote-host
|
||||
# round trip is unnecessary overhead.
|
||||
10
profiles/harness.agent-dev.yaml
Normal file
10
profiles/harness.agent-dev.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
id: harness.agent-dev
|
||||
version: "1.0.0"
|
||||
rein: rein-aharness
|
||||
sandbox_profile: profile.agent-dev
|
||||
tool_profile: green-commit-only
|
||||
metadata:
|
||||
latency_class: standard
|
||||
# Pairs with sand-boxer's profile.agent-dev (ext.compose-ssh, remote
|
||||
# host over SSH). Use harness.agent-dev-local for fast local iteration
|
||||
# without the SSH hop.
|
||||
30
pyproject.toml
Normal file
30
pyproject.toml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "glas-harness"
|
||||
version = "0.1.0"
|
||||
description = "Meta-framework routing between concrete agent-harness backends (reins)"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pydantic>=2.0",
|
||||
"pyyaml>=6.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
glas-harness = "glas_harness.cli:main"
|
||||
|
||||
[tool.uv.sources]
|
||||
sandboxer = { path = "../sand-boxer", editable = true }
|
||||
|
||||
[project.optional-dependencies]
|
||||
sandbox = ["sandboxer"]
|
||||
dev = ["pytest>=8"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/glas_harness"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
16
registry/reins/rein-aharness.yaml
Normal file
16
registry/reins/rein-aharness.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
id: rein-aharness
|
||||
title: Claude Code CLI rein
|
||||
description: >
|
||||
Governed, unattended/scheduled harness driving Claude Code CLI sessions.
|
||||
Renamed from agent-harness (ADR-001). Deployed on Railiance for tenant
|
||||
binky-control.
|
||||
handler: glas_harness.reins.rein_aharness:ReinAharness
|
||||
capabilities:
|
||||
session_style: unattended
|
||||
model_class: frontier
|
||||
credential_source: rein-local
|
||||
# rein-local: kaizen-agentic (blueprints), activity-core/issue-core
|
||||
# (intake), OpenBao/ops-warden (credentials) — see
|
||||
# rein-aharness/workplans/HARNESS-WP-0002-T04 for whether this stays
|
||||
# rein-local or moves into glas-harness.
|
||||
status: implemented
|
||||
13
registry/reins/rein-openweights.yaml
Normal file
13
registry/reins/rein-openweights.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
id: rein-openweights
|
||||
title: OpenRouter open-weight agentic loop
|
||||
description: >
|
||||
Agentic tool-use harness driving current open-weight models via
|
||||
OpenRouter/llm-connect, as an alternative to frontier-vendor CLIs.
|
||||
Chartered by ADR-001; not yet implementing this contract.
|
||||
handler: glas_harness.reins.rein_openweights:ReinOpenWeights
|
||||
capabilities:
|
||||
session_style: unattended
|
||||
model_class: open-weight
|
||||
credential_source: tbd
|
||||
# tbd: resolved by GLAS-WP-0001-T06 before this rein is implemented.
|
||||
status: planned
|
||||
0
src/glas_harness/__init__.py
Normal file
0
src/glas_harness/__init__.py
Normal file
42
src/glas_harness/cli.py
Normal file
42
src/glas_harness/cli.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""glas-harness CLI — minimal gateway invocation (GLAS-WP-0001-T04)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(prog="glas-harness")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
run = sub.add_parser("run", help="Run one task through a rein inside a sand-boxer sandbox")
|
||||
run.add_argument("--sandbox-profile", required=True, help="e.g. profile.bwrap-local")
|
||||
run.add_argument("--repo", required=True, help="Local repo path to mirror into the sandbox")
|
||||
run.add_argument("--title", required=True)
|
||||
run.add_argument("--description", required=True)
|
||||
run.add_argument("--actor", default="agt")
|
||||
run.add_argument("--project", default="glas-harness")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.command == "run":
|
||||
from glas_harness.gateway import run_task_through_rein
|
||||
|
||||
result = run_task_through_rein(
|
||||
sandbox_profile=args.sandbox_profile,
|
||||
repo=args.repo,
|
||||
title=args.title,
|
||||
description=args.description,
|
||||
actor=args.actor,
|
||||
project=args.project,
|
||||
)
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0 if result["tool_ok"] else 1
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
54
src/glas_harness/contract.py
Normal file
54
src/glas_harness/contract.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""The harness contract a concrete rein implements.
|
||||
|
||||
See docs/harness-contract.md for the full session-lifecycle diagram and
|
||||
GLAS-WP-0001-T01. glas-harness owns the outer loop (profile resolution,
|
||||
sandbox request/teardown via sand-boxer, State Hub reporting, actor
|
||||
attribution); a rein owns the inner agentic loop.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class SandboxHandle:
|
||||
"""What glas-harness got back from sand-boxer's create()."""
|
||||
|
||||
sandbox_id: str
|
||||
host: str
|
||||
reachability: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
name: str
|
||||
args: dict[str, Any] = field(default_factory=dict)
|
||||
actor: str = "agt"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolResult:
|
||||
ok: bool
|
||||
output: str = ""
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class Rein(ABC):
|
||||
"""Base class for concrete harness backends (reins)."""
|
||||
|
||||
@abstractmethod
|
||||
def start_session(
|
||||
self, profile: dict[str, Any], inputs: dict[str, str], sandbox: SandboxHandle
|
||||
) -> dict[str, str]:
|
||||
"""Begin an agent session bound to a sandbox. Returns a session handle."""
|
||||
|
||||
@abstractmethod
|
||||
def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult:
|
||||
"""Run one tool call under the session's policy."""
|
||||
|
||||
@abstractmethod
|
||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
||||
"""Close the session. Returns a summary (commit sha, outcome, ...)."""
|
||||
72
src/glas_harness/gateway.py
Normal file
72
src/glas_harness/gateway.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Minimal gateway proving the harness contract against rein-aharness.
|
||||
|
||||
GLAS-WP-0001-T04: resolve a sand-boxer profile, request a sandbox,
|
||||
dispatch one rein-aharness task through the Rein contract, verify a
|
||||
commit landed, tear the sandbox down. This is the parity proof gating
|
||||
any later "retire rein-aharness as a standalone concern" conversation —
|
||||
it is not itself that conversation.
|
||||
|
||||
Requires the `sandbox` extra (sand-boxer installed as a sibling
|
||||
editable dependency).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sandboxer.core.manager import SandboxManager
|
||||
from sandboxer.models import Consumer, SandboxCreateRequest
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
||||
from glas_harness.reins.rein_aharness import ReinAharness
|
||||
|
||||
|
||||
def run_task_through_rein(
|
||||
*,
|
||||
sandbox_profile: str,
|
||||
repo: str,
|
||||
title: str,
|
||||
description: str,
|
||||
rein: Rein | None = None,
|
||||
actor: str = "agt",
|
||||
project: str = "glas-harness",
|
||||
manager: SandboxManager | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve `sandbox_profile`, run one task inside it via `rein`, verify, tear down.
|
||||
|
||||
Defaults to `ReinAharness` when no rein is supplied — the only
|
||||
implemented rein as of GLAS-WP-0001-T04. `rein-openweights` plugs in
|
||||
the same way once REIN-OW-WP-0001 lands.
|
||||
"""
|
||||
manager = manager or SandboxManager()
|
||||
rein = rein or ReinAharness()
|
||||
|
||||
request = SandboxCreateRequest(
|
||||
profile=sandbox_profile,
|
||||
inputs={"repo": repo},
|
||||
consumer=Consumer(actor=actor, project=project),
|
||||
)
|
||||
status = manager.create(request)
|
||||
try:
|
||||
reachability = status.reachability.model_dump(mode="json") if status.reachability else {}
|
||||
sandbox = SandboxHandle(
|
||||
sandbox_id=status.sandbox_id, host=status.host or "", reachability=reachability
|
||||
)
|
||||
|
||||
session = rein.start_session(
|
||||
profile={"id": sandbox_profile},
|
||||
inputs={"title": title, "description": description},
|
||||
sandbox=sandbox,
|
||||
)
|
||||
tool_result = rein.dispatch_tool(session, ToolCall(name="run_task", actor=actor))
|
||||
summary = rein.end_session(session)
|
||||
|
||||
return {
|
||||
"sandbox_id": status.sandbox_id,
|
||||
"tool_ok": tool_result.ok,
|
||||
"tool_output": tool_result.output,
|
||||
"tool_error": tool_result.error,
|
||||
"summary": summary,
|
||||
}
|
||||
finally:
|
||||
manager.destroy(status.sandbox_id)
|
||||
0
src/glas_harness/reins/__init__.py
Normal file
0
src/glas_harness/reins/__init__.py
Normal file
102
src/glas_harness/reins/rein_aharness.py
Normal file
102
src/glas_harness/reins/rein_aharness.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""ReinAharness — glas-harness adapter around the rein-aharness CLI.
|
||||
|
||||
rein-aharness's `run` command performs an entire bounded agentic session
|
||||
(persona load, Claude Code CLI subprocess, commit verification) as one
|
||||
opaque unit — it does not yet expose per-tool-call hooks from outside
|
||||
(that refactor is tracked in
|
||||
rein-aharness/workplans/HARNESS-WP-0002-T03). Until then, dispatch_tool
|
||||
collapses the whole run into a single call rather than true per-tool
|
||||
granularity; this adapter proves the contract's *shape* (start/dispatch/
|
||||
end + sandbox handoff), not fine-grained tool interception yet.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
||||
|
||||
|
||||
class ReinAharnessNotInstalled(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class ReinAharness(Rein):
|
||||
def __init__(self, cli_bin: str = "agent-harness") -> None:
|
||||
self.cli_bin = cli_bin
|
||||
|
||||
def _bin(self) -> str:
|
||||
resolved = shutil.which(self.cli_bin)
|
||||
if not resolved:
|
||||
raise ReinAharnessNotInstalled(
|
||||
f"'{self.cli_bin}' not found on PATH — install rein-aharness first"
|
||||
)
|
||||
return resolved
|
||||
|
||||
def start_session(
|
||||
self, profile: dict[str, Any], inputs: dict[str, str], sandbox: SandboxHandle
|
||||
) -> dict[str, str]:
|
||||
target_repo = (
|
||||
inputs.get("target_repo")
|
||||
or sandbox.reachability.get("workspace_dir")
|
||||
or sandbox.reachability.get("remote_dir")
|
||||
)
|
||||
if not target_repo:
|
||||
raise ValueError("no target_repo resolvable from inputs or sandbox reachability")
|
||||
|
||||
task_file = inputs.get("task_file")
|
||||
if not task_file:
|
||||
task_spec = {
|
||||
"title": inputs["title"],
|
||||
"description": inputs["description"],
|
||||
"target_repo": target_repo,
|
||||
"agent": inputs.get("agent", "coach"),
|
||||
}
|
||||
fd = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", prefix="glas-harness-task-", delete=False
|
||||
)
|
||||
json.dump(task_spec, fd)
|
||||
fd.close()
|
||||
task_file = fd.name
|
||||
|
||||
return {
|
||||
"sandbox_id": sandbox.sandbox_id,
|
||||
"task_file": task_file,
|
||||
"target_repo": target_repo,
|
||||
"head_before": self._git_head(target_repo),
|
||||
}
|
||||
|
||||
def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult:
|
||||
argv = [
|
||||
self._bin(),
|
||||
"run",
|
||||
"--task-file",
|
||||
session["task_file"],
|
||||
"--no-hub",
|
||||
"--no-metrics",
|
||||
]
|
||||
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||
ok = proc.returncode == 0
|
||||
return ToolResult(ok=ok, output=proc.stdout, error=None if ok else proc.stderr)
|
||||
|
||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
||||
head_after = self._git_head(session["target_repo"])
|
||||
committed = bool(head_after) and head_after != session.get("head_before")
|
||||
return {
|
||||
"commit_sha": head_after or "",
|
||||
"committed": str(committed),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _git_head(repo: str) -> str:
|
||||
proc = subprocess.run(
|
||||
["git", "-C", str(Path(repo).expanduser()), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return proc.stdout.strip() if proc.returncode == 0 else ""
|
||||
88
tests/test_gateway.py
Normal file
88
tests/test_gateway.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from sandboxer.models import Reachability, SandboxState, SandboxStatus
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
||||
from glas_harness.gateway import run_task_through_rein
|
||||
|
||||
|
||||
class _FakeRein(Rein):
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[str] = []
|
||||
|
||||
def start_session(self, profile, inputs, sandbox: SandboxHandle):
|
||||
self.calls.append("start_session")
|
||||
assert sandbox.sandbox_id == "sbx1"
|
||||
assert sandbox.reachability.get("workspace_dir") == "/tmp/ws"
|
||||
return {"session": "s1"}
|
||||
|
||||
def dispatch_tool(self, session, tool_call: ToolCall) -> ToolResult:
|
||||
self.calls.append("dispatch_tool")
|
||||
assert tool_call.name == "run_task"
|
||||
return ToolResult(ok=True, output="done")
|
||||
|
||||
def end_session(self, session):
|
||||
self.calls.append("end_session")
|
||||
return {"commit_sha": "deadbeef", "committed": "True"}
|
||||
|
||||
|
||||
def _fake_status(sandbox_id: str = "sbx1") -> SandboxStatus:
|
||||
now = datetime.now(UTC)
|
||||
return SandboxStatus(
|
||||
sandbox_id=sandbox_id,
|
||||
profile_id="profile.bwrap-local",
|
||||
extension_id="ext.bwrap",
|
||||
state=SandboxState.READY,
|
||||
consumer={"actor": "agt", "project": "glas-harness"},
|
||||
host="localhost",
|
||||
reachability=Reachability(host="localhost", pid="123", workspace_dir="/tmp/ws"),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
|
||||
def test_run_task_through_rein_creates_and_destroys_sandbox() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
rein = _FakeRein()
|
||||
|
||||
result = run_task_through_rein(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=rein,
|
||||
manager=manager,
|
||||
)
|
||||
|
||||
assert rein.calls == ["start_session", "dispatch_tool", "end_session"]
|
||||
manager.create.assert_called_once()
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
assert result["tool_ok"] is True
|
||||
assert result["summary"]["committed"] == "True"
|
||||
|
||||
|
||||
def test_run_task_through_rein_destroys_sandbox_even_on_failure() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
|
||||
class _FailingRein(_FakeRein):
|
||||
def dispatch_tool(self, session, tool_call):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
rein = _FailingRein()
|
||||
|
||||
try:
|
||||
run_task_through_rein(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=rein,
|
||||
manager=manager,
|
||||
)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
103
tests/test_rein_aharness.py
Normal file
103
tests/test_rein_aharness.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import json
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
||||
from glas_harness.reins.rein_aharness import ReinAharness, ReinAharnessNotInstalled
|
||||
|
||||
|
||||
def test_is_rein_subclass() -> None:
|
||||
assert issubclass(ReinAharness, Rein)
|
||||
|
||||
|
||||
def test_bin_raises_if_not_installed() -> None:
|
||||
rein = ReinAharness(cli_bin="does-not-exist-binary")
|
||||
with pytest.raises(ReinAharnessNotInstalled):
|
||||
rein._bin()
|
||||
|
||||
|
||||
def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
||||
|
||||
rein = ReinAharness()
|
||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
||||
session = rein.start_session(
|
||||
profile={"id": "harness.agent-dev-local"},
|
||||
inputs={"title": "t", "description": "d"},
|
||||
sandbox=sandbox,
|
||||
)
|
||||
|
||||
assert session["target_repo"] == str(repo)
|
||||
assert session["head_before"]
|
||||
task_spec = json.loads(open(session["task_file"]).read())
|
||||
assert task_spec["title"] == "t"
|
||||
assert task_spec["target_repo"] == str(repo)
|
||||
|
||||
|
||||
def test_start_session_requires_resolvable_target_repo() -> None:
|
||||
rein = ReinAharness()
|
||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={})
|
||||
with pytest.raises(ValueError, match="target_repo"):
|
||||
rein.start_session(profile={}, inputs={"title": "t", "description": "d"}, sandbox=sandbox)
|
||||
|
||||
|
||||
def test_dispatch_tool_invokes_agent_harness_cli() -> None:
|
||||
rein = ReinAharness()
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
|
||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
||||
with patch("glas_harness.reins.rein_aharness.subprocess.run", return_value=fake_result) as run:
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
argv = run.call_args.args[0]
|
||||
assert argv[:3] == ["/usr/bin/agent-harness", "run", "--task-file"]
|
||||
assert result.ok is True
|
||||
assert result.output == "ok"
|
||||
|
||||
|
||||
def test_dispatch_tool_reports_failure() -> None:
|
||||
rein = ReinAharness()
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
|
||||
fake_result = MagicMock(returncode=1, stdout="", stderr="boom")
|
||||
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
||||
with patch("glas_harness.reins.rein_aharness.subprocess.run", return_value=fake_result):
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
assert result.ok is False
|
||||
assert result.error == "boom"
|
||||
|
||||
|
||||
def test_end_session_detects_new_commit(tmp_path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
||||
|
||||
rein = ReinAharness()
|
||||
head_before = rein._git_head(str(repo))
|
||||
|
||||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "task"], cwd=repo, check=True)
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
||||
assert summary["committed"] == "True"
|
||||
assert summary["commit_sha"] != head_before
|
||||
|
||||
|
||||
def test_end_session_no_new_commit(tmp_path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
||||
|
||||
rein = ReinAharness()
|
||||
head = rein._git_head(str(repo))
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head})
|
||||
assert summary["committed"] == "False"
|
||||
|
|
@ -42,7 +42,7 @@ glas-harness` entry per the registry-first-reuse principle.
|
|||
|
||||
```task
|
||||
id: GLAS-WP-0001-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "c09e617b-a2d1-42c4-8c90-47ab0056c09c"
|
||||
```
|
||||
|
|
@ -58,7 +58,7 @@ fast local iteration without an SSH hop.
|
|||
|
||||
```task
|
||||
id: GLAS-WP-0001-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "e599f276-a010-4284-bddd-a5a121b57de9"
|
||||
```
|
||||
|
|
@ -72,9 +72,20 @@ rein-aharness's adapter, verify commit, report to State Hub. This is the
|
|||
parity proof gating any later "retire agent-harness as a standalone
|
||||
concern" conversation — deliberately not attempted until this exists.
|
||||
|
||||
Wiring is built and unit-tested: `src/glas_harness/gateway.py` +
|
||||
`reins/rein_aharness.py` implement start_session/dispatch_tool/
|
||||
end_session, tested against a real local git repo and a mocked
|
||||
`SandboxManager`/`agent-harness` subprocess (`tests/test_gateway.py`,
|
||||
`tests/test_rein_aharness.py`). **Not yet done:** an actual live run
|
||||
(`glas-harness run --sandbox-profile profile.bwrap-local ...`) against
|
||||
the real Claude Code CLI — that spends real API credits/credentials and
|
||||
was deliberately left for a human-triggered run rather than executed
|
||||
autonomously. State Hub reporting from the gateway itself is also not
|
||||
wired yet (only rein-aharness's own `--no-hub` path was exercised).
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0001-T04
|
||||
status: todo
|
||||
status: progress
|
||||
priority: high
|
||||
state_hub_task_id: "cdcacf98-62a1-47ae-92b9-e0320968ac1f"
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue