Constrain controlled CLI sessions and prove native budget overshoot
Some checks failed
Governed runtime contract / contract (push) Failing after 27s
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:
parent
e0b3ff99a2
commit
4ae245a88f
14 changed files with 752 additions and 10 deletions
84
docs/evidence/2026-09-09-native-cli-overrun.json
Normal file
84
docs/evidence/2026-09-09-native-cli-overrun.json
Normal 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]"
|
||||||
|
}
|
||||||
118
docs/evidence/2026-09-09-native-cli-tools.json
Normal file
118
docs/evidence/2026-09-09-native-cli-tools.json
Normal 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]"
|
||||||
|
}
|
||||||
84
docs/native-cli-boundary-proof.md
Normal file
84
docs/native-cli-boundary-proof.md
Normal 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.
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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}).
|
||||||
|
|
|
||||||
|
|
@ -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 \
|
||||||
|
|
|
||||||
|
|
@ -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
228
tests/native_cli_fixture.py
Normal 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()
|
||||||
148
tests/test_native_cli_boundary.py
Normal file
148
tests/test_native_cli_boundary.py
Normal 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"
|
||||||
|
)
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue