feat: add versioned execution profiles
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

This commit is contained in:
tegwick 2026-08-21 00:21:53 +02:00
parent 641e85f5a8
commit 1cd890d871
34 changed files with 2087 additions and 471 deletions

View file

@ -1,76 +1,34 @@
# Channel contract
How an external invocation reaches the gateway, per `GLAS-WP-0003`.
Mirrors `docs/harness-contract.md`'s `Rein` at a different layer: `Rein`
governs how a session runs once glas-harness has decided to run one; a
`Channel` governs how an external caller (CLI today; a future Slack
message, HTTP request, or scheduled trigger) gets translated into that
decision in the first place, and how the result gets translated back.
## Interface
A channel translates an external invocation into profile-driven gateway input;
it does not select a rein class or execute a provider itself.
```python
@dataclass
class GatewayInvocation:
"""Everything run_task_through_rein needs, channel-agnostic."""
sandbox_profile: str
harness_profile: str
repo: str
title: str
description: str
actor: str = "agt"
project: str = "glas-harness"
report_to_hub: bool = True
class Channel(ABC):
"""Base class for concrete invocation channels (CLI, Slack, HTTP, ...)."""
@abstractmethod
def parse_invocation(self, raw: Any) -> GatewayInvocation:
"""Turn a channel-specific invocation into a GatewayInvocation."""
@abstractmethod
def render_result(self, result: dict[str, Any]) -> Any:
"""Turn a gateway result dict into whatever this channel returns."""
```
`src/glas_harness/channels/base.py`. A channel does not choose the rein
or run the gateway itself — `cli.py`'s `main()` still does that, calling
`parse_invocation`/`render_result` around the existing
`run_task_through_rein` call. Keeping channel construction and gateway
invocation separate (rather than folding gateway-calling into the
`Channel` ABC itself) is what lets `CLIChannel` be a pure
input/output mapper, easy to unit test without touching sand-boxer or a
rein at all.
`CLIChannel` is the current implementation. The CLI requires
`--harness-profile <id[@version]>`; the old sandbox-only input is removed
because it could not identify the rein, model route, tool policy, or contract
revision. Channels may obtain a profile choice from their own approved
configuration, but must pass it explicitly and must surface resolution refusal
to their caller.
## CLIChannel — the first, and so far only, implementation
The channel renders the full direct `GatewayResult`. State Hub receives only
the compact `ExecutionEvidence` subset.
`src/glas_harness/channels/cli_channel.py`. Maps an argparse
`Namespace` to `GatewayInvocation` (one field each, `--no-hub` inverts
to `report_to_hub`) and renders a result dict as
`json.dumps(result, indent=2)` — exactly what `cli.py` did inline before
this existed. This was a refactor, not a behavior change: verified with
a real live run through `python3 -m glas_harness.cli run ...` producing
byte-identical output shape to the pre-refactor version, plus unit tests
for the mapping in isolation (`tests/test_cli_channel.py`).
## Adding a channel
## Adding a second channel (not done — pattern only)
No second channel exists yet, and none should be built speculatively —
same discipline as ADR-002/ADR-004 (generalize from a real second
example, not in anticipation of one). If/when one is needed, e.g. a
Slack channel:
1. Implement `Channel` in `src/glas_harness/channels/slack_channel.py`:
`parse_invocation` turns a Slack event payload into a
`GatewayInvocation` (message text → `title`/`description`, a
configured default `sandbox_profile`, the Slack user id as `actor`
or `project`); `render_result` turns the result dict into a Slack
message (success/failure, commit sha if present).
2. Whatever process receives Slack events constructs the channel and
calls `run_task_through_rein` the same way `cli.py` does — no change
needed to `gateway.py`, `contract.py`, or any rein.
3. If two real channels turn out to share invocation-parsing logic (e.g.
both need the same `sandbox_profile` defaulting rule), factor that
shared piece out then — not before, and not into the `Channel` ABC
itself unless every channel needs it.
Implement `parse_invocation` and `render_result`, then call `run_execution`
with an `ExecutionRequest`. Carry any assignment/role/duty/goal/resource
references supplied by the upstream workforce system. Do not duplicate profile
resolution, sandbox coordination, leadership decisions, or rein construction in
the channel.

View 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.

View 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.

View file

@ -1,124 +1,97 @@
# Harness contract
The interface a concrete harness backend ("rein") implements so glas-harness
can route to it, per `docs/adr/ADR-001-rein-harness-family.md`. Mirrors
sand-boxer's `SandboxExtension` ABC (`provision`/`wait_ready`/`teardown`) at
the harness level, one layer up: sand-boxer establishes *where* dangerous
work runs, this contract governs *how an agent session runs* on top of it.
Contract version: `1.0` (`glas_harness.contract.CONTRACT_VERSION`).
## Interface
This is the stable boundary between Glas, its consumers, and concrete harness
backends (reins). Glas owns profile resolution and the outer lifecycle; a rein
owns its inner agentic loop. Sand-boxer owns environment provisioning.
## Consumer boundary
`ExecutionRequest` requires an explicit `harness_profile_ref`, repository,
title, and description. It may carry correlation and organizational references
(`assignment_ref`, `role_ref`, `duty_ref`, `goal_refs`, and
`resource_envelope_refs`). Those references link execution evidence to
leadership/workforce records; they do not transfer organizational authority to
Glas.
The gateway returns `GatewayResult` with:
- `ok`, derived from normalized outcome;
- `evidence`, safe for compact State Hub reporting; and
- direct-caller-only `tool_output` and `tool_error`.
Raw prompts, model output, tool output, and credential material are excluded
from the State Hub detail.
## Rein lifecycle
```python
class Rein(ABC):
"""Base class for concrete harness backends (reins)."""
@abstractmethod
def start_session(
self, profile: HarnessProfile, inputs: dict[str, str], sandbox: SandboxHandle
) -> dict[str, str]:
"""Begin an agent session bound to a sandbox. Returns a session handle."""
) -> dict[str, str]: ...
@abstractmethod
def dispatch_tool(
self, session: dict[str, str], tool_call: ToolCall
) -> ToolResult:
"""Run one tool call under the session's policy. Returns result + audit envelope."""
) -> ToolResult: ...
@abstractmethod
def end_session(self, session: dict[str, str]) -> dict[str, str]:
"""Close the session. Returns a summary (commit sha, tokens, duration, outcome)."""
def end_session(self, session: dict[str, str]) -> ExecutionSummary: ...
```
- `start_session` resolves the rein's own setup (persona/blueprint loading,
credential acquisition) against the sandbox handle glas-harness already
obtained from sand-boxer — the rein never provisions or tears down a
sandbox itself.
- `dispatch_tool` enforces the tool-profile allow-list glas-harness resolved
(the same allow-list concept rein-aharness's `profiles.py` already
implements today, just invoked through the contract instead of only the
rein's own CLI).
- `end_session` is where the existing "commit exists = success" signal
(rein-aharness) or an equivalent per-rein success criterion is reported
back to glas-harness for State Hub posting. glas-harness posts the audit
event; the rein returns the facts.
## Session lifecycle (who calls what)
```
glas-harness rein sand-boxer
──────────── ──── ──────────
resolve harness.* profile
request sandbox ──────────────────────────────▶ create()
receive sandbox handle ◀──────────────────────────────
start_session(profile, in, sbx) ─▶ acquire creds, load persona
◀─ session handle
dispatch_tool(session, call) ─▶ run under allow-list
◀─ result + audit envelope
(repeat dispatch_tool ...)
end_session(session) ─▶ verify success, summarize
◀─ summary
post State Hub event
request sandbox teardown ──────────────────────────────▶ destroy()
```text
Glas rein sand-boxer
---- ---- ----------
resolve profile + rein descriptor
create sandbox ------------------------------------------> create
start_session(profile, inputs, handle) -> rein setup
dispatch_tool(session, call) -> inner loop
end_session(session) -> normalized facts
destroy sandbox -----------------------------------------> destroy
publish compact ExecutionEvidence
```
glas-harness owns the *outer* loop (profile resolution, sandbox
request/teardown, State Hub reporting, actor attribution). The rein owns
the *inner* loop (its own agentic reasoning, whatever that looks like —
Claude Code CLI subprocess for `rein-aharness`, a direct OpenRouter
tool-use loop for `rein-openweights`).
There is no production default rein. Direct `Rein` injection remains a narrow
library/test seam but still requires a valid profile so profile, sandbox,
model, tool policy, and evidence are explicit.
## Actor attribution
## Stability and extension rules
Every `dispatch_tool` call and lifecycle transition carries an actor
(`adm`/`agt`/`atm`) per glas-harness INTENT.md's audit requirement.
glas-harness stamps this at dispatch time; reins do not need their own
actor model.
All contract models use Pydantic with unknown fields forbidden. Stable `1.0`
fields are the declared fields in `src/glas_harness/contract.py`. Optional
measurements such as tokens, duration, resolved model, commit, and artifacts
remain `null` when unknown; unsupported tool-event visibility is
`unavailable`, not an empty claim of completeness.
## Composable reins as middleware (deferred — sketch only, no code)
Rein-specific, non-secret additions belong only in declared `metadata` fields.
Adding a required field, changing a field meaning/type, or removing a field
requires a new contract version. Adding an optional field may remain compatible
only when old consumers can safely ignore it and every strict boundary model is
updated together. A profile and rein descriptor must both declare the exact
supported contract version.
`docs/adr/ADR-002-credential-brokering-and-composable-reins.md` part 2
raised whether monitoring/evaluation/optimization should become their
own composable reins — middleware that wraps or observes a base rein's
`dispatch_tool` calls, rather than each being a complete alternative
harness backend the way `rein-aharness`/`rein-openweights` are. Resolved
in `docs/adr/ADR-004-composable-reins-stay-deferred.md`: **still
deferred, sketch recorded, no code written.** The two observability
additions since (rein-aharness's per-tool-call audit stream,
`glas-harness`'s own gateway hub event) both turned out to be simpler as
direct implementations — neither needed a separate wrapping rein — so
there still isn't a second real capability wanting this shape, only the
original one (llm-connect's optional Functional-layer modules,
untouched).
Sensitive values are never contract extensions. Profiles may contain
credential-route references, while credential resolution and provider setup
remain rein-local under ADR-002.
If a real second candidate appears, the shape would be:
## Failure semantics
```python
class Middleware(ABC):
"""Wraps another Rein's dispatch_tool, does not replace start_session/end_session."""
The gateway returns evidence for every normal refusal/failure path:
def __init__(self, inner: Rein) -> None:
self.inner = inner
- `resolution`: unknown, disabled, ambiguous, incompatible, or unsafe profile;
- `sandbox_create`;
- `session_start`;
- `execution` (including a rein-declared unsuccessful result);
- `session_end`; and
- `teardown`.
@abstractmethod
def dispatch_tool(self, session: dict[str, str], tool_call: ToolCall) -> ToolResult:
"""Call self.inner.dispatch_tool(...), observe/transform, return the result."""
Profile resolution happens before sandbox creation. Once a sandbox exists,
teardown is attempted on every path. `refused` means governed execution did not
proceed; `failed` means an attempted lifecycle did not complete successfully.
def start_session(self, profile, inputs, sandbox) -> dict[str, str]:
return self.inner.start_session(profile, inputs, sandbox)
## Deliberate non-goals
def end_session(self, session) -> dict[str, str]:
return self.inner.end_session(session)
```
A chain of `Middleware` wrapping a base `Rein` still satisfies the `Rein`
ABC itself (composition, not a parallel type), so glas-harness's gateway
would not need to change to consume one — this is deliberately *not* a
speculative gateway-side change, just a documented shape to reach for
if/when a second candidate shows up.
## Open questions this contract does not resolve yet
- The exact `SandboxHandle`/`ToolCall`/`ToolResult` field shapes — these
have stabilized in practice (both reins implement them identically)
but are not yet declared frozen/versioned the way sand-boxer's models
are.
The contract does not schedule work, source blueprints, allocate workers,
resolve leadership, broker credentials, select an unapproved model by price, or
standardize rein internals. Composable rein middleware remains deferred by
ADR-004 until a second concrete need exists.

38
docs/intakes/residuals.md Normal file
View 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.
```