feat: capture worker artifacts before teardown and carry native limits
All checks were successful
ci / validate (push) Successful in 2m39s
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:
parent
44991600f3
commit
fb57c0228f
9 changed files with 159 additions and 3 deletions
40
docs/evidence/2026-09-09-runtime-transfer.json
Normal file
40
docs/evidence/2026-09-09-runtime-transfer.json
Normal 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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 [],
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue