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
|
## Developer Workflow
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
# Test
|
||||||
python3 -m pytest tests/ -q
|
python3 -m pytest tests/ -q
|
||||||
|
|
||||||
# Run — resolves a sand-boxer sandbox, runs one task through a rein, verifies
|
# Validate the executable profile/rein catalog
|
||||||
glas-harness run --sandbox-profile profile.bwrap-local \
|
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 "..."
|
--repo <path> --title "..." --description "..."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
84
INTENT.md
84
INTENT.md
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: glas-harness
|
repo: glas-harness
|
||||||
updated: "2026-06-22"
|
updated: "2026-08-20"
|
||||||
---
|
---
|
||||||
|
|
||||||
# INTENT
|
# INTENT
|
||||||
|
|
@ -9,8 +9,8 @@ updated: "2026-06-22"
|
||||||
> glas-harness is the Coulomb **meta-framework for agent harnesses** — a unified
|
> glas-harness is the Coulomb **meta-framework for agent harnesses** — a unified
|
||||||
> API and extension platform for running agentic assistants with tools, memory,
|
> API and extension platform for running agentic assistants with tools, memory,
|
||||||
> channels, and subagent delegation, while **consuming** sand-boxer for isolated
|
> channels, and subagent delegation, while **consuming** sand-boxer for isolated
|
||||||
> execution. This file is preliminary; refine as the harness boundary is
|
> execution. The implemented nucleus is a versioned, profile-driven execution
|
||||||
> implemented.
|
> router over concrete reins.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ Coulomb splits that stack deliberately:
|
||||||
| **Code generation** — specs, generation, PR output | **snuggle-inventor** |
|
| **Code generation** — specs, generation, PR output | **snuggle-inventor** |
|
||||||
|
|
||||||
glas-harness exists to be the harness side of that split: **one consistent way to
|
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.
|
integration path to sand-boxer when tools need isolation.
|
||||||
|
|
||||||
sand-boxer is OpenRouter for sandboxes. glas-harness is the parallel **harness
|
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.
|
2. **What can the agent invoke?** Tool catalog, policies, elevated escape hatches.
|
||||||
3. **What does the agent remember?** Memory, skills, identity documents, cron.
|
3. **What does the agent remember?** Memory, skills, identity documents, cron.
|
||||||
4. **Where does the user interact?** Channel extensions (CLI, chat, email, …).
|
4. **Where does the user interact?** Channel extensions (CLI, chat, email, …).
|
||||||
5. **When do tools run in isolation?** Sandbox policy (`mode`, `scope`,
|
5. **When do tools run in isolation?** A named sand-boxer profile selected by
|
||||||
`workspaceAccess`) — fulfilled by **sand-boxer**, not implemented here.
|
the Glas profile — fulfilled by **sand-boxer**, not implemented here.
|
||||||
6. **How are subagents delegated?** Isolated sub-sessions with bounded context.
|
6. **How are subagents delegated?** Isolated sub-sessions with bounded context.
|
||||||
7. **What happened?** Session and tool audit events to State Hub.
|
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
|
generator, the scheduler, work-state authority, tunnel/CA owner, or production
|
||||||
service host on Railiance01.
|
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
|
## Coulomb sibling boundaries
|
||||||
|
|
@ -148,7 +173,11 @@ glas-harness is a **meta-framework** with four pillars (preliminary):
|
||||||
|
|
||||||
### 1. Unified harness API
|
### 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
|
- Start / resume / end sessions; subagent spawn and join
|
||||||
- Tool dispatch with policy checks and audit metadata
|
- Tool dispatch with policy checks and audit metadata
|
||||||
|
|
@ -160,14 +189,18 @@ human operators via CLI.
|
||||||
|
|
||||||
### 2. Harness profile catalog
|
### 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
|
- concrete rein and required capabilities;
|
||||||
- Sandbox policy defaults (`mode`, `scope`, `workspaceAccess`) — OpenClaw-aligned
|
- sand-boxer profile;
|
||||||
- Memory and skills layout conventions
|
- tool profile;
|
||||||
- Channel allowlist
|
- model provider, route, class, and explicit model identifier;
|
||||||
- Model routing hints (consumer of `llm-connect`, not owner)
|
- token, timeout, and turn limits; and
|
||||||
- Registered in `registry/` via reuse-surface
|
- 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:
|
Example pairing:
|
||||||
|
|
||||||
|
|
@ -268,11 +301,11 @@ for integration design only.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sandbox consumption contract (preliminary)
|
## Sandbox consumption contract
|
||||||
|
|
||||||
When harness policy requires isolation, glas-harness:
|
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 }`
|
2. Calls sand-boxer `create` with `consumer: { harness: glas-harness, session_id, actor }`
|
||||||
3. Stores `sandbox_id` and reachability descriptor on the session
|
3. Stores `sandbox_id` and reachability descriptor on the session
|
||||||
4. Routes `exec`, `read`, `write`, `edit`, and related tools through that handle
|
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**.
|
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?
|
- 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?
|
- 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
|
Implemented: strict contract version `1.0`, executable profile catalog,
|
||||||
2. **Harness profile schema sketch** — distinct from sand-boxer profile schema
|
rein-registry validation, explicit CLI/channel selection, sand-boxer lifecycle,
|
||||||
3. **sand-boxer integration doc** — consumer contract (may start in sand-boxer repo)
|
two concrete rein constellations, and normalized State Hub evidence.
|
||||||
4. **First harness profile** — `harness.agent-dev` paired with `profile.agent-dev`
|
|
||||||
5. **CLI gateway stub** — minimal session + local tools (no channels yet)
|
Next outcomes should be driven by real consumers: an additional channel,
|
||||||
6. **Registry entry** — e.g. `capability.platform.agent-harness`
|
memory/skills integration, bounded subagent delegation, and an organizational
|
||||||
7. **State Hub session events** — tool audit envelope
|
assignment adapter. None should weaken explicit profile selection or move
|
||||||
|
workforce authority into Glas.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
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
|
## One-liner
|
||||||
|
|
||||||
Meta-framework routing between concrete agent-harness backends ("reins"),
|
Primary, profile-driven execution layer routing governed work between concrete
|
||||||
consuming sand-boxer for isolation. See `docs/adr/ADR-001-rein-harness-family.md`.
|
agent-harness backends ("reins") while consuming sand-boxer for isolation. See
|
||||||
|
`docs/adr/ADR-001-rein-harness-family.md`.
|
||||||
|
|
||||||
## Core Idea
|
## Core Idea
|
||||||
|
|
||||||
glas-harness owns the harness contract (`docs/harness-contract.md`:
|
glas-harness owns the harness contract (`docs/harness-contract.md`:
|
||||||
`start_session`/`dispatch_tool`/`end_session`) and the outer loop — profile
|
`start_session`/`dispatch_tool`/`end_session`) and the outer loop — profile
|
||||||
resolution, sandbox request/teardown via sand-boxer, actor attribution. A
|
resolution, rein/model/tool-policy selection, sandbox request/teardown via
|
||||||
rein owns its own inner agentic loop and is invoked through that contract.
|
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
|
## 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`)
|
(`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
|
runs one task through a rein, verifies, tears down
|
||||||
(`src/glas_harness/gateway.py`)
|
(`src/glas_harness/gateway.py`)
|
||||||
- Rein adapters that shell out to each rein's own CLI
|
- 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`
|
`rein_openweights.py`
|
||||||
- The rein registry (`registry/reins/`) and harness profile catalog
|
- The rein registry (`registry/reins/`) and harness profile catalog
|
||||||
(`profiles/`)
|
(`profiles/`)
|
||||||
|
- Compact, non-secret execution evidence and attribution references for State
|
||||||
|
Hub and other audit consumers
|
||||||
|
|
||||||
## Out of Scope
|
## Out of Scope
|
||||||
|
|
||||||
- Sandbox provisioning, profiles, placement — owned by `sand-boxer`
|
- Sandbox provisioning, profiles, placement — owned by `sand-boxer`
|
||||||
- A rein's own agentic loop, tool execution, credential acquisition —
|
- A rein's own agentic loop, tool execution, credential acquisition —
|
||||||
owned by the rein itself (`rein-aharness`, `rein-openweights`, ...)
|
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`)
|
(`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`,
|
- e2e validation, code generation — owned by `wise-validator`,
|
||||||
`snuggle-inventor`
|
`snuggle-inventor`
|
||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
`GLAS-WP-0001` (harness contract + first two reins) is done. Both
|
The contract and first two reins are implemented. Profiles are executable,
|
||||||
`rein-aharness` and `rein-openweights` are live-proven through the gateway
|
versioned runtime inputs; CLI and gateway invocation require an explicit
|
||||||
against their real underlying providers (Claude Code CLI, OpenRouter).
|
profile; and both rein/model constellations are live-proven with real commits,
|
||||||
Not yet built: channel bridges, memory/skills layer, glas-harness's own
|
sandbox cleanup, and the same evidence shape.
|
||||||
State Hub reporting from the gateway (currently each rein reports its own
|
The CLI channel and Glas-owned State Hub gateway reporting are implemented.
|
||||||
events).
|
|
||||||
|
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
|
## Getting Oriented
|
||||||
|
|
||||||
- Start with: `INTENT.md`, `docs/adr/ADR-001-rein-harness-family.md`
|
- 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`
|
- Agent instructions: `AGENTS.md`
|
||||||
- Workplans: `workplans/`
|
- Workplans: `workplans/`
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
| workplan | GLAS-WP-0001 | finished | — | workplans/GLAS-WP-0001-harness-router-foundation.md |
|
| 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-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-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-T01 | done | — | workplans/GLAS-0001-statehub-bootstrap.md |
|
||||||
| task | GLAS-0001-T02 | 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 |
|
| 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-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-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-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
|
# Channel contract
|
||||||
|
|
||||||
How an external invocation reaches the gateway, per `GLAS-WP-0003`.
|
A channel translates an external invocation into profile-driven gateway input;
|
||||||
Mirrors `docs/harness-contract.md`'s `Rein` at a different layer: `Rein`
|
it does not select a rein class or execute a provider itself.
|
||||||
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
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@dataclass
|
@dataclass
|
||||||
class GatewayInvocation:
|
class GatewayInvocation:
|
||||||
"""Everything run_task_through_rein needs, channel-agnostic."""
|
harness_profile: str
|
||||||
sandbox_profile: str
|
|
||||||
repo: str
|
repo: str
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
actor: str = "agt"
|
actor: str = "agt"
|
||||||
project: str = "glas-harness"
|
project: str = "glas-harness"
|
||||||
report_to_hub: bool = True
|
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
|
`CLIChannel` is the current implementation. The CLI requires
|
||||||
or run the gateway itself — `cli.py`'s `main()` still does that, calling
|
`--harness-profile <id[@version]>`; the old sandbox-only input is removed
|
||||||
`parse_invocation`/`render_result` around the existing
|
because it could not identify the rein, model route, tool policy, or contract
|
||||||
`run_task_through_rein` call. Keeping channel construction and gateway
|
revision. Channels may obtain a profile choice from their own approved
|
||||||
invocation separate (rather than folding gateway-calling into the
|
configuration, but must pass it explicitly and must surface resolution refusal
|
||||||
`Channel` ABC itself) is what lets `CLIChannel` be a pure
|
to their caller.
|
||||||
input/output mapper, easy to unit test without touching sand-boxer or a
|
|
||||||
rein at all.
|
|
||||||
|
|
||||||
## 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
|
## Adding a channel
|
||||||
`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 second channel (not done — pattern only)
|
Implement `parse_invocation` and `render_result`, then call `run_execution`
|
||||||
|
with an `ExecutionRequest`. Carry any assignment/role/duty/goal/resource
|
||||||
No second channel exists yet, and none should be built speculatively —
|
references supplied by the upstream workforce system. Do not duplicate profile
|
||||||
same discipline as ADR-002/ADR-004 (generalize from a real second
|
resolution, sandbox coordination, leadership decisions, or rein construction in
|
||||||
example, not in anticipation of one). If/when one is needed, e.g. a
|
the channel.
|
||||||
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.
|
|
||||||
|
|
|
||||||
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
|
# Harness contract
|
||||||
|
|
||||||
The interface a concrete harness backend ("rein") implements so glas-harness
|
Contract version: `1.0` (`glas_harness.contract.CONTRACT_VERSION`).
|
||||||
can route to it, per `docs/adr/ADR-001-rein-harness-family.md`. Mirrors
|
|
||||||
sand-boxer's `SandboxExtension` ABC (`provision`/`wait_ready`/`teardown`) at
|
|
||||||
the harness level, one layer up: sand-boxer establishes *where* dangerous
|
|
||||||
work runs, this contract governs *how an agent session runs* on top of it.
|
|
||||||
|
|
||||||
## Interface
|
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
|
```python
|
||||||
class Rein(ABC):
|
class Rein(ABC):
|
||||||
"""Base class for concrete harness backends (reins)."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def start_session(
|
def start_session(
|
||||||
self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
|
self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]: ...
|
||||||
"""Begin an agent session bound to a sandbox. Returns a session handle."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def dispatch_tool(
|
def dispatch_tool(
|
||||||
self, session: dict[str, str], tool_call: ToolCall
|
self, session: dict[str, str], tool_call: ToolCall
|
||||||
) -> ToolResult:
|
) -> ToolResult: ...
|
||||||
"""Run one tool call under the session's policy. Returns result + audit envelope."""
|
|
||||||
|
|
||||||
@abstractmethod
|
def end_session(self, session: dict[str, str]) -> ExecutionSummary: ...
|
||||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
|
||||||
"""Close the session. Returns a summary (commit sha, tokens, duration, outcome)."""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `start_session` resolves the rein's own setup (persona/blueprint loading,
|
```text
|
||||||
credential acquisition) against the sandbox handle glas-harness already
|
Glas rein sand-boxer
|
||||||
obtained from sand-boxer — the rein never provisions or tears down a
|
---- ---- ----------
|
||||||
sandbox itself.
|
resolve profile + rein descriptor
|
||||||
- `dispatch_tool` enforces the tool-profile allow-list glas-harness resolved
|
create sandbox ------------------------------------------> create
|
||||||
(the same allow-list concept rein-aharness's `profiles.py` already
|
start_session(profile, inputs, handle) -> rein setup
|
||||||
implements today, just invoked through the contract instead of only the
|
dispatch_tool(session, call) -> inner loop
|
||||||
rein's own CLI).
|
end_session(session) -> normalized facts
|
||||||
- `end_session` is where the existing "commit exists = success" signal
|
destroy sandbox -----------------------------------------> destroy
|
||||||
(rein-aharness) or an equivalent per-rein success criterion is reported
|
publish compact ExecutionEvidence
|
||||||
back to glas-harness for State Hub posting. glas-harness posts the audit
|
|
||||||
event; the rein returns the facts.
|
|
||||||
|
|
||||||
## Session lifecycle (who calls what)
|
|
||||||
|
|
||||||
```
|
|
||||||
glas-harness rein sand-boxer
|
|
||||||
──────────── ──── ──────────
|
|
||||||
resolve harness.* profile
|
|
||||||
request sandbox ──────────────────────────────▶ create()
|
|
||||||
receive sandbox handle ◀──────────────────────────────
|
|
||||||
start_session(profile, in, sbx) ─▶ acquire creds, load persona
|
|
||||||
◀─ session handle
|
|
||||||
dispatch_tool(session, call) ─▶ run under allow-list
|
|
||||||
◀─ result + audit envelope
|
|
||||||
(repeat dispatch_tool ...)
|
|
||||||
end_session(session) ─▶ verify success, summarize
|
|
||||||
◀─ summary
|
|
||||||
post State Hub event
|
|
||||||
request sandbox teardown ──────────────────────────────▶ destroy()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
glas-harness owns the *outer* loop (profile resolution, sandbox
|
There is no production default rein. Direct `Rein` injection remains a narrow
|
||||||
request/teardown, State Hub reporting, actor attribution). The rein owns
|
library/test seam but still requires a valid profile so profile, sandbox,
|
||||||
the *inner* loop (its own agentic reasoning, whatever that looks like —
|
model, tool policy, and evidence are explicit.
|
||||||
Claude Code CLI subprocess for `rein-aharness`, a direct OpenRouter
|
|
||||||
tool-use loop for `rein-openweights`).
|
|
||||||
|
|
||||||
## Actor attribution
|
## Stability and extension rules
|
||||||
|
|
||||||
Every `dispatch_tool` call and lifecycle transition carries an actor
|
All contract models use Pydantic with unknown fields forbidden. Stable `1.0`
|
||||||
(`adm`/`agt`/`atm`) per glas-harness INTENT.md's audit requirement.
|
fields are the declared fields in `src/glas_harness/contract.py`. Optional
|
||||||
glas-harness stamps this at dispatch time; reins do not need their own
|
measurements such as tokens, duration, resolved model, commit, and artifacts
|
||||||
actor model.
|
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
|
Sensitive values are never contract extensions. Profiles may contain
|
||||||
raised whether monitoring/evaluation/optimization should become their
|
credential-route references, while credential resolution and provider setup
|
||||||
own composable reins — middleware that wraps or observes a base rein's
|
remain rein-local under ADR-002.
|
||||||
`dispatch_tool` calls, rather than each being a complete alternative
|
|
||||||
harness backend the way `rein-aharness`/`rein-openweights` are. Resolved
|
|
||||||
in `docs/adr/ADR-004-composable-reins-stay-deferred.md`: **still
|
|
||||||
deferred, sketch recorded, no code written.** The two observability
|
|
||||||
additions since (rein-aharness's per-tool-call audit stream,
|
|
||||||
`glas-harness`'s own gateway hub event) both turned out to be simpler as
|
|
||||||
direct implementations — neither needed a separate wrapping rein — so
|
|
||||||
there still isn't a second real capability wanting this shape, only the
|
|
||||||
original one (llm-connect's optional Functional-layer modules,
|
|
||||||
untouched).
|
|
||||||
|
|
||||||
If a real second candidate appears, the shape would be:
|
## Failure semantics
|
||||||
|
|
||||||
```python
|
The gateway returns evidence for every normal refusal/failure path:
|
||||||
class Middleware(ABC):
|
|
||||||
"""Wraps another Rein's dispatch_tool, does not replace start_session/end_session."""
|
|
||||||
|
|
||||||
def __init__(self, inner: Rein) -> None:
|
- `resolution`: unknown, disabled, ambiguous, incompatible, or unsafe profile;
|
||||||
self.inner = inner
|
- `sandbox_create`;
|
||||||
|
- `session_start`;
|
||||||
|
- `execution` (including a rein-declared unsuccessful result);
|
||||||
|
- `session_end`; and
|
||||||
|
- `teardown`.
|
||||||
|
|
||||||
@abstractmethod
|
Profile resolution happens before sandbox creation. Once a sandbox exists,
|
||||||
def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult:
|
teardown is attempted on every path. `refused` means governed execution did not
|
||||||
"""Call self.inner.dispatch_tool(...), observe/transform, return the result."""
|
proceed; `failed` means an attempted lifecycle did not complete successfully.
|
||||||
|
|
||||||
def start_session(self, profile, inputs, sandbox) -> dict[str, str]:
|
## Deliberate non-goals
|
||||||
return self.inner.start_session(profile, inputs, sandbox)
|
|
||||||
|
|
||||||
def end_session(self, session) -> dict[str, str]:
|
The contract does not schedule work, source blueprints, allocate workers,
|
||||||
return self.inner.end_session(session)
|
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.
|
||||||
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.
|
|
||||||
|
|
|
||||||
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
|
id: harness.agent-dev-local
|
||||||
version: "1.0.0"
|
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
|
sandbox_profile: profile.bwrap-local
|
||||||
tool_profile: green-commit-only
|
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:
|
metadata:
|
||||||
latency_class: fast
|
latency_class: fast
|
||||||
|
stream_tool_events: true
|
||||||
# Pairs with sand-boxer's profile.bwrap-local (ext.bwrap, same-host,
|
# 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
|
# no SSH hop) — for CI/local dev where the compose-ssh remote-host
|
||||||
# round trip is unnecessary overhead.
|
# 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
|
id: harness.agent-dev
|
||||||
version: "1.0.0"
|
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
|
sandbox_profile: profile.agent-dev
|
||||||
tool_profile: green-commit-only
|
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:
|
metadata:
|
||||||
latency_class: standard
|
latency_class: standard
|
||||||
|
stream_tool_events: true
|
||||||
# Pairs with sand-boxer's profile.agent-dev (ext.compose-ssh, remote
|
# Pairs with sand-boxer's profile.agent-dev (ext.compose-ssh, remote
|
||||||
# host over SSH). Use harness.agent-dev-local for fast local iteration
|
# host over SSH). Use harness.agent-dev-local for fast local iteration
|
||||||
# without the SSH hop.
|
# without the SSH hop.
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ dev = ["pytest>=8"]
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/glas_harness"]
|
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]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
pythonpath = ["src"]
|
pythonpath = ["src"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,18 @@
|
||||||
|
|
||||||
Markdown-first capability index for federation and reuse planning.
|
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
|
## Authoring
|
||||||
|
|
||||||
1. Copy a capability entry template (see reuse-surface `templates/capability-entry.template.md`).
|
1. Copy a capability entry template (see reuse-surface `templates/capability-entry.template.md`).
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
id: rein-aharness
|
id: rein-aharness
|
||||||
|
version: "0.1.0"
|
||||||
title: Claude Code CLI rein
|
title: Claude Code CLI rein
|
||||||
description: >
|
description: >
|
||||||
Governed, unattended/scheduled harness driving Claude Code CLI sessions.
|
Governed, unattended/scheduled harness driving Claude Code CLI sessions.
|
||||||
Renamed from agent-harness (ADR-001). Deployed on Railiance for tenant
|
Renamed from agent-harness (ADR-001). Deployed on Railiance for tenant
|
||||||
binky-control.
|
binky-control.
|
||||||
handler: glas_harness.reins.rein_aharness:ReinAharness
|
handler: glas_harness.reins.rein_aharness:ReinAharness
|
||||||
|
contract_versions: ["1.0"]
|
||||||
capabilities:
|
capabilities:
|
||||||
session_style: unattended
|
session_style: unattended
|
||||||
model_class: frontier
|
model_class: frontier
|
||||||
credential_source: rein-local
|
credential_source: rein-local
|
||||||
# rein-local: kaizen-agentic (blueprints), activity-core/issue-core
|
tool_event_visibility: complete
|
||||||
# (intake), OpenBao/ops-warden (credentials) — see
|
|
||||||
# rein-aharness/workplans/HARNESS-WP-0002-T04 for whether this stays
|
|
||||||
# rein-local or moves into glas-harness.
|
|
||||||
status: implemented
|
status: implemented
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
id: rein-openweights
|
id: rein-openweights
|
||||||
|
version: "0.1.0"
|
||||||
title: OpenRouter open-weight agentic loop
|
title: OpenRouter open-weight agentic loop
|
||||||
description: >
|
description: >
|
||||||
Agentic tool-use harness driving current open-weight models via
|
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
|
rein-openweights/workplans/REIN-OW-WP-0001-T01), as an alternative to
|
||||||
frontier-vendor CLIs. Chartered by ADR-001.
|
frontier-vendor CLIs. Chartered by ADR-001.
|
||||||
handler: glas_harness.reins.rein_openweights:ReinOpenWeights
|
handler: glas_harness.reins.rein_openweights:ReinOpenWeights
|
||||||
|
contract_versions: ["1.0"]
|
||||||
capabilities:
|
capabilities:
|
||||||
session_style: unattended
|
session_style: unattended
|
||||||
model_class: open-weight
|
model_class: open-weight
|
||||||
credential_source: rein-local
|
credential_source: rein-local
|
||||||
# rein-local per ADR-002 (Option B): rein-openweights acquires its own
|
tool_event_visibility: unavailable
|
||||||
# OpenRouter credential (OpenBao, or OPENROUTER_API_KEY env var).
|
|
||||||
# glas-harness does not broker it.
|
|
||||||
status: implemented
|
status: implemented
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from typing import Any
|
||||||
class GatewayInvocation:
|
class GatewayInvocation:
|
||||||
"""Everything run_task_through_rein needs, channel-agnostic."""
|
"""Everything run_task_through_rein needs, channel-agnostic."""
|
||||||
|
|
||||||
sandbox_profile: str
|
harness_profile: str
|
||||||
repo: str
|
repo: str
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from glas_harness.channels.base import Channel, GatewayInvocation
|
||||||
class CLIChannel(Channel):
|
class CLIChannel(Channel):
|
||||||
def parse_invocation(self, raw: argparse.Namespace) -> GatewayInvocation:
|
def parse_invocation(self, raw: argparse.Namespace) -> GatewayInvocation:
|
||||||
return GatewayInvocation(
|
return GatewayInvocation(
|
||||||
sandbox_profile=raw.sandbox_profile,
|
harness_profile=raw.harness_profile,
|
||||||
repo=raw.repo,
|
repo=raw.repo,
|
||||||
title=raw.title,
|
title=raw.title,
|
||||||
description=raw.description,
|
description=raw.description,
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,14 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(prog="glas-harness")
|
parser = argparse.ArgumentParser(prog="glas-harness")
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
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 = sub.add_parser(
|
||||||
run.add_argument("--sandbox-profile", required=True, help="e.g. profile.bwrap-local")
|
"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("--repo", required=True, help="Local repo path to mirror into the sandbox")
|
||||||
run.add_argument("--title", required=True)
|
run.add_argument("--title", required=True)
|
||||||
run.add_argument("--description", 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("--project", default="glas-harness")
|
||||||
run.add_argument("--no-hub", action="store_true", help="Skip the gateway's own hub reporting")
|
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)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
if args.command == "run":
|
if args.command == "run":
|
||||||
from glas_harness.channels.cli_channel import CLIChannel
|
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()
|
channel = CLIChannel()
|
||||||
invocation = channel.parse_invocation(args)
|
invocation = channel.parse_invocation(args)
|
||||||
result = run_task_through_rein(
|
result = run_execution(
|
||||||
sandbox_profile=invocation.sandbox_profile,
|
ExecutionRequest(
|
||||||
repo=invocation.repo,
|
harness_profile_ref=invocation.harness_profile,
|
||||||
title=invocation.title,
|
repo=invocation.repo,
|
||||||
description=invocation.description,
|
title=invocation.title,
|
||||||
actor=invocation.actor,
|
description=invocation.description,
|
||||||
project=invocation.project,
|
actor=invocation.actor,
|
||||||
report_to_hub=invocation.report_to_hub,
|
project=invocation.project,
|
||||||
|
report_to_hub=invocation.report_to_hub,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
print(channel.render_result(result))
|
print(channel.render_result(result.model_dump(mode="json")))
|
||||||
return 0 if result["tool_ok"] else 1
|
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
|
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 owns the outer lifecycle and the stable boundary models. A rein owns its
|
||||||
GLAS-WP-0001-T01. glas-harness owns the outer loop (profile resolution,
|
inner agentic loop. Contract models reject unknown fields so a profile or
|
||||||
sandbox request/teardown via sand-boxer, State Hub reporting, actor
|
backend cannot silently widen the execution surface.
|
||||||
attribution); a rein owns the inner agentic loop.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass, field
|
from typing import Any, Literal
|
||||||
from typing import Any
|
|
||||||
|
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 ContractModel(BaseModel):
|
||||||
class SandboxHandle:
|
model_config = ConfigDict(extra="forbid")
|
||||||
"""What glas-harness got back from sand-boxer's create()."""
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
sandbox_id: str
|
||||||
host: str
|
host: str
|
||||||
reachability: dict[str, Any] = field(default_factory=dict)
|
reachability: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class ToolCall(ContractModel):
|
||||||
class ToolCall:
|
|
||||||
name: str
|
name: str
|
||||||
args: dict[str, Any] = field(default_factory=dict)
|
args: dict[str, Any] = Field(default_factory=dict)
|
||||||
actor: str = "agt"
|
actor: str = "agt"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class ToolResult(ContractModel):
|
||||||
class ToolResult:
|
|
||||||
ok: bool
|
ok: bool
|
||||||
output: str = ""
|
output: str = ""
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
# Per-tool-call audit trail, populated when the rein can observe its own
|
events: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
# inner loop's individual tool invocations in real time (e.g. parsing
|
events_completeness: Literal["complete", "partial", "unavailable"] = "unavailable"
|
||||||
# `claude --output-format stream-json`). Empty when a rein only exposes
|
tokens_spent: int | None = Field(default=None, ge=0)
|
||||||
# its whole task as one opaque call — that's a real limit of CLI-wrapped
|
duration_s: float | None = Field(default=None, ge=0)
|
||||||
# agents, not a shortcut: neither Claude Code nor rein-openweights's own
|
resolved_model: str | None = None
|
||||||
# loop lets glas-harness externally execute individual tool calls, only
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
# observe them. See HARNESS-WP-0002-T03.
|
|
||||||
events: list[dict[str, Any]] = field(default_factory=list)
|
|
||||||
|
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):
|
class Rein(ABC):
|
||||||
|
|
@ -49,14 +204,14 @@ class Rein(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def start_session(
|
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]:
|
) -> dict[str, str]:
|
||||||
"""Begin an agent session bound to a sandbox. Returns a session handle."""
|
"""Begin an agent session bound to a sandbox."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult:
|
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
|
@abstractmethod
|
||||||
def end_session(self, session: dict[str, str]) -> dict[str, str]:
|
def end_session(self, session: dict[str, str]) -> ExecutionSummary:
|
||||||
"""Close the session. Returns a summary (commit sha, outcome, ...)."""
|
"""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,
|
The gateway resolves a versioned harness profile before creating a sandbox.
|
||||||
dispatch one rein-aharness task through the Rein contract, verify a
|
There is deliberately no default rein or model in governed execution.
|
||||||
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).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
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.core.manager import SandboxManager
|
||||||
from sandboxer.models import Consumer, SandboxCreateRequest
|
from sandboxer.models import Consumer, SandboxCreateRequest
|
||||||
|
|
||||||
from glas_harness import hub
|
from glas_harness import hub
|
||||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
from glas_harness.contract import (
|
||||||
from glas_harness.reins.rein_aharness import ReinAharness
|
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(
|
def run_task_through_rein(
|
||||||
*,
|
*,
|
||||||
sandbox_profile: str,
|
harness_profile: str,
|
||||||
repo: str,
|
repo: str,
|
||||||
title: str,
|
title: str,
|
||||||
description: str,
|
description: str,
|
||||||
|
|
@ -39,89 +203,113 @@ def run_task_through_rein(
|
||||||
actor: str = "agt",
|
actor: str = "agt",
|
||||||
project: str = "glas-harness",
|
project: str = "glas-harness",
|
||||||
manager: SandboxManager | None = None,
|
manager: SandboxManager | None = None,
|
||||||
|
catalog: ProfileCatalog | None = None,
|
||||||
report_to_hub: bool = True,
|
report_to_hub: bool = True,
|
||||||
) -> dict[str, Any]:
|
) -> dict:
|
||||||
"""Resolve `sandbox_profile`, run one task inside it via `rein`, verify, tear down.
|
"""Compatibility-shaped wrapper around the versioned execution request.
|
||||||
|
|
||||||
Defaults to `ReinAharness` when no rein is supplied — the only
|
It intentionally requires a harness profile. Direct rein injection is
|
||||||
implemented rein as of GLAS-WP-0001-T04. `rein-openweights` plugs in
|
retained only as a library/test seam and never selects a default backend.
|
||||||
the same way once REIN-OW-WP-0001 lands.
|
|
||||||
"""
|
"""
|
||||||
manager = manager or SandboxManager()
|
|
||||||
rein = rein or ReinAharness()
|
|
||||||
|
|
||||||
request = SandboxCreateRequest(
|
result = run_execution(
|
||||||
profile=sandbox_profile,
|
ExecutionRequest(
|
||||||
inputs={"repo": repo},
|
harness_profile_ref=harness_profile,
|
||||||
consumer=Consumer(actor=actor, project=project),
|
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)
|
return result.model_dump(mode="json")
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _post_gateway_event(
|
def _build_result(
|
||||||
*,
|
*,
|
||||||
rein: Rein,
|
request: ExecutionRequest,
|
||||||
sandbox_profile: str,
|
request_id: str,
|
||||||
sandbox_id: str,
|
started_at: str,
|
||||||
project: str,
|
started: float,
|
||||||
actor: str,
|
outcome: str,
|
||||||
title: str,
|
failure_stage: str | None,
|
||||||
result: dict[str, Any] | None,
|
|
||||||
error: str | None,
|
error: str | None,
|
||||||
) -> None:
|
profile,
|
||||||
ok = bool(result and result.get("tool_ok"))
|
descriptor,
|
||||||
hub.post_progress_event(
|
sandbox_id: str | None,
|
||||||
summary=f"gateway run: {title} ({'ok' if ok else 'failed'})",
|
tool_result: ToolResult | None,
|
||||||
event_type="gateway_run",
|
summary: ExecutionSummary | None,
|
||||||
detail={
|
refs: dict,
|
||||||
"sandbox_profile": sandbox_profile,
|
) -> GatewayResult:
|
||||||
"sandbox_id": sandbox_id,
|
duration = max(0.0, time.monotonic() - started)
|
||||||
"rein": type(rein).__name__,
|
resolved_model = (
|
||||||
"project": project,
|
(summary.resolved_model if summary else None)
|
||||||
"actor": actor,
|
or (tool_result.resolved_model if tool_result else None)
|
||||||
"task_title": title,
|
or (profile.model.model if profile else None)
|
||||||
"ok": ok,
|
)
|
||||||
"result": result,
|
tokens_spent = (
|
||||||
"error": error,
|
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)
|
json.dump(task_spec, fd)
|
||||||
fd.close()
|
fd.close()
|
||||||
return fd.name
|
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
|
import subprocess
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
from glas_harness.contract import (
|
||||||
from glas_harness.reins._shared import git_head, write_task_file
|
ExecutionSummary,
|
||||||
|
HarnessProfile,
|
||||||
|
Rein,
|
||||||
|
SandboxHandle,
|
||||||
|
ToolCall,
|
||||||
|
ToolResult,
|
||||||
|
)
|
||||||
|
from glas_harness.reins._shared import git_head, parse_json_object, write_task_file
|
||||||
|
|
||||||
|
|
||||||
class ReinAharnessNotInstalled(RuntimeError):
|
class ReinAharnessNotInstalled(RuntimeError):
|
||||||
|
|
@ -34,9 +41,20 @@ class ReinAharnessNotInstalled(RuntimeError):
|
||||||
|
|
||||||
|
|
||||||
class ReinAharness(Rein):
|
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.cli_bin = cli_bin
|
||||||
self.stream_tool_events = stream_tool_events
|
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:
|
def _bin(self) -> str:
|
||||||
resolved = shutil.which(self.cli_bin)
|
resolved = shutil.which(self.cli_bin)
|
||||||
|
|
@ -47,7 +65,7 @@ class ReinAharness(Rein):
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
def start_session(
|
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]:
|
) -> dict[str, str]:
|
||||||
target_repo = (
|
target_repo = (
|
||||||
inputs.get("target_repo")
|
inputs.get("target_repo")
|
||||||
|
|
@ -58,7 +76,11 @@ class ReinAharness(Rein):
|
||||||
raise ValueError("no target_repo resolvable from inputs or sandbox reachability")
|
raise ValueError("no target_repo resolvable from inputs or sandbox reachability")
|
||||||
|
|
||||||
task_file = inputs.get("task_file") or write_task_file(
|
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 {
|
return {
|
||||||
|
|
@ -79,10 +101,29 @@ class ReinAharness(Rein):
|
||||||
]
|
]
|
||||||
if self.stream_tool_events:
|
if self.stream_tool_events:
|
||||||
argv.append("--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)
|
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||||
ok = proc.returncode == 0
|
ok = proc.returncode == 0
|
||||||
output, events = self._split_stream_events(proc.stdout)
|
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
|
@staticmethod
|
||||||
def _split_stream_events(stdout: str) -> tuple[str, list[dict[str, Any]]]:
|
def _split_stream_events(stdout: str) -> tuple[str, list[dict[str, Any]]]:
|
||||||
|
|
@ -105,10 +146,25 @@ class ReinAharness(Rein):
|
||||||
remaining_lines.append(line)
|
remaining_lines.append(line)
|
||||||
return "\n".join(remaining_lines), events
|
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"])
|
head_after = git_head(session["target_repo"])
|
||||||
committed = bool(head_after) and head_after != session.get("head_before")
|
committed = bool(head_after) and head_after != session.get("head_before")
|
||||||
return {
|
reported_ok = bool(self._last_result.get("ok", committed))
|
||||||
"commit_sha": head_after or "",
|
reason = self._last_result.get("reason") or None
|
||||||
"committed": str(committed),
|
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
|
import subprocess
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
from glas_harness.contract import (
|
||||||
from glas_harness.reins._shared import git_head, write_task_file
|
ExecutionSummary,
|
||||||
|
HarnessProfile,
|
||||||
|
Rein,
|
||||||
|
SandboxHandle,
|
||||||
|
ToolCall,
|
||||||
|
ToolResult,
|
||||||
|
)
|
||||||
|
from glas_harness.reins._shared import git_head, parse_json_object, write_task_file
|
||||||
|
|
||||||
|
|
||||||
class ReinOpenWeightsNotInstalled(RuntimeError):
|
class ReinOpenWeightsNotInstalled(RuntimeError):
|
||||||
|
|
@ -22,9 +29,20 @@ class ReinOpenWeightsNotInstalled(RuntimeError):
|
||||||
|
|
||||||
|
|
||||||
class ReinOpenWeights(Rein):
|
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.cli_bin = cli_bin
|
||||||
self.model = model
|
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:
|
def _bin(self) -> str:
|
||||||
resolved = shutil.which(self.cli_bin)
|
resolved = shutil.which(self.cli_bin)
|
||||||
|
|
@ -35,7 +53,7 @@ class ReinOpenWeights(Rein):
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
def start_session(
|
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]:
|
) -> dict[str, str]:
|
||||||
target_repo = (
|
target_repo = (
|
||||||
inputs.get("target_repo")
|
inputs.get("target_repo")
|
||||||
|
|
@ -46,7 +64,10 @@ class ReinOpenWeights(Rein):
|
||||||
raise ValueError("no target_repo resolvable from inputs or sandbox reachability")
|
raise ValueError("no target_repo resolvable from inputs or sandbox reachability")
|
||||||
|
|
||||||
task_file = inputs.get("task_file") or write_task_file(
|
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 {
|
return {
|
||||||
|
|
@ -60,14 +81,48 @@ class ReinOpenWeights(Rein):
|
||||||
argv = [self._bin(), "run", "--task-file", session["task_file"], "--no-hub"]
|
argv = [self._bin(), "run", "--task-file", session["task_file"], "--no-hub"]
|
||||||
if self.model:
|
if self.model:
|
||||||
argv += ["--model", 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)
|
proc = subprocess.run(argv, capture_output=True, text=True)
|
||||||
ok = proc.returncode == 0
|
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"])
|
head_after = git_head(session["target_repo"])
|
||||||
committed = bool(head_after) and head_after != session.get("head_before")
|
committed = bool(head_after) and head_after != session.get("head_before")
|
||||||
return {
|
reported_ok = bool(self._last_result.get("ok", committed))
|
||||||
"commit_sha": head_after or "",
|
reason = self._last_result.get("reason") or None
|
||||||
"committed": str(committed),
|
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:
|
def _args(**overrides) -> argparse.Namespace:
|
||||||
defaults = dict(
|
defaults = dict(
|
||||||
sandbox_profile="profile.bwrap-local",
|
harness_profile="harness.agent-dev-local@1.0.0",
|
||||||
repo="/tmp/repo",
|
repo="/tmp/repo",
|
||||||
title="t",
|
title="t",
|
||||||
description="d",
|
description="d",
|
||||||
|
|
@ -28,7 +28,7 @@ def test_parse_invocation_maps_all_fields() -> None:
|
||||||
invocation = channel.parse_invocation(_args())
|
invocation = channel.parse_invocation(_args())
|
||||||
|
|
||||||
assert invocation == GatewayInvocation(
|
assert invocation == GatewayInvocation(
|
||||||
sandbox_profile="profile.bwrap-local",
|
harness_profile="harness.agent-dev-local@1.0.0",
|
||||||
repo="/tmp/repo",
|
repo="/tmp/repo",
|
||||||
title="t",
|
title="t",
|
||||||
description="d",
|
description="d",
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,18 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from sandboxer.models import Reachability, SandboxState, SandboxStatus
|
from sandboxer.models import Reachability, SandboxState, SandboxStatus
|
||||||
|
|
||||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall, ToolResult
|
from glas_harness.contract import (
|
||||||
from glas_harness.gateway import run_task_through_rein
|
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):
|
class _FakeRein(Rein):
|
||||||
|
|
@ -13,6 +23,7 @@ class _FakeRein(Rein):
|
||||||
|
|
||||||
def start_session(self, profile, inputs, sandbox: SandboxHandle):
|
def start_session(self, profile, inputs, sandbox: SandboxHandle):
|
||||||
self.calls.append("start_session")
|
self.calls.append("start_session")
|
||||||
|
assert str(profile.ref) == PROFILE
|
||||||
assert sandbox.sandbox_id == "sbx1"
|
assert sandbox.sandbox_id == "sbx1"
|
||||||
assert sandbox.reachability.get("workspace_dir") == "/tmp/ws"
|
assert sandbox.reachability.get("workspace_dir") == "/tmp/ws"
|
||||||
return {"session": "s1"}
|
return {"session": "s1"}
|
||||||
|
|
@ -20,11 +31,24 @@ class _FakeRein(Rein):
|
||||||
def dispatch_tool(self, session, tool_call: ToolCall) -> ToolResult:
|
def dispatch_tool(self, session, tool_call: ToolCall) -> ToolResult:
|
||||||
self.calls.append("dispatch_tool")
|
self.calls.append("dispatch_tool")
|
||||||
assert tool_call.name == "run_task"
|
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):
|
def end_session(self, session):
|
||||||
self.calls.append("end_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:
|
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 = MagicMock()
|
||||||
manager.create.return_value = _fake_status()
|
manager.create.return_value = _fake_status()
|
||||||
rein = _FakeRein()
|
rein = _FakeRein()
|
||||||
|
|
||||||
result = run_task_through_rein(
|
result = run_execution(_request(), rein=rein, manager=manager)
|
||||||
sandbox_profile="profile.bwrap-local",
|
|
||||||
repo="/tmp/repo",
|
|
||||||
title="t",
|
|
||||||
description="d",
|
|
||||||
rein=rein,
|
|
||||||
manager=manager,
|
|
||||||
report_to_hub=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert rein.calls == ["start_session", "dispatch_tool", "end_session"]
|
assert rein.calls == ["start_session", "dispatch_tool", "end_session"]
|
||||||
manager.create.assert_called_once()
|
manager.create.assert_called_once()
|
||||||
manager.destroy.assert_called_once_with("sbx1")
|
manager.destroy.assert_called_once_with("sbx1")
|
||||||
assert result["tool_ok"] is True
|
assert result.ok is True
|
||||||
assert result["summary"]["committed"] == "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 = MagicMock()
|
||||||
manager.create.return_value = _fake_status()
|
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):
|
def dispatch_tool(self, session, tool_call):
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
rein = _FailingRein()
|
result = run_execution(_request(), rein=_FailingRein(), manager=manager)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
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")
|
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 = MagicMock()
|
||||||
manager.create.return_value = _fake_status()
|
manager.create.return_value = _fake_status()
|
||||||
rein = _FakeRein()
|
|
||||||
|
|
||||||
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
|
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
|
||||||
run_task_through_rein(
|
result = run_execution(_request(report_to_hub=True), rein=_FakeRein(), manager=manager)
|
||||||
sandbox_profile="profile.bwrap-local",
|
|
||||||
repo="/tmp/repo",
|
|
||||||
title="t",
|
|
||||||
description="d",
|
|
||||||
rein=rein,
|
|
||||||
manager=manager,
|
|
||||||
)
|
|
||||||
|
|
||||||
post.assert_called_once()
|
post.assert_called_once()
|
||||||
kwargs = post.call_args.kwargs
|
detail = post.call_args.kwargs["detail"]
|
||||||
assert kwargs["event_type"] == "gateway_run"
|
assert detail["outcome"] == "succeeded"
|
||||||
assert "ok)" in kwargs["summary"]
|
assert detail["profile_ref"] == PROFILE
|
||||||
assert kwargs["detail"]["ok"] is True
|
assert "tool_output" not in detail
|
||||||
assert kwargs["detail"]["sandbox_id"] == "sbx1"
|
assert "sensitive direct output" not in str(detail)
|
||||||
assert kwargs["detail"]["rein"] == "_FakeRein"
|
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 = MagicMock()
|
||||||
manager.create.return_value = _fake_status()
|
manager.create.return_value = _fake_status()
|
||||||
|
|
||||||
class _FailingRein(_FakeRein):
|
class _FailingRein(_FakeRein):
|
||||||
def dispatch_tool(self, session, tool_call):
|
def dispatch_tool(self, session, tool_call):
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("provider body containing sensitive material")
|
||||||
|
|
||||||
rein = _FailingRein()
|
|
||||||
|
|
||||||
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
|
with patch("glas_harness.gateway.hub.post_progress_event", return_value=True) as post:
|
||||||
try:
|
result = run_execution(
|
||||||
run_task_through_rein(
|
_request(report_to_hub=True), rein=_FailingRein(), manager=manager
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
import pytest
|
||||||
|
|
||||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
||||||
|
from glas_harness.profiles import ProfileCatalog
|
||||||
from glas_harness.reins.rein_aharness import ReinAharness, ReinAharnessNotInstalled
|
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()
|
rein = ReinAharness()
|
||||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
||||||
session = rein.start_session(
|
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"},
|
inputs={"title": "t", "description": "d"},
|
||||||
sandbox=sandbox,
|
sandbox=sandbox,
|
||||||
)
|
)
|
||||||
|
|
@ -47,7 +48,11 @@ def test_start_session_requires_resolvable_target_repo() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_tool_invokes_agent_harness_cli() -> 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"}
|
session = {"task_file": "/tmp/task.json"}
|
||||||
|
|
||||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
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]
|
argv = run.call_args.args[0]
|
||||||
assert argv[:3] == ["/usr/bin/agent-harness", "run", "--task-file"]
|
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.ok is True
|
||||||
assert result.output == "ok"
|
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)
|
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})
|
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
||||||
assert summary["committed"] == "True"
|
assert summary.committed is True
|
||||||
assert summary["commit_sha"] != head_before
|
assert summary.outcome == "succeeded"
|
||||||
|
assert summary.commit_sha != head_before
|
||||||
|
|
||||||
|
|
||||||
def test_end_session_no_new_commit(tmp_path) -> None:
|
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))
|
head = git_head(str(repo))
|
||||||
|
|
||||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head})
|
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:
|
def test_dispatch_tool_streams_events_when_enabled() -> None:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
||||||
|
from glas_harness.profiles import ProfileCatalog
|
||||||
from glas_harness.reins.rein_openweights import ReinOpenWeights, ReinOpenWeightsNotInstalled
|
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()
|
rein = ReinOpenWeights()
|
||||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
||||||
session = rein.start_session(
|
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"},
|
inputs={"title": "t", "description": "d"},
|
||||||
sandbox=sandbox,
|
sandbox=sandbox,
|
||||||
)
|
)
|
||||||
|
|
@ -63,7 +64,12 @@ def test_dispatch_tool_invokes_rein_openweights_cli() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_dispatch_tool_passes_model_when_set() -> 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"}
|
session = {"task_file": "/tmp/task.json"}
|
||||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
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]
|
argv = run.call_args.args[0]
|
||||||
assert "--model" in argv
|
assert "--model" in argv
|
||||||
assert "meta-llama/llama-3.1-70b-instruct" 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:
|
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)
|
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})
|
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
||||||
assert summary["committed"] == "True"
|
assert summary.committed is True
|
||||||
assert summary["commit_sha"] != head_before
|
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