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:
2026-06-07 13:29:58 +00:00
parent 9ab789ec5f
commit a7f0dcdeb9
13 changed files with 718 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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