Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
Renames the written-usage rule into practice across the contract surface:
docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files,
taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the
.opencode/agents/ prompts. Every replacement is a sense-preserving
rewrite (colons, semicolons, commas, parentheses, sentence splits, varied),
never a mechanical swap; no spaced hyphens were substituted. Enum fields,
frontmatter, tables, wire data, and cited identifiers survive verbatim;
en dashes inside numeric ranges keep their meaning.

Verification: taskgraph validate (43 tasks), repo grep clean (single
exempt hit: the verification command's own search pattern in AGENTS.md),
cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
2026-11-04 20:28:00 +00:00

331 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: w4-op-rows
name: "Wave 4: the client op rows (exec/pty, direct-tcpip, git exec, sftp decline)"
status: completed
depends_on: [w4-dial]
scope: moderate
risk: medium
impact: phase
level: implementation
---
## Description
Land the op rows: family op → vanilla translation per
`client.md`'s table, pointed upstream at a vanilla server (the
reverse-runner shape: the hub drives every channel open). Type
symmetry at the seam is the contract: family-protocol types in,
vanilla wire ops out, no vanilla-concept leaks up. The POC-3 §D.3
mechanics are the body of knowledge this task lands in code:
- **Session exec/pty rows** (under `session`): `channel_open_session`
+ the exec/pty-req/shell-req translations onto the alktty session
types (pipe vs PTY; exit semantics): `Channel::exec()`, with **no
`request_exec` at this pin**; Success/Failure arrive on the channel
queue via `wait()`-drain, **not** as `Err` from the request call
(drain until the expected message, `continue` past the rest);
`window_change`/`signal` are fire-and-forget and must be sent
**before** `into_stream()` (the Channel is consumed by conversion);
**never consume the channel queue before `into_stream()`**, because a
`wait()` on a fresh channel breaks it (POC-1 §6, both sides). The
data pump + the exit consumption mirror the server direction's
derivation contract (the pipe-mode pid-only signal caveat rides the
alktty exit-chunk semantics, `mapping.md`'s contract, consumed not
re-derived).
- **`direct-tcpip` typed dial** (under `tunnel`), the row the `-L`
leg and the alksocks `DialFn` compose over: `channel_open_direct_tcpip`
resolves to `Err(ChannelOpenFailure)` on server reject (POC-1 §6),
giving the typed open with no `wait()` before `into_stream`; **the connection
stays usable after a declined open** (pinned by row). The
`direct-streamlocal` twin with the runtime-Unix gate (OQ-11
symmetry: universal compile, clean off-Unix decline).
- **Git exec consumption** (under `git`), the same exec grammar the
server side parses (`git-{upload,receive}-pack '/repo'`),
**reused, not forked**, pointed upstream at a vanilla `sshd`:
parse → exec → the pack duplex pumped → exit-status consumed. The
per-length limits and service-only decline classes ride the shared
grammar's verdicts.
- **The sftp subsystem placeholder decline** (every build, OQ-13),
the client-side half of the tested decline placeholder: a
`subsystem "sftp"` open declines cleanly in every build (the
feature exists, the route doesn't until alkfs lands). The
server-side rows already landed in wave 2; this lands the client
mirror so both directions are contract-tested.
- **Contract rows through the server double, per build**, with the
`w4-dial` fake-`sshd` vehicle grown with the op-row surfaces:
featured + working rows (exec exit-status, pty window-change
ordering, direct-tcpip pump, git exec round trip), decline rows
(server-rejected opens, the sftp decline, off-Unix streamlocal),
the connection-stays-usable row after each decline class. The
real-tool smoke posture decided + recorded (a real `sshd` exec is
cheap where tooling permits, check-gated like the wave-3 smokes;
else named wave-5 e2e territory explicitly).
## Acceptance Criteria
- [x] Exec/pty rows land under `session` with the POC-3 §D.3
mechanics pinned in code + rows (`exec()` not `request_exec`;
`wait()`-drain Success/Failure; `window_change`/`signal` before
`into_stream`; queue never consumed before conversion)
- [x] `direct-tcpip` (+ streamlocal twin, runtime-Unix gate) lands
under `tunnel`: typed open, reject → `Err(ChannelOpenFailure)`,
the connection stays usable after a decline (row-pinned)
- [x] Git exec consumption lands under `git` over the shared grammar
(no forked parser); the pack duplex pumps; exit-status consumed
- [x] The sftp subsystem placeholder declines in every build (the
client-side OQ-13 mirror)
- [x] Per-build contract rows green through the server double; the
smoke posture decided + recorded
- [x] `cargo test` (+ per-feature + `--all-features`), clippy ×2
(`-D warnings`), fmt, `cargo doc --no-deps` pass
## References
- docs/architecture/client.md §Op rows (the table), §Correctness
facts, §What the crate does *not* include (job supervision is
assembly work; the crate gives channels + exit semantics)
- docs/architecture/mapping.md §The exit derivation contract (the
mirror the client consumes), docs/architecture/decisions/003-feature-graph.md
(the feature rows), docs/architecture/decisions/006-mapping-bridge-and-dispatch.md
(exec-only git, both directions)
- docs/research/poc-3-mapping-bakeoff.md §D.3 (the client mechanics,
the body of knowledge), docs/research/poc-1-auth-bridge.md §6 (the
open-call `Err` semantics), docs/research/reverse-runner.md (the
per-connection surface table, the consumer evidence)
- src/git_map.rs (the grammar reused), src/tty_map.rs (the exit-chunk
semantics consumed), russh 0.65 `client::Handle` (`channel_open_session`
/ `channel_open_direct_tcpip` / `channel_open_direct_streamlocal`)
## Notes
### What landed (2026-10-29)
- `src/client_ops.rs` (new, every build; rows feature-gated inside):
the op rows, the family op → vanilla translation surfaces, all
on
`SshConnection` (`dial`'s handle; sibling-module method impls over
the crate accessors `russh_handle()` / `budgets()`):
- *session*: `exec` (pipe), `pty_exec` (`ssh -t`), `shell(Option<
TerminalParamsWire>)` (pty with params; the pipe shell with
`None`, mirroring the server direction's decided pty-less shell
parity) → `RemoteSession`, the **family chunk plane** (§below).
- *tunnel*: `tunnel_direct(host, port)` /
`tunnel_direct_unix(path)` → `TunnelChannel` (a plain byte
duplex, which the alksocks `DialFn` composes; the `-D` leg's
composition needs no dependency, ADR-003 §1a).
- *git*: `git_service(alkgit::Service, repo_id)` →
`GitPackSession` (raw pack duplex + stderr duplex + the
consumed-exit oneshot).
- *every build*: `sftp()` → `Err(OpError::SftpNotServed)` (the
`Ok` type is `Infallible`, so the feature ships but the route
doesn't, OQ-13, now tested in both the featureless and the
featured build).
- `src/exit_code.rs` (new, extracted from `tty_map.rs`, the wave's
structural edit): the derivation contract's machinery
(`SIGNAL_NAMES` / `ExitReport` / `derive_exit` /
`sig_from_name` / `sig_control_name`) is now **two-direction
territory**; the module gained the client arm's
`signal_code_from_name` (the wire `exit-signal` name → the family
`−signum` code, `−1` for the unmapped/the sentinel collision) and
the module docs carry the client arm's rules as the same
contract's inverse. `tty_map.rs` consumes it (no fork anywhere);
`handler.rs`'s signal row follows the moved path.
- `src/error.rs`: [`OpError`], the op rows' closed taxonomy
(`OpenDecline(OpenDeclineReason)`, the *family-mirrored* copy of
the typed open's wire class,
`Other{code,reason}` verbatim, nothing collapsed /
`RequestRefused` (the `wait()`-drain's `Failure` reply, as POC-3
§D.3's not-an-`Err` fact surfaced as the op's decline class) /
`ChannelEnded` (the drain saw the channel's end; nothing
fabricated) / `CommandTooLong` (the exec-length budget's
client-mirror verdict, pre-wire) / `InvalidRepoId` (the git
validation gate mirrored, pre-wire) / `UnixUnavailable` (the OQ-11
gate, pre-wire) / `SftpNotServed` (the placeholder) /
`Wire(russh::Error)` (transparent); the `From<russh::Error>` maps
the typed open's `ChannelOpenFailure` into the family mirror,
with one conversion point, unit rows pin every class).
- `src/dispatch.rs`: the shared grammar's **construction arm**,
`build_git_exec_command(route, repo_id)` beside `parse_exec`, with
unit rows proving the construction round-trips the parse for both
services (the grammar's gate is the `Service` *type*: the
construction arm can emit no other token, so the client rows are
structurally unreachable by `unknown-service`/`service-only`,
recorded).
- `src/client.rs`: the crate accessors (`russh_handle`/`budgets`,
feature-gated; no public-surface change); `src/lib.rs`: the
`client_ops` module + the per-feature surface re-exports
(`RemoteSession` / `TunnelChannel` / `GitPackSession`,
`OpError`/`OpenDeclineReason` public).
- `tests/common/sshd_double.rs`: the fake `sshd` grown with the
op-row script, with the session requests' reply knob, the scripted
program (stdout/stderr/exit in the `exit-then-close` vanilla
discipline; a no-output/no-exit script launches *nothing*, keeping
interactive sessions open), the direct opens' echo-or-reject
behavior, a counted-refusal knob for the
stays-usable-after-a-decline rows, and the observation surface
(`SshdObserved`: recorded execs/ptys/windows/signals, the wire
translations' witnesses); `tests/client_ops.rs`: the contract rows
(below).
### Decided + recorded
1. **The family plane (the session rows) is the alktty chunk framing
in both directions**, the exact plane the server direction's
bridge speaks, consumed not re-derived: stdin chunks + `CTRL_IN`
(sentinel/`Resize`/`Signal`/`Eof`) toward the wire; stdout/stderr
chunks + the **in-band exit chunk, last** toward the hub. The
exit ride is the family exit-currency (`ControlMessage::Exit`'s
`i32`): wire `ExitStatus(N)` → `N`, wire `ExitSignal(name)` →
`−signum` via the table's inverse read (unmapped names ride the
`−1` sentinel, including a genuine SIGHUP, the same collision
rule 3 consumes server-side). A session ending with no report
delivers no exit chunk (the no-exit-chunk close family's mirror).
2. **The exec/pty/git rows never `into_stream()`; the tunnel rows
never wait.** The mechanics facts pin the *shapes*: the
conversion's reader swallows exactly the non-`Data` messages the
exit reports ride on, so an exit-consuming row is structurally a
**split-halves wait-pump** (`Channel::split()` plus a spawned drain
of `wait()`, the same shape `w4-git-exit-close` pinned server-side);
the write half stays alive, so the fire-and-forget
`window_change`/`signal` rows ride mid-session; the ordering
fact's constraint, that controls precede `into_stream()`, is
respected by *construction* across both row kinds). The tunnel
rows go typed-open → straight `into_stream()`, no queue
consumption ever (POC-1 §6's fresh-`wait()` caveat, honored by
construction); `client.md` §Correctness facts records the landed
shape.
3. **The hub→door plane reads without the error-frame
disambiguation** (`ChunkReader::read_chunk`, not
`tty_map::read_bridge_frame`): that plane's chunk types are
stdin `0` / `CTRL_IN` `3`; a negotiation-error frame never exists
toward the door, and a `0x00`-leading read there is a legitimately
valid zero-length stdin chunk, so `read_bridge_frame`'s
`0x00`-disambiguation (whose home is the alktty→hub direction,
where error frames live) would misparse it. The wave-3 harness's
mistake became this module's first live-row bug, caught by the
contract rows before landing; recorded so the disambiguation rule
("the server never sends 0 or 3", alktty ADR-052 §5) is understood
as *directional*.
4. **The pumps' terminal/kill structure** (the drop-is-wire-clean
path): every pump pair shares a kill watch the *output* pump's
terminal paths fire (the exit chunk / the channel's end / a hub
write failure); the *input* pump closes the wire channel on it.
stdin-EOF (the sentinel, a `CTRL_IN` Eof, or the hub's input half
dropping) sends the wire EOF *once* and parks the pump in the
idle-for-kill state (the session keeps pumping; stdin closure
never ends the session). Dropping the surface (both halves) closes
the channel, wire-clean, nothing dangling outside the dial's
own wall clock.
5. **The launch drains are message-capped** (`DRAIN_MESSAGE_CAP`,
1024): a peer answering only with non-reply messages cannot loop
the drain unboundedly (bounded resources; the drain's idle waits
are bounded by the dial's always-set inactivity timeout). A
refusal closes the channel behind itself (`drain_launch_request`,
which the row owned; never half-serves).
6. **The pty translation**: family `TerminalParamsWire` → wire
`pty_request` verbatim (term/cols/rows/pixels, `term: None` → the
empty-terminal-string value; **the modes JSON never rides the
wire**, always the empty set per the OQ-44 reserved posture
mirrored: the door never invents wire terminal modes); a refused
pty request refuses the whole row (never a pty-less fall), the
never-half-capability row.
7. **The git row's gates before any wire op** (the shared grammar's
verdicts): `validate_repo_id` → `InvalidRepoId`; the built
command's length → `CommandTooLong`. The other grammar decline
classes are structurally unreachable client-side (the
`Service`-typed input carries only the two serving services).
stderr rides a split raw duplex (the hub may drop it); the pack
duplex is the protocol's own byte plane, untouched.
8. **The tunnel rows' originator**: the door's label
(`"alk-ssh"`, port 0): nothing invented, nothing leaked; peer
logs name the door (recorded; a real originator would be the
assembly's local-listener fact, and the `DialFn` shape carries
the target only).
9. **No client-side in-crate channel cap admits ops**: the server
direction's cap is a wire-battery defense; the client rows are
hub-driven (the family app chose the dial). Bounded resources on
this direction: the always-on wall clock/keepalives, the russh
windows, the bounded duplexes/mpscs, the drain cap.
10. **No crate events on the client op rows**: the outcomes ride the
typed surfaces (`Result`/channels), as the decline* event
vocabulary takes a wire channel id, and a *client-side* declined
open has none to name (the typed open carries no id back);
the events remain the server-direction (and dial-skeleton) rows'
surface. Recorded so the asymmetry is a decision, not an
omission.
11. **The smoke posture** (decided + recorded, per the task's item):
the session rows' cheap real-stack smoke is the
alkssh-client-op-row ↔ alkssh-server loopback (the client exec
row against the server direction's *real* session rows, with both
chunk-bridge arms over one live wire), landed as the row
`smoke_client_exec_row_against_the_server_directions_session_rows`
(gated on `sh` like the wave-3 smokes, skipped-with-reason when
absent). A **real-vanilla-`sshd`** dial smoke is wave-5 e2e
territory explicitly, with nothing silently dropped.
### Test rows (`tests/client_ops.rs`, one per decline/contract class; per-build binaries)
- *every build*: the sftp placeholder's decline (before any wire op,
no channel opened at the peer) + the featured-build variant (under
`sftp`), and the connection-stays-usable close on both.
- *session* (10 rows + the smoke): the exec family-plane row
(command rides the wire verbatim; stdout/stderr chunks distinct;
the exit chunk in-band, last; the stdin sentinel = wire EOF), the
refused-exec `RequestRefused` row, the pre-wire budget row (no
channel opened), the pty-shell param-translation row (the family
`TerminalParamsWire` verbatim, the reserved modes never riding),
the pipe-shell parity row (no pty request), the live-pump control
rows (CTRL_IN Resize → wire `window_change`; Signal → wire
`signal`, name verbatim), the exit-signal mirror row (terminal
`exit-signal("TERM")` → the family code `−15` in-band), the
never-half-capability row (a refused pty refuses the whole row, so
no exec behind it ever rode the wire), the server-rejected-open
decline row (`OpenDeclined(Prohibited)`) with the
stays-usable-after-the-decline launch on the same connection, and
the smoke.
- *tunnel*: the direct-tcpip pump row (raw byte duplex
round-trips; `poll_shutdown` EOF rides), the
reject-maps-the-class + stays-usable row (`ConnectFailed` mirror;
the streamlocal twin then opens on the same connection on-Unix;
`#[cfg(not(unix))]`'s OQ-11 decline row compiles off-Unix only, per the
universal-compile/clean-decline gate mirrored from the server
side), the twin's on-Unix pump served in the same row.
- *git*: the grammar's construction riding the wire verbatim + the
pack duplex pump + the `exit-status(0)` consumed into the family
code, the pre-wire `InvalidRepoId` / budget rows (no channel
opened), and the receive-pack-refused `RequestRefused` row.
- The `PumpBuf`/drain caps and the exit-currency mirror are unit
rows inside `client_ops.rs` / `exit_code.rs` / `error.rs`
(taxonomy) / `dispatch.rs` (the construction arm's round-trip).
### Verification
`cargo test` (default), per-feature (`--features {session, git,
tunnel, sftp}`), pairs, and `--all-features`, all 15 test binaries
green in the full builds (the client_ops binary: 18 rows under
`--all-features`, the per-build sets: session 12 / git 5 / tunnel 3
/ featureless 1); `cargo clippy --all-features --all-targets` and
`cargo clippy --all-targets` both `-D warnings` clean; `cargo fmt
--check` clean; `cargo doc --no-deps` warning-free.
## Summary
The client op rows landed: family ops in (`alktty` session shapes /
alkgit `Service`), vanilla wire ops out, no vanilla concepts riding
up (`OpError`'s family-mirrored taxonomy; `OpenDeclineReason` as the
typed-open decline's one-to-one mirror). The session rows give the
hub the alktty chunk plane in both directions with the exit
consumed in-band as the family exit chunk (the derivation contract's
client arm, extracted into the shared two-direction `exit_code.rs`
module; table and rules never forked there); the tunnel rows are the
typed `direct-tcpip`/`direct-streamlocal` opens into raw byte
duplexes (rejects mirror to `OpenDeclineReason`, the connection
stays usable, row-pinned per decline class); the git row builds the
vanilla exec command through the shared grammar's new construction
arm and pumps the raw pack duplex with the exit consumed; the sftp
placeholder declines in every build before any wire op. The
mechanics facts are pinned in code (the `wait()`-drain shapes, the
split-halves pump for exit-consuming rows, the
typed-open-then-straight-conversion for the exit-less ones), the
fake-`sshd` double grew the scripted op-row surfaces + observations,
and 18 contract rows per-build + one smoke landed through it.