glas-harness/docs/harness-contract.md
tegwick 9bbff9d336
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
ADR-004: composable reins as middleware stays deferred (GLAS-WP-0002-T01)
Checked ADR-002 part 2's deferred question against the two observability
additions that landed since (rein-aharness's tool-event stream,
glas-harness's own gateway hub event) -- both turned out simpler as
direct implementations, neither needed a wrapping middleware layer.
Still zero real candidates for that shape. docs/harness-contract.md
gains the Middleware ABC as a documented, unimplemented sketch for if a
real third case ever appears -- no code written now.

Also flagged GLAS-WP-0002-T02 (live OpenBao verification) as blocked:
`bao token lookup` from this workstation returns 403, no usable vault
session to provision a new AppRole with. Needs the operator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 20:22:55 +02:00

5.6 KiB

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

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:

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.