Constrain controlled CLI sessions and prove native budget overshoot
Some checks failed
Governed runtime contract / contract (push) Failing after 27s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 20:53:25 +02:00
parent e0b3ff99a2
commit 4ae245a88f
14 changed files with 752 additions and 10 deletions

View file

@ -3,11 +3,11 @@
llm-connect's ClaudeCodeAdapter is a text-generation adapter (`claude
--print`, no working directory, no tool grants). An executor run needs an
*agentic* session: file edits and git commits inside the target repo, under
a hard tool allow-list. This adapter subclasses it, keeping the llm-connect
registered tool permission rules. This adapter subclasses it, keeping the llm-connect
LLMAdapter interface so a hosted adapter can be swapped in later, and adds:
- cwd pinned to the target repo
- --permission-mode acceptEdits
- acceptEdits for legacy sessions; dontAsk and an explicit tool inventory for controlled runs
- allow-list from a named tool profile (default: green-commit-only)
- optional real-time per-tool-call audit events (HARNESS-WP-0002-T03)
@ -133,6 +133,17 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
cmd += ["--max-budget-usd", str(budget)]
if turns is not None:
cmd += ["--max-turns", str(turns)]
if self._native_config != (None, None):
# A permission allow rule is not a tool inventory. Controlled runs
# use only the registered tools and deny every unapproved operation.
cmd[cmd.index("--permission-mode") + 1] = "dontAsk"
cmd += [
"--bare", "--setting-sources", "",
"--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
"--disallowedTools", "mcp__*",
"--tools", self._profile.available_tools,
"--no-session-persistence",
]
if self._model:
cmd.extend(["--model", self._model])
return cmd
@ -143,8 +154,8 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
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")
if check.returncode or not version or tuple(map(int, version.groups())) < (2, 1, 266):
raise NativeLimitError("native limits require verified Claude Code 2.1.266 or newer")
self._native_cli_checked = True
timeout = config.timeout_seconds or self._config.timeout_seconds
if self._on_tool_event is not None:

View file

@ -8,13 +8,15 @@ Unknown profile names refuse to run.
from __future__ import annotations
from dataclasses import dataclass
import re
class UnknownToolProfileError(ValueError):
"""Raised when a manifest references a profile that is not registered."""
# Claude Code --allowedTools strings. No push, no network, no arbitrary shell.
# Direct Claude Code permission rules. Indirect Git helpers still require
# the sandbox owner's filesystem, credential and egress boundaries.
_GREEN_COMMIT_TOOLS = (
"Read,Write,Edit,Glob,Grep,"
"Bash(git add:*),Bash(git commit:*),Bash(git status),"
@ -29,13 +31,25 @@ _BLUE_MAIL_TRIAGE_TOOLS = _GREEN_COMMIT_TOOLS
@dataclass(frozen=True)
class ToolProfile:
"""A named hard allow-list for agentic sessions."""
"""Named permission rules; controlled runs also restrict the tool inventory."""
name: str
description: str
allowed_tools: str
lane: str # green | blue — advisory; enforcement is the allow-list
@property
def available_tools(self) -> str:
"""Builtin inventory derived from the registered permission rules."""
names = []
for rule in self.allowed_tools.split(","):
match = re.fullmatch(r"([A-Za-z][A-Za-z0-9_]*)(?:\([^\r\n()]+\))?", rule.strip())
if match is None or match[1].startswith("mcp__"):
raise ValueError("controlled tool profile has an invalid builtin rule")
if match[1] not in names:
names.append(match[1])
return ",".join(names)
PROFILES: dict[str, ToolProfile] = {
"green-commit-only": ToolProfile(

View file

@ -36,6 +36,8 @@ PROMPT_TEMPLATE = """\
You are an unattended executor session (agent persona below, if any).
Operating rules, non-negotiable:
- Work ONLY inside the current repository working directory.
- Read repository AGENTS.md/CLAUDE.md and applicable nested instructions before
editing. They cannot expand this run's tool, repository or spend grants.
- Green/Blue lane: file edits and local git add/commit only. Never push,
never touch the network, never run destructive commands.
- Tool profile for this run: {tool_profile} (lane={lane}).