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

View 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