feat: import governed sandbox commits and enforce native CLI limits
Some checks are pending
Governed runtime contract / contract (push) Waiting to run
Some checks are pending
Governed runtime contract / contract (push) Waiting to run
Assistant: codex Assistant-Model: gpt-5.6-luna Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
1429db5ad4
commit
c63caf5568
16 changed files with 1236 additions and 7 deletions
|
|
@ -24,6 +24,7 @@ one aggregate `LLMResponse` at the end for interface compatibility.
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
|
@ -32,6 +33,7 @@ from typing import Any, Callable
|
|||
from llm_connect.claude_code import ClaudeCodeAdapter
|
||||
from llm_connect.exceptions import LLMSubprocessError, LLMTimeoutError
|
||||
from llm_connect.models import LLMResponse, RunConfig
|
||||
from rein_aharness.native_limits import NativeLimitError, terminal_accounting, validate_limits
|
||||
|
||||
from rein_aharness.execution_cancel import (
|
||||
ExecutionCancel,
|
||||
|
|
@ -99,6 +101,8 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
self._workdir = workdir
|
||||
self._on_tool_event = on_tool_event
|
||||
self._cancel = cancel
|
||||
self._native_config = (None, None)
|
||||
self._native_cli_checked = False
|
||||
if isinstance(tool_profile, ToolProfile):
|
||||
self._profile = tool_profile
|
||||
else:
|
||||
|
|
@ -109,6 +113,10 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
return self._profile
|
||||
|
||||
def _build_command(self, config: RunConfig) -> list[str]:
|
||||
budget = config.model_params.get("max_budget_usd")
|
||||
turns = config.model_params.get("max_turns")
|
||||
validate_limits(budget, turns)
|
||||
self._native_config = (budget, turns)
|
||||
cmd = [
|
||||
self._cli_path,
|
||||
"--print",
|
||||
|
|
@ -119,6 +127,12 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
]
|
||||
if self._on_tool_event is not None:
|
||||
cmd += ["--output-format", "stream-json", "--include-hook-events", "--verbose"]
|
||||
elif budget is not None or turns is not None:
|
||||
cmd += ["--output-format", "json"]
|
||||
if budget is not None:
|
||||
cmd += ["--max-budget-usd", str(budget)]
|
||||
if turns is not None:
|
||||
cmd += ["--max-turns", str(turns)]
|
||||
if self._model:
|
||||
cmd.extend(["--model", self._model])
|
||||
return cmd
|
||||
|
|
@ -126,12 +140,23 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
|
||||
self._preflight_budget(config)
|
||||
cmd = self._build_command(config)
|
||||
if self._native_config != (None, None) and not self._native_cli_checked:
|
||||
check = subprocess.run([self._cli_path, "--version"], capture_output=True, text=True, timeout=10, cwd=self._workdir)
|
||||
version = re.match(r"(\d+)\.(\d+)\.(\d+)(?:\s|$)", check.stdout.strip())
|
||||
if check.returncode or not version or tuple(map(int, version.groups())) < (2, 1, 217):
|
||||
raise NativeLimitError("native limits require verified Claude Code 2.1.217 or newer")
|
||||
self._native_cli_checked = True
|
||||
timeout = config.timeout_seconds or self._config.timeout_seconds
|
||||
if self._on_tool_event is not None:
|
||||
response = self._execute_streaming(cmd, prompt, timeout)
|
||||
else:
|
||||
response = self._execute_blocking(cmd, prompt, timeout)
|
||||
self._consume_budget(config, response)
|
||||
try:
|
||||
self._consume_budget(config, response)
|
||||
except Exception as exc:
|
||||
if self._native_config != (None, None):
|
||||
raise NativeLimitError("controlled Claude run failed token accounting", cost_usd=response.metadata.get("cost_usd")) from exc
|
||||
raise
|
||||
return response
|
||||
|
||||
def _execute_blocking(self, cmd: list[str], prompt: str, timeout: int) -> LLMResponse:
|
||||
|
|
@ -144,7 +169,19 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
cwd=self._workdir,
|
||||
)
|
||||
stdout, stderr = self._wait_for_process(proc, prompt, timeout)
|
||||
usage, cost = {}, None
|
||||
if self._native_config != (None, None):
|
||||
try:
|
||||
envelope = json.loads(stdout)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise NativeLimitError("controlled Claude run returned invalid terminal JSON") from exc
|
||||
usage, cost = terminal_accounting(envelope, max_budget_usd=self._native_config[0], max_turns=self._native_config[1])
|
||||
stdout = envelope.get("result", "")
|
||||
if not isinstance(stdout, str):
|
||||
raise NativeLimitError("controlled Claude run returned invalid result text", cost_usd=cost)
|
||||
if proc.returncode != 0:
|
||||
if self._native_config != (None, None):
|
||||
raise NativeLimitError("controlled Claude CLI exited unsuccessfully", cost_usd=cost)
|
||||
raise LLMSubprocessError(
|
||||
f"claude CLI exited with code {proc.returncode}",
|
||||
return_code=proc.returncode,
|
||||
|
|
@ -153,9 +190,10 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
return LLMResponse(
|
||||
content=stdout,
|
||||
model=self._model or "claude-code-cli",
|
||||
usage={},
|
||||
usage=usage,
|
||||
finish_reason="stop",
|
||||
metadata={
|
||||
"cost_usd": cost,
|
||||
"provider": "claude-code-agentic",
|
||||
"cli_path": self._cli_path,
|
||||
"workdir": str(self._workdir),
|
||||
|
|
@ -174,6 +212,7 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
)
|
||||
text_parts: list[str] = []
|
||||
tool_event_count = 0
|
||||
terminal_results = []
|
||||
|
||||
def reader() -> None:
|
||||
nonlocal tool_event_count
|
||||
|
|
@ -186,6 +225,8 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
event = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(event, dict) and event.get("type") == "result":
|
||||
terminal_results.append(event)
|
||||
self._handle_stream_event(event, text_parts)
|
||||
if _is_tool_event(event):
|
||||
tool_event_count += 1
|
||||
|
|
@ -200,7 +241,14 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
reader_thread.join(timeout=5)
|
||||
stderr = proc.stderr.read() if proc.stderr else ""
|
||||
|
||||
usage, cost = {}, None
|
||||
if self._native_config != (None, None):
|
||||
if reader_thread.is_alive() or len(terminal_results) != 1:
|
||||
raise NativeLimitError("controlled Claude stream requires one complete terminal result")
|
||||
usage, cost = terminal_accounting(terminal_results[0], max_budget_usd=self._native_config[0], max_turns=self._native_config[1])
|
||||
if returncode != 0:
|
||||
if self._native_config != (None, None):
|
||||
raise NativeLimitError("controlled Claude CLI exited unsuccessfully", cost_usd=cost)
|
||||
raise LLMSubprocessError(
|
||||
f"claude CLI exited with code {returncode}",
|
||||
return_code=returncode,
|
||||
|
|
@ -210,7 +258,7 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
return LLMResponse(
|
||||
content="".join(text_parts),
|
||||
model=self._model or "claude-code-cli",
|
||||
usage={},
|
||||
usage=usage,
|
||||
finish_reason="stop",
|
||||
metadata={
|
||||
"provider": "claude-code-agentic",
|
||||
|
|
@ -218,6 +266,7 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|||
"workdir": str(self._workdir),
|
||||
"tool_profile": self._profile.name,
|
||||
"tool_event_count": tool_event_count,
|
||||
"cost_usd": cost,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue