Files
alktunnels/tasks/tunnels/direct-op.md
T
glm-5.3-flash 0ceecf582b docs: remove em dashes from repo prose and forbid them going forward
- Rewrote the remaining em dashes outside docs/reviews/ (preserved as
  historical record, matching the alktty precedent): tests/ (313),
  fuzz/ (34 plus one shell comment), tasks/ (671 plus 12 kept in
  fences), docs/research + sdd_process + plans (525 plus 25 in
  fences), docs/architecture/overview.md fence labels (13), README,
  CHANGELOG, AGENTS.md, fuzz/README.md (176), and the
  .opencode/agents prompts (110)
- Rewrote fenced content too: ASCII tree and module-map label
  separators, Rust snippet comments, and template placeholders; wire
  data, JSON/BAST examples, and transcripts survive verbatim
- Rewrote 6 assert/expect message literals in tests/ and 2 format
  strings in fuzz/shared/src; no test data changed
- Added Project Convention 18: no em dashes in prose (doc comments,
  markdown including docs/ and tasks/, agent prompts, commit
  messages); fence content rule, en-dash clause rule, and the
  no-spaced-hyphen prohibition follow alktty's convention 15

Follows 6b20924 (src doc comments) and cd0afe0 (docs/architecture).

Verification: cargo test (141 pass); cargo clippy --all-targets -D
warnings; cargo fmt --check; cargo doc --no-deps; wasm check and
clippy for wasm32-unknown-unknown with -D warnings; em dash grep over
the repo returns matches only in docs/reviews/
2026-11-03 14:12:46 +00:00

11 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
tunnels/direct-op Direct op, params, spec, establisher, consumer open_direct (ADR-007) completed
broad medium component implementation
wire
params
direct-op
producer
consumer
egress

Description

Implement the direct op (channels/tunnel/direct, ADR-007) end to end: the params type + spec builder (src/params.rs), the establisher

  • registration (src/producer.rs), and the consumer constructor (src/consumer.rs). The op rides the same alk/tunnel ALPN (ADR-002 Amendment 1's flavor-form op ids; the upstream discovery derivation landed in alkcall 0.8.0, ADR-047 amendment 3). This is the ssh direct-tcpip / SOCKS5 CONNECT egress capability with structural ACL separation from tunnel:open.

params.rs: types + spec

pub const OP_TUNNEL_DIRECT: &str = "channels/tunnel/direct";
pub const TUNNEL_DIRECT_SCOPE: &str = "tunnel:direct";

/// The substrate-shaped address object (ADR-007 §2): keyed by the
/// sibling `substrate` field of the params. Also the `peer` value
/// shape of the forwarded op (ADR-008 §3); define once here.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SubstrateAddr {
    HostPort { host: String, port: u16 },
    Path { path: String },
}

/// The direct-op input params (ADR-007 §2). Wire-stable.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TunnelDirectParams {
    pub substrate: Substrate,
    pub target: SubstrateAddr,
}

pub fn tunnel_direct_spec() -> OperationSpec
  • Spec per wire.md §The Direct Op: Sub-typed, Visibility::External, ChannelOpenSpec::new(TUNNEL_ALPN), scope ["tunnel:direct"] (never implied by tunnel:open, ADR-006 Amendment 1), output schema {channel_id: integer > 0}.
  • input_schema pins both fields required and the target sub-schema per-substrate, as a oneOf on the sibling substrate value (tcp/udp → {host, port}, unix → {path}), so malformed targets fail the registry's schema validation (the typed INVALID_INPUT class) before the establisher runs. The establisher's serde parse is the second gate (defensive; see the error-mapping note).
  • deny_unknown_fields on both params types (the loud-rejection posture, ADR-001's precedent).
  • The Substrate enum is reused as the discriminator (no new substrate values (ADR-001's additive rule).

producer.rs: the establisher + registration

pub fn direct_establisher(dial: DialFn) -> OpenEstablisher

pub fn register_tunnel_direct_openable(
    core: &ChannelCore,
    on_registry: &Arc<OperationRegistry>,
    auth: AuthContext,
    dial: DialFn,                     // the SAME DialFn the base op uses
    identity_witness: Option<IdentityWitness>,
) -> Result<(), String>
  • DialFn reuse is the design pin: the establisher parses TunnelDirectParams, renders the target to the same backing-string form the registry backings use ("host:port" for tcp/udp, the path for unix), and calls the injected DialFn(substrate, rendered). No second dial type: a producer that registered dial_tcp for the base op registers the direct op with zero new substrate code (ADR-007's local interaction note). The UDP framed-adapter wrap (ADR-003) happens inside the dial closure (the local feature's connect_udp already does this), so the establisher stays substrate-agnostic.
  • No registry lookup: unknown_resource can never fire (ADR-007 §2). Error mapping (ADR-007 §4's table minus the registry row): the serde-parse failure of a schema-valid-shaped target maps HandlerError (the schema gate should have caught it; the parse is the backstop); dial failures pass through the DialFn's own typed errors unchanged (dial_failed / resource_shortage / handler_error).
  • The per-call auth carries the opener identity (CF-006); the witness seam records it, same as the base op (the direct op is the arbitrary-egress capability; identity visibility here matters more, not less).
  • Registration uses the SAME pump handler (make_tunnel_pump_handler (the direct op's plan payload is a TargetHandle exactly like the base op's, and plan-flow is unchanged).

consumer.rs: open_direct

pub async fn open_direct(
    client: &ChannelClient,
    target: SubstrateAddr,
    substrate: Substrate,
) -> Result<Self, TunnelOpenError>
  • A separate constructor, not a params enum (ADR-007 §5: two scopes are two capabilities; the caller states which one it holds). Serializes {substrate, target}, open_channel(OP_TUNNEL_DIRECT, …, TUNNEL_ALPN), from_halves, with an identical session type, data planes, and teardown API as open.
  • Uses plain open_channel (no reply fields expected on the direct op, because dial establishers never bind; wire.md §The Direct Op).
  • Documented error surface: the base open errors minus unknown_resource; malformed targets are the schema/ invalid_input-class failure (registry-side); an old producer rejects the op loudly (NOT_FOUND, the SSH posture).

Tests

Unit (params.rs): TunnelDirectParams round-trip per substrate; untagged SubstrateAddr disambiguation ({host, port} vs {path}); deny_unknown_fields rejection; spec-shape conformance to wire.md §The Direct Op (op id, scope, oneOf schema structure; structural assertions, no jsonschema dep here, per tunnels/params's precedent).

Integration (extend the duplex harness from tunnels/producer-open-op):

  • open_direct e2e round-trip through pump_bidi (tcp duplex target).
  • open_direct udp e2e (the framed adapter wraps at the dial closure, so the datagram boundary is preserved).
  • Malformed target (unix without path) → schema-class failure, no phantom session.
  • Producer without the direct op registered → NOT_FOUND-class.
  • Identity without tunnel:direct (but with tunnel:open) → FORBIDDEN (the scopes do not imply each other).
  • Direct establisher sees the per-call opener identity (witness).

Acceptance Criteria

  • TunnelDirectParams/SubstrateAddr round-trip; malformed targets rejected (serde + schema shape pinned)
  • tunnel_direct_spec() matches wire.md §The Direct Op exactly
  • direct_establisher: no registry lookup, target rendering into the DialFn backing string, typed errors per ADR-007 §4
  • register_tunnel_direct_openable reuses the base pump handler + dial injection; witness seam present
  • TunnelSession::open_direct (separate ctor; identical session/teardown semantics)
  • Integration tests: e2e round-trips (tcp + udp), malformed-target rejection, scope separation (FORBIDDEN), NOT_FOUND posture, per-call identity witness
  • Clippy/fmt clean; wasm32 check passes (no substrate code added)
  • cargo test green

References

  • docs/architecture/decisions/007-direct-open-op.md (the normative op)
  • docs/architecture/producer.md §The Direct Op; consumer.md §Direct path; wire.md §The Direct Op
  • docs/architecture/decisions/006-access-control-posture.md Amendment 1
  • tasks/tunnels/params.md + tasks/tunnels/producer-open-op.md (the shapes this extends; the harness to extend)

Notes

  • The input_schema pins the per-substrate target sub-schema as two if/then branches under allOf, keyed on the sibling substrate value (tcp/udp → {host, port} with additionalProperties: false; unix → {path}, same posture); JSON Schema if/then composes through alkcall's jsonschema 0.46 compile, and the integration tests prove the gate fires (INVALID_INPUT before the establisher). Structural assertions pin the branch shape per tunnels/params's no-jsonschema-dep precedent.
  • Untagged SubstrateAddr is deliberately lenient about extra target fields (untagged variants carry no deny_unknown_fields): a cross-shape target parses as whichever variant fits. The schema is the shape-vs-substrate gate; the establisher's serde parse is the backstop only (unknown substrate value → handler_error, direct_establisher maps TunnelEstablishError::HandlerError). This asymmetry is documented in both the type docs and the test.
  • TunnelDirectParams::render_target() centralizes the backing-string form ("host:port" / path); the establisher injects the SAME DialFn with the rendered string; the render is pinned by test to the registry-backing form.
  • TunnelSession::open_direct uses plain open_channel (no reply fields, since dial establishers never bind); SubstrateAddr re-exports as the future forwarded-op peer shape (ADR-008 §3, defined once).
  • Harness: RegistrationMode::Direct, wire_direct / wire_direct_with_identity (the raw-call topology), wire_direct_forward / wire_direct_forward_with_identity (the session-level forward topology; identity parameterized so the session-level FORBIDDEN probe runs through the real client API), direct_identity / both_scopes_identity.

Summary

Implemented the direct op end to end per ADR-007:

  • params.rs: OP_TUNNEL_DIRECT / TUNNEL_DIRECT_SCOPE constants; SubstrateAddr (untagged {host, port} | {path}, the ADR-008 §3 peer shape too); TunnelDirectParams (deny_unknown_fields, render_target()); tunnel_direct_spec() (Sub-typed, External, ChannelOpenSpec::new(TUNNEL_ALPN), scope ["tunnel:direct"], output {channel_id: integer > 0}, per-substrate if/then target sub-schema). 7 unit tests: round- trips per substrate, untagged disambiguation, loud unknown-field rejection, spec-shape conformance, schema structure.
  • producer.rs: direct_establisher / direct_establisher_with_witness (parse → render → injected DialFn; no registry lookup; typed-error mapping per ADR-007 §4 minus the registry row; CF-006 witness seam) + register_tunnel_direct_openable (direct spec + establisher + the SAME make_tunnel_pump_handler, plan-flow unchanged).
  • consumer.rs: TunnelSession::open_direct(client, target, substrate): separate constructor, serializes {substrate, target}, plain open_channel(OP_TUNNEL_DIRECT, …, TUNNEL_ALPN), from_halves, with identical session/data-planes/teardown as open.
  • lib.rs: the new surface re-exported.

Tests: tests/producer_direct_op.rs has 15 integration tests over the duplex harness: tcp + udp e2e round-trips through pump_bidi (datagram boundary preserved via the framed dial closure), malformed targets (unix without path, tcp without port, missing target) → INVALID_INPUT with no phantom channel/session, dial-failure pass-through (dial_failed), NOT_FOUND on a producer without the op, scope separation both ways (tunnel:open ↛ tunnel:direct and tunnel:direct ↛ tunnel:open, all FORBIDDEN (raw-call AND session-level), CF-006 witness on the direct establisher (raw + session paths), the establisher parse backstop (handler_error), and the target-rendering pin (the dial closure observes the exact backing-string form). Harness: Direct registration mode + wire_direct* topologies + direct_identity/both_scopes_identity.

Verified: cargo test green (93 native; 94 with --all-features), clippy -D warnings clean (native + wasm32), fmt clean, wasm32 check passes (default crate stays wasm-clean; no substrate code was added), cargo doc --no-deps clean. CHANGELOG updated (Unreleased → Added).