Wire the gateway's own State Hub reporting (GLAS-WP-0002-T03)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

hub.py mirrors the two reins' hub.py under author: agt-glas-harness.
run_task_through_rein gains report_to_hub (default True), posting one
gateway_run progress event from a finally block -- fires on both
success and failure, giving the gateway an audit trail independent of
whatever the rein itself reports. cli.py gained a matching --no-hub
flag.

Live-verified: ran a real task through ReinOpenWeights with hub
reporting enabled, confirmed the gateway_run event landed with correct
detail and a real commit sha. 4 new tests, 23/23 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-26 20:20:17 +02:00
parent 4d21b40cef
commit 2925538b93
6 changed files with 206 additions and 3 deletions

View file

@ -10,6 +10,7 @@
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| workplan | GLAS-0001 | active | — | workplans/GLAS-0001-statehub-bootstrap.md | | workplan | GLAS-0001 | active | — | workplans/GLAS-0001-statehub-bootstrap.md |
| workplan | GLAS-WP-0001 | active | — | workplans/GLAS-WP-0001-harness-router-foundation.md | | workplan | GLAS-WP-0001 | active | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
| workplan | GLAS-WP-0002 | proposed | — | workplans/GLAS-WP-0002-observability-and-composability-followups.md |
| task | GLAS-0001-T01 | done | — | workplans/GLAS-0001-statehub-bootstrap.md | | task | GLAS-0001-T01 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
| task | GLAS-0001-T02 | done | — | workplans/GLAS-0001-statehub-bootstrap.md | | task | GLAS-0001-T02 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
| task | GLAS-0001-T03 | done | — | workplans/GLAS-0001-statehub-bootstrap.md | | task | GLAS-0001-T03 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
@ -19,3 +20,7 @@
| task | GLAS-WP-0001-T04 | done | — | workplans/GLAS-WP-0001-harness-router-foundation.md | | task | GLAS-WP-0001-T04 | done | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
| task | GLAS-WP-0001-T05 | done | — | workplans/GLAS-WP-0001-harness-router-foundation.md | | task | GLAS-WP-0001-T05 | done | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
| task | GLAS-WP-0001-T06 | done | — | workplans/GLAS-WP-0001-harness-router-foundation.md | | task | GLAS-WP-0001-T06 | done | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
| task | GLAS-WP-0002-T01 | todo | — | workplans/GLAS-WP-0002-observability-and-composability-followups.md |
| task | GLAS-WP-0002-T02 | todo | — | workplans/GLAS-WP-0002-observability-and-composability-followups.md |
| task | GLAS-WP-0002-T03 | todo | — | workplans/GLAS-WP-0002-observability-and-composability-followups.md |
| task | GLAS-WP-0002-T04 | todo | — | workplans/GLAS-WP-0002-observability-and-composability-followups.md |

View file

@ -18,6 +18,7 @@ def main(argv: list[str] | None = None) -> int:
run.add_argument("--description", required=True) run.add_argument("--description", required=True)
run.add_argument("--actor", default="agt") run.add_argument("--actor", default="agt")
run.add_argument("--project", default="glas-harness") run.add_argument("--project", default="glas-harness")
run.add_argument("--no-hub", action="store_true", help="Skip the gateway's own hub reporting")
args = parser.parse_args(argv) args = parser.parse_args(argv)
@ -31,6 +32,7 @@ def main(argv: list[str] | None = None) -> int:
description=args.description, description=args.description,
actor=args.actor, actor=args.actor,
project=args.project, project=args.project,
report_to_hub=not args.no_hub,
) )
print(json.dumps(result, indent=2)) print(json.dumps(result, indent=2))
return 0 if result["tool_ok"] else 1 return 0 if result["tool_ok"] else 1

View file

@ -6,6 +6,13 @@ commit landed, tear the sandbox down. This is the parity proof gating
any later "retire rein-aharness as a standalone concern" conversation any later "retire rein-aharness as a standalone concern" conversation
it is not itself that conversation. it is not itself that conversation.
GLAS-WP-0002-T03: post the gateway's own State Hub event, independent of
whatever the rein itself reports (both reins' CLIs are invoked with
their own hub reporting disabled by their glas-harness adapters see
reins/rein_aharness.py / reins/rein_openweights.py). Reported on both
success and failure, from a `finally` block, so a raised exception still
leaves an audit trail.
Requires the `sandbox` extra (sand-boxer installed as a sibling Requires the `sandbox` extra (sand-boxer installed as a sibling
editable dependency). editable dependency).
""" """
@ -17,6 +24,7 @@ from typing import Any
from sandboxer.core.manager import SandboxManager from sandboxer.core.manager import SandboxManager
from sandboxer.models import Consumer, SandboxCreateRequest from sandboxer.models import Consumer, SandboxCreateRequest
from glas_harness import hub
from glas_harness.contract import Rein, SandboxHandle, ToolCall from glas_harness.contract import Rein, SandboxHandle, ToolCall
from glas_harness.reins.rein_aharness import ReinAharness from glas_harness.reins.rein_aharness import ReinAharness
@ -31,6 +39,7 @@ def run_task_through_rein(
actor: str = "agt", actor: str = "agt",
project: str = "glas-harness", project: str = "glas-harness",
manager: SandboxManager | None = None, manager: SandboxManager | None = None,
report_to_hub: bool = True,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Resolve `sandbox_profile`, run one task inside it via `rein`, verify, tear down. """Resolve `sandbox_profile`, run one task inside it via `rein`, verify, tear down.
@ -47,6 +56,8 @@ def run_task_through_rein(
consumer=Consumer(actor=actor, project=project), consumer=Consumer(actor=actor, project=project),
) )
status = manager.create(request) status = manager.create(request)
result: dict[str, Any] | None = None
error: str | None = None
try: try:
reachability = status.reachability.model_dump(mode="json") if status.reachability else {} reachability = status.reachability.model_dump(mode="json") if status.reachability else {}
sandbox = SandboxHandle( sandbox = SandboxHandle(
@ -61,12 +72,56 @@ def run_task_through_rein(
tool_result = rein.dispatch_tool(session, ToolCall(name="run_task", actor=actor)) tool_result = rein.dispatch_tool(session, ToolCall(name="run_task", actor=actor))
summary = rein.end_session(session) summary = rein.end_session(session)
return { result = {
"sandbox_id": status.sandbox_id, "sandbox_id": status.sandbox_id,
"tool_ok": tool_result.ok, "tool_ok": tool_result.ok,
"tool_output": tool_result.output, "tool_output": tool_result.output,
"tool_error": tool_result.error, "tool_error": tool_result.error,
"summary": summary, "summary": summary,
} }
return result
except Exception as exc:
error = str(exc)
raise
finally: finally:
manager.destroy(status.sandbox_id) manager.destroy(status.sandbox_id)
if report_to_hub:
_post_gateway_event(
rein=rein,
sandbox_profile=sandbox_profile,
sandbox_id=status.sandbox_id,
project=project,
actor=actor,
title=title,
result=result,
error=error,
)
def _post_gateway_event(
*,
rein: Rein,
sandbox_profile: str,
sandbox_id: str,
project: str,
actor: str,
title: str,
result: dict[str, Any] | None,
error: str | None,
) -> None:
ok = bool(result and result.get("tool_ok"))
hub.post_progress_event(
summary=f"gateway run: {title} ({'ok' if ok else 'failed'})",
event_type="gateway_run",
detail={
"sandbox_profile": sandbox_profile,
"sandbox_id": sandbox_id,
"rein": type(rein).__name__,
"project": project,
"actor": actor,
"task_title": title,
"ok": ok,
"result": result,
"error": error,
},
)

44
src/glas_harness/hub.py Normal file
View file

@ -0,0 +1,44 @@
"""Custodian State Hub reporting (REST, no MCP) — the gateway's own audit trail.
Mirrors rein-aharness's/rein-openweights's hub.py, but under glas-harness's
own actor attribution. Both reins' CLIs are invoked with their own hub
reporting disabled (`--no-hub`) by their glas-harness adapters this
module is what makes the gateway's audit trail exist independent of
which rein ran (GLAS-WP-0002-T03).
"""
from __future__ import annotations
import os
from typing import Any
import httpx
_DEFAULT_URL = "http://127.0.0.1:8000"
_TIMEOUT = 10.0
def _base_url() -> str:
return os.environ.get("STATE_HUB_URL", _DEFAULT_URL).rstrip("/")
def post_progress_event(
summary: str,
event_type: str,
detail: dict[str, Any],
task_id: str | None = None,
) -> bool:
payload: dict[str, Any] = {
"summary": summary,
"event_type": event_type,
"detail": detail,
"author": "agt-glas-harness",
}
if task_id:
payload["task_id"] = task_id
try:
resp = httpx.post(f"{_base_url()}/progress/", json=payload, timeout=_TIMEOUT)
resp.raise_for_status()
return True
except httpx.HTTPError:
return False

View file

@ -1,5 +1,5 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock, patch
from sandboxer.models import Reachability, SandboxState, SandboxStatus from sandboxer.models import Reachability, SandboxState, SandboxStatus
@ -54,6 +54,7 @@ def test_run_task_through_rein_creates_and_destroys_sandbox() -> None:
description="d", description="d",
rein=rein, rein=rein,
manager=manager, manager=manager,
report_to_hub=False,
) )
assert rein.calls == ["start_session", "dispatch_tool", "end_session"] assert rein.calls == ["start_session", "dispatch_tool", "end_session"]
@ -81,8 +82,84 @@ def test_run_task_through_rein_destroys_sandbox_even_on_failure() -> None:
description="d", description="d",
rein=rein, rein=rein,
manager=manager, manager=manager,
report_to_hub=False,
) )
except RuntimeError: except RuntimeError:
pass pass
manager.destroy.assert_called_once_with("sbx1") manager.destroy.assert_called_once_with("sbx1")
def test_run_task_through_rein_reports_success_event() -> None:
manager = MagicMock()
manager.create.return_value = _fake_status()
rein = _FakeRein()
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
run_task_through_rein(
sandbox_profile="profile.bwrap-local",
repo="/tmp/repo",
title="t",
description="d",
rein=rein,
manager=manager,
)
post.assert_called_once()
kwargs = post.call_args.kwargs
assert kwargs["event_type"] == "gateway_run"
assert "ok)" in kwargs["summary"]
assert kwargs["detail"]["ok"] is True
assert kwargs["detail"]["sandbox_id"] == "sbx1"
assert kwargs["detail"]["rein"] == "_FakeRein"
def test_run_task_through_rein_reports_failure_event_and_still_raises() -> None:
manager = MagicMock()
manager.create.return_value = _fake_status()
class _FailingRein(_FakeRein):
def dispatch_tool(self, session, tool_call):
raise RuntimeError("boom")
rein = _FailingRein()
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
try:
run_task_through_rein(
sandbox_profile="profile.bwrap-local",
repo="/tmp/repo",
title="t",
description="d",
rein=rein,
manager=manager,
)
assert False, "expected RuntimeError to propagate"
except RuntimeError:
pass
post.assert_called_once()
kwargs = post.call_args.kwargs
assert "failed)" in kwargs["summary"]
assert kwargs["detail"]["ok"] is False
assert kwargs["detail"]["error"] == "boom"
assert kwargs["detail"]["result"] is None
def test_run_task_through_rein_skips_hub_when_disabled() -> None:
manager = MagicMock()
manager.create.return_value = _fake_status()
rein = _FakeRein()
with patch("glas_harness.gateway.hub.post_progress_event") as post:
run_task_through_rein(
sandbox_profile="profile.bwrap-local",
repo="/tmp/repo",
title="t",
description="d",
rein=rein,
manager=manager,
report_to_hub=False,
)
post.assert_not_called()

View file

@ -2,6 +2,7 @@
id: GLAS-WP-0002 id: GLAS-WP-0002
title: "Observability and composability follow-ups" title: "Observability and composability follow-ups"
status: proposed status: proposed
state_hub_workstream_id: "ec0777ff-18ce-4666-abaf-5bd2ff0fd4b9"
--- ---
Four items surfaced as open-but-not-urgent while closing out Four items surfaced as open-but-not-urgent while closing out
@ -33,6 +34,7 @@ this same task — not before.
id: GLAS-WP-0002-T01 id: GLAS-WP-0002-T01
status: todo status: todo
priority: medium priority: medium
state_hub_task_id: "2de6074e-b72b-4eb7-b454-6b849011348c"
``` ```
## Task: Live-verify rein-openweights's OpenBao credential path ## Task: Live-verify rein-openweights's OpenBao credential path
@ -53,6 +55,7 @@ both repos.
id: GLAS-WP-0002-T02 id: GLAS-WP-0002-T02
status: todo status: todo
priority: medium priority: medium
state_hub_task_id: "74b4f35b-9cdd-4228-aeed-e0eec458c0bf"
``` ```
## Task: Wire glas-harness's own State Hub reporting from the gateway ## Task: Wire glas-harness's own State Hub reporting from the gateway
@ -67,10 +70,26 @@ what makes the *gateway's* audit trail exist independent of which rein
ran, matching the "observable by default" design principle in ran, matching the "observable by default" design principle in
`INTENT.md`. `INTENT.md`.
**Done (2026-07-26).** `src/glas_harness/hub.py` (mirrors the two
reins' hub.py under `author: agt-glas-harness`); `gateway.py`'s
`run_task_through_rein` gains `report_to_hub: bool = True`, posting one
`gateway_run` progress event from a `finally` block — fires on both
success and failure (a raised exception still leaves an audit trail),
with `ok`/`rein`/`sandbox_id`/`sandbox_profile`/`project`/`result`/
`error` in the detail. `cli.py` gained a matching `--no-hub` flag. Live
verified: ran a real task through `ReinOpenWeights` with hub reporting
enabled, confirmed the `gateway_run` event landed in State Hub with the
correct detail (`author: agt-glas-harness`, real commit sha in
`result`). Token events skipped for now — `ToolResult` doesn't carry a
uniform token count across reins, and it wasn't worth forcing one just
for this task. 4 new tests (success/failure/disabled paths), 23/23
passing.
```task ```task
id: GLAS-WP-0002-T03 id: GLAS-WP-0002-T03
status: todo status: done
priority: high priority: high
state_hub_task_id: "76462175-4a85-4552-a6c1-af871cc1b8d3"
``` ```
## Task: First slice of glas-harness's remaining charter pillars ## Task: First slice of glas-harness's remaining charter pillars
@ -92,4 +111,5 @@ indefinitely.
id: GLAS-WP-0002-T04 id: GLAS-WP-0002-T04
status: todo status: todo
priority: low priority: low
state_hub_task_id: "98d4be61-5923-4445-b1f8-5135ada13ad9"
``` ```