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>
124 lines
5.6 KiB
Markdown
124 lines
5.6 KiB
Markdown
# 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.
|