feat: support governed Glas execution
This commit is contained in:
parent
34e179ea5d
commit
a9071245b2
6 changed files with 122 additions and 15 deletions
24
README.md
24
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).
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue