feat: add versioned execution profiles
This commit is contained in:
parent
641e85f5a8
commit
1cd890d871
34 changed files with 2087 additions and 471 deletions
11
AGENTS.md
11
AGENTS.md
|
|
@ -14,15 +14,18 @@
|
|||
## Developer Workflow
|
||||
|
||||
```bash
|
||||
# Install (editable, pulls in the sand-boxer sibling checkout)
|
||||
# Install core + sandbox and the concrete reins enabled on this host
|
||||
python3 -m venv .venv && source .venv/bin/activate
|
||||
pip install -e . -e ../sand-boxer pytest
|
||||
pip install -e . -e ../sand-boxer -e '../rein-aharness[llm]' -e ../rein-openweights pytest
|
||||
|
||||
# Test
|
||||
python3 -m pytest tests/ -q
|
||||
|
||||
# Run — resolves a sand-boxer sandbox, runs one task through a rein, verifies
|
||||
glas-harness run --sandbox-profile profile.bwrap-local \
|
||||
# Validate the executable profile/rein catalog
|
||||
glas-harness profiles
|
||||
|
||||
# Run — resolves the selected constellation, executes, verifies, and tears down
|
||||
glas-harness run --harness-profile harness.agent-dev-local@1.0.0 \
|
||||
--repo <path> --title "..." --description "..."
|
||||
```
|
||||
|
||||
|
|
|
|||
86
INTENT.md
86
INTENT.md
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
domain: infotech
|
||||
repo: glas-harness
|
||||
updated: "2026-06-22"
|
||||
updated: "2026-08-20"
|
||||
---
|
||||
|
||||
# INTENT
|
||||
|
|
@ -9,8 +9,8 @@ updated: "2026-06-22"
|
|||
> glas-harness is the Coulomb **meta-framework for agent harnesses** — a unified
|
||||
> API and extension platform for running agentic assistants with tools, memory,
|
||||
> channels, and subagent delegation, while **consuming** sand-boxer for isolated
|
||||
> execution. This file is preliminary; refine as the harness boundary is
|
||||
> implemented.
|
||||
> execution. The implemented nucleus is a versioned, profile-driven execution
|
||||
> router over concrete reins.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ Coulomb splits that stack deliberately:
|
|||
| **Code generation** — specs, generation, PR output | **snuggle-inventor** |
|
||||
|
||||
glas-harness exists to be the harness side of that split: **one consistent way to
|
||||
run agents**, with extensions for channels and tool backends, and a single
|
||||
execute assigned agent work**, with extensions for channels and tool backends, and a single
|
||||
integration path to sand-boxer when tools need isolation.
|
||||
|
||||
sand-boxer is OpenRouter for sandboxes. glas-harness is the parallel **harness
|
||||
|
|
@ -59,8 +59,8 @@ It answers:
|
|||
2. **What can the agent invoke?** Tool catalog, policies, elevated escape hatches.
|
||||
3. **What does the agent remember?** Memory, skills, identity documents, cron.
|
||||
4. **Where does the user interact?** Channel extensions (CLI, chat, email, …).
|
||||
5. **When do tools run in isolation?** Sandbox policy (`mode`, `scope`,
|
||||
`workspaceAccess`) — fulfilled by **sand-boxer**, not implemented here.
|
||||
5. **When do tools run in isolation?** A named sand-boxer profile selected by
|
||||
the Glas profile — fulfilled by **sand-boxer**, not implemented here.
|
||||
6. **How are subagents delegated?** Isolated sub-sessions with bounded context.
|
||||
7. **What happened?** Session and tool audit events to State Hub.
|
||||
|
||||
|
|
@ -68,6 +68,31 @@ It must **not** become the sandbox provisioner, the e2e validator, the code
|
|||
generator, the scheduler, work-state authority, tunnel/CA owner, or production
|
||||
service host on Railiance01.
|
||||
|
||||
## Relationship to leadership and workforce management
|
||||
|
||||
Leadership and workforce management govern responsibility: roles, duties,
|
||||
goals, assignments, resource envelopes, escalation, and who may initiate work.
|
||||
Glas governs execution after that decision. Its request can carry opaque
|
||||
`assignment_ref`, `role_ref`, `duty_ref`, `goal_refs`, and
|
||||
`resource_envelope_refs` so evidence remains attributable without duplicating
|
||||
the organizational records.
|
||||
|
||||
This separation is intentional:
|
||||
|
||||
```text
|
||||
leadership / workforce authority
|
||||
-> authorized assignment + organizational references
|
||||
-> Glas ExecutionRequest + explicit harness profile
|
||||
-> resolved rein/model/sandbox/tool-policy constellation
|
||||
-> normalized evidence linked back to the assignment
|
||||
```
|
||||
|
||||
Glas may refuse an unavailable or incompatible execution profile. It does not
|
||||
choose a leader, allocate a worker, reinterpret a duty, compile a role into a
|
||||
blueprint, or decide when work should run. Those decisions remain inspectable
|
||||
in their owning system while Glas supplies a stable, replaceable execution
|
||||
boundary.
|
||||
|
||||
---
|
||||
|
||||
## Coulomb sibling boundaries
|
||||
|
|
@ -148,7 +173,11 @@ glas-harness is a **meta-framework** with four pillars (preliminary):
|
|||
|
||||
### 1. Unified harness API
|
||||
|
||||
One surface for session lifecycle across channel and automation consumers:
|
||||
One versioned surface for session lifecycle across channel and automation
|
||||
consumers. The implemented gateway accepts an explicit `ExecutionRequest`,
|
||||
resolves a harness profile, executes one bounded rein session, and returns
|
||||
`ExecutionEvidence`. Longer-lived sessions and subagent join semantics remain
|
||||
future work.
|
||||
|
||||
- Start / resume / end sessions; subagent spawn and join
|
||||
- Tool dispatch with policy checks and audit metadata
|
||||
|
|
@ -160,14 +189,18 @@ human operators via CLI.
|
|||
|
||||
### 2. Harness profile catalog
|
||||
|
||||
Named, versioned **harness profiles** (distinct from sand-boxer sandbox profiles):
|
||||
Named, versioned **harness profiles** (distinct from sand-boxer sandbox profiles)
|
||||
are runtime inputs, not documentation. They currently bind:
|
||||
|
||||
- Default toolset and tool policy
|
||||
- Sandbox policy defaults (`mode`, `scope`, `workspaceAccess`) — OpenClaw-aligned
|
||||
- Memory and skills layout conventions
|
||||
- Channel allowlist
|
||||
- Model routing hints (consumer of `llm-connect`, not owner)
|
||||
- Registered in `registry/` via reuse-surface
|
||||
- concrete rein and required capabilities;
|
||||
- sand-boxer profile;
|
||||
- tool profile;
|
||||
- model provider, route, class, and explicit model identifier;
|
||||
- token, timeout, and turn limits; and
|
||||
- credential-route references without credential values.
|
||||
|
||||
Resolution is deterministic and fail-closed. Governed execution has no implicit
|
||||
rein, model, sandbox, or tool policy.
|
||||
|
||||
Example pairing:
|
||||
|
||||
|
|
@ -268,11 +301,11 @@ for integration design only.
|
|||
|
||||
---
|
||||
|
||||
## Sandbox consumption contract (preliminary)
|
||||
## Sandbox consumption contract
|
||||
|
||||
When harness policy requires isolation, glas-harness:
|
||||
|
||||
1. Resolves sandbox profile id (from harness profile or session override)
|
||||
1. Resolves the sandbox profile id from the versioned harness profile
|
||||
2. Calls sand-boxer `create` with `consumer: { harness: glas-harness, session_id, actor }`
|
||||
3. Stores `sandbox_id` and reachability descriptor on the session
|
||||
4. Routes `exec`, `read`, `write`, `edit`, and related tools through that handle
|
||||
|
|
@ -280,7 +313,7 @@ When harness policy requires isolation, glas-harness:
|
|||
|
||||
Harness owns **tool semantics**; sand-boxer owns **environment lifecycle**.
|
||||
|
||||
Open questions (for first workplan):
|
||||
Open integration questions:
|
||||
|
||||
- Does glas-harness proxy exec or delegate SSH/tunnel to the agent client?
|
||||
- How are mirror vs remote-canonical workspace modes exposed to tool implementations?
|
||||
|
|
@ -290,15 +323,16 @@ T08 lands.
|
|||
|
||||
---
|
||||
|
||||
## Near-term outcomes (preliminary)
|
||||
## Implemented nucleus and next outcomes
|
||||
|
||||
1. **This charter** — `INTENT.md` aligned with sand-boxer sibling boundaries
|
||||
2. **Harness profile schema sketch** — distinct from sand-boxer profile schema
|
||||
3. **sand-boxer integration doc** — consumer contract (may start in sand-boxer repo)
|
||||
4. **First harness profile** — `harness.agent-dev` paired with `profile.agent-dev`
|
||||
5. **CLI gateway stub** — minimal session + local tools (no channels yet)
|
||||
6. **Registry entry** — e.g. `capability.platform.agent-harness`
|
||||
7. **State Hub session events** — tool audit envelope
|
||||
Implemented: strict contract version `1.0`, executable profile catalog,
|
||||
rein-registry validation, explicit CLI/channel selection, sand-boxer lifecycle,
|
||||
two concrete rein constellations, and normalized State Hub evidence.
|
||||
|
||||
Next outcomes should be driven by real consumers: an additional channel,
|
||||
memory/skills integration, bounded subagent delegation, and an organizational
|
||||
assignment adapter. None should weaken explicit profile selection or move
|
||||
workforce authority into Glas.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -313,4 +347,4 @@ A mature glas-harness is Coulomb's **default agent runtime**:
|
|||
- Extensions add channels and tool backends without forking core gateway logic
|
||||
|
||||
The harness thinks and coordinates. sand-boxer establishes the box. wise-validator
|
||||
proves correctness. snuggle-inventor invents code. **glas-harness runs the agent.**
|
||||
proves correctness. snuggle-inventor invents code. **glas-harness runs the agent.**
|
||||
|
|
|
|||
58
README.md
58
README.md
|
|
@ -1 +1,57 @@
|
|||
Agent harness platform and meta framework.
|
||||
# glas-harness
|
||||
|
||||
`glas-harness` is the primary execution-layer abstraction for governed agent
|
||||
work. Consumers select a versioned Glas profile; Glas resolves the concrete
|
||||
rein, model route, sandbox profile, tool policy, and limits, runs the outer
|
||||
lifecycle, and returns one normalized evidence envelope.
|
||||
|
||||
The consumer contract does not name a Python rein class or provider CLI:
|
||||
|
||||
```text
|
||||
assignment / channel / activity
|
||||
-> ExecutionRequest(harness_profile_ref=...)
|
||||
-> Glas profile resolution
|
||||
-> rein + model + sandbox + tool policy
|
||||
-> normalized ExecutionEvidence
|
||||
```
|
||||
|
||||
## Profiles
|
||||
|
||||
List and validate every committed executable profile:
|
||||
|
||||
```bash
|
||||
glas-harness profiles
|
||||
glas-harness profiles --json
|
||||
```
|
||||
|
||||
Current constellations include:
|
||||
|
||||
- `harness.agent-dev-local@1.0.0`: `rein-aharness`, Claude Code model route,
|
||||
local bwrap sandbox.
|
||||
- `harness.agent-dev-openweights-local@1.0.0`: `rein-openweights`, OpenRouter
|
||||
open-weight route, local bwrap sandbox.
|
||||
- `harness.agent-dev@1.0.0`: `rein-aharness`, remote agent-dev sandbox.
|
||||
|
||||
Run a task through an explicit profile:
|
||||
|
||||
```bash
|
||||
glas-harness run \
|
||||
--harness-profile harness.agent-dev-local@1.0.0 \
|
||||
--repo /absolute/path/to/repo \
|
||||
--title "Bounded change" \
|
||||
--description "Make the requested change and commit it"
|
||||
```
|
||||
|
||||
There is no governed default rein, model, sandbox, or tool profile. Unknown,
|
||||
disabled, incompatible, and ambiguous selections are refused before sandbox
|
||||
creation.
|
||||
|
||||
See [SCOPE.md](SCOPE.md), [INTENT.md](INTENT.md),
|
||||
[docs/execution-profiles.md](docs/execution-profiles.md), and
|
||||
[docs/harness-contract.md](docs/harness-contract.md).
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
uv run pytest -q
|
||||
```
|
||||
|
|
|
|||
51
SCOPE.md
51
SCOPE.md
|
|
@ -2,21 +2,31 @@
|
|||
|
||||
## One-liner
|
||||
|
||||
Meta-framework routing between concrete agent-harness backends ("reins"),
|
||||
consuming sand-boxer for isolation. See `docs/adr/ADR-001-rein-harness-family.md`.
|
||||
Primary, profile-driven execution layer routing governed work between concrete
|
||||
agent-harness backends ("reins") while consuming sand-boxer for isolation. See
|
||||
`docs/adr/ADR-001-rein-harness-family.md`.
|
||||
|
||||
## Core Idea
|
||||
|
||||
glas-harness owns the harness contract (`docs/harness-contract.md`:
|
||||
`start_session`/`dispatch_tool`/`end_session`) and the outer loop — profile
|
||||
resolution, sandbox request/teardown via sand-boxer, actor attribution. A
|
||||
rein owns its own inner agentic loop and is invoked through that contract.
|
||||
resolution, rein/model/tool-policy selection, sandbox request/teardown via
|
||||
sand-boxer, actor attribution, and normalized evidence. A rein owns its own
|
||||
inner agentic loop and is invoked through that contract.
|
||||
|
||||
Workforce and leadership systems decide *who is responsible for what* and pass
|
||||
assignment, role, duty, goal, and resource-envelope references. Glas decides
|
||||
*how that already-authorized assignment executes*. It does not assign workers,
|
||||
elect leaders, decompose organizational goals, or schedule work.
|
||||
|
||||
## In Scope
|
||||
|
||||
- The `Rein` ABC and `SandboxHandle`/`ToolCall`/`ToolResult` types
|
||||
- Versioned strict execution request, profile, lifecycle, result, and evidence
|
||||
models plus the `Rein` ABC
|
||||
(`src/glas_harness/contract.py`)
|
||||
- The gateway that resolves a harness profile, requests a sand-boxer sandbox,
|
||||
- Deterministic harness-profile and rein-registry validation/resolution
|
||||
(`src/glas_harness/profiles.py`)
|
||||
- The gateway that resolves an explicit harness profile, requests a sandbox,
|
||||
runs one task through a rein, verifies, tears down
|
||||
(`src/glas_harness/gateway.py`)
|
||||
- Rein adapters that shell out to each rein's own CLI
|
||||
|
|
@ -24,29 +34,42 @@ rein owns its own inner agentic loop and is invoked through that contract.
|
|||
`rein_openweights.py`
|
||||
- The rein registry (`registry/reins/`) and harness profile catalog
|
||||
(`profiles/`)
|
||||
- Compact, non-secret execution evidence and attribution references for State
|
||||
Hub and other audit consumers
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Sandbox provisioning, profiles, placement — owned by `sand-boxer`
|
||||
- A rein's own agentic loop, tool execution, credential acquisition —
|
||||
owned by the rein itself (`rein-aharness`, `rein-openweights`, ...)
|
||||
- Scheduling/task sourcing — stays rein-local
|
||||
- Leadership, workforce allocation, role/duty/goal definitions, assignment
|
||||
authority, scheduling, and task sourcing — owned by their domain systems and
|
||||
passed to Glas as references
|
||||
- Scheduling/blueprint sourcing inside the execution layer — stays rein-local
|
||||
(`docs/adr/ADR-003-scheduling-and-blueprint-sourcing-stay-rein-local.md`)
|
||||
- Provider/model catalog authority and inference APIs — owned by providers and
|
||||
`llm-connect`; Glas carries the approved route and resolved identifier
|
||||
- Secrets and credential brokering — credentials remain rein-local and profiles
|
||||
contain route references only
|
||||
- e2e validation, code generation — owned by `wise-validator`,
|
||||
`snuggle-inventor`
|
||||
|
||||
## Current State
|
||||
|
||||
`GLAS-WP-0001` (harness contract + first two reins) is done. Both
|
||||
`rein-aharness` and `rein-openweights` are live-proven through the gateway
|
||||
against their real underlying providers (Claude Code CLI, OpenRouter).
|
||||
Not yet built: channel bridges, memory/skills layer, glas-harness's own
|
||||
State Hub reporting from the gateway (currently each rein reports its own
|
||||
events).
|
||||
The contract and first two reins are implemented. Profiles are executable,
|
||||
versioned runtime inputs; CLI and gateway invocation require an explicit
|
||||
profile; and both rein/model constellations are live-proven with real commits,
|
||||
sandbox cleanup, and the same evidence shape.
|
||||
The CLI channel and Glas-owned State Hub gateway reporting are implemented.
|
||||
|
||||
Still outside the current prototype: additional real channel extensions,
|
||||
memory/skills services, subagent delegation, requirements-based profile
|
||||
selection, and workforce-management APIs. Those are not implicit capabilities
|
||||
of the profile router.
|
||||
|
||||
## Getting Oriented
|
||||
|
||||
- Start with: `INTENT.md`, `docs/adr/ADR-001-rein-harness-family.md`
|
||||
- Contract: `docs/harness-contract.md`
|
||||
- Contract: `docs/harness-contract.md`, `docs/execution-profiles.md`
|
||||
- Agent instructions: `AGENTS.md`
|
||||
- Workplans: `workplans/`
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
| workplan | GLAS-WP-0001 | finished | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
|
||||
| workplan | GLAS-WP-0002 | finished | — | workplans/GLAS-WP-0002-observability-and-composability-followups.md |
|
||||
| workplan | GLAS-WP-0003 | finished | — | workplans/GLAS-WP-0003-first-channel-extension.md |
|
||||
| workplan | GLAS-WP-0004 | finished | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-0001-T01 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
|
||||
| task | GLAS-0001-T02 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
|
||||
| task | GLAS-0001-T03 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
|
||||
|
|
@ -29,3 +30,12 @@
|
|||
| task | GLAS-WP-0003-T02 | done | — | workplans/GLAS-WP-0003-first-channel-extension.md |
|
||||
| task | GLAS-WP-0003-T03 | done | — | workplans/GLAS-WP-0003-first-channel-extension.md |
|
||||
| task | GLAS-WP-0003-T04 | done | — | workplans/GLAS-WP-0003-first-channel-extension.md |
|
||||
| task | GLAS-WP-0004-T01 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-WP-0004-T02 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-WP-0004-T03 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-WP-0004-T04 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-WP-0004-T05 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-WP-0004-T06 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-WP-0004-T07 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| task | GLAS-WP-0004-T08 | done | — | workplans/GLAS-WP-0004-versioned-execution-constellation-profiles.md |
|
||||
| intake | GLAS-IN-0001 | done | blue | docs/intakes/residuals.md |
|
||||
|
|
|
|||
|
|
@ -1,76 +1,34 @@
|
|||
# Channel contract
|
||||
|
||||
How an external invocation reaches the gateway, per `GLAS-WP-0003`.
|
||||
Mirrors `docs/harness-contract.md`'s `Rein` at a different layer: `Rein`
|
||||
governs how a session runs once glas-harness has decided to run one; a
|
||||
`Channel` governs how an external caller (CLI today; a future Slack
|
||||
message, HTTP request, or scheduled trigger) gets translated into that
|
||||
decision in the first place, and how the result gets translated back.
|
||||
|
||||
## Interface
|
||||
A channel translates an external invocation into profile-driven gateway input;
|
||||
it does not select a rein class or execute a provider itself.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class GatewayInvocation:
|
||||
"""Everything run_task_through_rein needs, channel-agnostic."""
|
||||
sandbox_profile: str
|
||||
harness_profile: str
|
||||
repo: str
|
||||
title: str
|
||||
description: str
|
||||
actor: str = "agt"
|
||||
project: str = "glas-harness"
|
||||
report_to_hub: bool = True
|
||||
|
||||
|
||||
class Channel(ABC):
|
||||
"""Base class for concrete invocation channels (CLI, Slack, HTTP, ...)."""
|
||||
|
||||
@abstractmethod
|
||||
def parse_invocation(self, raw: Any) -> GatewayInvocation:
|
||||
"""Turn a channel-specific invocation into a GatewayInvocation."""
|
||||
|
||||
@abstractmethod
|
||||
def render_result(self, result: dict[str, Any]) -> Any:
|
||||
"""Turn a gateway result dict into whatever this channel returns."""
|
||||
```
|
||||
|
||||
`src/glas_harness/channels/base.py`. A channel does not choose the rein
|
||||
or run the gateway itself — `cli.py`'s `main()` still does that, calling
|
||||
`parse_invocation`/`render_result` around the existing
|
||||
`run_task_through_rein` call. Keeping channel construction and gateway
|
||||
invocation separate (rather than folding gateway-calling into the
|
||||
`Channel` ABC itself) is what lets `CLIChannel` be a pure
|
||||
input/output mapper, easy to unit test without touching sand-boxer or a
|
||||
rein at all.
|
||||
`CLIChannel` is the current implementation. The CLI requires
|
||||
`--harness-profile <id[@version]>`; the old sandbox-only input is removed
|
||||
because it could not identify the rein, model route, tool policy, or contract
|
||||
revision. Channels may obtain a profile choice from their own approved
|
||||
configuration, but must pass it explicitly and must surface resolution refusal
|
||||
to their caller.
|
||||
|
||||
## CLIChannel — the first, and so far only, implementation
|
||||
The channel renders the full direct `GatewayResult`. State Hub receives only
|
||||
the compact `ExecutionEvidence` subset.
|
||||
|
||||
`src/glas_harness/channels/cli_channel.py`. Maps an argparse
|
||||
`Namespace` to `GatewayInvocation` (one field each, `--no-hub` inverts
|
||||
to `report_to_hub`) and renders a result dict as
|
||||
`json.dumps(result, indent=2)` — exactly what `cli.py` did inline before
|
||||
this existed. This was a refactor, not a behavior change: verified with
|
||||
a real live run through `python3 -m glas_harness.cli run ...` producing
|
||||
byte-identical output shape to the pre-refactor version, plus unit tests
|
||||
for the mapping in isolation (`tests/test_cli_channel.py`).
|
||||
## Adding a channel
|
||||
|
||||
## Adding a second channel (not done — pattern only)
|
||||
|
||||
No second channel exists yet, and none should be built speculatively —
|
||||
same discipline as ADR-002/ADR-004 (generalize from a real second
|
||||
example, not in anticipation of one). If/when one is needed, e.g. a
|
||||
Slack channel:
|
||||
|
||||
1. Implement `Channel` in `src/glas_harness/channels/slack_channel.py`:
|
||||
`parse_invocation` turns a Slack event payload into a
|
||||
`GatewayInvocation` (message text → `title`/`description`, a
|
||||
configured default `sandbox_profile`, the Slack user id as `actor`
|
||||
or `project`); `render_result` turns the result dict into a Slack
|
||||
message (success/failure, commit sha if present).
|
||||
2. Whatever process receives Slack events constructs the channel and
|
||||
calls `run_task_through_rein` the same way `cli.py` does — no change
|
||||
needed to `gateway.py`, `contract.py`, or any rein.
|
||||
3. If two real channels turn out to share invocation-parsing logic (e.g.
|
||||
both need the same `sandbox_profile` defaulting rule), factor that
|
||||
shared piece out then — not before, and not into the `Channel` ABC
|
||||
itself unless every channel needs it.
|
||||
Implement `parse_invocation` and `render_result`, then call `run_execution`
|
||||
with an `ExecutionRequest`. Carry any assignment/role/duty/goal/resource
|
||||
references supplied by the upstream workforce system. Do not duplicate profile
|
||||
resolution, sandbox coordination, leadership decisions, or rein construction in
|
||||
the channel.
|
||||
|
|
|
|||
62
docs/evidence/GLAS-WP-0004-live-proof-2026-08-20.md
Normal file
62
docs/evidence/GLAS-WP-0004-live-proof-2026-08-20.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# GLAS-WP-0004 live profile evidence — 2026-08-20
|
||||
|
||||
Acceptance task for both profiles: change one `README.md` status value from
|
||||
`pending` to `complete`, make no other content change, and commit it. Each run
|
||||
used a separate temporary initialized repository and `profile.bwrap-local`.
|
||||
No credential value, prompt transcript, or raw model output is recorded here.
|
||||
|
||||
## `harness.agent-dev-local@1.0.0`
|
||||
|
||||
- Result: succeeded.
|
||||
- Contract: `1.0`.
|
||||
- Rein: `rein-aharness@0.1.0`.
|
||||
- Model route/resolved model: `claude-code-cli` / `claude-sonnet-4-6`.
|
||||
- Tool profile: `green-commit-only`.
|
||||
- Sandbox ID: `e61624cb`; local lifecycle store verified `destroyed`.
|
||||
- Request ID: `96b2ec85-3823-4088-bcf4-a008c02e2354`.
|
||||
- Commit: `795f8ef8b414f4470e812005f4103b8c5f126829`.
|
||||
- Tool-event visibility: complete, 14 events.
|
||||
- Reported execution time: 29.051 seconds.
|
||||
|
||||
The first attempt also proved normalized dependency/startup failure and
|
||||
sandbox cleanup: the acceptance environment lacked rein-aharness's optional
|
||||
`llm-connect` install. After installing the documented sibling dependency, the
|
||||
same profile and task succeeded. Production packaging must install the concrete
|
||||
rein with its execution dependencies.
|
||||
|
||||
## `harness.agent-dev-openweights-local@1.0.0`
|
||||
|
||||
- Result: succeeded on the 2026-08-21 local-time recheck after credential repair.
|
||||
- Contract: `1.0`.
|
||||
- Rein: `rein-openweights@0.1.0`.
|
||||
- Model route/resolved model: `rein-openweights-openrouter` /
|
||||
`qwen/qwen-2.5-72b-instruct`.
|
||||
- Tool profile: `green-commit-only`.
|
||||
- Sandbox ID: `220482bc`; local lifecycle store verified `destroyed`.
|
||||
- Request ID: `b783af7f-e728-4dee-9034-f1c2262aa9ea`.
|
||||
- Commit: `b0600b25066731c6e1fc458409429f76a844f959`.
|
||||
- Turns/tokens: 3 / 3,497 of the 60,000-token profile budget.
|
||||
- Tool-event visibility: unavailable, reported explicitly rather than as an
|
||||
empty claim of completeness.
|
||||
- Reported execution time: 300.095 seconds.
|
||||
|
||||
The recheck exposed two credential-selection facts without revealing a value:
|
||||
an inherited `OPENROUTER_API_KEY` still contained the retired key and therefore
|
||||
masked the repaired workload lane, while the AppRole files existed at the
|
||||
documented standard directory but code required an environment variable to use
|
||||
them. The successful run removed the stale ambient override and selected the
|
||||
approved AppRole directory. `rein-openweights` now defaults to
|
||||
`~/.local/rein-openweights/approle`; explicit environment credentials retain
|
||||
development precedence and are documented as needing refresh/removal after
|
||||
rotation.
|
||||
|
||||
## Acceptance status
|
||||
|
||||
Both explicit profiles resolved and dispatched distinct rein/model
|
||||
constellations, completed the same semantic task with a real commit, returned
|
||||
the common Glas evidence envelope, and destroyed their local sandboxes. No
|
||||
alternative workload credential was borrowed.
|
||||
|
||||
Rollback for consumers is to repin to the last approved Glas profile. The old
|
||||
`--sandbox-profile` governed path and implicit `ReinAharness()` default are not
|
||||
restored.
|
||||
87
docs/execution-profiles.md
Normal file
87
docs/execution-profiles.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Execution profiles and workforce handoff
|
||||
|
||||
Glas profiles are versioned execution constellations. They make the execution
|
||||
layer replaceable without changing an upstream workforce assignment.
|
||||
|
||||
```text
|
||||
RoleAssignment / activity / channel
|
||||
| organizational refs + bounded task + harness_profile_ref
|
||||
v
|
||||
ExecutionRequest
|
||||
| deterministic profile validation
|
||||
v
|
||||
rein + model route + sandbox profile + tool profile + limits
|
||||
| one outer lifecycle
|
||||
v
|
||||
ExecutionEvidence -> assignment/audit consumers
|
||||
```
|
||||
|
||||
## Profile schema
|
||||
|
||||
Each YAML document under `profiles/` declares:
|
||||
|
||||
- stable `id`, semantic `version`, exact `contract_version`, and status;
|
||||
- rein registry ID and required capabilities;
|
||||
- sand-boxer `sandbox_profile`;
|
||||
- rein-enforced `tool_profile`;
|
||||
- model provider, explicit model identifier, model class, and route;
|
||||
- positive token/timeout/turn limits when applicable; and
|
||||
- credential route references and non-secret metadata.
|
||||
|
||||
The catalog discovers files deterministically, forbids unknown schema fields,
|
||||
rejects duplicate revisions, and checks the selected profile against
|
||||
`registry/reins/*.yaml`. Unpinned lookup is accepted only when exactly one
|
||||
enabled revision exists. Inline secrets and token-looking values are refused.
|
||||
|
||||
Use `glas-harness profiles` as the catalog/packaging validation command.
|
||||
|
||||
Catalog validation proves schema/compatibility, not host installation. An
|
||||
execution host must install `glas-harness` with sand-boxer support and each rein
|
||||
used by its enabled profiles. The Claude-backed rein also needs its documented
|
||||
`rein-aharness[llm]`/sibling `llm-connect` adapter dependency. Missing runtime
|
||||
dependencies surface as startup/execution failure evidence and still trigger
|
||||
sandbox teardown.
|
||||
|
||||
## Consumer request
|
||||
|
||||
See `examples/execution-request.json`. Consumers know only a profile reference,
|
||||
not `ReinAharness`, `ReinOpenWeights`, Claude Code arguments, or OpenRouter
|
||||
client details. An assignment system may set the organizational references; a
|
||||
channel may omit fields it does not own.
|
||||
|
||||
If the requested profile, contract, rein, capability, resource envelope, or
|
||||
credential route is unavailable, the caller must receive a refusal/failure. No
|
||||
consumer may silently substitute a different rein or model constellation.
|
||||
|
||||
The current exact-profile path does not interpret resource envelopes or choose
|
||||
profiles from requirements. A future requirements resolver belongs at this
|
||||
Glas boundary, must be deterministic and explainable, and must refuse ambiguity.
|
||||
|
||||
## Two equivalent selections
|
||||
|
||||
For the same task and organizational references, switch only:
|
||||
|
||||
```text
|
||||
harness.agent-dev-local@1.0.0
|
||||
-> rein-aharness / claude-sonnet-4-6 / profile.bwrap-local
|
||||
|
||||
harness.agent-dev-openweights-local@1.0.0
|
||||
-> rein-openweights / qwen/qwen-2.5-72b-instruct / profile.bwrap-local
|
||||
```
|
||||
|
||||
Both use `green-commit-only` and return `GatewayResult` / `ExecutionEvidence`.
|
||||
This proves interchangeability at the Glas boundary; it does not claim the
|
||||
models have identical quality, price, or provider behavior.
|
||||
|
||||
## Ownership handoff
|
||||
|
||||
- Agentic-resources/KaizenAgentic: leadership, role, duty, goal, resource
|
||||
envelope, worker/agent instance, and assignment definitions.
|
||||
- Activity-core or a channel: when work is invoked and the bounded task input.
|
||||
- Glas: versioned execution selection, lifecycle, attribution, and evidence.
|
||||
- Rein: inner loop, backend-specific policy enforcement and credentials.
|
||||
- llm-connect/provider: inference/provider boundary and model capabilities.
|
||||
- Sand-boxer: sandbox profiles, placement, provisioning, and teardown.
|
||||
|
||||
Rollback is selection-based: repin a caller to its last approved Glas profile.
|
||||
Do not bypass Glas with `--sandbox-profile` or add a hidden default rein.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
38
docs/intakes/residuals.md
Normal file
38
docs/intakes/residuals.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# glas-harness residual intakes
|
||||
|
||||
## GLAS-IN-0001 — Restore open-weight live-profile credential lane
|
||||
|
||||
```yaml
|
||||
id: GLAS-IN-0001
|
||||
kind: intake
|
||||
title: "Restore the rein-openweights OpenRouter lane and close the dual-profile live proof"
|
||||
lane: blue
|
||||
status: done
|
||||
priority: high
|
||||
owner: ops-mason
|
||||
repo: glas-harness
|
||||
origin: residual
|
||||
origin_ref: GLAS-WP-0004
|
||||
description: |
|
||||
On 2026-08-20, harness.agent-dev-openweights-local@1.0.0 resolved and
|
||||
dispatched the same bounded fixture used by the successful rein-aharness
|
||||
profile. The existing rein-local credential reached OpenRouter but the
|
||||
provider returned HTTP 401 User not found before a model turn. Glas returned
|
||||
normalized, redacted failure evidence and tore down sandbox bc9dec6a.
|
||||
|
||||
Route catalog id: rein-openweights-openrouter-approle. Credential values must
|
||||
not be placed in this record, State Hub, Git, or chat.
|
||||
|
||||
The owning credential operator should rotate or repair that exact workload
|
||||
lane, verify field presence without disclosure, and notify glas-harness.
|
||||
Then rerun the bounded acceptance task through
|
||||
harness.agent-dev-openweights-local@1.0.0, record a real commit plus cleanup
|
||||
evidence, and mark GLAS-WP-0004-T07 done. Do not borrow llm-connect's
|
||||
separate OpenRouter credential.
|
||||
|
||||
Completed 2026-08-21. The repaired lane authenticated after removing a stale
|
||||
inherited OPENROUTER_API_KEY override and selecting the standard AppRole
|
||||
directory. The profile completed in 3 turns, spent 3,497 tokens, produced
|
||||
commit b0600b25066731c6e1fc458409429f76a844f959, and sandbox 220482bc was
|
||||
verified destroyed. The AppRole directory is now the code default.
|
||||
```
|
||||
21
examples/execution-request.json
Normal file
21
examples/execution-request.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"harness_profile_ref": "harness.agent-dev-local@1.0.0",
|
||||
"repo": "/absolute/path/to/target-repo",
|
||||
"title": "Implement the bounded assignment",
|
||||
"description": "Apply the approved change, run focused checks, and commit the result.",
|
||||
"actor": "agt",
|
||||
"project": "example-domain",
|
||||
"request_id": "execution-018f",
|
||||
"correlation_id": "activity-42",
|
||||
"assignment_ref": "role-assignment:agent-7:42",
|
||||
"role_ref": "role:maintainer@3",
|
||||
"duty_ref": "duty:bounded-change@2",
|
||||
"goal_refs": [
|
||||
"goal:reliable-delivery@5"
|
||||
],
|
||||
"resource_envelope_refs": [
|
||||
"resource-envelope:standard-coding@1"
|
||||
],
|
||||
"expected_output": "A verified commit and normalized execution evidence",
|
||||
"report_to_hub": true
|
||||
}
|
||||
|
|
@ -1,10 +1,26 @@
|
|||
id: harness.agent-dev-local
|
||||
version: "1.0.0"
|
||||
rein: rein-aharness
|
||||
contract_version: "1.0"
|
||||
status: enabled
|
||||
rein:
|
||||
id: rein-aharness
|
||||
required_capabilities:
|
||||
session_style: unattended
|
||||
model_class: frontier
|
||||
sandbox_profile: profile.bwrap-local
|
||||
tool_profile: green-commit-only
|
||||
model:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4-6
|
||||
model_class: frontier
|
||||
route: claude-code-cli
|
||||
limits:
|
||||
budget_tokens: 60000
|
||||
timeout_seconds: 900
|
||||
credential_route_refs: []
|
||||
metadata:
|
||||
latency_class: fast
|
||||
stream_tool_events: true
|
||||
# Pairs with sand-boxer's profile.bwrap-local (ext.bwrap, same-host,
|
||||
# no SSH hop) — for CI/local dev where the compose-ssh remote-host
|
||||
# round trip is unnecessary overhead.
|
||||
|
|
|
|||
25
profiles/harness.agent-dev-openweights-local.yaml
Normal file
25
profiles/harness.agent-dev-openweights-local.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
id: harness.agent-dev-openweights-local
|
||||
version: "1.0.0"
|
||||
contract_version: "1.0"
|
||||
status: enabled
|
||||
rein:
|
||||
id: rein-openweights
|
||||
required_capabilities:
|
||||
session_style: unattended
|
||||
model_class: open-weight
|
||||
sandbox_profile: profile.bwrap-local
|
||||
tool_profile: green-commit-only
|
||||
model:
|
||||
provider: openrouter
|
||||
model: qwen/qwen-2.5-72b-instruct
|
||||
model_class: open-weight
|
||||
route: rein-openweights-openrouter
|
||||
limits:
|
||||
budget_tokens: 60000
|
||||
timeout_seconds: 900
|
||||
max_turns: 20
|
||||
credential_route_refs:
|
||||
- openrouter-api-key
|
||||
metadata:
|
||||
latency_class: variable
|
||||
live_proof_model: qwen/qwen-2.5-72b-instruct
|
||||
|
|
@ -1,10 +1,26 @@
|
|||
id: harness.agent-dev
|
||||
version: "1.0.0"
|
||||
rein: rein-aharness
|
||||
contract_version: "1.0"
|
||||
status: enabled
|
||||
rein:
|
||||
id: rein-aharness
|
||||
required_capabilities:
|
||||
session_style: unattended
|
||||
model_class: frontier
|
||||
sandbox_profile: profile.agent-dev
|
||||
tool_profile: green-commit-only
|
||||
model:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4-6
|
||||
model_class: frontier
|
||||
route: claude-code-cli
|
||||
limits:
|
||||
budget_tokens: 60000
|
||||
timeout_seconds: 900
|
||||
credential_route_refs: []
|
||||
metadata:
|
||||
latency_class: standard
|
||||
stream_tool_events: true
|
||||
# Pairs with sand-boxer's profile.agent-dev (ext.compose-ssh, remote
|
||||
# host over SSH). Use harness.agent-dev-local for fast local iteration
|
||||
# without the SSH hop.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ dev = ["pytest>=8"]
|
|||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/glas_harness"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
"profiles" = "glas_harness/data/profiles"
|
||||
"registry/reins" = "glas_harness/data/reins"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
Markdown-first capability index for federation and reuse planning.
|
||||
|
||||
`registry/reins/*.yaml` is also the executable rein registry consumed by
|
||||
`ProfileCatalog`. Each descriptor has a semantic rein version, supported Glas
|
||||
contract versions, a restricted in-package handler, capabilities, and status.
|
||||
A profile is executable only when its rein is implemented, supports the exact
|
||||
contract version, and satisfies every required capability.
|
||||
|
||||
Validate the rein registry together with all harness profiles using:
|
||||
|
||||
```bash
|
||||
glas-harness profiles --json
|
||||
```
|
||||
|
||||
## Authoring
|
||||
|
||||
1. Copy a capability entry template (see reuse-surface `templates/capability-entry.template.md`).
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
id: rein-aharness
|
||||
version: "0.1.0"
|
||||
title: Claude Code CLI rein
|
||||
description: >
|
||||
Governed, unattended/scheduled harness driving Claude Code CLI sessions.
|
||||
Renamed from agent-harness (ADR-001). Deployed on Railiance for tenant
|
||||
binky-control.
|
||||
handler: glas_harness.reins.rein_aharness:ReinAharness
|
||||
contract_versions: ["1.0"]
|
||||
capabilities:
|
||||
session_style: unattended
|
||||
model_class: frontier
|
||||
credential_source: rein-local
|
||||
# rein-local: kaizen-agentic (blueprints), activity-core/issue-core
|
||||
# (intake), OpenBao/ops-warden (credentials) — see
|
||||
# rein-aharness/workplans/HARNESS-WP-0002-T04 for whether this stays
|
||||
# rein-local or moves into glas-harness.
|
||||
tool_event_visibility: complete
|
||||
status: implemented
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
id: rein-openweights
|
||||
version: "0.1.0"
|
||||
title: OpenRouter open-weight agentic loop
|
||||
description: >
|
||||
Agentic tool-use harness driving current open-weight models via
|
||||
|
|
@ -6,11 +7,10 @@ description: >
|
|||
rein-openweights/workplans/REIN-OW-WP-0001-T01), as an alternative to
|
||||
frontier-vendor CLIs. Chartered by ADR-001.
|
||||
handler: glas_harness.reins.rein_openweights:ReinOpenWeights
|
||||
contract_versions: ["1.0"]
|
||||
capabilities:
|
||||
session_style: unattended
|
||||
model_class: open-weight
|
||||
credential_source: rein-local
|
||||
# rein-local per ADR-002 (Option B): rein-openweights acquires its own
|
||||
# OpenRouter credential (OpenBao, or OPENROUTER_API_KEY env var).
|
||||
# glas-harness does not broker it.
|
||||
tool_event_visibility: unavailable
|
||||
status: implemented
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from typing import Any
|
|||
class GatewayInvocation:
|
||||
"""Everything run_task_through_rein needs, channel-agnostic."""
|
||||
|
||||
sandbox_profile: str
|
||||
harness_profile: str
|
||||
repo: str
|
||||
title: str
|
||||
description: str
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from glas_harness.channels.base import Channel, GatewayInvocation
|
|||
class CLIChannel(Channel):
|
||||
def parse_invocation(self, raw: argparse.Namespace) -> GatewayInvocation:
|
||||
return GatewayInvocation(
|
||||
sandbox_profile=raw.sandbox_profile,
|
||||
harness_profile=raw.harness_profile,
|
||||
repo=raw.repo,
|
||||
title=raw.title,
|
||||
description=raw.description,
|
||||
|
|
|
|||
|
|
@ -10,8 +10,14 @@ def main(argv: list[str] | None = None) -> int:
|
|||
parser = argparse.ArgumentParser(prog="glas-harness")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
run = sub.add_parser("run", help="Run one task through a rein inside a sand-boxer sandbox")
|
||||
run.add_argument("--sandbox-profile", required=True, help="e.g. profile.bwrap-local")
|
||||
run = sub.add_parser(
|
||||
"run", help="Run one task through a versioned Glas execution profile"
|
||||
)
|
||||
run.add_argument(
|
||||
"--harness-profile",
|
||||
required=True,
|
||||
help="Explicit profile id[@version], e.g. harness.agent-dev-local@1.0.0",
|
||||
)
|
||||
run.add_argument("--repo", required=True, help="Local repo path to mirror into the sandbox")
|
||||
run.add_argument("--title", required=True)
|
||||
run.add_argument("--description", required=True)
|
||||
|
|
@ -19,25 +25,56 @@ def main(argv: list[str] | None = None) -> int:
|
|||
run.add_argument("--project", default="glas-harness")
|
||||
run.add_argument("--no-hub", action="store_true", help="Skip the gateway's own hub reporting")
|
||||
|
||||
profiles = sub.add_parser("profiles", help="Validate and list executable Glas profiles")
|
||||
profiles.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.command == "run":
|
||||
from glas_harness.channels.cli_channel import CLIChannel
|
||||
from glas_harness.gateway import run_task_through_rein
|
||||
from glas_harness.contract import ExecutionRequest
|
||||
from glas_harness.gateway import run_execution
|
||||
|
||||
channel = CLIChannel()
|
||||
invocation = channel.parse_invocation(args)
|
||||
result = run_task_through_rein(
|
||||
sandbox_profile=invocation.sandbox_profile,
|
||||
repo=invocation.repo,
|
||||
title=invocation.title,
|
||||
description=invocation.description,
|
||||
actor=invocation.actor,
|
||||
project=invocation.project,
|
||||
report_to_hub=invocation.report_to_hub,
|
||||
result = run_execution(
|
||||
ExecutionRequest(
|
||||
harness_profile_ref=invocation.harness_profile,
|
||||
repo=invocation.repo,
|
||||
title=invocation.title,
|
||||
description=invocation.description,
|
||||
actor=invocation.actor,
|
||||
project=invocation.project,
|
||||
report_to_hub=invocation.report_to_hub,
|
||||
)
|
||||
)
|
||||
print(channel.render_result(result))
|
||||
return 0 if result["tool_ok"] else 1
|
||||
print(channel.render_result(result.model_dump(mode="json")))
|
||||
return 0 if result.ok else 1
|
||||
|
||||
if args.command == "profiles":
|
||||
import json
|
||||
|
||||
from glas_harness.profiles import ProfileCatalog, ProfileError
|
||||
|
||||
try:
|
||||
rows = [
|
||||
context.model_dump(mode="json")
|
||||
for context in ProfileCatalog().validate_all()
|
||||
]
|
||||
except ProfileError as exc:
|
||||
print(f"invalid profile catalog: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
if args.json:
|
||||
print(json.dumps(rows, indent=2))
|
||||
else:
|
||||
for row in rows:
|
||||
print(
|
||||
f"{row['profile']['id']}@{row['profile']['version']}\t"
|
||||
f"rein={row['rein_id']}@{row['rein_version']}\t"
|
||||
f"model={row['model']['model']}\t"
|
||||
f"sandbox={row['sandbox_profile']}"
|
||||
)
|
||||
return 0
|
||||
|
||||
return 1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,47 +1,202 @@
|
|||
"""The harness contract a concrete rein implements.
|
||||
"""Versioned contract between glas-harness and concrete reins.
|
||||
|
||||
See docs/harness-contract.md for the full session-lifecycle diagram and
|
||||
GLAS-WP-0001-T01. glas-harness owns the outer loop (profile resolution,
|
||||
sandbox request/teardown via sand-boxer, State Hub reporting, actor
|
||||
attribution); a rein owns the inner agentic loop.
|
||||
Glas owns the outer lifecycle and the stable boundary models. A rein owns its
|
||||
inner agentic loop. Contract models reject unknown fields so a profile or
|
||||
backend cannot silently widen the execution surface.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
CONTRACT_VERSION = "1.0"
|
||||
_SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class SandboxHandle:
|
||||
"""What glas-harness got back from sand-boxer's create()."""
|
||||
class ContractModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class HarnessProfileRef(ContractModel):
|
||||
id: str = Field(pattern=r"^harness\.[a-z0-9][a-z0-9._-]*$")
|
||||
version: str
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def validate_version(cls, value: str) -> str:
|
||||
if not _SEMVER.match(value):
|
||||
raise ValueError("profile version must be semantic versioning")
|
||||
return value
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.id}@{self.version}"
|
||||
|
||||
|
||||
class ModelRoute(ContractModel):
|
||||
provider: str = Field(min_length=1)
|
||||
model: str = Field(min_length=1)
|
||||
model_class: Literal["frontier", "open-weight", "specialized", "other"]
|
||||
route: str = Field(default="rein-native", min_length=1)
|
||||
|
||||
|
||||
class ExecutionLimits(ContractModel):
|
||||
budget_tokens: int | None = Field(default=None, gt=0)
|
||||
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||
max_turns: int | None = Field(default=None, gt=0)
|
||||
|
||||
|
||||
class ReinSelection(ContractModel):
|
||||
id: str = Field(pattern=r"^rein-[a-z0-9][a-z0-9-]*$")
|
||||
required_capabilities: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class HarnessProfile(ContractModel):
|
||||
id: str = Field(pattern=r"^harness\.[a-z0-9][a-z0-9._-]*$")
|
||||
version: str
|
||||
contract_version: str
|
||||
status: Literal["enabled", "disabled"] = "enabled"
|
||||
rein: ReinSelection
|
||||
sandbox_profile: str = Field(pattern=r"^profile\.[a-z0-9][a-z0-9._-]*$")
|
||||
tool_profile: str = Field(min_length=1)
|
||||
model: ModelRoute
|
||||
limits: ExecutionLimits = Field(default_factory=ExecutionLimits)
|
||||
credential_route_refs: list[str] = Field(default_factory=list)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def validate_version(cls, value: str) -> str:
|
||||
return HarnessProfileRef.validate_version(value)
|
||||
|
||||
@property
|
||||
def ref(self) -> HarnessProfileRef:
|
||||
return HarnessProfileRef(id=self.id, version=self.version)
|
||||
|
||||
|
||||
class ReinDescriptor(ContractModel):
|
||||
id: str = Field(pattern=r"^rein-[a-z0-9][a-z0-9-]*$")
|
||||
version: str
|
||||
title: str
|
||||
description: str = ""
|
||||
handler: str = Field(pattern=r"^glas_harness\.reins\.[A-Za-z0-9_]+:[A-Za-z0-9_]+$")
|
||||
contract_versions: list[str] = Field(min_length=1)
|
||||
capabilities: dict[str, Any] = Field(default_factory=dict)
|
||||
status: Literal["implemented", "disabled", "experimental"] = "implemented"
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def validate_version(cls, value: str) -> str:
|
||||
return HarnessProfileRef.validate_version(value)
|
||||
|
||||
|
||||
class SandboxHandle(ContractModel):
|
||||
sandbox_id: str
|
||||
host: str
|
||||
reachability: dict[str, Any] = field(default_factory=dict)
|
||||
reachability: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
class ToolCall(ContractModel):
|
||||
name: str
|
||||
args: dict[str, Any] = field(default_factory=dict)
|
||||
args: dict[str, Any] = Field(default_factory=dict)
|
||||
actor: str = "agt"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolResult:
|
||||
class ToolResult(ContractModel):
|
||||
ok: bool
|
||||
output: str = ""
|
||||
error: str | None = None
|
||||
# Per-tool-call audit trail, populated when the rein can observe its own
|
||||
# inner loop's individual tool invocations in real time (e.g. parsing
|
||||
# `claude --output-format stream-json`). Empty when a rein only exposes
|
||||
# its whole task as one opaque call — that's a real limit of CLI-wrapped
|
||||
# agents, not a shortcut: neither Claude Code nor rein-openweights's own
|
||||
# loop lets glas-harness externally execute individual tool calls, only
|
||||
# observe them. See HARNESS-WP-0002-T03.
|
||||
events: list[dict[str, Any]] = field(default_factory=list)
|
||||
events: list[dict[str, Any]] = Field(default_factory=list)
|
||||
events_completeness: Literal["complete", "partial", "unavailable"] = "unavailable"
|
||||
tokens_spent: int | None = Field(default=None, ge=0)
|
||||
duration_s: float | None = Field(default=None, ge=0)
|
||||
resolved_model: str | None = None
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ExecutionSummary(ContractModel):
|
||||
committed: bool
|
||||
commit_sha: str | None = None
|
||||
outcome: Literal["succeeded", "failed", "refused"]
|
||||
reason: str | None = None
|
||||
tokens_spent: int | None = Field(default=None, ge=0)
|
||||
duration_s: float | None = Field(default=None, ge=0)
|
||||
resolved_model: str | None = None
|
||||
artifacts: list[str] = Field(default_factory=list)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ExecutionRequest(ContractModel):
|
||||
harness_profile_ref: str = Field(min_length=1)
|
||||
repo: str = Field(min_length=1)
|
||||
title: str = Field(min_length=1)
|
||||
description: str
|
||||
actor: str = "agt"
|
||||
project: str = "glas-harness"
|
||||
request_id: str | None = None
|
||||
correlation_id: str | None = None
|
||||
assignment_ref: str | None = None
|
||||
role_ref: str | None = None
|
||||
duty_ref: str | None = None
|
||||
goal_refs: list[str] = Field(default_factory=list)
|
||||
resource_envelope_refs: list[str] = Field(default_factory=list)
|
||||
expected_output: str | None = None
|
||||
report_to_hub: bool = True
|
||||
|
||||
|
||||
class ResolvedExecutionContext(ContractModel):
|
||||
contract_version: str
|
||||
profile: HarnessProfileRef
|
||||
rein_id: str
|
||||
rein_version: str
|
||||
sandbox_profile: str
|
||||
tool_profile: str
|
||||
model: ModelRoute
|
||||
limits: ExecutionLimits
|
||||
|
||||
|
||||
class ExecutionEvidence(ContractModel):
|
||||
request_id: str
|
||||
correlation_id: str | None = None
|
||||
actor: str
|
||||
project: str
|
||||
target_repo: str
|
||||
contract_version: str = CONTRACT_VERSION
|
||||
profile_ref: str | None = None
|
||||
rein_id: str | None = None
|
||||
rein_version: str | None = None
|
||||
model_route: str | None = None
|
||||
resolved_model: str | None = None
|
||||
sandbox_profile: str | None = None
|
||||
sandbox_id: str | None = None
|
||||
tool_profile: str | None = None
|
||||
outcome: Literal["succeeded", "failed", "refused"]
|
||||
failure_stage: Literal[
|
||||
"resolution", "sandbox_create", "session_start", "execution", "session_end", "teardown"
|
||||
] | None = None
|
||||
error: str | None = None
|
||||
started_at: str
|
||||
finished_at: str
|
||||
duration_s: float = Field(ge=0)
|
||||
tokens_spent: int | None = Field(default=None, ge=0)
|
||||
token_budget: int | None = Field(default=None, ge=0)
|
||||
commit_sha: str | None = None
|
||||
artifacts: list[str] = Field(default_factory=list)
|
||||
tool_events_count: int = Field(default=0, ge=0)
|
||||
tool_events_completeness: Literal["complete", "partial", "unavailable"] = "unavailable"
|
||||
refs: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class GatewayResult(ContractModel):
|
||||
ok: bool
|
||||
evidence: ExecutionEvidence
|
||||
# Direct caller output only. gateway.py deliberately excludes these fields
|
||||
# from State Hub detail because they can contain prompts/model responses.
|
||||
tool_output: str = ""
|
||||
tool_error: str | None = None
|
||||
|
||||
|
||||
class Rein(ABC):
|
||||
|
|
@ -49,14 +204,14 @@ class Rein(ABC):
|
|||
|
||||
@abstractmethod
|
||||
def start_session(
|
||||
self, profile: dict[str, Any], inputs: dict[str, str], sandbox: SandboxHandle
|
||||
self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
|
||||
) -> dict[str, str]:
|
||||
"""Begin an agent session bound to a sandbox. Returns a session handle."""
|
||||
"""Begin an agent session bound to a sandbox."""
|
||||
|
||||
@abstractmethod
|
||||
def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult:
|
||||
"""Run one tool call under the session's policy."""
|
||||
"""Run one tool call under the session policy."""
|
||||
|
||||
@abstractmethod
|
||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
||||
"""Close the session. Returns a summary (commit sha, outcome, ...)."""
|
||||
def end_session(self, session: dict[str, str]) -> ExecutionSummary:
|
||||
"""Close the session and return normalized rein evidence."""
|
||||
|
|
|
|||
|
|
@ -1,37 +1,201 @@
|
|||
"""Minimal gateway proving the harness contract against rein-aharness.
|
||||
"""Profile-driven Glas gateway.
|
||||
|
||||
GLAS-WP-0001-T04: resolve a sand-boxer profile, request a sandbox,
|
||||
dispatch one rein-aharness task through the Rein contract, verify a
|
||||
commit landed, tear the sandbox down. This is the parity proof gating
|
||||
any later "retire rein-aharness as a standalone concern" conversation —
|
||||
it is not itself that conversation.
|
||||
|
||||
GLAS-WP-0002-T03: post the gateway's own State Hub event, independent of
|
||||
whatever the rein itself reports (both reins' CLIs are invoked with
|
||||
their own hub reporting disabled by their glas-harness adapters — see
|
||||
reins/rein_aharness.py / reins/rein_openweights.py). Reported on both
|
||||
success and failure, from a `finally` block, so a raised exception still
|
||||
leaves an audit trail.
|
||||
|
||||
Requires the `sandbox` extra (sand-boxer installed as a sibling
|
||||
editable dependency).
|
||||
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
|
||||
|
||||
from typing import Any
|
||||
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 Rein, SandboxHandle, ToolCall
|
||||
from glas_harness.reins.rein_aharness import ReinAharness
|
||||
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(
|
||||
*,
|
||||
sandbox_profile: str,
|
||||
harness_profile: str,
|
||||
repo: str,
|
||||
title: str,
|
||||
description: str,
|
||||
|
|
@ -39,89 +203,113 @@ def run_task_through_rein(
|
|||
actor: str = "agt",
|
||||
project: str = "glas-harness",
|
||||
manager: SandboxManager | None = None,
|
||||
catalog: ProfileCatalog | None = None,
|
||||
report_to_hub: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve `sandbox_profile`, run one task inside it via `rein`, verify, tear down.
|
||||
) -> dict:
|
||||
"""Compatibility-shaped wrapper around the versioned execution request.
|
||||
|
||||
Defaults to `ReinAharness` when no rein is supplied — the only
|
||||
implemented rein as of GLAS-WP-0001-T04. `rein-openweights` plugs in
|
||||
the same way once REIN-OW-WP-0001 lands.
|
||||
It intentionally requires a harness profile. Direct rein injection is
|
||||
retained only as a library/test seam and never selects a default backend.
|
||||
"""
|
||||
manager = manager or SandboxManager()
|
||||
rein = rein or ReinAharness()
|
||||
|
||||
request = SandboxCreateRequest(
|
||||
profile=sandbox_profile,
|
||||
inputs={"repo": repo},
|
||||
consumer=Consumer(actor=actor, project=project),
|
||||
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,
|
||||
)
|
||||
status = manager.create(request)
|
||||
result: dict[str, Any] | None = None
|
||||
error: str | None = None
|
||||
try:
|
||||
reachability = status.reachability.model_dump(mode="json") if status.reachability else {}
|
||||
sandbox = SandboxHandle(
|
||||
sandbox_id=status.sandbox_id, host=status.host or "", reachability=reachability
|
||||
)
|
||||
|
||||
session = rein.start_session(
|
||||
profile={"id": sandbox_profile},
|
||||
inputs={"title": title, "description": description},
|
||||
sandbox=sandbox,
|
||||
)
|
||||
tool_result = rein.dispatch_tool(session, ToolCall(name="run_task", actor=actor))
|
||||
summary = rein.end_session(session)
|
||||
|
||||
result = {
|
||||
"sandbox_id": status.sandbox_id,
|
||||
"tool_ok": tool_result.ok,
|
||||
"tool_output": tool_result.output,
|
||||
"tool_error": tool_result.error,
|
||||
"summary": summary,
|
||||
}
|
||||
return result
|
||||
except Exception as exc:
|
||||
error = str(exc)
|
||||
raise
|
||||
finally:
|
||||
manager.destroy(status.sandbox_id)
|
||||
if report_to_hub:
|
||||
_post_gateway_event(
|
||||
rein=rein,
|
||||
sandbox_profile=sandbox_profile,
|
||||
sandbox_id=status.sandbox_id,
|
||||
project=project,
|
||||
actor=actor,
|
||||
title=title,
|
||||
result=result,
|
||||
error=error,
|
||||
)
|
||||
return result.model_dump(mode="json")
|
||||
|
||||
|
||||
def _post_gateway_event(
|
||||
def _build_result(
|
||||
*,
|
||||
rein: Rein,
|
||||
sandbox_profile: str,
|
||||
sandbox_id: str,
|
||||
project: str,
|
||||
actor: str,
|
||||
title: str,
|
||||
result: dict[str, Any] | None,
|
||||
request: ExecutionRequest,
|
||||
request_id: str,
|
||||
started_at: str,
|
||||
started: float,
|
||||
outcome: str,
|
||||
failure_stage: str | None,
|
||||
error: str | None,
|
||||
) -> None:
|
||||
ok = bool(result and result.get("tool_ok"))
|
||||
hub.post_progress_event(
|
||||
summary=f"gateway run: {title} ({'ok' if ok else 'failed'})",
|
||||
event_type="gateway_run",
|
||||
detail={
|
||||
"sandbox_profile": sandbox_profile,
|
||||
"sandbox_id": sandbox_id,
|
||||
"rein": type(rein).__name__,
|
||||
"project": project,
|
||||
"actor": actor,
|
||||
"task_title": title,
|
||||
"ok": ok,
|
||||
"result": result,
|
||||
"error": error,
|
||||
},
|
||||
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,
|
||||
)
|
||||
|
|
|
|||
222
src/glas_harness/profiles.py
Normal file
222
src/glas_harness/profiles.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
"""Runtime loader for versioned Glas harness profiles and rein descriptors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from glas_harness.contract import (
|
||||
CONTRACT_VERSION,
|
||||
HarnessProfile,
|
||||
Rein,
|
||||
ReinDescriptor,
|
||||
ResolvedExecutionContext,
|
||||
)
|
||||
|
||||
|
||||
class ProfileError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownProfileError(ProfileError):
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousProfileError(ProfileError):
|
||||
pass
|
||||
|
||||
|
||||
class IncompatibleProfileError(ProfileError):
|
||||
pass
|
||||
|
||||
|
||||
_SENSITIVE_KEY = re.compile(
|
||||
r"(^|_)(api_key|password|passwd|secret|secret_value|token_value|private_key)$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_SENSITIVE_VALUE = re.compile(r"^(sk-[A-Za-z0-9_-]{12,}|hvs\.[A-Za-z0-9_-]{12,})$")
|
||||
|
||||
|
||||
def _source_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _default_data_dir(kind: str) -> Path:
|
||||
env_name = "GLAS_PROFILE_DIR" if kind == "profiles" else "GLAS_REIN_REGISTRY_DIR"
|
||||
if configured := os.environ.get(env_name):
|
||||
return Path(configured).expanduser().resolve()
|
||||
packaged = Path(__file__).resolve().parent / "data" / kind
|
||||
if packaged.is_dir():
|
||||
return packaged
|
||||
return _source_root() / ("profiles" if kind == "profiles" else "registry/reins")
|
||||
|
||||
|
||||
def _read_yaml(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
data = yaml.safe_load(path.read_text())
|
||||
except (OSError, yaml.YAMLError) as exc:
|
||||
raise ProfileError(f"cannot read {path}: {exc}") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise ProfileError(f"{path}: expected a YAML object")
|
||||
_reject_inline_secrets(data, path=path)
|
||||
return data
|
||||
|
||||
|
||||
def _reject_inline_secrets(value: Any, *, path: Path, key_path: str = "") -> None:
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
key_str = str(key)
|
||||
child_path = f"{key_path}.{key_str}" if key_path else key_str
|
||||
if _SENSITIVE_KEY.search(key_str) and child not in (None, "", [], {}):
|
||||
raise ProfileError(f"{path}: inline secret material forbidden at {child_path}")
|
||||
_reject_inline_secrets(child, path=path, key_path=child_path)
|
||||
elif isinstance(value, list):
|
||||
for index, child in enumerate(value):
|
||||
_reject_inline_secrets(child, path=path, key_path=f"{key_path}[{index}]")
|
||||
elif isinstance(value, str) and _SENSITIVE_VALUE.match(value):
|
||||
raise ProfileError(f"{path}: token-looking inline value forbidden at {key_path}")
|
||||
|
||||
|
||||
def _capability_satisfies(actual: Any, required: Any) -> bool:
|
||||
if isinstance(required, list):
|
||||
if not isinstance(actual, list):
|
||||
return False
|
||||
return all(item in actual for item in required)
|
||||
return actual == required
|
||||
|
||||
|
||||
class ProfileCatalog:
|
||||
def __init__(
|
||||
self,
|
||||
profile_dir: str | Path | None = None,
|
||||
rein_dir: str | Path | None = None,
|
||||
) -> None:
|
||||
self.profile_dir = Path(profile_dir) if profile_dir else _default_data_dir("profiles")
|
||||
self.rein_dir = Path(rein_dir) if rein_dir else _default_data_dir("reins")
|
||||
self._profiles: dict[tuple[str, str], HarnessProfile] | None = None
|
||||
self._reins: dict[str, ReinDescriptor] | None = None
|
||||
|
||||
def profiles(self) -> dict[tuple[str, str], HarnessProfile]:
|
||||
if self._profiles is None:
|
||||
loaded: dict[tuple[str, str], HarnessProfile] = {}
|
||||
for path in sorted(self.profile_dir.glob("*.yaml")):
|
||||
try:
|
||||
profile = HarnessProfile.model_validate(_read_yaml(path))
|
||||
except ValidationError as exc:
|
||||
raise ProfileError(f"{path}: invalid harness profile: {exc}") from exc
|
||||
key = (profile.id, profile.version)
|
||||
if key in loaded:
|
||||
raise ProfileError(f"duplicate harness profile {profile.id}@{profile.version}")
|
||||
loaded[key] = profile
|
||||
self._profiles = loaded
|
||||
return self._profiles
|
||||
|
||||
def reins(self) -> dict[str, ReinDescriptor]:
|
||||
if self._reins is None:
|
||||
loaded: dict[str, ReinDescriptor] = {}
|
||||
for path in sorted(self.rein_dir.glob("*.yaml")):
|
||||
try:
|
||||
descriptor = ReinDescriptor.model_validate(_read_yaml(path))
|
||||
except ValidationError as exc:
|
||||
raise ProfileError(f"{path}: invalid rein descriptor: {exc}") from exc
|
||||
if descriptor.id in loaded:
|
||||
raise ProfileError(f"duplicate rein descriptor {descriptor.id}")
|
||||
loaded[descriptor.id] = descriptor
|
||||
self._reins = loaded
|
||||
return self._reins
|
||||
|
||||
def resolve(self, reference: str) -> tuple[HarnessProfile, ReinDescriptor]:
|
||||
profile_id, separator, version = reference.partition("@")
|
||||
candidates = [
|
||||
profile
|
||||
for (candidate_id, candidate_version), profile in self.profiles().items()
|
||||
if candidate_id == profile_id and (not separator or candidate_version == version)
|
||||
]
|
||||
if not candidates:
|
||||
raise UnknownProfileError(f"unknown harness profile: {reference}")
|
||||
if len(candidates) != 1:
|
||||
refs = ", ".join(sorted(str(candidate.ref) for candidate in candidates))
|
||||
raise AmbiguousProfileError(
|
||||
f"ambiguous harness profile {reference}; pin one of: {refs}"
|
||||
)
|
||||
profile = candidates[0]
|
||||
if profile.status != "enabled":
|
||||
raise IncompatibleProfileError(f"harness profile disabled: {profile.ref}")
|
||||
if profile.contract_version != CONTRACT_VERSION:
|
||||
raise IncompatibleProfileError(
|
||||
f"profile {profile.ref} requires contract {profile.contract_version}; "
|
||||
f"gateway supports {CONTRACT_VERSION}"
|
||||
)
|
||||
|
||||
descriptor = self.reins().get(profile.rein.id)
|
||||
if descriptor is None:
|
||||
raise IncompatibleProfileError(
|
||||
f"profile {profile.ref} references unknown rein {profile.rein.id}"
|
||||
)
|
||||
if descriptor.status != "implemented":
|
||||
raise IncompatibleProfileError(
|
||||
f"rein {descriptor.id} is not enabled for governed execution "
|
||||
f"(status={descriptor.status})"
|
||||
)
|
||||
if profile.contract_version not in descriptor.contract_versions:
|
||||
raise IncompatibleProfileError(
|
||||
f"rein {descriptor.id}@{descriptor.version} does not implement "
|
||||
f"contract {profile.contract_version}"
|
||||
)
|
||||
for name, required in profile.rein.required_capabilities.items():
|
||||
actual = descriptor.capabilities.get(name)
|
||||
if not _capability_satisfies(actual, required):
|
||||
raise IncompatibleProfileError(
|
||||
f"rein {descriptor.id} capability {name!r} is {actual!r}; "
|
||||
f"profile requires {required!r}"
|
||||
)
|
||||
return profile, descriptor
|
||||
|
||||
def resolve_context(self, reference: str) -> ResolvedExecutionContext:
|
||||
profile, descriptor = self.resolve(reference)
|
||||
return ResolvedExecutionContext(
|
||||
contract_version=CONTRACT_VERSION,
|
||||
profile=profile.ref,
|
||||
rein_id=descriptor.id,
|
||||
rein_version=descriptor.version,
|
||||
sandbox_profile=profile.sandbox_profile,
|
||||
tool_profile=profile.tool_profile,
|
||||
model=profile.model,
|
||||
limits=profile.limits,
|
||||
)
|
||||
|
||||
def build_rein(self, profile: HarnessProfile, descriptor: ReinDescriptor) -> Rein:
|
||||
module_name, class_name = descriptor.handler.split(":", 1)
|
||||
module = importlib.import_module(module_name)
|
||||
rein_type = getattr(module, class_name)
|
||||
if not inspect.isclass(rein_type) or not issubclass(rein_type, Rein):
|
||||
raise IncompatibleProfileError(
|
||||
f"handler {descriptor.handler} does not implement Rein"
|
||||
)
|
||||
candidate_kwargs = {
|
||||
"model": profile.model.model,
|
||||
"tool_profile": profile.tool_profile,
|
||||
"max_turns": profile.limits.max_turns,
|
||||
"budget_tokens": profile.limits.budget_tokens,
|
||||
"stream_tool_events": bool(profile.metadata.get("stream_tool_events", False)),
|
||||
}
|
||||
parameters = inspect.signature(rein_type).parameters
|
||||
kwargs = {
|
||||
name: value
|
||||
for name, value in candidate_kwargs.items()
|
||||
if name in parameters and value is not None
|
||||
}
|
||||
return rein_type(**kwargs)
|
||||
|
||||
def validate_all(self) -> list[ResolvedExecutionContext]:
|
||||
return [
|
||||
self.resolve_context(str(profile.ref))
|
||||
for profile in sorted(self.profiles().values(), key=lambda item: (item.id, item.version))
|
||||
]
|
||||
|
|
@ -26,3 +26,19 @@ def write_task_file(title: str, description: str, target_repo: str, **extra: Any
|
|||
json.dump(task_spec, fd)
|
||||
fd.close()
|
||||
return fd.name
|
||||
|
||||
|
||||
def parse_json_object(text: str) -> dict[str, Any]:
|
||||
"""Parse a rein CLI's final JSON object from otherwise human-readable output."""
|
||||
|
||||
decoder = json.JSONDecoder()
|
||||
for index, character in enumerate(text):
|
||||
if character != "{":
|
||||
continue
|
||||
try:
|
||||
value, end = decoder.raw_decode(text[index:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and not text[index + end :].strip():
|
||||
return value
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,15 @@ import shutil
|
|||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
||||
from glas_harness.reins._shared import git_head, write_task_file
|
||||
from glas_harness.contract import (
|
||||
ExecutionSummary,
|
||||
HarnessProfile,
|
||||
Rein,
|
||||
SandboxHandle,
|
||||
ToolCall,
|
||||
ToolResult,
|
||||
)
|
||||
from glas_harness.reins._shared import git_head, parse_json_object, write_task_file
|
||||
|
||||
|
||||
class ReinAharnessNotInstalled(RuntimeError):
|
||||
|
|
@ -34,9 +41,20 @@ class ReinAharnessNotInstalled(RuntimeError):
|
|||
|
||||
|
||||
class ReinAharness(Rein):
|
||||
def __init__(self, cli_bin: str = "rein-aharness", stream_tool_events: bool = False) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
cli_bin: str = "rein-aharness",
|
||||
stream_tool_events: bool = False,
|
||||
model: str | None = None,
|
||||
tool_profile: str | None = None,
|
||||
budget_tokens: int | None = None,
|
||||
) -> None:
|
||||
self.cli_bin = cli_bin
|
||||
self.stream_tool_events = stream_tool_events
|
||||
self.model = model
|
||||
self.tool_profile = tool_profile
|
||||
self.budget_tokens = budget_tokens
|
||||
self._last_result: dict[str, Any] = {}
|
||||
|
||||
def _bin(self) -> str:
|
||||
resolved = shutil.which(self.cli_bin)
|
||||
|
|
@ -47,7 +65,7 @@ class ReinAharness(Rein):
|
|||
return resolved
|
||||
|
||||
def start_session(
|
||||
self, profile: dict[str, Any], inputs: dict[str, str], sandbox: SandboxHandle
|
||||
self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
|
||||
) -> dict[str, str]:
|
||||
target_repo = (
|
||||
inputs.get("target_repo")
|
||||
|
|
@ -58,7 +76,11 @@ class ReinAharness(Rein):
|
|||
raise ValueError("no target_repo resolvable from inputs or sandbox reachability")
|
||||
|
||||
task_file = inputs.get("task_file") or write_task_file(
|
||||
inputs["title"], inputs["description"], target_repo, agent=inputs.get("agent", "coach")
|
||||
inputs["title"],
|
||||
inputs["description"],
|
||||
target_repo,
|
||||
agent=inputs.get("agent", "coach"),
|
||||
timeout_seconds=profile.limits.timeout_seconds or 600,
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -79,10 +101,29 @@ class ReinAharness(Rein):
|
|||
]
|
||||
if self.stream_tool_events:
|
||||
argv.append("--stream-tool-events")
|
||||
if self.model:
|
||||
argv += ["--model", self.model]
|
||||
if self.tool_profile:
|
||||
argv += ["--tool-profile", self.tool_profile]
|
||||
if self.budget_tokens:
|
||||
argv += ["--budget-tokens", str(self.budget_tokens)]
|
||||
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||
ok = proc.returncode == 0
|
||||
output, events = self._split_stream_events(proc.stdout)
|
||||
return ToolResult(ok=ok, output=output, error=None if ok else proc.stderr, events=events)
|
||||
self._last_result = parse_json_object(output)
|
||||
return ToolResult(
|
||||
ok=ok,
|
||||
output=output,
|
||||
error=None if ok else (proc.stderr or self._last_result.get("reason")),
|
||||
events=events,
|
||||
events_completeness="complete" if self.stream_tool_events else "unavailable",
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
metadata={
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _split_stream_events(stdout: str) -> tuple[str, list[dict[str, Any]]]:
|
||||
|
|
@ -105,10 +146,25 @@ class ReinAharness(Rein):
|
|||
remaining_lines.append(line)
|
||||
return "\n".join(remaining_lines), events
|
||||
|
||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
||||
def end_session(self, session: dict[str, str]) -> ExecutionSummary:
|
||||
head_after = git_head(session["target_repo"])
|
||||
committed = bool(head_after) and head_after != session.get("head_before")
|
||||
return {
|
||||
"commit_sha": head_after or "",
|
||||
"committed": str(committed),
|
||||
}
|
||||
reported_ok = bool(self._last_result.get("ok", committed))
|
||||
reason = self._last_result.get("reason") or None
|
||||
outcome = "succeeded" if committed and reported_ok else (
|
||||
"refused" if isinstance(reason, str) and reason.startswith("refused:") else "failed"
|
||||
)
|
||||
return ExecutionSummary(
|
||||
commit_sha=head_after or None,
|
||||
committed=committed,
|
||||
outcome=outcome,
|
||||
reason=reason,
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
artifacts=[head_after] if committed and head_after else [],
|
||||
metadata={
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
"persona_source": self._last_result.get("persona_source"),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,15 @@ import shutil
|
|||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
||||
from glas_harness.reins._shared import git_head, write_task_file
|
||||
from glas_harness.contract import (
|
||||
ExecutionSummary,
|
||||
HarnessProfile,
|
||||
Rein,
|
||||
SandboxHandle,
|
||||
ToolCall,
|
||||
ToolResult,
|
||||
)
|
||||
from glas_harness.reins._shared import git_head, parse_json_object, write_task_file
|
||||
|
||||
|
||||
class ReinOpenWeightsNotInstalled(RuntimeError):
|
||||
|
|
@ -22,9 +29,20 @@ class ReinOpenWeightsNotInstalled(RuntimeError):
|
|||
|
||||
|
||||
class ReinOpenWeights(Rein):
|
||||
def __init__(self, cli_bin: str = "rein-openweights", model: str | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
cli_bin: str = "rein-openweights",
|
||||
model: str | None = None,
|
||||
max_turns: int | None = None,
|
||||
budget_tokens: int | None = None,
|
||||
tool_profile: str | None = None,
|
||||
) -> None:
|
||||
self.cli_bin = cli_bin
|
||||
self.model = model
|
||||
self.max_turns = max_turns
|
||||
self.budget_tokens = budget_tokens
|
||||
self.tool_profile = tool_profile
|
||||
self._last_result: dict[str, Any] = {}
|
||||
|
||||
def _bin(self) -> str:
|
||||
resolved = shutil.which(self.cli_bin)
|
||||
|
|
@ -35,7 +53,7 @@ class ReinOpenWeights(Rein):
|
|||
return resolved
|
||||
|
||||
def start_session(
|
||||
self, profile: dict[str, Any], inputs: dict[str, str], sandbox: SandboxHandle
|
||||
self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
|
||||
) -> dict[str, str]:
|
||||
target_repo = (
|
||||
inputs.get("target_repo")
|
||||
|
|
@ -46,7 +64,10 @@ class ReinOpenWeights(Rein):
|
|||
raise ValueError("no target_repo resolvable from inputs or sandbox reachability")
|
||||
|
||||
task_file = inputs.get("task_file") or write_task_file(
|
||||
inputs["title"], inputs["description"], target_repo
|
||||
inputs["title"],
|
||||
inputs["description"],
|
||||
target_repo,
|
||||
timeout_seconds=profile.limits.timeout_seconds or 600,
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -60,14 +81,48 @@ class ReinOpenWeights(Rein):
|
|||
argv = [self._bin(), "run", "--task-file", session["task_file"], "--no-hub"]
|
||||
if self.model:
|
||||
argv += ["--model", self.model]
|
||||
if self.max_turns:
|
||||
argv += ["--max-turns", str(self.max_turns)]
|
||||
if self.budget_tokens:
|
||||
argv += ["--budget-tokens", str(self.budget_tokens)]
|
||||
if self.tool_profile:
|
||||
argv += ["--tool-profile", self.tool_profile]
|
||||
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||
ok = proc.returncode == 0
|
||||
return ToolResult(ok=ok, output=proc.stdout, error=None if ok else proc.stderr)
|
||||
self._last_result = parse_json_object(proc.stdout)
|
||||
return ToolResult(
|
||||
ok=ok,
|
||||
output=proc.stdout,
|
||||
error=None if ok else (proc.stderr or self._last_result.get("reason")),
|
||||
events_completeness="unavailable",
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
metadata={
|
||||
"turns": self._last_result.get("turns"),
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
},
|
||||
)
|
||||
|
||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
||||
def end_session(self, session: dict[str, str]) -> ExecutionSummary:
|
||||
head_after = git_head(session["target_repo"])
|
||||
committed = bool(head_after) and head_after != session.get("head_before")
|
||||
return {
|
||||
"commit_sha": head_after or "",
|
||||
"committed": str(committed),
|
||||
}
|
||||
reported_ok = bool(self._last_result.get("ok", committed))
|
||||
reason = self._last_result.get("reason") or None
|
||||
outcome = "succeeded" if committed and reported_ok else (
|
||||
"refused" if reason == "no OpenRouter credential resolved" else "failed"
|
||||
)
|
||||
return ExecutionSummary(
|
||||
commit_sha=head_after or None,
|
||||
committed=committed,
|
||||
outcome=outcome,
|
||||
reason=reason,
|
||||
tokens_spent=self._last_result.get("tokens_spent"),
|
||||
duration_s=self._last_result.get("execution_time_s"),
|
||||
resolved_model=self._last_result.get("model") or self.model,
|
||||
artifacts=[head_after] if committed and head_after else [],
|
||||
metadata={
|
||||
"turns": self._last_result.get("turns"),
|
||||
"tool_profile": self._last_result.get("tool_profile") or self.tool_profile,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
21
tests/test_cli.py
Normal file
21
tests/test_cli.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from glas_harness.cli import main
|
||||
from glas_harness.contract import ExecutionRequest
|
||||
|
||||
|
||||
def test_profiles_command_lists_committed_constellations(capsys) -> None:
|
||||
assert main(["profiles", "--json"]) == 0
|
||||
|
||||
rows = json.loads(capsys.readouterr().out)
|
||||
assert {row["rein_id"] for row in rows} == {"rein-aharness", "rein-openweights"}
|
||||
|
||||
|
||||
def test_execution_request_example_matches_contract() -> None:
|
||||
fixture = Path(__file__).resolve().parents[1] / "examples" / "execution-request.json"
|
||||
|
||||
request = ExecutionRequest.model_validate_json(fixture.read_text())
|
||||
|
||||
assert request.harness_profile_ref == "harness.agent-dev-local@1.0.0"
|
||||
assert request.assignment_ref == "role-assignment:agent-7:42"
|
||||
|
|
@ -11,7 +11,7 @@ def test_cli_channel_is_a_channel() -> None:
|
|||
|
||||
def _args(**overrides) -> argparse.Namespace:
|
||||
defaults = dict(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
harness_profile="harness.agent-dev-local@1.0.0",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
|
|
@ -28,7 +28,7 @@ def test_parse_invocation_maps_all_fields() -> None:
|
|||
invocation = channel.parse_invocation(_args())
|
||||
|
||||
assert invocation == GatewayInvocation(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
harness_profile="harness.agent-dev-local@1.0.0",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,18 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
from sandboxer.models import Reachability, SandboxState, SandboxStatus
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
||||
from glas_harness.gateway import run_task_through_rein
|
||||
from glas_harness.contract import (
|
||||
ExecutionRequest,
|
||||
ExecutionSummary,
|
||||
Rein,
|
||||
SandboxHandle,
|
||||
ToolCall,
|
||||
ToolResult,
|
||||
)
|
||||
from glas_harness.gateway import run_execution, run_task_through_rein
|
||||
|
||||
|
||||
PROFILE = "harness.agent-dev-local@1.0.0"
|
||||
|
||||
|
||||
class _FakeRein(Rein):
|
||||
|
|
@ -13,6 +23,7 @@ class _FakeRein(Rein):
|
|||
|
||||
def start_session(self, profile, inputs, sandbox: SandboxHandle):
|
||||
self.calls.append("start_session")
|
||||
assert str(profile.ref) == PROFILE
|
||||
assert sandbox.sandbox_id == "sbx1"
|
||||
assert sandbox.reachability.get("workspace_dir") == "/tmp/ws"
|
||||
return {"session": "s1"}
|
||||
|
|
@ -20,11 +31,24 @@ class _FakeRein(Rein):
|
|||
def dispatch_tool(self, session, tool_call: ToolCall) -> ToolResult:
|
||||
self.calls.append("dispatch_tool")
|
||||
assert tool_call.name == "run_task"
|
||||
return ToolResult(ok=True, output="done")
|
||||
return ToolResult(
|
||||
ok=True,
|
||||
output="sensitive direct output",
|
||||
events=[{"type": "tool_use"}],
|
||||
events_completeness="complete",
|
||||
tokens_spent=123,
|
||||
resolved_model="claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
def end_session(self, session):
|
||||
self.calls.append("end_session")
|
||||
return {"commit_sha": "deadbeef", "committed": "True"}
|
||||
return ExecutionSummary(
|
||||
committed=True,
|
||||
commit_sha="deadbeef",
|
||||
outcome="succeeded",
|
||||
tokens_spent=123,
|
||||
resolved_model="claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
|
||||
def _fake_status(sandbox_id: str = "sbx1") -> SandboxStatus:
|
||||
|
|
@ -42,29 +66,39 @@ def _fake_status(sandbox_id: str = "sbx1") -> SandboxStatus:
|
|||
)
|
||||
|
||||
|
||||
def test_run_task_through_rein_creates_and_destroys_sandbox() -> None:
|
||||
def _request(*, profile: str = PROFILE, report_to_hub: bool = False) -> ExecutionRequest:
|
||||
return ExecutionRequest(
|
||||
harness_profile_ref=profile,
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
request_id="req-1",
|
||||
correlation_id="corr-1",
|
||||
assignment_ref="assignment:42",
|
||||
report_to_hub=report_to_hub,
|
||||
)
|
||||
|
||||
|
||||
def test_run_execution_creates_and_destroys_sandbox() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
rein = _FakeRein()
|
||||
|
||||
result = run_task_through_rein(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=rein,
|
||||
manager=manager,
|
||||
report_to_hub=False,
|
||||
)
|
||||
result = run_execution(_request(), rein=rein, manager=manager)
|
||||
|
||||
assert rein.calls == ["start_session", "dispatch_tool", "end_session"]
|
||||
manager.create.assert_called_once()
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
assert result["tool_ok"] is True
|
||||
assert result["summary"]["committed"] == "True"
|
||||
assert result.ok is True
|
||||
assert result.evidence.profile_ref == PROFILE
|
||||
assert result.evidence.rein_id == "rein-aharness"
|
||||
assert result.evidence.commit_sha == "deadbeef"
|
||||
assert result.evidence.tokens_spent == 123
|
||||
assert result.evidence.refs["assignment_ref"] == "assignment:42"
|
||||
assert result.tool_output == "sensitive direct output"
|
||||
|
||||
|
||||
def test_run_task_through_rein_destroys_sandbox_even_on_failure() -> None:
|
||||
def test_run_execution_normalizes_execution_failure_and_tears_down() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
|
||||
|
|
@ -72,94 +106,89 @@ def test_run_task_through_rein_destroys_sandbox_even_on_failure() -> None:
|
|||
def dispatch_tool(self, session, tool_call):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
rein = _FailingRein()
|
||||
|
||||
try:
|
||||
run_task_through_rein(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=rein,
|
||||
manager=manager,
|
||||
report_to_hub=False,
|
||||
)
|
||||
except RuntimeError:
|
||||
pass
|
||||
result = run_execution(_request(), rein=_FailingRein(), manager=manager)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.evidence.outcome == "failed"
|
||||
assert result.evidence.failure_stage == "execution"
|
||||
assert result.evidence.error == "execution failed; inspect direct caller error"
|
||||
assert result.tool_error == "boom"
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
|
||||
|
||||
def test_run_task_through_rein_reports_success_event() -> None:
|
||||
def test_run_execution_refuses_unknown_profile_before_sandbox() -> None:
|
||||
manager = MagicMock()
|
||||
|
||||
result = run_execution(_request(profile="harness.unknown@1.0.0"), manager=manager)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.evidence.outcome == "refused"
|
||||
assert result.evidence.failure_stage == "resolution"
|
||||
assert "unknown harness profile" in (result.evidence.error or "")
|
||||
manager.create.assert_not_called()
|
||||
|
||||
|
||||
def test_hub_receives_normalized_evidence_without_raw_output() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
rein = _FakeRein()
|
||||
|
||||
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
|
||||
run_task_through_rein(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=rein,
|
||||
manager=manager,
|
||||
)
|
||||
result = run_execution(_request(report_to_hub=True), rein=_FakeRein(), manager=manager)
|
||||
|
||||
post.assert_called_once()
|
||||
kwargs = post.call_args.kwargs
|
||||
assert kwargs["event_type"] == "gateway_run"
|
||||
assert "ok)" in kwargs["summary"]
|
||||
assert kwargs["detail"]["ok"] is True
|
||||
assert kwargs["detail"]["sandbox_id"] == "sbx1"
|
||||
assert kwargs["detail"]["rein"] == "_FakeRein"
|
||||
detail = post.call_args.kwargs["detail"]
|
||||
assert detail["outcome"] == "succeeded"
|
||||
assert detail["profile_ref"] == PROFILE
|
||||
assert "tool_output" not in detail
|
||||
assert "sensitive direct output" not in str(detail)
|
||||
assert result.tool_output == "sensitive direct output"
|
||||
|
||||
|
||||
def test_run_task_through_rein_reports_failure_event_and_still_raises() -> None:
|
||||
def test_hub_failure_detail_excludes_raw_provider_error() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
|
||||
class _FailingRein(_FakeRein):
|
||||
def dispatch_tool(self, session, tool_call):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
rein = _FailingRein()
|
||||
raise RuntimeError("provider body containing sensitive material")
|
||||
|
||||
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
|
||||
try:
|
||||
run_task_through_rein(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=rein,
|
||||
manager=manager,
|
||||
)
|
||||
assert False, "expected RuntimeError to propagate"
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
post.assert_called_once()
|
||||
kwargs = post.call_args.kwargs
|
||||
assert "failed)" in kwargs["summary"]
|
||||
assert kwargs["detail"]["ok"] is False
|
||||
assert kwargs["detail"]["error"] == "boom"
|
||||
assert kwargs["detail"]["result"] is None
|
||||
|
||||
|
||||
def test_run_task_through_rein_skips_hub_when_disabled() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
rein = _FakeRein()
|
||||
|
||||
with patch("glas_harness.gateway.hub.post_progress_event") as post:
|
||||
run_task_through_rein(
|
||||
sandbox_profile="profile.bwrap-local",
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=rein,
|
||||
manager=manager,
|
||||
report_to_hub=False,
|
||||
result = run_execution(
|
||||
_request(report_to_hub=True), rein=_FailingRein(), manager=manager
|
||||
)
|
||||
|
||||
post.assert_not_called()
|
||||
detail = post.call_args.kwargs["detail"]
|
||||
assert detail["error"] == "execution failed; inspect direct caller error"
|
||||
assert "sensitive material" not in str(detail)
|
||||
assert "sensitive material" in (result.tool_error or "")
|
||||
|
||||
|
||||
def test_wrapper_requires_and_reports_harness_profile() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
|
||||
result = run_task_through_rein(
|
||||
harness_profile=PROFILE,
|
||||
repo="/tmp/repo",
|
||||
title="t",
|
||||
description="d",
|
||||
rein=_FakeRein(),
|
||||
manager=manager,
|
||||
report_to_hub=False,
|
||||
)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["evidence"]["profile_ref"] == PROFILE
|
||||
|
||||
|
||||
def test_teardown_failure_is_visible_in_evidence() -> None:
|
||||
manager = MagicMock()
|
||||
manager.create.return_value = _fake_status()
|
||||
manager.destroy.side_effect = RuntimeError("cannot teardown")
|
||||
|
||||
result = run_execution(_request(), rein=_FakeRein(), manager=manager)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.evidence.failure_stage == "teardown"
|
||||
assert result.evidence.error == "teardown failed; inspect direct caller error"
|
||||
assert result.tool_error == "cannot teardown"
|
||||
|
|
|
|||
164
tests/test_profiles.py
Normal file
164
tests/test_profiles.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from glas_harness.profiles import (
|
||||
AmbiguousProfileError,
|
||||
IncompatibleProfileError,
|
||||
ProfileCatalog,
|
||||
ProfileError,
|
||||
UnknownProfileError,
|
||||
)
|
||||
from glas_harness.reins.rein_aharness import ReinAharness
|
||||
from glas_harness.reins.rein_openweights import ReinOpenWeights
|
||||
|
||||
|
||||
def _write(path: Path, name: str, text: str) -> None:
|
||||
(path / name).write_text(text)
|
||||
|
||||
|
||||
def _profile(
|
||||
*,
|
||||
profile_id: str = "harness.test",
|
||||
version: str = "1.0.0",
|
||||
contract: str = "1.0",
|
||||
rein: str = "rein-test",
|
||||
required: str = "session_style: unattended",
|
||||
extra: str = "",
|
||||
) -> str:
|
||||
return f"""\
|
||||
id: {profile_id}
|
||||
version: \"{version}\"
|
||||
contract_version: \"{contract}\"
|
||||
status: enabled
|
||||
rein:
|
||||
id: {rein}
|
||||
required_capabilities:
|
||||
{required}
|
||||
sandbox_profile: profile.test
|
||||
tool_profile: green-commit-only
|
||||
model:
|
||||
provider: test
|
||||
model: model-1
|
||||
model_class: other
|
||||
route: test-route
|
||||
limits:
|
||||
budget_tokens: 100
|
||||
{extra}
|
||||
"""
|
||||
|
||||
|
||||
def _rein(*, rein_id: str = "rein-test", capability: str = "unattended") -> str:
|
||||
return f"""\
|
||||
id: {rein_id}
|
||||
version: \"1.0.0\"
|
||||
title: Test rein
|
||||
handler: glas_harness.reins.rein_aharness:ReinAharness
|
||||
contract_versions: [\"1.0\"]
|
||||
capabilities:
|
||||
session_style: {capability}
|
||||
status: implemented
|
||||
"""
|
||||
|
||||
|
||||
def test_committed_catalog_resolves_both_constellations() -> None:
|
||||
catalog = ProfileCatalog()
|
||||
|
||||
contexts = catalog.validate_all()
|
||||
|
||||
assert len(contexts) == 3
|
||||
assert {context.rein_id for context in contexts} == {
|
||||
"rein-aharness",
|
||||
"rein-openweights",
|
||||
}
|
||||
profile, descriptor = catalog.resolve(
|
||||
"harness.agent-dev-openweights-local@1.0.0"
|
||||
)
|
||||
rein = catalog.build_rein(profile, descriptor)
|
||||
assert isinstance(rein, ReinOpenWeights)
|
||||
assert rein.model == "qwen/qwen-2.5-72b-instruct"
|
||||
assert rein.tool_profile == "green-commit-only"
|
||||
assert rein.max_turns == 20
|
||||
assert rein.budget_tokens == 60000
|
||||
|
||||
profile, descriptor = catalog.resolve("harness.agent-dev-local@1.0.0")
|
||||
rein = catalog.build_rein(profile, descriptor)
|
||||
assert isinstance(rein, ReinAharness)
|
||||
assert rein.model == "claude-sonnet-4-6"
|
||||
assert rein.tool_profile == "green-commit-only"
|
||||
assert rein.budget_tokens == 60000
|
||||
|
||||
|
||||
def test_unknown_profile_fails_closed() -> None:
|
||||
with pytest.raises(UnknownProfileError, match="unknown harness profile"):
|
||||
ProfileCatalog().resolve("harness.missing@1.0.0")
|
||||
|
||||
|
||||
def test_unpinned_multi_version_profile_is_ambiguous(tmp_path) -> None:
|
||||
profiles = tmp_path / "profiles"
|
||||
reins = tmp_path / "reins"
|
||||
profiles.mkdir()
|
||||
reins.mkdir()
|
||||
_write(profiles, "one.yaml", _profile(version="1.0.0"))
|
||||
_write(profiles, "two.yaml", _profile(version="2.0.0"))
|
||||
_write(reins, "rein.yaml", _rein())
|
||||
|
||||
with pytest.raises(AmbiguousProfileError, match="pin one of"):
|
||||
ProfileCatalog(profiles, reins).resolve("harness.test")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("profile_text", "match"),
|
||||
[
|
||||
(_profile(version="not-semver"), "semantic versioning"),
|
||||
(_profile(extra="unexpected: true"), "extra_forbidden"),
|
||||
(_profile(extra="api_key: sk-this-is-inline-and-forbidden"), "inline secret"),
|
||||
],
|
||||
)
|
||||
def test_malformed_or_sensitive_profiles_are_rejected(
|
||||
tmp_path, profile_text: str, match: str
|
||||
) -> None:
|
||||
profiles = tmp_path / "profiles"
|
||||
reins = tmp_path / "reins"
|
||||
profiles.mkdir()
|
||||
reins.mkdir()
|
||||
_write(profiles, "profile.yaml", profile_text)
|
||||
_write(reins, "rein.yaml", _rein())
|
||||
|
||||
with pytest.raises(ProfileError, match=match):
|
||||
ProfileCatalog(profiles, reins).profiles()
|
||||
|
||||
|
||||
def test_duplicate_profile_revision_is_rejected(tmp_path) -> None:
|
||||
profiles = tmp_path / "profiles"
|
||||
reins = tmp_path / "reins"
|
||||
profiles.mkdir()
|
||||
reins.mkdir()
|
||||
_write(profiles, "one.yaml", _profile())
|
||||
_write(profiles, "two.yaml", _profile())
|
||||
_write(reins, "rein.yaml", _rein())
|
||||
|
||||
with pytest.raises(ProfileError, match="duplicate harness profile"):
|
||||
ProfileCatalog(profiles, reins).profiles()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("profile_text", "rein_text", "match"),
|
||||
[
|
||||
(_profile(contract="2.0"), _rein(), "requires contract 2.0"),
|
||||
(_profile(rein="rein-missing"), _rein(), "unknown rein"),
|
||||
(_profile(required="session_style: interactive"), _rein(), "capability"),
|
||||
],
|
||||
)
|
||||
def test_incompatible_profile_or_rein_is_rejected(
|
||||
tmp_path, profile_text: str, rein_text: str, match: str
|
||||
) -> None:
|
||||
profiles = tmp_path / "profiles"
|
||||
reins = tmp_path / "reins"
|
||||
profiles.mkdir()
|
||||
reins.mkdir()
|
||||
_write(profiles, "profile.yaml", profile_text)
|
||||
_write(reins, "rein.yaml", rein_text)
|
||||
|
||||
with pytest.raises(IncompatibleProfileError, match=match):
|
||||
ProfileCatalog(profiles, reins).resolve("harness.test@1.0.0")
|
||||
|
|
@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
|
|||
import pytest
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
||||
from glas_harness.profiles import ProfileCatalog
|
||||
from glas_harness.reins.rein_aharness import ReinAharness, ReinAharnessNotInstalled
|
||||
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
rein = ReinAharness()
|
||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
||||
session = rein.start_session(
|
||||
profile={"id": "harness.agent-dev-local"},
|
||||
profile=ProfileCatalog().resolve("harness.agent-dev-local@1.0.0")[0],
|
||||
inputs={"title": "t", "description": "d"},
|
||||
sandbox=sandbox,
|
||||
)
|
||||
|
|
@ -47,7 +48,11 @@ def test_start_session_requires_resolvable_target_repo() -> None:
|
|||
|
||||
|
||||
def test_dispatch_tool_invokes_agent_harness_cli() -> None:
|
||||
rein = ReinAharness()
|
||||
rein = ReinAharness(
|
||||
model="claude-sonnet-4-6",
|
||||
tool_profile="green-commit-only",
|
||||
budget_tokens=1234,
|
||||
)
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
|
||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
|
|
@ -57,6 +62,9 @@ def test_dispatch_tool_invokes_agent_harness_cli() -> None:
|
|||
|
||||
argv = run.call_args.args[0]
|
||||
assert argv[:3] == ["/usr/bin/agent-harness", "run", "--task-file"]
|
||||
assert argv[argv.index("--model") + 1] == "claude-sonnet-4-6"
|
||||
assert argv[argv.index("--tool-profile") + 1] == "green-commit-only"
|
||||
assert argv[argv.index("--budget-tokens") + 1] == "1234"
|
||||
assert result.ok is True
|
||||
assert result.output == "ok"
|
||||
|
||||
|
|
@ -88,8 +96,9 @@ def test_end_session_detects_new_commit(tmp_path) -> None:
|
|||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "task"], cwd=repo, check=True)
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
||||
assert summary["committed"] == "True"
|
||||
assert summary["commit_sha"] != head_before
|
||||
assert summary.committed is True
|
||||
assert summary.outcome == "succeeded"
|
||||
assert summary.commit_sha != head_before
|
||||
|
||||
|
||||
def test_end_session_no_new_commit(tmp_path) -> None:
|
||||
|
|
@ -104,7 +113,8 @@ def test_end_session_no_new_commit(tmp_path) -> None:
|
|||
head = git_head(str(repo))
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head})
|
||||
assert summary["committed"] == "False"
|
||||
assert summary.committed is False
|
||||
assert summary.outcome == "failed"
|
||||
|
||||
|
||||
def test_dispatch_tool_streams_events_when_enabled() -> None:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
|
|||
import pytest
|
||||
|
||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
||||
from glas_harness.profiles import ProfileCatalog
|
||||
from glas_harness.reins.rein_openweights import ReinOpenWeights, ReinOpenWeightsNotInstalled
|
||||
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
rein = ReinOpenWeights()
|
||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
||||
session = rein.start_session(
|
||||
profile={"id": "harness.agent-dev-local"},
|
||||
profile=ProfileCatalog().resolve("harness.agent-dev-openweights-local@1.0.0")[0],
|
||||
inputs={"title": "t", "description": "d"},
|
||||
sandbox=sandbox,
|
||||
)
|
||||
|
|
@ -63,7 +64,12 @@ def test_dispatch_tool_invokes_rein_openweights_cli() -> None:
|
|||
|
||||
|
||||
def test_dispatch_tool_passes_model_when_set() -> None:
|
||||
rein = ReinOpenWeights(model="meta-llama/llama-3.1-70b-instruct")
|
||||
rein = ReinOpenWeights(
|
||||
model="meta-llama/llama-3.1-70b-instruct",
|
||||
max_turns=8,
|
||||
budget_tokens=1234,
|
||||
tool_profile="green-commit-only",
|
||||
)
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
|
||||
|
|
@ -74,6 +80,9 @@ def test_dispatch_tool_passes_model_when_set() -> None:
|
|||
argv = run.call_args.args[0]
|
||||
assert "--model" in argv
|
||||
assert "meta-llama/llama-3.1-70b-instruct" in argv
|
||||
assert argv[argv.index("--max-turns") + 1] == "8"
|
||||
assert argv[argv.index("--budget-tokens") + 1] == "1234"
|
||||
assert argv[argv.index("--tool-profile") + 1] == "green-commit-only"
|
||||
|
||||
|
||||
def test_dispatch_tool_reports_failure() -> None:
|
||||
|
|
@ -102,5 +111,6 @@ def test_end_session_detects_new_commit(tmp_path) -> None:
|
|||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "task"], cwd=repo, check=True)
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
||||
assert summary["committed"] == "True"
|
||||
assert summary["commit_sha"] != head_before
|
||||
assert summary.committed is True
|
||||
assert summary.outcome == "succeeded"
|
||||
assert summary.commit_sha != head_before
|
||||
|
|
|
|||
|
|
@ -0,0 +1,316 @@
|
|||
---
|
||||
id: GLAS-WP-0004
|
||||
type: workplan
|
||||
title: "Versioned execution-constellation profiles and explicit rein routing"
|
||||
domain: infotech
|
||||
repo: glas-harness
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: execution-constellation-profiles
|
||||
created: "2026-08-20"
|
||||
updated: "2026-08-21"
|
||||
---
|
||||
|
||||
# Versioned execution-constellation profiles and explicit rein routing
|
||||
|
||||
## Context
|
||||
|
||||
The accepted family architecture is sound: `glas-harness` owns the harness
|
||||
contract and outer lifecycle while a concrete rein owns its inner agentic loop.
|
||||
Both `rein-aharness` and `rein-openweights` implement the contract and have been
|
||||
live-proven through the gateway.
|
||||
|
||||
The prototype does not yet make that flexibility real for normal consumers:
|
||||
|
||||
- `profiles/*.yaml` name a rein, sandbox profile, and tool profile, but no
|
||||
production code loads or validates those files;
|
||||
- `glas-harness run` accepts `--sandbox-profile`, not a Glas harness profile;
|
||||
- `run_task_through_rein()` silently constructs `ReinAharness` when no rein is
|
||||
injected;
|
||||
- both committed profiles select `rein-aharness`, so the profile catalog does
|
||||
not demonstrate a second execution constellation;
|
||||
- model selection is a rein-constructor detail (`ReinOpenWeights(model=...)`),
|
||||
not a versioned, attributable profile input; and
|
||||
- `SandboxHandle`, `ToolCall`, `ToolResult`, and gateway result/evidence shapes
|
||||
are implemented but not versioned or declared compatible.
|
||||
|
||||
This workplan turns Glas from a proven adapter seam into the primary,
|
||||
versioned execution-layer interface for agent workforce assignments and other
|
||||
consumers. A consumer should be able to select or request an approved Glas
|
||||
profile without embedding a concrete rein, vendor harness, base model,
|
||||
sandbox, or tool-policy implementation into its own domain contract.
|
||||
|
||||
## Goal
|
||||
|
||||
Provide a versioned Glas execution request and profile path that resolves an
|
||||
approved execution constellation:
|
||||
|
||||
```text
|
||||
consumer execution request
|
||||
-> glas-harness profile/requirements resolution
|
||||
-> concrete rein + model route + sandbox profile + tool profile
|
||||
-> versioned Rein contract
|
||||
-> normalized execution result and audit evidence
|
||||
```
|
||||
|
||||
The decisive proof is one semantically identical safe task executed through
|
||||
two explicit Glas profiles—one backed by `rein-aharness`, one by
|
||||
`rein-openweights`—without changing the workforce, role, goal, or task model.
|
||||
|
||||
## Boundaries and invariants
|
||||
|
||||
- `glas-harness` owns execution-profile schema, validation, resolution, rein
|
||||
registry lookup, sandbox coordination, actor attribution, and gateway
|
||||
evidence.
|
||||
- A rein owns its inner agentic loop, backend-specific setup, and credentials,
|
||||
consistent with ADR-002.
|
||||
- `llm-connect` and provider adapters retain inference/provider boundaries.
|
||||
Glas may carry a model route/reference and auditable resolved model, but it
|
||||
does not become an LLM API, model catalog of record, or secret broker.
|
||||
- `sand-boxer` owns sandbox provisioning, profiles, placement, and teardown
|
||||
implementation; Glas selects and consumes a sandbox profile.
|
||||
- Scheduling and blueprint sourcing remain outside the Glas gateway under
|
||||
ADR-003. `activity-core`, channels, or rein-local intake supply work; this
|
||||
workplan standardizes execution after invocation.
|
||||
- Profiles and evidence contain credential-route references only, never secret
|
||||
values.
|
||||
- Unknown, disabled, incompatible, or ambiguous profiles fail closed. No
|
||||
production invocation silently falls back to `rein-aharness` or a default
|
||||
model.
|
||||
- Existing live `rein-aharness` operation remains available throughout the
|
||||
migration; cutover requires explicit compatibility and rollback evidence.
|
||||
|
||||
## Task: Version and freeze the minimum execution contract
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Define a small versioned contract for the existing `Rein` lifecycle and its
|
||||
boundary types. Use typed models for at least:
|
||||
|
||||
- execution request and resolved execution context;
|
||||
- harness profile reference and profile revision/version;
|
||||
- `SandboxHandle`, `ToolCall`, `ToolResult`, and execution summary;
|
||||
- optional per-tool events and rein-specific extension metadata; and
|
||||
- compatibility declaration between a profile, Glas contract version, and rein.
|
||||
|
||||
Document which fields are stable, optional, extensible, and sensitive. Preserve
|
||||
the existing three-step lifecycle unless an implementation blocker is proven;
|
||||
do not introduce speculative middleware or move rein internals into Glas.
|
||||
|
||||
**Done when:** the contract has an explicit version, validation tests cover
|
||||
required/optional/unknown fields and incompatible versions, and both existing
|
||||
rein adapters satisfy the same typed interface without fabricated evidence.
|
||||
|
||||
## Task: Implement the harness-profile schema, loader, and validator
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T02
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Turn `profiles/*.yaml` into a real runtime contract. The minimum profile should
|
||||
identify:
|
||||
|
||||
- stable profile ID and semantic version;
|
||||
- compatible Glas contract version;
|
||||
- rein registry reference and required rein capabilities;
|
||||
- sandbox profile reference;
|
||||
- tool-policy/profile reference;
|
||||
- model route/reference or bounded selection policy, including the resolved
|
||||
model identifier required in evidence;
|
||||
- execution limits or references needed at the gateway boundary; and
|
||||
- credential/policy route references, never values.
|
||||
|
||||
Implement deterministic discovery, parsing, validation, and lookup. Validate
|
||||
the selected profile against `registry/reins/*.yaml`, including status and
|
||||
capabilities. Reject duplicate IDs/versions, unknown reins, incompatible
|
||||
contract versions, unavailable required capabilities, and unsafe inline secret
|
||||
material.
|
||||
|
||||
**Done when:** committed profiles are loaded by production code, malformed and
|
||||
incompatible fixtures fail closed with actionable errors, and the profile
|
||||
catalog has schema and parity tests.
|
||||
|
||||
## Task: Make gateway and channel invocation explicitly profile-driven
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T03
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Add `--harness-profile <id[@version]>` to the CLI/channel contract and resolve
|
||||
the rein, model route, sandbox profile, and tool profile through the loader.
|
||||
Remove the implicit `ReinAharness()` choice from governed/production paths.
|
||||
|
||||
Keep direct rein injection as a narrow test/library seam if useful. If
|
||||
`--sandbox-profile` must remain temporarily, classify it as an explicit legacy
|
||||
or development path, warn clearly, give it a documented removal condition,
|
||||
and never let it masquerade as profile resolution.
|
||||
|
||||
**Done when:** normal CLI and gateway execution requires an explicit valid
|
||||
Glas profile, unknown/disabled/ambiguous selections perform no sandbox or rein
|
||||
side effect, and tests prove that the resolved rein—not a hard-coded default—
|
||||
receives the session.
|
||||
|
||||
## Task: Add two real execution constellations
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T04
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Normalize the existing `rein-aharness` profiles and add at least one committed
|
||||
`rein-openweights` profile. Make the model and sandbox choices explicit enough
|
||||
that audit evidence can reconstruct the selected constellation while keeping
|
||||
provider credentials rein-local.
|
||||
|
||||
Cover both an exact pinned profile reference and the minimum safe form of a
|
||||
requirements request if implemented. Requirements resolution must be
|
||||
deterministic, explain why a profile matched, and refuse an ambiguous result;
|
||||
it must not silently optimize for an undocumented model or cost preference.
|
||||
|
||||
**Done when:** the same safe task fixture can be routed to either rein by
|
||||
changing only the Glas execution selection, and tests prove that the selected
|
||||
model route, sandbox profile, and tool profile reach the correct adapter.
|
||||
|
||||
## Task: Normalize execution evidence and failure semantics
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T05
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Define one result/audit envelope across reins containing at least:
|
||||
|
||||
- request/correlation ID, actor, project/target, and timestamps;
|
||||
- Glas contract and profile ID/version;
|
||||
- resolved rein ID/version, model route/resolved model, sandbox profile and
|
||||
sandbox ID, and tool profile;
|
||||
- outcome, refusal/failure class, duration, token/cost fields when known, and
|
||||
commit/artifact references; and
|
||||
- optional tool events with explicit availability/completeness semantics.
|
||||
|
||||
Keep unknown distinct from zero and unsupported distinct from empty. Redact
|
||||
raw prompts, raw model output, secrets, and credential material from the State
|
||||
Hub event. Ensure sandbox teardown and a compact failure event occur for
|
||||
resolution, startup, execution, verification, and teardown failures.
|
||||
|
||||
**Done when:** both reins return the common envelope, negative-path tests cover
|
||||
each lifecycle stage, and evidence identifies the complete execution
|
||||
constellation without exposing sensitive content.
|
||||
|
||||
## Task: Publish the consumer contract and cross-repository handoffs
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T06
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Document how consumer-owned agent instances, future agentic-resources
|
||||
`RoleAssignment` records, activity-core work, and channels reference Glas.
|
||||
Provide a fixture containing at least:
|
||||
|
||||
- `harness_profile_ref` or a versioned execution-requirements object;
|
||||
- correlation, assignment, role, duty, goal, and resource-envelope references;
|
||||
- expected output/evidence and execution limits; and
|
||||
- explicit refusal behavior when the profile or envelope is unavailable.
|
||||
|
||||
Record cross-repository follow-ups as messages, capability requests, or live
|
||||
work records in their owning repositories rather than implementing scheduling,
|
||||
workforce allocation, or blueprint compilation inside Glas.
|
||||
|
||||
**Done when:** a consumer can construct a valid execution request without
|
||||
knowing a rein class or provider CLI, and ownership documentation agrees across
|
||||
Glas, agentic-resources, KaizenAgentic, activity-core, and the example reins.
|
||||
|
||||
## Task: Compatibility migration and dual-rein live proof
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T07
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Provide a staged migration for existing direct `rein-aharness` and
|
||||
`--sandbox-profile` consumers:
|
||||
|
||||
1. inventory callers and preserve the current live path;
|
||||
2. add explicit profiles matching current behavior;
|
||||
3. run compatibility tests and one non-destructive live proof through each
|
||||
rein using approved credential routes;
|
||||
4. compare normalized evidence and verify sandbox cleanup;
|
||||
5. migrate callers individually with rollback instructions; and
|
||||
6. remove the implicit rein default only after telemetry or repository search
|
||||
proves no governed caller depends on it.
|
||||
|
||||
No live provider credential may be printed, committed, or copied into State
|
||||
Hub. Use credential routing before requesting access.
|
||||
|
||||
**2026-08-20 evidence:** the same bounded fixture was dispatched through both
|
||||
explicit profiles. `rein-aharness` succeeded with a real commit and complete
|
||||
tool-event evidence. `rein-openweights` resolved, ran, returned normalized
|
||||
evidence, and tore down its sandbox, but the existing rein-local OpenRouter
|
||||
lane was rejected by the provider with HTTP 401 before a model turn. Live
|
||||
open-weight success waits on the owner repairing/rotating catalog route
|
||||
`rein-openweights-openrouter-approle`; no sibling workload credential will be
|
||||
borrowed. See `docs/evidence/GLAS-WP-0004-live-proof-2026-08-20.md`.
|
||||
|
||||
**Completed 2026-08-21:** after the operator repaired the key, the profile
|
||||
completed the same fixture in three turns with 3,497 tokens and commit
|
||||
`b0600b25066731c6e1fc458409429f76a844f959`; sandbox `220482bc` was verified
|
||||
destroyed. The check also corrected the rein's documented-but-missing default
|
||||
AppRole directory. Both real backend success criteria and rollback evidence are
|
||||
now met.
|
||||
|
||||
**Done when:** both real backends complete the same bounded acceptance task
|
||||
through explicit Glas profiles, current `rein-aharness` service behavior has
|
||||
not regressed, rollback is documented, and the implicit production default is
|
||||
retired or has a metered retirement record with a named owner.
|
||||
|
||||
## Task: Refresh architecture, operations, and release evidence
|
||||
|
||||
```task
|
||||
id: GLAS-WP-0004-T08
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Update `INTENT.md`, `SCOPE.md`, the harness/channel contracts, registry docs,
|
||||
CLI help, and operational examples to describe the implemented profile-driven
|
||||
path. Correct stale current-state claims, including gateway State Hub reporting
|
||||
that is already implemented. Add CI coverage for profile/catalog validation
|
||||
and record the compatibility/versioning policy.
|
||||
|
||||
Run the full Glas suite plus the contract tests in both rein repositories.
|
||||
Record non-secret live evidence and State Hub progress. Before marking the
|
||||
workplan finished, hand off every remaining actionable item as a live work
|
||||
record rather than leaving it only in prose.
|
||||
|
||||
**Done when:** documentation matches runtime behavior, tests and live proofs
|
||||
pass, generated work records are synchronized, and the workplan has no
|
||||
untracked residual implementation gap.
|
||||
|
||||
## Overall acceptance
|
||||
|
||||
This workplan is complete only when:
|
||||
|
||||
1. Glas profiles are runtime inputs, not unused YAML documentation.
|
||||
2. Governed execution never silently defaults to `rein-aharness` or a model.
|
||||
3. A consumer references Glas rather than importing or naming a concrete rein.
|
||||
4. Two explicit profiles run the same safe task through different rein/model
|
||||
constellations with a common evidence envelope.
|
||||
5. Profile, rein, model, sandbox, tool policy, actor, and contract versions are
|
||||
reconstructable from non-secret evidence.
|
||||
6. Scheduling, blueprint ownership, provider credentials, sandbox provisioning,
|
||||
and workforce allocation remain with their declared owners.
|
||||
7. Existing live `rein-aharness` operation has a tested compatibility and
|
||||
rollback path.
|
||||
Loading…
Add table
Add a link
Reference in a new issue