# 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 ```python @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: 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.