# Harness contract The interface a concrete harness backend ("rein") implements so glas-harness can route to it, per `docs/adr/ADR-001-rein-harness-family.md`. Mirrors sand-boxer's `SandboxExtension` ABC (`provision`/`wait_ready`/`teardown`) at the harness level, one layer up: sand-boxer establishes *where* dangerous work runs, this contract governs *how an agent session runs* on top of it. ## Interface ```python class Rein(ABC): """Base class for concrete harness backends (reins).""" @abstractmethod def start_session( self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle ) -> dict[str, str]: """Begin an agent session bound to a sandbox. Returns a session handle.""" @abstractmethod def dispatch_tool( self, session: dict[str, str], tool_call: ToolCall ) -> ToolResult: """Run one tool call under the session's policy. Returns result + audit envelope.""" @abstractmethod def end_session(self, session: dict[str, str]) -> dict[str, str]: """Close the session. Returns a summary (commit sha, tokens, duration, outcome).""" ``` - `start_session` resolves the rein's own setup (persona/blueprint loading, credential acquisition) against the sandbox handle glas-harness already obtained from sand-boxer — the rein never provisions or tears down a sandbox itself. - `dispatch_tool` enforces the tool-profile allow-list glas-harness resolved (the same allow-list concept rein-aharness's `profiles.py` already implements today, just invoked through the contract instead of only the rein's own CLI). - `end_session` is where the existing "commit exists = success" signal (rein-aharness) or an equivalent per-rein success criterion is reported back to glas-harness for State Hub posting. glas-harness posts the audit event; the rein returns the facts. ## Session lifecycle (who calls what) ``` glas-harness rein sand-boxer ──────────── ──── ────────── resolve harness.* profile request sandbox ──────────────────────────────▶ create() receive sandbox handle ◀────────────────────────────── start_session(profile, in, sbx) ─▶ acquire creds, load persona ◀─ session handle dispatch_tool(session, call) ─▶ run under allow-list ◀─ result + audit envelope (repeat dispatch_tool ...) end_session(session) ─▶ verify success, summarize ◀─ summary post State Hub event request sandbox teardown ──────────────────────────────▶ destroy() ``` glas-harness owns the *outer* loop (profile resolution, sandbox request/teardown, State Hub reporting, actor attribution). The rein owns the *inner* loop (its own agentic reasoning, whatever that looks like — Claude Code CLI subprocess for `rein-aharness`, a direct OpenRouter tool-use loop for `rein-openweights`). ## Actor attribution Every `dispatch_tool` call and lifecycle transition carries an actor (`adm`/`agt`/`atm`) per glas-harness INTENT.md's audit requirement. glas-harness stamps this at dispatch time; reins do not need their own actor model. ## Composable reins as middleware (deferred — sketch only, no code) `docs/adr/ADR-002-credential-brokering-and-composable-reins.md` part 2 raised whether monitoring/evaluation/optimization should become their own composable reins — middleware that wraps or observes a base rein's `dispatch_tool` calls, rather than each being a complete alternative harness backend the way `rein-aharness`/`rein-openweights` are. Resolved in `docs/adr/ADR-004-composable-reins-stay-deferred.md`: **still deferred, sketch recorded, no code written.** The two observability additions since (rein-aharness's per-tool-call audit stream, `glas-harness`'s own gateway hub event) both turned out to be simpler as direct implementations — neither needed a separate wrapping rein — so there still isn't a second real capability wanting this shape, only the original one (llm-connect's optional Functional-layer modules, untouched). If a real second candidate appears, the shape would be: ```python class Middleware(ABC): """Wraps another Rein's dispatch_tool, does not replace start_session/end_session.""" def __init__(self, inner: Rein) -> None: self.inner = inner @abstractmethod def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult: """Call self.inner.dispatch_tool(...), observe/transform, return the result.""" def start_session(self, profile, inputs, sandbox) -> dict[str, str]: return self.inner.start_session(profile, inputs, sandbox) def end_session(self, session) -> dict[str, str]: return self.inner.end_session(session) ``` A chain of `Middleware` wrapping a base `Rein` still satisfies the `Rein` ABC itself (composition, not a parallel type), so glas-harness's gateway would not need to change to consume one — this is deliberately *not* a speculative gateway-side change, just a documented shape to reach for if/when a second candidate shows up. ## Open questions this contract does not resolve yet - The exact `SandboxHandle`/`ToolCall`/`ToolResult` field shapes — these have stabilized in practice (both reins implement them identically) but are not yet declared frozen/versioned the way sand-boxer's models are.