From 53f2abf7e01a96fd7585a4c69ecb8f754f69a221 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 01:13:23 +0200 Subject: [PATCH] fix: isolate rein results and normalize manager startup failures Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb --- docs/anthropic-workload-key.md | 10 +++- src/glas_harness/gateway.py | 2 +- src/glas_harness/reins/rein_aharness.py | 28 +++++----- src/glas_harness/reins/rein_openweights.py | 30 +++++------ tests/test_gateway.py | 14 +++++ tests/test_rein_session_isolation.py | 36 +++++++++++++ workplans/ADHOC-2026-09-06.md | 51 +++++++++++++++++++ ...12-first-local-profile-production-proof.md | 13 ++++- 8 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 tests/test_rein_session_isolation.py create mode 100644 workplans/ADHOC-2026-09-06.md diff --git a/docs/anthropic-workload-key.md b/docs/anthropic-workload-key.md index f44f684..306610e 100644 --- a/docs/anthropic-workload-key.md +++ b/docs/anthropic-workload-key.md @@ -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. diff --git a/src/glas_harness/gateway.py b/src/glas_harness/gateway.py index 6a9bf48..a84b59c 100644 --- a/src/glas_harness/gateway.py +++ b/src/glas_harness/gateway.py @@ -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, diff --git a/src/glas_harness/reins/rein_aharness.py b/src/glas_harness/reins/rein_aharness.py index d76f52c..7ac7826 100644 --- a/src/glas_harness/reins/rein_aharness.py +++ b/src/glas_harness/reins/rein_aharness.py @@ -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"), }, ) diff --git a/src/glas_harness/reins/rein_openweights.py b/src/glas_harness/reins/rein_openweights.py index ffa846a..d0352da 100644 --- a/src/glas_harness/reins/rein_openweights.py +++ b/src/glas_harness/reins/rein_openweights.py @@ -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, }, ) diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 199cee5..a2d77a5 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -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() diff --git a/tests/test_rein_session_isolation.py b/tests/test_rein_session_isolation.py new file mode 100644 index 0000000..5553ccd --- /dev/null +++ b/tests/test_rein_session_isolation.py @@ -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" diff --git a/workplans/ADHOC-2026-09-06.md b/workplans/ADHOC-2026-09-06.md new file mode 100644 index 0000000..f013f88 --- /dev/null +++ b/workplans/ADHOC-2026-09-06.md @@ -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. diff --git a/workplans/GLAS-WP-0012-first-local-profile-production-proof.md b/workplans/GLAS-WP-0012-first-local-profile-production-proof.md index ee808ca..49bec91 100644 --- a/workplans/GLAS-WP-0012-first-local-profile-production-proof.md +++ b/workplans/GLAS-WP-0012-first-local-profile-production-proof.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.