feat: support governed Glas execution

This commit is contained in:
tegwick 2026-08-21 00:21:53 +02:00
parent 34e179ea5d
commit a9071245b2
6 changed files with 122 additions and 15 deletions

View file

@ -26,12 +26,28 @@ python3 -m venv .venv && source .venv/bin/activate
pip install -e .
# Credential: either export OPENROUTER_API_KEY directly, or set
# REIN_OPENWEIGHTS_APPROLE_DIR / REIN_OPENWEIGHTS_OPENROUTER_KV_PATH for
# OpenBao-based acquisition (see src/rein_openweights/credentials.py).
# OpenBao-based acquisition defaults to role_id/secret_id under
# ~/.local/rein-openweights/approle and KV path reins/rein-openweights/openrouter.
# REIN_OPENWEIGHTS_APPROLE_DIR / REIN_OPENWEIGHTS_OPENROUTER_KV_PATH override them.
export OPENROUTER_API_KEY=sk-...
rein-openweights run --task-file examples/task-hello-sandbox.json --no-hub
rein-openweights run \
--task-file examples/task-hello-sandbox.json \
--model qwen/qwen-2.5-72b-instruct \
--tool-profile green-commit-only \
--budget-tokens 60000 \
--max-turns 20 \
--no-hub
```
Tests: `python3 -m pytest tests/ -q` (26 tests, no network/OpenBao calls —
`green-commit-only` is currently the sole supported tool profile. Unknown tool
profiles fail before credential lookup. In governed use these flags are supplied
by a versioned `glas-harness` profile; workforce/activity consumers should
reference Glas rather than this CLI directly.
An explicitly exported `OPENROUTER_API_KEY` has highest precedence for local
development. Unset or refresh it when validating the OpenBao/AppRole lane; an
old inherited value will otherwise mask a newly rotated workload key.
Tests: `python3 -m pytest tests/ -q` (27 tests, no network/OpenBao calls —
everything mocked at the `httpx`/`bao` subprocess boundary).

View file

@ -17,6 +17,7 @@ def main(argv: list[str] | None = None) -> int:
run.add_argument("--model", default=None)
run.add_argument("--max-turns", type=int, default=20)
run.add_argument("--budget-tokens", type=int, default=60_000)
run.add_argument("--tool-profile", default="green-commit-only")
run.add_argument("--no-hub", action="store_true", help="Skip hub reporting")
args = parser.parse_args(argv)
@ -29,6 +30,7 @@ def main(argv: list[str] | None = None) -> int:
model=args.model or DEFAULT_MODEL,
max_turns=args.max_turns,
budget_tokens=args.budget_tokens,
tool_profile=args.tool_profile,
report_to_hub=not args.no_hub,
)
print(json.dumps(asdict(result), indent=2))

View file

@ -16,6 +16,7 @@ from pathlib import Path
_KV_PATH_ENV = "REIN_OPENWEIGHTS_OPENROUTER_KV_PATH"
_DEFAULT_KV_PATH = "reins/rein-openweights/openrouter"
_DEFAULT_APPROLE_DIR = Path("~/.local/rein-openweights/approle").expanduser()
_KV_FIELD = "api_key"
_BAO_TIMEOUT = 30
@ -38,11 +39,10 @@ def _bao(*args: str, env: dict[str, str] | None = None) -> str:
def _acquire_token() -> str | None:
approle_dir = os.environ.get("REIN_OPENWEIGHTS_APPROLE_DIR")
if not approle_dir:
return None
role_id_file = Path(approle_dir) / "role_id"
secret_id_file = Path(approle_dir) / "secret_id"
configured = os.environ.get("REIN_OPENWEIGHTS_APPROLE_DIR")
approle_dir = Path(configured).expanduser() if configured else _DEFAULT_APPROLE_DIR
role_id_file = approle_dir / "role_id"
secret_id_file = approle_dir / "secret_id"
if not (role_id_file.is_file() and secret_id_file.is_file()):
return None
return _bao(

View file

@ -8,6 +8,7 @@ completion unless --no-hub is set.
from __future__ import annotations
import subprocess
import time
from dataclasses import asdict, dataclass
from pathlib import Path
@ -19,6 +20,7 @@ from rein_openweights.openrouter_client import OpenRouterClient
from rein_openweights.taskspec import TaskSpec
DEFAULT_MODEL = "meta-llama/llama-3.1-70b-instruct"
SUPPORTED_TOOL_PROFILES = {"green-commit-only"}
@dataclass
@ -29,6 +31,9 @@ class RunResult:
turns: int
tokens_spent: int
reason: str = ""
model: str | None = None
execution_time_s: float = 0.0
tool_profile: str = "green-commit-only"
def _git_head(repo: Path) -> str:
@ -44,18 +49,34 @@ def run_task(
model: str = DEFAULT_MODEL,
max_turns: int = 20,
budget_tokens: int = 60_000,
tool_profile: str = "green-commit-only",
api_key: str | None = None,
report_to_hub: bool = True,
) -> RunResult:
started = time.monotonic()
task = TaskSpec.load(task_file)
repo = Path(task.target_repo).expanduser().resolve()
head_before = _git_head(repo)
if tool_profile not in SUPPORTED_TOOL_PROFILES:
result = RunResult(
ok=False, committed=False, commit_sha="", turns=0, tokens_spent=0,
reason=f"unsupported tool profile: {tool_profile}",
model=model,
execution_time_s=time.monotonic() - started,
tool_profile=tool_profile,
)
_report(result, task, report_to_hub)
return result
key = api_key or resolve_openrouter_api_key()
if not key:
result = RunResult(
ok=False, committed=False, commit_sha="", turns=0, tokens_spent=0,
reason="no OpenRouter credential resolved",
model=model,
execution_time_s=time.monotonic() - started,
tool_profile=tool_profile,
)
_report(result, task, report_to_hub)
return result
@ -63,9 +84,24 @@ def run_task(
client = OpenRouterClient(api_key=key, model=model)
budget = BudgetTracker(total=budget_tokens)
loop_result = run_loop(
client, repo, task.title, task.description, max_turns=max_turns, budget=budget
)
try:
loop_result = run_loop(
client, repo, task.title, task.description, max_turns=max_turns, budget=budget
)
except Exception as exc: # provider/client failures become rein evidence
result = RunResult(
ok=False,
committed=False,
commit_sha=_git_head(repo),
turns=0,
tokens_spent=budget.spent,
reason=f"session failed: {exc}",
model=model,
execution_time_s=time.monotonic() - started,
tool_profile=tool_profile,
)
_report(result, task, report_to_hub)
return result
head_after = _git_head(repo)
committed = bool(head_after) and head_after != head_before
@ -77,6 +113,9 @@ def run_task(
turns=loop_result.turns,
tokens_spent=budget.spent,
reason=loop_result.error or ("" if committed else "no commit produced"),
model=model,
execution_time_s=time.monotonic() - started,
tool_profile=tool_profile,
)
_report(result, task, report_to_hub)
return result

View file

@ -8,9 +8,9 @@ def test_explicit_env_var_short_circuits(monkeypatch):
assert resolve_openrouter_api_key() == "sk-explicit"
def test_falls_back_to_bao_kv_when_no_approle(monkeypatch):
def test_falls_back_to_bao_kv_when_no_approle(monkeypatch, tmp_path):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("REIN_OPENWEIGHTS_APPROLE_DIR", raising=False)
monkeypatch.setenv("REIN_OPENWEIGHTS_APPROLE_DIR", str(tmp_path / "missing"))
with patch("rein_openweights.credentials._bao", return_value="sk-from-vault") as bao:
key = resolve_openrouter_api_key()
@ -44,9 +44,30 @@ def test_approle_lane_exchanges_token_before_kv_read(monkeypatch, tmp_path):
assert calls[1][0] == "kv"
def test_returns_none_when_bao_fails(monkeypatch):
def test_returns_none_when_bao_fails(monkeypatch, tmp_path):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.setenv("REIN_OPENWEIGHTS_APPROLE_DIR", str(tmp_path / "missing"))
def test_uses_standard_approle_directory_by_default(monkeypatch, tmp_path):
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("REIN_OPENWEIGHTS_APPROLE_DIR", raising=False)
role_id = tmp_path / "role_id"
secret_id = tmp_path / "secret_id"
role_id.write_text("role-123")
secret_id.write_text("secret-456")
monkeypatch.setattr("rein_openweights.credentials._DEFAULT_APPROLE_DIR", tmp_path)
calls = []
def fake_bao(*args, env=None):
calls.append(args)
return "vault-token" if args[0] == "write" else "sk-from-vault"
with patch("rein_openweights.credentials._bao", side_effect=fake_bao):
assert resolve_openrouter_api_key() == "sk-from-vault"
assert [call[0] for call in calls] == ["write", "kv"]
from rein_openweights.credentials import CredentialError

View file

@ -73,3 +73,32 @@ def test_run_task_fails_fast_without_credential(tmp_path):
assert result.ok is False
assert "credential" in result.reason
def test_run_task_refuses_unknown_tool_profile_before_credential_lookup(tmp_path):
repo = _init_repo(tmp_path)
task_file = _task_file(tmp_path, repo)
with patch("rein_openweights.runner.resolve_openrouter_api_key") as resolve:
result = run_task(task_file, tool_profile="unsafe-shell", report_to_hub=False)
assert result.ok is False
assert result.reason == "unsupported tool profile: unsafe-shell"
assert result.tool_profile == "unsafe-shell"
resolve.assert_not_called()
def test_run_task_normalizes_provider_failure(tmp_path):
repo = _init_repo(tmp_path)
task_file = _task_file(tmp_path, repo)
with (
patch("rein_openweights.runner.resolve_openrouter_api_key", return_value="sk-test"),
patch("rein_openweights.runner.run_loop", side_effect=RuntimeError("provider down")),
):
result = run_task(task_file, report_to_hub=False)
assert result.ok is False
assert result.committed is False
assert result.reason == "session failed: provider down"
assert result.model