repo scaffold + AGENTS.md + Phase 0 research

- Cargo scaffold: feature gates (quinn/tcp/acme), lean tokio subset,
  placeholder lib; Cargo.lock committed with time pinned to 0.3.36 so
  rust-version = 1.85 is actually satisfiable (rcgen's default time
  resolution requires 1.88 — alknet-tls fails the same check)
- AGENTS.md adapted from alktunnels: TLS-crate conventions (behavior-
  preservation invariants, fail-closed verifier selection, one ACME
  state machine, config-construction scope boundary, no wasm target)
- .opencode/agents: implementation-specialist conventions + coordinator
  prompt template + architect deferral examples updated for alktls
- docs/research/phase-0.md: extraction inventory with verified
  invariants (line-referenced), spec-vs-code gaps (TlsError shape,
  for_tcp_tls, config-type ownership), rewrite requirements,
  OQ-TLS-01..07, MSRV verification record

Verified: cargo test, clippy -D warnings, fmt --check, doc --no-deps,
test --all-features, rustup run 1.85 cargo check
This commit is contained in:
2026-09-09 16:25:55 +00:00
parent dbc77af3d3
commit a570bee0fe
11 changed files with 3140 additions and 73 deletions
+56 -62
View File
@@ -211,69 +211,63 @@ Read `AGENTS.md` at project root for full details. Key rules:
1. **No comments in code** — Per project convention. Doc comments (`///`, `//!`)
are fine and expected on public API. Inline `//` comments only when the user
asks or when a non-obvious safety/correctness constraint would otherwise be
missed (e.g., "a two-pump tunnel must shut down the opposite sink on pump
completion — `try_join!` alone deadlocks").
2. **Error handling** — `thiserror` for library error types (`TunnelError`;
`HandlerError`/`StreamError` come from alkcall::core). No panics in library
code. No `unwrap()` or `expect()` outside tests. For poisoned
`RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())`.
3. **`tokio` is the async runtime** — all I/O is async. The tunnel pumps, the
channels integration, and the consumer session type are all async. Use
`tokio::sync` primitives (`oneshot`, `mpsc`) for lifecycle correlation and
per-direction data flow.
4. **WASM target is load-bearing** — the default crate (protocol-only) MUST
compile to `wasm32-unknown-unknown`. Use the wasm-clean tokio subset
(`rt`, `sync`, `io-util`, `macros`, `time`); **do NOT use
`features = ["full"]`**. Substrate backends (local TCP/UDP sockets, process
listeners) are feature-gated and never imported from the
shared/producer/consumer modules.
5. **Wire format is stable** — the tunnel payload rides inside channels data
channels as raw bytes (channels strips its 8-byte header transparently; the
tunnel protocol owns whatever framing it puts inside the `BiStream`). Any
negotiation/setup frame is self-contained (length-prefixed JSON per alktty
ADR-006 precedent), not alkcall's `EventEnvelope` framing. Wire-format
changes after the first consumer are additive-only.
6. **Producer/consumer, not server/client** — both sides of a channels
connection can initiate. Use "producer"/"consumer" or "accept side"/"connect
side," not "server"/"client."
7. **Substrate-agnostic by construction** — the protocol layer must not know
whether bytes come from TCP, UDP, a Unix socket, or stdio. Target
addressing, direction, and lifecycle bookkeeping must not hardcode a
substrate.
8. **Two-pump shutdown-on-completion is a contract** — each pump MUST shut
down the opposite sink when it completes; `tokio::try_join!` alone
deadlocks (POC-validated, alknet ADR-078). Emit EOF sentinels (zero-length
chunks) on clean sink shutdown.
9. **No forced local binding** — a tunnel must not require the producer (or
consumer) to bind a local port. Support both SSH `-L` and `-R` style
directions and unbound/listen-optional flows; the binding decision belongs
to the caller.
10. **Backpressure and limits are inherited** — bounded per-channel buffers,
the 256-channel cap, monotonic IDs, and zero-length-sentinel EOF are
alkcall channels invariants. Do not build a second demux/mux or re-derive
limits.
11. **Vendored core types come from alkcall** — `Connection`,
`ProtocolHandler`, `BiStream`, `BidiStreamSource`, `AuthContext`,
`Identity`, `IdentityProvider`, `AccessControl`, `OwnershipProvider`,
`HandlerError`, `StreamError` come from `alkcall::core`. Do not vendor
copies. Pin `alkcall = "0.4.0"`; bump deliberately; fix issues upstream.
12. **BAST document for the wire format** — binary framing (if any beyond
pass-through) carries a BAST document under `docs/architecture/`
conforming to `https://alk.dev/bast/v1/schema`. alktunnels does not depend
on alktype; hand-rolled codecs are fine for trivial formats.
13. **Access control** — scope-gate tunnel opens (`TUNNEL_OPEN_SCOPE`, shape
following alktty's `TTY_OPEN_SCOPE`); the channels path gets authorization
via `ChannelCore::register_openable`, which runs the ACL before the open
handler. Optionally consult `OwnershipProvider`
(`provider.owns(id_ref, kind, &id, "tunnel")` — the 4-arg shape).
14. **Naming conventions** — Rust standard: `snake_case` for functions/variables/
asks or when a non-obvious security/correctness constraint would otherwise be
missed (e.g., "the root store must never be empty — a container with no
system CA bundle still needs to verify public X.509 remotes; alknet
ADR-088 §5").
2. **Error handling** — `thiserror` for the library error type (`TlsError`,
`#[non_exhaustive]`, one variant per failure category — the alknet ADR-088
shape). No panics in library code. No `unwrap()` or `expect()` outside
tests. For poisoned `RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())`.
3. **`tokio` is the async runtime** — all I/O is async. The ACME state machine
is a spawned task; cert loading is sync file I/O behind an `async fn`
signature for API uniformity. Use the wasm-clean tokio subset
(`rt`, `sync`, `macros`); **do NOT use `features = ["full"]`** in
`[dependencies]` (dev-dependencies may use `full`).
4. **The default crate stays lean** — TLS setup and config types only.
Transport-specific wrapping is feature-gated: `quinn` (the `for_quinn()`
accessors), `tcp` (`tokio-rustls`), `acme` (the ACME state machine, a heavy
dep). The `rustls` dep is always present. Wasm is not a load-bearing target
here (crypto stacks and file I/O are platform code).
5. **Behavior-preservation invariants are load-bearing** — `max_early_data_size
= u32::MAX` on all server config paths (0-RTT); `aws_lc_rs::default_provider()`
on all paths (alknet ADR-084); `AcceptAnyCertVerifier::supported_verify_schemes()`
returns ED25519 + ECDSA P-256/P-384 + RSA PSS/PKCS1 verbatim; `acme-tls/1`
ALPN appended by the crate for the ACME path only (alknet ADR-027 §7);
non-empty root store (merge `webpki-roots` when the platform store is
empty — alknet ADR-088 §5).
6. **Fail closed** — verifier selection (fingerprint pin / CA / fail closed)
must never silently downgrade. Known peer + fingerprint → pin; unknown +
X.509 → CA; unknown + raw key → fail closed at handshake (alknet ADR-034).
Client-auth cert presentation follows the local identity; `Acme` is a
server-only identity (config error on the client path).
7. **One identity, N transports; one ACME state machine** — `TlsServerConfig` /
`TlsClientConfig` are built once and shared (the inner rustls config is
Clone — Arc-shared resolvers). `TlsServerConfig` is not `Clone` (it holds
the ACME task's `JoinHandle`); share via `Arc`. Never spawn a second ACME
state machine for a domain already being served (alknet ADR-082 §The
cert-reuse problem). This crate is the cert provider, not the accept loop.
8. **TLS-crate scope boundary** — this crate owns config *construction*.
Handshake-time outcomes flow through the transport's connector, not through
`TlsError` (alknet ADR-088 §6). ACME runtime errors are stream events,
logged in the spawned task. Do not grow `TlsError` to cover handshake
outcomes.
9. **Feature gates** — transport-specific deps are opt-in (`quinn`, `tcp`,
`acme`). Verify `cargo test` (default) and `cargo test --all-features`
both pass whenever features are touched.
10. **Upstream posture** — this crate extracts working code from alknet
(`crates/alknet-tls`, `crates/alknet-core` config/fingerprint). The alknet
ADRs and the `crates/tls` README spec are the reference; where this crate
deviates, record the deviation as an ADR here rather than silently
diverging. Config types (`TlsIdentity`, `Ed25519SecretKey`) are expected
to move here from `alknet-core`; do not re-import them from alknet.
11. **Naming conventions** — Rust standard: `snake_case` for functions/variables/
modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants.
15. **Module structure** — one module per file under `src/`, re-exported from
`src/lib.rs`. Public API surface is `lib.rs` re-exports. Producer half
(adapter / open-handler), consumer half (typed session/client), shared
wire/target-addressing modules; backends feature-gated and never imported
from shared/producer/consumer modules.
12. **Module structure** — one module per file under `src/`, re-exported from
`src/lib.rs`. Public API surface is `lib.rs` re-exports. The alknet shape
is the reference: `server.rs` (server config + resolvers), `client.rs`
(client config + verifiers), `pem.rs` (cert/key loading), `signing.rs`
(shared signing helpers).
## Key Principles