Split the last open substrate-placement question: - Unix: ships with the local feature v1 (dial_unix — same halves shape as TCP; the wire enum already carried unix per ADR-001; the params task's schema list includes all three values) - Stdio: OUT of scope — a spawned process's stdin/stdout/stderr IS alktty's pipe mode (LocalTtyBackend + tokio::process + Stdio::piped, alktty tty-local.md): three multiplexed logical streams + the exit-code control chunk (alktty ADR-004) + signal forwarding (REQ-TTY-02). A stdio bridge here would be alktty's runner mode with the terminal stripped out — a strictly worse duplicate that also drops the semantics that matter (a byte tunnel has neither exit codes nor signals). Remote command execution composes via alktty on the same channels substrate. Updated: open-questions.md OQ-TN-14 (resolved), overview.md feature gate + deps + OQ summary, producer.md OQ ref, OQ-TN-10 promotion (#3 split), phase-0-findings + both POC summaries' resolution notes, params task (schema enum includes unix), local-socket-halves task (unix ships, stdio does NOT — with the composition rationale), oq-tn-14-tracker task repurposed (boundary-maintenance: re-opens only if a consumer needs stdio-without-process-semantics — which would need its own ADR, or if the alktty/alktunnels boundary needs sharpening). Verified: taskgraph valid (12 tasks, no cycles)
190 lines
8.9 KiB
Markdown
190 lines
8.9 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-09-07
|
|
---
|
|
|
|
# alktunnels — Producer Half
|
|
|
|
The producer half: the `channels/tunnel/sub` open op — its spec, its
|
|
establisher (dial + listen shapes), its pump handler, and its
|
|
registration. The producer is whoever can reach the tunnel target;
|
|
connection direction is irrelevant (OQ-TN-03's hub model). The reverse
|
|
POC validated the producer running on the connect side of its own
|
|
connection (`from_connection_with_serving`).
|
|
|
|
Design decisions: [ADR-001](decisions/001-open-params-layout.md)
|
|
(params), [ADR-002](decisions/002-alpn-strategy.md) (ALPN),
|
|
[ADR-003](decisions/003-codec-and-udp-framing.md) (codec),
|
|
[ADR-004](decisions/004-no-backend-trait.md) (no backend trait). This
|
|
document is the normative WHAT.
|
|
|
|
## The Open-Op Spec
|
|
|
|
```text
|
|
operation id: channels/tunnel/sub
|
|
op type: Sub (reply = one { channel_id } envelope; data plane on the BiStream)
|
|
ALPN marker: alk/tunnel (ChannelOpenSpec)
|
|
visibility: External
|
|
input schema: { resource: string (required), substrate: enum [tcp, udp, unix] (required) }
|
|
output schema: { channel_id: integer > 0 }
|
|
access control: required_scopes: ["tunnel:open"]
|
|
description: SHOULD carry a human-readable hint (round-trips through discovery, OQ-TN-08)
|
|
```
|
|
|
|
Registration is via `ChannelCore::register_openable_with_establisher`
|
|
(alkcall ADR-049 §5): the registry schema-validates input, runs the
|
|
ACL, allocates the channel (connection-owner rule, ADR-047 §5), runs
|
|
the establisher as the awaited bounded establishment phase, then
|
|
spawns the pump handler. The generic channel ops
|
|
(`channel/close`, `channel/control`, `channel/resources/subscribe`)
|
|
register separately via `ChannelOperations::register_on` — the
|
|
producer (or its assembly layer) MUST register them on the serving
|
|
registry so peers can manage channels out-of-band (the recipe alkcall
|
|
review 007 §Part B verified — `alkcall/docs/reviews/
|
|
007-establishment-follow-ups-review.md`; R-01 = the `Establishment`
|
|
plan payload, R-02 = the `OpenHandler` lifetime contract).
|
|
|
|
**Registration timing:** the openable may register AFTER
|
|
`from_connection_with_serving` returns (the connect-side serving
|
|
posture — the reverse POC's W2): the dispatcher reads through the
|
|
shared `Arc<OperationRegistry>` per dispatch. Late registration is a
|
|
supported assembly shape, not a race.
|
|
|
|
## The Establisher
|
|
|
|
The establisher is the open op's awaited, bounded establishment phase
|
|
(alkcall ADR-049 §1/§2): validate params semantically, consult
|
|
ownership if wired, dial/accept the substrate, and return
|
|
`Establishment::new(plan)` — the dialed handle rides the typed-opaque
|
|
plan (`ChannelPlan = Arc<dyn Any + Send + Sync>`; the `+ Sync` bound
|
|
constrains plan payloads — socket-backed handles carry it naturally,
|
|
reverse POC F-1, documented upstream). On failure:
|
|
`EstablishmentError` (the typed reason codes) → the wrapper tears the
|
|
channel down and replies `channel:open_failed`.
|
|
|
|
**Two shapes** (both return the same plan-flow; ADR-004):
|
|
|
|
### 1. Dial establisher (the `-L`/forward shape)
|
|
|
|
Registry lookup of `(resource, substrate)` → backing → dial →
|
|
halves. The dial function is substrate code injected at assembly
|
|
(ADR-004: the establisher closure closes over it); the protocol crate
|
|
never sees a socket type. Failure mapping:
|
|
|
|
| Producer condition | Typed reason |
|
|
|---|---|
|
|
| resource not in the registry (or wrong substrate) | `unknown_resource` |
|
|
| dial refused / timed out / unreachable | `dial_failed` |
|
|
| bind/connect resource exhaustion (fds, ports, slots) | `resource_shortage` |
|
|
| establisher-internal failure | `handler_error` |
|
|
| the bound exceeded | `timeout` (wrapper-level) |
|
|
|
|
### 2. Listen establisher (the producer-side listener variant)
|
|
|
|
For a producer whose resource is a LISTENER (SSH `-R` far-side
|
|
listener; a hub exposing its own port to a third party): the same
|
|
open op, the same params; the establisher pops the next accepted
|
|
connection from an assembly-owned listener queue and returns it as
|
|
the plan payload. The listener lifecycle (bind, accept loop, cancel)
|
|
is assembly code — the protocol never binds (OQ-TN-04). Error
|
|
mapping: an empty queue during establishment is `resource_shortage`;
|
|
a closed listener is `dial_failed`-class. No new wire surface — the
|
|
listen variant is plan-flow with a different halves source.
|
|
|
|
**Identity:** the establisher receives the **per-call opener
|
|
identity** (the dispatch-resolved identity overlaid onto the
|
|
install-time context — alkcall 0.7.0 CF-006). On hub-forwarded opens
|
|
it sees the end client, not the hub. `auth` carries the opener's
|
|
identity plus the transport-truthful fields (`alpn`, `remote_addr`,
|
|
`tls_client_fingerprint`) — never rewritten. Consult it for
|
|
ownership-scoped resources if the assembly layer wants
|
|
`OwnershipProvider` checks here (OQ-TN-08's optional half).
|
|
|
|
**Concurrency:** each open's establisher result flows to ITS pump
|
|
handler — concurrent opens of the same resource are independent
|
|
(the R-01 plan flow — alkcall review 007 R-01, the
|
|
`Establishment::new(plan)` payload; the forward POC's resource-keyed
|
|
handoff race is structurally gone, confirmed under concurrency by the
|
|
reverse POC).
|
|
|
|
## The Pump Handler
|
|
|
|
The `OpenHandler` (alkcall ADR-049/050 shape:
|
|
`Fn(Value, Option<ChannelPlan>, Connection, AuthContext) ->
|
|
JoinHandle<()>`):
|
|
|
|
1. Downcast the plan to the concrete handle type
|
|
(`Arc::downcast::<TargetHandle>` — establisher and handler agree
|
|
on the type; alkcall never inspects it).
|
|
2. `conn.accept_bi()` — the channel's yield-once `BiStream`.
|
|
3. `pump_bidi(bidi, handle.read, handle.write)` — **awaited inline**
|
|
inside the handler's task. The returned `JoinHandle` MUST track
|
|
the data-plane lifetime (R-02): the wrapper awaits it and its
|
|
completion triggers channel teardown. **Early return =
|
|
teardown-at-birth** — both pumps see instant EOF with zero bytes;
|
|
the "spawn-and-forget" shape is the one bug class the R-02
|
|
telemetry (yield-once flag + birth-teardown debug hint) diagnoses
|
|
at runtime. Hand-rolling the two-pump shape is out (ADR-050).
|
|
4. UDP: the plan payload is the FRAMED adapter (ADR-003) — the pump
|
|
is unchanged; the codec lives at the substrate/pump boundary.
|
|
|
|
**The handler is substrate-agnostic by construction:** it cannot know
|
|
whether the halves came from TCP, UDP, a unix socket, or an
|
|
in-process pipe, and must not (AGENTS.md convention 7).
|
|
|
|
## Registration API (the spec surface)
|
|
|
|
```rust
|
|
pub fn register_tunnel_openable(
|
|
core: &ChannelCore, // the manager + policy pair
|
|
registry: &ResourceRegistry, // assembly-owned: (resource, substrate) -> backing
|
|
on_registry: &Arc<OperationRegistry>, // the session's dispatch registry
|
|
// (the per-session fork per alkcall ADR-047 §4
|
|
// amendment, 2026-09-03 — NOT the connection
|
|
// overlay; Layer-2-registered open ops resolve
|
|
// NOT_FOUND, review 004 F-01)
|
|
auth: AuthContext, // the install-time context (per-call overlay is upstream's)
|
|
dial: DialFn, // substrate halves function (ADR-004; `local` feature or assembly)
|
|
) -> Result<(), String>
|
|
```
|
|
|
|
- `ResourceRegistry` (assembly construct, OQ-TN-11): maps
|
|
`(resource, substrate)` → backing. The POCs' `HashMap` shape is the
|
|
candidate; the collision domain is the producer's registry
|
|
(ADR-001 — two producers on one connection may expose the same
|
|
resource name independently).
|
|
- The scope constant `TUNNEL_OPEN_SCOPE = "tunnel:open"`; the open-op
|
|
spec builder is public so assembly layers can compose variants
|
|
(e.g. extra ACL fields) without forking the wire shape.
|
|
- The pump handler factory is public for the same reason.
|
|
|
|
## Access control posture
|
|
|
|
Tunnels reach local networks — the open gate is the security boundary
|
|
(OQ-TN-08):
|
|
|
|
- The registry runs `AccessControl` before the establisher (the ACL
|
|
rides `register_openable`; free via alkcall).
|
|
- `TUNNEL_OPEN_SCOPE` is the base gate; the assembly layer MAY add
|
|
resource-scoped checks via the establisher (ownership on the
|
|
resource name) — the protocol provides the seam, not the policy.
|
|
- No target allowlists, no tunnel-specific policy machinery in v1 —
|
|
the far side owns the resource (OQ-TN-08 resolution); discovery is
|
|
the ACL-filtered ops listing.
|
|
|
|
## Open Questions
|
|
|
|
- **OQ-TN-11**: resource naming collision domain + lifecycle
|
|
(partially resolved — the per-producer registry is the leading
|
|
candidate)
|
|
- **OQ-TN-14**: resolved (split: unix in, stdio out — alktty's pipe
|
|
mode owns process stdio; see open-questions.md)
|
|
|
|
## References
|
|
|
|
- [wire.md](wire.md) (the open op's wire surface),
|
|
[overview.md](overview.md) (module map)
|
|
- Forward POC `docs/research/poc-summary.md`; reverse POC
|
|
`docs/research/reverse-poc-summary.md` (both producer shapes ran)
|
|
- alkcall ADR-047/049 (open ops + establishment), ADR-050 (`pump_bidi`),
|
|
ledger CF-005/CF-006 (identity precedence + per-call opener) |