feat: add versioned execution profiles
This commit is contained in:
parent
641e85f5a8
commit
1cd890d871
34 changed files with 2087 additions and 471 deletions
|
|
@ -1,124 +1,97 @@
|
|||
# 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.
|
||||
Contract version: `1.0` (`glas_harness.contract.CONTRACT_VERSION`).
|
||||
|
||||
## Interface
|
||||
This is the stable boundary between Glas, its consumers, and concrete harness
|
||||
backends (reins). Glas owns profile resolution and the outer lifecycle; a rein
|
||||
owns its inner agentic loop. Sand-boxer owns environment provisioning.
|
||||
|
||||
## Consumer boundary
|
||||
|
||||
`ExecutionRequest` requires an explicit `harness_profile_ref`, repository,
|
||||
title, and description. It may carry correlation and organizational references
|
||||
(`assignment_ref`, `role_ref`, `duty_ref`, `goal_refs`, and
|
||||
`resource_envelope_refs`). Those references link execution evidence to
|
||||
leadership/workforce records; they do not transfer organizational authority to
|
||||
Glas.
|
||||
|
||||
The gateway returns `GatewayResult` with:
|
||||
|
||||
- `ok`, derived from normalized outcome;
|
||||
- `evidence`, safe for compact State Hub reporting; and
|
||||
- direct-caller-only `tool_output` and `tool_error`.
|
||||
|
||||
Raw prompts, model output, tool output, and credential material are excluded
|
||||
from the State Hub detail.
|
||||
|
||||
## Rein lifecycle
|
||||
|
||||
```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."""
|
||||
) -> dict[str, str]: ...
|
||||
|
||||
@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."""
|
||||
) -> ToolResult: ...
|
||||
|
||||
@abstractmethod
|
||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
||||
"""Close the session. Returns a summary (commit sha, tokens, duration, outcome)."""
|
||||
def end_session(self, session: dict[str, str]) -> ExecutionSummary: ...
|
||||
```
|
||||
|
||||
- `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()
|
||||
```text
|
||||
Glas rein sand-boxer
|
||||
---- ---- ----------
|
||||
resolve profile + rein descriptor
|
||||
create sandbox ------------------------------------------> create
|
||||
start_session(profile, inputs, handle) -> rein setup
|
||||
dispatch_tool(session, call) -> inner loop
|
||||
end_session(session) -> normalized facts
|
||||
destroy sandbox -----------------------------------------> destroy
|
||||
publish compact ExecutionEvidence
|
||||
```
|
||||
|
||||
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`).
|
||||
There is no production default rein. Direct `Rein` injection remains a narrow
|
||||
library/test seam but still requires a valid profile so profile, sandbox,
|
||||
model, tool policy, and evidence are explicit.
|
||||
|
||||
## Actor attribution
|
||||
## Stability and extension rules
|
||||
|
||||
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.
|
||||
All contract models use Pydantic with unknown fields forbidden. Stable `1.0`
|
||||
fields are the declared fields in `src/glas_harness/contract.py`. Optional
|
||||
measurements such as tokens, duration, resolved model, commit, and artifacts
|
||||
remain `null` when unknown; unsupported tool-event visibility is
|
||||
`unavailable`, not an empty claim of completeness.
|
||||
|
||||
## Composable reins as middleware (deferred — sketch only, no code)
|
||||
Rein-specific, non-secret additions belong only in declared `metadata` fields.
|
||||
Adding a required field, changing a field meaning/type, or removing a field
|
||||
requires a new contract version. Adding an optional field may remain compatible
|
||||
only when old consumers can safely ignore it and every strict boundary model is
|
||||
updated together. A profile and rein descriptor must both declare the exact
|
||||
supported contract version.
|
||||
|
||||
`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).
|
||||
Sensitive values are never contract extensions. Profiles may contain
|
||||
credential-route references, while credential resolution and provider setup
|
||||
remain rein-local under ADR-002.
|
||||
|
||||
If a real second candidate appears, the shape would be:
|
||||
## Failure semantics
|
||||
|
||||
```python
|
||||
class Middleware(ABC):
|
||||
"""Wraps another Rein's dispatch_tool, does not replace start_session/end_session."""
|
||||
The gateway returns evidence for every normal refusal/failure path:
|
||||
|
||||
def __init__(self, inner: Rein) -> None:
|
||||
self.inner = inner
|
||||
- `resolution`: unknown, disabled, ambiguous, incompatible, or unsafe profile;
|
||||
- `sandbox_create`;
|
||||
- `session_start`;
|
||||
- `execution` (including a rein-declared unsuccessful result);
|
||||
- `session_end`; and
|
||||
- `teardown`.
|
||||
|
||||
@abstractmethod
|
||||
def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult:
|
||||
"""Call self.inner.dispatch_tool(...), observe/transform, return the result."""
|
||||
Profile resolution happens before sandbox creation. Once a sandbox exists,
|
||||
teardown is attempted on every path. `refused` means governed execution did not
|
||||
proceed; `failed` means an attempted lifecycle did not complete successfully.
|
||||
|
||||
def start_session(self, profile, inputs, sandbox) -> dict[str, str]:
|
||||
return self.inner.start_session(profile, inputs, sandbox)
|
||||
## Deliberate non-goals
|
||||
|
||||
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.
|
||||
The contract does not schedule work, source blueprints, allocate workers,
|
||||
resolve leadership, broker credentials, select an unapproved model by price, or
|
||||
standardize rein internals. Composable rein middleware remains deferred by
|
||||
ADR-004 until a second concrete need exists.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue