diff --git a/tasks/core/auth-service-irpc.md b/tasks/core/auth-service-irpc.md new file mode 100644 index 0000000..73e5b26 --- /dev/null +++ b/tasks/core/auth-service-irpc.md @@ -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` 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 \ No newline at end of file diff --git a/tasks/core/config-identity-provider-into-handler.md b/tasks/core/config-identity-provider-into-handler.md new file mode 100644 index 0000000..5f24bcb --- /dev/null +++ b/tasks/core/config-identity-provider-into-handler.md @@ -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` and passes it to `ServerHandler` +- `ServerHandler` holds `Arc` instead of `Arc` +- `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` and `Arc>` instead of `Arc` +- [ ] `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 \ No newline at end of file diff --git a/tasks/core/config-service-irpc.md b/tasks/core/config-service-irpc.md new file mode 100644 index 0000000..a4f6186 --- /dev/null +++ b/tasks/core/config-service-irpc.md @@ -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` 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` and returns `Arc`, `Arc` +- [ ] `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 \ No newline at end of file diff --git a/tasks/core/config-static-dynamic-split.md b/tasks/core/config-static-dynamic-split.md new file mode 100644 index 0000000..f53b34c --- /dev/null +++ b/tasks/core/config-static-dynamic-split.md @@ -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`) +- `ForwardingPolicy` — allow/deny rules for channel targets (new, Phase 1.3) +- `RateLimitConfig` — rate limiting parameters + +**Key changes**: +- `ServerHandler` currently holds `Arc`. Replace with `Arc>` 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` used in `ServerHandler` instead of `Arc` +- [ ] `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 +- crates/alknet-core/src/server/serve.rs — current ServeOptions and Server + +## Notes + +> To be filled by implementation agent + +## Summary + +> To be filled on completion \ No newline at end of file diff --git a/tasks/core/forwarding-policy.md b/tasks/core/forwarding-policy.md new file mode 100644 index 0000000..d87c619 --- /dev/null +++ b/tasks/core/forwarding-policy.md @@ -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 }` +- `ForwardingAction` enum: `Allow` | `Deny` +- `ForwardingRule` struct: `{ target: TargetPattern, action: ForwardingAction, principals: Vec, transports: Vec }` +- `TargetPattern` enum: `Any`, `Host(String)`, `Cidr(IpNetwork)`, `PortRange(String, Range)` +- 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` (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 \ No newline at end of file diff --git a/tasks/core/identity-type-provider.md b/tasks/core/identity-type-provider.md new file mode 100644 index 0000000..4a0c3fd --- /dev/null +++ b/tasks/core/identity-type-provider.md @@ -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, resources: HashMap> }` +- `IdentityProvider` trait: `resolve_from_fingerprint(&str) -> Option` and `resolve_from_token(&AuthToken) -> Option` +- `ConfigIdentityProvider`: reads from `ArcSwap`, the default implementation for minimal deployments +- `AuthToken` type for future token-based auth (WebTransport, etc.) + +**Key changes**: +- `ServerHandler::auth_publickey()` currently reads from `Arc` 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`, 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` 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 \ No newline at end of file diff --git a/tasks/core/interface-trait-definition.md b/tasks/core/interface-trait-definition.md new file mode 100644 index 0000000..589add3 --- /dev/null +++ b/tasks/core/interface-trait-definition.md @@ -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` +- `InterfaceConfig` enum: `Ssh(SshInterfaceConfig)`, `RawFraming(RawFramingConfig)` +- `SshInterfaceConfig`: `auth: Arc`, `forwarding: Arc>`, `host_key: Arc` +- `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 \ No newline at end of file diff --git a/tasks/core/multi-transport-listeners.md b/tasks/core/multi-transport-listeners.md new file mode 100644 index 0000000..7edad41 --- /dev/null +++ b/tasks/core/multi-transport-listeners.md @@ -0,0 +1,54 @@ +--- +id: core/multi-transport-listeners +name: Implement multi-transport listeners with Vec +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`, 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` +- 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>` and `Arc` +- [ ] 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 \ No newline at end of file diff --git a/tasks/core/napi-reload-api.md b/tasks/core/napi-reload-api.md new file mode 100644 index 0000000..7134588 --- /dev/null +++ b/tasks/core/napi-reload-api.md @@ -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 \ No newline at end of file diff --git a/tasks/core/operation-context-registry.md b/tasks/core/operation-context-registry.md new file mode 100644 index 0000000..e676675 --- /dev/null +++ b/tasks/core/operation-context-registry.md @@ -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) +- `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 \ No newline at end of file diff --git a/tasks/core/operationenv-local-dispatch.md b/tasks/core/operationenv-local-dispatch.md new file mode 100644 index 0000000..7fb0183 --- /dev/null +++ b/tasks/core/operationenv-local-dispatch.md @@ -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 \ No newline at end of file diff --git a/tasks/core/ssh-interface-extraction.md b/tasks/core/ssh-interface-extraction.md new file mode 100644 index 0000000..997553e --- /dev/null +++ b/tasks/core/ssh-interface-extraction.md @@ -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 \ No newline at end of file diff --git a/tasks/review/phase1-core-modifications.md b/tasks/review/phase1-core-modifications.md new file mode 100644 index 0000000..73969bc --- /dev/null +++ b/tasks/review/phase1-core-modifications.md @@ -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 \ No newline at end of file