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
+5 -5
View File
@@ -329,7 +329,7 @@ A decision should be `deferred(scope)` when:
- The use case isn't concrete (e.g., "we don't know what the agent crate
will need from the call protocol")
- The options depend on something that doesn't exist yet (e.g.,
"depends on the alknet-http crate spec")
"depends on the alknet rewrite's dial-seam shape")
- The trade-off requires data that can only come from implementation
(e.g., "need performance benchmarks to choose between X and Y")
- The decision is genuinely not needed for the current scope (e.g., "the
@@ -373,10 +373,10 @@ A decision should be `deferred(unclear)` when:
(implies it's decided).
2. **State the blocking condition** (`deferred(scope)`) or
**investigation target** (`deferred(unclear)`) — what specific thing
would unblock this? Be concrete: "blocked on: alknet-agent crate spec
exists" or "investigation: work through 2+ example outbound-dial use
cases (hub→worker, worker→hub) to see how verifier-selection +
provider + connector compose."
would unblock this? Be concrete: "blocked on: the alknet rewrite's
dial-seam shape exists" or "investigation: work through 2+ example
outbound-dial use cases (hub→worker, worker→hub) to see how
verifier-selection + provider + connector compose."
3. **State the impacts** — what does this block downstream? Be
specific: "blocks the first hub deployment because the hub dials
workers" not "blocks the hub crate." This is the triage signal that
+7 -6
View File
@@ -191,7 +191,7 @@ also include:
Example prompt template:
```
You are an implementation specialist for the @alkdev/alknet project.
You are an implementation specialist for the @alkdev/alktls project.
Your task: {{task}}
@@ -204,13 +204,14 @@ Your task: {{task}}
7. Push: git push origin $(git branch --show-current)
8. Notify: worktree({action: "notify", args: {message: "Task completed: {{task}}. <brief summary>", level: "info"}})
Key project constraints (@alkdev/alknet):
Key project constraints (@alkdev/alktls):
- Rust: use cargo build, cargo clippy, cargo fmt, cargo test
- No comments in code
- anyhow::Result for application errors, thiserror for library error types
- Feature flags for transports (tls, iroh, acme)
- Async via tokio runtime
- No panics in library code
- thiserror for the library error type (`TlsError`, `#[non_exhaustive]`)
- Feature gates for transports (quinn, tcp, acme)
- Async via tokio runtime (no `features = ["full"]` in dependencies)
- No panics in library code; no unwrap/expect outside tests
- Behavior-preservation invariants are load-bearing (see AGENTS.md §5)
```
### Partial Generation Spawning
+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