feat: capture worker artifacts before teardown and carry native limits
All checks were successful
ci / validate (push) Successful in 2m39s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-09 12:36:15 +02:00
parent 44991600f3
commit fb57c0228f
9 changed files with 159 additions and 3 deletions

View file

@ -0,0 +1,40 @@
{
"schema": "hfact.runtime-transfer-verification.v1",
"date": "2026-09-09",
"rein_tests": 290,
"glas_tests": 133,
"actual_bwrap_owner_execution": true,
"task_authoring": "deterministic fixture",
"queue": "fixture client; heartbeat/close/replay exercised",
"artifact_tests": [
"exact commit retained after sandbox destruction",
"granted path acceptance",
"one commit ancestry",
"corrupt and truncated bundle",
"symlink refusal",
"expanded object limit",
"wrong head",
"missing capture",
"teardown failure",
"lease loss",
"source changed externally",
"ignored cache excluded"
],
"native_controls": [
"profile USD and turn limits reach Claude CLI",
"version preflight",
"strict terminal cost/token/turn accounting",
"limit exhaustion refuses success"
],
"provider_requests": 0,
"paid_factory_attempts": 0,
"profile_promoted": false,
"protected_runtime_rebuilt": false,
"remaining": [
"durable daily/total budget reservations and uncertain outcome recovery",
"provider cap semantics and conservative EUR treatment",
"matching protected runtime pins and exact factory identity/placement",
"G0 operating admission",
"real model G1 and natural Railiance queue G2"
]
}

View file

@ -113,3 +113,24 @@ price, or provider behavior.
Rollback is selection-based: repin a caller to its last approved Glas profile.
Do not bypass Glas with `--sandbox-profile` or add a hidden default rein.
## Worker artifact return and native limits (2026-09-09)
A trusted worker may supply `run_execution(..., artifact_capture=callback)`.
This callback is process-local and cannot be supplied in an ExecutionRequest.
It runs after successful session cleanup and before sandbox destruction. Capture
failure is recorded as `artifact_capture`; destruction is still attempted. The
worker imports only after both cleanup and destruction succeeded. The callback
must retain bounded bytes privately; GatewayResult/Hub evidence carries no Git
bundle. Rein-aharness owns baseline/grant validation, source import and durable
acceptance under its repository transaction.
`ExecutionLimits.max_budget_usd` is an optional positive finite USD amount for
rein-aharness; unsupported reins refuse it. The existing positive integer
`max_turns` is now propagated to that rein. Glas generates the controlled task
file from profile limits and rejects a supplied task file when native limits
are set. Bounded `cost_usd` accounting is carried through tool, summary and
execution evidence. These changes require matching rein/Glas runtime pins.
No catalog profile is promoted or enabled by these fields. Factory admission
still requires provider cap semantics, daily/total reservations, currency policy,
credentials, placement and operating-owner acceptance.

View file

@ -46,7 +46,8 @@ class ModelRoute(ContractModel):
class ExecutionLimits(ContractModel):
budget_tokens: int | None = Field(default=None, gt=0)
timeout_seconds: int | None = Field(default=None, gt=0)
max_turns: int | None = Field(default=None, gt=0)
max_turns: int | None = Field(default=None, gt=0, strict=True)
max_budget_usd: float | None = Field(default=None, gt=0, allow_inf_nan=False, strict=True)
class ReinSelection(ContractModel):
@ -136,6 +137,7 @@ class ToolResult(ContractModel):
events: list[dict[str, Any]] = Field(default_factory=list)
events_completeness: Literal["complete", "partial", "unavailable"] = "unavailable"
tokens_spent: int | None = Field(default=None, ge=0)
cost_usd: float | None = Field(default=None, ge=0, allow_inf_nan=False)
duration_s: float | None = Field(default=None, ge=0)
resolved_model: str | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
@ -147,6 +149,7 @@ class ExecutionSummary(ContractModel):
outcome: Literal["succeeded", "failed", "refused"]
reason: str | None = None
tokens_spent: int | None = Field(default=None, ge=0)
cost_usd: float | None = Field(default=None, ge=0, allow_inf_nan=False)
duration_s: float | None = Field(default=None, ge=0)
resolved_model: str | None = None
artifacts: list[str] = Field(default_factory=list)
@ -203,13 +206,14 @@ class ExecutionEvidence(ContractModel):
tool_profile: str | None = None
outcome: Literal["succeeded", "failed", "refused"]
failure_stage: Literal[
"resolution", "sandbox_create", "session_start", "execution", "session_end", "teardown"
"resolution", "sandbox_create", "session_start", "execution", "session_end", "artifact_capture", "teardown"
] | None = None
error: str | None = None
started_at: str
finished_at: str
duration_s: float = Field(ge=0)
tokens_spent: int | None = Field(default=None, ge=0)
cost_usd: float | None = Field(default=None, ge=0, allow_inf_nan=False)
token_budget: int | None = Field(default=None, ge=0)
commit_sha: str | None = None
artifacts: list[str] = Field(default_factory=list)

View file

@ -7,6 +7,7 @@ There is deliberately no default rein or model in governed execution.
from __future__ import annotations
import time
from collections.abc import Callable
import uuid
from datetime import UTC, datetime
@ -63,6 +64,7 @@ def run_execution(
catalog: ProfileCatalog | None = None,
rein: Rein | None = None,
manager: SandboxManager | None = None,
artifact_capture: Callable[[SandboxHandle, ExecutionSummary], None] | None = None,
) -> GatewayResult:
"""Resolve and run one request, returning evidence for every outcome."""
@ -209,6 +211,13 @@ def run_execution(
outcome = "failed"
failure_stage = "teardown"
error = str(exc)
if artifact_capture is not None and outcome == "succeeded" and summary is not None:
try:
artifact_capture(sandbox, summary)
except Exception as exc:
outcome = "failed"
failure_stage = "artifact_capture"
error = str(exc)
if status is not None:
try:
manager.destroy(status.sandbox_id)
@ -336,6 +345,7 @@ def _build_result(
duration_s=execution_duration if execution_duration is not None else duration,
tokens_spent=tokens_spent,
token_budget=profile.limits.budget_tokens if profile else None,
cost_usd=summary.cost_usd if summary and summary.cost_usd is not None else tool_result.cost_usd if tool_result else None,
commit_sha=summary.commit_sha if summary else None,
artifacts=summary.artifacts if summary else [],
tool_events_count=len(tool_result.events) if tool_result else 0,

View file

@ -178,6 +178,9 @@ class ProfileCatalog:
f"gateway supports {CONTRACT_VERSION}"
)
if profile.limits.max_budget_usd is not None and profile.rein.id != "rein-aharness":
raise IncompatibleProfileError("native USD limits are implemented only by rein-aharness")
descriptor = self.reins().get(profile.rein.id)
if descriptor is None:
raise IncompatibleProfileError(

View file

@ -64,6 +64,8 @@ class ReinAharness(Rein):
self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
) -> dict[str, Any]:
transport = transport_from_sandbox(sandbox)
if inputs.get("task_file") and (profile.limits.max_budget_usd is not None or profile.limits.max_turns is not None):
raise ValueError("native limits require a Glas-generated task file")
target_repo = transport.workspace
head_before = transport.git_head()
generated_task_file = not bool(inputs.get("task_file"))
@ -74,6 +76,8 @@ class ReinAharness(Rein):
"target_repo": target_repo,
"agent": inputs.get("agent", "coach"),
"timeout_seconds": profile.limits.timeout_seconds or 600,
"max_budget_usd": profile.limits.max_budget_usd,
"max_turns": profile.limits.max_turns,
}
)
@ -116,6 +120,7 @@ class ReinAharness(Rein):
events=events,
events_completeness="complete" if self.stream_tool_events else "unavailable",
tokens_spent=last_result.get("tokens_spent"),
cost_usd=last_result.get("cost_usd"),
duration_s=last_result.get("execution_time_s"),
resolved_model=last_result.get("model") or self.model,
metadata={
@ -160,6 +165,7 @@ class ReinAharness(Rein):
outcome=outcome,
reason=reason,
tokens_spent=last_result.get("tokens_spent"),
cost_usd=last_result.get("cost_usd"),
duration_s=last_result.get("execution_time_s"),
resolved_model=last_result.get("model") or self.model,
artifacts=[head_after] if committed and head_after else [],

View file

@ -553,3 +553,47 @@ def test_reachability_preparation_failure_is_reported_and_destroyed():
assert 'private owner detail' not in str(post.call_args)
assert rein.calls == []
manager.destroy.assert_called_once_with('sbx1')
def test_artifact_capture_occurs_after_cleanup_before_destroy():
manager = MagicMock()
manager.create.return_value = _fake_status()
rein = _FakeRein()
order = []
rein.cleanup_session = lambda session: order.append("cleanup")
manager.destroy.side_effect = lambda sandbox: order.append("destroy")
def capture(sandbox, summary):
assert summary.commit_sha == "deadbeef"
assert sandbox._owner_execute is not None
order.append("capture")
result = run_execution(_request(), catalog=_catalog_with_readiness(), rein=rein,
manager=manager, artifact_capture=capture)
assert result.ok
assert order == ["cleanup", "capture", "destroy"]
assert "bundle" not in result.model_dump_json()
def test_artifact_capture_failure_still_destroys_sandbox():
manager = MagicMock()
manager.create.return_value = _fake_status()
def capture(*args):
raise ValueError("invalid artifact")
result = run_execution(_request(), catalog=_catalog_with_readiness(), rein=_FakeRein(),
manager=manager, artifact_capture=capture)
assert not result.ok
assert result.evidence.failure_stage == "artifact_capture"
assert result.evidence.sandbox_destroy == "succeeded"
manager.destroy.assert_called_once_with("sbx1")
def test_cleanup_failure_never_captures_artifact():
manager = MagicMock()
manager.create.return_value = _fake_status()
rein = _FakeRein()
rein.cleanup_session = MagicMock(side_effect=ValueError("cleanup"))
capture = MagicMock()
result = run_execution(_request(), catalog=_catalog_with_readiness(), rein=rein,
manager=manager, artifact_capture=capture)
assert not result.ok
capture.assert_not_called()
manager.destroy.assert_called_once()

View file

@ -210,3 +210,19 @@ def test_cleanup_session_removes_only_generated_task_file() -> None:
{"transport": transport, "task_file": "/sandbox/task.json", "generated_task_file": False}
)
transport.remove_file.assert_not_called()
def test_profile_native_limits_are_carried_in_owned_task_file():
from glas_harness.contract import ExecutionLimits
profile = ProfileCatalog().resolve("harness.agent-dev-local@1.0.0")[0].model_copy(update={
"limits":ExecutionLimits(max_budget_usd=.25,max_turns=3,timeout_seconds=30)})
transport=MagicMock(spec=ExecutionTransport)
transport.workspace="/sandbox"
transport.git_head.return_value="a"*40
rein=ReinAharness()
with patch("glas_harness.reins.rein_aharness.transport_from_sandbox",return_value=transport):
rein.start_session(profile,{"title":"task","description":"prompt asks for unlimited money"},SandboxHandle(sandbox_id="fixture",host="localhost"))
assert transport.write_task_file.call_args.args[0]["max_budget_usd"] == .25
assert transport.write_task_file.call_args.args[0]["max_turns"] == 3
with pytest.raises(ValueError,match="Glas-generated"):
rein.start_session(profile,{"task_file":"/untrusted.json"},object())

View file

@ -8,7 +8,7 @@ status: active
owner: codex
topic_slug: production-dependency-coordination
created: "2026-09-06"
updated: "2026-09-06"
updated: "2026-09-09"
state_hub_workstream_id: "94c02b1f-66ed-588d-bdd7-7158107b85fb"
---
@ -65,6 +65,18 @@ Check revisions and positive/negative evidence; feed verified returns into
GLAS-WP-0012-T02. Keep unfulfilled owner work live, and do not close this plan
while actionable requests lack an owning record or acknowledged disposition.
2026-09-09 implementation return: the worker now captures a bounded Git bundle
through sandbox owner execution after rein cleanup, then validates and imports
the exact one-commit result under its original baseline, grant and lease after
successful teardown. The actual bwrap/Glas/worker test passed with a deterministic
authoring fixture and response-lost close replay, without duplicate execution.
Native profile USD/turn limits now reach Claude CLI controls and require valid
terminal accounting; missing/exhausted accounting refuses success. Daily/total
reservation, EUR treatment and live provider semantics remain HFACT-WP-0001-T01.
The matching rein/Glas code must be rebuilt and admitted in the protected runtime;
this local proof does not close live G1/G2 or authorize a model request. See the
2026-09-09 runtime-transfer evidence and the owning runtime documentation.
## Delivery receipts
| Owner | Root message / reply thread |