glas-harness/src/glas_harness/gateway.py
tegwick 1cd890d871
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
feat: add versioned execution profiles
2026-08-21 00:21:53 +02:00

315 lines
9.7 KiB
Python

"""Profile-driven Glas gateway.
The gateway resolves a versioned harness profile before creating a sandbox.
There is deliberately no default rein or model in governed execution.
"""
from __future__ import annotations
import time
import uuid
from datetime import UTC, datetime
from sandboxer.core.manager import SandboxManager
from sandboxer.models import Consumer, SandboxCreateRequest
from glas_harness import hub
from glas_harness.contract import (
CONTRACT_VERSION,
ExecutionEvidence,
ExecutionRequest,
ExecutionSummary,
GatewayResult,
Rein,
SandboxHandle,
ToolCall,
ToolResult,
)
from glas_harness.profiles import ProfileCatalog
def _now() -> str:
return datetime.now(UTC).isoformat()
def _request_refs(request: ExecutionRequest) -> dict:
refs = {
"assignment_ref": request.assignment_ref,
"role_ref": request.role_ref,
"duty_ref": request.duty_ref,
"goal_refs": request.goal_refs,
"resource_envelope_refs": request.resource_envelope_refs,
"expected_output": request.expected_output,
}
return {key: value for key, value in refs.items() if value not in (None, [], "")}
def run_execution(
request: ExecutionRequest,
*,
catalog: ProfileCatalog | None = None,
rein: Rein | None = None,
manager: SandboxManager | None = None,
) -> GatewayResult:
"""Resolve and run one request, returning evidence for every outcome."""
request_id = request.request_id or str(uuid.uuid4())
started_at = _now()
started = time.monotonic()
catalog = catalog or ProfileCatalog()
refs = _request_refs(request)
profile = None
descriptor = None
sandbox_id: str | None = None
tool_result: ToolResult | None = None
summary: ExecutionSummary | None = None
outcome = "failed"
failure_stage = None
error: str | None = None
try:
profile, descriptor = catalog.resolve(request.harness_profile_ref)
selected_rein = rein or catalog.build_rein(profile, descriptor)
except Exception as exc:
outcome = "refused"
failure_stage = "resolution"
error = str(exc)
result = _build_result(
request=request,
request_id=request_id,
started_at=started_at,
started=started,
outcome=outcome,
failure_stage=failure_stage,
error=error,
profile=profile,
descriptor=descriptor,
sandbox_id=None,
tool_result=None,
summary=None,
refs=refs,
)
_report(request, result)
return result
manager = manager or SandboxManager()
status = None
try:
try:
status = manager.create(
SandboxCreateRequest(
profile=profile.sandbox_profile,
inputs={"repo": request.repo},
consumer=Consumer(actor=request.actor, project=request.project),
ttl=None,
)
)
sandbox_id = status.sandbox_id
except Exception as exc:
failure_stage = "sandbox_create"
error = str(exc)
raise
reachability = (
status.reachability.model_dump(mode="json", exclude_none=True)
if status.reachability
else {}
)
sandbox = SandboxHandle(
sandbox_id=status.sandbox_id,
host=status.host or "",
reachability=reachability,
)
try:
session = selected_rein.start_session(
profile,
{
"title": request.title,
"description": request.description,
"target_repo": request.repo,
"request_id": request_id,
},
sandbox,
)
except Exception as exc:
failure_stage = "session_start"
error = str(exc)
raise
try:
tool_result = selected_rein.dispatch_tool(
session, ToolCall(name="run_task", actor=request.actor)
)
except Exception as exc:
failure_stage = "execution"
error = str(exc)
raise
try:
summary = selected_rein.end_session(session)
except Exception as exc:
failure_stage = "session_end"
error = str(exc)
raise
if tool_result.ok and summary.outcome == "succeeded":
outcome = "succeeded"
else:
outcome = summary.outcome if summary.outcome in {"failed", "refused"} else "failed"
failure_stage = "execution"
error = tool_result.error or summary.reason or "rein reported unsuccessful execution"
except Exception:
# The exact error/stage is captured above. All paths still tear down and
# return normalized evidence rather than leaking a provider exception.
pass
finally:
if status is not None:
try:
manager.destroy(status.sandbox_id)
except Exception as exc:
if outcome == "succeeded" or not error:
outcome = "failed"
failure_stage = "teardown"
error = str(exc)
result = _build_result(
request=request,
request_id=request_id,
started_at=started_at,
started=started,
outcome=outcome,
failure_stage=failure_stage,
error=error,
profile=profile,
descriptor=descriptor,
sandbox_id=sandbox_id,
tool_result=tool_result,
summary=summary,
refs=refs,
)
_report(request, result)
return result
def run_task_through_rein(
*,
harness_profile: str,
repo: str,
title: str,
description: str,
rein: Rein | None = None,
actor: str = "agt",
project: str = "glas-harness",
manager: SandboxManager | None = None,
catalog: ProfileCatalog | None = None,
report_to_hub: bool = True,
) -> dict:
"""Compatibility-shaped wrapper around the versioned execution request.
It intentionally requires a harness profile. Direct rein injection is
retained only as a library/test seam and never selects a default backend.
"""
result = run_execution(
ExecutionRequest(
harness_profile_ref=harness_profile,
repo=repo,
title=title,
description=description,
actor=actor,
project=project,
report_to_hub=report_to_hub,
),
catalog=catalog,
rein=rein,
manager=manager,
)
return result.model_dump(mode="json")
def _build_result(
*,
request: ExecutionRequest,
request_id: str,
started_at: str,
started: float,
outcome: str,
failure_stage: str | None,
error: str | None,
profile,
descriptor,
sandbox_id: str | None,
tool_result: ToolResult | None,
summary: ExecutionSummary | None,
refs: dict,
) -> GatewayResult:
duration = max(0.0, time.monotonic() - started)
resolved_model = (
(summary.resolved_model if summary else None)
or (tool_result.resolved_model if tool_result else None)
or (profile.model.model if profile else None)
)
tokens_spent = (
summary.tokens_spent if summary and summary.tokens_spent is not None
else tool_result.tokens_spent if tool_result else None
)
execution_duration = (
summary.duration_s if summary and summary.duration_s is not None
else tool_result.duration_s if tool_result else None
)
evidence_error = error if failure_stage == "resolution" else (
f"{failure_stage} failed; inspect direct caller error" if error and failure_stage else None
)
evidence = ExecutionEvidence(
request_id=request_id,
correlation_id=request.correlation_id,
actor=request.actor,
project=request.project,
target_repo=request.repo,
contract_version=CONTRACT_VERSION,
profile_ref=str(profile.ref) if profile else None,
rein_id=descriptor.id if descriptor else None,
rein_version=descriptor.version if descriptor else None,
model_route=profile.model.route if profile else None,
resolved_model=resolved_model,
sandbox_profile=profile.sandbox_profile if profile else None,
sandbox_id=sandbox_id,
tool_profile=profile.tool_profile if profile else None,
outcome=outcome,
failure_stage=failure_stage,
error=evidence_error,
started_at=started_at,
finished_at=_now(),
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,
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,
tool_events_completeness=(
tool_result.events_completeness if tool_result else "unavailable"
),
refs=refs,
)
return GatewayResult(
ok=outcome == "succeeded",
evidence=evidence,
tool_output=tool_result.output if tool_result else "",
tool_error=error or (tool_result.error if tool_result else None),
)
def _report(request: ExecutionRequest, result: GatewayResult) -> None:
if not request.report_to_hub:
return
evidence = result.evidence.model_dump(mode="json", exclude_none=True)
hub.post_progress_event(
summary=(
f"gateway run: {request.title} "
f"({'ok' if result.ok else result.evidence.outcome})"
),
event_type="gateway_run",
detail=evidence,
)