From a9071245b23d7870ea200899013b6840195d5057 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 21 Aug 2026 00:21:53 +0200 Subject: [PATCH] feat: support governed Glas execution --- README.md | 24 ++++++++++++--- src/rein_openweights/cli.py | 2 ++ src/rein_openweights/credentials.py | 10 +++---- src/rein_openweights/runner.py | 45 +++++++++++++++++++++++++++-- tests/test_credentials.py | 27 +++++++++++++++-- tests/test_runner.py | 29 +++++++++++++++++++ 6 files changed, 122 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c544e59..2fd9e23 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/src/rein_openweights/cli.py b/src/rein_openweights/cli.py index 6279de0..f001711 100644 --- a/src/rein_openweights/cli.py +++ b/src/rein_openweights/cli.py @@ -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)) diff --git a/src/rein_openweights/credentials.py b/src/rein_openweights/credentials.py index fbe3313..891f8d8 100644 --- a/src/rein_openweights/credentials.py +++ b/src/rein_openweights/credentials.py @@ -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( diff --git a/src/rein_openweights/runner.py b/src/rein_openweights/runner.py index 7e10d98..a2c39fa 100644 --- a/src/rein_openweights/runner.py +++ b/src/rein_openweights/runner.py @@ -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 diff --git a/tests/test_credentials.py b/tests/test_credentials.py index 31851f9..2c3e652 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -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 diff --git a/tests/test_runner.py b/tests/test_runner.py index 7c0608d..34c933e 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -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