fix: isolate rein results and normalize manager startup failures
Some checks failed
ci / validate (push) Has been cancelled
Some checks failed
ci / validate (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
parent
73cee37ad8
commit
53f2abf7e0
8 changed files with 152 additions and 32 deletions
|
|
@ -18,7 +18,7 @@ passes and apply-plan refuses the incomplete request.
|
|||
| Provider expiry | `2027-01-31T22:00:00+01:00` = `2027-01-31T21:00:00Z` (user supplied) |
|
||||
| KV-v2 entry | `platform/workloads/glas-harness/claude-agent-dev` |
|
||||
| Only secret field | `ANTHROPIC_API_KEY` |
|
||||
| Read policy name | `workload-kv-read-glas-claude-agent-dev` |
|
||||
| Proposed read policy name | `se-prod-glas-claude-agent-dev-anthropic` |
|
||||
| Proposed route | `glas-claude-agent-dev-anthropic` |
|
||||
| Consumer | Sand-boxer owner delivery to the selected Claude workload |
|
||||
|
||||
|
|
@ -119,3 +119,11 @@ policy name; neither policy has been applied by this work. Adoption is tracked
|
|||
in SECRETS-WP-0009. The production exec gate refuses before OpenBao while
|
||||
SECRETS-WP-0007-T04 and SECRETS-WP-0008-T02/T06 remain unresolved.
|
||||
No production profile selects the unactivated route.
|
||||
|
||||
Owner update received 2026-09-06 (message
|
||||
`3bd23bc9-e863-4f1f-8c76-415812ec0656`): secrets-engine reports the local
|
||||
authorization join fixed at `627810b`. Live activation still waits on the
|
||||
approval-engine ActionAuthorization claim endpoint and access-engine Check
|
||||
service, then configuration, per-lane approval and positive/negative checks.
|
||||
The owner explicitly distinguishes these durable objects from State Hub
|
||||
decisions. This update does not activate the route or verify the provider key.
|
||||
|
|
|
|||
|
|
@ -108,11 +108,11 @@ def run_execution(
|
|||
_report(request, result)
|
||||
return result
|
||||
|
||||
manager = manager or SandboxManager()
|
||||
status = None
|
||||
session = None
|
||||
try:
|
||||
try:
|
||||
manager = manager if manager is not None else SandboxManager()
|
||||
status = manager.create(
|
||||
SandboxCreateRequest(
|
||||
profile=profile.sandbox_profile,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ class ReinAharness(Rein):
|
|||
self.model = model
|
||||
self.tool_profile = tool_profile
|
||||
self.budget_tokens = budget_tokens
|
||||
self._last_result: dict[str, Any] = {}
|
||||
|
||||
def _bin(self, transport: ExecutionTransport) -> str:
|
||||
try:
|
||||
|
|
@ -109,18 +108,18 @@ class ReinAharness(Rein):
|
|||
proc = transport.run(argv, timeout=session["timeout_seconds"])
|
||||
ok = proc.returncode == 0
|
||||
output, events = self._split_stream_events(proc.stdout)
|
||||
self._last_result = parse_json_object(output)
|
||||
last_result = session["last_result"] = parse_json_object(output)
|
||||
return ToolResult(
|
||||
ok=ok,
|
||||
output=output,
|
||||
error=None if ok else (proc.stderr or self._last_result.get("reason")),
|
||||
error=None if ok else (proc.stderr or last_result.get("reason")),
|
||||
events=events,
|
||||
events_completeness="complete" if self.stream_tool_events else "unavailable",
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
tokens_spent=last_result.get("tokens_spent"),
|
||||
duration_s=last_result.get("execution_time_s"),
|
||||
resolved_model=last_result.get("model") or self.model,
|
||||
metadata={
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
"tool_profile": last_result.get("tool_profile") or self.tool_profile,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -147,10 +146,11 @@ class ReinAharness(Rein):
|
|||
|
||||
def end_session(self, session: dict[str, Any]) -> ExecutionSummary:
|
||||
transport: ExecutionTransport = session["transport"]
|
||||
last_result = session.get("last_result", {})
|
||||
head_after = transport.git_head()
|
||||
committed = bool(head_after) and head_after != session.get("head_before")
|
||||
reported_ok = bool(self._last_result.get("ok", committed))
|
||||
reason = self._last_result.get("reason") or None
|
||||
reported_ok = bool(last_result.get("ok", committed))
|
||||
reason = last_result.get("reason") or None
|
||||
outcome = "succeeded" if committed and reported_ok else (
|
||||
"refused" if isinstance(reason, str) and reason.startswith("refused:") else "failed"
|
||||
)
|
||||
|
|
@ -159,13 +159,13 @@ class ReinAharness(Rein):
|
|||
committed=committed,
|
||||
outcome=outcome,
|
||||
reason=reason,
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
tokens_spent=last_result.get("tokens_spent"),
|
||||
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 [],
|
||||
metadata={
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
"persona_source": self._last_result.get("persona_source"),
|
||||
"tool_profile": last_result.get("tool_profile") or self.tool_profile,
|
||||
"persona_source": last_result.get("persona_source"),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ class ReinOpenWeights(Rein):
|
|||
self.max_turns = max_turns
|
||||
self.budget_tokens = budget_tokens
|
||||
self.tool_profile = tool_profile
|
||||
self._last_result: dict[str, Any] = {}
|
||||
|
||||
def _bin(self, transport: ExecutionTransport) -> str:
|
||||
try:
|
||||
|
|
@ -94,27 +93,28 @@ class ReinOpenWeights(Rein):
|
|||
argv += ["--tool-profile", self.tool_profile]
|
||||
proc = transport.run(argv, timeout=session["timeout_seconds"])
|
||||
ok = proc.returncode == 0
|
||||
self._last_result = parse_json_object(proc.stdout)
|
||||
last_result = session["last_result"] = parse_json_object(proc.stdout)
|
||||
return ToolResult(
|
||||
ok=ok,
|
||||
output=proc.stdout,
|
||||
error=None if ok else (proc.stderr or self._last_result.get("reason")),
|
||||
error=None if ok else (proc.stderr or last_result.get("reason")),
|
||||
events_completeness="unavailable",
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
tokens_spent=last_result.get("tokens_spent"),
|
||||
duration_s=last_result.get("execution_time_s"),
|
||||
resolved_model=last_result.get("model") or self.model,
|
||||
metadata={
|
||||
"turns": self._last_result.get("turns"),
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
"turns": last_result.get("turns"),
|
||||
"tool_profile": last_result.get("tool_profile") or self.tool_profile,
|
||||
},
|
||||
)
|
||||
|
||||
def end_session(self, session: dict[str, Any]) -> ExecutionSummary:
|
||||
transport: ExecutionTransport = session["transport"]
|
||||
last_result = session.get("last_result", {})
|
||||
head_after = transport.git_head()
|
||||
committed = bool(head_after) and head_after != session.get("head_before")
|
||||
reported_ok = bool(self._last_result.get("ok", committed))
|
||||
reason = self._last_result.get("reason") or None
|
||||
reported_ok = bool(last_result.get("ok", committed))
|
||||
reason = last_result.get("reason") or None
|
||||
outcome = "succeeded" if committed and reported_ok else (
|
||||
"refused" if reason == "no OpenRouter credential resolved" else "failed"
|
||||
)
|
||||
|
|
@ -123,13 +123,13 @@ class ReinOpenWeights(Rein):
|
|||
committed=committed,
|
||||
outcome=outcome,
|
||||
reason=reason,
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
tokens_spent=last_result.get("tokens_spent"),
|
||||
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 [],
|
||||
metadata={
|
||||
"turns": self._last_result.get("turns"),
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
"turns": last_result.get("turns"),
|
||||
"tool_profile": last_result.get("tool_profile") or self.tool_profile,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -401,3 +401,17 @@ def test_teardown_failure_is_visible_in_evidence() -> None:
|
|||
assert result.evidence.failure_stage == "teardown"
|
||||
assert result.evidence.error == "teardown failed; inspect direct caller error"
|
||||
assert result.tool_error == "cannot teardown"
|
||||
|
||||
|
||||
def test_manager_initialization_failure_returns_redacted_evidence():
|
||||
with patch("glas_harness.gateway.SandboxManager", side_effect=RuntimeError(
|
||||
"private manager configuration"
|
||||
)), patch("glas_harness.gateway.hub.post_progress_event") as post:
|
||||
result = run_execution(_request(report_to_hub=True),
|
||||
catalog=_catalog_with_readiness(), rein=_FakeRein())
|
||||
assert not result.ok
|
||||
assert result.evidence.failure_stage == "sandbox_create"
|
||||
assert result.evidence.sandbox_id is None
|
||||
assert result.tool_error == "private manager configuration"
|
||||
assert "private manager configuration" not in str(post.call_args)
|
||||
post.assert_called_once()
|
||||
|
|
|
|||
36
tests/test_rein_session_isolation.py
Normal file
36
tests/test_rein_session_isolation.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""A reusable adapter must keep each sandbox's result in its own session."""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from glas_harness.contract import ToolCall
|
||||
from glas_harness.reins.rein_aharness import ReinAharness
|
||||
from glas_harness.reins.rein_openweights import ReinOpenWeights
|
||||
from glas_harness.transport import ExecutionTransport
|
||||
|
||||
|
||||
@pytest.mark.parametrize("adapter", [ReinAharness, ReinOpenWeights])
|
||||
def test_interleaved_sessions_keep_their_own_outcomes_and_usage(adapter):
|
||||
rein = adapter()
|
||||
sessions = []
|
||||
for index, ok in enumerate([True, False]):
|
||||
transport = MagicMock(spec=ExecutionTransport)
|
||||
transport.resolve_executable.return_value = "/runtime/rein"
|
||||
transport.git_head.return_value = f"head-{index}"
|
||||
transport.run.return_value = MagicMock(returncode=0 if ok else 1, stderr="",
|
||||
stdout=json.dumps({"ok": ok, "tokens_spent": 10 + index,
|
||||
"model": f"model-{index}", "reason": None if ok else "failed"}))
|
||||
sessions.append({"transport": transport, "task_file": f"/workspace-{index}/task",
|
||||
"timeout_seconds": 30, "head_before": "baseline"})
|
||||
# Finish A only after B has reported its different result.
|
||||
for session in sessions:
|
||||
rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
first, second = [rein.end_session(session) for session in sessions]
|
||||
assert first.outcome == "succeeded"
|
||||
assert first.tokens_spent == 10
|
||||
assert first.resolved_model == "model-0"
|
||||
assert second.outcome == "failed"
|
||||
assert second.tokens_spent == 11
|
||||
assert second.resolved_model == "model-1"
|
||||
51
workplans/ADHOC-2026-09-06.md
Normal file
51
workplans/ADHOC-2026-09-06.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
id: GLAS-WP-ADHOC-2026-09-06
|
||||
type: workplan
|
||||
title: "Repository review: session isolation and startup failure reporting"
|
||||
domain: infotech
|
||||
repo: glas-harness
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: repository-review
|
||||
created: "2026-09-06"
|
||||
updated: "2026-09-06"
|
||||
---
|
||||
|
||||
# Repository review
|
||||
|
||||
The working tree was clean at review start. GLAS-WP-0012 is the only unfinished
|
||||
workplan. Its real-key proof still waits on owner activation and runtime
|
||||
acceptance; the latest owner update is recorded there. These bounded local
|
||||
fixes do not change profile readiness. No residuals from these fixes.
|
||||
|
||||
## Isolate reusable rein session results
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-09-06-T01
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Both adapters stored the parsed CLI response on the adapter instance. When two
|
||||
sessions interleaved, ending the first used the second session's outcome,
|
||||
model and usage. Store the response in its session and read it from there.
|
||||
The regression test reproduces the mixed outcome in both adapters before the
|
||||
fix and validates distinct outcomes/model/usage afterwards.
|
||||
|
||||
## Normalize sandbox manager initialization failure
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-09-06-T02
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Move default manager construction inside the sandbox-create error boundary.
|
||||
Initialization failure now returns a failed GatewayResult with sandbox_create
|
||||
stage and no sandbox id. Direct callers retain the error; Hub evidence omits
|
||||
its raw contents. The regression test failed with an escaping exception before
|
||||
the fix and passes afterwards.
|
||||
|
||||
Validation: full suite 104 passed; profile catalog validation passed with
|
||||
existing readiness unchanged. Reviewed credential owner update and refreshed
|
||||
the proposed policy reference in docs/anthropic-workload-key.md.
|
||||
|
|
@ -8,7 +8,7 @@ status: blocked
|
|||
owner: codex
|
||||
topic_slug: first-local-profile-production-proof
|
||||
created: "2026-09-05"
|
||||
updated: "2026-09-05"
|
||||
updated: "2026-09-06"
|
||||
state_hub_workstream_id: "170bf1ae-337f-5553-8d1e-03b07100e08f"
|
||||
---
|
||||
|
||||
|
|
@ -223,3 +223,14 @@ policy/AppRole proposal and activation now live in SECRETS-WP-0009. Its
|
|||
production exec refuses before OpenBao because durable access-engine decision
|
||||
records and scoped service authority remain unavailable. T02 remains waiting
|
||||
on that activation, pinned Claude startup and real provider/task acceptance.
|
||||
|
||||
## 2026-09-06 credential owner update
|
||||
|
||||
Reviewed secrets-engine message `3bd23bc9-e863-4f1f-8c76-415812ec0656`.
|
||||
Owner reports the local authorization join implemented at `627810b`. Remaining
|
||||
native activation depends on approval-engine serving the durable
|
||||
ActionAuthorization claim endpoint and access-engine serving Check, followed
|
||||
by configuration, per-lane approval and positive/negative verification.
|
||||
State Hub decisions are not a substitute for the durable authorization object.
|
||||
SECRETS-WP-0009-T03 and this plan's T02 remain waiting; runtime pinning and
|
||||
combined real proof remain required. No readiness change or real-key read.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue