channels/base.py: GatewayInvocation dataclass + Channel ABC (parse_invocation/render_result), mirroring Rein one layer up -- a channel turns an external invocation into what run_task_through_rein needs, and turns its result back into whatever the invocation medium expects. channels/cli_channel.py: CLIChannel, a pure refactor of what cli.py did inline -- argparse Namespace -> GatewayInvocation, result dict -> json.dumps(..., indent=2). cli.py's run handler is now a thin construct-parse-invoke-render wrapper. docs/channel-contract.md documents the pattern and sketches (without building) a second channel. Found and fixed a real bug while live-testing this refactor: ReinAharness's default cli_bin was still "agent-harness", stale from before HARNESS-WP-0002-T02's rename. 4 new tests (27/27 passing), plus a real live run through python3 -m glas_harness.cli against rein-aharness/real Claude Code confirming identical output shape and a real commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3.3 KiB
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
@dataclass
class GatewayInvocation:
"""Everything run_task_through_rein needs, channel-agnostic."""
sandbox_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 — the first, and so far only, implementation
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 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:
- Implement
Channelinsrc/glas_harness/channels/slack_channel.py:parse_invocationturns a Slack event payload into aGatewayInvocation(message text →title/description, a configured defaultsandbox_profile, the Slack user id asactororproject);render_resultturns the result dict into a Slack message (success/failure, commit sha if present). - Whatever process receives Slack events constructs the channel and
calls
run_task_through_reinthe same waycli.pydoes — no change needed togateway.py,contract.py, or any rein. - If two real channels turn out to share invocation-parsing logic (e.g.
both need the same
sandbox_profiledefaulting rule), factor that shared piece out then — not before, and not into theChannelABC itself unless every channel needs it.