Bootstrap one metered owner cycle against a pinned standalone runtime
Some checks failed
Governed runtime contract / contract (push) Failing after 23s
Some checks failed
Governed runtime contract / contract (push) Failing after 23s
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
3e4c976090
commit
565b07716a
11 changed files with 664 additions and 2 deletions
|
|
@ -19,7 +19,7 @@
|
|||
"version": "0.1.0"
|
||||
},
|
||||
{
|
||||
"commit": "bfe0e4c4c86003892796ba0fb990ae21d72278e1",
|
||||
"commit": "be42de7caf90e7d9fe6c8d7fea263603167ac8c5",
|
||||
"distribution": "sandboxer",
|
||||
"source": "../sand-boxer",
|
||||
"version": "0.0.0"
|
||||
|
|
|
|||
73
docs/owner-bootstrap.md
Normal file
73
docs/owner-bootstrap.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# One-cycle owner bootstrap
|
||||
|
||||
`rein-aharness metered-once --owner-config /absolute/private/owner.json` is the
|
||||
explicit exec-env child entry point. It prepares the exact admitted pins, consumes
|
||||
only the deliberately delivered `ANTHROPIC_API_KEY`, removes provider authentication
|
||||
and base-URL variables before other child processes, and runs at most one claim
|
||||
cycle. The provider key is held by MessagesOwner outside the sandbox. Missing/mixed
|
||||
credentials, invalid pins, missing ledgers, blocked profiles, failed readiness and
|
||||
unclaimed refusals return nonzero. Receipt output excludes raw errors and prompts.
|
||||
There is no key fetch, alternate provider, key file, OAuth/HOME or daemon fallback.
|
||||
`--check` validates local pins without a key, queue access or workload dispatch;
|
||||
it can advance the ledger's clock watermark but never initializes/reset its schema.
|
||||
|
||||
Use the standalone candidate interpreter as `python -I -B -m rein_aharness.cli`.
|
||||
`-I` excludes editable PYTHONPATH/user-site fallbacks; `-B` prevents bytecode writes
|
||||
from changing the complete artifact digest. The source and namespace rein commands
|
||||
continue to use their existing entry points. The owner config selects the pinned
|
||||
runtime through the trusted, ephemeral sandbox binding; it cannot add egress or
|
||||
provider credentials to the child. Normal `claim-loop` behavior is unchanged.
|
||||
|
||||
The mode-0600, regular, owner-owned config has exactly these fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1",
|
||||
"authority_ref": "REPLACE_WITH_ACCEPTED_SPEND_AUTHORITY",
|
||||
"spend_policy_sha256": "REPLACE_WITH_ACCEPTED_SPEND_POLICY_DIGEST",
|
||||
"messages_policy": {
|
||||
"tariff_ref": "REPLACE_WITH_ACCEPTED_PROVIDER_BOUNDS_AND_RATES",
|
||||
"model": "claude-sonnet-4-6",
|
||||
"context_tokens": 0,
|
||||
"max_output_tokens": 0,
|
||||
"input_microusd_per_token": 0,
|
||||
"output_microusd_per_token": 0,
|
||||
"allowed_betas": []
|
||||
},
|
||||
"runtime": {
|
||||
"path": "/absolute/accepted/runtime",
|
||||
"sha256": "REPLACE_WITH_ACCEPTED_COMPLETE_ARTIFACT_DIGEST"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The placeholders and zero bounds deliberately refuse. The authority reference and
|
||||
spend digest must match the private existing SpendPolicy. Worker/project, exact
|
||||
profile/descriptor digests, operational readiness, model, empty-egress bwrap profile
|
||||
and runtime digest are checked before claim. Runtime, owner state and target checkout
|
||||
must not overlap. Provision parent and request ledgers as separate reviewed actions.
|
||||
The normal ACTIVITY_CORE/AGENT_HARNESS worker and state configuration still applies.
|
||||
|
||||
This config is not an authorization decision or a custody provenance proof. The
|
||||
invoking credential engine must already have passed its exact action approval,
|
||||
consume, scoped backend/readiness and admitted consumer checks. Its reviewed command
|
||||
must be the fixed one-cycle owner entry point, with the accepted immutable config.
|
||||
The current `glas-claude-agent-dev-anthropic` catalog describes delivery through the
|
||||
sandbox helper and does not yet admit this owner holder. SECRETS-WP-0009-T03 and
|
||||
HFACT-WP-0001-T03/T04 retain that review, existing client/audit/service dependencies,
|
||||
Railiance placement and live negative tests. No real key is read by local validation.
|
||||
|
||||
This initial bootstrap obtains delivery before one claim cycle; an empty queue still
|
||||
uses that delivery attempt. It intentionally exits after that cycle. Native scheduled
|
||||
activation must not wrap a persistent claim loop with one reusable provider credential.
|
||||
Later per-run acquisition for a continuous worker belongs to REINAH-WP-0003-T05/T06
|
||||
and the same credential owner; it must retain exact action/lease/budget semantics.
|
||||
|
||||
`scripts/prove-metered-runtime.py` runs with a built candidate's `python -I -B`.
|
||||
It verifies all four packages and definitions come from the artifact, exercises this
|
||||
CLI once against an empty fake HTTP queue with a synthetic exec-env key, and runs the
|
||||
pinned actual Claude CLI through the protected bwrap mount and metered owner. The
|
||||
positive fake stream and pre-forward insufficient-capacity refusal are separate
|
||||
cases. It checks unchanged artifact digest and teardown. Queue/provider/key/profile
|
||||
are disposable fixtures, never evidence of live admission. The project records the
|
||||
candidate result in `prj-helixforge-factory/evidence/2026-09-09-owner-bootstrap.json`.
|
||||
|
|
@ -4,7 +4,9 @@ Source API: `rein_aharness.messages_owner.MessagesOwner`. The trusted host boots
|
|||
constructs it with an accepted immutable `MessagesPolicy` and an explicitly supplied
|
||||
provider key, then sets `OpsRunConfig.messages_owner`. This is an in-process owner
|
||||
capability, not a queue field, serialized profile or remote sandbox API parameter.
|
||||
The normal CLI does not acquire a key or construct an owner. Set
|
||||
The normal claim-loop CLI does not acquire a key or construct an owner. The explicit
|
||||
[metered-once bootstrap](owner-bootstrap.md) now supplies the one-cycle exec-env
|
||||
child path; native delivery still needs admission. Set
|
||||
`AGENT_HARNESS_REQUIRE_REQUEST_ADMISSION=1` in a future admitted service so a missing
|
||||
bootstrap refuses before claiming work. Parent spend admission must also be configured;
|
||||
provision the RequestLedger schema explicitly before dispatch. No live bootstrap or
|
||||
|
|
|
|||
|
|
@ -554,6 +554,13 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
poll.add_argument("--no-hub", action="store_true", help="Skip hub on execute")
|
||||
|
||||
metered = sub.add_parser(
|
||||
"metered-once", help="One admitted owner claim cycle with explicit exec-env delivery"
|
||||
)
|
||||
metered.add_argument("--owner-config", required=True, type=Path)
|
||||
metered.add_argument("--check", action="store_true", help="Validate local pins without claiming")
|
||||
metered.add_argument("--no-hub", action="store_true")
|
||||
|
||||
claim_loop = sub.add_parser(
|
||||
"claim-loop",
|
||||
help="Continuously claim ops_runs, select approach, execute, complete/fail",
|
||||
|
|
@ -622,6 +629,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||
if args.command == "poll":
|
||||
return _cmd_poll(args)
|
||||
|
||||
if args.command == "metered-once":
|
||||
from rein_aharness.owner_bootstrap import run_once
|
||||
return run_once(args.owner_config, check_only=args.check, report_to_hub=not args.no_hub)
|
||||
if args.command == "claim-loop":
|
||||
return _cmd_claim_loop(args)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class MessagesOwner:
|
|||
provider_key: str = field(repr=False)
|
||||
upstream_url: str = "https://api.anthropic.com"
|
||||
allow_test_http: bool = False
|
||||
runtime_path: Path | None = None
|
||||
runtime_sha256: str | None = None
|
||||
|
||||
@contextmanager
|
||||
def activate(self, run, config, spend, profile, cancel: ExecutionCancel):
|
||||
|
|
@ -100,6 +102,7 @@ class MessagesOwner:
|
|||
socket_path, token, profile.sandbox_profile, "agt",
|
||||
config.execution_project, run.id,
|
||||
private_paths=(spend.path.parent,),
|
||||
runtime_path=self.runtime_path, runtime_sha256=self.runtime_sha256,
|
||||
)
|
||||
server.start()
|
||||
timer.start()
|
||||
|
|
|
|||
158
rein_aharness/owner_bootstrap.py
Normal file
158
rein_aharness/owner_bootstrap.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""One-cycle entry point for an explicitly admitted exec-env owner child.
|
||||
|
||||
Custody/approval belong to the invoking delivery engine. This module neither
|
||||
fetches a key nor claims that a configuration file authorizes credential access.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import threading
|
||||
|
||||
from llm_connect.messages_gate import MessagesPolicy
|
||||
from rein_aharness.messages_owner import MessagesOwner
|
||||
from rein_aharness.ops_run_client import ActivityCoreOpsClient, OpsRunConfig
|
||||
from rein_aharness.spend_admission import SpendAdmissionError, _private_file, worker_spend
|
||||
|
||||
|
||||
class BootstrapRefused(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def prepare(path: Path, config: OpsRunConfig) -> tuple[MessagesPolicy, Path, str]:
|
||||
"""Validate value-free, owner-controlled pins without a key or queue claim."""
|
||||
from glas_harness.profiles import ProfileCatalog
|
||||
from sandboxer.extensions.runtime import verified_runtime
|
||||
try:
|
||||
_private_file(path)
|
||||
if not path.is_absolute() or path.resolve() != path or path.stat().st_size > 65536:
|
||||
raise ValueError
|
||||
|
||||
def unique(pairs):
|
||||
result = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise ValueError
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
data = json.loads(path.read_text(), object_pairs_hook=unique)
|
||||
if set(data) != {"version", "authority_ref", "spend_policy_sha256", "messages_policy", "runtime"}:
|
||||
raise ValueError
|
||||
if data["version"] != "1":
|
||||
raise ValueError
|
||||
spend = worker_spend(config)
|
||||
if spend is None or data["authority_ref"] != spend.policy.authority_ref:
|
||||
raise ValueError
|
||||
if data["spend_policy_sha256"] != spend.policy.sha256:
|
||||
raise ValueError
|
||||
if (config.worker_id != spend.policy.worker_id
|
||||
or config.execution_project != spend.policy.project
|
||||
or path.is_relative_to(Path(spend.policy.target_repo).resolve())):
|
||||
raise ValueError
|
||||
values = data["messages_policy"]
|
||||
if not isinstance(values, dict):
|
||||
raise ValueError
|
||||
if "allowed_betas" in values:
|
||||
if not isinstance(values["allowed_betas"], list):
|
||||
raise ValueError
|
||||
values["allowed_betas"] = tuple(values["allowed_betas"])
|
||||
policy = MessagesPolicy(**values)
|
||||
runtime = verified_runtime({"runtime": data["runtime"]})
|
||||
if runtime is None or not runtime.is_absolute() or runtime.resolve() != runtime:
|
||||
raise ValueError
|
||||
for private in (path.parent, spend.path.parent, Path(spend.policy.target_repo)):
|
||||
a, b = runtime.resolve(), private.resolve()
|
||||
if a.is_relative_to(b) or b.is_relative_to(a):
|
||||
raise ValueError
|
||||
catalog = ProfileCatalog()
|
||||
profile, descriptor = catalog.resolve(spend.policy.profile_ref)
|
||||
catalog.require_operational(profile)
|
||||
from rein_aharness.spend_admission import digest
|
||||
if (digest(profile.model_dump(mode="json")) != spend.policy.profile_sha256
|
||||
or digest(descriptor.model_dump(mode="json")) != spend.policy.descriptor_sha256
|
||||
or profile.model.model != policy.model or profile.credential_route_refs):
|
||||
raise ValueError
|
||||
from sandboxer.profiles.loader import load_profile
|
||||
sandbox = load_profile(profile.sandbox_profile)
|
||||
if sandbox.extension != "ext.bwrap" or sandbox.network.default != "deny" or sandbox.network.egress or sandbox.setup.secret_refs:
|
||||
raise ValueError
|
||||
spend.preflight()
|
||||
# Provisioning is a separate owner action; never silently initialize/reset.
|
||||
with spend._db() as db:
|
||||
db.execute("SELECT run_id, token_sha256, revoked FROM request_routes LIMIT 0")
|
||||
db.execute("SELECT receipt, state FROM request_reservations LIMIT 0")
|
||||
return policy, runtime, data["runtime"]["sha256"]
|
||||
except Exception:
|
||||
raise BootstrapRefused("owner bootstrap configuration refused") from None
|
||||
|
||||
|
||||
def run_once(path: Path, *, check_only: bool = False, report_to_hub: bool = True) -> int:
|
||||
# Only this explicit exec-env child entry point consumes the conventional key.
|
||||
# Remove it before any subprocess or queue client can inherit it. No OAuth,
|
||||
# operator HOME, alternate provider URL or credential-file fallback is used.
|
||||
provider_key = None if check_only else os.environ.get("ANTHROPIC_API_KEY")
|
||||
alternate_auth = None if check_only else os.environ.get("ANTHROPIC_AUTH_TOKEN")
|
||||
for name in ("ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL"):
|
||||
os.environ.pop(name, None)
|
||||
stop = threading.Event()
|
||||
previous = {}
|
||||
config = None
|
||||
try:
|
||||
config = OpsRunConfig.from_env()
|
||||
policy, runtime_path, runtime_sha256 = prepare(path, config)
|
||||
if check_only:
|
||||
print(json.dumps({"ok": True, "check_only": True, "dispatch_enabled": False,
|
||||
"policy_sha256": policy.sha256, "runtime_sha256": runtime_sha256}))
|
||||
return 0
|
||||
if (alternate_auth or not isinstance(provider_key, str) or not provider_key
|
||||
or len(provider_key) > 8192
|
||||
or any(ord(c) < 33 or ord(c) > 126 for c in provider_key)):
|
||||
raise BootstrapRefused("explicit exec-env provider delivery required")
|
||||
owner = MessagesOwner(policy, provider_key, runtime_path=runtime_path,
|
||||
runtime_sha256=runtime_sha256)
|
||||
|
||||
class OneCycleOwner:
|
||||
def activate(self, run, config, spend, profile, cancel):
|
||||
if stop.is_set():
|
||||
cancel.cancel("signal")
|
||||
return owner.activate(run, config, spend, profile, cancel)
|
||||
|
||||
config.messages_owner = OneCycleOwner()
|
||||
config.require_request_admission = True
|
||||
config.require_spend_admission = True
|
||||
client = ActivityCoreOpsClient(config)
|
||||
from rein_aharness.claim_loop import _cancel_active_run, process_one
|
||||
from rein_aharness.readiness import run_readiness_checks
|
||||
|
||||
def cancelled(signum, frame):
|
||||
stop.set()
|
||||
_cancel_active_run("signal")
|
||||
|
||||
for signum in (signal.SIGINT, signal.SIGTERM):
|
||||
previous[signum] = signal.signal(signum, cancelled)
|
||||
if not run_readiness_checks(client).ok:
|
||||
raise BootstrapRefused("owner readiness refused")
|
||||
if stop.is_set():
|
||||
raise BootstrapRefused("owner stopped before claim")
|
||||
result = process_one(client, report_to_hub=report_to_hub)
|
||||
ok = not stop.is_set() and (result.empty or (result.claimed and result.ok is True))
|
||||
print(json.dumps({"ok": ok, "claimed": result.claimed, "empty": result.empty,
|
||||
"run_id": (result.run_id or "")[:200], "ops_state": result.ops_state}))
|
||||
return 0 if ok else 1
|
||||
except (BootstrapRefused, SpendAdmissionError):
|
||||
print(json.dumps({"ok": False, "code": "owner_bootstrap_refused"}))
|
||||
return 2
|
||||
except Exception:
|
||||
# Provider/queue exceptions and config excerpts cannot enter logs/receipts.
|
||||
print(json.dumps({"ok": False, "code": "owner_cycle_failed"}))
|
||||
return 1
|
||||
finally:
|
||||
for signum, handler in previous.items():
|
||||
signal.signal(signum, handler)
|
||||
if config is not None:
|
||||
config.messages_owner = None
|
||||
provider_key = None
|
||||
252
scripts/prove-metered-runtime.py
Normal file
252
scripts/prove-metered-runtime.py
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
"""Installed owner + CLI + bwrap proof with a fake provider and an empty fake queue.
|
||||
|
||||
Run using the candidate's python -I -B. Never acquires a real credential.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import hashlib
|
||||
import http.server
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
import yaml
|
||||
from glas_harness.contract import ExecutionLimits, OperationalReadiness
|
||||
from glas_harness.profiles import ProfileCatalog
|
||||
from llm_connect.messages_gate import MessagesPolicy
|
||||
from llm_connect.models import RunConfig
|
||||
from rein_aharness.adapter import AgenticClaudeCodeAdapter
|
||||
from rein_aharness.execution_cancel import ExecutionCancel
|
||||
from rein_aharness.messages_owner import MessagesOwner
|
||||
from rein_aharness.ops_run_client import OpsRun, OpsRunConfig
|
||||
from rein_aharness.repository_grant import RepositoryGrant
|
||||
from rein_aharness.request_admission import RequestLedger
|
||||
from rein_aharness.spend_admission import SpendLedger, SpendPolicy, digest
|
||||
from sandboxer.extensions.runtime import runtime_digest
|
||||
from sandboxer.models import Consumer, SandboxCreateRequest, SandboxExecRequest
|
||||
from sandboxer.profiles.loader import profiles_dir
|
||||
from sandboxer.extensions.registry import extensions_dir
|
||||
|
||||
BETAS = (
|
||||
"claude-code-20250219", "interleaved-thinking-2025-05-14",
|
||||
"thinking-token-count-2026-05-13", "context-management-2025-06-27",
|
||||
"prompt-caching-scope-2026-01-05", "effort-2025-11-24",
|
||||
)
|
||||
DUMMY_KEY = "synthetic-provider-key-no-live-authority"
|
||||
|
||||
|
||||
def response_bytes():
|
||||
events = [
|
||||
{"type": "message_start", "message": {"id": "msg_fixture", "type": "message",
|
||||
"role": "assistant", "model": "claude-sonnet-4-6", "content": [],
|
||||
"stop_reason": None, "stop_sequence": None,
|
||||
"usage": {"input_tokens": 100, "output_tokens": 0,
|
||||
"cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}}},
|
||||
{"type": "content_block_start", "index": 0,
|
||||
"content_block": {"type": "text", "text": ""}},
|
||||
{"type": "content_block_delta", "index": 0,
|
||||
"delta": {"type": "text_delta", "text": "fixture complete"}},
|
||||
{"type": "content_block_stop", "index": 0},
|
||||
{"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
||||
"usage": {"input_tokens": 100, "output_tokens": 10,
|
||||
"cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}},
|
||||
{"type": "message_stop"},
|
||||
]
|
||||
return "".join("event: " + x["type"] + "\ndata: " + json.dumps(x) + "\n\n" for x in events).encode()
|
||||
|
||||
|
||||
class FixtureServer(http.server.ThreadingHTTPServer):
|
||||
provider_calls = 0
|
||||
queue_claims = 0
|
||||
provider_key_correct = False
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def reply(self, body, content_type="application/json"):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path.startswith("/ops-runs"):
|
||||
self.reply(b'{"items":[]}')
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def do_POST(self):
|
||||
self.rfile.read(int(self.headers.get("Content-Length", "0")))
|
||||
if self.path == "/ops-runs/claim":
|
||||
self.server.queue_claims += 1
|
||||
self.reply(b'{"items":[]}')
|
||||
elif self.path == "/v1/messages":
|
||||
self.server.provider_calls += 1
|
||||
self.server.provider_key_correct = self.headers.get("x-api-key") == DUMMY_KEY
|
||||
self.reply(response_bytes(), "text/event-stream")
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
|
||||
def case(root, runtime, checksum, server, cap, bootstrap=False):
|
||||
root.mkdir()
|
||||
private = root / "private"; private.mkdir(mode=0o700)
|
||||
source = root / "source"; source.mkdir()
|
||||
subprocess.run(["git", "init", "-q", str(source)], check=True)
|
||||
(source / "README.md").write_text("Disposable fixture. Do not edit files.\n")
|
||||
profiles = root / "profiles"; profiles.mkdir()
|
||||
catalog = ProfileCatalog()
|
||||
profile, descriptor = catalog.resolve("harness.agent-dev-local@1.0.0")
|
||||
profile = profile.model_copy(update={
|
||||
"limits": ExecutionLimits(max_budget_usd=cap, max_turns=4),
|
||||
"operational_readiness": OperationalReadiness(status="ready", reason="disposable fixture only",
|
||||
owner="tests", evidence_ref="test:installed-owner"),
|
||||
})
|
||||
(profiles / "fixture.yaml").write_text(yaml.safe_dump(profile.model_dump(mode="json")))
|
||||
grant = RepositoryGrant("1", ("result.txt",), 1, 1, False)
|
||||
policy = SpendPolicy(
|
||||
version="1", envelope_id="fixture", authority_ref="fixture:no-live-authority",
|
||||
valid_from="2026-09-01T00:00:00Z", expires_at="2099-01-01T00:00:00Z",
|
||||
timezone="Europe/Berlin", worker_id="fixture-worker", activity_definition_id="fixture-definition",
|
||||
target_repo=str(source), project="fixture-factory", profile_ref=str(profile.ref),
|
||||
profile_sha256=digest(profile.model_dump(mode="json")),
|
||||
descriptor_sha256=digest(descriptor.model_dump(mode="json")), repository_grant_id=grant.grant_id,
|
||||
max_budget_usd=str(cap), max_liability_usd="5" if cap > 0.01 else "0.01", max_turns=4,
|
||||
eur_per_usd="1", per_run_eur="5", daily_eur="10", total_eur="15",
|
||||
)
|
||||
parent = SpendLedger(private / "spend.sqlite3", policy); parent.initialize()
|
||||
meter = RequestLedger(parent); meter.initialize()
|
||||
policy_path = private / "spend-policy.json"
|
||||
policy_path.write_text(json.dumps(policy.__dict__)); policy_path.chmod(0o600)
|
||||
messages = MessagesPolicy("fixture:not-live-prices", profile.model.model, 200000, 32000,
|
||||
3, 15, allowed_betas=BETAS)
|
||||
owner_path = private / "owner.json"
|
||||
owner_path.write_text(json.dumps({"version": "1", "authority_ref": policy.authority_ref,
|
||||
"spend_policy_sha256": policy.sha256, "messages_policy": messages.__dict__,
|
||||
"runtime": {"path": str(runtime), "sha256": checksum}})); owner_path.chmod(0o600)
|
||||
bootstrap_result = None
|
||||
if bootstrap:
|
||||
env = {"PATH": os.defpath, "LANG": "C.UTF-8", "PYTHONDONTWRITEBYTECODE": "1",
|
||||
"ACTIVITY_CORE_URL": f"http://127.0.0.1:{server.server_port}",
|
||||
"AGENT_HARNESS_WORKER_ID": policy.worker_id,
|
||||
"AGENT_HARNESS_EXECUTION_PROJECT": policy.project,
|
||||
"AGENT_HARNESS_SPEND_POLICY": str(policy_path),
|
||||
"AGENT_HARNESS_SPEND_LEDGER": str(parent.path),
|
||||
"GLAS_PROFILE_DIR": str(profiles), "XDG_DATA_HOME": str(root / "data"),
|
||||
"ANTHROPIC_API_KEY": DUMMY_KEY}
|
||||
child = subprocess.run([str(runtime / "bin/python3"), "-I", "-B", "-m", "rein_aharness.cli",
|
||||
"metered-once", "--owner-config", str(owner_path), "--no-hub"],
|
||||
env=env, capture_output=True, text=True, timeout=30)
|
||||
assert child.returncode == 0, "installed bootstrap refused disposable configuration"
|
||||
bootstrap_result = json.loads(child.stdout)
|
||||
assert bootstrap_result["empty"] and server.queue_claims == 1
|
||||
assert server.provider_calls == 0 and not parent.status()["reservations"]
|
||||
config = OpsRunConfig(worker_id=policy.worker_id, execution_project=policy.project)
|
||||
item = OpsRun(id="fixture-run", activity_definition_id=policy.activity_definition_id,
|
||||
idempotency_key="fixture-key", target_repo=str(source), title="fixture", description="",
|
||||
state="claimed", claim_owner=policy.worker_id, attempt=1, repository_grant=grant,
|
||||
harness_profile_ref=str(profile.ref),
|
||||
lease_until=(datetime.now(UTC) + timedelta(seconds=90)).isoformat())
|
||||
parent.reserve(item)
|
||||
owner = MessagesOwner(messages, DUMMY_KEY, f"http://127.0.0.1:{server.server_port}", True,
|
||||
runtime_path=runtime, runtime_sha256=checksum)
|
||||
before = server.provider_calls
|
||||
consumer = Consumer(actor="agt", project=policy.project, run_id=item.id)
|
||||
with owner.activate(item, config, parent, profile, ExecutionCancel()) as manager:
|
||||
status = manager.create(SandboxCreateRequest(profile="profile.bwrap-local", consumer=consumer,
|
||||
inputs={"repo": str(source)}))
|
||||
try:
|
||||
assert status.state.value == "ready", "sandbox not ready"
|
||||
probe = manager.execute(status.sandbox_id, SandboxExecRequest(
|
||||
consumer=consumer, timeout_seconds=30, command=["python3", "-c", """import os,json,subprocess,sys
|
||||
from pathlib import Path
|
||||
runtime=Path('/opt/sandboxer/runtime')
|
||||
try:
|
||||
(runtime/'must-not-write').write_text('no')
|
||||
except OSError: readonly=True
|
||||
else: readonly=False
|
||||
print(json.dumps({'runtime_readonly':readonly,'python_prefix':sys.prefix,
|
||||
'cli_version':subprocess.check_output(['claude','--version'],text=True).strip(),
|
||||
'private_absent':not Path(sys.argv[1]).exists(),'source_absent':not Path(sys.argv[2]).exists(),
|
||||
'proxy_absent':not any('proxy' in k.lower() for k in os.environ),
|
||||
'interfaces':[x.split(':')[0].strip() for x in Path('/proc/net/dev').read_text().splitlines()[2:]]}))
|
||||
""", str(private), str(source)]))
|
||||
assert probe.exit_code == 0, "installed runtime probe failed"
|
||||
facts = json.loads(probe.stdout)
|
||||
assert facts["runtime_readonly"] and facts["private_absent"] and facts["source_absent"]
|
||||
assert facts["proxy_absent"] and facts["interfaces"] == ["lo"]
|
||||
assert facts["cli_version"] == "2.1.266 (Claude Code)"
|
||||
adapter = AgenticClaudeCodeAdapter(workdir=Path(status.inputs["workspace_dir"]),
|
||||
model=profile.model.model)
|
||||
argv = adapter._build_command(RunConfig(model_params={"max_budget_usd": cap, "max_turns": 4}))
|
||||
executed = manager.execute(status.sandbox_id, SandboxExecRequest(
|
||||
consumer=consumer, command=argv, stdin_text="Reply fixture complete. Do not edit files.",
|
||||
timeout_seconds=45))
|
||||
terminal = json.loads(executed.stdout)
|
||||
if cap <= 0.01:
|
||||
assert executed.exit_code != 0 and terminal["is_error"]
|
||||
assert server.provider_calls == before and not meter.status()
|
||||
else:
|
||||
assert executed.exit_code == 0 and not terminal.get("is_error")
|
||||
assert server.provider_calls == before + 1 and server.provider_key_correct
|
||||
assert len(meter.status()) == 1 and meter.status()[0]["state"] == "charged"
|
||||
token = manager._messages_route.token
|
||||
finally:
|
||||
manager.destroy(status.sandbox_id)
|
||||
assert not Path(status.inputs["workspace_dir"]).exists()
|
||||
with parent._db() as db:
|
||||
assert db.execute("SELECT revoked FROM request_routes").fetchone()[0] == 1
|
||||
output = {"native_threshold_usd": cap, "probe": facts,
|
||||
"provider_requests": server.provider_calls-before, "cli_exit_code": executed.exit_code,
|
||||
"cli_is_error": terminal.get("is_error"), "cli_estimated_usd": terminal.get("total_cost_usd"),
|
||||
"request_reservations": meter.status(), "route_revoked": True, "workspace_removed": True,
|
||||
"bootstrap": bootstrap_result}
|
||||
assert token not in json.dumps(output) and DUMMY_KEY not in json.dumps(output)
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--runtime", required=True, type=Path)
|
||||
parser.add_argument("--sha256", required=True)
|
||||
args = parser.parse_args()
|
||||
runtime = args.runtime.resolve()
|
||||
assert sys.flags.isolated and sys.dont_write_bytecode, "run candidate python with -I -B"
|
||||
assert runtime_digest(runtime) == args.sha256
|
||||
imports = {}
|
||||
for package in ("rein_aharness", "llm_connect", "glas_harness", "sandboxer"):
|
||||
path = Path(importlib.import_module(package).__file__).resolve()
|
||||
assert path.is_relative_to(runtime), "editable source fallback"
|
||||
imports[package] = str(path.relative_to(runtime))
|
||||
assert profiles_dir().is_relative_to(runtime) and extensions_dir().is_relative_to(runtime)
|
||||
os.environ["SANDBOXER_NO_STATE_HUB"] = "1"
|
||||
with tempfile.TemporaryDirectory(prefix="installed-metered-proof-") as directory:
|
||||
root = Path(directory)
|
||||
os.environ["XDG_DATA_HOME"] = str(root / "data")
|
||||
server = FixtureServer(("127.0.0.1", 0), Handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
try:
|
||||
positive = case(root / "positive", runtime, args.sha256, server, 1.0, bootstrap=True)
|
||||
refused = case(root / "refused", runtime, args.sha256, server, 0.01)
|
||||
finally:
|
||||
server.shutdown(); server.server_close()
|
||||
assert runtime_digest(runtime) == args.sha256, "proof mutated the candidate artifact"
|
||||
print(json.dumps({"scope": "standalone installed packages and real CLI/bwrap; synthetic key/provider/empty queue",
|
||||
"ok": True, "runtime_sha256": args.sha256,
|
||||
"claude_sha256": hashlib.sha256((runtime/'bin/claude').read_bytes()).hexdigest(),
|
||||
"imports": imports, "packaged_definitions": True, "positive": positive, "refused": refused,
|
||||
"artifact_unchanged": True, "live_factory_attempts": 0}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -13,6 +13,7 @@ PYTHONPATH="${REPO_ROOT}:${REPO_ROOT}/../llm-connect" \
|
|||
tests/test_spend_admission.py \
|
||||
tests/test_request_admission.py \
|
||||
tests/test_messages_owner.py \
|
||||
tests/test_owner_bootstrap.py \
|
||||
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_profiled_close_failure_happens_after_repository_lock_release \
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ PYTHONPATH="${REPO_ROOT}:${REPO_ROOT}/../llm-connect" \
|
|||
tests/test_spend_admission.py \
|
||||
tests/test_request_admission.py \
|
||||
tests/test_messages_owner.py \
|
||||
tests/test_owner_bootstrap.py \
|
||||
tests/test_glas_execution.py \
|
||||
tests/test_ops_run_client.py \
|
||||
tests/test_claim_loop.py \
|
||||
|
|
|
|||
136
tests/test_owner_bootstrap.py
Normal file
136
tests/test_owner_bootstrap.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"""Synthetic owner config/key; no native custody, provider or queue call."""
|
||||
|
||||
from dataclasses import replace
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from glas_harness.contract import ExecutionLimits, OperationalReadiness
|
||||
from glas_harness.profiles import ProfileCatalog
|
||||
from llm_connect.messages_gate import MessagesPolicy
|
||||
from sandboxer.extensions.runtime import runtime_digest
|
||||
from rein_aharness.claim_loop import ProcessResult
|
||||
from rein_aharness.owner_bootstrap import BootstrapRefused, prepare, run_once
|
||||
from rein_aharness.request_admission import RequestLedger
|
||||
from rein_aharness.spend_admission import SpendLedger, digest
|
||||
from test_spend_admission import ledger as ledger, configured
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prepared(ledger, tmp_path, monkeypatch):
|
||||
catalog = ProfileCatalog()
|
||||
profile, descriptor = catalog.resolve("harness.agent-dev-local@1.0.0")
|
||||
profile = profile.model_copy(update={
|
||||
"sandbox_profile": "profile.bwrap-local", "credential_route_refs": [],
|
||||
"limits": ExecutionLimits(max_budget_usd=4.0, max_turns=8),
|
||||
"operational_readiness": OperationalReadiness(status="ready", reason="fixture only",
|
||||
owner="tests", evidence_ref="test:bootstrap"),
|
||||
})
|
||||
catalog.profiles()[(profile.id, profile.version)] = profile
|
||||
monkeypatch.setattr("glas_harness.profiles.ProfileCatalog", lambda: catalog)
|
||||
policy = replace(ledger.policy, profile_ref=str(profile.ref),
|
||||
profile_sha256=digest(profile.model_dump(mode="json")),
|
||||
descriptor_sha256=digest(descriptor.model_dump(mode="json")))
|
||||
parent = SpendLedger(ledger.path.parent / "prepared.sqlite3", policy)
|
||||
parent.initialize()
|
||||
RequestLedger(parent).initialize()
|
||||
config = configured(parent)
|
||||
runtime = tmp_path / "runtime"
|
||||
(runtime / "bin").mkdir(parents=True)
|
||||
(runtime / "bin/python3").write_bytes(b"not executed; fixture runtime structure")
|
||||
(runtime / "pyvenv.cfg").write_text("fixture only")
|
||||
messages = MessagesPolicy("fixture:upper-rate", profile.model.model, 1000, 1000, 1000, 1000)
|
||||
data = {"version": "1", "authority_ref": policy.authority_ref,
|
||||
"spend_policy_sha256": policy.sha256, "messages_policy": messages.__dict__,
|
||||
"runtime": {"path": str(runtime), "sha256": runtime_digest(runtime)}}
|
||||
path = parent.path.parent / "owner.json"
|
||||
path.write_text(json.dumps(data)); path.chmod(0o600)
|
||||
monkeypatch.setattr("rein_aharness.owner_bootstrap.OpsRunConfig.from_env", lambda: config)
|
||||
return path, config, data, runtime
|
||||
|
||||
|
||||
def test_prepare_pins_without_key_or_claim(prepared, monkeypatch):
|
||||
path, config, data, runtime = prepared
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
policy, selected, checksum = prepare(path, config)
|
||||
assert selected == runtime and checksum == data["runtime"]["sha256"]
|
||||
assert policy.model == data["messages_policy"]["model"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["authority", "policy_digest", "model", "version", "unknown_field",
|
||||
"duplicate", "public", "runtime_changed", "schema_missing"])
|
||||
def test_bad_bootstrap_refuses_before_claim(prepared, monkeypatch, capsys, case):
|
||||
path, config, data, runtime = prepared
|
||||
if case == "authority": data["authority_ref"] = "unrelated"
|
||||
elif case == "policy_digest": data["spend_policy_sha256"] = "0" * 64
|
||||
elif case == "model": data["messages_policy"]["model"] = "unadmitted"
|
||||
elif case == "version": data["version"] = "2"
|
||||
elif case == "unknown_field": data["upstream_url"] = "https://not-admitted.invalid"
|
||||
elif case == "runtime_changed": (runtime / "added-file").write_text("changed")
|
||||
elif case == "schema_missing":
|
||||
import sqlite3
|
||||
with sqlite3.connect(config.spend_ledger_path) as db: db.execute("DROP TABLE request_routes")
|
||||
path.write_text(json.dumps(data))
|
||||
if case == "duplicate": path.write_text(path.read_text().replace('"version": "1"', '"version": "1", "version": "1"'))
|
||||
if case == "public": path.chmod(0o644)
|
||||
claimant = MagicMock()
|
||||
monkeypatch.setattr("rein_aharness.claim_loop.process_one", claimant)
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "fixture-owner-key")
|
||||
assert run_once(path) == 2
|
||||
claimant.assert_not_called()
|
||||
assert "fixture-owner-key" not in capsys.readouterr().out
|
||||
assert "ANTHROPIC_API_KEY" not in os.environ
|
||||
|
||||
|
||||
@pytest.mark.parametrize("check,key,alternate,code", [
|
||||
(True, None, None, 0), (False, None, None, 2),
|
||||
(False, "fixture-key", "alternate-token", 2), (False, "invalid key", None, 2),
|
||||
])
|
||||
def test_check_and_missing_or_mixed_auth_never_claim(prepared, monkeypatch, check, key, alternate, code):
|
||||
path, _, _, _ = prepared
|
||||
for name, value in [("ANTHROPIC_API_KEY", key), ("ANTHROPIC_AUTH_TOKEN", alternate)]:
|
||||
if value is None: monkeypatch.delenv(name, raising=False)
|
||||
else: monkeypatch.setenv(name, value)
|
||||
claimant = MagicMock()
|
||||
monkeypatch.setattr("rein_aharness.claim_loop.process_one", claimant)
|
||||
assert run_once(path, check_only=check) == code
|
||||
claimant.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("result,code", [
|
||||
(ProcessResult(claimed=False, empty=True), 0),
|
||||
(ProcessResult(claimed=True, ok=True, run_id="fixture-run"), 0),
|
||||
(ProcessResult(claimed=False, ok=False, reason="private exception"), 1),
|
||||
(ProcessResult(claimed=True, ok=False, reason="private prompt"), 1),
|
||||
])
|
||||
def test_exactly_one_cycle_without_key_in_environment(prepared, monkeypatch, capsys, result, code):
|
||||
path, config, data, runtime = prepared
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "fixture-key")
|
||||
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
|
||||
monkeypatch.setattr("rein_aharness.readiness.run_readiness_checks", lambda client: SimpleNamespace(ok=True))
|
||||
seen = []
|
||||
def process(client, **kwargs):
|
||||
assert client.config is config
|
||||
assert config.require_request_admission and config.require_spend_admission
|
||||
assert config.messages_owner is not None
|
||||
assert "ANTHROPIC_API_KEY" not in os.environ
|
||||
seen.append(client)
|
||||
return result
|
||||
monkeypatch.setattr("rein_aharness.claim_loop.process_one", process)
|
||||
assert run_once(path, report_to_hub=False) == code
|
||||
output = capsys.readouterr().out
|
||||
assert len(seen) == 1 and "fixture-key" not in output and "private" not in output
|
||||
|
||||
|
||||
def test_unexpected_exception_is_bounded(prepared, monkeypatch, capsys):
|
||||
path, _, _, _ = prepared
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "fixture-key")
|
||||
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
|
||||
monkeypatch.setattr("rein_aharness.readiness.run_readiness_checks", lambda client: SimpleNamespace(ok=True))
|
||||
monkeypatch.setattr("rein_aharness.claim_loop.process_one", MagicMock(side_effect=RuntimeError("fixture-key")))
|
||||
assert run_once(path) == 1
|
||||
assert json.loads(capsys.readouterr().out) == {"ok": False, "code": "owner_cycle_failed"}
|
||||
|
|
@ -765,3 +765,29 @@ protected runtime/CLI artifact, Railiance host/profile/consumer/custody/recovery
|
|||
admission, live provider compatibility and accepted bounds/tariffs/FX, then G0 and
|
||||
natural model/queue evidence. No protected runtime was installed or promoted,
|
||||
no existing CCR changed, no secret read or paid execution took place.
|
||||
|
||||
### Explicit one-cycle owner bootstrap and installed-candidate proof — 2026-09-09
|
||||
|
||||
Implemented `metered-once` with exact private policy/runtime/profile pins, explicit
|
||||
exec-env key consumption outside bwrap, environment scrubbing, one claim cycle,
|
||||
nonzero refusal and bounded receipt output. `--check` never claims or fetches a key.
|
||||
Nineteen synthetic bootstrap tests cover invalid/changed config/artifact/schema,
|
||||
missing/mixed authentication, exactly one empty/success/refused cycle and sanitized
|
||||
exceptions. The existing cancellation/lease lifecycle remains in MessagesOwner.
|
||||
Runtime selection passes through the trusted ephemeral sandbox binding.
|
||||
|
||||
The worker's source suite retains 392 passing non-native cases; the four native
|
||||
cases use the exact proved 2.1.266 binary/digest. The workstation alias has advanced
|
||||
to 2.1.267, which correctly fails the existing pin check; this is not a migration.
|
||||
SAND-WP-0015 now supplies packaged owner definitions and a frozen-lock build mode.
|
||||
Run `scripts/prove-metered-runtime.py` using the candidate's isolated interpreter;
|
||||
source/receipt details live in docs/owner-bootstrap.md and the project's
|
||||
`evidence/2026-09-09-owner-bootstrap.json`.
|
||||
|
||||
T05 remains progress and T06 wait. This supplies executable bootstrap source, not
|
||||
native delivery acceptance. SECRETS-WP-0009-T03 must admit the changed credential
|
||||
holder and fixed child command through the existing action/consume/backend gates.
|
||||
The current catalog/CCRs, live credentials, profile readiness, host service and
|
||||
factory queue remain unchanged. Protected placement, provider bounds/tariffs/FX,
|
||||
G0 and natural execution remain. Per-run acquisition for a future continuous worker
|
||||
is retained here; a delivered key must not authorize an unbounded daemon lifetime.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue