docs(arch): resolve OQ-45 — no application-level windowing; QUIC per-stream flow control suffices

The backpressure chain is complete by construction: QUIC flow control
→ bounded drainer channel → bounded stdout channel → OS pipe/PTY buffer
→ process write() blocks. Every link awaits its producer; no unbounded
buffer breaks the chain. The reversal path (an additive ControlMessage
variant, not a wire-format header change) is noted in ADR-052's
consequences as a two-way door, not the expected path.

Tightened ADR-052 assumption 1 from a hedge ('expected to work; a POC
would confirm') to a decided constraint. Updated all OQ-45 references
across the tty spec docs from 'open (low risk)' to 'resolved'.
This commit is contained in:
glm-5.2 committed 2026-07-07 09:36:18 +00:00
1 parent df5d04af1d
commit db4da1e6f0
7 files changed
+131 -59

No files matched your search

+2 -2
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-07-06
last_updated: 2026-07-07
---
# alknet-tty
@@ -48,7 +48,7 @@ the PTY (docker, SSH, local process) via a `TtyBackend` trait.
|----|-------|--------|-----------|
| OQ-43 | `TtyControl` trait object `Clone` constraint | resolved | `control: Option<Box<dyn TtyControl + Send + Unpin + Clone>>` via an `Arc`-backed `Clone` newtype; confirmed by the POC's concrete `PtyControl` |
| OQ-44 | Terminal modes (TTY modes) | deferred(scope) | `TerminalParams.modes` reserved; default terminal modes suffice for current scope; blocked on a concrete mode-control use case |
| OQ-45 | Flow control for high-throughput stdout | open (low risk) | QUIC per-stream flow control is expected to suffice; a high-volume POC would confirm |
| OQ-45 | Flow control for high-throughput stdout | resolved | QUIC per-stream flow control is the backpressure mechanism (chain complete by construction); no application-level windowing. Reversal is an additive `ControlMessage` variant, not a wire-format change |
| OQ-46 | Runner API surface | deferred(scope) | The runner mechanism (pipe mode) is in alknet-tty; runner policy (job management, log persistence, task graph) is a downstream crate, not in scope here |
| OQ-47 | Stdin closure canonical signal | resolved | Either a zero-length stdin chunk or a `{"type":"eof"}` control chunk; both are accepted; the spec recommends `eof` for explicitness |
+2 -2
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-07-06
last_updated: 2026-07-07
---
# alknet-tty — Overview
@@ -263,7 +263,7 @@ See [open-questions.md](../../open-questions.md) for full details.
- **OQ-43** (resolved): `TtyControl` as a `Clone` trait object.
- **OQ-44** (deferred(scope)): Terminal modes (TTY modes).
- **OQ-45** (open, low risk): Flow control for high-throughput stdout.
- **OQ-45** (resolved): Flow control for high-throughput stdout — no application-level windowing; QUIC per-stream flow control is the backpressure mechanism.
- **OQ-46** (deferred(scope)): Runner API surface.
- **OQ-47** (resolved): Stdin closure canonical signal.
+31 -27
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-07-06
last_updated: 2026-07-07
---
# alknet-tty — TtyAdapter and Session Lifecycle
@@ -204,16 +204,19 @@ revokes. The adapter's access control declares against the ADR-050 model:
`identity.scopes` for the `tty:open` scope (or a deployment-configured
scope) before allocating the session. A caller without the scope gets
a negotiation error (`{"error":"forbidden"}`) and the stream closes.
- **Resource ownership for backend-specific resources.** A docker
backend's session targets a specific container (`BackendParams::Docker
{ container }`); the adapter extracts the container ID via the
`OperationSpec.resource_id_path` analog (here, a hardcoded field — the
tty adapter is not an `OperationSpec`, so the extraction is direct
from the negotiation frame's `container` field) and checks
`OwnershipProvider::owns(identity, "container", id, "tty")` if an
ownership provider is wired. A local backend's session has no
pre-existing resource — the process is the resource, and the caller
that opened the session owns it by construction.
- **Resource ownership for backend-specific resources.** Some
backends target a pre-existing resource (a docker backend targets a
specific container); others create their own (a local backend's
process, an SSH backend's channel). The adapter delegates the
resource-id extraction to the backend via
`TtyBackend::resource_id(&params)` (ADR-053), which returns
`None` (no pre-existing resource — the session creates its own) or
`Some((kind, id))` (the caller must own this resource). The adapter
checks `OwnershipProvider::owns(identity, kind, id, "tty")` if an
ownership provider is wired and the backend returns `Some`. The
adapter does not parse backend-specific JSON itself — the extraction
is backend-driven, so adding a backend with a new resource shape (e.g.,
a Kubernetes backend targeting a pod) requires no adapter change.
- **`forwarded_for` for proxied sessions.** A hub that proxies a
terminal session to a worker carries the end user's identity as
`forwarded_for` (ADR-032); the worker authorizes the hub (its direct
@@ -222,22 +225,23 @@ revokes. The adapter's access control declares against the ADR-050 model:
The tty adapter is a `ProtocolHandler`, not an `OperationSpec`-registered
operation — it doesn't go through the call protocol's
`OperationRegistry::invoke()`. The access-control shape is the adapter's
own (scope-gate + ownership check at negotiation), declaring against the
ADR-050 model but not consuming `OperationSpec.resource_id_path` (that
field is for call-protocol operations; the tty adapter is its own
ALPN). See ADR-050 §"Specifics" for the model this declares against.
own (scope-gate + backend-driven ownership check at negotiation),
declaring against the ADR-050 model but not consuming
`OperationSpec.resource_id_path` (that field is for call-protocol
operations; the tty adapter is its own ALPN, and the resource-id
extraction is delegated to the backend via `resource_id()` rather than
a path expression). See ADR-050 §"Specifics" for the model this declares
against.
The concrete choices — the scope name (`tty:open`), the check-at-
negotiation timing, and the hardcoded `container` field extraction for
docker backend-specific resources (rather than a generic path like
`OperationSpec.resource_id_path`) — are **two-way-door** choices within
the one-way `TtyAdapter` shape. The scope name can be renamed (a
deployment-configured scope, not a wire-format constant); the hardcoded
field extraction can be generalized if a second backend with a different
resource-id location appears (the adapter would branch by backend). No
ADR is warranted for these until a second backend forces the
generalization — they are reversible implementation choices, not
architectural commitments.
The concrete choice — the scope name (`tty:open`) and the
check-at-negotiation timing — is a **two-way-door** choice within the
one-way `TtyAdapter` shape. The scope name can be renamed (a
deployment-configured scope, not a wire-format constant). The
resource-id extraction is backend-driven via `resource_id()`, so new
backends with new resource shapes require no adapter change — the
generalization is already in place (ADR-053). No ADR is warranted for
the scope name; it is a reversible implementation choice, not an
architectural commitment.
### Connection and Stream Lifecycle
@@ -291,7 +295,7 @@ architectural commitments.
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-45** (open, low risk): Flow control for high-throughput stdout.
- **OQ-45** (resolved): Flow control for high-throughput stdout — no application-level windowing; QUIC per-stream flow control is the backpressure mechanism.
## References
+16 -7
View File
@@ -1,6 +1,6 @@
---
status: draft
last_updated: 2026-07-06
last_updated: 2026-07-07
---
# alknet-tty — Wire Format
@@ -96,9 +96,11 @@ Fields:
- `env` — environment variables (empty = inherit).
Backend-specific selector fields ride alongside (e.g., `"container":
"abc123"` for docker). The adapter parses the negotiation frame, extracts
the `backend` string, and passes the backend-specific fields to the
selected backend's `allocate()` as `BackendParams` (ADR-053).
"abc123"` for docker). The adapter parses the negotiation frame,
extracts the `backend` string, and passes the remaining backend-specific
fields to the selected backend's `allocate()` as an opaque
`serde_json::Map` (ADR-053) — the adapter does not interpret them; the
backend deserializes its own strongly-typed params struct.
After the negotiation frame, the stream switches to raw chunks. There is
no `call.responded`/`call.completed` — this is not the call protocol.
@@ -239,8 +241,13 @@ a session — see [tty-adapter.md](tty-adapter.md).
and servers parse. A 5th channel type requires a new ALPN
(`alknet/tty/v2` per ADR-006), not a negotiated addition.
- **No windowing.** The chunk format has no flow-control window; QUIC's
per-stream flow control handles backpressure. See OQ-45 (high-throughput
stdout; expected to suffice; a POC would confirm).
per-stream flow control is the backpressure mechanism (OQ-45 resolved:
the backpressure chain is complete by construction — QUIC flow control
→ bounded drainer channel → bounded stdout channel → OS pipe/PTY
buffer → process `write()` blocks; no unbounded buffer breaks the
chain). The reversal path, if ever needed, is an additive
`ControlMessage` variant on stream_type 3, not a wire-format header
change.
- **No negotiation round-trip.** The client writes the negotiation frame
and starts sending chunks; the server reads the frame and starts
pumping. There is no "the server acknowledges the negotiation before
@@ -268,7 +275,9 @@ a session — see [tty-adapter.md](tty-adapter.md).
See [open-questions.md](../../open-questions.md) for full details.
- **OQ-44** (deferred(scope)): Terminal modes.
- **OQ-45** (open, low risk): Flow control for high-throughput stdout.
- **OQ-45** (resolved): Flow control for high-throughput stdout — no
application-level windowing; QUIC per-stream flow control is the
backpressure mechanism.
- **OQ-47** (resolved): Stdin closure canonical signal.
## References