tasks: decompose Phase 1 core modifications into 12 atomic implementation tasks
Phase 1 of the integration plan modifies alknet-core to support the architectural changes from Phase 0 ADRs and specs. Decomposed into dependency-ordered tasks across config split, identity, forwarding policy, OperationEnv, interface abstraction, and NAPI reload API. Critical path: config-split → identity → forwarding → wire-into-handler → interface-trait → ssh-interface-extraction → review. Two highest-risk tasks (interface-trait-definition, ssh-interface-extraction) are split from §1.8 per the integration plan's note that it may need sub-phases. OperationEnv is split into types and runtime per Phase 1 local-dispatch-only constraint.
This commit is contained in:
48
tasks/core/auth-service-irpc.md
Normal file
48
tasks/core/auth-service-irpc.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: core/auth-service-irpc
|
||||
name: Implement AuthProtocol irpc service enum behind feature flag
|
||||
status: pending
|
||||
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
|
||||
52
tasks/core/config-identity-provider-into-handler.md
Normal file
52
tasks/core/config-identity-provider-into-handler.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
id: core/config-identity-provider-into-handler
|
||||
name: Wire IdentityProvider and ForwardingPolicy into ServerHandler
|
||||
status: pending
|
||||
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
|
||||
44
tasks/core/config-service-irpc.md
Normal file
44
tasks/core/config-service-irpc.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: core/config-service-irpc
|
||||
name: Implement ConfigProtocol irpc service and ConfigServiceImpl
|
||||
status: pending
|
||||
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
|
||||
67
tasks/core/config-static-dynamic-split.md
Normal file
67
tasks/core/config-static-dynamic-split.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: core/config-static-dynamic-split
|
||||
name: Implement StaticConfig / DynamicConfig split with ArcSwap hot-reload
|
||||
status: pending
|
||||
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
|
||||
62
tasks/core/forwarding-policy.md
Normal file
62
tasks/core/forwarding-policy.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: core/forwarding-policy
|
||||
name: Implement ForwardingPolicy with rule-based allow/deny
|
||||
status: pending
|
||||
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
|
||||
57
tasks/core/identity-type-provider.md
Normal file
57
tasks/core/identity-type-provider.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
id: core/identity-type-provider
|
||||
name: Implement Identity struct and IdentityProvider trait
|
||||
status: pending
|
||||
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
|
||||
54
tasks/core/interface-trait-definition.md
Normal file
54
tasks/core/interface-trait-definition.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: core/interface-trait-definition
|
||||
name: Define Interface trait and InterfaceConfig types
|
||||
status: pending
|
||||
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
|
||||
54
tasks/core/multi-transport-listeners.md
Normal file
54
tasks/core/multi-transport-listeners.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: core/multi-transport-listeners
|
||||
name: Implement multi-transport listeners with Vec<ListenerConfig>
|
||||
status: pending
|
||||
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
|
||||
46
tasks/core/napi-reload-api.md
Normal file
46
tasks/core/napi-reload-api.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: core/napi-reload-api
|
||||
name: Add NAPI reload API for DynamicConfig and ForwardingPolicy
|
||||
status: pending
|
||||
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
|
||||
56
tasks/core/operation-context-registry.md
Normal file
56
tasks/core/operation-context-registry.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: core/operation-context-registry
|
||||
name: Implement OperationContext, OperationRegistry, and OperationSpec
|
||||
status: pending
|
||||
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
|
||||
54
tasks/core/operationenv-local-dispatch.md
Normal file
54
tasks/core/operationenv-local-dispatch.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: core/operationenv-local-dispatch
|
||||
name: Implement OperationEnv local dispatch and event envelope framing
|
||||
status: pending
|
||||
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
|
||||
62
tasks/core/ssh-interface-extraction.md
Normal file
62
tasks/core/ssh-interface-extraction.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: core/ssh-interface-extraction
|
||||
name: Extract SshInterface from ServerHandler — refactor SSH into Layer 2
|
||||
status: pending
|
||||
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
|
||||
62
tasks/review/phase1-core-modifications.md
Normal file
62
tasks/review/phase1-core-modifications.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
id: review/phase1-core-modifications
|
||||
name: Review Phase 1 core modifications — config split, identity, forwarding, OperationEnv, interface abstraction
|
||||
status: pending
|
||||
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
|
||||
|
||||
> To be filled by review agent
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Reference in New Issue
Block a user