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) |
|
| 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` |
|
| KV-v2 entry | `platform/workloads/glas-harness/claude-agent-dev` |
|
||||||
| Only secret field | `ANTHROPIC_API_KEY` |
|
| 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` |
|
| Proposed route | `glas-claude-agent-dev-anthropic` |
|
||||||
| Consumer | Sand-boxer owner delivery to the selected Claude workload |
|
| 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
|
in SECRETS-WP-0009. The production exec gate refuses before OpenBao while
|
||||||
SECRETS-WP-0007-T04 and SECRETS-WP-0008-T02/T06 remain unresolved.
|
SECRETS-WP-0007-T04 and SECRETS-WP-0008-T02/T06 remain unresolved.
|
||||||
No production profile selects the unactivated route.
|
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)
|
_report(request, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
manager = manager or SandboxManager()
|
|
||||||
status = None
|
status = None
|
||||||
session = None
|
session = None
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
manager = manager if manager is not None else SandboxManager()
|
||||||
status = manager.create(
|
status = manager.create(
|
||||||
SandboxCreateRequest(
|
SandboxCreateRequest(
|
||||||
profile=profile.sandbox_profile,
|
profile=profile.sandbox_profile,
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ class ReinAharness(Rein):
|
||||||
self.model = model
|
self.model = model
|
||||||
self.tool_profile = tool_profile
|
self.tool_profile = tool_profile
|
||||||
self.budget_tokens = budget_tokens
|
self.budget_tokens = budget_tokens
|
||||||
self._last_result: dict[str, Any] = {}
|
|
||||||
|
|
||||||
def _bin(self, transport: ExecutionTransport) -> str:
|
def _bin(self, transport: ExecutionTransport) -> str:
|
||||||
try:
|
try:
|
||||||
|
|
@ -109,18 +108,18 @@ class ReinAharness(Rein):
|
||||||
proc = transport.run(argv, timeout=session["timeout_seconds"])
|
proc = transport.run(argv, timeout=session["timeout_seconds"])
|
||||||
ok = proc.returncode == 0
|
ok = proc.returncode == 0
|
||||||
output, events = self._split_stream_events(proc.stdout)
|
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(
|
return ToolResult(
|
||||||
ok=ok,
|
ok=ok,
|
||||||
output=output,
|
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=events,
|
||||||
events_completeness="complete" if self.stream_tool_events else "unavailable",
|
events_completeness="complete" if self.stream_tool_events else "unavailable",
|
||||||
tokens_spent=self._last_result.get("tokens_spent"),
|
tokens_spent=last_result.get("tokens_spent"),
|
||||||
duration_s=self._last_result.get("execution_time_s"),
|
duration_s=last_result.get("execution_time_s"),
|
||||||
resolved_model=self._last_result.get("model") or self.model,
|
resolved_model=last_result.get("model") or self.model,
|
||||||
metadata={
|
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:
|
def end_session(self, session: dict[str, Any]) -> ExecutionSummary:
|
||||||
transport: ExecutionTransport = session["transport"]
|
transport: ExecutionTransport = session["transport"]
|
||||||
|
last_result = session.get("last_result", {})
|
||||||
head_after = transport.git_head()
|
head_after = transport.git_head()
|
||||||
committed = bool(head_after) and head_after != session.get("head_before")
|
committed = bool(head_after) and head_after != session.get("head_before")
|
||||||
reported_ok = bool(self._last_result.get("ok", committed))
|
reported_ok = bool(last_result.get("ok", committed))
|
||||||
reason = self._last_result.get("reason") or None
|
reason = last_result.get("reason") or None
|
||||||
outcome = "succeeded" if committed and reported_ok else (
|
outcome = "succeeded" if committed and reported_ok else (
|
||||||
"refused" if isinstance(reason, str) and reason.startswith("refused:") else "failed"
|
"refused" if isinstance(reason, str) and reason.startswith("refused:") else "failed"
|
||||||
)
|
)
|
||||||
|
|
@ -159,13 +159,13 @@ class ReinAharness(Rein):
|
||||||
committed=committed,
|
committed=committed,
|
||||||
outcome=outcome,
|
outcome=outcome,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
tokens_spent=self._last_result.get("tokens_spent"),
|
tokens_spent=last_result.get("tokens_spent"),
|
||||||
duration_s=self._last_result.get("execution_time_s"),
|
duration_s=last_result.get("execution_time_s"),
|
||||||
resolved_model=self._last_result.get("model") or self.model,
|
resolved_model=last_result.get("model") or self.model,
|
||||||
artifacts=[head_after] if committed and head_after else [],
|
artifacts=[head_after] if committed and head_after else [],
|
||||||
metadata={
|
metadata={
|
||||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
"tool_profile": last_result.get("tool_profile") or self.tool_profile,
|
||||||
"persona_source": self._last_result.get("persona_source"),
|
"persona_source": last_result.get("persona_source"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ class ReinOpenWeights(Rein):
|
||||||
self.max_turns = max_turns
|
self.max_turns = max_turns
|
||||||
self.budget_tokens = budget_tokens
|
self.budget_tokens = budget_tokens
|
||||||
self.tool_profile = tool_profile
|
self.tool_profile = tool_profile
|
||||||
self._last_result: dict[str, Any] = {}
|
|
||||||
|
|
||||||
def _bin(self, transport: ExecutionTransport) -> str:
|
def _bin(self, transport: ExecutionTransport) -> str:
|
||||||
try:
|
try:
|
||||||
|
|
@ -94,27 +93,28 @@ class ReinOpenWeights(Rein):
|
||||||
argv += ["--tool-profile", self.tool_profile]
|
argv += ["--tool-profile", self.tool_profile]
|
||||||
proc = transport.run(argv, timeout=session["timeout_seconds"])
|
proc = transport.run(argv, timeout=session["timeout_seconds"])
|
||||||
ok = proc.returncode == 0
|
ok = proc.returncode == 0
|
||||||
self._last_result = parse_json_object(proc.stdout)
|
last_result = session["last_result"] = parse_json_object(proc.stdout)
|
||||||
return ToolResult(
|
return ToolResult(
|
||||||
ok=ok,
|
ok=ok,
|
||||||
output=proc.stdout,
|
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",
|
events_completeness="unavailable",
|
||||||
tokens_spent=self._last_result.get("tokens_spent"),
|
tokens_spent=last_result.get("tokens_spent"),
|
||||||
duration_s=self._last_result.get("execution_time_s"),
|
duration_s=last_result.get("execution_time_s"),
|
||||||
resolved_model=self._last_result.get("model") or self.model,
|
resolved_model=last_result.get("model") or self.model,
|
||||||
metadata={
|
metadata={
|
||||||
"turns": self._last_result.get("turns"),
|
"turns": last_result.get("turns"),
|
||||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
"tool_profile": last_result.get("tool_profile") or self.tool_profile,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def end_session(self, session: dict[str, Any]) -> ExecutionSummary:
|
def end_session(self, session: dict[str, Any]) -> ExecutionSummary:
|
||||||
transport: ExecutionTransport = session["transport"]
|
transport: ExecutionTransport = session["transport"]
|
||||||
|
last_result = session.get("last_result", {})
|
||||||
head_after = transport.git_head()
|
head_after = transport.git_head()
|
||||||
committed = bool(head_after) and head_after != session.get("head_before")
|
committed = bool(head_after) and head_after != session.get("head_before")
|
||||||
reported_ok = bool(self._last_result.get("ok", committed))
|
reported_ok = bool(last_result.get("ok", committed))
|
||||||
reason = self._last_result.get("reason") or None
|
reason = last_result.get("reason") or None
|
||||||
outcome = "succeeded" if committed and reported_ok else (
|
outcome = "succeeded" if committed and reported_ok else (
|
||||||
"refused" if reason == "no OpenRouter credential resolved" else "failed"
|
"refused" if reason == "no OpenRouter credential resolved" else "failed"
|
||||||
)
|
)
|
||||||
|
|
@ -123,13 +123,13 @@ class ReinOpenWeights(Rein):
|
||||||
committed=committed,
|
committed=committed,
|
||||||
outcome=outcome,
|
outcome=outcome,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
tokens_spent=self._last_result.get("tokens_spent"),
|
tokens_spent=last_result.get("tokens_spent"),
|
||||||
duration_s=self._last_result.get("execution_time_s"),
|
duration_s=last_result.get("execution_time_s"),
|
||||||
resolved_model=self._last_result.get("model") or self.model,
|
resolved_model=last_result.get("model") or self.model,
|
||||||
artifacts=[head_after] if committed and head_after else [],
|
artifacts=[head_after] if committed and head_after else [],
|
||||||
metadata={
|
metadata={
|
||||||
"turns": self._last_result.get("turns"),
|
"turns": last_result.get("turns"),
|
||||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
"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.failure_stage == "teardown"
|
||||||
assert result.evidence.error == "teardown failed; inspect direct caller error"
|
assert result.evidence.error == "teardown failed; inspect direct caller error"
|
||||||
assert result.tool_error == "cannot teardown"
|
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
|
owner: codex
|
||||||
topic_slug: first-local-profile-production-proof
|
topic_slug: first-local-profile-production-proof
|
||||||
created: "2026-09-05"
|
created: "2026-09-05"
|
||||||
updated: "2026-09-05"
|
updated: "2026-09-06"
|
||||||
state_hub_workstream_id: "170bf1ae-337f-5553-8d1e-03b07100e08f"
|
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
|
production exec refuses before OpenBao because durable access-engine decision
|
||||||
records and scoped service authority remain unavailable. T02 remains waiting
|
records and scoped service authority remain unavailable. T02 remains waiting
|
||||||
on that activation, pinned Claude startup and real provider/task acceptance.
|
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