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

@ -0,0 +1,84 @@
{
"case": "overrun",
"binary_version": "2.1.266 (Claude Code)",
"binary_sha256": "19842705e989393fce936804df6d2ab034860e24b8f8880357981d87ffd83fac",
"argv": [
"/opt/claude",
"--print",
"--permission-mode",
"dontAsk",
"--allowedTools",
"Read,Write,Edit,Glob,Grep,Bash(git add:*),Bash(git commit:*),Bash(git status),Bash(git log:*),Bash(git diff:*),Bash(date:*),Bash(ls:*)",
"--output-format",
"json",
"--max-budget-usd",
"0.01",
"--max-turns",
"4",
"--bare",
"--setting-sources",
"",
"--strict-mcp-config",
"--mcp-config",
"{\"mcpServers\":{}}",
"--disallowedTools",
"mcp__*",
"--tools",
"Read,Write,Edit,Glob,Grep,Bash",
"--no-session-persistence",
"--model",
"claude-sonnet-4-6"
],
"scope": "synthetic API in isolated network namespace; no real credential or provider request",
"returncode": 1,
"terminal": {
"type": "result",
"subtype": "error_max_budget_usd",
"is_error": true,
"total_cost_usd": 0.18015,
"num_turns": 1,
"usage": {
"output_tokens_details": {
"thinking_tokens": 0
},
"input_tokens": 0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 0,
"server_tool_use": {
"web_search_requests": 0,
"web_fetch_requests": 0
},
"service_tier": "standard",
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"inference_geo": "",
"iterations": [],
"speed": "standard"
},
"permission_denials": []
},
"requests": [
{
"path": "/v1/messages?beta=true",
"model": "claude-sonnet-4-6",
"max_tokens": 32000,
"tools": [
"Bash",
"Edit",
"Read"
],
"tool_results": [],
"ambient_context_loaded": false
}
],
"markers": {
"hook-ran": false,
"mcp-ran": false,
"denied-ran": false
},
"created_file": null,
"network_namespace": "net:[4026532321]"
}

View file

@ -0,0 +1,118 @@
{
"case": "tools",
"binary_version": "2.1.266 (Claude Code)",
"binary_sha256": "19842705e989393fce936804df6d2ab034860e24b8f8880357981d87ffd83fac",
"argv": [
"/opt/claude",
"--print",
"--permission-mode",
"dontAsk",
"--allowedTools",
"Read,Write,Edit,Glob,Grep,Bash(git add:*),Bash(git commit:*),Bash(git status),Bash(git log:*),Bash(git diff:*),Bash(date:*),Bash(ls:*)",
"--output-format",
"json",
"--max-budget-usd",
"1",
"--max-turns",
"4",
"--bare",
"--setting-sources",
"",
"--strict-mcp-config",
"--mcp-config",
"{\"mcpServers\":{}}",
"--disallowedTools",
"mcp__*",
"--tools",
"Read,Write,Edit,Glob,Grep,Bash",
"--no-session-persistence",
"--model",
"claude-sonnet-4-6"
],
"scope": "synthetic API in isolated network namespace; no real credential or provider request",
"returncode": 0,
"terminal": {
"type": "result",
"subtype": "success",
"is_error": false,
"total_cost_usd": 0.0009000000000000001,
"num_turns": 4,
"usage": {
"input_tokens": 200,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 20,
"output_tokens_details": {
"thinking_tokens": 0
},
"server_tool_use": {
"web_search_requests": 0,
"web_fetch_requests": 0
},
"service_tier": "standard",
"cache_creation": {
"ephemeral_1h_input_tokens": 0,
"ephemeral_5m_input_tokens": 0
},
"inference_geo": "",
"iterations": [],
"speed": "standard"
},
"permission_denials": [
{
"tool_name": "Bash",
"tool_use_id": "tool_denied",
"tool_input": {
"command": "printf bypass > /work/denied-ran",
"description": "Fixture forbidden operation"
}
}
]
},
"requests": [
{
"path": "/v1/messages?beta=true",
"model": "claude-sonnet-4-6",
"max_tokens": 32000,
"tools": [
"Bash",
"Edit",
"Read"
],
"tool_results": [],
"ambient_context_loaded": false
},
{
"path": "/v1/messages?beta=true",
"model": "claude-sonnet-4-6",
"max_tokens": 32000,
"tools": [
"Bash",
"Edit",
"Read"
],
"tool_results": [
{
"id": "tool_allowed",
"is_error": false
},
{
"id": "tool_create",
"is_error": false
},
{
"id": "tool_denied",
"is_error": true
}
],
"ambient_context_loaded": false
}
],
"markers": {
"hook-ran": false,
"mcp-ran": false,
"denied-ran": false
},
"created_file": "fixture-created\n",
"network_namespace": "net:[4026532321]"
}

View file

@ -0,0 +1,84 @@
# Native CLI boundary proof — 2026-09-09
The installed Claude Code 2.1.266 binary was exercised against a deterministic
Anthropic-shaped API in a separate bwrap network namespace. The namespace had
no provider network route, operator home, real credential or inference service.
Only the CLI binary, system runtime and a temporary fixture workspace were mounted.
The executable receipt records the binary SHA-256 and exact adapter-generated argv.
## Findings and resulting change
With `--max-budget-usd 0.01`, the first fixture response reported 60,000 input
and 10 output tokens. The CLI estimated **USD 0.18015**, then exited with
`error_max_budget_usd`. Only one messages request reached the fixture. This
reproduces a stop after the response, not a pre-request maximum-liability check.
The rein terminal-accounting validator refused success and preserved known cost.
No amount in this experiment was billed by a provider.
The existing spend ledger remains necessary: it reserves declared per-run
liability and retains unknown outcomes. The experiment does not establish that
the declared liability is a valid bound on every possible native request. A
provider-facing request guard is the next HFACT-WP-0001-T01 implementation return;
raising the native threshold or accepting a newer binary cannot substitute for it.
The adapter also previously used `--allowedTools` with `acceptEdits`. Those flags
alone did not constrain the available tool inventory or repository-provided
customizations. Runs with native USD or turn controls now require CLI >=2.1.266
and use `dontAsk`, explicit builtin tools derived from registered rules, `--bare`,
empty settings sources, empty strict MCP configuration, an MCP deny rule, and no
session persistence. Legacy unconfigured sessions retain their existing behavior.
The worker prompt explicitly requires reading applicable AGENTS.md/CLAUDE.md;
these files are instructions, not permission to load executable configuration or
expand grants. The factory's protected runtime must be rebuilt and re-proven for
the new minimum; this source change installs or admits no runtime.
The real binary's bare mode exposed Bash, Read and Edit, a subset of the explicit
inventory. A synthetic tool sequence successfully ran permitted `git status` and
created a new file through Edit. An unapproved shell write was denied. A fixture
project containing a broad Bash permission override, a SessionStart hook and an
MCP command could activate none of them; its ambient CLAUDE.md was not auto-loaded.
The second messages request contained the expected allowed/denied tool results.
These tests prove the specified direct-tool and configuration boundaries. They
are not an arbitrary-code sandbox proof: permitted Git operations can invoke
repository-controlled helpers, and file edits can influence later commands.
Filesystem, credential and network confinement remain sandbox-owner controls.
To enforce spend against such indirect execution, the workload must not possess
a provider credential plus an unmetered route to the provider. A run-scoped,
owner-controlled request path must cover every forwarded request and retry.
## Reproduction and acceptance
`REIN_REAL_CLAUDE=1` enables `tests/test_native_cli_boundary.py`. The default binary
comes from `claude` on PATH; `REIN_CLAUDE_PROOF_BINARY` can select an exact installed
file. The fixture deliberately requires version 2.1.266; revalidate and record a
new receipt before claiming the same proof for another version. The adapter's
minimum-version gate does not itself prove arbitrary future binaries.
```text
REIN_REAL_CLAUDE=1 REIN_REAL_BWRAP=1 python -m pytest tests/ -q
```
The reproducible fixture is `tests/native_cli_fixture.py`; optional
`REIN_CLAUDE_PROOF_OUTPUT` writes bounded evidence without prompts, headers,
credentials or response bodies. The actual CLI checks are opt-in because ordinary
CI does not provision that binary. Deterministic native launch/accounting tests
are now included in both mandatory runtime and recovery gates. Paired receipts:
`docs/evidence/2026-09-09-native-cli-overrun.json` and
`docs/evidence/2026-09-09-native-cli-tools.json`.
No paid factory request, profile promotion, credential reader or production
rollout was authorized or performed. REINAH-WP-0003-T05/T06 and HFACT-WP-0001-T01/
T03/T04/T05 retain the request-admission, exact identity, protected placement and
natural queue/model execution returns.
## Upstream contract reviewed
The [CLI reference](https://code.claude.com/docs/en/cli-reference) distinguishes
available tools from auto-permission rules and describes the native stop flag.
The [headless guide](https://code.claude.com/docs/en/headless) documents bare-mode
configuration isolation and the API-key authentication path. The
[cost guide](https://code.claude.com/docs/en/costs) identifies CLI totals as local
estimates, not authoritative billing, and notes the residency-cost correction in
2.1.239. Reviewed 2026-09-09; the local executable findings above are narrower
than a claim about all provider billing or future CLI releases.

View file

@ -26,7 +26,7 @@ still requires the existing recovery process to classify the resulting commit.
Glas profile limits `max_budget_usd` and `max_turns` are carried in its generated Glas profile limits `max_budget_usd` and `max_turns` are carried in its generated
TaskSpec into the agentic Claude adapter. A supplied task file cannot bypass TaskSpec into the agentic Claude adapter. A supplied task file cannot bypass
these profile controls. Positive finite USD amounts and positive integer turn these profile controls. Positive finite USD amounts and positive integer turn
limits are required. A controlled run verifies Claude Code >= 2.1.217, supplies limits are required. A controlled run verifies Claude Code >= 2.1.266, supplies
`--max-budget-usd` / `--max-turns`, and requires one successful terminal JSON `--max-budget-usd` / `--max-turns`, and requires one successful terminal JSON
result with valid cost, turn and token accounting. Limit exhaustion, missing result with valid cost, turn and token accounting. Limit exhaustion, missing
accounting and reported overruns cannot produce a successful run. Bounded USD accounting and reported overruns cannot produce a successful run. Bounded USD
@ -56,3 +56,9 @@ invalid/exhausted results and refusal of older CLI versions without inference.
The subsequent [durable spend admission](spend-admission.md) return implements The subsequent [durable spend admission](spend-admission.md) return implements
private daily/total reservation and unknown-outcome recovery in the worker. private daily/total reservation and unknown-outcome recovery in the worker.
Provider liability/FX proof and final operating admission remain open. Provider liability/FX proof and final operating admission remain open.
The [installed CLI boundary proof](native-cli-boundary-proof.md) reproduces native
budget overshoot and verifies controlled configuration/tool confinement. Native
USD/turn-controlled runs now use bare mode and dontAsk with an explicit inventory;
applicable repository instructions must be read as task context. The first paid
factory run still requires a provider-facing request admission guard.

View file

@ -131,3 +131,9 @@ or verify the operator's FX assumption. G0 remains blocked until those semantics
are proven for the pinned provider/CLI, including retries, cache, subagents and are proven for the pinned provider/CLI, including retries, cache, subagents and
in-flight work. Exact credential/identity/egress/placement and protected-runtime in-flight work. Exact credential/identity/egress/placement and protected-runtime
installation are also still required before natural factory execution. installation are also still required before natural factory execution.
The [native CLI proof](native-cli-boundary-proof.md) now demonstrates the missing
pre-request bound: one fixture response overshot a USD 0.01 threshold to an
estimated USD 0.18015. HFACT T01 must implement and prove a metered request path
outside the workload, including direct-provider bypass denial. The full-charge
reservation/reconciliation implementation remains unchanged.

View file

@ -3,11 +3,11 @@
llm-connect's ClaudeCodeAdapter is a text-generation adapter (`claude llm-connect's ClaudeCodeAdapter is a text-generation adapter (`claude
--print`, no working directory, no tool grants). An executor run needs an --print`, no working directory, no tool grants). An executor run needs an
*agentic* session: file edits and git commits inside the target repo, under *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: LLMAdapter interface so a hosted adapter can be swapped in later, and adds:
- cwd pinned to the target repo - 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) - allow-list from a named tool profile (default: green-commit-only)
- optional real-time per-tool-call audit events (HARNESS-WP-0002-T03) - 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)] cmd += ["--max-budget-usd", str(budget)]
if turns is not None: if turns is not None:
cmd += ["--max-turns", str(turns)] 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: if self._model:
cmd.extend(["--model", self._model]) cmd.extend(["--model", self._model])
return cmd return cmd
@ -143,8 +154,8 @@ class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
if self._native_config != (None, None) and not self._native_cli_checked: 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) 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()) 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): 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.217 or newer") raise NativeLimitError("native limits require verified Claude Code 2.1.266 or newer")
self._native_cli_checked = True self._native_cli_checked = True
timeout = config.timeout_seconds or self._config.timeout_seconds timeout = config.timeout_seconds or self._config.timeout_seconds
if self._on_tool_event is not None: 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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import re
class UnknownToolProfileError(ValueError): class UnknownToolProfileError(ValueError):
"""Raised when a manifest references a profile that is not registered.""" """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 = ( _GREEN_COMMIT_TOOLS = (
"Read,Write,Edit,Glob,Grep," "Read,Write,Edit,Glob,Grep,"
"Bash(git add:*),Bash(git commit:*),Bash(git status)," "Bash(git add:*),Bash(git commit:*),Bash(git status),"
@ -29,13 +31,25 @@ _BLUE_MAIL_TRIAGE_TOOLS = _GREEN_COMMIT_TOOLS
@dataclass(frozen=True) @dataclass(frozen=True)
class ToolProfile: class ToolProfile:
"""A named hard allow-list for agentic sessions.""" """Named permission rules; controlled runs also restrict the tool inventory."""
name: str name: str
description: str description: str
allowed_tools: str allowed_tools: str
lane: str # green | blue — advisory; enforcement is the allow-list 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] = { PROFILES: dict[str, ToolProfile] = {
"green-commit-only": ToolProfile( "green-commit-only": ToolProfile(

View file

@ -36,6 +36,8 @@ PROMPT_TEMPLATE = """\
You are an unattended executor session (agent persona below, if any). You are an unattended executor session (agent persona below, if any).
Operating rules, non-negotiable: Operating rules, non-negotiable:
- Work ONLY inside the current repository working directory. - 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, - Green/Blue lane: file edits and local git add/commit only. Never push,
never touch the network, never run destructive commands. never touch the network, never run destructive commands.
- Tool profile for this run: {tool_profile} (lane={lane}). - Tool profile for this run: {tool_profile} (lane={lane}).

View file

@ -9,6 +9,7 @@ PYTHON_BIN="${REIN_CONTRACT_PYTHON:-${REPO_ROOT}/.venv/bin/python}"
"${PYTHON_BIN}" -c 'import glas_harness.contract, llm_connect, sandboxer.models' "${PYTHON_BIN}" -c 'import glas_harness.contract, llm_connect, sandboxer.models'
PYTHONPATH="${REPO_ROOT}:${REPO_ROOT}/../llm-connect" \ PYTHONPATH="${REPO_ROOT}:${REPO_ROOT}/../llm-connect" \
"${PYTHON_BIN}" -m pytest \ "${PYTHON_BIN}" -m pytest \
tests/test_native_limits.py \
tests/test_spend_admission.py \ tests/test_spend_admission.py \
tests/test_claim_loop.py::test_process_one_initial_heartbeat_rejection_refuses_dispatch \ tests/test_claim_loop.py::test_process_one_initial_heartbeat_rejection_refuses_dispatch \
tests/test_claim_loop.py::test_process_one_lease_loss_cancels_registered_adapter_process \ tests/test_claim_loop.py::test_process_one_lease_loss_cancels_registered_adapter_process \

View file

@ -10,6 +10,7 @@ PYTHON_BIN="${REIN_CONTRACT_PYTHON:-${REPO_ROOT}/.venv/bin/python}"
"${PYTHON_BIN}" -c 'import glas_harness.contract, llm_connect, sandboxer.models' "${PYTHON_BIN}" -c 'import glas_harness.contract, llm_connect, sandboxer.models'
PYTHONPATH="${REPO_ROOT}:${REPO_ROOT}/../llm-connect" \ PYTHONPATH="${REPO_ROOT}:${REPO_ROOT}/../llm-connect" \
"${PYTHON_BIN}" -m pytest \ "${PYTHON_BIN}" -m pytest \
tests/test_native_limits.py \
tests/test_spend_admission.py \ tests/test_spend_admission.py \
tests/test_glas_execution.py \ tests/test_glas_execution.py \
tests/test_ops_run_client.py \ tests/test_ops_run_client.py \

228
tests/native_cli_fixture.py Normal file
View file

@ -0,0 +1,228 @@
"""Runs inside a network namespace: deterministic HTTP responses, no inference."""
import http.server
import json
import os
import subprocess
import threading
from pathlib import Path
root = Path("/work")
settings = json.loads((root / "case.json").read_text())
seen = []
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
results = []
for message in body.get("messages", []):
if isinstance(message.get("content"), list):
results.extend(
{
"id": block.get("tool_use_id"),
"is_error": block.get("is_error", False),
}
for block in message["content"]
if block.get("type") == "tool_result"
)
seen.append(
{
"path": self.path,
"model": body.get("model"),
"max_tokens": body.get("max_tokens"),
"tools": [tool.get("name") for tool in body.get("tools", [])],
"tool_results": results,
"ambient_context_loaded": "AMBIENT_CONTEXT_SENTINEL"
in json.dumps(body),
}
)
if "count_tokens" in self.path:
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"input_tokens":100}')
return
message_number = sum("count_tokens" not in request["path"] for request in seen)
if settings["case"] == "tools" and message_number == 1:
blocks = [
{
"type": "tool_use",
"id": "tool_allowed",
"name": "Bash",
"input": {
"command": "git status",
"description": "Read fixture status",
},
},
{
"type": "tool_use",
"id": "tool_create",
"name": "Edit",
"input": {
"file_path": "/work/target/result.txt",
"old_string": "",
"new_string": "fixture-created\n",
},
},
{
"type": "tool_use",
"id": "tool_denied",
"name": "Bash",
"input": {
"command": "printf bypass > /work/denied-ran",
"description": "Fixture forbidden operation",
},
},
]
stop = "tool_use"
else:
blocks = [{"type": "text", "text": "fixture complete"}]
stop = "end_turn"
usage = {
"input_tokens": 60000 if settings["case"] == "overrun" else 100,
"output_tokens": 0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
}
events = [
(
"message_start",
{
"type": "message_start",
"message": {
"id": "msg_fixture_" + str(message_number),
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-6",
"content": [],
"stop_reason": None,
"stop_sequence": None,
"usage": usage,
},
},
)
]
for index, block in enumerate(blocks):
if block["type"] == "text":
start = {"type": "text", "text": ""}
delta = {"type": "text_delta", "text": block["text"]}
else:
start = {**block, "input": {}}
delta = {
"type": "input_json_delta",
"partial_json": json.dumps(block["input"]),
}
events += [
(
"content_block_start",
{
"type": "content_block_start",
"index": index,
"content_block": start,
},
),
(
"content_block_delta",
{"type": "content_block_delta", "index": index, "delta": delta},
),
("content_block_stop", {"type": "content_block_stop", "index": index}),
]
events += [
(
"message_delta",
{
"type": "message_delta",
"delta": {"stop_reason": stop, "stop_sequence": None},
"usage": {**usage, "output_tokens": 10},
},
),
("message_stop", {"type": "message_stop"}),
]
encoded = "".join(
"event: " + name + "\ndata: " + json.dumps(value) + "\n\n"
for name, value in events
).encode()
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
(root / "target" / ".claude").mkdir(parents=True)
(root / "target" / ".claude" / "settings.json").write_text(
json.dumps(
{
"permissions": {"allow": ["Bash"]},
"hooks": {
"SessionStart": [
{"hooks": [{"type": "command", "command": "touch /work/hook-ran"}]}
]
},
}
)
)
(root / "target" / ".mcp.json").write_text(
json.dumps(
{
"mcpServers": {
"fixture": {"command": "/bin/sh", "args": ["-c", "touch /work/mcp-ran"]}
}
}
)
)
(root / "target" / "CLAUDE.md").write_text("AMBIENT_CONTEXT_SENTINEL\n")
subprocess.run(["git", "init", "-q", str(root / "target")], check=True)
env = dict(
os.environ,
ANTHROPIC_BASE_URL="http://127.0.0.1:" + str(server.server_port),
ANTHROPIC_API_KEY="fixture-no-provider-secret",
CLAUDE_CONFIG_DIR="/work/config",
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1",
DISABLE_AUTOUPDATER="1",
)
try:
proc = subprocess.run(
settings["argv"],
input="Run the deterministic fixture.",
text=True,
capture_output=True,
check=False,
env=env,
cwd="/work/target",
timeout=45,
)
terminal = json.loads(proc.stdout)
output = {
"returncode": proc.returncode,
"terminal": {
key: terminal.get(key)
for key in (
"type",
"subtype",
"is_error",
"total_cost_usd",
"num_turns",
"usage",
"permission_denials",
)
},
"requests": seen,
"markers": {
name: (root / name).exists()
for name in ("hook-ran", "mcp-ran", "denied-ran")
},
"created_file": (root / "target" / "result.txt").read_text()
if (root / "target" / "result.txt").exists()
else None,
"network_namespace": os.readlink("/proc/self/ns/net"),
}
print(json.dumps(output))
finally:
server.shutdown()

View file

@ -0,0 +1,148 @@
"""Opt-in installed Claude binary against a namespace-local synthetic API."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import subprocess
from pathlib import Path
import pytest
from llm_connect.models import RunConfig
from rein_aharness.adapter import AgenticClaudeCodeAdapter
from rein_aharness.native_limits import NativeLimitError, terminal_accounting
pytestmark = pytest.mark.skipif(
os.environ.get("REIN_REAL_CLAUDE") != "1",
reason="opt-in installed CLI boundary proof",
)
@pytest.mark.parametrize("case", ["overrun", "tools"])
def test_installed_cli_boundary(tmp_path, case):
binary = Path(
os.environ.get("REIN_CLAUDE_PROOF_BINARY") or shutil.which("claude") or ""
).resolve(strict=True)
version = subprocess.check_output([str(binary), "--version"], text=True).strip()
assert version.startswith("2.1.266 "), (
"revalidate the fixture for this binary version before claiming the pinned proof"
)
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, model="claude-sonnet-4-6")
cap = 0.01 if case == "overrun" else 1
argv = adapter._build_command(
RunConfig(model_params={"max_budget_usd": cap, "max_turns": 4})
)
argv[0] = "/opt/claude"
(tmp_path / "case.json").write_text(json.dumps({"case": case, "argv": argv}))
(tmp_path / "passwd").write_text(
f"fixture:x:{os.getuid()}:{os.getgid()}:Fixture:/work/home:/bin/sh\n"
)
(tmp_path / "group").write_text(f"fixture:x:{os.getgid()}:\n")
(tmp_path / "home").mkdir()
shutil.copyfile(
Path(__file__).with_name("native_cli_fixture.py"), tmp_path / "fixture.py"
)
command = [
"bwrap",
"--unshare-all",
"--die-with-parent",
"--new-session",
"--ro-bind",
"/usr",
"/usr",
"--ro-bind",
"/lib",
"/lib",
"--ro-bind",
"/lib64",
"/lib64",
"--symlink",
"usr/bin",
"/bin",
"--proc",
"/proc",
"--dev",
"/dev",
"--tmpfs",
"/tmp",
"--dir",
"/etc",
"--ro-bind",
str(tmp_path / "passwd"),
"/etc/passwd",
"--ro-bind",
str(tmp_path / "group"),
"/etc/group",
"--dir",
"/opt",
"--ro-bind",
str(binary),
"/opt/claude",
"--bind",
str(tmp_path),
"/work",
"--chdir",
"/work",
"--clearenv",
"--setenv",
"PATH",
"/usr/bin:/bin",
"/usr/bin/python3",
"/work/fixture.py",
]
proc = subprocess.run(command, capture_output=True, text=True, timeout=55, check=False)
assert proc.returncode == 0, proc.stderr[-1000:]
result = json.loads(proc.stdout)
assert result["network_namespace"] != os.readlink("/proc/self/ns/net")
assert result["markers"] == {
"hook-ran": False,
"mcp-ran": False,
"denied-ran": False,
}
requests = [row for row in result["requests"] if "count_tokens" not in row["path"]]
expected_tools = {"Read", "Write", "Edit", "Glob", "Grep", "Bash"}
for request in requests:
# The CLI may add its terminal EndConversation tool; it cannot add work tools.
assert set(request["tools"]) - {"EndConversation"} <= expected_tools
assert {"Bash", "Read", "Edit"} <= set(request["tools"])
assert request["model"] == "claude-sonnet-4-6"
assert not request["ambient_context_loaded"]
terminal = result["terminal"]
if case == "overrun":
assert len(requests) == 1
assert proc.returncode == 0 and result["returncode"] == 1
assert terminal["subtype"] == "error_max_budget_usd"
assert terminal["total_cost_usd"] == pytest.approx(0.18015)
assert terminal["total_cost_usd"] > cap
with pytest.raises(NativeLimitError) as caught:
terminal_accounting(terminal, max_budget_usd=cap, max_turns=4)
assert caught.value.cost_usd == terminal["total_cost_usd"]
else:
assert result["returncode"] == 0 and terminal["subtype"] == "success"
assert len(requests) == 2
assert {"id": "tool_allowed", "is_error": False} in requests[1]["tool_results"]
assert {"id": "tool_denied", "is_error": True} in requests[1]["tool_results"]
assert {"id": "tool_create", "is_error": False} in requests[1]["tool_results"]
assert result["created_file"] == "fixture-created\n"
usage, cost = terminal_accounting(terminal, max_budget_usd=cap, max_turns=4)
assert usage["total_tokens"] > 0 and cost > 0
if output_dir := os.environ.get("REIN_CLAUDE_PROOF_OUTPUT"):
target = Path(output_dir)
target.mkdir(parents=True, exist_ok=True)
(target / f"{case}.json").write_text(
json.dumps(
{
"case": case,
"binary_version": version,
"binary_sha256": hashlib.sha256(binary.read_bytes()).hexdigest(),
"argv": argv,
"scope": "synthetic API in isolated network namespace; no real credential or provider request",
**result,
},
indent=2,
)
+ "\n"
)

View file

@ -42,7 +42,7 @@ def test_native_controls_reach_cli_and_account_usage(tmp_path, stream):
patch( patch(
"rein_aharness.adapter.subprocess.run", "rein_aharness.adapter.subprocess.run",
return_value=subprocess.CompletedProcess( return_value=subprocess.CompletedProcess(
[], 0, "2.1.263 (Claude Code)\n", "" [], 0, "2.1.266 (Claude Code)\n", ""
), ),
), ),
patch("rein_aharness.adapter.subprocess.Popen", return_value=proc) as invoke, patch("rein_aharness.adapter.subprocess.Popen", return_value=proc) as invoke,
@ -83,7 +83,7 @@ def test_error_preserves_only_bounded_cost():
assert caught.value.cost_usd == 0.1 and "secret" not in str(caught.value) assert caught.value.cost_usd == 0.1 and "secret" not in str(caught.value)
@pytest.mark.parametrize("version", ["2.1.216 (Claude Code)", "unrecognized"]) @pytest.mark.parametrize("version", ["2.1.216 (Claude Code)", "2.1.265 (Claude Code)", "unrecognized"])
def test_old_cli_refuses_before_prompt(tmp_path, version): def test_old_cli_refuses_before_prompt(tmp_path, version):
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path) adapter = AgenticClaudeCodeAdapter(workdir=tmp_path)
with ( with (
@ -93,7 +93,7 @@ def test_old_cli_refuses_before_prompt(tmp_path, version):
), ),
patch("rein_aharness.adapter.subprocess.Popen") as invoke, patch("rein_aharness.adapter.subprocess.Popen") as invoke,
): ):
with pytest.raises(NativeLimitError, match="2.1.217"): with pytest.raises(NativeLimitError, match="2.1.266"):
adapter.execute_prompt( adapter.execute_prompt(
"task", RunConfig(model_params={"max_budget_usd": 0.25}) "task", RunConfig(model_params={"max_budget_usd": 0.25})
) )
@ -135,3 +135,24 @@ def test_runner_forwards_task_limits(tmp_path):
) )
assert result.ok assert result.ok
assert adapter.configs[0].model_params == {"max_budget_usd": 0.25, "max_turns": 3} assert adapter.configs[0].model_params == {"max_budget_usd": 0.25, "max_turns": 3}
@pytest.mark.parametrize("stream", [False, True])
def test_controlled_session_uses_closed_tools_and_skips_ambient_config(tmp_path, stream):
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, on_tool_event=(lambda e: None) if stream else None)
cmd = adapter._build_command(RunConfig(model_params={"max_budget_usd": 1, "max_turns": 4}))
assert cmd[cmd.index("--permission-mode")+1] == "dontAsk"
assert cmd[cmd.index("--tools")+1] == "Read,Write,Edit,Glob,Grep,Bash"
assert cmd[cmd.index("--setting-sources")+1] == ""
assert json.loads(cmd[cmd.index("--mcp-config")+1]) == {"mcpServers": {}}
assert "--bare" in cmd and "--strict-mcp-config" in cmd
assert cmd[cmd.index("--disallowedTools")+1] == "mcp__*"
def test_invalid_registered_tool_rule_refuses_before_prompt(tmp_path):
from rein_aharness.profiles import ToolProfile
adapter = AgenticClaudeCodeAdapter(workdir=tmp_path, tool_profile=ToolProfile("invalid", "test", "Read,*", "green"))
with patch("rein_aharness.adapter.subprocess.Popen") as invoke:
with pytest.raises(ValueError, match="builtin rule"):
adapter.execute_prompt("task", RunConfig(model_params={"max_turns": 1}))
invoke.assert_not_called()

View file

@ -654,6 +654,24 @@ installation and Railiance recovery proof. T06 remains `wait` for the admitted
real-model and natural queue run. No factory policy, paid execution, deployment, real-model and natural queue run. No factory policy, paid execution, deployment,
profile promotion or G0 grant was created by this source change. profile promotion or G0 grant was created by this source change.
### Installed native CLI proof — 2026-09-09
Claude Code 2.1.266 was tested against a synthetic API in a private network
namespace. Its USD 0.01 stop threshold allowed one response estimated at USD
0.18015 before refusal, demonstrating why declared run reservations alone do not
supply a hard provider bound. Controlled native runs now restrict builtin tools,
use dontAsk and bare mode, suppress ambient settings/MCP, and require the tested
minimum version. Actual CLI tests prove permitted Git status and file creation,
forbidden shell denial, and no fixture hook/MCP/permission-override activation.
See [the executable proof](../docs/native-cli-boundary-proof.md).
T05 stays progress for the provider-facing request guard, protected rebuild and
owner admission. T06 stays wait for the real admitted model/natural queue proof.
Do not infer network/credential confinement from direct CLI tool rules: indirect
Git helpers remain inside the sandbox owner's responsibility. No paid request,
credential read or deployment was performed during the fixture proof.
## Re-prove one governed profiled run and close residuals ## Re-prove one governed profiled run and close residuals
```task ```task