greenfield: clean slate for ALPN-as-service pivot
Delete old source crates (alknet-core, alknet, alknet-napi), old architecture docs (ADRs, specs, open questions), old research docs (phase2, event-sourcing, feasibility, etc.), old tasks, and obsolete reference material (gitserver/MPL, honker, nats, rustfs, polyglot, keystone, distributed-identity). Keep: alknet-secret (standalone, compiles), pivot docs, iroh and ssh references, rudolfs reference (MIT/Apache, fork candidate), ops docs, sdd_process.md, and licenses. Previous implementation preserved at /workspace/@alkdev/alknet-main/ for reference during porting. Workspace compiles: cargo check + 14 tests pass for alknet-secret.
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-026-transport-interface-separation
|
||||
name: Write ADR-026 — Transport/interface separation (three-layer model)
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-026 establishing the three-layer model: Transport (Layer 1), Interface (Layer 2), Protocol (Layer 3). This is the most architecturally significant new ADR — it redefines SSH as an interface (not a transport) and enables the DNS control channel, raw framing, and future WebTransport as (Transport, Interface) pairs.
|
||||
|
||||
The three layers:
|
||||
- **Layer 1: Transport** — produces byte streams. TCP, TLS, iroh, DNS (as byte carrier), WebTransport. A `Transport` still produces `AsyncRead + AsyncWrite + Unpin + Send`.
|
||||
- **Layer 2: Interface** — consumes a `Transport::Stream` and produces call protocol events (sessions). SSH is an interface. Raw framing (4-byte length prefix + JSON EventEnvelope) is an interface. DNS control channel is a (DNS transport, raw framing interface) pair.
|
||||
- **Layer 3: Protocol** — carries semantics. Call protocol events, operation registry, service calls. Protocol is agnostic to both Transport and Interface below it.
|
||||
|
||||
A **connection** is always a (Transport, Interface) pair. The valid combinations are enumerated:
|
||||
- (TLS, SSH) — standard alknet tunnel
|
||||
- (TCP, SSH) — plain SSH tunnel
|
||||
- (iroh, SSH) — P2P SSH tunnel
|
||||
- (DNS, raw framing) — DNS control channel
|
||||
- (WebTransport, SSH) — browser SSH tunnel (future)
|
||||
- (WebTransport, raw framing) — browser call protocol (future)
|
||||
- (TCP, raw framing) — direct call protocol, local mesh
|
||||
|
||||
Key changes from current architecture:
|
||||
- SSH is an interface, not a transport. Currently deeply embedded in ServerHandler.
|
||||
- The `TransportKind` enum gains `Dns` and `WebTransport` variants (initially tags only).
|
||||
- Raw framing (4-byte BE length prefix + JSON) is an interface without SSH wrapping.
|
||||
- DNS control channel carries call protocol frames directly — it does NOT wrap SSH inside DNS.
|
||||
|
||||
This ADR requires careful review because it's the foundation for Phase 1.8 (Interface Abstraction), which is the most invasive code change.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/026-transport-interface-separation.md` exists
|
||||
- [ ] ADR follows established format
|
||||
- [ ] Context explains why SSH is currently tangled with transport and why separating them matters (enables DNS, raw framing, WebTransport without SSH)
|
||||
- [ ] Decision states: three layers; SSH is Layer 2 not Layer 1; Transport trait produces byte streams unchanged; Interface trait consumes Transport::Stream and produces call protocol sessions; connection = (Transport, Interface) pair; valid pairs enumerated
|
||||
- [ ] Shows the Interface trait signature (consume stream, produce sessions)
|
||||
- [ ] Lists the valid (Transport, Interface) combinations
|
||||
- [ ] Consequences: enables DNS control channel without SSH wrapping; enables raw framing for service mesh; SSH becomes pluggable; ServerHandler is refactored into SshInterface
|
||||
- [ ] DNS control channel carries call protocol directly (NOT SSH inside DNS) — explicitly stated
|
||||
- [ ] References: research/core.md DNS section, integration-plan.md Phase 1.8
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/core.md — transport layer, DNS transport section
|
||||
- docs/research/integration-plan.md — Phase 1.8, three-layer model, DNS as (DNS transport, raw framing interface)
|
||||
- docs/architecture/transport.md — current Transport trait (unchanged at Layer 1)
|
||||
- docs/architecture/server.md — current ServerHandler (will become SshInterface)
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-027-crate-decomposition
|
||||
name: Write ADR-027 — Crate decomposition
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-029-identity-core-type
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-027 defining the crate decomposition for the alknet project: what crates exist, what each contains, and crucially what the dependency graph looks like (which must be acyclic).
|
||||
|
||||
Crate structure:
|
||||
- **alknet-core**: transport, SSH, call protocol, config, auth types, identity, OperationSpec, Interface trait. Depends on: russh, tokio, irpc (feature-gated), serde. Does NOT depend on: alknet-secret, alknet-storage, alknet-flowgraph.
|
||||
- **alknet-secret**: BIP39, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM, SecretProtocol irpc service. Depends on: bip39, ed25519-bip32 (or rust-bip32-ed25519), aes-gcm, sha2, irpc. Does NOT depend on: alknet-core, alknet-storage.
|
||||
- **alknet-storage**: SQLite-backed metagraph, identity tables, ACL graph, honker integration, StorageProtocol irpc service. Depends on: rusqlite, honker, petgraph, jsonschema, irpc. Does NOT depend on alknet-core (but implements alknet-core's IdentityProvider trait via the trait, not a crate dep). Does NOT depend on alknet-secret (but references EncryptedData type format).
|
||||
- **alknet-flowgraph**: FlowGraph<N,E> over petgraph, operation graph, call graph, type compatibility. Depends on: petgraph, serde, jsonschema. Does NOT depend on: alknet-core, alknet-storage, alknet-secret.
|
||||
- **alknet-napi**: Node.js native addon. Depends on: alknet-core.
|
||||
- **alknet** (CLI binary): Assembles everything. Depends on: alknet-core, alknet-secret (feature), alknet-storage (feature), alknet-flowgraph (feature), toml.
|
||||
|
||||
The narrow interface points: `Identity` type, `IdentityProvider` trait, and `OperationSpec` are in alknet-core. External crates implement core traits or serialize to formats core understands.
|
||||
|
||||
This ADR must also address the irpc feature flag question (OQ: resolved — irpc is behind a feature flag in alknet-core, independent in other crates) and the storage/secret irpc dependency question (resolved — each crate depends on irpc independently).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/027-crate-decomposition.md` exists
|
||||
- [ ] ADR follows established format
|
||||
- [ ] Context explains why decomposition is needed: core shouldn't depend on heavy services; different deployment topologies need different subsets; circular dependencies prevent clean builds
|
||||
- [ ] Decision states: the six crates, their contents, and their dependencies
|
||||
- [ ] Includes the dependency graph ASCII art from integration-plan.md
|
||||
- [ ] States the narrow interface points: Identity, IdentityProvider, OperationSpec
|
||||
- [ ] States that irpc is a feature flag in alknet-core and an independent dep elsewhere
|
||||
- [ ] States that alknet-storage implements IdentityProvider via the trait (not a crate dependency on alknet-core)
|
||||
- [ ] States that alknet-storage references alknet-secret's EncryptedData wire format (type-level compatibility, not crate dep)
|
||||
- [ ] Consequences: core is lean; services are pluggable; no circular deps; deployment topology determines which crates to include
|
||||
- [ ] References: integration-plan.md dependency graph, ADR-029
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 2, dependency graph
|
||||
- docs/research/core.md — alknet-core contents
|
||||
- docs/research/services.md — service protocols
|
||||
- docs/research/storage.md — alknet-storage contents
|
||||
- docs/research/flow.md — alknet-flowgraph contents
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-028-auth-irpc-service
|
||||
name: Write ADR-028 — Auth as irpc service
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-029-identity-core-type
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-028 establishing that auth verification is provided via an irpc service protocol, with the `IdentityProvider` trait as the interface contract and `ConfigIdentityProvider` (ArcSwap-backed) as the default implementation.
|
||||
|
||||
This ADR defines the relationship between the trait-based path and the irpc path:
|
||||
|
||||
1. `IdentityProvider` trait in `alknet_core::auth` — the contract that callers depend on
|
||||
2. `ConfigIdentityProvider` — default impl, reads from `ArcSwap<DynamicConfig>`, no database needed
|
||||
3. `AuthProtocol` irpc service enum — `VerifyPubkey`, `VerifyToken`, `ReloadKeys`, `CheckAccess` — behind `irpc` feature flag
|
||||
4. Future: `StorageIdentityProvider` (in alknet-storage) backed by SQLite — additive, not replacing the trait
|
||||
|
||||
The critical design point: callers go through `IdentityProvider`. The irpc service is one way to satisfy the trait. Feature-gating (`irpc` feature) means nodes that only do SSH tunneling don't need the service layer overhead. Both paths produce the same result — an `Identity` or rejection.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/028-auth-irpc-service.md` exists
|
||||
- [ ] ADR follows established format
|
||||
- [ ] Context explains why a service layer is needed: for head nodes serving many users, in-memory key lookup doesn't scale; irpc provides async boundary for database-backed auth
|
||||
- [ ] Decision states: IdentityProvider trait is the contract; ConfigIdentityProvider is the default; AuthProtocol irpc service is behind feature flag; irpc path and trait path produce identical Identity results; StorageIdentityProvider in alknet-storage is a future additive impl
|
||||
- [ ] Shows AuthProtocol enum (`VerifyPubkey`, `VerifyToken`, `ReloadKeys`, `CheckAccess`) and AuthResult type
|
||||
- [ ] Consequences: minimal deployments use ArcSwap without irpc; production deployments wire SQLite-backed service; feature flag keeps core lean
|
||||
- [ ] References: research/services.md AuthProtocol, auth.md, research/configuration.md auth service approach, ADR-029
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/services.md — AuthProtocol definition
|
||||
- docs/architecture/auth.md — IdentityProvider trait, Identity struct
|
||||
- docs/research/configuration.md — auth service approach
|
||||
- docs/research/integration-plan.md — ADR 028 entry, Phase 1.4
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-029-identity-core-type
|
||||
name: Write ADR-029 — Identity as core type
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: single
|
||||
risk: low
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-029 establishing `Identity` struct and `IdentityProvider` trait as core types in `alknet-core`.
|
||||
|
||||
The `Identity` struct and `IdentityProvider` trait are already defined in `auth.md` (the draft architecture spec). This ADR formalizes the decision that they live in `alknet-core` — not in alknet-storage, not in alknet-services — so that core auth, forwarding policy, and call protocol all reference the same type without circular dependencies.
|
||||
|
||||
The key constraint: alknet-core defines the trait, external crates provide implementations. `ConfigIdentityProvider` (ArcSwap-backed, in core) is the default. `StorageIdentityProvider` (SQLite-backed, in alknet-storage) is the production impl. Core never depends on storage.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/029-identity-core-type.md` exists
|
||||
- [ ] ADR follows established format
|
||||
- [ ] Context explains why Identity must be in core: auth, forwarding, call protocol all need it; can't have circular deps
|
||||
- [ ] Decision states: `Identity { id, scopes, resources }` and `IdentityProvider` trait live in `alknet_core::auth`; `id` is a fingerprint (config-based auth) or account UUID (database-backed auth); derivation and storage are external concerns; default `ConfigIdentityProvider` reads from `DynamicConfig.auth`; production `StorageIdentityProvider` is in alknet-storage
|
||||
- [ ] Consequences: alknet-core has no database dependency; alknet-storage implements the core trait; the `id` field serves dual purpose (fingerprint or UUID)
|
||||
- [ ] Resolves OQ-18: IdentityProvider owns scopes, ForwardingPolicy uses scopes from Identity
|
||||
- [ ] References: auth.md, research/services.md Identity section, research/integration-plan.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/auth.md — Identity and IdentityProvider trait definitions
|
||||
- docs/research/services.md — Identity section
|
||||
- docs/research/integration-plan.md — ADR 029 entry, Phase 1.2
|
||||
- docs/architecture/open-questions.md — OQ-18
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-030-static-dynamic-config-split
|
||||
name: Write ADR-030 — Static/dynamic config split
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-030 establishing the split between `StaticConfig` (immutable after startup) and `DynamicConfig` (hot-reloadable at runtime) in alknet-core.
|
||||
|
||||
This is largely a promotion from the well-analyzed research in `docs/research/configuration.md`. The ADR records why this split matters, what goes in each config, and how reload works.
|
||||
|
||||
Key points:
|
||||
- StaticConfig: transport mode, listen addr, TLS config, iroh config, host key, stealth mode, max auth attempts, max connections per IP — everything that requires socket/TLS renegotation to change
|
||||
- DynamicConfig: auth policy (authorized keys, cert authorities), forwarding policy, rate limits — everything checked per-connection or per-channel
|
||||
- ArcSwap for lock-free hot reload of DynamicConfig
|
||||
- ServeOptions builder pattern is preserved; StaticConfig is constructed from ServeOptions
|
||||
- TOML config file is an optional convenience input format (amends ADR-011, doesn't replace programmatic API)
|
||||
- ConfigReloadHandle with `reload(DynamicConfig)` method
|
||||
- NAPI exposes `reloadAuth()`, `reloadForwarding()`, `reloadAll()` on AlknetServer
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/030-static-dynamic-config-split.md` exists
|
||||
- [ ] ADR follows established format
|
||||
- [ ] Context explains the three failures: no hot reload of auth, no forwarding policy, no structured config beyond CLI flags
|
||||
- [ ] Decision states: StaticConfig vs DynamicConfig split; ArcSwap for DynamicConfig; ServeOptions preserved as builder; TOML as optional convenience; ConfigService wraps reloads; amends ADR-011
|
||||
- [ ] Lists what's in StaticConfig and what's in DynamicConfig
|
||||
- [ ] Consequences: auth and forwarding can be reloaded without restart; config file users get TOML format; programmatic-first API preserved
|
||||
- [ ] References: research/configuration.md, ADR-011
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/configuration.md — full analysis, nearly spec-ready
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — programmatic-first decision (amended, not superseded)
|
||||
- docs/research/integration-plan.md — ADR 030 entry, Phase 1.1
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-031-forwarding-policy
|
||||
name: Write ADR-031 — Forwarding policy
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-031 establishing the forwarding policy model for `channel_open_direct_tcpip` access control.
|
||||
|
||||
Currently any authenticated client can open a channel to any destination. This ADR defines `ForwardingPolicy`, `ForwardingRule`, and `TargetPattern` as part of `DynamicConfig` (reloadable without restart).
|
||||
|
||||
Key design decisions from the research:
|
||||
- Default-allow for migration compatibility (preserves current behavior)
|
||||
- Default-deny is recommended for production
|
||||
- Rules are evaluated per-channel-open, matched against the authenticated `Identity` from `IdentityProvider`
|
||||
- `TransportKind` match in rules enables transport-specific restrictions (e.g., WebTransport clients restricted to alknet-* channels)
|
||||
- OQ-12 resolved: start with global rules + principal matching from Identity.scopes; per-user scope from peer_credentials.metadata.scopes via IdentityProvider
|
||||
- OQ-16 resolved: add TransportKind match in ForwardingRule; WebTransport clients can be scoped
|
||||
- OQ-18 resolved: IdentityProvider owns scopes, ForwardingPolicy consumes them
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/031-forwarding-policy.md` exists
|
||||
- [ ] ADR follows established format
|
||||
- [ ] Context explains the security gap: any authenticated client gets unrestricted access
|
||||
- [ ] Decision states: ForwardingPolicy with allow/deny rules, TargetPattern matching, default-allow for migration, TransportKind-aware rules, ForwardingPolicy is part of DynamicConfig (reloadable), Identity.scopes consumed by policy
|
||||
- [ ] Includes ForwardingRule and TargetPattern type signatures
|
||||
- [ ] Consequences: operators can restrict access per identity, per destination, per transport; default-allow preserves backward compatibility
|
||||
- [ ] Resolves OQ-12, OQ-16, OQ-18 (reference in ADR)
|
||||
- [ ] References: research/configuration.md, auth.md, open-questions.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/configuration.md — ForwardingPolicy section
|
||||
- docs/architecture/auth.md — Identity.scopes and IdentityProvider
|
||||
- docs/architecture/open-questions.md — OQ-12, OQ-16, OQ-18
|
||||
- docs/research/integration-plan.md — ADR 031 entry, Phase 1.3
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-032-event-boundary-discipline
|
||||
name: Write ADR-032 — Event boundary discipline
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: single
|
||||
risk: low
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-032 establishing event boundary discipline as a hard architectural constraint.
|
||||
|
||||
The research (services.md, storage.md) identifies three distinct communication patterns with clear boundaries:
|
||||
|
||||
1. **Domain events** (Honker streams) — internal to the owning service, for state reconstruction. Never cross service boundaries without projection.
|
||||
2. **irpc service calls** — synchronous request-response, within a node or cluster. Internal to the system.
|
||||
3. **Call protocol events** (EventEnvelope) — cross-node, cross-language integration events. These are what cross boundaries.
|
||||
|
||||
The ADR must state this as a hard constraint, not a suggestion. Conflating these three patterns is an anti-pattern that leads to leaky event stores and coupling.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/032-event-boundary-discipline.md` exists
|
||||
- [ ] ADR follows established format (Status, Context, Decision, Consequences, References)
|
||||
- [ ] Context explains the three patterns and why conflating them is harmful
|
||||
- [ ] Decision states: domain events stay within the owning service; irpc calls are synchronous internal boundaries; call protocol events are the only events that cross node boundaries; projection from domain events to integration events is required when crossing boundaries
|
||||
- [ ] Consequences include: prevents leaky event stores, services are independently deployable, Honker and irpc are implementation details not exposed across boundaries
|
||||
- [ ] References: research/services.md, research/storage.md, integration-plan.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/services.md — event boundary discipline section
|
||||
- docs/research/storage.md — Honker integration, event boundaries
|
||||
- docs/research/integration-plan.md — ADR 032 entry
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-033-operationenv-irpc-call-protocol
|
||||
name: Write ADR-033 — OperationEnv, irpc, and call protocol relationship
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
- architecture/adr-027-crate-decomposition
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-033 establishing OperationEnv as the universal composition mechanism that unifies irpc services and call protocol operations from the handler's perspective.
|
||||
|
||||
This is the most conceptually complex ADR. It must clearly establish:
|
||||
|
||||
1. **OperationEnv is not an implementation detail from @alkdev/operations** — it's the universal composition mechanism. Handlers compose through `context.env[namespace][op](input)` regardless of dispatch path.
|
||||
|
||||
2. **Three dispatch paths**, all producing the same result:
|
||||
- **Local dispatch**: Direct function call through the operation registry. Zero serialization.
|
||||
- **Service dispatch (irpc)**: In-cluster, Rust-to-Rust. Postcard serialization over tokio channels (local) or QUIC streams (remote). Domain-level.
|
||||
- **Remote dispatch (call protocol)**: Cross-node, cross-language. JSON EventEnvelope over any (Transport, Interface) pair. Integration-level.
|
||||
|
||||
3. **irpc is one dispatch backend for OperationEnv**, not a replacement for it. The call protocol is another dispatch backend. Both are Layer 3, at different scope boundaries.
|
||||
|
||||
4. **The behavioral contract**: namespace + operation name → invoke with input, return output. The Rust implementation can use typed method dispatch or a registry internally, but the handler-facing API must preserve this contract.
|
||||
|
||||
5. **An irpc service CAN be exposed as a call protocol operation** — the registry maps the path to a handler that internally calls the irpc service.
|
||||
|
||||
This ADR resolves the potential confusion where "service" could mean irpc service protocol, call protocol operation, or external service. The consistent naming is: irpc service (in-cluster, Rust-to-Rust, postcard), operation (path-based, call protocol, cross-node, JSON), external service (any reachable endpoint). OperationEnv unifies them.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/033-operationenv-irpc-call-protocol.md` exists
|
||||
- [ ] ADR follows established format
|
||||
- [ ] Context explains the confusion: irpc service vs call protocol operation vs external service are different dispatch mechanisms that need unification
|
||||
- [ ] Decision states: OperationEnv is the universal composition mechanism; three dispatch paths (local, irpc, remote) all produce ResponseEnvelope; handlers don't know the dispatch path; irpc is one backend, not a replacement; an irpc service can back a call protocol operation
|
||||
- [ ] Shows the dispatch diagram: OperationEnv → local | irpc service | remote call protocol
|
||||
- [ ] Shows the composition layers: Call Protocol (Layer 3, external, JSON) → irpc Service (Layer 3, internal, postcard) → Honker Streams (domain events)
|
||||
- [ ] Shows OperationEnv wiring example for minimal and production deployments
|
||||
- [ ] Defines the consistent naming: irpc service / operation / external service
|
||||
- [ ] Consequences: handlers compose through a single interface regardless of deployment; adapters (MCP, HTTP, DNS) map to operations through this interface; irpc gives type-safe efficient in-cluster calls; call protocol gives universal cross-language cross-node calls
|
||||
- [ ] Hard constraint explicitly stated: the OperationEnv composition model must match the behavioral contract from @alkdev/operations
|
||||
- [ ] References: research/services.md, @alkdev/operations, integration-plan.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/services.md — OperationContext, OperationEnv, irpc service layer
|
||||
- docs/research/integration-plan.md — ADR 033 entry, the three-layer clarification, OperationEnv section
|
||||
- docs/architecture/call-protocol.md — OperationSpec, OperationRegistry, call protocol events
|
||||
- @alkdev/operations — TypeScript OperationEnv implementation
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: architecture/adr-034-head-worker-terminology
|
||||
name: Write ADR-034 — Head/worker terminology
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: single
|
||||
risk: trivial
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Write ADR-034 formalizing the decision to use head/worker terminology instead of hub/spoke throughout the project.
|
||||
|
||||
This decision has already been applied in practice — `call-protocol.md`, `auth.md`, `open-questions.md`, and `napi-and-pubsub.md` were all updated to head/worker. The existing ADRs (024, 025) retain their original hub/spoke language because ADRs are historical records. ADR-018 is noted as superseded/extended by ADR-024 and the three-layer model.
|
||||
|
||||
The ADR exists to formally record the decision so future contributors understand why and can reference it.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/decisions/034-head-worker-terminology.md` exists
|
||||
- [ ] ADR follows the established format (Status, Context, Decision, Consequences, References)
|
||||
- [ ] Context explains why hub/spoke is being replaced (mesh topologies, a head is also a worker)
|
||||
- [ ] Decision states: head/worker everywhere in new specs and code; ADRs retain original language as historical records
|
||||
- [ ] Consequences note: natural mesh formation, consistency with integration plan terminology
|
||||
- [ ] References: integration-plan.md, ADR-024, ADR-025
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 0 ADR 034 entry, inconsistencies section item 1
|
||||
- docs/architecture/decisions/024-bidirectional-call-protocol.md — uses hub/spoke historically
|
||||
- docs/architecture/decisions/025-handler-spec-separation.md — uses hub/spoke historically
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: architecture/review-adr-foundation
|
||||
name: Review Phase 0a ADRs — foundation decisions before spec writing
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-034-head-worker-terminology
|
||||
- architecture/adr-032-event-boundary-discipline
|
||||
- architecture/adr-029-identity-core-type
|
||||
- architecture/adr-030-static-dynamic-config-split
|
||||
- architecture/adr-031-forwarding-policy
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
- architecture/adr-027-crate-decomposition
|
||||
- architecture/adr-026-transport-interface-separation
|
||||
- architecture/adr-033-operationenv-irpc-call-protocol
|
||||
scope: broad
|
||||
risk: low
|
||||
impact: project
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review all Phase 0a ADRs (026-034) before proceeding to spec writing (Phase 0b). This is the critical checkpoint where we validate that the architectural decisions are consistent and complete before downstream specs reference them.
|
||||
|
||||
This review should happen before any spec documents are created or updated, since specs will reference ADR numbers.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All 9 ADRs (026-034) are written and follow the established format
|
||||
- [ ] ADRs cross-reference each other correctly (e.g., ADR-028 references ADR-029, ADR-027 references ADR-029)
|
||||
- [ ] No ADR contradicts another (e.g., ADR-026's Interface trait must be compatible with ADR-033's OperationEnv)
|
||||
- [ ] Crate dependency graph from ADR-027 is acyclic (core depends on nothing heavy)
|
||||
- [ ] Layer boundaries from ADR-026 cleanly separate Transport, Interface, and Protocol
|
||||
- [ ] OperationEnv dispatch model from ADR-033 is consistent with ADR-028 (auth service) and ADR-027 (crate decomposition)
|
||||
- [ ] Terminology is consistent (head/worker everywhere per ADR-034)
|
||||
- [ ] Open questions referenced in ADRs have proposed resolutions consistent with the decision
|
||||
- [ ] Each ADR has clear consequences (both positive and negative)
|
||||
- [ ] ADRs are added to the README.md ADR table
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/ — all existing ADRs 001-025 for format reference
|
||||
- docs/research/integration-plan.md — Phase 0 review checklist
|
||||
|
||||
## Notes
|
||||
|
||||
ADRs reviewed for cross-reference consistency, terminology alignment, and acyclic dependency graph. Two fixes applied: ADR-027 dependency graph diagram clarified and missing cross-reference to ADR-028 added; ADR-033 missing cross-references to ADR-026 and ADR-028 added.
|
||||
|
||||
## Summary
|
||||
|
||||
All 9 Phase 0a ADRs (026-034) reviewed. No contradictions found between ADRs. Minor fixes: ADR-027 dependency graph made clearer, ADR-027 and ADR-033 cross-references completed. ADR numbering gaps (020-022) noted in README.md footnote.
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
id: architecture/review-spec-foundation
|
||||
name: Review Phase 0 specs — validate consistency, completeness, and ADR alignment
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/spec-configuration
|
||||
- architecture/spec-identity
|
||||
- architecture/spec-secret-service
|
||||
- architecture/spec-storage
|
||||
- architecture/spec-flowgraph
|
||||
- architecture/spec-interface
|
||||
- architecture/spec-services
|
||||
- architecture/spec-update-overview
|
||||
- architecture/spec-update-auth
|
||||
- architecture/spec-update-call-protocol
|
||||
- architecture/spec-update-server
|
||||
- architecture/spec-update-napi
|
||||
- architecture/spec-update-readme
|
||||
- architecture/spec-update-open-questions
|
||||
scope: broad
|
||||
risk: low
|
||||
impact: project
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review all Phase 0 spec documents after they're written. This is the Phase 0 review checklist from the integration plan, applied against the actual deliverables.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] **No inline decision rationale** — all "why" decisions are in ADRs, specs reference ADR numbers
|
||||
- [ ] **No inline open questions** — all OQs are in open-questions.md, specs reference OQ numbers
|
||||
- [ ] **Terminology is consistent** — head/worker everywhere (no hub/spoke in specs, ADRs retain historical language)
|
||||
- [ ] **Layer boundaries are clear** — every component belongs to exactly one layer (Transport, Interface, Protocol)
|
||||
- [ ] **Phase boundaries are clear** — specs distinguish what ships in Phase 1 (ConfigIdentityProvider, ArcSwap, local dispatch) from what's contracted for later (StorageIdentityProvider, irpc service layer, application services, multi-node deployment). No spec should imply that alknet-storage, alknet-secret, or the irpc service implementations already exist.
|
||||
- [ ] **Every spec has YAML frontmatter** with status and last_updated
|
||||
- [ ] **Identity is consistently defined** — Identity struct is `{id, scopes, resources}` everywhere (identity.md is canonical, auth.md references it)
|
||||
- [ ] **OperationEnv is consistently described** — three dispatch paths match across services.md, call-protocol.md, and identity.md
|
||||
- [ ] **irpc positioning is consistent** — always described as one dispatch backend for OperationEnv, never as a replacement for the call protocol
|
||||
- [ ] **Interface trait is consistent** — SshInterface and RawFramingInterface match across interface.md and server.md
|
||||
- [ ] **ForwardingPolicy is consistently placed** — in DynamicConfig, checked before proxy spawn, reference in server.md and configuration.md
|
||||
- [ ] **README.md and ADR table** include all new documents and ADRs
|
||||
- [ ] **No broken links** between doc references
|
||||
- [ ] **All specs follow the format**: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 0: Review Checklist
|
||||
- docs/architecture/ — all architecture docs
|
||||
|
||||
## Notes
|
||||
|
||||
Architecture-reviewer subagent found 4 critical, 14 warnings, 8 suggestions. All 4 critical issues fixed: OQ-SVC-05 added to open-questions.md, "NAPI/hub" → "NAPI/head", "AuthService" → "AuthProtocol"/"ConfigIdentityProvider", "noq" → "irpc over iroh QUIC streams". Key warnings fixed: inline OQ text replaced with cross-references in services.md/secret-service.md/storage.md, OQ-SVC-04 status corrected to resolved, duplicate links removed, server.md ForwardingPolicy phrasing aligned with ADR-031, ADR numbering gap footnote added. Additional improvements: ConfigServiceImpl defined in configuration.md, broken workspace-absolute links fixed (feasibility/certbot/fail2ban/event_source_types copied into docs/research/), README doc descriptions updated.
|
||||
|
||||
## Summary
|
||||
|
||||
All Phase 0 specs reviewed for consistency, completeness, and ADR alignment. 4 critical issues and key warnings resolved. ConfigServiceImpl formally defined. Link portability addressed by copying referenced docs into project tree.
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-configuration
|
||||
name: Promote configuration.md from research to architecture spec
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-030-static-dynamic-config-split
|
||||
- architecture/adr-031-forwarding-policy
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Promote `docs/research/configuration.md` to `docs/architecture/configuration.md` as a proper architecture spec document. The research doc is nearly spec-ready — this task is primarily cleanup, restructuring, and aligning with ADR decisions.
|
||||
|
||||
**Source**: `docs/research/configuration.md` (651 lines, well-analyzed)
|
||||
|
||||
**Key cleanup items**:
|
||||
1. Remove duplicate "## Problem" heading (lines 20-21 both say `## Problem`)
|
||||
2. Resolve open questions per ADRs: OQ-12 (global rules + principal matching via IdentityProvider), OQ-13 (no file watching, confirmed), OQ-14 (ArcSwap, confirmed), OQ-16 (TransportKind match in ForwardingRule), OQ-18 (IdentityProvider owns scopes)
|
||||
3. Remove inline decision rationale — reference ADR-030, ADR-031, ADR-028
|
||||
4. Remove inline open questions — reference open-questions.md OQ numbers
|
||||
5. Add YAML frontmatter: `status: draft`, `last_updated: <date>`
|
||||
6. Restructure to follow established spec format (What, Why, Architecture, Constraints, Open Questions, Design Decisions)
|
||||
7. Update terminology: head/worker (already done in research doc)
|
||||
8. Reconcile ADR-011: TOML config file amends ADR-011 (convenience layer), doesn't supersede it
|
||||
9. Remove research-only sections that are exploration/analysis — keep only the decisions and their architecture
|
||||
|
||||
**What stays**: StaticConfig/DynamicConfig split, ArcSwap model, ForwardingPolicy design, multi-transport listeners, ConfigService, NAPI reload API, TOML format, CLI vs programmatic behavior table
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/configuration.md` exists with YAML frontmatter (`status: draft`)
|
||||
- [ ] No duplicate "## Problem" heading
|
||||
- [ ] All inline decision rationale replaced with ADR references (030, 031, 028)
|
||||
- [ ] All inline open questions replaced with OQ references
|
||||
- [ ] OQ-12 resolved: global rules + principal matching, reference ADR-031
|
||||
- [ ] OQ-16 resolved: TransportKind match, reference ADR-031
|
||||
- [ ] OQ-18 resolved: IdentityProvider owns scopes, reference ADR-029
|
||||
- [ ] TOML config file positioned as amending ADR-011, not replacing programmatic API
|
||||
- [ ] Follows spec format: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
- [ ] Consistent head/worker terminology throughout
|
||||
- [ ] `docs/architecture/README.md` updated to include configuration.md in architecture docs table
|
||||
- [ ] `docs/research/configuration.md` retains its content (not deleted — it's research source material)
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/configuration.md — source material to promote
|
||||
- docs/architecture/decisions/030-static-dynamic-config-split.md — ADR to reference
|
||||
- docs/architecture/decisions/031-forwarding-policy.md — ADR to reference
|
||||
- docs/architecture/decisions/028-auth-irpc-service.md — ADR to reference
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — amended by TOML config
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-flowgraph
|
||||
name: Create flowgraph.md architecture spec (or stub referencing crate docs)
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-027-crate-decomposition
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Create `docs/architecture/flowgraph.md` — an architecture spec for the `alknet-flowgraph` crate, covering FlowGraph<N,E>, operation graph construction, call graph population, and type compatibility checking.
|
||||
|
||||
Like storage.md, this can be a contract-level document if the crate will have its own docs later. The key contract point: `OperationSpec` and `CallNodeAttrs` types must match alknet-core's definitions, with serialization as the bridge.
|
||||
|
||||
**Source**: `docs/research/flow.md` (472 lines, straightforward port of TypeScript design)
|
||||
|
||||
This is the lowest-risk new spec — pure computation crate, no I/O, no external state, straightforward petgraph port.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/flowgraph.md` exists with YAML frontmatter (`status: draft`)
|
||||
- [ ] Follows spec format: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
- [ ] Documents `FlowGraph<N, E>` generic graph over `petgraph::DiGraph`
|
||||
- [ ] Documents `NodeAttributes` / `EdgeAttributes` traits
|
||||
- [ ] Documents operation graph construction from `OperationSpec`s
|
||||
- [ ] Documents call graph population from `EventEnvelope` events
|
||||
- [ ] Documents type compatibility checking (jsonschema)
|
||||
- [ ] Documents cycle detection, topological sort, reachability queries
|
||||
- [ ] Documents serde serialization/deserialization
|
||||
- [ ] States crate dependencies: petgraph, serde, serde_json, jsonschema, thiserror
|
||||
- [ ] States crate does NOT depend on alknet-core, alknet-storage, or alknet-secret
|
||||
- [ ] States interface back to core: OperationSpec and CallNodeAttrs types match alknet-core's definitions; bridge is serialization
|
||||
- [ ] References ADR-027
|
||||
- [ ] `docs/architecture/README.md` updated to include flowgraph.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/flow.md — full FlowGraph, operation graph, call graph design
|
||||
- docs/research/integration-plan.md — Phase 2.3 (alknet-flowgraph)
|
||||
- docs/architecture/decisions/027-crate-decomposition.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-identity
|
||||
name: Create identity.md architecture spec
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-029-identity-core-type
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Create `docs/architecture/identity.md` — a new architecture spec defining the `Identity` type, `IdentityProvider` trait, and the auth flows for SSH key-based and token-based authentication.
|
||||
|
||||
This is mostly a carry-forward from `auth.md` (which already defines `IdentityProvider` trait and `Identity` struct) plus the Identity section from `research/services.md`. The key addition is making the IdentityProvider vs AuthService relationship explicit per ADR-028: `IdentityProvider` is the contract, `ConfigIdentityProvider` is the default ArcSwap-backed impl, and `AuthProtocol` irpc service is one way to satisfy the trait (behind feature flag).
|
||||
|
||||
**Source material**:
|
||||
- `auth.md` sections: IdentityProvider Trait, AuthPolicy Structure, Auth Flow in the Server, Token Authentication
|
||||
- `research/services.md` AuthService section (AuthProtocol enum, AuthResult type)
|
||||
- ADR-029 (identity as core type), ADR-028 (auth as irpc service), ADR-023 (unified auth)
|
||||
|
||||
**Relationship to auth.md**: After identity.md exists, auth.md should be updated to reference identity.md for the `Identity` and `IdentityProvider` definitions rather than defining them inline. This is handled in the `auth.md` update task.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/identity.md` exists with YAML frontmatter (`status: draft`)
|
||||
- [ ] Follows spec format: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
- [ ] Defines `Identity` struct: `{ id, scopes, resources }` — canonical definition per ADR-029
|
||||
- [ ] Defines `IdentityProvider` trait: `resolve_from_fingerprint()`, `resolve_from_token()`
|
||||
- [ ] Documents default implementation: `ConfigIdentityProvider` reading from `ArcSwap<DynamicConfig.auth>`
|
||||
- [ ] Documents head implementation: `StorageIdentityProvider` backed by SQLite `peer_credentials` + ACL graph (in alknet-storage, not core)
|
||||
- [ ] Documents irpc service path: `AuthProtocol` enum (VerifyPubkey, VerifyToken, ReloadKeys, CheckAccess) behind `irpc` feature flag per ADR-028
|
||||
- [ ] Shows both auth flows: SSH key path and token auth path, both resolving to same `Identity`
|
||||
- [ ] Consistent head/worker terminology
|
||||
- [ ] References ADR-029, ADR-028, ADR-023
|
||||
- [ ] `docs/architecture/README.md` updated to include identity.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/auth.md — existing IdentityProvider and Identity definitions
|
||||
- docs/research/services.md — AuthService, AuthProtocol enum
|
||||
- docs/architecture/decisions/029-identity-core-type.md — identity placement decision
|
||||
- docs/architecture/decisions/028-auth-irpc-service.md — auth as irpc service
|
||||
- docs/architecture/decisions/023-unified-auth-shared-key-material.md — unified auth
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-interface
|
||||
name: Create interface.md architecture spec (Layer 2)
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-026-transport-interface-separation
|
||||
- architecture/adr-033-operationenv-irpc-call-protocol
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Create `docs/architecture/interface.md` — the new Layer 2 spec defining the Interface abstraction. This is the most architecturally significant new spec document.
|
||||
|
||||
Currently, SSH is deeply embedded in the server handler. The Interface trait extracts it into a pluggable Layer 2 component:
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Interface: Send + Sync + 'static {
|
||||
type Session;
|
||||
async fn accept(stream: TransportStream, config: &InterfaceConfig) -> Result<Self::Session>;
|
||||
// The session produces call protocol events and handles responses
|
||||
}
|
||||
```
|
||||
|
||||
The spec must define:
|
||||
1. **The `Interface` trait** — consumes a `Transport::Stream` and produces call protocol sessions
|
||||
2. **`SshInterface`** — wraps existing russh handler, produces SSH channels + control channel
|
||||
3. **`RawFramingInterface`** — reads length-prefixed JSON EventEnvelope frames directly, no SSH wrapping
|
||||
4. **Valid (Transport, Interface) pairs** — enumerated per ADR-026
|
||||
5. **How the call protocol is interface-agnostic** — it receives EventEnvelope frames from any interface
|
||||
|
||||
**The hard part**: The existing `ServerHandler` owns auth, channel management, and proxy logic. The spec must cleanly define what moves to Layer 3 (call protocol), what stays in the interface, and what's shared. The split needs to be clean — this is explicitly flagged as needing careful design review in the integration plan.
|
||||
|
||||
**Depends on ADR-033 because**: The Interface trait must produce call protocol events, and OperationEnv defines how those events are dispatched. The Interface needs to produce the right shape of events for the OperationEnv to consume.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/interface.md` exists with YAML frontmatter (`status: draft`)
|
||||
- [ ] Follows spec format: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
- [ ] Defines the three-layer model (reference ADR-026): Transport (Layer 1), Interface (Layer 2), Protocol (Layer 3)
|
||||
- [ ] Defines `Interface` trait with signature, bounds, and lifecycle
|
||||
- [ ] Defines `SshInterface` — what it wraps from existing ServerHandler (auth delegation, channel open, proxy)
|
||||
- [ ] Defines `RawFramingInterface` — 4-byte BE length prefix + JSON EventEnvelope, no SSH
|
||||
- [ ] Enumerates valid (Transport, Interface) pairs per ADR-026 table
|
||||
- [ ] Defines what `InterfaceConfig` contains (different per interface type)
|
||||
- [ ] Clearly separates what moves to call protocol (Layer 3) vs what stays in the interface (Layer 2)
|
||||
- [ ] Shows how `auth_publickey()` maps to SshInterface (not RawFramingInterface which uses token auth)
|
||||
- [ ] Shows how `channel_open_direct_tcpip()` proxy logic relates to Layer 3 forwarding policy
|
||||
- [ ] DNS control channel explicitly defined as (DNS transport, raw framing interface) — NOT SSH inside DNS
|
||||
- [ ] Call protocol is interface-agnostic: receives EventEnvelope from any interface
|
||||
- [ ] References ADR-026, ADR-033
|
||||
- [ ] `docs/architecture/README.md` updated to include interface.md
|
||||
- [ ] Flags any open design questions (e.g., exactly how auth maps across interfaces)
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 1.8, three-layer model, valid (Transport, Interface) pairs
|
||||
- docs/research/core.md — DNS transport, interface concept
|
||||
- docs/architecture/decisions/026-transport-interface-separation.md
|
||||
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md
|
||||
- docs/architecture/server.md — current ServerHandler (source of SshInterface logic)
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-secret-service
|
||||
name: Create secret-service.md architecture spec
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-027-crate-decomposition
|
||||
- architecture/adr-032-event-boundary-discipline
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Create `docs/architecture/secret-service.md` — a new architecture spec for the `alknet-secret` crate and its `SecretProtocol` irpc service.
|
||||
|
||||
This slides from the research in `docs/research/services.md` (SecretProtocol definition) and `docs/research/storage.md` (secrets section, key derivation paths). The secret service is well-bounded: BIP39 mnemonics, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption for external credentials, and a lock/unlock lifecycle.
|
||||
|
||||
**Scope**: alknet-secret crate definition, not alknet-core changes.
|
||||
|
||||
**Key content from research**:
|
||||
- SecretProtocol enum: Unlock, Lock, DeriveEd25519, DeriveEncryptionKey, DeriveEthereumKey, DerivePassword, Encrypt, Decrypt
|
||||
- DerivedKey, KeyType, EncryptedData types
|
||||
- Security model: locked/unlocked states, seed in RAM only, never on disk
|
||||
- Derivation path constants (SLIP-0044 coin type 74')
|
||||
- Event boundary: SecretService domain events (honker streams for key derivation notifications) stay internal. External consumers use irpc calls or call protocol operations that project to integration events.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/secret-service.md` exists with YAML frontmatter (`status: draft`)
|
||||
- [ ] Follows spec format: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
- [ ] Documents BIP39 mnemonic generation and seed derivation
|
||||
- [ ] Documents SLIP-0010 Ed25519 HD key derivation (SLIP-0044 coin type 74')
|
||||
- [ ] Documents AES-256-GCM encryption/decryption for external credentials
|
||||
- [ ] Documents SecretProtocol irpc service: Unlock, Lock, DeriveEd25519, DeriveEncryptionKey, Encrypt, Decrypt variants
|
||||
- [ ] Documents EncryptedData type (key_version, salt, iv, ciphertext)
|
||||
- [ ] Documents derivation path constants
|
||||
- [ ] Documents security model: locked/unlocked states, seed lifecycle, never persisted
|
||||
- [ ] States crate dependencies: bip39, ed25519-bip32, aes-gcm, sha2, irpc
|
||||
- [ ] States crate does NOT depend on alknet-core or alknet-storage
|
||||
- [ ] States interface back to core: EncryptedData format referenced by alknet-storage (wire format compatibility, not crate dependency)
|
||||
- [ ] Event boundary per ADR-032: honker streams internal, irpc calls internal, no direct EventEnvelope emission
|
||||
- [ ] References ADR-027, ADR-032
|
||||
- [ ] `docs/architecture/README.md` updated to include secret-service.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/services.md — SecretProtocol definition, DerivedKey, KeyType, EncryptedData
|
||||
- docs/research/storage.md — secrets section, key derivation paths
|
||||
- docs/research/integration-plan.md — Phase 2.1 (alknet-secret)
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-services
|
||||
name: Create services.md architecture spec (irpc service layer + OperationEnv)
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-033-operationenv-irpc-call-protocol
|
||||
- architecture/adr-027-crate-decomposition
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
- architecture/adr-032-event-boundary-discipline
|
||||
scope: broad
|
||||
risk: high
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Create `docs/architecture/services.md` — the irpc service layer spec. This integrates three things that the research treated separately:
|
||||
|
||||
1. **irpc service protocols** — AuthProtocol, SecretProtocol, ConfigProtocol, StorageProtocol — their enum definitions, wire formats, and backends
|
||||
2. **OperationEnv** — the universal composition mechanism with three dispatch paths (local, irpc, remote)
|
||||
3. **OperationContext** — the request context that handlers receive
|
||||
|
||||
This is the second most complex new spec (after interface.md). The integration plan spends the most words on this topic because it's where the most confusion existed between irpc services, call protocol operations, and external services.
|
||||
|
||||
The spec must make it crystal clear:
|
||||
- irpc services are in-cluster, Rust-to-Rust, postcard serialization
|
||||
- Call protocol operations are cross-node, cross-language, JSON EventEnvelope
|
||||
- OperationEnv unifies them from the handler's perspective
|
||||
- An irpc service can back a call protocol operation via OperationEnv
|
||||
- Both are Layer 3 but at different scope boundaries
|
||||
|
||||
**Source**: `docs/research/services.md` (808 lines) + integration plan's OperationEnv and dispatch path sections + ADR-033
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/services.md` exists with YAML frontmatter (`status: draft`)
|
||||
- [ ] Follows spec format: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
- [ ] Documents irpc service pattern: `#[rpc_requests]` enum, Serializable vs WithChannels, `Client<S>`
|
||||
- [ ] Documents all four service protocols: AuthProtocol, SecretProtocol, ConfigProtocol, StorageProtocol (type signatures, not full implementations — those go in per-crate specs)
|
||||
- [ ] Documents OperationContext struct: request_id, parent_request_id, identity, metadata, env, trusted
|
||||
- [ ] Documents OperationEnv as universal composition mechanism per ADR-033
|
||||
- [ ] Shows three dispatch paths with examples: local (direct call), irpc service (postcard over mpsc/QUIC), remote (call protocol EventEnvelope)
|
||||
- [ ] Shows OperationEnv wiring for minimal and production deployments
|
||||
- [ ] Shows how adapters (MCP, OpenAPI, HTTP, DNS) map to OperationEnv
|
||||
- [ ] Consistent naming: irpc service / operation / external service (per ADR-033)
|
||||
- [ ] Composition diagram: Call Protocol → irpc Service → Honker Streams (per ADR-032)
|
||||
- [ ] Hard constraint stated: handler-facing OperationEnv API matches @alkdev/operations behavioral contract
|
||||
- [ ] Event boundary per ADR-032: domain events never cross boundaries without projection
|
||||
- [ ] References ADR-027, ADR-028, ADR-032, ADR-033
|
||||
- [ ] `docs/architecture/README.md` updated to include services.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/services.md — full service protocol definitions, OperationContext, OperationEnv
|
||||
- docs/research/integration-plan.md — OperationEnv section, three dispatch paths, adapter patterns
|
||||
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md
|
||||
- docs/architecture/decisions/027-crate-decomposition.md
|
||||
- docs/architecture/decisions/028-auth-irpc-service.md
|
||||
- docs/architecture/decisions/032-event-boundary-discipline.md
|
||||
- @alkdev/operations — TypeScript OperationEnv implementation
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-storage
|
||||
name: Create storage.md architecture spec (or stub referencing crate docs)
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-027-crate-decomposition
|
||||
- architecture/adr-029-identity-core-type
|
||||
- architecture/adr-032-event-boundary-discipline
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Create `docs/architecture/storage.md` — an architecture spec for the `alknet-storage` crate, covering the metagraph data model, identity tables, ACL graph, honker integration, and StorageProtocol irpc service.
|
||||
|
||||
The integration plan notes this could be "a new spec or reference alknet-storage's own docs." Since alknet-storage doesn't exist yet as a crate, we need an architecture spec here to define its contract — especially the interface back to core (`StorageIdentityProvider` implementing alknet-core's `IdentityProvider` trait).
|
||||
|
||||
If the crate will have its own detailed docs later, this spec can be a contract-level document: what storage provides, what it depends on, and how it connects to core.
|
||||
|
||||
**Source**: `docs/research/storage.md` (460 lines, comprehensive)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `docs/architecture/storage.md` exists with YAML frontmatter (`status: draft`)
|
||||
- [ ] Follows spec format: What, Why, Architecture, Constraints, Open Questions, Design Decisions
|
||||
- [ ] Documents metagraph data model: GraphType, NodeType, EdgeType, Graph, Node, Edge
|
||||
- [ ] Documents identity tables: accounts, organizations, peer_credentials, api_keys, audit_logs
|
||||
- [ ] Documents ACL as metagraph: PrincipalNode, DelegatesEdge, access control graph
|
||||
- [ ] Documents encrypted node type: bridges to alknet-secret's EncryptedData format
|
||||
- [ ] Documents honker integration: stream_publish/subscribe, notify/listen, queue/claim
|
||||
- [ ] Documents System DB vs Tenant DB separation
|
||||
- [ ] Documents `StorageIdentityProvider`: implements alknet-core's `IdentityProvider` trait (queries peer_credentials + ACL graph) per ADR-029
|
||||
- [ ] Documents `StorageProtocol` irpc service with key variants
|
||||
- [ ] States crate dependencies: rusqlite, honker, petgraph, jsonschema, irpc
|
||||
- [ ] States crate does NOT depend on alknet-core (implements core's trait by depending on alknet-core types, not the full crate — or via the trait definition only)
|
||||
- [ ] Event boundary per ADR-032: honker streams stay within storage service, StorageProtocol serves as internal boundary, call protocol events are projections
|
||||
- [ ] References ADR-027, ADR-029, ADR-032
|
||||
- [ ] `docs/architecture/README.md` updated to include storage.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/storage.md — full metagraph, identity, ACL, honker definitions
|
||||
- docs/research/integration-plan.md — Phase 2.2 (alknet-storage)
|
||||
- docs/architecture/decisions/027-crate-decomposition.md
|
||||
- docs/architecture/decisions/029-identity-core-type.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-update-auth
|
||||
name: Update auth.md — add IdentityProvider vs AuthService relationship
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/spec-identity
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/auth.md` to add the IdentityProvider vs AuthService relationship and update for the `AuthProtocol` irpc service per ADR-028.
|
||||
|
||||
The current auth.md already defines `IdentityProvider` trait and `Identity` struct — which is good. After identity.md exists as the canonical home for those definitions, auth.md should reference identity.md instead of defining them inline.
|
||||
|
||||
**Changes needed**:
|
||||
1. Replace inline `Identity` and `IdentityProvider` definitions with references to identity.md
|
||||
2. Add section on `AuthProtocol` irpc service (VerifyPubkey, VerifyToken, ReloadKeys, CheckAccess) behind `irpc` feature flag
|
||||
3. Add section on `ConfigIdentityProvider` as the default impl (ArcSwap-backed)
|
||||
4. Clarify the relationship: `IdentityProvider` is the contract, irpc `AuthProtocol` is one way to implement it, `ConfigIdentityProvider` is another
|
||||
5. Remove inline decision rationale about IdentityProvider placement — reference ADR-029
|
||||
6. Reference ADR-028 for the irpc service decision
|
||||
|
||||
**What stays the same**: Token authentication design, AuthPolicy structure, browser-side token construction, WebTransport session request inspection, security considerations, all existing constraints.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Identity` and `IdentityProvider` definitions reference identity.md (canonical) rather than defining inline
|
||||
- [ ] `AuthProtocol` irpc service documented with variants (VerifyPubkey, VerifyToken, ReloadKeys, CheckAccess) per ADR-028
|
||||
- [ ] `ConfigIdentityProvider` documented as default implementation (ArcSwap path)
|
||||
- [ ] Relationship between trait-based path and irpc path clearly stated
|
||||
- [ ] `irpc` feature flag mentioned for AuthProtocol
|
||||
- [ ] Inline decision rationale replaced with ADR references (028, 029)
|
||||
- [ ] `last_updated` in YAML frontmatter updated
|
||||
- [ ] No hub/spoke terminology
|
||||
- [ ] References section updated to include identity.md, ADR-028, ADR-029
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/auth.md — current content to update
|
||||
- docs/research/integration-plan.md — auth.md update entry
|
||||
- docs/architecture/decisions/028-auth-irpc-service.md
|
||||
- docs/architecture/decisions/029-identity-core-type.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-update-call-protocol
|
||||
name: Update call-protocol.md — add OperationEnv dispatch paths, irpc as backend
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-033-operationenv-irpc-call-protocol
|
||||
- architecture/spec-services
|
||||
- architecture/spec-interface
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/call-protocol.md` to add OperationEnv as the universal composition mechanism with three dispatch paths, and show how irpc is one backend for OperationEnv (not a replacement).
|
||||
|
||||
The current call-protocol.md already covers: operation paths, EventEnvelope wire format, call protocol events, bidirectional routing, head/worker architecture, operation registry, ACL, service discovery, PendingRequestMap, and protocol adapter layer. It was already updated for head/worker terminology.
|
||||
|
||||
**What's missing** (per integration plan):
|
||||
1. **OperationEnv as universal composition mechanism** — the handler-facing API that unifies local calls, irpc service calls, and remote call protocol calls
|
||||
2. **Three dispatch paths** — local dispatch, irpc service dispatch, remote dispatch, all producing the same ResponseEnvelope
|
||||
3. **OperationContext** — request_id, parent_request_id, identity, metadata, env, trusted
|
||||
4. **How irpc is one backend** — not a replacement for the call protocol or for OperationEnv
|
||||
5. **How a call protocol handler can call an irpc service internally** (e.g., /head/auth/verify calls AuthProtocol::VerifyPubkey) — note: this is Phase 2+ composition; Phase 1 uses IdentityProvider directly
|
||||
|
||||
**Phase boundary note**: Phase 1 ships with local dispatch only (direct function calls through the operation registry). The irpc service dispatch and remote dispatch paths are contracted here but not built yet. OperationEnv should be documented with all three paths, but the spec should make it clear that Phase 1 is local-only. The agent service pattern example (`/head/agent/chat`) is a downstream application concern, not a core requirement.
|
||||
|
||||
**What stays the same**: Operation paths, EventEnvelope, call protocol events, bidirectional routing, head/worker architecture, PendingRequestMap, protocol adapter layer.
|
||||
|
||||
**Note**: Hub/spoke was already updated to head/worker. ADR references are partial (currently references 018, 024, 025).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] OperationEnv section added, documenting it as universal composition mechanism per ADR-033
|
||||
- [ ] Three dispatch paths documented with examples: local (zero serialization), irpc service (postcard), remote (JSON EventEnvelope)
|
||||
- [ ] OperationContext struct documented (request_id, parent_request_id, identity, metadata, env, trusted)
|
||||
- [ ] ResponseEnvelope as universal return type documented
|
||||
- [ ] Operation handlers receive `(input: Value, context: OperationContext) -> ResponseEnvelope`
|
||||
- [ ] irpc explicitly positioned as one dispatch backend for OperationEnv per ADR-033
|
||||
- [ ] Phase boundary clear: Phase 1 is local dispatch only; irpc and remote dispatch paths are contracted but not built yet
|
||||
- [ ] Agent service pattern is noted as a downstream application concern, not a core requirement
|
||||
- [ ] Hard constraint stated: OperationEnv composition model matches @alkdev/operations behavioral contract
|
||||
- [ ] ADR table updated with references to 028, 033
|
||||
- [ ] Reference to services.md for full OperationEnv and irpc service details
|
||||
- [ ] Reference to interface.md for how EventEngrams flow from interfaces
|
||||
- [ ] `last_updated` in YAML frontmatter updated
|
||||
- [ ] No hub/spoke terminology in any new content
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/call-protocol.md — current content to update
|
||||
- docs/research/integration-plan.md — call-protocol.md update entry
|
||||
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md
|
||||
- @alkdev/operations — TypeScript OperationEnv
|
||||
|
||||
## Notes
|
||||
|
||||
Added OperationEnv section (universal composition, three dispatch paths, service assembly, Phase boundary), OperationContext struct, ResponseEnvelope/CallError types, irpc-as-one-backend positioning, Phase 1 local-only constraint, agent service pattern clarified as downstream concern, ADRs 028/033 added, references to identity.md/interface.md/services.md added.
|
||||
|
||||
## Summary
|
||||
|
||||
Call protocol spec updated with OperationEnv, three dispatch paths, OperationContext, and clear irpc positioning. All acceptance criteria met.
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-update-napi
|
||||
name: Update napi-and-pubsub.md — add reload API and call protocol references
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/spec-configuration
|
||||
- architecture/adr-033-operationenv-irpc-call-protocol
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/napi-and-pubsub.md` to add the NAPI reload API (reloadAuth, reloadForwarding, reloadAll) and references to the call protocol integration.
|
||||
|
||||
The current spec is reviewed and stable. Changes are additive:
|
||||
|
||||
1. Add `reloadAuth()`, `reloadForwarding()`, `reloadAll()` methods to `AlknetServer` interface
|
||||
2. Add note about call protocol integration: NAPI consumers can register operation handlers
|
||||
3. Add note about irpc service creation for NAPI consumers (behind feature flag)
|
||||
4. Reference configuration.md and services.md for details
|
||||
|
||||
**What stays the same**: Everything else. The NAPI wrapper, pubsub adapter, connect vs serve semantics, stream handling, constraints — all unchanged.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `AlknetServer` interface in NAPI section updated with reloadAuth(), reloadForwarding(), reloadAll()
|
||||
- [ ] TypeScript interface signature for reload methods matches research/configuration.md NAPI section
|
||||
- [ ] Note added: call protocol integration — NAPI consumers can register operation handlers
|
||||
- [ ] Note added: irpc service creation available for NAPI consumers (feature-gated)
|
||||
- [ ] References to configuration.md and services.md added
|
||||
- [ ] `last_updated` in YAML frontmatter updated
|
||||
- [ ] Existing content unchanged except where additions are made
|
||||
- [ ] ADR references remain correct
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md — current content
|
||||
- docs/research/configuration.md — NAPI Reload API section
|
||||
- docs/research/integration-plan.md — Phase 3.4 (NAPI layer updates)
|
||||
|
||||
## Notes
|
||||
|
||||
Added reload API (reloadAuth, reloadForwarding, reloadAll with ForwardingPolicyConfig and ForwardingRuleConfig interfaces), call protocol integration note, irpc service creation note (Phase 2+), ADR-030 reference, and references to configuration.md, services.md, call-protocol.md.
|
||||
|
||||
## Summary
|
||||
|
||||
NAPI spec updated with dynamic config reload API per ADR-030. All acceptance criteria met.
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-update-open-questions
|
||||
name: Update open-questions.md — resolve questions per ADR decisions
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-031-forwarding-policy
|
||||
- architecture/adr-029-identity-core-type
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
- architecture/adr-030-static-dynamic-config-split
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/open-questions.md` to record resolutions for the open questions that the new ADRs address.
|
||||
|
||||
**Questions to resolve**:
|
||||
- **OQ-12** (Per-user forwarding scope vs global rules): Resolved per ADR-031 — start with global rules + principal matching. Per-user scope from peer_credentials.metadata.scopes via IdentityProvider.
|
||||
- **OQ-16** (Transport-specific forwarding policy): Resolved per ADR-031 — add TransportKind match in ForwardingRule. WebTransport clients can be restricted to alknet-* channels.
|
||||
- **OQ-18** (Source of Identity.scopes): Resolved per ADR-029 and ADR-031 — IdentityProvider owns scopes, ForwardingPolicy uses scopes from Identity.
|
||||
- **OQ-22** (Client streaming in call protocol): Resolved per integration plan — defer. Current model (single request, optional streaming response) covers all identified use cases.
|
||||
- **New** (irpc dependency: always or feature flag?): Resolved per ADR-027 — feature flag. Nodes that only do SSH tunneling don't need the service layer.
|
||||
- **New** (DNS control channel scope): Resolved per ADR-026 — call protocol frames only (no SSH over DNS).
|
||||
- **New** (alknet-storage and alknet-secret irpc dependency): Resolved per ADR-027 — independently.
|
||||
|
||||
**Questions that remain open** (deferred):
|
||||
- **OQ-15** (TLS + WebTransport + iroh QUIC coexistence): Deferred to Phase 4 per integration plan.
|
||||
- **OQ-19** (Separate TLS identity for WebTransport): Deferred to Phase 4.
|
||||
- **OQ-20** (Worker registration and discovery): Still open per integration plan. Register on connect, cleanup on disconnect is the leading approach but needs spec in call-protocol.md.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] OQ-12 marked as resolved with ADR-031 reference
|
||||
- [ ] OQ-16 marked as resolved with ADR-031 reference
|
||||
- [ ] OQ-18 marked as resolved with ADR-029/ADR-031 reference
|
||||
- [ ] OQ-22 marked as resolved (deferred) with note
|
||||
- [ ] New OQ (irpc feature flag) added and resolved with ADR-027 reference
|
||||
- [ ] New OQ (DNS control channel scope) added and resolved with ADR-026 reference
|
||||
- [ ] New OQ (storage/secret irpc dep) added and resolved with ADR-027 reference
|
||||
- [ ] OQ-15, OQ-19, OQ-20 remain open with notes on deferral
|
||||
- [ ] `last_updated` in YAML frontmatter updated
|
||||
- [ ] Format consistent with existing resolved entries (strikethrough priority, ADR reference)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/open-questions.md — current content
|
||||
- docs/research/integration-plan.md — "Open Questions to Resolve Before Phase 1" section
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-update-overview
|
||||
name: Update overview.md — add crate structure, Layer 3, services, identity
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-027-crate-decomposition
|
||||
- architecture/adr-026-transport-interface-separation
|
||||
- architecture/adr-033-operationenv-irpc-call-protocol
|
||||
- architecture/adr-029-identity-core-type
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/overview.md` to reflect the expanded scope from the integration plan. The current overview documents the alpha scope (SSH tunneling). It needs additions for:
|
||||
|
||||
1. **Crate structure** — alknet-core, alknet-secret, alknet-storage, alknet-flowgraph, alknet-napi, alknet (CLI). Per ADR-027. Note which crates exist now (core, napi) vs which are contracted for later phases (secret, storage, flowgraph).
|
||||
2. **Three-layer model** — Transport (Layer 1), Interface (Layer 2), Protocol (Layer 3). SSH is an interface, not a transport. Per ADR-026.
|
||||
3. **Service layer concept** — irpc services for in-cluster communication, OperationEnv for composition. Per ADR-033. Note this is Phase 2+ for the service implementations; Phase 1 uses ConfigIdentityProvider and ConfigServiceImpl directly.
|
||||
4. **Identity as core type** — Identity struct and IdentityProvider trait in alknet-core. Per ADR-029. Phase 1 ships ConfigIdentityProvider only; StorageIdentityProvider is contracted for when alknet-storage is built.
|
||||
5. **Updated dependency table** — new dependencies (irpc feature-gated, bip39, rusqlite, honker, petgraph, jsonschema)
|
||||
6. **Updated ADR table** — add ADRs 026-034
|
||||
7. **Updated architecture constraints** — add: Interface as Layer 2, OperationEnv as universal composition, event boundary discipline, static/dynamic config split
|
||||
|
||||
The existing content (purpose, SSH tunneling, transport pluggability, etc.) stays. We're adding, not replacing.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Crate structure section added with phase notes: core and napi exist now; secret, storage, flowgraph are Phase 2+ contracts
|
||||
- [ ] Phase boundary noted: service implementations (irpc) are Phase 2+; Phase 1 uses ConfigIdentityProvider and ArcSwap directly
|
||||
- [ ] Three-layer model mentioned in architecture constraints per ADR-026
|
||||
- [ ] Service layer concept mentioned: irpc + OperationEnv per ADR-033
|
||||
- [ ] Identity and IdentityProvider mentioned as core types per ADR-029
|
||||
- [ ] Updated dependency table with new crate dependencies
|
||||
- [ ] ADR table updated: ADRs 026-034 added with correct titles and status
|
||||
- [ ] Architecture constraints updated: add Layer 2 interface concept, OperationEnv, event boundary, static/dynamic config
|
||||
- [ ] All new references to architecture specs link correctly (identity.md, services.md, interface.md, configuration.md, etc. — even if those specs are still being written)
|
||||
- [ ] `last_updated` in YAML frontmatter updated
|
||||
- [ ] No hub/spoke terminology remains
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — expanded scope, dependency graph
|
||||
- docs/architecture/overview.md — current content to update
|
||||
|
||||
## Notes
|
||||
|
||||
Added crate structure table (6 crates, phase notes), three-layer model section, service layer section with Phase boundary note, Identity section with struct definition, expanded exports list, rebuilt dependency table per-crate, added 8 new architecture constraints (three-layer model, event boundary, OperationEnv, forwarding policy, static/dynamic config, unified auth, config split), expanded ADR table from 19 to 31 entries, updated references to include all new specs + irpc.
|
||||
|
||||
## Summary
|
||||
|
||||
Overview spec updated from alpha scope to full Phase 0 scope. All acceptance criteria met.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-update-readme
|
||||
name: Update architecture README.md — add new docs and ADRs to tables
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/spec-configuration
|
||||
- architecture/spec-identity
|
||||
- architecture/spec-secret-service
|
||||
- architecture/spec-storage
|
||||
- architecture/spec-flowgraph
|
||||
- architecture/spec-interface
|
||||
- architecture/spec-services
|
||||
- architecture/review-adr-foundation
|
||||
scope: narrow
|
||||
risk: trivial
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/README.md` to add all new architecture documents and ADRs (026-034) to their respective tables. This is the final documentation assembly task — it runs after all new spec docs are created.
|
||||
|
||||
**Changes to Architecture Documents table**:
|
||||
Add rows for: identity.md, services.md, interface.md, configuration.md, secret-service.md, storage.md, flowgraph.md
|
||||
|
||||
**Changes to Research Documents table**:
|
||||
No changes needed — research docs stay as-is.
|
||||
|
||||
**Changes to ADR Table**:
|
||||
Add ADRs 026-034 with correct titles and status (Accepted).
|
||||
|
||||
**Changes to Current State section**:
|
||||
Update to reflect the new scope: services, identity, interface layer, configuration architecture.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Architecture Documents table includes all new docs (identity.md, services.md, interface.md, configuration.md, secret-service.md, storage.md, flowgraph.md) with correct status and descriptions
|
||||
- [ ] ADR table includes ADRs 026-034 with correct titles and "Accepted" status
|
||||
- [ ] Current State section updated to reflect expanded scope
|
||||
- [ ] `last_updated` in YAML frontmatter updated
|
||||
- [ ] No broken links (all new doc references point to files that exist)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/README.md — current content to update
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
id: architecture/spec-update-server
|
||||
name: Update server.md — add DynamicConfig, ForwardingPolicy, IdentityProvider references
|
||||
status: completed
|
||||
depends_on:
|
||||
- architecture/adr-030-static-dynamic-config-split
|
||||
- architecture/adr-031-forwarding-policy
|
||||
- architecture/adr-028-auth-irpc-service
|
||||
- architecture/adr-026-transport-interface-separation
|
||||
- architecture/spec-configuration
|
||||
- architecture/spec-identity
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/server.md` to reflect the architectural changes from Phase 1: DynamicConfig, ForwardingPolicy in channel handling, IdentityProvider replacing direct ServerAuthConfig reads, and the interface abstraction concept.
|
||||
|
||||
**Phase boundary note**: Phase 1 ships `ConfigIdentityProvider` (ArcSwap-backed) as the only `IdentityProvider` implementation. The irpc `AuthProtocol` and `StorageIdentityProvider` are contracted in the specs but not built yet. Server.md should describe what the server actually does in Phase 1 — reading auth from `ArcSwap<DynamicConfig>` via `ConfigIdentityProvider` — with a forward reference to identity.md for the full trait hierarchy. Don't describe irpc service wiring or SQLite-backed auth as if they exist.
|
||||
|
||||
The current server.md is thorough but reflects the alpha architecture where auth is read directly from `ServerAuthConfig` and there's no forwarding policy concept.
|
||||
|
||||
**Changes needed**:
|
||||
1. Update Authentication section: auth goes through `IdentityProvider` trait (reference identity.md, ADR-029), with `ConfigIdentityProvider` as the Phase 1 impl reading from `ArcSwap<DynamicConfig>` (reference ADR-030). Note that `StorageIdentityProvider` is a future implementation.
|
||||
2. Add ForwardingPolicy check in Channel Handling section: before proxy spawn, evaluate ForwardingPolicy against Identity (reference configuration.md, ADR-031)
|
||||
3. Replace `Arc<ServerAuthConfig>` with `Arc<ArcSwap<DynamicConfig>>` in ServerHandler description (reference ADR-030)
|
||||
4. Add note about Interface abstraction: SSH is one interface (Layer 2), ServerHandler logic maps to SshInterface (reference interface.md, ADR-026) — but detail is in interface.md, not here
|
||||
5. Update CLI interface section: mention `--config` flag for TOML config, `[[listeners]]` for multi-transport
|
||||
6. Update constraint about single transport: "Currently binds to a single transport" → note that multi-transport is coming per ADR-030
|
||||
|
||||
**What stays the same**: TLS cert provisioning, stealth mode, outbound proxy modes, logging/rate limiting, graceful shutdown, error handling, most CLI flags.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Authentication section updated: references `IdentityProvider` trait with `ConfigIdentityProvider` as Phase 1 impl, notes `StorageIdentityProvider` as future
|
||||
- [ ] Channel Handling section updated: ForwardingPolicy check before proxy spawn, reference ADR-031
|
||||
- [ ] ServerHandler struct updated: `Arc<ArcSwap<DynamicConfig>>`, not `Arc<ServerAuthConfig>`
|
||||
- [ ] Note added about Interface abstraction pointing to interface.md and ADR-026
|
||||
- [ ] CLI section mentions `--config` flag (TOML) and `[[listeners]]` for multi-transport
|
||||
- [ ] Single-transport constraint softened (noted as current, changing per ADR-030)
|
||||
- [ ] Phase boundary clear: what ships in Phase 1 vs what's contracted for later
|
||||
- [ ] `last_updated` in YAML frontmatter updated
|
||||
- [ ] ADR table updated with references to 026, 028, 029, 030, 031
|
||||
- [ ] References section updated to include configuration.md, identity.md, interface.md
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — current content to update
|
||||
- docs/architecture/decisions/030-static-dynamic-config-split.md
|
||||
- docs/architecture/decisions/031-forwarding-policy.md
|
||||
- docs/architecture/decisions/028-auth-irpc-service.md
|
||||
- docs/architecture/decisions/026-transport-interface-separation.md
|
||||
|
||||
## Notes
|
||||
|
||||
Rewrote Authentication section around IdentityProvider trait with Phase 1/Phase 2 boundary. Added ForwardingPolicy check before proxy spawn. Added Interface Abstraction section (ServerHandler → SshInterface refactoring). Added --config flag and [[listeners]] TOML to CLI section. Softened single-transport constraint. Added ADRs 026/028/029/030/031 to design decisions and references section.
|
||||
|
||||
## Summary
|
||||
|
||||
Server spec updated for IdentityProvider, ForwardingPolicy, DynamicConfig, and Interface abstraction. Phase 1 boundary clear. All acceptance criteria met.
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
id: auth/client-auth-handler
|
||||
name: Implement client-side SSH authentication with Ed25519 key pairs
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/key-loading
|
||||
- auth/error-types
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the client-side SSH authentication. The client presents an Ed25519 private key during SSH handshake. This creates the `russh::client::Handler` implementation and the `russh::client::ConnectStreamConfig` that uses the loaded key.
|
||||
|
||||
No password auth. The client handler is simpler than the server — it just needs to provide the private key and handle the auth callback from russh.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/auth/client_auth.rs` exports `ClientAuthConfig` and client handler
|
||||
- [ ] `ClientAuthConfig` holds: `private_key: KeyPair`, optional `public_key: PublicKey`
|
||||
- [ ] `ClientAuthConfig::from_key_source(source: KeySource) -> Result<Self>` — loads key via key-loading module
|
||||
- [ ] Implements `russh::client::Handler` with `auth_publickey()` returning the public key
|
||||
- [ ] Client handler returns `russh::client::AuthResult::Accept` or appropriate auth state
|
||||
- [ ] Unit tests: valid key creates handler, auth flow succeeds with mock SSH session
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — "Authentication is Ed25519 public key or OpenSSH certificate (ADR-012)"
|
||||
- docs/architecture/decisions/012-auth-ed25519-and-cert-authority.md — key-based auth only
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: auth/error-types
|
||||
name: Define error types for transport, auth, channel, and configuration layers
|
||||
status: completed
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: trivial
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define the error hierarchy per the overview.md layered error pattern:
|
||||
- **Transport errors** — connection failures, TLS handshake failures, iroh endpoint errors
|
||||
- **Auth errors** — key rejection, certificate validation failures, missing keys
|
||||
- **Channel errors** — target unreachable, proxy failure
|
||||
- **Config errors** — invalid flags, key file not found, bind failure
|
||||
|
||||
Use `thiserror` for structured error types propagated via `anyhow::Result` in the public API. The key design: transport/auth errors cause reconnection (client) or rejection (server). Channel-level errors close that channel without killing the session.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/error.rs` exports error types
|
||||
- [ ] `TransportError` enum: `ConnectionFailed`, `HandshakeFailed`, `Timeout`, `ProxyFailed`
|
||||
- [ ] `AuthError` enum: `KeyRejected`, `CertInvalid`, `CertExpired`, `CertPrincipalMismatch`, `NoMatchingKey`
|
||||
- [ ] `ChannelError` enum: `TargetUnreachable`, `ProxyConnectFailed`, `ChannelClosed`
|
||||
- [ ] `ConfigError` enum: `InvalidFlag`, `KeyFileNotFound`, `BindFailed`, `IncompatibleOptions`
|
||||
- [ ] All error types implement `std::error::Error` via `thiserror`, `Display`, and `Debug`
|
||||
- [ ] Error types have `source` chaining where appropriate (e.g., `TransportError::HandshakeFailed { source: std::io::Error }`)
|
||||
- [ ] Re-exported from `crates/alknet-core/src/lib.rs`
|
||||
- [ ] Unit tests for Display output of each error variant
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/overview.md — "Error handling follows a consistent layered pattern"
|
||||
- docs/architecture/client.md — error handling section (transport → reconnect, channel → close)
|
||||
- docs/architecture/server.md — error handling section
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
id: auth/key-loading
|
||||
name: Implement SSH key material loading (file paths and in-memory data)
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/error-types
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement key material loading that accepts both file paths and in-memory data per the programmatic-first API (ADR-011). Key inputs (`--identity`, `--authorized-keys`, `--cert-authority`, `--key`) accept either:
|
||||
- **File path**: load from filesystem
|
||||
- **In-memory data**: raw key bytes provided programmatically
|
||||
|
||||
All keys must be in **OpenSSH key format** (not PEM/PKCS#1/PKCS#8). This module handles:
|
||||
- Loading private keys (OpenSSH format: `-----BEGIN OPENSSH PRIVATE KEY-----`)
|
||||
- Loading public keys (OpenSSH format: `ssh-ed25519 AAAA... user@host`)
|
||||
- Loading authorized_keys files (standard OpenSSH format)
|
||||
- Parsing `cert-authority` entries in authorized_keys
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/auth/keys.rs` exports key loading functions
|
||||
- [ ] `KeySource` enum: `File(PathBuf)` and `Memory(Vec<u8>)` for unified key input handling
|
||||
- [ ] `load_private_key(source: KeySource) -> Result<russh::key::KeyPair>` — loads OpenSSH private key from file or memory
|
||||
- [ ] `load_public_keys(source: KeySource) -> Result<Vec<russh::key::PublicKey>>` — loads one or more public keys from authorized_keys format
|
||||
- [ ] Parses standard `authorized_keys` format including options (e.g., `cert-authority,permit-port-forwarding ssh-ed25519 AAAA...`)
|
||||
- [ ] `CertAuthorityEntry` struct: `public_key: PublicKey, options: Vec<String>` parsed from authorized_keys cert-authority lines
|
||||
- [ ] Returns `ConfigError::KeyFileNotFound` for missing file paths
|
||||
- [ ] Returns `ConfigError::InvalidFlag` with clear message for PEM-encoded (non-OpenSSH) keys
|
||||
- [ ] Unit tests: load Ed25519 key from file, load from memory, parse authorized_keys with multiple entries, reject PEM format
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — Key Material Format section
|
||||
- docs/architecture/server.md — Key Material Format section
|
||||
- docs/architecture/decisions/012-auth-ed25519-and-cert-authority.md — authorized_keys format with cert-authority
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — programmatic-first, file paths or in-memory
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: auth/server-auth-handler
|
||||
name: Implement server-side authentication (Ed25519 keys + OpenSSH cert-authority)
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/key-loading
|
||||
- auth/error-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the server-side SSH authentication logic per ADR-012:
|
||||
|
||||
1. **Ed25519 public key**: `auth_publickey()` checks presented key against the authorized set using constant-time comparison
|
||||
2. **OpenSSH certificate authority**: validates presented certificate — checks CA signature, expiry, and principal restrictions (`permit-port-forwarding`, `no-pty`, `source-address`)
|
||||
|
||||
No password authentication over SSH. This is the `russh::server::Handler::auth_publickey()` implementation that the server handler will call.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/auth/server_auth.rs` exports `ServerAuthConfig` and auth logic
|
||||
- [ ] `ServerAuthConfig` holds: `authorized_keys: HashSet<PublicKey>`, `cert_authorities: Vec<CertAuthorityEntry>`
|
||||
- [ ] `ServerAuthConfig::from_keys_and_ca()` constructor: loads authorized keys and cert-authority entries from provided key sources
|
||||
- [ ] Auth check function: given a presented key/certificate, return `Accept` or `Reject`
|
||||
- [ ] Ed25519 key matching uses constant-time comparison (via `russh`/`ssh-key` crate builtins)
|
||||
- [ ] Certificate validation checks: CA signature valid, cert not expired, principal restrictions enforced
|
||||
- [ ] Certificate options respected: `permit-port-forwarding`, `no-pty`, `source-address`
|
||||
- [ ] Returns `AuthError::KeyRejected` or `AuthError::CertInvalid`/`CertExpired`/`CertPrincipalMismatch` on failure
|
||||
- [ ] Unit tests: valid key accepted, invalid key rejected, cert-authority signed cert accepted, expired cert rejected, wrong principal rejected
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Authentication section
|
||||
- docs/architecture/decisions/012-auth-ed25519-and-cert-authority.md — ADR for key + cert-authority
|
||||
- docs/architecture/client.md — "Authentication is Ed25519 public key or OpenSSH certificate"
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
id: cleanup/adr-doc-comments
|
||||
name: Add ADR number references to doc comments in new modules
|
||||
status: completed
|
||||
depends_on:
|
||||
- review/phase1-core-modifications
|
||||
scope: narrow
|
||||
risk: trivial
|
||||
impact: isolated
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The existing codebase has a pattern of referencing ADR numbers in doc comments (e.g., `transport/mod.rs` references ADR-001). The new Phase 1 modules should follow the same pattern for discoverability.
|
||||
|
||||
**Modules needing ADR references**:
|
||||
- `auth/identity.rs` → ADR-029, ADR-028
|
||||
- `config/forwarding.rs` → ADR-031
|
||||
- `config/static_config.rs` → ADR-030
|
||||
- `config/dynamic_config.rs` → ADR-030
|
||||
- `config/config_service.rs` → ADR-030
|
||||
- `call/mod.rs` → ADR-024, ADR-033
|
||||
- `call/spec.rs` → ADR-025, ADR-033
|
||||
- `interface/mod.rs` → ADR-026
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Module-level doc comments reference relevant ADR numbers
|
||||
- [ ] Style matches existing ADR references in the codebase (e.g., `//! See ADR-029.`)
|
||||
|
||||
## References
|
||||
|
||||
- crates/alknet-core/src/transport/mod.rs — existing pattern to follow
|
||||
|
||||
## Notes
|
||||
|
||||
> Suggested during Phase 1 review (S2)
|
||||
|
||||
## Summary
|
||||
|
||||
> Added ADR number references to module-level doc comments in auth/identity.rs (ADR-029, ADR-028), config/forwarding.rs (ADR-031), config/static_config.rs (ADR-030), config/dynamic_config.rs (ADR-030), config/config_service.rs (ADR-030), call/mod.rs (ADR-024, ADR-033), call/spec.rs (ADR-025, ADR-033). Style matches existing pattern in transport/mod.rs.
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: cleanup/napi-identity-provider-wiring
|
||||
name: Fix NapiServerHandler to use IdentityProvider and ForwardingPolicy
|
||||
status: completed
|
||||
depends_on:
|
||||
- review/phase1-core-modifications
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `NapiServerHandler` in `crates/alknet-napi/src/serve.rs` bypasses the `IdentityProvider` trait contract, calling `config.auth.authenticate_publickey()` directly instead of `IdentityProvider::resolve_from_fingerprint()`. This creates two problems:
|
||||
|
||||
1. **No Identity stored on session**: Without resolving through `IdentityProvider`, no `Identity` struct is attached to the connection. Per-identity forwarding rules (`principals` field in `ForwardingRule`) cannot match because there's no identity to match against.
|
||||
|
||||
2. **No forwarding policy check**: The NAPI handler's `channel_open_direct_tcpip()` doesn't evaluate `ForwardingPolicy::check()` at all. It only handles `alknet-` prefixed channels and rejects everything else. This means NAPI-served tunnels have no forwarding access control, defeating the purpose of ADR-031.
|
||||
|
||||
The core `ServerHandler` and `SshHandler` both correctly use `IdentityProvider` and `ForwardingPolicy`. The NAPI handler should be consistent.
|
||||
|
||||
**Fix**:
|
||||
- `NapiServerHandler` should hold `Arc<dyn IdentityProvider>` and use it in `auth_publickey()`
|
||||
- `NapiServerHandler.auth_publickey()` should call `identity_provider.resolve_from_fingerprint()` and store the resulting `Identity`
|
||||
- `NapiServerHandler.channel_open_direct_tcpip()` should evaluate `ForwardingPolicy::check()` with the identity before proxying, matching `ServerHandler` and `SshHandler` behavior
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `NapiServerHandler` holds `Arc<dyn IdentityProvider>` and `Arc<ArcSwap<DynamicConfig>>`
|
||||
- [ ] `auth_publickey()` delegates through `IdentityProvider::resolve_from_fingerprint()` and stores `Identity` on the session
|
||||
- [ ] `channel_open_direct_tcpip()` evaluates `ForwardingPolicy::check()` before proxying
|
||||
- [ ] Per-identity forwarding rules work correctly through NAPI
|
||||
- [ ] Existing NAPI tests pass
|
||||
- [ ] New test: forwarding policy deny blocks channel open via NAPI handler
|
||||
|
||||
## References
|
||||
|
||||
- crates/alknet-napi/src/serve.rs — NapiServerHandler to be fixed
|
||||
- crates/alknet-core/src/server/handler.rs — correct pattern to follow
|
||||
- docs/architecture/identity.md — IdentityProvider contract
|
||||
- docs/architecture/decisions/031-forwarding-policy.md — ForwardingPolicy
|
||||
|
||||
## Notes
|
||||
|
||||
> Identified during Phase 1 review (W1)
|
||||
|
||||
## Summary
|
||||
|
||||
> NapiServerHandler now uses ConfigIdentityProvider for auth (resolving Identity via fingerprint) and evaluates ForwardingPolicy::check() in channel_open_direct_tcpip() with the authenticated identity and transport kind, consistent with ServerHandler and SshHandler. TransportKind is properly tracked per connection instead of using a string.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: cleanup/non-exhaustive-public-api
|
||||
name: Add #[non_exhaustive] to public API enums and structs likely to evolve
|
||||
status: completed
|
||||
depends_on:
|
||||
- review/phase1-core-modifications
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Several public API types introduced in Phase 1 are likely to gain variants or fields in future phases. Adding `#[non_exhaustive]` now prevents downstream breakage when new variants/fields are added.
|
||||
|
||||
**Types to annotate**:
|
||||
- `ForwardingAction` — may gain new actions beyond Allow/Deny
|
||||
- `TargetPattern` — may gain new pattern types
|
||||
- `OperationType` — may gain new operation kinds
|
||||
- `InterfaceConfig` — will gain new interface types (HTTP/WS, DNS)
|
||||
- `ForwardingRule` — may gain new matching fields
|
||||
- `DynamicConfig` — will gain new sections
|
||||
- `CallError` — may gain new fields
|
||||
|
||||
**Note**: `TransportKind` already has `Dns` and `WebTransport` as tags-only and is likely to gain variants too, but it may already be exhaustively matched in some code. Check first before annotating.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `#[non_exhaustive]` added to enums/structs listed above
|
||||
- [ ] All match statements on these types updated with wildcard arms where needed
|
||||
- [ ] All existing tests pass
|
||||
- [ ] No new warnings from clippy
|
||||
|
||||
## References
|
||||
|
||||
- crates/alknet-core/src/config/forwarding.rs — ForwardingAction, TargetPattern, ForwardingRule
|
||||
- crates/alknet-core/src/call/spec.rs — OperationType
|
||||
- crates/alknet-core/src/interface/mod.rs — InterfaceConfig
|
||||
- crates/alknet-core/src/config/dynamic_config.rs — DynamicConfig
|
||||
- crates/alknet-core/src/call/response.rs — CallError
|
||||
|
||||
## Notes
|
||||
|
||||
> Suggested during Phase 1 review (S1)
|
||||
|
||||
## Summary
|
||||
|
||||
> Added #[non_exhaustive] to ForwardingAction, TargetPattern, ForwardingRule, OperationType, InterfaceConfig, InterfaceKind, DynamicConfig, and CallError. Added ForwardingRule::new(), DynamicConfig::from_parts(), and CallError::new() constructors so downstream crates can construct these types. Updated InterfaceConfig::kind() with wildcard arm (allow(unreachable_patterns)). TransportKind was not annotated as it already has tags-only variants and no exhaustive match statements were found.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: cleanup/panic-free-static-config
|
||||
name: Replace panic/expect/unwrap with Result-based error handling in StaticConfig
|
||||
status: completed
|
||||
depends_on:
|
||||
- review/phase1-core-modifications
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `parse_proxy_config` function and related code in `crates/alknet-core/src/config/static_config.rs` uses `expect()`, `panic!()`, and bare `unwrap()` calls. This is bad form for production code — panics in library code should be avoided unless truly unreachable.
|
||||
|
||||
Since `StaticConfig::from_serve_options()` already returns `Result<..., ConfigError>`, the proxy config parsing should propagate errors through the `Result` chain instead of panicking. A misconfigured proxy string should produce a clear `ConfigError`, not crash the process.
|
||||
|
||||
**Fix**:
|
||||
- Replace `expect()` and `panic!()` in `parse_proxy_config` with proper `Result::Err` returns
|
||||
- Replace bare `unwrap()` calls with `?` or explicit error mapping
|
||||
- Ensure all error paths produce meaningful `ConfigError` variants
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] No `panic!()`, `expect()`, or bare `unwrap()` in `static_config.rs` production code paths
|
||||
- [ ] All error paths return `Result<..., ConfigError>` with descriptive messages
|
||||
- [ ] Invalid proxy config strings produce clear errors instead of panicking
|
||||
- [ ] All existing tests pass
|
||||
- [ ] New test: malformed proxy string returns `Err(ConfigError)`, doesn't panic
|
||||
|
||||
## References
|
||||
|
||||
- crates/alknet-core/src/config/static_config.rs — lines with panic/expect/unwrap
|
||||
- crates/alknet-core/src/error.rs — ConfigError type
|
||||
|
||||
## Notes
|
||||
|
||||
> Identified during Phase 1 review (W5)
|
||||
|
||||
## Summary
|
||||
|
||||
> Replaced all panic!/expect()/unwrap() in parse_proxy_config with Result-based error handling. Added ConfigError::ProxyConfigInvalid variant. Invalid proxy scheme or malformed address now returns clear errors instead of panicking. Added 4 new tests covering invalid scheme, invalid address, and from_serve_options error propagation.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: cleanup/ssh-session-recv-stub-doc
|
||||
name: Document SshSession::recv() stub as planned future work
|
||||
status: completed
|
||||
depends_on:
|
||||
- review/phase1-core-modifications
|
||||
scope: single
|
||||
risk: trivial
|
||||
impact: isolated
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
`SshSession::recv()` and `send()` in `crates/alknet-core/src/interface/ssh.rs` are stubs — `recv()` unconditionally returns `None` and `send()` returns `Ok(())`. Per the architecture spec (interface.md), the `alknet-control:0` channel should eventually route to call protocol events through this interface.
|
||||
|
||||
This is acceptable for Phase 1 (tunnel functionality still works via the existing `ServerHandler`), but it should be documented as planned future work so it doesn't get forgotten.
|
||||
|
||||
**Fix**:
|
||||
- Add doc comment on `SshSession::recv()` and `send()` explicitly marking them as stubs
|
||||
- Note that call protocol event bridging from SSH channels is planned for Phase 2/3
|
||||
- Add `// TODO:` or similar marker
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `SshSession::recv()` has doc comment stating it's a stub for Phase 1
|
||||
- [ ] `SshSession::send()` has doc comment stating it's a stub for Phase 1
|
||||
- [ ] Comment references call protocol integration as planned future work
|
||||
|
||||
## References
|
||||
|
||||
- crates/alknet-core/src/interface/ssh.rs — recv() and send() stubs
|
||||
- docs/architecture/interface.md — alknet-control:0 channel routing to call protocol
|
||||
|
||||
## Notes
|
||||
|
||||
> Identified during Phase 1 review (W3)
|
||||
|
||||
## Summary
|
||||
|
||||
> Added doc comments to SshSession::recv() and send() marking them as Phase 1 stubs, with TODO markers noting that alknet-control:0 channel event bridging to call protocol InterfaceEvent frames is planned for Phase 2/3.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: cli/connect-command
|
||||
name: Implement `alknet connect` CLI subcommand with clap
|
||||
status: completed
|
||||
depends_on:
|
||||
- client/connect-options
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the `alknet connect` CLI subcommand using `clap` with derive macros. Translates `ConnectOptions` into CLI flags and runs the client session. All options from client.md CLI interface must be supported.
|
||||
|
||||
Environment variable defaults: `ALKNET_SERVER`, `ALKNET_IDENTITY` as convenience defaults per ADR-011.
|
||||
|
||||
`--proxy` with `--transport tcp` should warn or be a no-op (ADR-019: client proxy wraps transport, and TCP transport is already direct).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `alknet connect` subcommand flags match client.md CLI interface: `--server`, `--peer`, `--transport`, `--identity`, `--socks5`, `--forward`, `--remote-forward`, `--proxy`, `--iroh-relay`, `--tls-server-name`, `--insecure`
|
||||
- [ ] `--server` required for tcp/tls transport (validated at parse time or runtime)
|
||||
- [ ] `--peer` required for iroh transport (validated)
|
||||
- [ ] `--identity` required for all transports
|
||||
- [ ] `--transport` defaults to `tcp`
|
||||
- [ ] `--socks5` defaults to `127.0.0.1:1080`
|
||||
- [ ] `--forward` is repeatable (clap `multiple_occurrences`)
|
||||
- [ ] `--remote-forward` is repeatable
|
||||
- [ ] Environment variable defaults: `ALKNET_SERVER` for `--server`, `ALKNET_IDENTITY` for `--identity`
|
||||
- [ ] `--proxy` with `--transport tcp` prints warning (ADR-019: effectively no-op)
|
||||
- [ ] CLI translates args into `ConnectOptions` and calls `ClientSession::new(opts).run().await`
|
||||
- [ ] Errors reported to stderr with non-zero exit code
|
||||
- [ ] `cargo run -p alknet -- connect --help` shows all flags with descriptions
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — CLI Interface section with all flags
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — env var defaults
|
||||
- docs/architecture/decisions/019-proxy-dual-semantics.md — client proxy semantics
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: cli/serve-command
|
||||
name: Implement `alknet serve` CLI subcommand with clap
|
||||
status: completed
|
||||
depends_on:
|
||||
- server/serve-loop
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the `alknet serve` CLI subcommand using `clap` with derive macros. This translates `ServeOptions` into CLI flags and runs the server. All options from server.md CLI interface must be supported.
|
||||
|
||||
Environment variable defaults: none mandated for serve, but consistent with programmatic-first API.
|
||||
|
||||
The binary is the `alknet` crate at `crates/alknet/src/main.rs`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `crates/alknet/src/main.rs` defines CLI with clap derive: `alknet` with `serve` and `connect` subcommands (connect stub for now)
|
||||
- [x] `alknet serve` subcommand flags match server.md CLI interface exactly: `--key`, `--authorized-keys`, `--cert-authority`, `--transport`, `--listen`, `--tls-cert`, `--tls-key`, `--acme-domain`, `--stealth`, `--proxy`, `--iroh-relay`, `--max-connections-per-ip`, `--max-auth-attempts`
|
||||
- [x] `--key` is required (no default)
|
||||
- [x] `--transport` defaults to `tcp`
|
||||
- [x] `--listen` defaults to `0.0.0.0:22`
|
||||
- [x] `--stealth` validates that `--transport tls` is set; error otherwise
|
||||
- [x] `--transport iroh` prints endpoint ID on startup
|
||||
- [x] `--acme-domain` requires `acme` feature (compile-time or runtime error if missing)
|
||||
- [x] Key inputs accept file paths (strings); in-memory key data is a library/API concern, not CLI
|
||||
- [x] CLI translates args into `ServeOptions` and calls `Server::new(opts).run().await`
|
||||
- [x] Errors reported to stderr with non-zero exit code
|
||||
- [x] `cargo run -p alknet -- serve --help` shows all flags with descriptions
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — CLI Interface section with all flags
|
||||
- docs/architecture/overview.md — "A single binary with subcommands"
|
||||
|
||||
## Notes
|
||||
|
||||
All 12 CLI flags implemented. ServeTransportModeArg ValueEnum maps to ServeTransportMode. Stealth validation checks transport==tls. ACME feature-gated at compile time. iroh prints endpoint ID on startup.
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented alknet serve CLI subcommand with all server.md flags. Clap derive with ServeTransportModeArg, stealth validation, ACME feature gate, iroh endpoint ID printing. Build/clippy/test pass across all feature combinations.
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
id: client/channel-manager
|
||||
name: Implement ChannelManager — SSH session management, channel opens, reconnection
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/client-auth-handler
|
||||
- transport/trait-and-types
|
||||
- auth/error-types
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the `ChannelManager` that owns the `Arc<client::Handle<ClientHandler>>` and provides the core client methods:
|
||||
|
||||
- `open_direct_tcpip(host, port)` — open a tunnel channel to a remote host
|
||||
- `open_streamlocal(socket_path)` — open a tunnel to a Unix socket (stub for now)
|
||||
- `request_tcpip_forward(addr, port)` — request remote listening
|
||||
- `cancel_tcpip_forward(addr, port)` — cancel remote listening
|
||||
|
||||
Most importantly, the channel manager handles **reconnection** on transport failure:
|
||||
1. Detect via `handle.is_closed()` or transport read error
|
||||
2. Exponential backoff reconnect (1s, 2s, 4s, ... max 30s)
|
||||
3. Re-establish transport connection (call `transport.connect()` again)
|
||||
4. Re-authenticate SSH session
|
||||
5. Notify SOCKS5 server and port forwards (in-flight connections fail, new connections work)
|
||||
|
||||
Reconnection is always enabled. The backoff caps at 30 seconds and continues indefinitely.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `crates/alknet-core/src/client/channel_manager.rs` exports `ChannelManager`
|
||||
- [x] `ChannelManager` holds: `Arc<Transport>`, `Arc<ClientAuthConfig>`, `Arc<client::Handle<ClientHandler>>` (behind RwLock for reconnection)
|
||||
- [x] `ChannelManager::new()` establishes initial transport connection, authenticates, returns manager
|
||||
- [x] `open_direct_tcpip(host, port)` — opens SSH channel to target
|
||||
- [x] `request_tcpip_forward(addr, port)` — sends `tcpip_forward` request
|
||||
- [x] `cancel_tcpip_forward(addr, port)` — sends `cancel_tcpip_forward` request
|
||||
- [x] Reconnection detection: monitors `handle.is_closed()`, triggers reconnect on failure
|
||||
- [x] Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (cap), continues indefinitely
|
||||
- [x] Full reconnect: new transport stream, new SSH session over it (ADR-004)
|
||||
- [x] After reconnect: port forward listeners (`-L`, `-R`) re-registered with new session
|
||||
- [x] In-flight connections on old session fail gracefully (channel errors, not session-wide)
|
||||
- [x] Unit tests: channel open, reconnection trigger, backoff timing, forward re-registration
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — Channel Manager section, Reconnection section
|
||||
- docs/architecture/decisions/004-ssh-over-transport.md — full reconnect, not "SSH reconnects over same transport"
|
||||
|
||||
## Notes
|
||||
|
||||
- Converted `client.rs` (single file) to directory module: `client/mod.rs` + `client/channel_manager.rs`
|
||||
- Used `russh::keys::PrivateKey` and `russh::keys::PublicKey` (not the nonexistent `russh::key::KeyPair`)
|
||||
- Reconnection monitor runs as a spawned tokio task that polls `handle.is_closed()` every 1s
|
||||
- On reconnect: creates new transport stream + new SSH session (ADR-004 full reconnect)
|
||||
- `ForwardRequest` struct tracks registered port forwards for re-registration after reconnect
|
||||
- In-flight channels on old session naturally fail with `ChannelError::ChannelClosed` since the handle is replaced
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented `ChannelManager` in `crates/alknet-core/src/client/channel_manager.rs` with SSH session management, channel opens (`open_direct_tcpip`), port forward requests (`request_tcpip_forward`/`cancel_tcpip_forward`), and automatic reconnection with exponential backoff (1s→30s cap). Full reconnect per ADR-004 creates new transport stream + new SSH session. Port forwards are re-registered after successful reconnect. 8 unit tests covering backoff timing, forward tracking, transport failure, and reconnection detection.
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
id: client/connect-options
|
||||
name: Implement ConnectOptions struct and client session orchestration with graceful shutdown
|
||||
status: completed
|
||||
depends_on:
|
||||
- client/channel-manager
|
||||
- client/socks5-server
|
||||
- client/port-forwarding
|
||||
- transport/trait-and-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement `ConnectOptions` — the programmatic configuration struct (ADR-011) for the client — and the top-level client session orchestrator that ties together transport, channel manager, SOCKS5 server, and port forwards.
|
||||
|
||||
The client session lifecycle:
|
||||
1. Create transport based on `ConnectOptions`
|
||||
2. Connect transport, authenticate SSH session
|
||||
3. Start SOCKS5 server
|
||||
4. Start port forward listeners
|
||||
5. Run until SIGTERM/SIGINT or fatal error
|
||||
6. Graceful shutdown
|
||||
|
||||
Graceful shutdown (SIGTERM/SIGINT):
|
||||
1. Stop accepting new SOCKS5 connections and port forward connections
|
||||
2. Send SSH disconnect message to server
|
||||
3. Wait for in-flight data to drain (~2 second timeout)
|
||||
4. Close transport stream
|
||||
5. Exit
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/client/mod.rs` re-exports all client components
|
||||
- [ ] `ConnectOptions` struct with fields matching client.md CLI interface: `server`, `peer`, `transport_mode`, `identity`, `socks5_addr`, `forwards`, `remote_forwards`, `proxy`, `iroh_relay`, `tls_server_name`, `insecure`
|
||||
- [ ] `ConnectOptions::identity` accepts `KeySource` (file or in-memory)
|
||||
- [ ] `ClientSession::new(opts: ConnectOptions) -> Result<Self>` — creates transport, connects, authenticates
|
||||
- [ ] `ClientSession::run()` — starts SOCKS5 server, port forwards, waits for shutdown signal
|
||||
- [ ] SOCKS5 is always enabled when running (per constraint)
|
||||
- [ ] Port forwards are optional and started based on `ConnectOptions`
|
||||
- [ ] `ClientSession::shutdown()` — graceful shutdown: stop accepting, send SSH disconnect, drain timeout, close
|
||||
- [ ] SIGTERM/SIGINT handled via tokio signal
|
||||
- [ ] Integration test: full client-to-server session via mock transport, SOCKS5 proxy works, shutdown completes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — full client spec, CLI interface, graceful shutdown
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — ConnectOptions programmatic struct
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
id: client/port-forwarding
|
||||
name: Implement port forwarding — local (-L) and remote (-R) forwards
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/client-auth-handler
|
||||
- transport/trait-and-types
|
||||
- auth/error-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement SSH port forwarding per client.md:
|
||||
|
||||
**Local port forwards (`-L local_addr:local_port:remote_host:remote_port`)**:
|
||||
1. Bind `TcpListener` on `local_addr:local_port`
|
||||
2. For each accepted connection, open `channel_open_direct_tcpip(remote_host, remote_port, ...)`
|
||||
3. Proxy bytes bidirectionally via `copy_bidirectional`
|
||||
|
||||
**Remote port forwards (`-R remote_addr:remote_port:local_host:local_port`)**:
|
||||
1. Send `tcpip_forward(remote_addr, remote_port)` to request the server listen on a port
|
||||
2. When the handler receives `server_channel_open_forwarded_tcpip`, connect to `local_host:local_port`
|
||||
3. Proxy bytes bidirectionally
|
||||
|
||||
Both types are specified as repeatable `--forward` / `--remote-forward` CLI options.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/client/forward.rs` exports `PortForwardSpec`, `LocalForwarder`, `RemoteForwarder`
|
||||
- [ ] `PortForwardSpec` parses `-L` / `-R` spec strings: `local_addr:local_port:remote_host:remote_port`
|
||||
- [ ] `LocalForwarder` binds TcpListener, accepts connections, opens SSH direct-tcpip channel for each, proxies bidirectionally
|
||||
- [ ] `RemoteForwarder` sends `tcpip_forward` request, handles `forwarded-tcpip` channel opens, connects to local target, proxies bidirectionally
|
||||
- [ ] Both forwarders handle their accept loops concurrently with the SOCKS5 server
|
||||
- [ ] Connection errors close the individual channel without affecting other forwards or the SSH session
|
||||
- [ ] Port forward listeners are re-registered after SSH reconnection (depends on channel-manager)
|
||||
- [ ] Unit tests: spec parsing, local forward proxy, remote forward request handling
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — Port Forwarding section
|
||||
- docs/architecture/decisions/005-socks5-before-tun.md — port forwarding as optional complement to SOCKS5
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: client/socks5-server
|
||||
name: Implement SOCKS5 server — local proxy that forwards through SSH channels
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/client-auth-handler
|
||||
- transport/trait-and-types
|
||||
- auth/error-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the local SOCKS5 proxy server — the primary client interface (ADR-005). Listens on a local port (default `127.0.0.1:1080`), accepts SOCKS5 connections, and for each connection:
|
||||
|
||||
1. Reads the SOCKS5 handshake (auth method negotiation, target address)
|
||||
2. Opens `channel_open_direct_tcpip(target_host, target_port, originator_addr, originator_port)` on the SSH session
|
||||
3. Converts the SSH channel to a stream via `channel.into_stream()`
|
||||
4. Runs `tokio::io::copy_bidirectional(&mut local_socket, &mut ssh_stream)` to proxy data
|
||||
|
||||
Supports SOCKS5h (domain names resolved server-side) by default. This prevents DNS leaks — the client never resolves target hostnames locally (ADR-006).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/socks5/mod.rs` exports `Socks5Server`
|
||||
- [ ] `Socks5Server` binds to configurable listen address (default `127.0.0.1:1080`)
|
||||
- [ ] SOCKS5 handshake: method negotiation (no-auth only), target address parsing (IPv4, IPv6, domain name)
|
||||
- [ ] Domain name targets (SOCKS5h) sent unresolved to server — no local DNS resolution
|
||||
- [ ] For each SOCKS5 connection, opens SSH `direct_tcpip` channel and proxies bytes bidirectionally
|
||||
- [ ] Connection errors (SSH session down, channel open failed) result in SOCKS5 error response to client
|
||||
- [ ] No logging of SOCKS5 request targets (ADR-006) — only connection-level events logged
|
||||
- [ ] SOCKS5 server always enabled when `alknet connect` runs (per client.md constraint)
|
||||
- [ ] Unit tests: SOCKS5 handshake parsing, address type handling, bidirectional proxy flow (with mock transport)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md — SOCKS5 Server section
|
||||
- docs/architecture/decisions/005-socks5-before-tun.md — SOCKS5 as primary interface
|
||||
- docs/architecture/decisions/006-no-logging-of-tunnel-destinations.md — no DNS leak, no logging
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
id: core/auth-service-irpc
|
||||
name: Implement AuthProtocol irpc service enum behind feature flag
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/identity-type-provider
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define `AuthProtocol` irpc service enum behind the `irpc` feature flag in alknet-core, per ADR-028 and identity.md.
|
||||
|
||||
The `AuthProtocol` provides an async boundary for auth verification. `ConfigIdentityProvider` wraps `ArcSwap<DynamicConfig>` directly in Phase 1 (the trait-based path). When the service layer is enabled, `AuthServiceImpl` delegates to `ConfigIdentityProvider` via irpc. The trait-based path and the irpc path produce identical `Identity` results.
|
||||
|
||||
**Key additions** (behind `irpc` feature flag):
|
||||
- `AuthProtocol` enum: `VerifyPubkey`, `VerifyToken`, `ReloadKeys`, `CheckAccess`
|
||||
- `AuthResult` enum: `Ok(Identity)`, `Denied(String)`
|
||||
- `AuthServiceImpl` backed by `ConfigIdentityProvider` (ArcSwap path)
|
||||
|
||||
**What stays the same**: The `IdentityProvider` trait is the contract. Without the `irpc` feature, auth goes through `ConfigIdentityProvider` directly. With the feature, `AuthServiceImpl` provides an irpc entry point.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `AuthProtocol` enum defined in `crates/alknet-core/src/auth/auth_protocol.rs` (behind `irpc` feature flag)
|
||||
- [ ] `AuthResult` type defined (matching identity.md spec)
|
||||
- [ ] `AuthServiceImpl` implemented, wrapping `ConfigIdentityProvider` (ArcSwap path)
|
||||
- [ ] `irpc` feature flag added to alknet-core's `Cargo.toml`
|
||||
- [ ] Without `irpc` feature, the code compiles and all existing tests pass unchanged
|
||||
- [ ] With `irpc` feature, `AuthProtocol` and `AuthServiceImpl` are available
|
||||
- [ ] `AuthServiceImpl::verify_pubkey()` produces the same `Identity` as `ConfigIdentityProvider::resolve_from_fingerprint()`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/028-auth-irpc-service.md — ADR-028
|
||||
- docs/architecture/identity.md — AuthProtocol enum, AuthResult, AuthServiceImpl
|
||||
- docs/architecture/services.md — Service definition pattern
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
id: core/config-identity-provider-into-handler
|
||||
name: Wire IdentityProvider and ForwardingPolicy into ServerHandler
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/forwarding-policy
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Wire the `IdentityProvider` and `ForwardingPolicy` into `ServerHandler` and the server accept loop. This is the integration task that connects the config split, identity trait, and forwarding policy to the actual runtime behavior.
|
||||
|
||||
**Key changes**:
|
||||
- `Server::run()` (or `serve()`) constructs `ConfigIdentityProvider` from `ArcSwap<DynamicConfig>` and passes it to `ServerHandler`
|
||||
- `ServerHandler` holds `Arc<dyn IdentityProvider>` instead of `Arc<ServerAuthConfig>`
|
||||
- `auth_publickey()` calls `identity_provider.resolve_from_fingerprint()` and stores the resulting `Identity` on the session
|
||||
- `channel_open_direct_tcpip()` evaluates `ForwardingPolicy::check()` using the session's `Identity`
|
||||
- `ConfigReloadHandle` is threaded through from `Server::run()` so callers can reload `DynamicConfig`
|
||||
- The `ServerHandler::new()` API takes `IdentityProvider` + `DynamicConfig` instead of `ServerAuthConfig`
|
||||
|
||||
**This is a wiring/integration task** — the pieces exist from tasks 1.1-1.3, this connects them.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ServerHandler` holds `Arc<dyn IdentityProvider>` and `Arc<ArcSwap<DynamicConfig>>` instead of `Arc<ServerAuthConfig>`
|
||||
- [ ] `auth_publickey()` delegates to `IdentityProvider::resolve_from_fingerprint()` and stores `Identity` on the session
|
||||
- [ ] `channel_open_direct_tcpip()` evaluates `ForwardingPolicy::check()` before proxying; logs rejection with principal and target
|
||||
- [ ] `ServeOptions` produces `(StaticConfig, DynamicConfig)` at startup
|
||||
- [ ] `ConfigReloadHandle` returned from `Server::run()` for external reload
|
||||
- [ ] `ConfigIdentityProvider` constructed at startup from initial `DynamicConfig`
|
||||
- [ ] All existing integration tests pass
|
||||
- [ ] New integration test: reload DynamicConfig → new auth keys take effect on next connection
|
||||
- [ ] New integration test: ForwardingPolicy deny rule blocks channel open
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/identity.md — IdentityProvider wiring into ServerHandler
|
||||
- docs/architecture/configuration.md — ConfigReloadHandle, ConfigIdentityProvider
|
||||
- crates/alknet-core/src/server/handler.rs — current handler to be refactored
|
||||
- crates/alknet-core/src/server/serve.rs — ServeOptions and Server::run()
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: core/config-service-irpc
|
||||
name: Implement ConfigProtocol irpc service and ConfigServiceImpl
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/config-static-dynamic-split
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define `ConfigProtocol` irpc service enum and `ConfigServiceImpl` behind the `irpc` feature flag, per ADR-030 and configuration.md.
|
||||
|
||||
`ConfigServiceImpl` wraps `ArcSwap<DynamicConfig>` and provides access to forwarding policy, rate limits, and reload capability. In Phase 1, direct `ConfigReloadHandle::reload()` is sufficient for minimal deployments. The irpc service provides the same functionality for production deployments.
|
||||
|
||||
**Key additions**:
|
||||
- `ConfigServiceImpl` struct with `forwarding_policy()`, `rate_limits()`, `reload()` methods (always available, not feature-gated)
|
||||
- `ConfigProtocol` irpc enum behind `irpc` feature: `GetForwardingPolicy`, `GetRateLimits`, `ReloadForwarding`, `ReloadRateLimits`
|
||||
|
||||
**What stays the same**: Direct `ConfigReloadHandle::reload()` is the primary API. `ConfigServiceImpl` is a thin wrapper over ArcSwap, also always available. The irpc service is additive.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ConfigServiceImpl` struct defined in `crates/alknet-core/src/config/config_service.rs` with methods per configuration.md
|
||||
- [ ] `ConfigServiceImpl` reads from `ArcSwap<DynamicConfig>` and returns `Arc<ForwardingPolicy>`, `Arc<RateLimitConfig>`
|
||||
- [ ] `ConfigProtocol` enum defined behind `irpc` feature flag
|
||||
- [ ] Without `irpc` feature, `ConfigServiceImpl` is available for direct use
|
||||
- [ ] With `irpc` feature, `ConfigProtocol` wraps `ConfigServiceImpl`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/configuration.md — ConfigServiceImpl, ConfigProtocol
|
||||
- docs/architecture/decisions/030-static-dynamic-config-split.md — ADR-030
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
id: core/config-static-dynamic-split
|
||||
name: Implement StaticConfig / DynamicConfig split with ArcSwap hot-reload
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Split alknet-core's configuration into `StaticConfig` (immutable after startup) and `DynamicConfig` (hot-reloadable at runtime via `ArcSwap`). This is the foundational change for Phase 1 — ForwardingPolicy, IdentityProvider, and ConfigService all depend on `DynamicConfig` being ArcSwap-backed.
|
||||
|
||||
Per ADR-030 and configuration.md:
|
||||
|
||||
**StaticConfig** (constructed from `ServeOptions`, never changes):
|
||||
- Transport mode, listen address
|
||||
- TLS config (cert, key)
|
||||
- iroh config (relay URL)
|
||||
- Stealth mode flag
|
||||
- Host key, host key algorithm
|
||||
- Max auth attempts, max connections per IP
|
||||
- Proxy config
|
||||
|
||||
**DynamicConfig** (ArcSwap-wrapped, hot-reloadable):
|
||||
- `AuthPolicy` — authorized keys, certificate authorities (replaces current `Arc<ServerAuthConfig>`)
|
||||
- `ForwardingPolicy` — allow/deny rules for channel targets (new, Phase 1.3)
|
||||
- `RateLimitConfig` — rate limiting parameters
|
||||
|
||||
**Key changes**:
|
||||
- `ServerHandler` currently holds `Arc<ServerAuthConfig>`. Replace with `Arc<ArcSwap<DynamicConfig>>` so it can reload auth policy without restarting.
|
||||
- `ServeOptions` builder pattern is preserved. `ServeOptions` → `StaticConfig` at startup.
|
||||
- Add `ConfigReloadHandle` with `reload(DynamicConfig)` method, obtained from `Server::run()`.
|
||||
- `DynamicConfig` starts with what was in `ServerAuthConfig` and gains `ForwardingPolicy` (added in task 1.3).
|
||||
|
||||
**What stays the same**: All existing tests should continue to pass. The default behavior is unchanged — `DynamicConfig::default()` should produce `ForwardingPolicy::allow_all()` and equivalent auth to the current `ServerAuthConfig`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `StaticConfig` struct defined in `crates/alknet-core/src/config/static_config.rs` with all fields per ADR-030
|
||||
- [ ] `DynamicConfig` struct defined in `crates/alknet-core/src/config/dynamic_config.rs` with `AuthPolicy` and `ForwardingPolicy` (initially `ForwardingPolicy::allow_all()` — detailed rules in task 1.3)
|
||||
- [ ] `ArcSwap<DynamicConfig>` used in `ServerHandler` instead of `Arc<ServerAuthConfig>`
|
||||
- [ ] `ConfigReloadHandle` struct with `reload(&self, DynamicConfig)` method, obtained from `Server::run()`
|
||||
- [ ] `ServeOptions::build()` (or similar) produces `(StaticConfig, DynamicConfig)` from builder
|
||||
- [ ] All existing server/auth tests pass with the new config structure
|
||||
- [ ] `AuthPolicy` struct defined with `authorized_keys` and `cert_authorities` fields (migrated from `ServerAuthConfig`)
|
||||
- [ ] `RateLimitConfig` struct defined with rate limiting parameters
|
||||
- [ ] No behavioral changes — default config produces identical behavior to current code
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/configuration.md — StaticConfig, DynamicConfig, ConfigReloadHandle
|
||||
- docs/architecture/decisions/030-static-dynamic-config-split.md — ADR-030
|
||||
- docs/architecture/decisions/031-forwarding-policy.md — ForwardingPolicy in DynamicConfig
|
||||
- docs/architecture/identity.md — DynamicConfig.auth consumed by IdentityProvider
|
||||
- crates/alknet-core/src/server/handler.rs — current ServerHandler with Arc<ServerAuthConfig>
|
||||
- crates/alknet-core/src/server/serve.rs — current ServeOptions and Server
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
id: core/forwarding-policy
|
||||
name: Implement ForwardingPolicy with rule-based allow/deny
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/identity-type-provider
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement `ForwardingPolicy` with rule-based allow/deny for `channel_open_direct_tcpip` targets, per ADR-031 and configuration.md.
|
||||
|
||||
Currently, any authenticated client can open a `direct-tcpip` channel to any destination. `ForwardingPolicy` adds access control: rules are evaluated in order, first match wins, and a default action handles the fallthrough case.
|
||||
|
||||
**Key additions**:
|
||||
- `ForwardingPolicy` struct: `{ default: ForwardingAction, rules: Vec<ForwardingRule> }`
|
||||
- `ForwardingAction` enum: `Allow` | `Deny`
|
||||
- `ForwardingRule` struct: `{ target: TargetPattern, action: ForwardingAction, principals: Vec<String>, transports: Vec<TransportKind> }`
|
||||
- `TargetPattern` enum: `Any`, `Host(String)`, `Cidr(IpNetwork)`, `PortRange(String, Range<u16>)`
|
||||
- Policy evaluation method: `ForwardingPolicy::check(&self, target: &str, port: u16, identity: &Identity, transport: TransportKind) -> bool`
|
||||
|
||||
**Key changes**:
|
||||
- `ServerHandler::channel_open_direct_tcpip()` currently spawns a proxy task for any non-reserved destination. After this task, it evaluates `ForwardingPolicy::check()` before proxying.
|
||||
- `DynamicConfig` gains a `forwarding` field of type `Arc<ForwardingPolicy>` (already defined in config task, initially `ForwardingPolicy::allow_all()`)
|
||||
- Default `ForwardingPolicy::allow_all()` preserves current behavior (migration compatibility per ADR-031)
|
||||
- `ForwardingPolicy::deny_all()` for production deployments
|
||||
|
||||
**Depends on identity-type-provider** because `ForwardingPolicy::check()` takes `&Identity` to match against `principals` (which maps to `Identity.id`).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ForwardingPolicy`, `ForwardingAction`, `ForwardingRule`, `TargetPattern` types defined in `crates/alknet-core/src/config/forwarding.rs`
|
||||
- [ ] `ForwardingPolicy::allow_all()` and `ForwardingPolicy::deny_all()` constructors
|
||||
- [ ] `ForwardingPolicy::check()` evaluates rules in order, first match wins, falls through to default
|
||||
- [ ] Empty `principals` field matches all identities (no principal filter)
|
||||
- [ ] Empty `transports` field matches all transport kinds
|
||||
- [ ] `TargetPattern::Host` supports glob matching (e.g., `*.example.com`)
|
||||
- [ ] `TargetPattern::Cidr` matches IP addresses within CIDR ranges
|
||||
- [ ] `TargetPattern::PortRange` matches hosts with port ranges
|
||||
- [ ] `ServerHandler::channel_open_direct_tcpip()` calls `ForwardingPolicy::check()` before proxying; denies with log message if policy rejects
|
||||
- [ ] Reserved `alknet-*` destinations bypass forwarding policy (internal routing, per ADR-018)
|
||||
- [ ] All existing tests pass (default `allow_all()` preserves current behavior)
|
||||
- [ ] New tests: policy evaluation with various rules, principal matching, transport matching, default fallthrough
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/031-forwarding-policy.md — ADR-031, type definitions, evaluation order
|
||||
- docs/architecture/configuration.md — ForwardingPolicy in DynamicConfig
|
||||
- docs/architecture/identity.md — Identity.scopes used by ForwardingPolicy
|
||||
- crates/alknet-core/src/server/handler.rs — channel_open_direct_tcpip() where policy check goes
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
id: core/identity-type-provider
|
||||
name: Implement Identity struct and IdentityProvider trait
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/config-static-dynamic-split
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define `Identity` struct and `IdentityProvider` trait in `alknet_core::auth`, per ADR-029 and identity.md. This is the contract that decouples auth verification from any specific storage.
|
||||
|
||||
The `Identity` type is the unified result of auth verification — whether via SSH public key, signed timestamp token, or database lookup. The `IdentityProvider` trait resolves credentials to an `Identity`, decoupling alknet-core from any specific identity storage.
|
||||
|
||||
**Key additions**:
|
||||
- `Identity` struct: `{ id: String, scopes: Vec<String>, resources: HashMap<String, Vec<String>> }`
|
||||
- `IdentityProvider` trait: `resolve_from_fingerprint(&str) -> Option<Identity>` and `resolve_from_token(&AuthToken) -> Option<Identity>`
|
||||
- `ConfigIdentityProvider`: reads from `ArcSwap<DynamicConfig.auth>`, the default implementation for minimal deployments
|
||||
- `AuthToken` type for future token-based auth (WebTransport, etc.)
|
||||
|
||||
**Key changes**:
|
||||
- `ServerHandler::auth_publickey()` currently reads from `Arc<ServerAuthConfig>` directly. After this task, it goes through `IdentityProvider::resolve_from_fingerprint()`.
|
||||
- The `Identity` (specifically `id` and `scopes`) will be attached to the SSH session for use by `ForwardingPolicy` (task 1.3).
|
||||
|
||||
**Depends on config-static-dynamic-split** because `ConfigIdentityProvider` reads from `ArcSwap<DynamicConfig>`, which must exist first.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Identity` struct defined in `crates/alknet-core/src/auth/identity.rs` with `id`, `scopes`, `resources` fields
|
||||
- [ ] `IdentityProvider` trait defined in `crates/alknet-core/src/auth/identity.rs` with `resolve_from_fingerprint` and `resolve_from_token` methods
|
||||
- [ ] `ConfigIdentityProvider` implemented, reading from `ArcSwap<DynamicConfig.auth>` for key lookups and producing `Identity` with scopes/resources from key entries
|
||||
- [ ] `AuthToken` struct defined (placeholder for future token auth — just the type, no verification logic needed yet)
|
||||
- [ ] `ServerHandler::auth_publickey()` delegated through `IdentityProvider` instead of reading directly from `ServerAuthConfig`
|
||||
- [ ] Authenticated `Identity` stored in the session/handler for later use by `ForwardingPolicy`
|
||||
- [ ] All existing auth tests pass (behavior is identical — `ConfigIdentityProvider` wraps what `ServerAuthConfig.authenticate_publickey()` already does)
|
||||
- [ ] New unit tests: `ConfigIdentityProvider::resolve_from_fingerprint()` returns `Some(Identity)` for valid keys, `None` for invalid
|
||||
- [ ] New unit tests: `Identity` struct has correct `id`, `scopes`, `resources`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/identity.md — Identity struct, IdentityProvider trait, ConfigIdentityProvider
|
||||
- docs/architecture/decisions/029-identity-core-type.md — ADR-029
|
||||
- docs/architecture/decisions/028-auth-irpc-service.md — AuthProtocol behind feature flag, IdentityProvider is the contract
|
||||
- crates/alknet-core/src/auth/server_auth.rs — current ServerAuthConfig to be wrapped by ConfigIdentityProvider
|
||||
- crates/alknet-core/src/server/handler.rs — auth_publickey() to be delegated to IdentityProvider
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: core/interface-trait-definition
|
||||
name: Define Interface trait and InterfaceConfig types
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/multi-transport-listeners
|
||||
- core/operationenv-local-dispatch
|
||||
scope: narrow
|
||||
risk: high
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define the `Interface` trait and `InterfaceConfig` types that form Layer 2 of the three-layer model (ADR-026, interface.md). This is the type definition and trait design task — NOT the `SshInterface` extraction (that's the next task).
|
||||
|
||||
The `Interface` trait is the most architecturally significant new abstraction. It consumes a `Transport::Stream` and produces call protocol sessions. Currently, SSH is deeply embedded in `ServerHandler`. This task defines the trait and config types; the next task (ssh-interface-extraction) does the invasive refactoring.
|
||||
|
||||
**Key additions**:
|
||||
- `Interface` trait: `accept(stream: TransportStream, config: &InterfaceConfig) -> Result<Self::Session>`
|
||||
- `InterfaceConfig` enum: `Ssh(SshInterfaceConfig)`, `RawFraming(RawFramingConfig)`
|
||||
- `SshInterfaceConfig`: `auth: Arc<dyn IdentityProvider>`, `forwarding: Arc<ArcSwap<DynamicConfig>>`, `host_key: Arc<PrivateKey>`
|
||||
- `RawFramingConfig`: minimal (no SSH-specific config; auth via transport or call protocol)
|
||||
- `InterfaceSession` trait or enum: what the produced session looks like — this is the key design question (OQ-IF-01)
|
||||
- Valid `(Transport, Interface)` pair enumeration
|
||||
|
||||
**The key design decision**: How does the Interface session type relate to the call protocol's `EventEnvelope` stream? Per interface.md OQ-IF-01, every session should produce `EventEnvelope` frames, but SSH sessions have channels and auth, while raw framing sessions are just a byte stream with framing. This task must resolve this question concretely.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Interface` trait defined in `crates/alknet-core/src/interface/mod.rs` with `accept()` and associated `Session` type
|
||||
- [ ] `InterfaceConfig` enum defined with `Ssh` and `RawFraming` variants
|
||||
- [ ] `SshInterfaceConfig` defined with `auth`, `forwarding`, `host_key` fields
|
||||
- [ ] `RawFramingConfig` defined (minimal)
|
||||
- [ ] Valid `(Transport, Interface)` pair enumeration defined (e.g., as a const or validation function)
|
||||
- [ ] The session type question (OQ-IF-01) is resolved: documented decision on how sessions produce EventEnvelope frames
|
||||
- [ ] Module re-exported from `crates/alknet-core/src/lib.rs`
|
||||
- [ ] Decision documented in task notes for the next task (ssh-interface-extraction)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/interface.md — Interface trait, SshInterface, RawFramingInterface, valid pairs
|
||||
- docs/architecture/decisions/026-transport-interface-separation.md — ADR-026
|
||||
- docs/architecture/call-protocol.md — EventEnvelope, call protocol events
|
||||
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md — Protocol is interface-agnostic
|
||||
|
||||
## Notes
|
||||
|
||||
> OQ-IF-01 MUST be resolved before or during this task: how does the Interface session type relate to the call protocol's EventEnvelope stream?
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: core/multi-transport-listeners
|
||||
name: Implement multi-transport listeners with Vec<ListenerConfig>
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/config-static-dynamic-split
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Change `ServeTransportMode` from a single enum to `Vec<ListenerConfig>`, allowing a server to accept connections on multiple transports simultaneously. Per configuration.md and ADR-026.
|
||||
|
||||
Currently, `Server::run()` accepts a single transport mode. After this change, `Server::run()` spawns one accept loop per listener, sharing `DynamicConfig`, `ConnectionRateLimiter`, sessions, and shutdown signal.
|
||||
|
||||
**Key changes**:
|
||||
- `ListenerConfig` struct: `{ transport_kind: TransportKind, listen_addr: SocketAddr, ... per-transport config }`
|
||||
- `ServeOptions` gains a `listeners()` method (builder) that accepts `Vec<ListenerConfig>`
|
||||
- Backwards compatibility: `ServeOptions.transport_mode()` still works (creates a single-element listeners vec)
|
||||
- `Server::run()` iterates over listeners, spawning one accept loop per transport
|
||||
- `TransportKind` enum gains `Dns` and `WebTransport` variants (initially tags only, no acceptor implementation)
|
||||
- `DynamicConfig` and `IdentityProvider` are Arc'd and shared across all listeners
|
||||
|
||||
**What stays the same**: Single-transport usage via `ServeOptions.transport_mode()` continues to work unchanged.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ListenerConfig` struct defined with `transport_kind`, `listen_addr`, and per-transport configuration
|
||||
- [ ] `TransportKind` gains `Dns` and `WebTransport` variants (tag only, no behavior)
|
||||
- [ ] `ServeOptions` has both `.transport_mode()` (single, backwards compat) and `.listeners()` (multi) builder methods
|
||||
- [ ] `Server::run()` spawns one accept loop per `ListenerConfig`, sharing `DynamicConfig`, `ConnectionRateLimiter`, and `IdentityProvider`
|
||||
- [ ] All listeners share the same `Arc<ArcSwap<DynamicConfig>>` and `Arc<dyn IdentityProvider>`
|
||||
- [ ] Graceful shutdown terminates all listener accept loops
|
||||
- [ ] TOML config file support: `[[listeners]]` array-of-tables syntax (added to `StaticConfig`)
|
||||
- [ ] All existing tests pass (single-transport behavior unchanged)
|
||||
- [ ] New tests: multi-transport server with TCP + TLS listeners simultaneously
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/configuration.md — Multi-Transport Listeners, ListenerConfig
|
||||
- docs/architecture/decisions/026-transport-interface-separation.md — TransportKind includes all Layer 1 types
|
||||
- crates/alknet-core/src/server/serve.rs — current ServeOptions and Server::run()
|
||||
- crates/alknet-core/src/server/handler.rs — current TransportKind
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: core/napi-reload-api
|
||||
name: Add NAPI reload API for DynamicConfig and ForwardingPolicy
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/config-identity-provider-into-handler
|
||||
- core/multi-transport-listeners
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Expose `reloadAuth()`, `reloadForwarding()`, and `reloadAll()` on the NAPI `AlknetServer` object, per configuration.md and napi-and-pubsub.md updates.
|
||||
|
||||
**Key changes**:
|
||||
- `AlknetServer` NAPI object holds a `ConfigReloadHandle` (from task 1.5)
|
||||
- `reloadAuth(config)`: creates new `AuthPolicy` and atomically swaps it via ArcSwap
|
||||
- `reloadForwarding(config)`: creates new `ForwardingPolicy` and atomically swaps it
|
||||
- `reloadAll(config)`: swaps the entire `DynamicConfig`
|
||||
- Call protocol integration: expose operation registry for NAPI consumers to register handlers (depends on operation-context-registry)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `AlknetServer` NAPI object has `reloadAuth()`, `reloadForwarding()`, `reloadAll()` methods
|
||||
- [ ] `reloadAuth()` creates new `AuthPolicy` from provided config and swaps via `ConfigReloadHandle`
|
||||
- [ ] `reloadForwarding()` creates new `ForwardingPolicy` and swaps via `ConfigReloadHandle`
|
||||
- [ ] `reloadAll()` swaps the entire `DynamicConfig`
|
||||
- [ ] All swaps are atomic via ArcSwap — existing connections continue, new connections get new config
|
||||
- [ ] NAPI type definitions for `ForwardingPolicyConfig` and auth config types
|
||||
- [ ] Existing NAPI tests pass
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/configuration.md — NAPI Reload API
|
||||
- docs/architecture/napi-and-pubsub.md — NAPI layer additions
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
id: core/operation-context-registry
|
||||
name: Implement OperationContext, OperationRegistry, and OperationSpec
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/identity-type-provider
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the call protocol's core types: `OperationContext`, `OperationSpec`, `OperationRegistry`, `ResponseEnvelope`, and `CallError` in `alknet_core::call`, per call-protocol.md and ADR-033.
|
||||
|
||||
This is the foundation for the OperationEnv composition mechanism. Phase 1 ships with local dispatch only — the registry maps operation paths to handler functions. irpc and remote dispatch are contracted in the spec but not built yet.
|
||||
|
||||
**Key additions**:
|
||||
- `OperationSpec` struct: name, namespace, op_type (Query/Mutation/Subscription), input_schema, output_schema, access_control
|
||||
- `OperationType` enum: Query, Mutation, Subscription
|
||||
- `AccessControl` struct: required_scopes, required_scopes_any, resource_type, resource_action
|
||||
- `OperationContext` struct: request_id, parent_request_id, identity, metadata, env (OperationEnv), trusted
|
||||
- `OperationRegistry` struct: maps paths to `(OperationSpec, handler)` pairs, supports registration and lookup
|
||||
- `ResponseEnvelope` struct: request_id, result (Result<Value, CallError>)
|
||||
- `CallError` struct: code, message, retryable
|
||||
- Handler signature: `fn(input: Value, context: OperationContext) -> ResponseEnvelope` (or async)
|
||||
|
||||
**Important**: `OperationEnv` is defined here but only with local dispatch in Phase 1. The `env` field on `OperationContext` allows handlers to compose operations by calling `context.env.invoke(namespace, op, input)`. In Phase 1, this always resolves to a direct function call through the registry.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `OperationSpec`, `OperationType`, `AccessControl` defined in `crates/alknet-core/src/call/spec.rs`
|
||||
- [ ] `OperationContext` defined in `crates/alknet-core/src/call/context.rs` with all fields per call-protocol.md
|
||||
- [ ] `ResponseEnvelope` and `CallError` defined in `crates/alknet-core/src/call/response.rs`
|
||||
- [ ] `OperationRegistry` defined in `crates/alknet-core/src/call/registry.rs` with `register()`, `lookup()`, `invoke()` methods
|
||||
- [ ] `OperationEnv` defined with `local()` constructor and `invoke(namespace, op, input)` method for local dispatch only
|
||||
- [ ] Handler signature is clear (sync or async, Value in, ResponseEnvelope out)
|
||||
- [ ] Namespace-based routing: `/head/auth/verify` → namespace "auth", op "verify"
|
||||
- [ ] Unit tests: register and invoke an operation, verify ResponseEnvelope result
|
||||
- [ ] Unit tests: AccessControl checks against Identity.scopes and Identity.resources
|
||||
- [ ] Module re-exported from `crates/alknet-core/src/lib.rs`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/call-protocol.md — OperationSpec, OperationRegistry, OperationContext, ResponseEnvelope
|
||||
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md — ADR-033, OperationEnv composition model
|
||||
- docs/architecture/services.md — OperationContext fields
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: core/operationenv-local-dispatch
|
||||
name: Implement OperationEnv local dispatch and event envelope framing
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/operation-context-registry
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement Phase 1 OperationEnv functionality: local dispatch through the operation registry, and the `EventEnvelope` wire format for the call protocol.
|
||||
|
||||
OperationEnv is the universal composition mechanism (ADR-033). Phase 1 ships with local dispatch only — `OperationEnv::local(registry)` creates an environment where `env.invoke(namespace, op, input)` directly calls the registered handler function.
|
||||
|
||||
**Key additions**:
|
||||
- `OperationEnv` struct with `local()` constructor and `invoke()` method
|
||||
- `EventEnvelope` struct: type (event type string), id (correlation key), payload ( serde_json::Value)
|
||||
- Frame encoding: 4-byte big-endian length prefix + UTF-8 JSON body
|
||||
- `PendingRequestMap` for call/subscribe correlation
|
||||
- Call protocol event types: `call.requested`, `call.responded`, `call.completed`, `call.aborted`, `call.error`
|
||||
- Service discovery operations: `/services/list`, `/services/schema` registered by default
|
||||
|
||||
**Local dispatch only**: In Phase 1, `OperationEnv::local()` creates an environment where all operations resolve to local function calls. The `service()` and `remote()` dispatch paths are stubbed out with `unimplemented!()` or returning an error, to be filled in Phase 2+.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `OperationEnv::local(registry)` creates an environment with local dispatch
|
||||
- [ ] `OperationEnv::invoke(namespace, op, input)` resolves to the local handler and returns `ResponseEnvelope`
|
||||
- [ ] `EventEnvelope` struct defined with `type`, `id`, `payload` fields per call-protocol.md
|
||||
- [ ] Frame encoding/decoding: 4-byte BE length prefix + JSON body
|
||||
- [ ] `PendingRequestMap` with call/subscribe entry types
|
||||
- [ ] Call protocol event type constants defined
|
||||
- [ ] Default operations registered: `/services/list`, `/services/schema`
|
||||
- [ ] Unit tests: local invoke → correct ResponseEnvelope
|
||||
- [ ] Unit tests: frame encoding round-trip
|
||||
- [ ] Unit tests: EventEnvelope serialization/deserialization
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/call-protocol.md — EventEnvelope, PendingRequestMap, service discovery
|
||||
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md — OperationEnv composition model, local dispatch first
|
||||
- docs/architecture/services.md — OperationEnv deployment topologies
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
id: core/ssh-interface-extraction
|
||||
name: Extract SshInterface from ServerHandler — refactor SSH into Layer 2
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/interface-trait-definition
|
||||
- core/config-identity-provider-into-handler
|
||||
scope: broad
|
||||
risk: high
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
This is the most invasive change in Phase 1. Extract SSH session handling from `ServerHandler` into `SshInterface`, making it implement the `Interface` trait defined in the previous task. Per interface.md and ADR-026, SshInterface becomes one implementation of the Interface trait alongside RawFramingInterface.
|
||||
|
||||
**Current state**: `ServerHandler` is a `russh::server::Handler` that owns auth, channel management, and proxy logic — all tangled together.
|
||||
|
||||
**Target state**:
|
||||
- `SshInterface` implements `Interface` and wraps the SSH handshake and session management
|
||||
- Auth delegation goes through `IdentityProvider` (already wired from task 1.5)
|
||||
- Channel open requests (including forwarding policy checks) are Layer 3 concerns routed through the Interface session
|
||||
- Port forwarding proxy logic is per-connection state managed by the session
|
||||
|
||||
**Key changes**:
|
||||
- `SshInterface` wraps `russh` server config and session handling
|
||||
- `SshInterface::accept()` takes a `TransportStream` and `SshInterfaceConfig`, performs SSH handshake, returns a session
|
||||
- The session produces call protocol events (for `alknet-control:0` channels) and handles channel routing
|
||||
- Forwarding policy check is invoked from Layer 3 (call protocol handler), not embedded in the interface
|
||||
- `RawFramingInterface` stub: just the type definition, no implementation (Phase 4+ for DNS and WebTransport)
|
||||
- The server accept loop uses `(Transport, Interface)` pairs instead of directly spawning SSH handlers
|
||||
|
||||
**This is explicitly flagged as high-risk** in the integration plan. The refactoring must preserve all existing behavior. Strategy: implement `SshInterface` as a wrapper around the existing `ServerHandler` logic initially, then progressively move concerns to the right layer.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `SshInterface` struct implements `Interface` trait with `accept()` method
|
||||
- [ ] `SshInterface::accept()` performs SSH handshake over `TransportStream`
|
||||
- [ ] Auth is delegated through `IdentityProvider` (not embedded in SshInterface)
|
||||
- [ ] Channel open requests route through the session to Layer 3 (forwarding policy, call protocol)
|
||||
- [ ] `alknet-control:0` channels produce call protocol events via the session
|
||||
- [ ] Port forwarding proxy logic is per-connection state, not embedded in the interface
|
||||
- [ ] `RawFramingInterface` type exists but is stub-only (no implementation beyond type definition)
|
||||
- [ ] Server accept loop uses `(Transport, Interface)` pairs to spawn sessions
|
||||
- [ ] All existing server/auth/transport tests pass unchanged
|
||||
- [ ] New tests: `SshInterface` session over TCP and TLS transports
|
||||
- [ ] New tests: forwarding policy applied when channels are opened (Layer 3, not Layer 2)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/interface.md — SshInterface, what stays in Layer 2 vs Layer 3, OQ-IF-02
|
||||
- docs/architecture/decisions/026-transport-interface-separation.md — ADR-026
|
||||
- crates/alknet-core/src/server/handler.rs — current ServerHandler to be extracted
|
||||
|
||||
## Notes
|
||||
|
||||
> This is the highest-risk task in Phase 1. Consider implementing incrementally: first wrap existing ServerHandler in SshInterface, then progressively move auth to IdentityProvider, channel routing to call protocol, etc. Each step should have passing tests before proceeding.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
id: api-keys-dynamic-config
|
||||
name: Add API keys to DynamicConfig.auth and extend IdentityProvider token resolution
|
||||
status: completed
|
||||
depends_on: [credential-provider-trait]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Add `[[auth.api_keys]]` support to `DynamicConfig` and extend `ConfigIdentityProvider::resolve_from_token()` to verify API keys alongside existing AuthTokens. API keys are shorter, simpler bearer strings (hash-verified, with optional TTL and scopes) for service accounts and automation — they don't require Ed25519 key pairs like AuthTokens do.
|
||||
|
||||
Per ADR-037 and research/phase2/interface-model.md (Config section):
|
||||
|
||||
**API Key format**: `alk_<random_prefix>_<secret>` (or similar). Storage uses SHA-256 hash of the full key. Lookup is by prefix (first N characters), then hash verification of the full key.
|
||||
|
||||
**Config format**:
|
||||
```toml
|
||||
[[auth.api_keys]]
|
||||
prefix = "alk_dGhl"
|
||||
hash = "sha256:abc123..."
|
||||
scopes = ["relay:connect"]
|
||||
description = "dashboard service account"
|
||||
```
|
||||
|
||||
**Key changes**:
|
||||
- Add `ApiKeyEntry` struct: `prefix`, `hash`, `scopes`, `description`, `optional ttl/expires_at`
|
||||
- Add `api_keys: Vec<ApiKeyEntry>` to `AuthPolicy` (or a separate section on `DynamicConfig`)
|
||||
- Extend `ConfigIdentityProvider::resolve_from_token()` to check API keys: prefix match → hash verification → return `Identity`
|
||||
- API keys produce `Identity { id: "<prefix>", scopes: <from entry>, resources: {} }`
|
||||
- The `AuthToken` path (Ed25519 signed timestamp) is unchanged — both go through the same `resolve_from_token()` method, discriminated by format/prefix
|
||||
|
||||
**Why this is Phase 2**: The HTTP interface (task 2.7) needs bearer token auth, and API keys are the simplest mechanism for `IdentityProvider::resolve_from_token()`. Without this, HTTP auth has no config-based auth mechanism.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ApiKeyEntry` struct defined with `prefix`, `hash`, `scopes`, `description`, `expires_at: Option<u64>` fields
|
||||
- [ ] `AuthPolicy` gains an `api_keys: Vec<ApiKeyEntry>` field (or `DynamicConfig` gains a separate `api_keys` section)
|
||||
- [ ] `ConfigIdentityProvider::resolve_from_token()` checks API keys: matches prefix, verifies SHA-256 hash of the full token, returns `Identity` on success
|
||||
- [ ] API key lookup: tokens starting with `alk_` (or configured prefix) are treated as API keys; others go through the `AuthToken` verification path
|
||||
- [ ] Expired API keys (where `expires_at` is set and in the past) are rejected
|
||||
- [ ] API key scopes propagate to the returned `Identity.scopes` field
|
||||
- [ ] `DynamicConfig::default()` includes an empty `api_keys` list (no behavioral change)
|
||||
- [ ] `ConfigReloadHandle` reloads API keys along with the rest of `AuthPolicy`
|
||||
- [ ] Unit test: valid API key authenticates via `resolve_from_token()`
|
||||
- [ ] Unit test: expired API key is rejected
|
||||
- [ ] Unit test: wrong hash is rejected
|
||||
- [ ] Unit test: unknown prefix is rejected (falls through to AuthToken path)
|
||||
- [ ] Unit test: API key scopes appear in the resolved `Identity`
|
||||
- [ ] All existing auth tests continue to pass (no behavioral change for SSH key auth)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/037-api-keys-dynamic-config.md — ADR-037
|
||||
- docs/research/phase2/interface-model.md — API keys in config, auth table
|
||||
- docs/research/integration-plan.md — Phase 2.6
|
||||
- crates/alknet-core/src/config/dynamic_config.rs — DynamicConfig, AuthPolicy
|
||||
- crates/alknet-core/src/auth/identity.rs — ConfigIdentityProvider, IdentityProvider trait
|
||||
|
||||
## Notes
|
||||
|
||||
> The prefix match approach means we don't store the full API key in config — just the first ~8 chars for fast lookup and the SHA-256 hash for verification. This mirrors how GitHub/personal access tokens work.
|
||||
|
||||
> Consider whether `api_keys` should live on `AuthPolicy` or be a separate section. Putting it on `AuthPolicy` keeps all auth-related config together and ensures atomic reloads. The `ConfigIdentityProvider` already has access to `Arc<ArcSwap<DynamicConfig>>` so it can read both `authorized_keys` and `api_keys` from the same reload.
|
||||
|
||||
> The `resolve_from_token()` method currently takes `&AuthToken` — API keys are NOT AuthTokens (they're simple bearer strings). The method signature may need to accept a generic `&str` or a new enum that can be either an AuthToken string or an API key string. Alternatively, `resolve_from_token()` can accept `&str` and internally discriminate by prefix/format.
|
||||
|
||||
## Summary
|
||||
|
||||
> Added ApiKeyEntry struct (prefix/hash/scopes/description/expires_at) to AuthPolicy. Extended ConfigIdentityProvider::resolve_from_token() to route alk_-prefixed tokens through SHA-256 hash verification with expiry checks. 14 new API key tests pass.
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
id: axum-http-router-scaffold
|
||||
name: Axum HTTP router scaffold with auth middleware and stealth handoff
|
||||
status: completed
|
||||
depends_on: [api-keys-dynamic-config, listenconfig-http-dns-stubs]
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Create an axum HTTP router scaffold behind the `http` feature flag, with auth middleware that extracts `Authorization: Bearer <token>` and calls `IdentityProvider::resolve_from_token()`, and a stealth mode handoff that replaces `send_fake_nginx_404` with routing detected HTTP traffic to the axum router.
|
||||
|
||||
Per the integration plan section 2.7 and research/phase2/tls-transport.md:
|
||||
|
||||
This task creates the structural scaffold for HTTP — auth middleware and stealth handoff only. No operational routes (no `POST /v1/{namespace}/{op}` handlers). The question of how HTTP paths map to operation invocations is intentionally deferred to Phase 5.
|
||||
|
||||
**Key components**:
|
||||
1. **Auth middleware**: Extract `Authorization: Bearer <token>` from HTTP request headers. Call `IdentityProvider::resolve_from_token()`. Attach resolved `Identity` to request extensions. Reject with 401 if token is missing or invalid. Both AuthTokens (Ed25519 signed) and API keys (hash-verified) go through this path.
|
||||
2. **Stealth handoff**: When `ListenerConfig::Http { stealth: true }`, replace `send_fake_nginx_404` with routing the detected-HTTP `BufReader<TlsStream>` to the axum router. The existing `ProtocolDetection` enum already has `Ssh` vs `Http` — the `Http` path currently sends a fake 404 and disconnects.
|
||||
3. **Default 404 handler**: Any unmatched route returns 404. No `/v1/*` routes are registered yet.
|
||||
4. **Dependency**: Add `axum` dependency behind `http` feature flag in `Cargo.toml`.
|
||||
|
||||
**Current state**:
|
||||
- `stealth.rs` has `detect_protocol()` returning `ProtocolDetection::Ssh` or `ProtocolDetection::Http`
|
||||
- `send_fake_nginx_404()` currently sends a fake nginx 404 response
|
||||
- No `axum` dependency exists yet
|
||||
- `IdentityProvider::resolve_from_token()` exists (will be extended with API keys by task 2.6)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `axum` dependency added to `Cargo.toml` behind `http` feature flag
|
||||
- [ ] `crates/alknet-core/src/http/` module created (behind `http` feature flag)
|
||||
- [ ] Auth middleware function: extracts `Authorization: Bearer <token>`, calls `IdentityProvider::resolve_from_token()`, attaches `Identity` to axum request extensions, returns 401 on missing/invalid token
|
||||
- [ ] Auth middleware supports both AuthTokens and API keys (via `resolve_from_token()` which dispatches based on format/prefix)
|
||||
- [ ] Stealth handoff: `stealth.rs` `send_fake_nginx_404` replaced with axum router handoff when `http` feature is enabled. When `http` feature is disabled, the fake 404 behavior remains.
|
||||
- [ ] Default 404 handler for unmatched routes (returns `404 Not Found`)
|
||||
- [ ] Axum `Router` scaffold constructed with auth middleware layer and default 404 fallback
|
||||
- [ ] `HttpInterface` struct from task 1 (stream/message interface split) gets its internal `Router` reference and `IdentityProvider` wired
|
||||
- [ ] `http` feature flag in `Cargo.toml` correctly gates the `axum` dependency and `http` module
|
||||
- [ ] Unit test: auth middleware extracts bearer token from `Authorization` header
|
||||
- [ ] Unit test: auth middleware returns 401 for missing token
|
||||
- [ ] Unit test: auth middleware returns 401 for invalid token
|
||||
- [ ] Unit test: auth middleware attaches `Identity` to request extensions on valid token
|
||||
- [ ] Integration test: stealth mode detection routes HTTP traffic to axum (not fake 404)
|
||||
- [ ] All existing server/stealth tests continue to pass (no behavioral change when `http` feature is disabled)
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 2.7
|
||||
- docs/research/phase2/tls-transport.md — Axum integration, stealth handoff, auth middleware
|
||||
- crates/alknet-core/src/server/stealth.rs — Current ProtocolDetection, send_fake_nginx_404
|
||||
- crates/alknet-core/src/auth/identity.rs — IdentityProvider::resolve_from_token()
|
||||
|
||||
## Notes
|
||||
|
||||
> The integration plan explicitly states: "No operational routes yet — the question of how HTTP paths map to operation invocations depends on the from_openapi / spec-generation work and is deferred to Phase 5." This task is a scaffold: auth middleware, stealth handoff, default 404. Full route registrations come later.
|
||||
|
||||
> For the stealth handoff, consider a compile-time approach: the `http` feature flag determines whether `send_fake_nginx_404` or the axum handoff is used. When `http` is disabled, the existing fake 404 behavior should remain unchanged.
|
||||
|
||||
> The axum router is created per-server (not per-request). It holds references to the `IdentityProvider` and `OperationEnv`/`OperationRegistry`.
|
||||
|
||||
> `send_fake_nginx_404` should NOT be deleted — just conditionally bypassed when the `http` feature is enabled and a `ListenerConfig::Http` listener is configured.
|
||||
|
||||
## Summary
|
||||
|
||||
> Added http feature flag with axum/hyper/hyper-util/tower dependencies. Created http module: auth middleware extracts Bearer token, calls resolve_from_token, attaches Identity to extensions; router scaffold with default 404 fallback, no operational routes. Stealth handoff routes ProtocolDetection::Http to axum when http feature enabled; fake nginx 404 preserved when disabled. HttpInterface gains build_router() method.
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
id: credential-provider-trait
|
||||
name: Define CredentialProvider trait, CredentialSet enum, and ConfigCredentialProvider implementation
|
||||
status: completed
|
||||
depends_on: [stream-interface-message-interface-split]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define the `CredentialProvider` trait and `CredentialSet` enum in `alknet_core::credentials`, implementing the outbound authentication abstraction that complements the inbound `IdentityProvider`. This is the "Phase A" / "Phase 2.4a" work — the trait and enum must exist in core before alknet-secret (Phase 3) can wire `SecretStoreCredentialProvider` against them.
|
||||
|
||||
Per ADR-036 and research/phase2/credential-provider.md:
|
||||
|
||||
- `CredentialProvider` resolves **outbound** credentials: "how does alknet authenticate TO external services?"
|
||||
- `CredentialSet` is a structured enum of credential types: `ApiKey`, `Basic`, `Bearer`, `S3AccessKey`, `OidcToken`, `Custom`
|
||||
- `ConfigCredentialProvider` reads API keys and static credentials from `DynamicConfig` — the Phase 2 default (simple, no secret service dependency)
|
||||
- `SecretStoreCredentialProvider` is a **stub** that returns `None` for all lookups until Phase 3 provides the alknet-secret dependency
|
||||
- Wire `CredentialProvider` into `OperationEnv`/`OperationContext` so handlers can access credentials
|
||||
|
||||
**Relationship to IdentityProvider**: These are opposite-direction abstractions. `IdentityProvider` resolves inbound auth (who is calling alknet). `CredentialProvider` resolves outbound auth (how alknet calls others). Both live at the same architectural layer.
|
||||
|
||||
**Relationship to OperationEnv**: Handlers compose through `context.env`. The `OperationEnv` needs access to `CredentialProvider` so that handlers calling external services can resolve credentials. This could be a dedicated field on `OperationContext` or accessible through the `env` — the implementation detail is flexible, but the behavioral contract must match: given a service name, return credentials for that service.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `CredentialProvider` trait defined in `crates/alknet-core/src/credentials/mod.rs` with `get_credentials(&self, service: &str) -> Option<CredentialSet>` and `refresh_credentials(&self, service: &str) -> Option<CredentialSet>`
|
||||
- [ ] `CredentialSet` enum defined with variants: `ApiKey { header_name, token }`, `Basic { username, password }`, `Bearer { token }`, `S3AccessKey { access_key, secret_key, session_token }`, `OidcToken { access_token, refresh_token, expires_at }`, `Custom { scheme, params }`
|
||||
- [ ] `ConfigCredentialProvider` struct implemented — reads credentials from `DynamicConfig.auth` (or a new `DynamicConfig.credentials` section). For Phase 2, this is a simple config-backed lookup returning `CredentialSet::Bearer` or `CredentialSet::ApiKey` entries.
|
||||
- [ ] `SecretStoreCredentialProvider` struct defined as a stub — `get_credentials()` always returns `None`. Full implementation deferred to Phase 3.
|
||||
- [ ] `CredentialProvider` wired into `OperationContext` or `OperationEnv` so handlers can access outbound credentials
|
||||
- [ ] `credentials` module re-exported from `crates/alknet-core/src/lib.rs`
|
||||
- [ ] Unit test: `ConfigCredentialProvider` returns configured credentials for a service name
|
||||
- [ ] Unit test: `ConfigCredentialProvider` returns `None` for unknown service names
|
||||
- [ ] Unit test: `SecretStoreCredentialProvider` stub returns `None` for all service names
|
||||
- [ ] Unit test: `OperationEnv`/`OperationContext` provides access to `CredentialProvider` from handler context
|
||||
- [ ] `CredentialSet` derives `Clone`, `Debug`, `serde::Serialize`, `serde::Deserialize`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/036-credentialprovider-core-type.md — ADR-036
|
||||
- docs/research/phase2/credential-provider.md — Full design rationale
|
||||
- docs/research/integration-plan.md — Phase 2.4
|
||||
- crates/alknet-core/src/auth/identity.rs — IdentityProvider (opposite direction, same pattern)
|
||||
- crates/alknet-core/src/call/env.rs — OperationEnv
|
||||
- crates/alknet-core/src/call/context.rs — OperationContext
|
||||
|
||||
## Notes
|
||||
|
||||
> This task is "2.4a" — the core types and config-backed implementation. "2.4b" (SecretStoreCredentialProvider backed by SecretProtocol::Decrypt) is deferred to Phase 3 when alknet-secret exists.
|
||||
|
||||
> For `ConfigCredentialProvider`, consider whether to add a `[[credentials]]` section to `DynamicConfig` or to reuse a subsection. The simplest Phase 2 approach is a new `credentials: HashMap<String, CredentialSet>` field on `DynamicConfig` that stores static bearer tokens/API keys from config.
|
||||
|
||||
> The `OperationEnv`/`OperationContext` wiring can follow either pattern: (a) a `credential_provider: Arc<dyn CredentialProvider>` field on `OperationContext`, or (b) `CredentialProvider` accessible through a registry-style `env.credentials(service)` method. The integration plan says "wire into OperationEnv so handlers can access credentials through context.env" — approach (b) aligns with the OperationEnv composition model. This is an implementation detail to resolve during implementation.
|
||||
|
||||
## Summary
|
||||
|
||||
> Added CredentialProvider trait, CredentialSet enum (ApiKey/Basic/Bearer/S3AccessKey/OidcToken/Custom), ConfigCredentialProvider (reads from DynamicConfig.credentials), SecretStoreCredentialProvider stub (returns None, deferred to Phase 3), wired into OperationEnv via env.credentials(service) method, and added credentials HashMap to DynamicConfig.
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
id: listenconfig-http-dns-stubs
|
||||
name: Add HttpListenerConfig/DnsListenerConfig wiring, StreamInterfaceKind/MessageInterfaceKind, and ListenerConfig enum helper methods
|
||||
status: completed
|
||||
depends_on: [stream-interface-message-interface-split]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
After the `stream-interface-message-interface-split` task restructures `ListenerConfig` from a flat struct to the ADR-035 enum form (with `Stream`, `Http`, `Dns` variants), removes `TransportKind::Dns`, and adds the `StreamInterfaceConfig`/`MessageInterfaceConfig`/`StreamInterfaceKind`/`MessageInterfaceKind`/`HttpListenerConfig`/`DnsListenerConfig` types, this task wires those new types into the server's accept loop and adds the helper methods, validation, and constructors that make the new `ListenerConfig` enum usable.
|
||||
|
||||
Per the integration plan section 2.5 and research/phase2/interface-model.md and tls-transport.md:
|
||||
|
||||
**What task 1 (stream-interface-message-interface-split) already did**:
|
||||
- `ListenerConfig` restructured to enum with `Stream { transport, interface }`, `Http { config: HttpListenerConfig }`, `Dns { config: DnsListenerConfig }` variants
|
||||
- `TransportKind::Dns` removed
|
||||
- `TransportKind::WebTransport` updated to `{ server_name: Option<String> }`
|
||||
- `StreamInterfaceConfig`/`MessageInterfaceConfig` enums defined
|
||||
- `StreamInterfaceKind`/`MessageInterfaceKind` enums defined
|
||||
- `HttpListenerConfig` and `DnsListenerConfig` struct types defined
|
||||
- `is_valid_pair()` updated for StreamInterface pairs only
|
||||
|
||||
**What this task adds** (the wiring layer on top):
|
||||
|
||||
- **ListenerConfig constructors and helpers**: The enum exists but needs `tcp()`, `tls()`, `iroh()` convenience constructors that produce `ListenerConfig::Stream`, and `http()` / `dns()` constructors that produce their respective variants. These replace the old struct-style builders that task 1 removed.
|
||||
- **ListenerConfig validation**: `validate()` method on the enum that checks: TLS cert/key requirements for Stream+Tls listeners, stealth-only-on-TLS for Http listeners, no TLS options on non-TLS variants.
|
||||
- **Server accept loop wiring**: Update `Server::run()` and `handle_connection()` to match on the `ListenerConfig` enum variants. The `Stream` variant runs through the existing SSH/raw-framing accept path. The `Http` and `Dns` variants are stubs for now (Http defers to task 2.7 for the axum router, Dns defers to Phase 5).
|
||||
- **Display implementations**: Ensure all new types have proper Display impls (task 1 likely added basic ones; add any missing).
|
||||
- **ServeOptions integration**: Update `ServeOptions` to work with the new `ListenerConfig` enum — the `listeners` field should accept the enum form, and `StaticConfig::from_serve_options()` should produce `ListenerConfig` enum values.
|
||||
|
||||
**Note on stealth mode**: The `HttpListenerConfig.stealth` field means "if true, do byte-peek protocol detection on incoming TLS connections". This connects to the existing `stealth.rs` protocol detection. The axum router scaffold (task 2.7) handles the routing when stealth mode detects HTTP traffic. This task just wires the config types into the server.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `ListenerConfig` enum has convenience constructors: `tcp(addr)` → `ListenerConfig::Stream`, `tls(addr)` → `ListenerConfig::Stream`, `iroh(addr)` → `ListenerConfig::Stream`, `http(config)` → `ListenerConfig::Http`, `dns(config)` → `ListenerConfig::Dns`
|
||||
- [ ] `HttpListenerConfig` has a builder-pattern API: `HttpListenerConfig::new(addr).tls(true).stealth(true)`
|
||||
- [ ] `DnsListenerConfig` has a builder-pattern API: `DnsListenerConfig::new(addr).tls(true)`
|
||||
- [ ] `ListenerConfig::validate()` works for all three variants: Stream checks TLS cert/key, Http checks stealth-only-with-TLS, Dns has minimal validation
|
||||
- [ ] `Server::run()` updated to match on `ListenerConfig` variants: Stream variant uses existing accept path, Http/Dns variants are stubs that log "not yet implemented" for now
|
||||
- [ ] `StaticConfig::from_serve_options()` produces `ListenerConfig` enum values correctly
|
||||
- [ ] `ServeOptions.listeners` field works with the new enum form
|
||||
- [ ] `is_valid_pair()` called during `ListenerConfig::Stream` validation
|
||||
- [ ] Serialization support (`serde::Serialize`/`Deserialize`) for all config types verified working
|
||||
- [ ] All existing server/transport tests pass (updated to use new enum constructors)
|
||||
- [ ] Unit test: `ListenerConfig::Http` variant constructs with `HttpListenerConfig`
|
||||
- [ ] Unit test: `ListenerConfig::Dns` variant constructs with `DnsListenerConfig`
|
||||
- [ ] Unit test: `ListenerConfig::Stream` validates TLS cert/key requirements
|
||||
- [ ] Unit test: stealth on non-TLS Http listener is rejected by validation
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 2.5
|
||||
- docs/research/phase2/interface-model.md — ListenerConfig, TransportKind, InterfaceKind redesign
|
||||
- docs/research/phase2/tls-transport.md — HTTP listener config, stealth mode
|
||||
- docs/architecture/decisions/035-streaminterface-messageinterface-split.md — ADR-035 (ListenerConfig enum form)
|
||||
- crates/alknet-core/src/interface/config.rs — InterfaceConfig (now StreamInterfaceConfig/MessageInterfaceConfig)
|
||||
- crates/alknet-core/src/interface/pairs.rs — Valid transport-interface pairs
|
||||
- crates/alknet-core/src/server/serve.rs — ListenerConfig, Server, ServeOptions, StaticConfig
|
||||
|
||||
## Notes
|
||||
|
||||
> This task depends heavily on what task 1 produces. Before starting, do `git fetch origin && git merge origin/main --no-edit` to get task 1's changes, then read the current state of `ListenerConfig`, `Server`, `ServeOptions`, and `StaticConfig` to understand the enum form they now take.
|
||||
|
||||
> The `Http` and `Dns` accept loop stubs should be minimal — just log a message and skip the connection. The full implementations come in task 2.7 (axum scaffold) and Phase 5 (DNS).
|
||||
|
||||
> The `stealth` field on `HttpListenerConfig` controls whether the server does byte-peek protocol detection (first bytes → SSH vs HTTP). When `stealth: true` on a listener sharing port 443 with SSH, the accept loop routes based on protocol detection. When `stealth: false`, the HTTP listener receives all traffic directly.
|
||||
|
||||
> The `tls: bool` field is separate from `stealth`. `tls: true` means "use TLS on this listener". `stealth: true` means "peek first bytes to detect SSH vs HTTP". These are orthogonal: you can have TLS + stealth (port 443), TLS without stealth (port 8443), plain HTTP without stealth (port 8080), etc.
|
||||
|
||||
> Use `#[non_exhaustive]` on `ListenerConfig`, `StreamInterfaceKind`, `MessageInterfaceKind`, and `MessageInterfaceConfig` so future variants (WebSocket, gRPC) don't break downstream.
|
||||
|
||||
## Summary
|
||||
|
||||
> Added HttpListenerConfig::new().tls().stealth() and DnsListenerConfig::new().tls() builder APIs. Integrated is_valid_pair() into StreamListenerConfig::validate(). Added ListenerConfig::validate() for Http (stealth-requires-TLS) and Dns variants. Refactored Server::run() to dispatch on ListenerConfig variants (Stream→existing accept loop, Http/Dns→warn stubs). 12 new unit tests.
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
id: raw-framing-interface-implementation
|
||||
name: Implement RawFramingInterface accept/recv/send with first-frame auth
|
||||
status: completed
|
||||
depends_on: [stream-interface-message-interface-split]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement `RawFramingInterface` and `RawFramingSession` to handle length-prefixed `EventEnvelope` frames over a byte stream, with first-frame authentication. Currently `RawFramingInterface::accept()` returns an error and `RawFramingSession` stubs exist.
|
||||
|
||||
Per the integration plan section 2.2 and interface.md:
|
||||
|
||||
**RawFramingInterface**: Reads 4-byte length-prefixed JSON `EventEnvelope` frames from a transport stream (TCP, TLS, iroh, etc.). No SSH wrapping — the raw framing interface carries call protocol events directly.
|
||||
|
||||
**First-frame auth**: The first `InterfaceEvent` on a `RawFramingSession` carries an auth token in the `InterfaceEvent.identity` field or a dedicated auth event type. After `IdentityProvider::resolve_from_token()` verifies the token and produces an `Identity`, the session is authenticated. Subsequent frames are call protocol `EventEnvelope` data. If auth fails, the session is terminated immediately.
|
||||
|
||||
**Current state of the code**:
|
||||
- `RawFramingInterface` accepts any `TransportStream` but returns an error
|
||||
- `RawFramingSession` is an empty struct with stub `recv()` (returns `None`) and `send()` (returns error)
|
||||
- `call::frame::{encode, decode, decode_with_remainder}` already implement the wire format
|
||||
- `IdentityProvider::resolve_from_token()` exists but is not yet wired to `AuthToken` verification (that's coming in the API keys task)
|
||||
|
||||
**Implementation approach**:
|
||||
1. `RawFramingInterface::accept()` takes a `TransportStream`, wraps it in a `BufReader` for buffered reading, stores it in `RawFramingSession`. The `RawFramingSession` is created in an "unauthenticated" state.
|
||||
2. `RawFramingSession::recv()` reads frames from the stream:
|
||||
- If unauthenticated: read the first frame, extract the auth token, call `IdentityProvider::resolve_from_token()`. On success, transition to "authenticated" with the resolved `Identity`. On failure, return an error (session terminated).
|
||||
- If authenticated: read `EventEnvelope` frames, wrap in `InterfaceEvent::with_identity(envelope, identity)`.
|
||||
3. `RawFramingSession::send()` writes `EventEnvelope` frames to the stream using `call::frame::encode`.
|
||||
|
||||
The `RawFramingSession` needs:
|
||||
- A `BufReader<Box<dyn TransportStream>>` for reading framed data
|
||||
- A `Box<dyn TransportStream>` (or WriteHalf) for writing framed data
|
||||
- An `Option<Identity>` tracking auth state
|
||||
- A reference to `IdentityProvider` for token resolution
|
||||
- A buffer for partial frame reads (`decode_with_remainder` pattern)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `RawFramingInterface::accept()` takes a `TransportStream` and `StreamInterfaceConfig::RawFraming` config, creates a `RawFramingSession` wrapping the stream
|
||||
- [ ] `RawFramingSession` holds a buffered reader and writer over the transport stream, an auth state (`Option<Identity>`), and a reference to `IdentityProvider`
|
||||
- [ ] `RawFramingSession::recv()` reads length-prefixed `EventEnvelope` frames from the stream using `call::frame::decode_with_remainder`
|
||||
- [ ] First-frame auth: the first `recv()` call resolves the auth token via `IdentityProvider::resolve_from_token()` and stores the resulting `Identity`
|
||||
- [ ] Subsequent `recv()` calls produce `InterfaceEvent::with_identity(envelope, identity)` using the authenticated identity
|
||||
- [ ] Auth failure terminates the session: `recv()` returns an error result on bad tokens
|
||||
- [ ] `RawFramingSession::send()` writes `EventEnvelope` frames to the stream using `call::frame::encode`
|
||||
- [ ] Unit test: `RawFramingInterface::accept()` succeeds with a valid stream
|
||||
- [ ] Unit test: `RawFramingSession` round-trips an `EventEnvelope` through `send()` and `recv()` (after mock auth)
|
||||
- [ ] Unit test: First-frame auth with a valid token transitions to authenticated state
|
||||
- [ ] Unit test: First-frame auth with an invalid token returns an error
|
||||
- [ ] Integration test: `RawFramingSession` over a `tokio::io::duplex` stream (simulated TCP) sends and receives multiple frames
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 2.2
|
||||
- docs/architecture/interface.md — RawFramingInterface, first-frame auth model
|
||||
- crates/alknet-core/src/interface/raw_framing.rs — Current stubs
|
||||
- crates/alknet-core/src/call/frame.rs — Frame encode/decode
|
||||
- crates/alknet-core/src/auth/identity.rs — IdentityProvider, resolve_from_token
|
||||
|
||||
## Notes
|
||||
|
||||
> The frame format is already implemented and tested in `call::frame`. This task is primarily about wiring the frame reader/writer to the `InterfaceSession` trait and adding first-frame auth logic.
|
||||
|
||||
> Consider using `tokio::io::BufReader` for buffered reading and `tokio::io::BufWriter` for buffered writing. The `decode_with_remainder` function handles partial reads by returning how many bytes were consumed — the session needs to maintain a read buffer for reassembly.
|
||||
|
||||
> The `RawFramingInterface` config should include an `Arc<dyn IdentityProvider>` for first-frame auth. This follows the same pattern as `SshInterfaceConfig`.
|
||||
|
||||
## Summary
|
||||
|
||||
> Implemented RawFramingInterface::accept() and RawFramingSession with first-frame auth. RawFramingConfig now has auth: Arc<dyn IdentityProvider>. RawFramingSession splits stream via tokio::io::split, recv() implements first-frame auth with AuthToken resolution via IdentityProvider::resolve_from_token(), send() encodes via call::frame::encode with buffered writes, read_frame() uses decode_with_remainder for partial reassembly. 7 new tests.
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
id: review-core-bridge-phase2
|
||||
name: Review all Phase 2 changes for spec conformance and prepare for Phase 3
|
||||
status: completed
|
||||
depends_on: [stream-interface-message-interface-split, ssh-session-call-protocol-bridge, raw-framing-interface-implementation, credential-provider-trait, api-keys-dynamic-config, listenconfig-http-dns-stubs, axum-http-router-scaffold]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: phase
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review all Phase 2 implementation for spec conformance, architectural consistency, and completeness before Phase 3 crate development begins. Per integration plan section 4.5, a second doc sync should capture any deviations between spec and implementation.
|
||||
|
||||
This review covers:
|
||||
1. **Spec conformance**: Do implementations match the architecture docs and ADRs (035, 036, 037)?
|
||||
2. **Layer boundary discipline**: Does every component belong to exactly one layer? No call protocol logic in the interface layer, no interface logic in the transport layer.
|
||||
3. **Terminology consistency**: head/worker everywhere (no hub/spoke), StreamInterface/MessageInterface (no bare "Interface" trait), consistent naming.
|
||||
4. **Test coverage**: Do all Phase 2 tasks have tests that verify acceptance criteria?
|
||||
5. **No circular dependencies**: alknet-core doesn't depend on alknet-secret, alknet-storage, or alknet-flowgraph.
|
||||
6. **Doc sync**: Update architecture docs to reflect Phase 2 implementation state. Specifically:
|
||||
- `interface.md` — StreamInterface/MessageInterface split, InterfaceRequest/InterfaceResponse
|
||||
- `auth.md` — API keys, resolve_from_token() changes
|
||||
- `configuration.md` — DynamicConfig additions (api_keys, credentials)
|
||||
- `call-protocol.md` — SshSession bridge, RawFraming auth flow
|
||||
- Any deviations between spec and implementation should be documented
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All Phase 2 tasks have acceptance criteria verified (each task's AC checklist is complete)
|
||||
- [ ] Layer boundaries are clean: interface layer produces/consumes `InterfaceEvent`; protocol layer handles `EventEnvelope`; transport layer provides byte streams
|
||||
- [ ] No `Interface` trait references remain (all renamed to `StreamInterface`)
|
||||
- [ ] No `TransportKind::Dns` in the enum (DNS is a `MessageInterface`)
|
||||
- [ ] `Cargo.toml` dependency check: alknet-core has no circular deps on external crates
|
||||
- [ ] `http` feature flag correctly gates axum dependency
|
||||
- [ ] Architecture docs updated for Phase 2 state:
|
||||
- [ ] `interface.md` reflects StreamInterface/MessageInterface split
|
||||
- [ ] `auth.md` reflects API keys in DynamicConfig
|
||||
- [ ] `configuration.md` reflects new DynamicConfig sections
|
||||
- [ ] `call-protocol.md` reflects functional SshSession bridge
|
||||
- [ ] All tests pass: `cargo test --all-features`
|
||||
- [ ] No compiler warnings on Phase 2 code
|
||||
- [ ] `taskgraph parallel --path tasks/integration/phase2` shows all tasks completed
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 4.5 doc sync
|
||||
- All Phase 2 ADRs: 035, 036, 037
|
||||
- All Phase 2 implementation tasks (2.1–2.7)
|
||||
- docs/architecture/ — architecture docs to update
|
||||
|
||||
## Notes
|
||||
|
||||
> This is a quality gate before Phase 3. The review should be thorough but shouldn't block on minor documentation phrasing. Focus on structural conformance: are layers respected, are traits correct, are dependencies acyclic?
|
||||
|
||||
> If any deviations between spec and implementation are found, document them in the relevant architecture doc with a "Deviation from spec" note explaining why.
|
||||
|
||||
## Summary
|
||||
|
||||
> Review complete. All 7 implementation tasks verified. 487 tests pass (all-features), clippy clean, fmt clean. No bare `Interface` trait references remain. No `TransportKind::Dns` in enum. No circular deps. `http` feature flag gates axum correctly. Layer boundaries clean. Architecture docs (interface.md, auth.md, configuration.md, call-protocol.md) updated with Phase 2 implementation notes.
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
id: ssh-session-call-protocol-bridge
|
||||
name: Bridge SshSession recv/send to call protocol via alknet-control:0 channel
|
||||
status: completed
|
||||
depends_on: [stream-interface-message-interface-split]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement `SshSession::recv()` and `SshSession::send()` to bridge SSH channel data to and from the call protocol's `InterfaceEvent`/`EventEnvelope` frames. Currently both methods are stubs: `recv()` always returns `None` and `send()` silently discards.
|
||||
|
||||
Per the integration plan section 2.1 and interface.md (OQ-IF-01, resolved):
|
||||
|
||||
The bridge works as follows:
|
||||
- When `SshHandler::channel_open_direct_tcpip` detects a destination starting with `alknet-`, it currently accepts the channel but doesn't bridge the data. The `ControlChannelRouter` exists in `control_channel.rs` but has no handler wired.
|
||||
- `SshSession::recv()` should read `EventEnvelope` frames from the `alknet-control:0` channel stream (using the 4-byte length prefix + JSON wire format from `call::frame::{encode, decode}`), wrap them in `InterfaceEvent` with the session's `Identity` (obtained during SSH auth).
|
||||
- `SshSession::send()` should write `EventEnvelope` frames to the `alknet-control:0` channel stream using the same framing format.
|
||||
- The `ControlChannelRouter` should be wired to bridge incoming channel data to the call protocol handler.
|
||||
|
||||
**Current state of the code**:
|
||||
- `SshSession::recv()` returns `None` (stub)
|
||||
- `SshSession::send()` discards silently (stub)
|
||||
- `ControlChannelRouter` in `control_channel.rs` has `route()` and `has_handler()` but no handler is registered
|
||||
- `call::frame::{encode, decode}` functions exist and are well-tested (4-byte BE length prefix + JSON)
|
||||
- `SshHandler` detects `alknet-*` destinations in `channel_open_direct_tcpip` but doesn't bridge data
|
||||
- `SshHandler` stores `authenticated_identity: Option<Identity>` from SSH auth
|
||||
- `InterfaceEvent` struct carries `EventEnvelope` + `Option<Identity>` — already defined
|
||||
|
||||
**Key design considerations**:
|
||||
- The `SshSession` needs access to the SSH channel's data stream to read/write `EventEnvelope` frames. This requires getting the `russh::Channel` data stream and framing it.
|
||||
- The `ControlChannelRouter` currently uses `Box<dyn DuplexStream>` — it can be wired as a `ControlChannelHandler` that reads frames from the stream and produces `InterfaceEvent`s.
|
||||
- The `alknet-control:0` channel is the first SSH direct-tcpip channel with the `alknet-control` destination. Additional `alknet-*` channels may follow.
|
||||
- The session's `Identity` (from SSH auth) must be attached to every `InterfaceEvent` produced by `recv()`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `SshSession::recv()` reads `EventEnvelope` frames from the SSH channel data stream and produces `InterfaceEvent` with the session's `Identity`
|
||||
- [ ] `SshSession::send()` writes `EventEnvelope` frames to the SSH channel data stream using `call::frame::encode`
|
||||
- [ ] `ControlChannelRouter` is wired as the handler for `alknet-control:0` channels, bridging SSH channel data to the call protocol
|
||||
- [ ] Frame encoding matches `call::frame::{encode, decode}` — 4-byte big-endian length prefix + UTF-8 JSON body
|
||||
- [ ] The session's `Identity` (from `SshHandler::authenticated_identity`) is attached to every `InterfaceEvent` produced by `recv()`
|
||||
- [ ] `SshHandler::channel_open_direct_tcpip` correctly routes `alknet-control:0` channels to the `ControlChannelRouter` handler
|
||||
- [ ] Unit test: `SshSession` can round-trip an `EventEnvelope` through `send()` and `recv()` (using a mock channel stream)
|
||||
- [ ] Unit test: `ControlChannelRouter.with_handler()` successfully routes channel data
|
||||
- [ ] All existing server/auth/transport tests continue to pass
|
||||
- [ ] No behavioral changes for non-`alknet-*` channel forwarding (port proxy logic unchanged)
|
||||
|
||||
## References
|
||||
|
||||
- docs/research/integration-plan.md — Phase 2.1
|
||||
- docs/architecture/interface.md — OQ-IF-01 resolution, InterfaceEvent model
|
||||
- docs/architecture/call-protocol.md — EventEnvelope, frame encoding
|
||||
- crates/alknet-core/src/interface/ssh.rs — SshSession stubs (recv/send)
|
||||
- crates/alknet-core/src/server/control_channel.rs — ControlChannelRouter
|
||||
- crates/alknet-core/src/call/frame.rs — frame encode/decode
|
||||
- crates/alknet-core/src/interface/session.rs — InterfaceEvent, InterfaceSession traits
|
||||
|
||||
## Notes
|
||||
|
||||
> This is the highest-risk task in Phase 2. The `russh` channel data stream API needs careful handling — getting a `Channel`'s data stream for async reading/writing is non-trivial and may require understanding russh's `data()` callback pattern vs. the `Channel::into_stream()` method.
|
||||
|
||||
> Consider implementing incrementally: first wire the `ControlChannelRouter` handler to produce `InterfaceEvent`s from raw channel data, then connect that to `SshSession::recv()`/`send()`. Each step should have passing tests before proceeding.
|
||||
|
||||
> The `SshSession` struct currently holds a `server::Handle` and a `JoinHandle`. It may need additional fields to track the control channel stream and the authenticated identity for producing `InterfaceEvent`s with identity attached.
|
||||
|
||||
## Summary
|
||||
|
||||
> Implemented SshSession recv/send bridge to call protocol via alknet-control:0 channel. Added FrameFramedReader/FrameFramedWriter for async length-prefixed EventEnvelope I/O. SshSession::recv() reads InterfaceEvents from mpsc channel bridged from SSH channel. SshSession::send() writes EventEnvelopes to mpsc channel bridged to SSH channel. ControlChannelBridge implements ControlChannelHandler. SshHandler routes alknet-control:0 channels to bridge task using tokio::select!. Session Identity attached to every InterfaceEvent.
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
id: stream-interface-message-interface-split
|
||||
name: Rename Interface → StreamInterface, add MessageInterface trait, remove TransportKind::Dns, restructure ListenerConfig
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Rename the current `Interface` trait to `StreamInterface`, add a `MessageInterface` trait for request/response interfaces (HTTP, DNS), remove `TransportKind::Dns` from the transport enum (DNS is a `MessageInterface`, not a transport), restructure `ListenerConfig` from a flat struct to the ADR-035 enum form, and align `TransportKind::WebTransport` with the spec. This is the most impactful structural change in Phase 2 because all subsequent tasks reference the new trait names and config types.
|
||||
|
||||
Per ADR-035 and research/phase2/interface-model.md:
|
||||
|
||||
- `Interface` → `StreamInterface` (the current trait becomes the stream-specific variant; persistent byte streams)
|
||||
- `InterfaceSession` stays as-is (it's already stream-specific)
|
||||
- Add `MessageInterface` trait: `handle_request(&self, request: InterfaceRequest) -> Result<InterfaceResponse>` (stateless request/response)
|
||||
- Add `InterfaceRequest` and `InterfaceResponse` types that normalize calls across message interfaces
|
||||
- Add `HttpInterface` stub (struct definition, no route implementations yet)
|
||||
- Add `DnsInterface` stub (struct definition only)
|
||||
- Restructure `InterfaceConfig` into `StreamInterfaceConfig` and `MessageInterfaceConfig`
|
||||
- Restructure `ListenerConfig` from its current flat struct form to the ADR-035 enum with `Stream`, `Http`, and `Dns` variants
|
||||
- **Remove `TransportKind::Dns`** — DNS is a `MessageInterface`, not a transport. This is a residual from Phase 1 that needs cleanup. All code referencing it must be removed: the `ListenerConfig::dns()` constructor, `pairs.rs` valid pairs table, `TransportKindBase::Dns`, and all related tests.
|
||||
- **Update `TransportKind::WebTransport`** field from `{ host: String }` to `{ server_name: Option<String> }` per ADR-035 (tag-only variant, `server_name` is optional)
|
||||
|
||||
**Why this must go first**: Every other Phase 2 task imports and references these types. The rename and new traits must land before SshSession bridge, RawFraming implementation, or HTTP scaffold work begins. The integration plan section 2.3 explicitly states: "This task should be done early in Phase 2 because all subsequent tasks reference the new trait names."
|
||||
|
||||
**Current state of the code** (IMPORTANT — these differ from what the research docs assumed):
|
||||
|
||||
- `Interface` trait in `crates/alknet-core/src/interface/mod.rs` with `accept()` → `Self::Session`
|
||||
- `InterfaceSession` trait in `crates/alknet-core/src/interface/session.rs` with `recv()` / `send()`
|
||||
- `InterfaceConfig` enum with `Ssh(SshInterfaceConfig)` and `RawFraming(RawFramingConfig)` variants
|
||||
- `InterfaceKind` enum with `Ssh` and `RawFraming` variants
|
||||
- `TransportKind` has `Tcp`, `Tls { server_name: Option<String> }`, `Iroh { endpoint_id: String }`, `Dns { domain: String }`, and `WebTransport { host: String }`
|
||||
- **`TransportKind::Dns` exists in the current code** — it must be removed. References exist in `pairs.rs` (`TransportKindBase::Dns`), `serve.rs` (`ListenerConfig::dns()` constructor, Display impl, validate logic), `transport/mod.rs` (Display impl, tests), and `lib.rs` re-exports.
|
||||
- **`TransportKind::WebTransport` exists but has `{ host: String }`** — must be changed to `{ server_name: Option<String> }` per ADR-035.
|
||||
- `ListenerConfig` is currently a **flat struct** (fields: `transport_kind`, `interface_kind`, `listen_addr`, `tls_cert`, `tls_key`, `acme_domain`, `stealth`, `iroh_relay`) with builder-pattern constructors (`tcp()`, `tls()`, `iroh()`, `dns()`, `webtransport()`) — NOT an enum. Must be restructured to the ADR-035 enum form.
|
||||
- `SshInterface` implements `Interface`, `SshSession` implements `InterfaceSession`
|
||||
- `RawFramingInterface`/`RawFramingSession` are stubs (Phase 1 left them)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Interface` trait renamed to `StreamInterface` throughout alknet-core (mod.rs, ssh.rs, raw_framing.rs, and all import sites including `lib.rs`)
|
||||
- [ ] `MessageInterface` trait defined in `crates/alknet-core/src/interface/mod.rs` with `handle_request(&self, request: InterfaceRequest) -> Result<InterfaceResponse>`
|
||||
- [ ] `InterfaceRequest` struct defined with `operation_path`, `input`, `auth_token`, `metadata` fields per interface-model.md
|
||||
- [ ] `InterfaceResponse` struct defined with `result`, `status`, `headers` fields per interface-model.md
|
||||
- [ ] `HttpInterface` stub struct defined (identity_provider, registry, env fields) — no route implementations
|
||||
- [ ] `DnsInterface` stub struct defined (domain, identity_provider, registry, env fields) — no implementation
|
||||
- [ ] `InterfaceConfig` restructured: `StreamInterfaceConfig::Ssh` and `StreamInterfaceConfig::RawFraming` replace current variants; `MessageInterfaceConfig` enum added with `Http` and `Dns` variants
|
||||
- [ ] `ListenerConfig` restructured from flat struct to enum with `Stream { transport: TransportKind, interface: StreamInterfaceKind }`, `Http { config: HttpListenerConfig }`, and `Dns { config: DnsListenerConfig }` variants per ADR-035
|
||||
- [ ] `StreamInterfaceKind` enum defined (corresponding to `StreamInterface` implementors: `Ssh`, `RawFraming`)
|
||||
- [ ] `MessageInterfaceKind` enum defined (corresponding to `MessageInterface` implementors: `Http`, `Dns`)
|
||||
- [ ] `TransportKind::Dns` **removed** from the enum and all references cleaned up: `TransportKindBase::Dns` in pairs.rs, `ListenerConfig::dns()` constructor, Display impls, validate logic, and all related tests
|
||||
- [ ] `TransportKind::WebTransport` field changed from `{ host: String }` to `{ server_name: Option<String> }` and all references updated
|
||||
- [ ] `is_valid_pair()` / `TransportKindBase` validation updated for StreamInterface pairs only (no DNS pairs)
|
||||
- [ ] All existing tests pass after the changes (SshInterface and RawFramingInterface still compile and pass)
|
||||
- [ ] New `StreamInterface` implementors still use `InterfaceSession` for `type Session`
|
||||
- [ ] `MessageInterface` has at least one compilation test (e.g., a mock struct implements it)
|
||||
- [ ] `HttpInterface` and `DnsInterface` stubs compile and exist in the type system
|
||||
- [ ] `lib.rs` re-exports all new types (`StreamInterface`, `MessageInterface`, `InterfaceRequest`, `InterfaceResponse`, `HttpInterface`, `DnsInterface`, `StreamInterfaceConfig`, `MessageInterfaceConfig`, `StreamInterfaceKind`, `MessageInterfaceKind`, `HttpListenerConfig`, `DnsListenerConfig`)
|
||||
- [ ] `HttpListenerConfig` struct defined with `bind_addr: SocketAddr`, `tls: bool`, `stealth: bool`
|
||||
- [ ] `DnsListenerConfig` struct defined with `bind_addr: SocketAddr`, `tls: bool`
|
||||
- [ ] `Display` implementations added for all new enums/structs
|
||||
- [ ] `#[non_exhaustive]` on `ListenerConfig`, `StreamInterfaceKind`, `MessageInterfaceKind`, `MessageInterfaceConfig`
|
||||
- [ ] Serde `Serialize`/`Deserialize` on all new config types
|
||||
- [ ] Updated tests cover the new `ListenerConfig` enum form, the removed `Dns` transport, and the `WebTransport` field rename
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/decisions/035-streaminterface-messageinterface-split.md — ADR-035
|
||||
- docs/research/phase2/interface-model.md — Full design rationale
|
||||
- docs/research/integration-plan.md — Phase 2.3
|
||||
- crates/alknet-core/src/interface/mod.rs — Current Interface trait
|
||||
- crates/alknet-core/src/interface/session.rs — InterfaceSession, InterfaceEvent
|
||||
- crates/alknet-core/src/interface/config.rs — Current InterfaceConfig
|
||||
- crates/alknet-core/src/interface/pairs.rs — Valid transport-interface pairs (has TransportKindBase::Dns to remove)
|
||||
- crates/alknet-core/src/transport/mod.rs — TransportKind enum (has Dns and WebTransport { host } to fix)
|
||||
- crates/alknet-core/src/server/serve.rs — ListenerConfig flat struct (to restructure to enum)
|
||||
- crates/alknet-core/src/lib.rs — Re-exports
|
||||
|
||||
## Notes
|
||||
|
||||
> This is the most mechanically invasive change in Phase 2 due to the rename and the `ListenerConfig` restructuring, but it's low-risk behaviorally. The `Interface` → `StreamInterface` rename is a find-and-replace operation. The new `MessageInterface` trait and stubs are purely additive. The `ListenerConfig` restructuring is the biggest change — it's currently a flat struct with builder-pattern constructors, and needs to become an enum with `Stream`, `Http`, and `Dns` variants per ADR-035. The `Server::new()` code and `StaticConfig::from_serve_options()` code that currently uses the struct form will need to be updated.
|
||||
|
||||
> The `TransportKind::Dns` removal is cleanup from Phase 1 — it was incorrectly added as a transport variant when DNS is actually a `MessageInterface`. All references must be removed: the `TransportKindBase::Dns` in `pairs.rs`, the `ListenerConfig::dns()` constructor in `serve.rs`, the `VALID_TRANSPORT_INTERFACE_PAIRS` table entry, and the Display/test code. The DNS functionality will be represented by `ListenerConfig::Dns { config: DnsListenerConfig }` instead.
|
||||
|
||||
> The `TransportKind::WebTransport` field change from `{ host: String }` to `{ server_name: Option<String> }` aligns with ADR-035. Since no code currently depends on the `host` field specifically (there's no WebTransport acceptor), this is a safe rename.
|
||||
|
||||
> The integration plan section 2.3 notes: "Existing `SshInterface` and `RawFramingInterface` become `StreamInterface` implementations. No behavior change for stream-based interfaces."
|
||||
|
||||
> Consider using `#[non_exhaustive]` on the new enums (`MessageInterfaceConfig`, `ListenerConfig`, `StreamInterfaceKind`, `MessageInterfaceKind`) so future variants (WebSocket, etc.) don't break downstream.
|
||||
|
||||
> When restructuring `ListenerConfig` from struct to enum, the `Server::new()`, `StaticConfig::from_serve_options()`, and `serve.rs` accept loop code will need updates. The `ServeOptions.listeners` field type changes. The `ListenerConfig::tcp()`, `tls()`, `iroh()` constructors should become associated functions that produce the `ListenerConfig::Stream` variant. New constructors for `Http` and `Dns` variants will be added.
|
||||
|
||||
## Summary
|
||||
|
||||
> Renamed Interface → StreamInterface throughout alknet-core. Added MessageInterface trait with InterfaceRequest/InterfaceResponse types. Added HttpInterface and DnsInterface stubs. Restructured InterfaceConfig into StreamInterfaceConfig + MessageInterfaceConfig. Added StreamInterfaceKind and MessageInterfaceKind enums. Restructured ListenerConfig from flat struct to enum with Stream/Http/Dns variants per ADR-035. Removed TransportKind::Dns (DNS is a MessageInterface). Changed TransportKind::WebTransport from { host: String } to { server_name: Option<String> }. Updated all import sites, pairs.rs, serve.rs, server/handler.rs, napi/serve.rs, lib.rs, and all tests. 415 tests pass, clippy clean, fmt clean.
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
id: crypto-test-vectors
|
||||
name: Add BIP39 and SLIP-0010 known-answer test vectors for derivation correctness
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service]
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
A crypto crate needs known-answer tests against published test vectors. The current test suite tests functional behavior (round-trip, determinism, error conditions) but not against published reference vectors. This means we don't know for certain that our BIP39 seed derivation or SLIP-0010 key derivation matches the standard.
|
||||
|
||||
**Required test vectors:**
|
||||
|
||||
### 1. BIP39 Test Vectors
|
||||
|
||||
From https://github.com/trezor/python-mnemonic/blob/master/vectors.json or the BIP39 reference:
|
||||
|
||||
- **Mnemonic → Seed**: Given a known mnemonic phrase and optional passphrase, the derived seed must match the published hex value byte-for-byte.
|
||||
- Test with both 12-word and 24-word mnemonics
|
||||
- Test with and without passphrase
|
||||
- The `bip39` crate may already have these internally; verify with external reference
|
||||
|
||||
### 2. SLIP-0010 Test Vectors
|
||||
|
||||
From https://github.com/satoshilabs/slips/blob/master/slip-0010.md:
|
||||
|
||||
- **Seed → Master Key**: Given a known seed, the HMAC-SHA512 master key must match the published test vector.
|
||||
- **Master Key → Child Key (Ed25519)**: Given the master key and known derivation indices, the derived child key must match the published test vector.
|
||||
- **Path Derivation**: Given a known seed and path `m/74'/0'/0'/0'`, the final key must be deterministic and verified against the chain of individual derivation steps.
|
||||
- SLIP-0010 test vector 1 uses the path `m/0h/1h/2h` with known seed, producing known keys at each step.
|
||||
|
||||
### 3. AES-256-GCM Test Vectors
|
||||
|
||||
From NIST SP 800-38D or equivalent:
|
||||
|
||||
- **Known key + known IV + known plaintext → known ciphertext**: Verify our AES-256-GCM implementation produces the expected ciphertext and tag.
|
||||
- This is more of a sanity check since we use the `aes-gcm` crate which is well-tested, but it ensures our key handling (derived key → AES key) is correct.
|
||||
|
||||
### 4. Cross-consistency Tests
|
||||
|
||||
- **Mnemonic → Seed → Master Key → Derived Key at known path**: End-to-end test that starts with a known mnemonic, derives the seed, then derives keys at known paths, and verifies the result at each step.
|
||||
- **Different mnemonics produce different keys**: Verify that two different mnemonics produce different keys at the same path (no accidental collisions).
|
||||
|
||||
**Implementation:**
|
||||
|
||||
Add a `tests/test_vectors.rs` integration test file. Test vectors are hardcoded hex strings verified against the published references.
|
||||
|
||||
For SLIP-0010 specifically, use the official test vectors from the SLIP-0010 specification. The SLIP-0010 spec provides:
|
||||
- Test vector 1: seed → master key, then child derivations at `m/0h`, `m/0h/1h`, `m/0h/1h/2h`
|
||||
- These use the "ed25519 seed" HMAC key and hardened-only derivation
|
||||
|
||||
For alknet-specific paths (`m/74'/0'/0'/0'`, etc.), generate test vectors once with a known mnemonic and commit the expected results. These serve as regression tests.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `tests/test_vectors.rs` file added with BIP39 known-answer test vectors
|
||||
- [ ] BIP39 test: known mnemonic + known passphrase → known seed (hex comparison)
|
||||
- [ ] BIP39 test: known mnemonic + no passphrase → known seed (hex comparison)
|
||||
- [ ] BIP39 test: different mnemonics produce different seeds
|
||||
- [ ] SLIP-0010 test: known seed → known master key (hex comparison against SLIP-0010 spec vector)
|
||||
- [ ] SLIP-0010 test: master key → derived child key at `m/0h` (matches SLIP-0010 test vector)
|
||||
- [ ] SLIP-0010 test: master key → derived child key at `m/0h/1h/2h` (matches SLIP-0010 test vector)
|
||||
- [ ] AES-256-GCM test: known key + known IV + known plaintext → known ciphertext (NIST vector or equivalent)
|
||||
- [ ] Cross-consistency test: end-to-end mnemonic → seed → derived key at `m/74'/0'/0'/0'`
|
||||
- [ ] All test vectors pass: `cargo test -p alknet-secret --test test_vectors`
|
||||
- [ ] Test vectors reference their source (URL or specification section) in comments
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — Test vectors section (after spec update)
|
||||
- SLIP-0010: https://github.com/satoshilabs/slips/blob/master/slip-0010.md
|
||||
- BIP39: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
|
||||
- BIP39 test vectors: https://github.com/trezor/python-mnemonic/blob/master/vectors.json
|
||||
- AES-256-GCM: NIST SP 800-38D
|
||||
- crates/alknet-secret/src/mnemonic.rs — BIP39 implementation
|
||||
- crates/alknet-secret/src/derivation.rs — SLIP-0010 implementation
|
||||
- crates/alknet-secret/src/encryption.rs — AES-256-GCM implementation
|
||||
|
||||
## Notes
|
||||
|
||||
> The `bip39` and `ed25519-bip32` crates may have their own internal test vectors. Our tests verify our *wrapper* code (Mnemonic, Seed, ExtendedPrivKey) produces the same results. If the underlying crates have known-answer tests, we can reference them but should still have our own integration tests that exercise the full stack.
|
||||
|
||||
> For alknet-specific paths (`m/74'/...`), there are no published test vectors (74' is reserved for alknet). We generate our own "known-answer" vectors from a fixed mnemonic and commit the expected hex values as regression tests.
|
||||
|
||||
> Use `hex` crate (already in dev-dependencies) for encoding/decoding test vector bytes.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
id: derive-password-implementation
|
||||
name: Implement deterministic password derivation for DerivePassword
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service, derivedkey-zeroize-security]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `SecretProtocol::DerivePassword` variant exists in the protocol enum but has no corresponding service method. The spec (after update) defines deterministic password derivation as:
|
||||
|
||||
- **Algorithm**: HMAC-SHA512 at the derivation path `m/74'/1'/0'/{hash}'` where `{hash}'` is a site-specific hardened index
|
||||
- **Output**: Truncate the derived key material to `length` bytes, encode as Base64url (URL-safe Base64 without padding)
|
||||
- **Path format**: `m/74'/1'/0'/{hash}'` — SLIP-0010 hardened-only derivation
|
||||
|
||||
The current `SecretServiceHandle` has methods for `DeriveEd25519`, `DeriveEncryptionKey`, `DeriveEthereumKey`, `Encrypt`, and `Decrypt`, but no `derive_password`.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add `SecretServiceHandle::derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, SecretServiceError>`
|
||||
|
||||
2. The implementation:
|
||||
- Derive a key at the given path using SLIP-0010 (same as Ed25519 derivation)
|
||||
- Take the first `length` bytes of the private key material
|
||||
- Encode as Base64url (no padding) using `base64::engine::general_purpose::URL_SAFE_NO_PAD`
|
||||
- The output is a deterministic password string that can be regenerated from the same seed + path
|
||||
|
||||
**Wait** — there's a design choice here. Should `DerivePassword` return raw bytes (`Vec<u8>`) or an encoded string (`String`)? The spec says the protocol variant returns `Vec<u8>`, but a "password" is typically a string. Let me check the protocol definition more carefully.
|
||||
|
||||
The protocol says:
|
||||
```rust
|
||||
#[rpc(tx=oneshot::Sender<Vec<u8>>)]
|
||||
#[wrap(DerivePassword)]
|
||||
DerivePassword { path: String, length: usize },
|
||||
```
|
||||
|
||||
So the return type is `Vec<u8>`. The encoding to a usable password string should happen at the call site or be a separate method. For the protocol, return raw derived bytes.
|
||||
|
||||
**Resolution**: `derive_password()` returns `Vec<u8>` (raw derived bytes). A convenience method `derive_password_string(path: &str, length: usize) -> Result<String, SecretServiceError>` returns the Base64url-encoded string for use as an actual password. The protocol variant returns `Vec<u8>`.
|
||||
|
||||
3. Add `SecretProtocol::DerivePassword` dispatch to the service actor (irpc task depends on `irpc-secret-protocol-integration`).
|
||||
|
||||
4. The `derive_password` method in `SecretServiceHandle`:
|
||||
```rust
|
||||
pub fn derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, SecretServiceError> {
|
||||
let inner = self.inner.read().unwrap();
|
||||
if !inner.unlocked {
|
||||
return Err(SecretServiceError::ServiceLocked);
|
||||
}
|
||||
let seed = inner.seed.as_ref().ok_or(SecretServiceError::ServiceLocked)?;
|
||||
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
|
||||
// Return first `length` bytes of private key
|
||||
let result = key.private_key()[..length.min(key.private_key().len())].to_vec();
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `SecretServiceHandle::derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, SecretServiceError>` method added
|
||||
- [ ] `derive_password` returns the first `length` bytes of the derived private key at the given path
|
||||
- [ ] `derive_password` requires unlocked state (returns `ServiceLocked` if locked)
|
||||
- [ ] `SecretServiceHandle::derive_password_string(&self, path: &str, length: usize) -> Result<String, SecretServiceError>` convenience method returns Base64url-encoded string
|
||||
- [ ] `derive_password` uses the key cache (if `key-caching-ttl` task is complete)
|
||||
- [ ] Unit test: `derive_password` at a known path returns deterministic bytes
|
||||
- [ ] Unit test: `derive_password` at the same path returns the same bytes (deterministic)
|
||||
- [ ] Unit test: `derive_password` at a different path returns different bytes
|
||||
- [ ] Unit test: `derive_password` length parameter truncates correctly
|
||||
- [ ] Unit test: `derive_password_string` returns valid Base64url (no padding)
|
||||
- [ ] Unit test: `derive_password` returns `ServiceLocked` error when service is locked
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — Key derivation, password derivation path
|
||||
- crates/alknet-secret/src/service.rs — SecretServiceHandle (add derive_password)
|
||||
- crates/alknet-secret/src/protocol.rs — SecretProtocol::DerivePassword variant
|
||||
- crates/alknet-secret/src/derivation.rs — derive_path_from_seed, PATHS
|
||||
|
||||
## Notes
|
||||
|
||||
> The `length` parameter in `DerivePassword` specifies bytes, not characters. Since Ed25519 derived keys are 32 bytes, the maximum useful length is 32. For longer passwords, the spec says to use Base64url encoding of the full 32 bytes, which gives a ~43-character string. Password managers typically want 16-32 byte keys encoded as ~22-43 character strings.
|
||||
|
||||
> The `PATHS::DEVICE_PREFIX` pattern (`m/74'/0'/0'`) allows parameterized device identity. Similarly, `site_password_path(hash)` (`m/74'/1'/0'/{hash}'`) allows site-specific passwords. Both work with the same derivation function — the path is just different.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
id: derivedkey-zeroize-security
|
||||
name: Make DerivedKey private_key Zeroize-derived and fix clone semantics for ADR-038 compliance
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `DerivedKey` struct in `protocol.rs` carries `private_key: Vec<u8>` which is sensitive key material, but it derives `Clone` and `Serialize`/`Deserialize` without any zeroize protection. Per ADR-038, all sensitive material must implement `Zeroize` and be zeroized on drop.
|
||||
|
||||
The current code:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DerivedKey {
|
||||
pub key_type: KeyType,
|
||||
pub private_key: Vec<u8>, // SENSITIVE — must zeroize
|
||||
pub public_key: Vec<u8>, // Not sensitive (public)
|
||||
}
|
||||
```
|
||||
|
||||
Problems:
|
||||
1. `private_key` doesn't derive `Zeroize` — it stays in memory after `DerivedKey` is dropped
|
||||
2. `Clone` copies `private_key` without zeroizing the source — if the clone is dropped, one copy may linger
|
||||
3. The struct is `Serialize` — serializing `private_key` to JSON is a potential leak vector (logging, debug output)
|
||||
|
||||
**Fix approach:**
|
||||
|
||||
- Make `DerivedKey` implement `Zeroize` with `#[zeroize(drop)]`
|
||||
- Replace `#[derive(Clone)]` with a manual `Clone` impl that zeroizes the source's `private_key` after copying (move semantics through clone — the source key is consumed, not left in memory)
|
||||
- OR change the API to return `DerivedKey` by value only (no Clone) — consumers get one copy and must zeroize it when done. This is the more conventional crypto API pattern.
|
||||
- Add `#[serde(skip_serializing)]` or a custom serializer that redacts `private_key` from JSON output (or use a dedicated display format that shows only the public key)
|
||||
- `KeyType` and `public_key` are not sensitive and can remain as-is
|
||||
|
||||
**Important**: This change affects the `SecretServiceHandle` methods that return `DerivedKey`. If `DerivedKey` becomes non-Clone, those methods must return `DerivedKey` by value (which they already do — they construct a new `DerivedKey` each time). The key caching task (which adds a cache) will need to handle this carefully.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `DerivedKey` derives `Zeroize` with `#[zeroize(drop)]` on the `private_key` field
|
||||
- [ ] `DerivedKey` does NOT derive `Clone` — it's a move-only type (consumers must zeroize when done)
|
||||
- [ ] `DerivedKey` serialization redacts `private_key` — JSON output shows a placeholder (e.g., `"[REDACTED]"`) instead of key bytes
|
||||
- [ ] `DerivedKey::zeroize()` overwrites `private_key` with zeros
|
||||
- [ ] `Drop` for `DerivedKey` calls `zeroize()` on the `private_key` field
|
||||
- [ ] Existing `SecretServiceHandle` methods compile without `Clone` (they already return `DerivedKey` by value)
|
||||
- [ ] Unit test: `DerivedKey` zeroes `private_key` on drop
|
||||
- [ ] Unit test: `DerivedKey` serialization does NOT contain `private_key` bytes
|
||||
- [ ] ADR-038 compliance: all types holding private key material derive `Zeroize`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — DerivedKey specification
|
||||
- docs/architecture/decisions/038-seed-lifecycle-memory-security.md — ADR-038
|
||||
- crates/alknet-secret/src/protocol.rs — Current DerivedKey definition
|
||||
- crates/alknet-secret/src/derivation.rs — ExtendedPrivKey (already Zeroize-derived)
|
||||
|
||||
## Notes
|
||||
|
||||
> The `ExtendedPrivKey` in `derivation.rs` already correctly implements `Zeroize` with `#[zeroize(drop)]`. This task brings the same security discipline to `DerivedKey`.
|
||||
|
||||
> Making `DerivedKey` non-Clone is the safer choice. In crypto APIs, returning key material by value and requiring explicit zeroization is the standard pattern. The key cache (in the caching task) will hold derived keys in an internal cache type, not in `DerivedKey` directly.
|
||||
|
||||
> For serialization redaction: consider a custom `Serialize` impl that serializes `private_key` as `"[REDACTED]"` for JSON display but `Deserialize` still reads the full bytes for protocol use. Alternatively, `private_key` can be skipped entirely in serialization (since `DerivedKey` is intended for local use, not wire transfer — the irpc protocol sends `DerivedKey` through postcard, not JSON). The key cache task will need to handle this.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: encryption-salt-kdf
|
||||
name: Document EncryptedData salt as reserved for future KDF-based key derivation
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `EncryptedData` struct has a `salt` field that is generated randomly during encryption but not used in the key derivation process. The current encryption flow is:
|
||||
|
||||
1. Derive key from seed at path `m/74'/2'/0'/0'`
|
||||
2. Use first 32 bytes of derived private key as AES-256-GCM key
|
||||
3. Generate random 12-byte IV
|
||||
4. Generate random 32-byte salt (stored but NOT used in key derivation)
|
||||
5. Encrypt with AES-256-GCM using the derived key + random IV
|
||||
6. Store `{key_version, salt, iv, data}` as `EncryptedData`
|
||||
|
||||
The salt is stored but serves no purpose. The spec update (spec-update-secret-service) resolves this by documenting the salt as reserved for future KDF-based key rotation. In v1, the encryption key is derived directly from the seed at path `m/74'/2'/0'/0'` without a salt-based KDF. HKDF-based key derivation is deferred to Phase B.
|
||||
|
||||
**Decision: Option B — Document salt as reserved.** The spec update has already made this decision. This task implements Option B only.
|
||||
|
||||
## Implementation (Option B only)
|
||||
|
||||
1. Add documentation to `encryption.rs` explaining that the `salt` field in `EncryptedData` is reserved for future KDF-based key derivation (Phase B). In v1, the encryption key is derived directly from the seed at path `m/74'/2'/0'/0'` without using the salt.
|
||||
2. Add a doc comment on the `EncryptedData.salt` field explaining its reserved purpose and that it is not used in v1 key derivation.
|
||||
3. Add a `// TODO(Phase B): Use salt in HKDF-based key derivation` comment on the salt generation in `encrypt()`.
|
||||
4. No code behavior changes — existing tests must pass unchanged.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `encryption.rs` module-level documentation explains that the salt field is reserved for future KDF-based key derivation
|
||||
- [ ] `EncryptedData` struct has doc comment on `salt` field explaining reserved purpose and that it is not used in v1 key derivation
|
||||
- [ ] `// TODO(Phase B)` comment on salt generation in `encrypt()`
|
||||
- [ ] No behavior changes — all existing tests pass unchanged
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — Encryption section (after spec update, which specifies "salt is reserved for future KDF-based key rotation")
|
||||
- crates/alknet-secret/src/encryption.rs — Current encrypt/decrypt implementation
|
||||
|
||||
## Notes
|
||||
|
||||
> The spec update task already decided on Option B. HKDF-based key derivation is deferred to Phase B. This task only documents the salt as reserved and adds TODO comments.
|
||||
|
||||
> The architect's message specifically called out: "The EncryptedData struct has a salt field but the encryption function generates a random salt per encryption without using it for key derivation. Either the salt should be used in a KDF, or the field should be documented as reserved." The spec update chose "document as reserved" for v1.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,153 +0,0 @@
|
||||
---
|
||||
id: irpc-secret-protocol-integration
|
||||
name: Wire SecretProtocol to irpc with local SecretServiceHandle and remote dispatch
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service, key-caching-ttl]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `SecretProtocol` enum in `protocol.rs` currently has a placeholder `SecretMessage = SecretProtocol` type alias. The spec (after update) defines two dispatch paths:
|
||||
|
||||
1. **Local dispatch (in-process)**: `SecretServiceHandle` — async methods that directly call into `SecretServiceInner`. No serialization overhead.
|
||||
2. **Remote dispatch (in-cluster)**: `SecretProtocol` irpc client — sends `SecretMessage` via mpsc (local node) or QUIC stream (remote worker). The service runs on a head node; workers request derived keys via irpc.
|
||||
|
||||
Per ADR-027, irpc is always-on in alknet-secret (not feature-gated). Per ADR-033, irpc is one dispatch backend for OperationEnv.
|
||||
|
||||
### irpc Crate Details
|
||||
|
||||
The `irpc` crate (version 0.16.0 on crates.io) provides the `#[rpc_requests]` derive macro that generates message enums with channel types. This is the same pattern used by n0/iroh projects.
|
||||
|
||||
**Key irpc concepts:**
|
||||
- `#[rpc_requests(message = SecretMessage)]` on the `SecretProtocol` enum generates a `SecretMessage` enum where each variant wraps the inner type in `WithChannels<Inner, SecretProtocol>`
|
||||
- Each variant annotated with `#[rpc(tx=oneshot::Sender<T>)]` gets a `oneshot::Sender<T>` channel for responses
|
||||
- `Client<SecretProtocol>` is the client type that can send messages locally (via mpsc) or remotely (via noq/QUIC)
|
||||
- The `rpc` feature in irpc is enabled by default and includes the remote transport (postcard + noq)
|
||||
- The `derive` feature in irpc enables the `#[rpc_requests]` macro
|
||||
|
||||
**Current pattern in alknet-core for reference:**
|
||||
- `AuthProtocol` in alknet-core (`crates/alknet-core/src/auth/auth_protocol.rs`) is currently a plain enum with synchronous methods on `AuthServiceImpl` — it does NOT use `#[rpc_requests]` yet. The alknet-core irpc feature flag exists but is empty. This is because alknet-core's irpc integration hasn't been implemented yet.
|
||||
- alknet-secret should use the actual `irpc` crate with `#[rpc_requests]` since it's the first crate to do the irpc integration properly.
|
||||
|
||||
**Workspace configuration:**
|
||||
- `irpc = "0.16"` needs to be added to the workspace `Cargo.toml` `[workspace.dependencies]` section
|
||||
- `alknet-secret/Cargo.toml` needs `irpc = { workspace = true }` and `irpc-derive = { workspace = true }` (the derive macro is in a separate crate)
|
||||
|
||||
**irpc dependency requirements:**
|
||||
- `irpc` with default features brings in `noq` (QUIC transport), `postcard` (serialization), and `tokio`. These are acceptable for alknet-secret.
|
||||
- The `derive` feature is needed for `#[rpc_requests]`.
|
||||
|
||||
### What needs to happen
|
||||
|
||||
1. **Add irpc as a workspace dependency**: Add `irpc = "0.16"` and `irpc-derive = "0.16"` to the workspace `Cargo.toml` `[workspace.dependencies]` section. Add `irpc = { workspace = true }` and `irpc-derive = { workspace = true }` to `alknet-secret/Cargo.toml`.
|
||||
|
||||
2. **Replace `SecretMessage` type alias with irpc-generated type**: Apply `#[rpc_requests(message = SecretMessage)]` to `SecretProtocol` with appropriate `#[rpc(tx=oneshot::Sender<T>)]` attributes on each variant. This generates:
|
||||
- `SecretMessage` enum with `WithChannels` wrappers
|
||||
- `Channels<SecretProtocol>` impl for each variant type
|
||||
- `From<VariantType> for SecretProtocol` impls
|
||||
- `Service` and `RemoteService` impls for `SecretProtocol`
|
||||
|
||||
3. **Update SecretProtocol enum for irpc**: The current enum has plain variants like `DeriveEd25519 { path: String }`. With irpc's `#[wrap]` attribute, each variant gets a wrapper struct:
|
||||
```rust
|
||||
#[rpc_requests(message = SecretMessage)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum SecretProtocol {
|
||||
#[rpc(tx=oneshot::Sender<DerivedKey>)]
|
||||
#[wrap(DeriveEd25519)]
|
||||
DeriveEd25519 { path: String },
|
||||
|
||||
#[rpc(tx=oneshot::Sender<DerivedKey>)]
|
||||
#[wrap(DeriveEncryptionKey)]
|
||||
DeriveEncryptionKey { path: String },
|
||||
|
||||
#[rpc(tx=oneshot::Sender<DerivedKey>)]
|
||||
#[wrap(DeriveEthereumKey)]
|
||||
DeriveEthereumKey { path: String },
|
||||
|
||||
#[rpc(tx=oneshot::Sender<Vec<u8>>)]
|
||||
#[wrap(DerivePassword)]
|
||||
DerivePassword { path: String, length: usize },
|
||||
|
||||
#[rpc(tx=oneshot::Sender<EncryptedData>)]
|
||||
#[wrap(Encrypt)]
|
||||
Encrypt { plaintext: String, key_version: u32 },
|
||||
|
||||
#[rpc(tx=oneshot::Sender<String>)]
|
||||
#[wrap(Decrypt)]
|
||||
Decrypt { encrypted: EncryptedData },
|
||||
|
||||
#[rpc(tx=oneshot::Sender<()>)]
|
||||
#[wrap(Lock)]
|
||||
Lock,
|
||||
|
||||
#[rpc(tx=oneshot::Sender<()>)]
|
||||
#[wrap(Unlock)]
|
||||
Unlock { passphrase: String },
|
||||
}
|
||||
```
|
||||
|
||||
4. **Create SecretServiceActor**: Wrap `SecretServiceHandle` in an actor that processes `SecretMessage` variants and sends responses through the oneshot channels. The actor runs as a `tokio::task`:
|
||||
```rust
|
||||
pub struct SecretServiceActor {
|
||||
handle: SecretServiceHandle,
|
||||
}
|
||||
|
||||
impl SecretServiceActor {
|
||||
pub async fn run(mut self, mut rx: tokio::sync::mpsc::Receiver<SecretMessage>) { ... }
|
||||
pub fn handle(&self) -> &SecretServiceHandle { &self.handle }
|
||||
}
|
||||
```
|
||||
|
||||
5. **Keep SecretServiceHandle as primary local API**: The `RwLock<SecretServiceInner>` pattern stays for direct in-process use. The actor wraps it for irpc dispatch.
|
||||
|
||||
6. **Update public API**: Re-export `SecretMessage`, `SecretServiceActor`, and `Client<SecretProtocol>` from `lib.rs`.
|
||||
|
||||
7. **Handle DerivedKey serialization for irpc**: Since `DerivedKey` will become non-Clone (per `derivedkey-zeroize-security` task) and needs custom serialization that redacts `private_key`, ensure the irpc wire format works correctly. The `#[wrap]` structs and `SecretMessage` need to serialize/deserialize `DerivedKey` — since irpc uses `postcard` for remote transport, the `Serialize`/`Deserialize` impls must handle the redacted `private_key` field appropriately. For local (mpsc) transport, `DerivedKey` is sent through oneshot channels without serialization, so the redacted serialization only matters for remote transport.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `irpc` and `irpc-derive` added as workspace dependencies in root `Cargo.toml`
|
||||
- [ ] `irpc` and `irpc-derive` added to `alknet-secret/Cargo.toml` as workspace dependencies
|
||||
- [ ] `SecretProtocol` enum annotated with `#[rpc_requests(message = SecretMessage)]` and `#[rpc(tx=...)]` attributes
|
||||
- [ ] `SecretMessage` is no longer a type alias — it's the irpc-generated message type
|
||||
- [ ] `SecretServiceActor` struct that wraps `SecretServiceHandle` and processes `SecretMessage` variants
|
||||
- [ ] `SecretServiceActor::run()` method that spawns a message loop as a `tokio::task`
|
||||
- [ ] `SecretServiceActor::spawn()` method that returns a `Client<SecretProtocol>` for sending messages
|
||||
- [ ] Each `SecretMessage` variant dispatches to the corresponding `SecretServiceHandle` method and sends response through oneshot channel
|
||||
- [ ] `SecretServiceHandle` remains the primary local API (RwLock-based, unchanged for direct use)
|
||||
- [ ] Unit test: `SecretServiceActor` processes `SecretMessage::Unlock` and responds successfully
|
||||
- [ ] Unit test: `SecretMessage::DeriveEd25519` dispatched through actor returns `DerivedKey`
|
||||
- [ ] Unit test: `SecretMessage::Lock` clears state and subsequent derive calls fail
|
||||
- [ ] `protocol.rs` updated: `SecretMessage` is the irpc-generated message type, not a type alias
|
||||
- [ ] `lib.rs` re-exports updated to include `SecretServiceActor` and `Client<SecretProtocol>`
|
||||
- [ ] `cargo test -p alknet-secret` passes with all existing tests
|
||||
- [ ] `cargo clippy -p alknet-secret -- -D warnings` passes
|
||||
- [ ] `cargo fmt -p alknet-secret -- --check` passes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — irpc service section (after spec update)
|
||||
- docs/architecture/decisions/027-crate-decomposition.md — ADR-027 (irpc always-on in alknet-secret)
|
||||
- docs/architecture/decisions/033-operationenv-irpc-call-protocol.md — ADR-033 (irpc as dispatch backend)
|
||||
- crates/alknet-secret/src/protocol.rs — Current SecretProtocol with placeholder SecretMessage
|
||||
- crates/alknet-secret/src/service.rs — SecretServiceHandle and SecretService
|
||||
- irpc crate (crates.io v0.16) — `#[rpc_requests]` derive macro, `Client<S>` type, `WithChannels`, `Channels` trait
|
||||
- crates/alknet-core/src/auth/auth_protocol.rs — AuthProtocol pattern (reference, but note: NOT using irpc yet)
|
||||
|
||||
## Notes
|
||||
|
||||
> The irpc crate is on crates.io at version 0.16.0. Use `irpc = "0.16"` and `irpc-derive = "0.16"` as workspace dependencies. Do NOT use a local path dependency.
|
||||
|
||||
> The `#[rpc_requests]` macro generates: (1) a `SecretMessage` enum with `WithChannels` wrappers for each variant, (2) `Channels<SecretProtocol>` impls, (3) `From` impls, (4) `Service` and `RemoteService` impls. See the irpc crate docs and examples for the exact generated code structure.
|
||||
|
||||
> The `SecretServiceHandle` with `RwLock` should remain as the primary local API. It's simpler, faster, and works well for single-process use. The `SecretServiceActor` wraps it for irpc dispatch. This two-API pattern matches the spec's "minimal deployment (local handle) vs production deployment (irpc service)" distinction.
|
||||
|
||||
> Since `DerivedKey` is becoming non-Clone with redacted serialization (per `derivedkey-zeroize-security`), the irpc integration needs to handle this. For local (mpsc) transport, `DerivedKey` moves through oneshot channels without serialization — no issue. For remote (postcard) transport, `DerivedKey` needs proper Serialize/Deserialize. The custom serialization should serialize `private_key` as bytes (not redacted) for postcard since it's a binary format used for in-cluster Rust-to-Rust communication — the redaction is for JSON/debug output only.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
id: key-caching-ttl
|
||||
name: Implement TTL-based key cache with LRU eviction for SecretService
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service, derivedkey-zeroize-security]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `SecretServiceHandle` currently re-derives keys from the seed on every call. The spec (after update) requires a TTL-based key cache with LRU eviction, cleared on `Lock`. This is the resolution of OQ-SVC-04.
|
||||
|
||||
The current `SecretServiceInner` holds:
|
||||
- `mnemonic: Option<Mnemonic>`
|
||||
- `seed: Option<Seed>`
|
||||
- `unlocked: bool`
|
||||
|
||||
It needs to add:
|
||||
- `cache: HashMap<String, CachedKey>` where the key is the derivation path string
|
||||
- `cache_ttl: Duration` (default 1 hour, configurable)
|
||||
- LRU eviction when cache exceeds a configurable max size
|
||||
|
||||
**Design considerations:**
|
||||
|
||||
- The cache key is the derivation path string (e.g., `m/74'/0'/0'/0'`). This means caching at the path level — if you derive the same path multiple times, you get the cached key (within TTL).
|
||||
- The cached value must hold the derived key material zeroize-protected, not just the public key.
|
||||
- TTL is checked on access, not via a background timer. Expired entries are evicted lazily.
|
||||
- `Lock` clears the cache entirely and zeroizes all cached entries.
|
||||
- Cache hits avoid re-derivation from seed, which is the main performance win.
|
||||
- The cache must be behind `RwLock` (already used for `SecretServiceInner`).
|
||||
|
||||
**Cache entry structure:**
|
||||
|
||||
```rust
|
||||
#[derive(Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
struct CachedKey {
|
||||
derived_at: Instant,
|
||||
key_type: KeyType,
|
||||
private_key: Vec<u8>, // Zeroize-protected
|
||||
public_key: Vec<u8>,
|
||||
}
|
||||
```
|
||||
|
||||
**Cache configuration:**
|
||||
|
||||
```rust
|
||||
pub struct CacheConfig {
|
||||
pub ttl: Duration, // Default: 1 hour
|
||||
pub max_entries: usize, // Default: 64
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- On `derive_*` call: check cache. If hit and `Instant::now() - derived_at < ttl`, return cached. If expired, evict and re-derive. If miss, derive and insert.
|
||||
- On `Lock`: zeroize all cache entries, clear the HashMap, zeroize seed and mnemonic (existing behavior).
|
||||
- On `Encrypt`/`Decrypt`: the encryption key at `PATHS::ENCRYPTION` is also cached (same path, same behavior).
|
||||
|
||||
**Implementation:**
|
||||
|
||||
Add a `cache` module to `alknet-secret/src/cache.rs` implementing `KeyCache` with `get`, `insert`, `evict_expired`, `clear` (zeroize on clear).
|
||||
|
||||
Wire into `SecretServiceInner` behind the existing `RwLock`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `cache.rs` module added to `alknet-secret/src/` with `KeyCache` struct
|
||||
- [ ] `CachedKey` struct with Zeroize-derived private key, `derived_at: Instant`, `key_type`, `public_key`
|
||||
- [ ] `CacheConfig` struct with `ttl: Duration` (default 1 hour) and `max_entries: usize` (default 64)
|
||||
- [ ] `KeyCache::get(path: &str) -> Option<&CachedKey>` returns cached entry if within TTL
|
||||
- [ ] `KeyCache::insert(path: &str, key: CachedKey)` inserts, evicts LRU if over max_entries
|
||||
- [ ] `KeyCache::evict_expired()` removes entries past TTL, zeroizing them
|
||||
- [ ] `KeyCache::clear()` zeroizes all entries and clears the HashMap
|
||||
- [ ] `SecretServiceInner` gains a `cache: KeyCache` field (behind RwLock)
|
||||
- [ ] `SecretServiceHandle::new()` accepts optional `CacheConfig` (defaults applied)
|
||||
- [ ] `derive_ed25519`, `derive_encryption_key`, `derive_ethereum_key` check cache before re-deriving
|
||||
- [ ] `Lock` clears the cache (zeroizes all cached entries)
|
||||
- [ ] Unit test: cache hit avoids re-derivation
|
||||
- [ ] Unit test: cache miss derives and caches
|
||||
- [ ] Unit test: expired entry is evicted on access and re-derived
|
||||
- [ ] Unit test: LRU eviction when cache exceeds max_entries
|
||||
- [ ] Unit test: Lock clears all cache entries and zeroizes them
|
||||
- [ ] Unit test: encrypt/decrypt uses cached encryption key
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — Key caching subsection (after spec update)
|
||||
- docs/architecture/decisions/038-seed-lifecycle-memory-security.md — Zeroize requirement
|
||||
- OQ-SVC-04 — Resolved: yes, cache with TTL default 1 hour
|
||||
- crates/alknet-secret/src/service.rs — SecretServiceInner (to add cache)
|
||||
- crates/alknet-secret/src/lib.rs — Module re-exports
|
||||
|
||||
## Notes
|
||||
|
||||
> This task depends on `derivedkey-zeroize-security` because `CachedKey` needs the same zeroize discipline that `DerivedKey` gets. If `DerivedKey` becomes non-Clone, `CachedKey` is a separate internal type that holds the same data but is managed within the cache.
|
||||
|
||||
> The LRU implementation can use `std::collections::HashMap` + a doubly-linked list (or `lru` crate for simplicity). Given the max_entries default of 64, even a simple scan-and-evict approach is fine. Prefer `lru` crate for correctness and simplicity.
|
||||
|
||||
> Cache configuration should be exposed through `SecretService::new()` or a `SecretServiceBuilder` pattern, not through `SecretServiceHandle::new()`. The handle wraps an already-configured service.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,104 +0,0 @@
|
||||
---
|
||||
id: review-alknet-secret-spec-conformance
|
||||
name: Review alknet-secret crate for spec conformance and prepare for Phase A integration
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service, derivedkey-zeroize-security, key-caching-ttl, irpc-secret-protocol-integration, derive-password-implementation, secp256k1-ethereum-derivation, encryption-salt-kdf, crypto-test-vectors]
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: phase
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review the complete alknet-secret crate for spec conformance, security properties, and readiness for Phase A integration (connecting to alknet-storage via EncryptedData wire format and alknet-core via OperationEnv/irpc).
|
||||
|
||||
This review covers all tasks in the secret-service task group. It verifies that the implementation matches the updated spec and that the crate is safe and correct for production use.
|
||||
|
||||
### Review Checklist
|
||||
|
||||
1. **Spec conformance**: Does every module, type, and method in the implementation match what the updated `secret-service.md` specifies?
|
||||
- Mnemonic module matches spec (BIP39, Language, Seed, Zeroize)
|
||||
- Derivation module matches spec (SLIP-0010, path constants, ExtendedPrivKey)
|
||||
- Encryption module matches spec (AES-256-GCM, EncryptedData, salt purpose)
|
||||
- Protocol module matches spec (SecretProtocol, SecretMessage, DerivedKey with Zeroize)
|
||||
- Service module matches spec (SecretServiceHandle, SecretServiceActor, lifecycle)
|
||||
- Cache module matches spec (KeyCache, TTL, LRU, cleared on Lock)
|
||||
|
||||
2. **Security properties (ADR-038)**:
|
||||
- All sensitive types derive `Zeroize` with `#[zeroize(drop)]`
|
||||
- `DerivedKey.private_key` is zeroized on drop (not Clone)
|
||||
- `Seed` is zeroized on drop
|
||||
- `Mnemonic` is zeroized on drop
|
||||
- `CachedKey.private_key` is zeroized on drop
|
||||
- `EncryptionKey` is zeroized on drop
|
||||
- `Lock` zeroizes all cached keys and the seed
|
||||
- No private key material leaks through `Debug`, `Display`, or `Serialize`
|
||||
|
||||
3. **irpc integration**:
|
||||
- `SecretMessage` is properly defined with oneshot channels
|
||||
- `SecretServiceActor` processes all message variants
|
||||
- Local use via `SecretServiceHandle` still works without irpc overhead
|
||||
- The actor model doesn't introduce deadlocks or race conditions
|
||||
|
||||
4. **Test vectors**:
|
||||
- BIP39 test vectors pass
|
||||
- SLIP-0010 test vectors pass
|
||||
- AES-256-GCM test vectors pass
|
||||
- Cross-consistency (mnemonic → seed → key) pass
|
||||
|
||||
5. **Feature flag**:
|
||||
- `secp256k1` feature flag works correctly
|
||||
- Without the flag, `derive_ethereum_key()` returns `UnsupportedKeyType`
|
||||
- With the flag, BIP-0032 derivation produces correct secp256k1 keys
|
||||
|
||||
6. **No circular dependencies**:
|
||||
- `alknet-secret` does not depend on `alknet-core` or `alknet-storage`
|
||||
- Check `Cargo.toml` for any accidental dependencies
|
||||
|
||||
7. **Wire format compatibility**:
|
||||
- `EncryptedData` serialization matches the spec (key_version, salt, iv, data — all Base64)
|
||||
- `DerivedKey` serialization redacts `private_key`
|
||||
- `SecretProtocol` variants serialize correctly for irpc (postcard format)
|
||||
|
||||
8. **Public API completeness**:
|
||||
- All spec'd re-exports are present in `lib.rs`
|
||||
- `SecretService`, `SecretServiceHandle`, `SecretServiceActor`, `SecretMessage` all re-exported
|
||||
- `CacheConfig` is available for configuration
|
||||
- Feature-gated types (`Secp256k1` items) are behind the `secp256k1` flag
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All tests pass: `cargo test -p alknet-secret --all-features`
|
||||
- [ ] No compiler warnings: `cargo clippy -p alknet-secret --all-features`
|
||||
- [ ] Formatting is clean: `cargo fmt -p alknet-secret -- --check`
|
||||
- [ ] Every module, type, and method in the implementation matches the updated spec
|
||||
- [ ] All sensitive types implement `Zeroize` with `#[zeroize(drop)]`
|
||||
- [ ] `DerivedKey` is not `Clone` and private_key is zeroized
|
||||
- [ ] `DerivedKey` serialization does not expose `private_key`
|
||||
- [ ] Key cache works: TTL eviction, LRU eviction, cleared on Lock
|
||||
- [ ] `SecretServiceActor` processes all `SecretMessage` variants correctly
|
||||
- [ ] BIP39, SLIP-0010, and AES-256-GCM test vectors pass
|
||||
- [ ] `secp256k1` feature flag gates Ethereum derivation correctly
|
||||
- [ ] `alknet-secret` has no dependency on `alknet-core` or `alknet-storage`
|
||||
- [ ] `EncryptedData` JSON serialization matches the spec format
|
||||
- [ ] `SecretProtocol` / `SecretMessage` types are correctly structured for irpc
|
||||
- [ ] Public API (`lib.rs` re-exports) matches the spec's crate interface section
|
||||
- [ ] Documentation comments on all public items
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — Updated spec
|
||||
- docs/architecture/decisions/038-seed-lifecycle-memory-security.md — ADR-038
|
||||
- docs/architecture/decisions/027-crate-decomposition.md — ADR-027
|
||||
- All implementation tasks in this task group
|
||||
|
||||
## Notes
|
||||
|
||||
> This review should be thorough but should not block on minor documentation phrasing. Focus on: security properties (zeroize), spec conformance (does the code match what the spec says), and integration readiness (can Phase A wire this crate to alknet-core and alknet-storage?).
|
||||
|
||||
> If any deviations between spec and implementation are found, document them and create follow-up tasks rather than fixing them during review.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
id: secp256k1-ethereum-derivation
|
||||
name: Add BIP-0032 secp256k1 derivation for Ethereum keys behind feature flag
|
||||
status: completed
|
||||
depends_on: [spec-update-secret-service, derivedkey-zeroize-security]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `SecretProtocol::DeriveEthereumKey` variant exists in the protocol and `SecretServiceHandle::derive_ethereum_key()` exists in the service, but the current implementation uses the same SLIP-0010 Ed25519 derivation for all key types. This is incorrect.
|
||||
|
||||
BIP-0032 secp256k1 derivation (used for Ethereum at path `m/44'/60'/0'/0/0`) is fundamentally different from SLIP-0010 Ed25519:
|
||||
|
||||
- **SLIP-0010** uses hardened-only derivation with HMAC-SHA512, producing Ed25519 keys
|
||||
- **BIP-0032** supports both hardened and unhardened derivation for secp256k1 (the Ethereum path has unhardened indices: `/0/0` at the end)
|
||||
- The master key derivation algorithm is different (HMAC-SHA512 with "Bitcoin seed" vs "ed25519 seed")
|
||||
- The key format is different (secp256k1 private key + compressed public key)
|
||||
|
||||
The Ethereum path `m/44'/60'/0'/0/0` has **unhardened** indices at positions 4 and 5 (the last two `0`s), which SLIP-0010 does not support (SLIP-0010 requires all indices to be hardened).
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Add the `secp256k1` crate (Rust bindings to libsecp256k1) as an optional dependency behind a `secp256k1` feature flag:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
secp256k1 = ["dep:secp256k1"]
|
||||
|
||||
[dependencies]
|
||||
secp256k1 = { version = "0.29", optional = true }
|
||||
```
|
||||
|
||||
**Note**: The Rust crate is named `secp256k1` on crates.io (it wraps the C library `libsecp256k1`). Do not use `libsecp256k1` — that is the C library name, not the Rust crate name.
|
||||
|
||||
2. Add a `ethereum.rs` module (behind `secp256k1` feature flag) that implements BIP-0032 secp256k1 derivation:
|
||||
- `derive_secp256k1_master_key(seed: &[u8]) -> Result<Secp256k1ExtendedKey, DerivationError>`
|
||||
- `derive_secp256k1_path(seed: &[u8], path: &str]) -> Result<ExtendedPrivKey, DerivationError>`
|
||||
- This uses HMAC-SHA512 with key "Bitcoin seed" (different from SLIP-0010's "ed25519 seed")
|
||||
- Supports both hardened (≥ 0x80000000) and unhardened indices
|
||||
|
||||
3. Update `SecretServiceHandle::derive_ethereum_key()` (behind `secp256k1` feature flag):
|
||||
- When feature is enabled, use BIP-0032 derivation for paths starting with `m/44'`
|
||||
- Return `DerivedKey { key_type: KeyType::Secp256k1, private_key, public_key }` where public_key is compressed (33 bytes)
|
||||
- When feature is NOT enabled, return `SecretServiceError::UnsupportedKeyType`
|
||||
|
||||
4. Update `DerivationError` to include `Secp256k1(String)` and `UnsupportedKeyType` variants.
|
||||
|
||||
5. The `parse_derivation_path` function in `derivation.rs` already supports unhardened indices (it correctly parses `/0/0` at the end). The dispatch to SLIP-0010 vs BIP-0032 should be based on the path's coin type or an explicit parameter, not automatic path detection. Use an explicit method: `derive_ethereum_key()` always uses BIP-0032.
|
||||
|
||||
**Current bug**: The existing `derive_ethereum_key` method calls `derive_path_from_seed` which uses SLIP-0010 (Ed25519 only). The path `m/44'/60'/0'/0/0` has unhardened indices that SLIP-0010 doesn't handle correctly. This must be fixed.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `secp256k1` crate (Rust bindings to libsecp256k1) added behind `secp256k1` feature flag in `Cargo.toml`
|
||||
- [ ] `ethereum.rs` module added (behind `secp256k1` feature flag) with BIP-0032 secp256k1 derivation
|
||||
- [ ] `derive_secp256k1_master_key()` uses HMAC-SHA512 with "Bitcoin seed" key
|
||||
- [ ] `derive_secp256k1_path()` supports both hardened and unhardened indices
|
||||
- [ ] `SecretServiceHandle::derive_ethereum_key()` uses BIP-0032 derivation when `secp256k1` feature is enabled
|
||||
- [ ] `SecretServiceHandle::derive_ethereum_key()` returns `UnsupportedKeyType` error when `secp256k1` feature is disabled
|
||||
- [ ] `SecretServiceHandle::derive_ed25519()` still uses SLIP-0010 (unchanged behavior)
|
||||
- [ ] `SecretServiceHandle::derive_encryption_key()` still uses SLIP-0010 (unchanged behavior)
|
||||
- [ ] `derive_ethereum_key()` returns `DerivedKey { key_type: Secp256k1, private_key: 32-byte-secp256k1-key, public_key: 33-byte-compressed-point }`
|
||||
- [ ] `DerivationError` gains `Secp256k1(String)` and `UnsupportedKeyType` variants
|
||||
- [ ] `lib.rs` conditionally exports `ethereum` module and `Secp256k1`-related types
|
||||
- [ ] Unit test: Ethereum derivation at path `m/44'/60'/0'/0/0` produces a valid secp256k1 keypair (behind `secp256k1` feature)
|
||||
- [ ] Unit test: Ethereum derivation produces different keys from Ed25519 derivation at the same seed
|
||||
- [ ] Unit test: Calling `derive_ethereum_key()` without `secp256k1` feature returns `UnsupportedKeyType`
|
||||
- [ ] Existing Ed25519 and encryption key derivation tests still pass
|
||||
- [ ] BIP-0032 known test vector: known seed → known secp256k1 master key (if available)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — secp256k1 derivation note (after spec update)
|
||||
- crates/alknet-secret/src/derivation.rs — Current SLIP-0010 only derivation
|
||||
- crates/alknet-secret/src/service.rs — derive_ethereum_key (currently uses SLIP-0010, needs fix)
|
||||
- BIP-0032: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
||||
- Ethereum path EIP-84: `m/44'/60'/0'/0/0`
|
||||
|
||||
## Notes
|
||||
|
||||
> This task should be done after `derivedkey-zeroize-security` since `derive_ethereum_key` returns `DerivedKey` which will have the zeroize changes applied.
|
||||
|
||||
> The `secp256k1` crate on crates.io (version 0.29+) provides Rust bindings to the C library `libsecp256k1`. The `ed25519-bip32` crate handles SLIP-0010 Ed25519. These are different algorithms and must not be mixed.
|
||||
|
||||
> For the Ethereum public key: BIP-0032 secp256k1 produces 33-byte compressed public keys. The `public_key` field in `DerivedKey` is `Vec<u8>`, so 33 bytes is fine. Document this size difference from Ed25519 (32-byte public keys).
|
||||
|
||||
> Consider whether `derive_path_from_seed` should be renamed to `derive_ed25519_path_from_seed` for clarity, since it's specifically SLIP-0010 Ed25519. The public API (`derive_ed25519`, `derive_encryption_key`, `derive_ethereum_key`) already makes this distinction, but the internal function name could be clearer.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
id: spec-update-secret-service
|
||||
name: Update secret-service.md spec to close implementation-identified gaps
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Update `docs/architecture/secret-service.md` to address seven gaps identified during the architect's initial implementation of alknet-secret. The tests pass and the code isn't "wrong," but the spec needs to be brought into alignment with what was learned and with what the implementation needs to be complete.
|
||||
|
||||
This is a **spec-first** task — no code changes here, just documentation. The spec must be complete enough that the subsequent implementation tasks can be picked up without ambiguity.
|
||||
|
||||
### Gaps to Close
|
||||
|
||||
1. **irpc integration undefined** (biggest gap): The spec says `SecretProtocol` uses `#[rpc_requests(message = SecretMessage)]` but doesn't define the concrete wiring. How does a local `SecretServiceHandle` relate to the irpc protocol? What's the `Client<SecretProtocol>` type? Where does the service run (in-process mpsc vs remote QUIC)? The current code has `pub type SecretMessage = SecretProtocol;` as a placeholder.
|
||||
|
||||
2. **Key caching strategy**: The security model table says "derived keys cached in RAM" but never specifies: what's the cache key? What's the TTL? Is it LRU? OQ-SVC-04 was resolved "yes, with TTL default 1 hour" but the spec doesn't reflect this resolution. Current implementation has no caching at all.
|
||||
|
||||
3. **`DerivedKey` security properties**: The struct carries `private_key: Vec<u8>` but doesn't derive `Zeroize`. ADR-038 says all sensitive material must zeroize. The spec should specify this.
|
||||
|
||||
4. **`DerivePassword` specification**: The `SecretProtocol::DerivePassword` variant exists but has no specification for how deterministic password derivation works: what hash function? What encoding? What character set?
|
||||
|
||||
5. **secp256k1/Ethereum derivation**: `DeriveEthereumKey` is in the protocol but the spec doesn't acknowledge that BIP-0032 secp256k1 (path `m/44'/60'/0'/0/0`) requires a fundamentally different derivation algorithm and crate than SLIP-0010 Ed25519.
|
||||
|
||||
6. **Test vectors requirement**: A crypto crate needs known-answer tests against published BIP39/SLIP-0010 test vectors. The spec doesn't reference any.
|
||||
|
||||
7. **`EncryptedData.salt` purpose**: The salt field is generated and stored but not used in key derivation. The spec should either specify that the salt is used in a KDF (PBKDF2/HKDF) to derive the AES key, or document it as reserved for future KDF-based key rotation.
|
||||
|
||||
### Changes to Make
|
||||
|
||||
In `docs/architecture/secret-service.md`:
|
||||
|
||||
- **Section: SecretProtocol irpc Service** — Replace the `SecretProtocol` enum definition with one that includes the irpc integration model: define `SecretServiceHandle` (local, in-process) vs `SecretProtocol` irpc client (remote), clarify the two dispatch paths, and specify that `SecretMessage` is the irpc wire type (generated by `#[rpc_requests]`).
|
||||
|
||||
- **Section: Security Model** — Add a "Key Caching" subsection specifying: derivation path as cache key, TTL of 1 hour (configurable), LRU eviction, cache cleared on `Lock`. Per OQ-SVC-04 resolution.
|
||||
|
||||
- **Section: Key Derivation** — Add a note after the `DerivedKey` struct that `private_key` must derive `Zeroize` per ADR-038. This means `DerivedKey` cannot use `#[derive(Clone)]` directly on the private key field; it needs a custom implementation that zeroizes the source on clone.
|
||||
|
||||
- **Section: Key Derivation** (after derivation paths table) — Add a "Password Derivation" subsection specifying: `DerivePassword` uses HMAC-SHA512 at the derivation path, truncates to `length` bytes, and encodes as Base64url (no special character set in v1). The path format is `m/74'/1'/0'/{hash}'` where `{hash}'` is a site-specific hardened index.
|
||||
|
||||
- **Section: Key Derivation** (after Ethereum path) — Add a "secp256k1 Derivation" note: `DeriveEthereumKey` uses BIP-0032 (not SLIP-0010) at path `m/44'/60'/0'/0/0`, requiring the `libsecp256k1` crate. This is a different derivation algorithm from Ed25519. The `alknet-secret` crate should gate this behind a `secp256k1` feature flag.
|
||||
|
||||
- **Section: AES-256-GCM Encryption** — Specify that `EncryptedData.salt` is currently reserved for future KDF-based key rotation. In v1, the encryption key is derived directly from the seed at path `m/74'/2'/0'/0'` without a salt-based KDF. The `salt` field is included for forward compatibility: when key rotation is implemented, the salt will be used as input to HKDF or PBKDF2 for stretch-based key derivation. For now, the salt is random and stored but not used in key derivation.
|
||||
|
||||
- **New Section: Test Vectors** — Require known-answer tests against:
|
||||
- BIP39 test vectors (mnemonic → seed)
|
||||
- SLIP-0010 test vectors (seed → master key, master key → child key at `m/74'/0'/0'/0'`)
|
||||
- IEEE P802.1ASck test vectors for AES-256-GCM (or equivalent published vectors)
|
||||
|
||||
- **Section: Open Questions** — Mark OQ-SVC-04 as resolved (key caching: yes, TTL default 1 hour, LRU eviction). Add note about OQ-SVC-03 (EncryptedData compatibility: wire format stable, migration path is re-encrypt with new key version).
|
||||
|
||||
- **Dependencies** section — Add `libsecp256k1` (behind `secp256k1` feature flag) and `hkdf`/`pbkdf2` (deferred, for Phase B KDF — listed as future dependency, not current). Update `irpc` line to clarify it's required but the integration model needs specification.
|
||||
|
||||
- **Crate Structure** — Update to include `cache.rs` module for key caching.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `secret-service.md` has a new subsection on irpc integration (local SecretServiceHandle vs remote SecretProtocol client, dispatch paths)
|
||||
- [ ] `secret-service.md` has a "Key Caching" subsection specifying derivation path as cache key, 1-hour TTL, LRU eviction, cleared on Lock
|
||||
- [ ] `secret-service.md` notes that `DerivedKey.private_key` must derive `Zeroize` per ADR-038
|
||||
- [ ] `secret-service.md` has a "Password Derivation" subsection specifying HMAC-SHA512 → Base64url encoding
|
||||
- [ ] `secret-service.md` has a "secp256k1 Derivation" note specifying BIP-0032 algorithm and `secp256k1` feature flag
|
||||
- [ ] `secret-service.md` specifies that `EncryptedData.salt` is reserved for future KDF-based key rotation, not used in v1 key derivation
|
||||
- [ ] `secret-service.md` has a "Test Vectors" section requiring BIP39, SLIP-0010, and AES-256-GCM known-answer tests
|
||||
- [ ] OQ-SVC-04 is marked as resolved in `secret-service.md`
|
||||
- [ ] Dependencies section updated with secp256k1 (feature-gated) and noted future KDF deps
|
||||
- [ ] Crate structure diagram updated to include `cache.rs`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — The spec being updated
|
||||
- docs/architecture/decisions/038-seed-lifecycle-memory-security.md — ADR-038 (zeroize requirement)
|
||||
- docs/architecture/decisions/027-crate-decomposition.md — ADR-027 (crate independence)
|
||||
- docs/research/services.md — SecretProtocol definition source
|
||||
- crates/alknet-secret/ — Current implementation (reference for what exists)
|
||||
|
||||
## Notes
|
||||
|
||||
> This is a spec-only task. No code changes. The goal is to make the spec complete enough that subsequent implementation tasks have zero ambiguity about what to build.
|
||||
|
||||
> The architect's message (msg_eacbd63b2001DsTCHZn04meEpB) identified exactly these 7 gaps. This task closes them in the spec so that the implementation tasks can be done against a single source of truth.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
id: unlock-passphrase-gap
|
||||
name: Fix Unlock protocol variant to carry both mnemonic and BIP39 passphrase
|
||||
status: complete
|
||||
depends_on: [irpc-secret-protocol-integration]
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
The `Unlock` variant in `SecretProtocol` currently accepts only a single
|
||||
`passphrase: String` field, which is used as the mnemonic phrase. The
|
||||
`SecretServiceHandle::unlock()` method takes two parameters —
|
||||
`phrase: &str` (the mnemonic) and `passphrase: Option<&str>` (the optional
|
||||
BIP39 password extension) — but the irpc `Unlock` message cannot convey the
|
||||
BIP39 passphrase.
|
||||
|
||||
The `SecretServiceActor::handle_message()` works around this by passing
|
||||
`passphrase` as the mnemonic and `None` for the BIP39 passphrase:
|
||||
|
||||
```rust
|
||||
SecretMessage::Unlock(msg) => {
|
||||
let Unlock { passphrase } = inner;
|
||||
let result = self.handle.unlock(&passphrase, None);
|
||||
```
|
||||
|
||||
This means users who protect their mnemonic with a BIP39 passphrase (the
|
||||
"25th word") cannot unlock the service via irpc. This is a protocol gap
|
||||
that should be fixed before Phase A integration, since the wire format
|
||||
becomes harder to change once consumers depend on it.
|
||||
|
||||
### What needs to happen
|
||||
|
||||
1. **Update `Unlock` variant in `protocol.rs`** to carry both fields:
|
||||
|
||||
```rust
|
||||
#[rpc(tx = oneshot::Sender<Result<(), SecretServiceError>>)]
|
||||
#[wrap(Unlock)]
|
||||
Unlock {
|
||||
/// The BIP39 mnemonic phrase (space-separated word list).
|
||||
mnemonic: String,
|
||||
/// Optional BIP39 passphrase (the "25th word" password extension).
|
||||
passphrase: Option<String>,
|
||||
},
|
||||
```
|
||||
|
||||
2. **Update `SecretServiceActor::handle_message()`** to pass both fields:
|
||||
|
||||
```rust
|
||||
SecretMessage::Unlock(msg) => {
|
||||
let Unlock { mnemonic, passphrase } = inner;
|
||||
let result = self.handle.unlock(&mnemonic, passphrase.as_deref());
|
||||
```
|
||||
|
||||
3. **Update the `Unlock` wrap struct** that `#[wrap]` generates — this is
|
||||
automatic via the `#[wrap(Unlock)]` attribute, so no manual change needed
|
||||
beyond updating the enum variant.
|
||||
|
||||
4. **Update the `unlock_new()` method's irpc path** (if any) — currently
|
||||
`unlock_new()` generates a random mnemonic and returns the phrase. There is
|
||||
no irpc variant for this; it's a local-only operation. No change needed.
|
||||
|
||||
5. **Add a test**: Verify that `Unlock { mnemonic, passphrase: Some("TREZOR") }`
|
||||
produces a different seed than `Unlock { mnemonic, passphrase: None }`.
|
||||
|
||||
6. **Verify backward compatibility**: Since `Unlock` is a new variant field,
|
||||
postcard deserialization of old messages (with only `passphrase`) will fail.
|
||||
This is acceptable because the secret service has not been deployed yet —
|
||||
there are no existing wire consumers. The protocol is still in development.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Unlock` variant in `protocol.rs` has `mnemonic: String` and
|
||||
`passphrase: Option<String>` fields
|
||||
- [ ] `SecretServiceActor::handle_message()` passes both fields to
|
||||
`SecretServiceHandle::unlock()`
|
||||
- [ ] Unit test: Unlock with mnemonic + passphrase produces different seed
|
||||
than Unlock with same mnemonic + no passphrase
|
||||
- [ ] Unit test: Unlock with mnemonic + None passphrase works (backward compat
|
||||
for the common case)
|
||||
- [ ] Unit test: Unlock via `SecretMessage` (irpc actor path) with both fields
|
||||
works correctly
|
||||
- [ ] `cargo test -p alknet-secret --all-features` passes
|
||||
- [ ] `cargo clippy -p alknet-secret --all-features -- -D warnings` passes
|
||||
- [ ] `cargo fmt -p alknet-secret -- --check` passes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/secret-service.md — SecretProtocol section (updated spec)
|
||||
- crates/alknet-secret/src/protocol.rs — `Unlock` variant (current: single field)
|
||||
- crates/alknet-secret/src/service.rs — `SecretServiceHandle::unlock()` and
|
||||
`SecretServiceActor::handle_message()`
|
||||
|
||||
## Notes
|
||||
|
||||
> This is a wire format change. Since alknet-secret is not yet deployed and has
|
||||
> no existing consumers, this is safe to change now. After Phase A integration,
|
||||
> wire format changes would require versioning or migration.
|
||||
|
||||
> The spec already reflects the target state (`Unlock { mnemonic, passphrase }`).
|
||||
> This task brings the implementation in line with the spec.
|
||||
|
||||
> The `Unlock { passphrase: String }` field name was misleading — "passphrase"
|
||||
> sounds like the BIP39 password, but it was actually the mnemonic phrase. The
|
||||
> new field names are unambiguous: `mnemonic` is the word list, `passphrase` is
|
||||
> the optional BIP39 password extension.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
id: meta/auth-layer
|
||||
name: Complete auth layer — error types, key loading, server auth, client auth
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/error-types
|
||||
- auth/key-loading
|
||||
- auth/server-auth-handler
|
||||
- auth/client-auth-handler
|
||||
scope: system
|
||||
risk: medium
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all auth module tasks. Once complete, the auth layer provides key loading from file or memory, server-side Ed25519 key + cert-authority validation, and client-side key-based authentication.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All auth tasks completed
|
||||
- [ ] Key loading supports file paths and in-memory data in OpenSSH format
|
||||
- [ ] Server accepts Ed25519 keys and cert-authority signed certificates
|
||||
- [ ] Client presents Ed25519 key pairs
|
||||
- [ ] Error types cover transport, auth, channel, and config failures
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md, docs/architecture/server.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
id: meta/cli-layer
|
||||
name: Complete CLI layer — alknet serve and alknet connect commands
|
||||
status: completed
|
||||
depends_on:
|
||||
- cli/serve-command
|
||||
- cli/connect-command
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters CLI tasks. Once complete, the `alknet` binary has both `serve` and `connect` subcommands with all flags matching the architecture specs.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Both CLI tasks completed
|
||||
- [x] `alknet serve --help` and `alknet connect --help` match architecture spec flag lists
|
||||
- [x] End-to-end: `alknet serve` + `alknet connect` establishes working SSH tunnel
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md, docs/architecture/server.md
|
||||
|
||||
## Summary
|
||||
|
||||
CLI layer complete. Both `alknet serve` and `alknet connect` subcommands implemented with all architecture spec flags.
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
id: meta/client-layer
|
||||
name: Complete client layer — SOCKS5, port forwarding, channel manager, ConnectOptions
|
||||
status: completed
|
||||
depends_on:
|
||||
- client/socks5-server
|
||||
- client/port-forwarding
|
||||
- client/channel-manager
|
||||
- client/connect-options
|
||||
scope: system
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all client module tasks. Once complete, the client establishes SSH sessions via any transport, runs a local SOCKS5 proxy, manages port forwards, handles reconnection with exponential backoff, and shuts down gracefully.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All client tasks completed
|
||||
- [ ] SOCKS5 proxy works with DNS leak prevention (SOCKS5h)
|
||||
- [ ] Local and remote port forwarding work
|
||||
- [ ] Channel manager handles reconnection with exponential backoff (1s → 30s cap)
|
||||
- [ ] Port forwards re-registered after reconnection
|
||||
- [ ] ConnectOptions programmatic struct and CLI flags available
|
||||
- [ ] Graceful shutdown on SIGTERM/SIGINT
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/client.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
id: meta/napi-layer
|
||||
name: Complete NAPI layer — project setup, connect(), serve()
|
||||
status: completed
|
||||
depends_on:
|
||||
- napi/project-setup
|
||||
- napi/connect-function
|
||||
- napi/serve-function
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters NAPI tasks. Once complete, the `@alkdev/alknet` Node.js native addon provides `connect()` and `serve()` returning duplex streams for TypeScript consumers.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] All NAPI tasks completed
|
||||
- [x] `connect()` returns Duplex stream, no SOCKS5, no port forwarding
|
||||
- [x] `serve()` returns AlknetServer with close() and onConnection events
|
||||
- [x] Key material from Buffer (in-memory) and file paths both work
|
||||
- [x] JS-to-Rust and Rust-to-JS error marshalling works correctly
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md
|
||||
|
||||
## Summary
|
||||
|
||||
NAPI layer complete. connect() returns AlknetStream (read/write/close), serve() returns AlknetServer with close()/onConnection(). Key material works from both file paths and in-memory Buffers. TCP transport fully supported; TLS/iroh return helpful errors in NAPI layer.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
id: meta/server-layer
|
||||
name: Complete server layer — handler, channel proxy, stealth, rate limiting, control channel, serve loop
|
||||
status: completed
|
||||
depends_on:
|
||||
- server/handler
|
||||
- server/channel-proxy
|
||||
- server/stealth-mode
|
||||
- server/rate-limiting-and-logging
|
||||
- server/control-channel
|
||||
- server/serve-loop
|
||||
scope: system
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all server module tasks. Once complete, the server accepts SSH connections via any transport, authenticates clients, proxies channel traffic to TCP targets (directly or via proxy), handles stealth mode, rate limits connections, routes reserved `alknet-` destinations, and shuts down gracefully.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] All server tasks completed
|
||||
- [x] Server handles SSH connections over TCP, TLS, and iroh transports
|
||||
- [x] Authentication via Ed25519 keys and cert-authority
|
||||
- [x] Channel proxying with direct, SOCKS5, and HTTP CONNECT outbound modes
|
||||
- [x] Stealth mode detects SSH vs HTTP and returns fake nginx 404
|
||||
- [x] Rate limiting and structured logging
|
||||
- [x] Control channel routing for `alknet-*` destinations
|
||||
- [x] Graceful shutdown
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md
|
||||
|
||||
## Notes
|
||||
|
||||
All server module tasks completed across Gens 4-7. Server layer is fully implemented.
|
||||
|
||||
## Summary
|
||||
|
||||
Server layer complete: handler (auth + channel dispatch), channel proxy (direct/SOCKS5/HTTP CONNECT), stealth mode (protocol multiplexing), rate limiting (per-IP connection limits), control channel (alknet-* destination routing), serve loop (accept loop + graceful shutdown). All 229 tests pass.
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: meta/transport-layer
|
||||
name: Complete transport layer — trait, TCP, TLS, iroh, ACME
|
||||
status: completed
|
||||
depends_on:
|
||||
- transport/trait-and-types
|
||||
- transport/tcp-transport
|
||||
- transport/tls-transport
|
||||
- transport/iroh-transport
|
||||
- transport/acme-cert-provisioning
|
||||
scope: system
|
||||
risk: high
|
||||
impact: phase
|
||||
level: planning
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Meta task that clusters all transport module tasks. Once complete, the transport layer provides a clean `Transport`/`TransportAcceptor` abstraction with TCP, TLS (feature-gated), iroh (feature-gated), and ACME (feature-gated) implementations. All transports produce the `AsyncRead + AsyncWrite + Unpin + Send` streams that SSH consumes.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All transport tasks completed
|
||||
- [ ] `Transport` trait produces duplex streams consumed by `russh::connect_stream()` / `russh::run_stream()`
|
||||
- [ ] TCP, TLS, iroh transports all work end-to-end
|
||||
- [ ] ACME cert provisioning integrates with TLS acceptor
|
||||
- [ ] Feature flags correctly gate optional transports
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: napi/connect-function
|
||||
name: Implement NAPI connect() — single SSH channel as Duplex stream
|
||||
status: completed
|
||||
depends_on:
|
||||
- napi/project-setup
|
||||
- client/channel-manager
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the NAPI `connect()` function per ADR-007. This is fundamentally different from CLI `alknet connect`:
|
||||
|
||||
- **NAPI `connect()`**: Opens a single SSH channel and returns it as a Node.js `Duplex` stream. No SOCKS5 server, no port forwarding. The caller reads and writes bytes directly.
|
||||
- **CLI `alknet connect`**: Full SSH client session with SOCKS5 server and port forwarding.
|
||||
|
||||
The function accepts `AlknetConnectOptions` and returns `Promise<Duplex>`. The NAPI layer handles transport selection, SSH authentication, and channel setup, then hands the caller a stream.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `#[napi]` function `connect(options: AlknetConnectOptions) -> Result<DuplexStream>` in `crates/alknet-napi/src/connect.rs`
|
||||
- [ ] `AlknetConnectOptions` struct with napi fields: `server`, `peer`, `transport`, `identity`, `tlsServerName`, `insecure`, `irohRelay`, `proxy`
|
||||
- [ ] Transport creation from options (tcp, tls, iroh) — same logic as CLI but programmatic
|
||||
- [ ] SSH client connection: create transport stream, authenticate, open single `direct_tcpip` channel
|
||||
- [ ] Channel returned as `napi::DuplexStream` for JavaScript consumption
|
||||
- [ ] Key material: `identity` field accepts file path (string) or `Buffer` (in-memory data) per ADR-011
|
||||
- [ ] Error marshalling: Rust errors become JavaScript exceptions with descriptive messages
|
||||
- [ ] TypeScript type: `(options: AlknetConnectOptions) => Promise<Duplex>`
|
||||
- [ ] Integration test from JS: connect to a test server, write/receive bytes through stream
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md — NAPI connect() spec, TypeScript interfaces
|
||||
- docs/architecture/decisions/007-napi-single-stream.md — single duplex stream rationale
|
||||
- docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve()
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: napi/project-setup
|
||||
name: Set up alknet-napi project with napi-rs build tooling and TypeScript types
|
||||
status: completed
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Set up the napi-rs project for the `@alkdev/alknet` Node.js native addon. This includes the napi-rs build configuration, TypeScript type definitions, and the package structure.
|
||||
|
||||
Per ADR-015 and ADR-016: napi-rs is the FFI bridge, and the wrapper exposes `connect()` and `serve()` functions. The NAPI layer is transport-agnostic — it doesn't know about pubsub's `EventEnvelope`.
|
||||
|
||||
The Cargo.toml skeleton was created in setup/project-init. This task configures the actual napi-rs build pipeline, TypeScript types, and verifies the build works.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-napi/` has `Cargo.toml` with `crate-type = ["cdylib"]`, `napi` and `napi-derive` dependencies
|
||||
- [ ] `crates/alknet-napi/src/lib.rs` with napi module registration
|
||||
- [ ] `packages/alknet-napi/` directory (or similar) with `package.json` named `@alkdev/alknet`
|
||||
- [ ] `packages/alknet-napi/tsconfig.json` for TypeScript type generation
|
||||
- [ ] TypeScript type definitions for `AlknetConnectOptions`, `AlknetServeOptions`, `AlknetServer`, `ConnectionInfo` matching napi-and-pubsub.md interfaces
|
||||
- [ ] `napi.config.js` or `NapiRs.config` with correct cargo path, module name
|
||||
- [ ] Build command: `npm run build` builds the native addon
|
||||
- [ ] Feature flags: `iroh` feature optional; base package includes tcp + tls
|
||||
- [ ] `npm install` and initial build succeed
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md — NAPI Wrapper section, TypeScript interfaces
|
||||
- docs/architecture/decisions/015-napi-rs-for-ffi-bridge.md — napi-rs choice
|
||||
- docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve()
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: napi/serve-function
|
||||
name: Implement NAPI serve() — server with connection events returning Duplex streams
|
||||
status: completed
|
||||
depends_on:
|
||||
- napi/project-setup
|
||||
- server/serve-loop
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the NAPI `serve()` function per ADR-016. Returns a `AlknetServer` object with a `close()` method and `onConnection` event emitter. Each incoming SSH connection produces a `Duplex` stream.
|
||||
|
||||
The function accepts `AlknetServeOptions` and returns `Promise<AlknetServer>`. The NAPI layer handles transport binding, SSH server setup, and connection handling.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `#[napi]` function `serve(options: AlknetServeOptions) -> Result<AlknetServer>` in `crates/alknet-napi/src/serve.rs`
|
||||
- [x] `AlknetServeOptions` struct with napi fields: `transport`, `hostKey`, `authorizedKeys`, `certAuthority`, `tlsCert`, `tlsKey`, `acmeDomain`, `listen`, `irohRelay`
|
||||
- [x] `AlknetServer` napi class with `close() -> Promise<void>` and `onConnection(callback)` event registration
|
||||
- [x] Each incoming connection produces a `Duplex` stream via the `onConnection` callback
|
||||
- [x] `ConnectionInfo` struct passed with each connection: `remoteAddr`, `transportKind`
|
||||
- [x] Key material: `hostKey`, `authorizedKeys` accept file path (string) or `Buffer` (in-memory)
|
||||
- [x] Server starts transport acceptor, authenticates connections, emits stream events
|
||||
- [x] `close()` triggers graceful shutdown
|
||||
- [x] TypeScript type matches napi-and-pubsub.md spec
|
||||
- [x] Integration test: JS serve() + connect() round-trip works
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/napi-and-pubsub.md — NAPI serve() spec, AlknetServer interface
|
||||
- docs/architecture/decisions/016-napi-expose-connect-and-serve.md — both connect() and serve()
|
||||
- docs/architecture/server.md — server configuration
|
||||
|
||||
## Notes
|
||||
|
||||
TCP transport fully implemented. TLS/iroh transports return helpful "not yet supported" errors. AlknetServerStream provides read/write/close. ConnectionInfo includes remoteAddr and transportKind.
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented NAPI serve() in crates/alknet-napi/src/serve.rs: AlknetServeOptions, AlknetServer with close()/onConnection(), AlknetServerStream (Duplex read/write/close), ConnectionInfo. TCP transport works end-to-end. 241 tests pass, clippy clean.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: review/complete-system
|
||||
name: Review complete system — CLI, NAPI, end-to-end integration
|
||||
status: completed
|
||||
depends_on:
|
||||
- meta/cli-layer
|
||||
- meta/napi-layer
|
||||
- review/server-and-client
|
||||
scope: system
|
||||
risk: low
|
||||
impact: project
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Final review of the complete alknet system. Verify CLI binary works end-to-end, NAPI wrapper provides correct JavaScript API, and both layers properly wrap the core library.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `alknet serve` + `alknet connect` end-to-end: SSH tunnel established, SOCKS5 proxy routes traffic
|
||||
- [x] All CLI flags work: transport modes (tcp, tls, iroh), auth options, proxy, stealth, rate limits
|
||||
- [x] Environment variables (`ALKNET_SERVER`, `ALKNET_IDENTITY`) work as defaults
|
||||
- [x] `--stealth` validates `--transport tls` requirement
|
||||
- [x] NAPI `connect()` returns Duplex stream; data flows bidirectionally
|
||||
- [x] NAPI `serve()` accepts connections; `onConnection` emits Duplex streams
|
||||
- [x] NAPI key material from Buffer works (not just file paths)
|
||||
- [x] Feature flags: `tls`, `iroh`, `acme` correctly gate optional functionality
|
||||
- [x] Base build (`cargo build -p alknet-core` with no features) compiles and works
|
||||
- [x] All tests pass: `cargo test --workspace`
|
||||
- [x] NAPI tests pass: `cd crates/alknet-napi && npm test`
|
||||
- [x] `cargo clippy --workspace` passes
|
||||
- [x] No logging of tunnel destinations anywhere in the system
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/overview.md, docs/architecture/napi-and-pubsub.md
|
||||
|
||||
## Summary
|
||||
|
||||
Final review complete. All acceptance criteria verified:
|
||||
- CLI binary: alknet serve/connect with all flags, env vars, stealth validation
|
||||
- NAPI: connect() returns AlknetStream, serve() returns AlknetServer with onConnection
|
||||
- Feature flags: tls, iroh, acme correctly gate optional code; base build compiles
|
||||
- ADR-006: no server-side logging of tunnel destinations
|
||||
- 241 tests pass, clippy clean with all features
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
id: review/core-foundation
|
||||
name: Review core foundation — transport traits, auth, error types, key loading
|
||||
status: completed
|
||||
depends_on:
|
||||
- meta/transport-layer
|
||||
- meta/auth-layer
|
||||
- setup/test-infrastructure
|
||||
scope: broad
|
||||
risk: low
|
||||
impact: phase
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review the core foundation layer before proceeding to server/client implementation. Verify that transport abstractions match architecture, auth logic is correct, errors follow the layered pattern, and key loading handles all spec'd formats.
|
||||
|
||||
This is the critical review before building the higher-level server and client components on top of these foundations.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Transport trait matches transport.md: correct bounds, object-safety, describe() method
|
||||
- [ ] TransportAcceptor matches transport.md: returns TransportInfo with correct metadata
|
||||
- [ ] TCP, TLS, iroh transports all produce correct stream types per implementations table
|
||||
- [ ] ACME integration with TLS works (or feature gates correctly prevent compilation without it)
|
||||
- [ ] Key loading handles file paths and in-memory data, rejects PEM format
|
||||
- [ ] authorized_keys parsing handles cert-authority entries with options
|
||||
- [ ] Server auth: Ed25519 key matching (constant-time), cert-authority validation (signature, expiry, principal)
|
||||
- [ ] Client auth: key pair presentation, Handler implementation
|
||||
- [ ] Error types cover all four layers (transport, auth, channel, config)
|
||||
- [ ] All tests pass: `cargo test --workspace`
|
||||
- [ ] `cargo clippy --workspace` passes with no warnings
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md, docs/architecture/client.md, docs/architecture/server.md
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
id: review/phase1-core-modifications
|
||||
name: Review Phase 1 core modifications — config split, identity, forwarding, OperationEnv, interface abstraction
|
||||
status: completed
|
||||
depends_on:
|
||||
- core/ssh-interface-extraction
|
||||
- core/operationenv-local-dispatch
|
||||
- core/auth-service-irpc
|
||||
- core/config-service-irpc
|
||||
- core/napi-reload-api
|
||||
scope: broad
|
||||
risk: medium
|
||||
impact: project
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review the Phase 1 core modifications after all implementation tasks are complete. This is a quality checkpoint before Phase 2 (external crates) and Phase 3 (integration/wiring).
|
||||
|
||||
**Review checklist**:
|
||||
1. All ADRs (026-034) are correctly reflected in implementation
|
||||
2. Crate dependencies are acyclic — core doesn't depend on secret, storage, or flowgraph
|
||||
3. Terminology is consistent — head/worker everywhere, no hub/spoke remaining
|
||||
4. Layer boundaries are clean — Interface produces call protocol events, Protocol is agnostic
|
||||
5. `IdentityProvider` trait is the sole contract for auth — no direct `ServerAuthConfig` usage remains
|
||||
6. `DynamicConfig` + ArcSwap provides hot-reload for auth and forwarding
|
||||
7. `ForwardingPolicy` default-allow preserves current behavior
|
||||
8. OperationEnv local dispatch works correctly through the registry
|
||||
9. Feature flags (`irpc`) compile correctly — core without feature flag has no irpc dependency
|
||||
10. All existing tests pass
|
||||
11. New test coverage for config reload, identity resolution, forwarding policy
|
||||
12. NAPI reload API functions correctly
|
||||
13. Interface trait and SshInterface extraction don't break SSH tunnel functionality
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Code adheres to architecture specs (configuration.md, identity.md, interface.md, call-protocol.md, services.md)
|
||||
- [ ] Patterns are consistent (IdentityProvider, DynamicConfig/ArcSwap, OperationEnv, Interface trait)
|
||||
- [ ] Tests cover core functionality: config hot-reload, identity resolution, forwarding policy evaluation, local dispatch
|
||||
- [ ] No cargo build errors or warnings
|
||||
- [ ] All feature flag combinations compile: default, irpc, tls, iroh, acme
|
||||
- [ ] Documentation comments reference ADR numbers
|
||||
- [ ] Phase 1 implementation notes are filled in on all task files
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/overview.md
|
||||
- docs/architecture/configuration.md
|
||||
- docs/architecture/identity.md
|
||||
- docs/architecture/interface.md
|
||||
- docs/architecture/call-protocol.md
|
||||
- docs/architecture/services.md
|
||||
- docs/architecture/decisions/ (ADR-026 through ADR-034)
|
||||
|
||||
## Notes
|
||||
|
||||
> Review completed by code-reviewer agent. All 13 checklist items passed. 5 warnings identified (W1: NAPI bypasses IdentityProvider = functional bug with cleanup task created; W2: task notes unfilled = by design; W3: SshSession::recv() stub = documented as future work with cleanup task; W4: ServerAuthConfig bridge = acceptable transition debt; W5: panic/unwrap in StaticConfig = code smell with cleanup task created). 4 non-blocking suggestions also generated cleanup tasks.
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 1 core modifications approved with minor changes recommended. 385 tests pass, all feature flags compile, clippy clean. All ADRs 026-034 correctly reflected. Main concern: NAPI handler bypasses IdentityProvider/ForwardingPolicy (cleanup task created). 5 cleanup tasks created to address warnings and suggestions before proceeding to Phase 2.
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: review/server-and-client
|
||||
name: Review server and client implementation — full SSH tunnel functionality
|
||||
status: completed
|
||||
depends_on:
|
||||
- meta/server-layer
|
||||
- meta/client-layer
|
||||
- review/core-foundation
|
||||
scope: broad
|
||||
risk: low
|
||||
impact: phase
|
||||
level: review
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Review the server and client implementations after the core foundation review. This is a critical checkpoint before the CLI and NAPI layers — the server and client must work correctly as a unit before wrapping them in CLI flags or NAPI bindings.
|
||||
|
||||
Verify end-to-end SSH tunnel flow: client connects → SOCKS5 proxy works → port forwards work → reconnection works → server handles channels → proxy modes work → stealth mode works.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Server accepts SSH connections over TCP, TLS, iroh (via integration tests)
|
||||
- [x] Client establishes SSH sessions and runs SOCKS5 proxy
|
||||
- [x] Channel proxy: direct TCP, SOCKS5 proxy, HTTP CONNECT proxy all work
|
||||
- [x] Stealth mode: non-SSH gets nginx 404, SSH connects normally
|
||||
- [x] Rate limiting: connection limits enforced, auth attempt limits enforced
|
||||
- [x] Logging: structured `tracing::info!` events match ADR-013 format
|
||||
- [x] No logging of tunnel destinations (ADR-006)
|
||||
- [x] Reconnection: transport failure → exponential backoff → reconnect → port forwards re-registered
|
||||
- [x] Reserved `alknet-` destinations routed to control channel, not TCP proxy
|
||||
- [x] Graceful shutdown works for both server and client
|
||||
- [x] All tests pass: `cargo test --workspace`
|
||||
- [x] `cargo clippy --workspace` passes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md, docs/architecture/client.md
|
||||
|
||||
## Summary
|
||||
|
||||
Server and client review passed with fixes. Key issues found and resolved:
|
||||
- wired channel proxy into handler (was dropping all non-alknet channels)
|
||||
- added client reconnection with exponential backoff + remote forward re-registration
|
||||
- fixed ADR-006 violations (removed server-side destination logging)
|
||||
- 241 tests pass, clippy clean
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
id: server/channel-proxy
|
||||
name: Implement server channel proxy — direct TCP and outbound proxy connections
|
||||
status: completed
|
||||
depends_on:
|
||||
- server/handler
|
||||
- auth/error-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the server's channel proxy logic that makes outbound TCP connections on behalf of SSH clients. When `channel_open_direct_tcpip(host, port)` is called for a non-reserved destination:
|
||||
|
||||
1. Connect to `host:port`, either directly or via the configured outbound proxy
|
||||
2. Run `tokio::io::copy_bidirectional` between the SSH channel stream and the outbound TCP stream
|
||||
3. Clean up when either side disconnects
|
||||
|
||||
Supports three outbound proxy modes per server.md: Direct, SOCKS5 proxy, HTTP CONNECT proxy.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/server/channel_proxy.rs` exports channel proxy functions
|
||||
- [ ] `ProxyConfig` enum: `Direct`, `Socks5 { addr: SocketAddr }`, `HttpConnect { addr: SocketAddr }`
|
||||
- [ ] `connect_outbound(target: SocketAddr, proxy: &ProxyConfig) -> Result<TcpStream>` — connects to target directly or via proxy
|
||||
- [ ] Direct mode: `TcpStream::connect(target)`
|
||||
- [ ] SOCKS5 proxy: establishes SOCKS5 handshake, sends CONNECT command for target
|
||||
- [ ] HTTP CONNECT proxy: sends `CONNECT host:port HTTP/1.1` to proxy, reads 200 response
|
||||
- [ ] `proxy_channel(channel: ChannelStream, target: SocketAddr, proxy: &ProxyConfig)` — spawns bidirectional copy task
|
||||
- [ ] Channel errors (target unreachable, proxy failure) close that channel without affecting SSH session
|
||||
- [ ] No logging of tunnel destinations (ADR-006) — only transport/auth events are logged
|
||||
- [ ] Unit tests: direct connection proxy, SOCKS5 proxy handshake, HTTP CONNECT proxy handshake, target unreachable handling
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Channel Handling, Outbound Proxy Modes sections
|
||||
- docs/architecture/decisions/006-no-logging-of-tunnel-destinations.md — no destination logging
|
||||
- docs/architecture/decisions/019-proxy-dual-semantics.md — server `--proxy` meaning
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: server/control-channel
|
||||
name: Implement alknet-control reserved channel for pubsub event bus bridging (ADR-018)
|
||||
status: completed
|
||||
depends_on:
|
||||
- server/handler
|
||||
- auth/error-types
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the control channel routing per ADR-018. When the server receives a `channel_open_direct_tcpip` request for `alknet-control:0`:
|
||||
|
||||
1. The handler detects the reserved `alknet-` prefix destination
|
||||
2. Instead of making a TCP connection, it bridges the SSH channel to an internal event bus handle
|
||||
3. `EventEnvelope` JSON flows bidirectionally over the SSH channel
|
||||
|
||||
The entire `alknet-` prefix is reserved — no TCP connections should be attempted for `alknet-*` destinations. The control channel is optional; servers without pubsub configured should accept the channel and provide a configurable behavior (reject or provide a loopback pipe).
|
||||
|
||||
At this stage, implement the routing logic and a `ControlChannel` trait that consumers can implement. The actual pubsub bridge implementation would be in a separate crate or behind a feature flag.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/server/control_channel.rs` exports `ControlChannelHandler` trait and routing logic
|
||||
- [ ] `ALKNET_CONTROL_DESTINATION` constant defined as `"alknet-control"` (ADR-018)
|
||||
- [ ] `ALKNET_PREFIX` constant defined as `"alknet-"` for namespace reservation
|
||||
- [ ] `ControlChannelHandler` trait: `async fn handle_channel(stream: Box<dyn AsyncRead + AsyncWrite + Unpin + Send>)`
|
||||
- [ ] Server handler detects `alknet-*` prefix and routes to `ControlChannelHandler` instead of TCP proxy
|
||||
- [ ] If no `ControlChannelHandler` configured, reject the channel open request (SSH channel open failure)
|
||||
- [ ] Non-reserved destinations continue through normal TCP proxy path
|
||||
- [ ] Server constraint enforced: no TCP connections to `alknet-*` destinations
|
||||
- [ ] Unit tests: reserved destination detected, non-reserved passes through, prefix matching works
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Channel Handling section (reserved destinations), Constraints section
|
||||
- docs/architecture/decisions/018-control-channel-for-pubsub.md — control channel rationale
|
||||
- docs/architecture/napi-and-pubsub.md — server-side control channel behavior
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
id: server/handler
|
||||
name: Implement ServerHandler — russh server handler with auth and channel dispatch
|
||||
status: completed
|
||||
depends_on:
|
||||
- auth/server-auth-handler
|
||||
- transport/trait-and-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the core `ServerHandler` that implements `russh::server::Handler`. This is the heart of the server. Per server.md, it has two primary responsibilities:
|
||||
|
||||
1. **`auth_publickey()`**: Delegated to `ServerAuthConfig` — checks key against authorized set or validates cert-authority
|
||||
2. **`channel_open_direct_tcpip()`**: Routes the channel — either to a TCP target (directly or via proxy) or internally for reserved `alknet-*` destinations (ADR-018)
|
||||
|
||||
At this stage, implement the handler struct, auth delegation, and the channel dispatch skeleton (actual TCP connection and proxy logic in dependent tasks).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/server/handler.rs` exports `ServerHandler`
|
||||
- [ ] `ServerHandler` implements `russh::server::Handler`
|
||||
- [ ] `ServerHandler` holds: `Arc<ServerAuthConfig>`, `outbound_proxy: Option<ProxyConfig>`, `remote_addr: Option<SocketAddr>`
|
||||
- [ ] `auth_publickey()` delegates to `ServerAuthConfig` and returns `Accept` or `Reject`
|
||||
- [ ] `channel_open_direct_tcpip()` dispatches: if `host.starts_with("alknet-")`, route to internal handler (stub for control channel); otherwise, spawn TCP proxy task (stub that logs and returns error for now)
|
||||
- [ ] One `ServerHandler` instance per connection; state is not shared between connections (unless explicitly Arc'd)
|
||||
- [ ] Structured auth logging via `tracing::info!` with `remote_addr`, `key_fingerprint`, `result` (ADR-013)
|
||||
- [ ] Unit tests: auth delegation works, reserved destination routing logic, unknown channel types rejected
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Server Handler Behavior section, channel handling
|
||||
- docs/architecture/decisions/018-control-channel-for-pubsub.md — reserved `alknet-*` destinations
|
||||
- docs/architecture/decisions/013-fail2ban-friendly-logging.md — structured auth logging
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: server/rate-limiting-and-logging
|
||||
name: Implement server rate limiting and fail2ban-friendly structured logging
|
||||
status: completed
|
||||
depends_on:
|
||||
- server/handler
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the two-layer abuse protection per ADR-013:
|
||||
|
||||
1. **Structured logging** at INFO level for fail2ban integration: auth attempts (remote_addr, user, key_fingerprint, accept/reject), connection opened/closed (remote_addr, transport, duration)
|
||||
2. **Built-in rate limiting**: `--max-connections-per-ip` (reject new connections from IPs with N active connections), `--max-auth-attempts` (disconnect after N failed auth attempts per connection)
|
||||
|
||||
No logging of tunnel destinations, DNS resolutions, or bytes transferred (ADR-006).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/server/rate_limit.rs` exports connection rate limiter
|
||||
- [ ] `ConnectionRateLimiter` tracks active connections per IP using `HashMap<IpAddr, usize>`
|
||||
- [ ] `ConnectionRateLimiter::check(ip) -> bool` — returns `true` if connection allowed, `false` if over limit
|
||||
- [ ] `ConnectionRateLimiter::on_connect(ip)` — increment counter
|
||||
- [ ] `ConnectionRateLimiter::on_disconnect(ip)` — decrement counter
|
||||
- [ ] `AuthAttemptLimiter` tracks failed auth attempts per connection
|
||||
- [ ] `AuthAttemptLimiter::check() -> bool` — returns `true` if under limit
|
||||
- [ ] `AuthAttemptLimiter::on_failure()` — increment failure counter
|
||||
- [ ] Structured `tracing::info!` logging on: auth attempt, connection opened, connection closed
|
||||
- [ ] Log format includes key-value pairs: `remote_addr`, `user`, `key_fingerprint`, `result`, `transport`, `duration`
|
||||
- [ ] No logging of: channel open targets, DNS resolutions, bytes transferred
|
||||
- [ ] Integration with `ServerHandler`: rate limiter checked before auth, auth attempt limiter checked during auth
|
||||
- [ ] Unit tests: connection limit enforced, auth attempt limit enforced, log format verification
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Logging and Rate Limiting section
|
||||
- docs/architecture/decisions/013-fail2ban-friendly-logging.md — logging format, rate limiting flags
|
||||
- docs/architecture/decisions/006-no-logging-of-tunnel-destinations.md — no destination logging
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
id: server/serve-loop
|
||||
name: Implement server accept loop, graceful shutdown, and ServeOptions config
|
||||
status: completed
|
||||
depends_on:
|
||||
- server/handler
|
||||
- server/channel-proxy
|
||||
- server/rate-limiting-and-logging
|
||||
- transport/trait-and-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the server's main accept loop and configuration. This ties together the transport acceptor, server handler, rate limiting, and logging into a coherent server process.
|
||||
|
||||
`ServeOptions` is the programmatic configuration struct (ADR-011) for the server. The accept loop:
|
||||
1. Binds a `TransportAcceptor` based on transport mode
|
||||
2. Accepts incoming connections (respecting rate limits)
|
||||
3. Creates a `ServerHandler` per connection
|
||||
4. Passes the stream to `russh::server::run_stream()`
|
||||
5. Handles graceful shutdown on SIGTERM/SIGINT
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] `crates/alknet-core/src/server/mod.rs` re-exports all server components
|
||||
- [x] `ServeOptions` struct with fields matching server.md CLI interface: `key`, `authorized_keys`, `cert_authority`, `transport_mode`, `listen_addr`, `tls_cert`, `tls_key`, `acme_domain`, `stealth`, `proxy`, `iroh_relay`, `max_connections_per_ip`, `max_auth_attempts`
|
||||
- [x] `Server::new(opts: ServeOptions) -> Result<Server>` — creates server with bound acceptor, auth config, rate limiter
|
||||
- [x] `Server::run()` — enters accept loop, for each connection: check rate limit → create handler → `run_stream()`
|
||||
- [x] Stealth mode integration: if enabled, protocol detection before `run_stream()`
|
||||
- [x] Graceful shutdown: `Server::shutdown()` method and signal handler (SIGTERM/SIGINT)
|
||||
- Stop accepting new connections
|
||||
- Send SSH disconnect to active sessions
|
||||
- Wait for drain timeout (~2 seconds per session)
|
||||
- Forcibly terminate remaining connections
|
||||
- [x] iroh mode: prints endpoint ID on startup
|
||||
- [x] `ServeOptions::key` and `ServeOptions::authorized_keys` accept `KeySource` (file or in-memory)
|
||||
- [x] Integration test: start server, client connects via mock transport, session works, shutdown completes
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — full server spec including graceful shutdown
|
||||
- docs/architecture/decisions/011-no-ssh-config-programmatic-api.md — ServeOptions programmatic struct
|
||||
|
||||
## Notes
|
||||
|
||||
Key design decisions:
|
||||
- `Server::run(acceptor, endpoint_info)` takes a generic `TransportAcceptor` and optional endpoint info string, keeping transport binding separate from the accept loop
|
||||
- `handle_disconnect` returns a future (`Handle::disconnect` is async in russh 0.49), takes `String` args
|
||||
- `shutdown_rx` is cloned to avoid needing `&mut self` on `Arc<Server>` in the select loop
|
||||
- `ServeTransportMode` is a separate enum from `TransportKind` to keep serve options independent of transport types
|
||||
- Stealth mode only applies when both `stealth=true` AND `transport_mode=Tls`
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented server accept loop and configuration in `crates/alknet-core/src/server/serve.rs`:
|
||||
- `ServeOptions` struct with all CLI interface fields, builder pattern, KeySource support
|
||||
- `Server::new()` creates server with russh config, auth config, rate limiter
|
||||
- `Server::run(acceptor, endpoint_info)` enters accept loop with rate limiting, stealth detection, russh::server::run_stream()
|
||||
- `Server::shutdown()` sends SSH disconnect to active sessions, waits drain timeout, aborts remaining
|
||||
- SIGTERM/SIGINT handler on unix platforms
|
||||
- iroh endpoint ID logged on startup
|
||||
- All 216 tests pass, clippy clean
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
id: server/stealth-mode
|
||||
name: Implement stealth mode — protocol multiplexing on port 443 (ADR-017)
|
||||
status: completed
|
||||
depends_on:
|
||||
- transport/tls-transport
|
||||
- server/handler
|
||||
scope: narrow
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement stealth mode per ADR-017. When `--stealth` is enabled alongside TLS transport on port 443:
|
||||
|
||||
1. After completing the TLS handshake, peek at the first bytes of the connection
|
||||
2. If the connection starts with `SSH-2.0-`, proceed with `russh::server::run_stream()`
|
||||
3. If the connection starts with anything else (HTTP, random data), respond with `HTTP/1.1 404 Not Found\r\nServer: nginx\r\n\r\n` and close
|
||||
|
||||
This makes the server appear as an nginx web server returning 404 errors to non-SSH connections, making it indistinguishable from a regular HTTPS site to port scanners and DPI systems.
|
||||
|
||||
Stealth mode requires TLS transport. The CLI should reject or warn if `--stealth` is used without `--transport tls`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/server/stealth.rs` exports stealth mode protocol detection
|
||||
- [ ] `detect_protocol(stream: TlsStream) -> ProtocolDetection` — peeks at first bytes to determine SSH vs HTTP
|
||||
- [ ] `ProtocolDetection` enum: `Ssh`, `Http` (or `Unknown`)
|
||||
- [ ] If SSH detected: pass stream to `russh::server::run_stream()`
|
||||
- [ ] If HTTP/unknown detected: write `HTTP/1.1 404 Not Found\r\nServer: nginx\r\n\r\n` then close
|
||||
- [ ] Peek uses `tokio::io::BufReader` or similar buffered read to avoid consuming the SSH banner bytes
|
||||
- [ ] Integration with `TlsAcceptor` flow: after accept + TLS handshake, optionally run protocol detection before passing to russh
|
||||
- [ ] Stealth mode flag validated: requires TLS transport, warn/reject otherwise
|
||||
- [ ] Unit tests: SSH banner detection, HTTP request detection, random data → fake nginx 404
|
||||
- [ ] Integration test: stealth server responds to HTTP scanner with 404, SSH client connects successfully
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — Stealth Mode section
|
||||
- docs/architecture/decisions/017-stealth-mode-protocol-multiplexing.md — protocol multiplexing design
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
id: setup/project-init
|
||||
name: Initialize Cargo workspace with alknet, alknet-core, and alknet-napi crates
|
||||
status: completed
|
||||
depends_on: []
|
||||
scope: moderate
|
||||
risk: low
|
||||
impact: project
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Set up the Rust workspace from scratch. The repo currently has only `docs/` and `.git/`. Initialize a Cargo workspace with three crate directories following the architecture spec:
|
||||
|
||||
- **`alknet-core`** — library crate with feature flags (`tls`, `iroh`, `acme`). All core logic lives here.
|
||||
- **`alknet`** — binary crate depending on `alknet-core`. CLI entry point.
|
||||
- **`alknet-napi`** — napi-rs crate for the Node.js native addon (skeleton only at this stage).
|
||||
|
||||
Per overview.md: `russh`, `tokio`, `clap`, `tracing`, `anyhow`/`thiserror` are core dependencies. `tokio-rustls`, `rustls`, `rustls-acme`, `iroh` are feature-gated.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Cargo.toml` workspace root with `[workspace]` members: `crates/alknet-core`, `crates/alknet`, `crates/alknet-napi`
|
||||
- [ ] `crates/alknet-core/Cargo.toml` with library crate, feature flags: `tls` (tokio-rustls + rustls), `iroh` (iroh), `acme` (rustls-acme, implies `tls`)
|
||||
- [ ] Core dependencies listed: `russh`, `tokio` (full), `tracing`, `anyhow`, `thiserror`, `tokio-util`
|
||||
- [ ] `crates/alknet/Cargo.toml` with binary crate, depends on `alknet-core` with default features, `clap` with `derive` feature
|
||||
- [ ] `crates/alknet-napi/Cargo.toml` with `cdylib` crate type, depends on `alknet-core`, `napi` and `napi-derive`
|
||||
- [ ] `crates/alknet-core/src/lib.rs` with module skeleton: `pub mod transport; pub mod client; pub mod server; pub mod auth; pub mod socks5; pub mod error;`
|
||||
- [ ] `crates/alknet/src/main.rs` with minimal `fn main()` skeleton
|
||||
- [ ] `crates/alknet-napi/src/lib.rs` with `#[macro_use] extern crate napi_derive;` and empty skeleton
|
||||
- [ ] `.gitignore` covers `target/`, `node_modules/`
|
||||
- [ ] `cargo check` succeeds for all workspace members
|
||||
- [ ] Feature flags resolve correctly: `cargo check -p alknet-core --features tls`, `--features iroh`, `--features acme`
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/overview.md — package structure, dependencies, feature flags
|
||||
- docs/architecture/napi-and-pubsub.md — alknet-napi crate purpose
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
id: setup/test-infrastructure
|
||||
name: Set up test infrastructure with tokio test helpers and integration test skeleton
|
||||
status: completed
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: trivial
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Set up test infrastructure so that subsequent tasks can write tests as they implement. Add test helpers for creating in-memory transport streams (mock transport), and skeleton integration test files for each component.
|
||||
|
||||
The mock transport is critical — it lets us test SSH client/server flows without actual network I/O, per ADR-001's consequence that "mock transports can produce in-memory streams."
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/tests/` directory with empty integration test skeletons: `transport_tests.rs`, `client_tests.rs`, `server_tests.rs`, `auth_tests.rs`
|
||||
- [ ] `crates/alknet-core/src/testutil.rs` module (behind `#[cfg(test)]` or a `testutil` feature) exporting `MockTransport` and `MockStream`
|
||||
- [ ] `MockStream` wraps `tokio::io::DuplexStream` implementing `AsyncRead + AsyncWrite + Unpin + Send`
|
||||
- [ ] `MockTransport` implements `Transport` trait (once defined) returning `MockStream` via `connect()`
|
||||
- [ ] `MockTransportAcceptor` implements `TransportAcceptor` (once defined) returning paired `MockStream` via `accept()`
|
||||
- [ ] `cargo test` succeeds (even if no real tests yet)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — Transport trait contract
|
||||
- docs/architecture/decisions/001-pluggable-transport.md — "mock transports can produce in-memory streams"
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
id: transport/acme-cert-provisioning
|
||||
name: Implement ACME Lets Encrypt certificate provisioning (feature-gated acme)
|
||||
status: completed
|
||||
depends_on:
|
||||
- transport/tls-transport
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement automatic TLS certificate provisioning via ACME (Let's Encrypt). Two modes per ADR-008:
|
||||
|
||||
1. **Domain-based ACME** (`--acme-domain`): Standard flow with HTTP-01 or TLS-ALPN-01 challenges. Domain-bound, auto-renewing.
|
||||
2. **IP-based ACME**: Short-lived certs via TLS-ALPN-01 on port 443. No domain needed.
|
||||
|
||||
Uses `rustls-acme` (pure Rust) to avoid external certbot dependency. Feature-gated behind `acme` (implies `tls`).
|
||||
|
||||
This integrates with `TlsAcceptor` by providing ACME-resolved certificates instead of manual cert/key files.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/transport/acme.rs` (behind `#[cfg(feature = "acme")]`)
|
||||
- [ ] Feature `acme` implies `tls` in Cargo.toml
|
||||
- [ ] `AcmeCertProvider` struct accepts: domain (domain-based) or IP mode flag
|
||||
- [ ] Domain-based mode: uses `rustls-acme` with HTTP-01/TLS-ALPN-01 challenge responder
|
||||
- [ ] IP-based mode: uses `rustls-acme` with TLS-ALPN-01 on port 443
|
||||
- [ ] `AcmeCertProvider` produces a `rustls::ServerConfig` that `TlsAcceptor` can use
|
||||
- [ ] Certificate auto-renewal handled by `rustls-acme` background task
|
||||
- [ ] `TlsAcceptor` updated to accept either manual certs OR an `AcmeCertProvider`
|
||||
- [ ] Integration with `TlsAcceptor::bind_acme()` or similar constructor
|
||||
- [ ] Unit tests for ACME config construction (challenge responder setup)
|
||||
- [ ] Integration test: ACME cert provisioning with Let's Encrypt staging (marked `#[ignore]` for CI)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/server.md — TLS certificate provisioning modes
|
||||
- docs/architecture/decisions/008-acme-lets-encrypt.md — ACME design, rustls-acme choice
|
||||
- docs/architecture/transport.md — feature flags, TLS transport constraints
|
||||
|
||||
## Notes
|
||||
|
||||
- `AcmeCertProvider` is the main entry point. It creates `AcmeState` and `ResolvesServerCertAcme` from `rustls-acme`.
|
||||
- The `ResolvesServerCertAcme` resolver is shared between the `AcmeState` background task and the `ServerConfig`, so cert updates propagate automatically.
|
||||
- `AcmeTlsAcceptor::bind_acme()` creates a TLS acceptor that uses ACME-provisioned certs and spawns a background tokio task for auto-renewal.
|
||||
- `TlsAcceptor::bind_acme()` also added for users who want to use ACME with the standard `TlsAcceptor` type directly.
|
||||
- The `AcmeConfig` stub in `tls.rs` is retained for backward compat with existing `TlsAcceptor::bind()`.
|
||||
- `acme` feature implies `tls` and adds `rustls-acme` + `futures` dependencies.
|
||||
- TLS-ALPN-01 challenge handling works via the `acme-tls/1` ALPN protocol registered in `ServerConfig` — the resolver dispatches challenge vs regular certs automatically.
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented ACME/Let's Encrypt certificate provisioning (ADR-008) behind the `acme` feature flag. `AcmeCertProvider` supports domain-based and IP-based modes using `rustls-acme`. `AcmeTlsAcceptor::bind_acme()` and `TlsAcceptor::bind_acme()` provide ACME-integrated TLS acceptance with automatic certificate renewal via a background tokio task. Unit tests cover config construction, builder patterns, and server config generation. Integration test for LE staging is marked `#[ignore]`.
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
id: transport/iroh-transport
|
||||
name: Implement IrohTransport and IrohAcceptor (feature-gated iroh)
|
||||
status: completed
|
||||
depends_on:
|
||||
- transport/trait-and-types
|
||||
- transport/tcp-transport
|
||||
scope: moderate
|
||||
risk: high
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement iroh QUIC P2P transport. Per ADR-003, use `tokio::io::join(recv_stream, send_stream)` to combine iroh's split QUIC streams into a single duplex stream that russh can consume.
|
||||
|
||||
Client-side: `IrohTransport` connects to a remote iroh endpoint, opens a bidirectional QUIC stream, and joins the halves.
|
||||
Server-side: `IrohAcceptor` creates an iroh endpoint, accepts incoming connections, accepts bidirectional streams.
|
||||
|
||||
iroh supports proxy configuration natively via `Endpoint::builder()`, which is how `--proxy` works with iroh transport (ADR-010).
|
||||
|
||||
Feature-gated behind `iroh` feature flag.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/transport/iroh.rs` (behind `#[cfg(feature = "iroh")]`)
|
||||
- [ ] `IrohTransport` holds: target endpoint ID (base58-decoded to `NodeId`), relay URL, optional proxy URL
|
||||
- [ ] `IrohTransport::connect()` calls `endpoint.connect(node_id, alpn)`, then `conn.open_bi()`, then `tokio::io::join(recv, send)`
|
||||
- [ ] ALPN value is `b"alknet-ssh"`
|
||||
- [ ] `IrohTransport::describe()` returns e.g. `"iroh://<endpoint-id>"`
|
||||
- [ ] `IrohAcceptor` holds an `iroh::Endpoint` instance
|
||||
- [ ] `IrohAcceptor::bind()` creates endpoint with relay URL and optional proxy config
|
||||
- [ ] `IrohAcceptor::accept()` calls `endpoint.accept()`, then `conn.accept_bi()`, then `tokio::io::join(recv, send)`
|
||||
- [ ] `IrohAcceptor` exposes `endpoint_id()` returning base58-encoded node ID for CLI display
|
||||
- [ ] Default relay is n0's `https://relay.iroh.network/` (ADR-009)
|
||||
- [ ] Proxy URL passed to `Endpoint::builder()` for outbound proxy (ADR-010)
|
||||
- [ ] `TransportInfo.transport_kind` is `TransportKind::Iroh { endpoint_id }`
|
||||
- [ ] Module re-exported from `transport/mod.rs` behind `#[cfg(feature = "iroh")]`
|
||||
- [ ] Unit tests: endpoint creation, stream join produces correct type
|
||||
- [ ] Integration test: iroh client connects to iroh server, stream is duplex (may need iroh relay, mark `#[ignore]` for CI)
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — IrohTransport row, iroh stream join, relay config
|
||||
- docs/architecture/decisions/003-iroh-stream-join.md — tokio::io::join rationale
|
||||
- docs/architecture/decisions/009-default-iroh-relay.md — default relay
|
||||
- docs/architecture/decisions/010-transport-chaining-cli.md — proxy configuration
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
id: transport/tcp-transport
|
||||
name: Implement TcpTransport and TcpAcceptor
|
||||
status: completed
|
||||
depends_on:
|
||||
- transport/trait-and-types
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement the simplest transport: plain TCP. `TcpTransport` connects via `TcpStream::connect(addr)` on the client side. `TcpAcceptor` accepts via `TcpListener::accept()` on the server side. This is the baseline transport that all others build upon conceptually.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/transport/tcp.rs` exports `TcpTransport` and `TcpAcceptor`
|
||||
- [ ] `TcpTransport` holds a `SocketAddr` target address
|
||||
- [ ] `TcpTransport::connect()` calls `TcpStream::connect(addr)` and returns the stream
|
||||
- [ ] `TcpTransport::describe()` returns e.g. `"tcp://1.2.3.4:22"`
|
||||
- [ ] `TcpAcceptor` holds a `TcpListener` and accept address
|
||||
- [ ] `TcpAcceptor::accept()` calls `listener.accept()`, returns `(stream, TransportInfo)` with `remote_addr` set and `TransportKind::Tcp`
|
||||
- [ ] `TcpAcceptor` constructor binds the listener: `TcpAcceptor::bind(addr)` async factory
|
||||
- [ ] Connection timeout handling (tokio default connect timeout is reasonable; document behavior)
|
||||
- [ ] Unit tests: connect creates a stream, accept receives a connection, describe format
|
||||
- [ ] Integration test: client connects to server via TCP, stream is duplex
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — TcpTransport row in implementations table
|
||||
- docs/architecture/overview.md — "TCP on port 22 for basic use"
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: transport/tls-transport
|
||||
name: Implement TlsTransport and TlsAcceptor (feature-gated tls)
|
||||
status: completed
|
||||
depends_on:
|
||||
- transport/tcp-transport
|
||||
- transport/trait-and-types
|
||||
scope: moderate
|
||||
risk: medium
|
||||
impact: component
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Implement TLS transport that wraps TCP with `tokio-rustls`. Client-side: `TlsTransport` establishes a TCP connection and wraps it with a TLS client session. Server-side: `TlsAcceptor` accepts TCP connections and wraps them with a TLS server session.
|
||||
|
||||
Supports:
|
||||
- Manual cert/key configuration (`--tls-cert`, `--tls-key`)
|
||||
- insecure mode (accept self-signed certs) for development
|
||||
- `tls_server_name` override for SNI (ADR-010)
|
||||
- Stealth mode support requires peeking at first bytes post-TLS-handshake (handled in server task, but TLS stream must support this)
|
||||
|
||||
Feature-gated behind `tls` feature flag.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/transport/tls.rs` (behind `#[cfg(feature = "tls")]`)
|
||||
- [ ] `TlsTransport` holds: target addr, optional `tls_server_name`, `insecure` flag, optional root cert for verification
|
||||
- [ ] `TlsTransport::connect()` does TCP connect then TLS client handshake via `tokio_rustls::TlsConnector`
|
||||
- [ ] When `insecure`, accepts any certificate (dangerous, `webpki_roots::CertStore` bypass or custom verifier)
|
||||
- [ ] When not `insecure`, verifies server cert against system roots + optional custom CA
|
||||
- [ ] `TlsTransport::describe()` returns e.g. `"tls://example.com:443"`
|
||||
- [ ] `TlsAcceptor` holds: `TcpListener`, `ServerConfig` (from `rustls::ServerConfig`)
|
||||
- [ ] `TlsAcceptor::accept()` does TCP accept then TLS server handshake via `tokio_rustls::TlsAcceptor`
|
||||
- [ ] `TlsAcceptor` constructor accepts: `tls_cert` path/data, `tls_key` path/data, optional ACME config (stub for now)
|
||||
- [ ] `TransportInfo.transport_kind` is `TransportKind::Tls { server_name }`
|
||||
- [ ] Module re-exported from `transport/mod.rs` behind `#[cfg(feature = "tls")]`
|
||||
- [ ] Unit tests for connect/accept with self-signed certs (insecure mode)
|
||||
- [ ] Integration test: full TLS client-to-server connection succeeds
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — TlsTransport row, TLS cert provisioning
|
||||
- docs/architecture/server.md — TLS certificate provisioning modes
|
||||
- docs/architecture/decisions/008-acme-lets-encrypt.md — ACME cert support (feature-gated)
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
id: transport/trait-and-types
|
||||
name: Define Transport trait, TransportAcceptor trait, TransportInfo, and TransportKind types
|
||||
status: completed
|
||||
depends_on:
|
||||
- setup/project-init
|
||||
scope: narrow
|
||||
risk: low
|
||||
impact: phase
|
||||
level: implementation
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Define the core transport abstraction types that everything else builds on. This is the foundation per ADR-001: a `Transport` trait that produces `AsyncRead + AsyncWrite + Unpin + Send` streams, and a `TransportAcceptor` trait for the server side.
|
||||
|
||||
The `TransportInfo` and `TransportKind` types carry metadata about incoming connections (remote address, transport kind) which the server handler needs for logging and auth decisions.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `crates/alknet-core/src/transport/mod.rs` exports `Transport` trait, `TransportAcceptor` trait, `TransportInfo`, `TransportKind`
|
||||
- [ ] `Transport` trait: `async fn connect(&self) -> Result<Self::Stream>` where `Self::Stream: AsyncRead + AsyncWrite + Unpin + Send + 'static`
|
||||
- [ ] `Transport::describe(&self) -> String` for human-readable logging
|
||||
- [ ] `TransportAcceptor` trait: `async fn accept(&self) -> Result<(Self::Stream, TransportInfo)>` with same stream bounds
|
||||
- [ ] `TransportInfo { remote_addr: Option<SocketAddr>, transport_kind: TransportKind }`
|
||||
- [ ] `TransportKind` enum: `Tcp`, `Tls { server_name: Option<String> }`, `Iroh { endpoint_id: String }`
|
||||
- [ ] Traits are `Send + Sync + 'static`
|
||||
- [ ] Re-exported from `crates/alknet-core/src/lib.rs`
|
||||
- [ ] Unit tests verifying trait objects can be constructed (trait is object-safe with `Box<dyn Transport<Stream = ...>>`)
|
||||
- [ ] Documentation comments on all public types referencing ADR-001, ADR-004
|
||||
|
||||
## References
|
||||
|
||||
- docs/architecture/transport.md — Transport trait, TransportAcceptor trait, TransportInfo, TransportKind definitions
|
||||
- docs/architecture/decisions/001-pluggable-transport.md — pluggable transport rationale
|
||||
- docs/architecture/decisions/004-ssh-over-transport.md — SSH runs over transport
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user