docs(architecture): resolve review #002 Tiers 1-3 — mechanical and consistency fixes
Governance (Tier 2):
- Advance ADR-022 and ADR-023 from Proposed to Accepted (specs already
depend on their types as source of truth)
- Amend ADR-015: mark Decision 3 and Assumption 6 as superseded by ADR-022;
update handler_identity type to CompositionAuthority
- Amend ADR-002: note handle() signature revised by ADR-007 (BiStream → Connection)
- Amend ADR-004: note 'enrich/replace' AuthContext language superseded by
ADR-011's immutability model; update to describe set_identity on Connection
- Update main README ADR table to show ADR-022/023 as Accepted
Spec-ADR consistency (Tier 3):
- Add abort_policy: AbortPolicy field to OperationContext struct (ADR-016
Decision 6 mandated this but the spec omitted it)
- Define AbortPolicy enum (AbortDependents | ContinueRunning) with Default impl
- Add abort_policy to build_root_context and LocalOperationEnv::invoke()
- Define the OperationEnv trait explicitly with invoke() and
invoke_with_policy() methods (was referenced as 'must remain a trait'
but never defined)
- Specify From<StreamError> for HandlerError impl with exact variant mapping
- Add Connection::from_quinn() / from_iroh() constructors (was referenced
as Connection::new() but never defined)
- Remove undefined CertAuthorityEntry placeholder from AuthPolicy v1 (will
be added additively when alknet-ssh lands)
- Fix config.md key-differences table: rate limits are in DynamicConfig,
not StaticConfig
Mechanical fixes (Tier 1):
- overview.md: 'closes the QUIC stream' → 'closes the connection' (stale
from pre-ADR-007 model)
- overview.md: OQ-04 entry updated from stale 'defer to implementation'
to 'resolved: static at startup'
- mnemonic-derivation.md: remove duplicate helper functions block (incomplete
first copy, complete second copy)
- ADR-003: add iroh (feature-gated) to alknet-core dependency list, added
by ADR-010
- ADR-021: fix ambiguous 'W1 drift issue from the vault review' cross-reference
- ADR-022: rephrase FromCall 'leaf locally' to 'leaf in the local registry'
- ADR-017: add error_schemas to from_call mirror list and services/schema
step (inconsistency with ADR-023)
- ADR-016: fix self-referential citation ('ADR-016 Assumption 5' → 'Assumption 5')
- Add ScopedOperationEnv::empty(), allows(), new() and
CompositionAuthority::none(), new() impl blocks (referenced but undefined)
- Add call.completed clarification for non-subscription calls
- Add services/schema leading-slash normalization note
- Crate README ADR tables: add missing ADR-013 (call), ADR-015 (core),
ADR-006 + ADR-010 (vault)
- Vault README: add consolidated 'Known Source Drift' table tracking all
four drift items (OsRng, unwrap, CURRENT_KEY_VERSION, spawn bug) in one
place, including the two previously missing from README
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-20
|
last_updated: 2026-06-22-20
|
||||||
---
|
---
|
||||||
|
|
||||||
# Alknet Architecture
|
# Alknet Architecture
|
||||||
@@ -56,8 +56,8 @@ last_updated: 2026-06-20
|
|||||||
| [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | Accepted |
|
| [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | Accepted |
|
||||||
| [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | Accepted |
|
| [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | Accepted |
|
||||||
| [021](decisions/021-key-rotation-via-version-indexed-paths.md) | Key Rotation via Version-Indexed Paths | Accepted |
|
| [021](decisions/021-key-rotation-via-version-indexed-paths.md) | Key Rotation via Version-Indexed Paths | Accepted |
|
||||||
| [022](decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Proposed |
|
| [022](decisions/022-handler-registration-provenance-and-composition-authority.md) | Handler Registration, Provenance, and Composition Authority | Accepted |
|
||||||
| [023](decisions/023-operation-error-schemas.md) | Operation Error Schemas | Proposed |
|
| [023](decisions/023-operation-error-schemas.md) | Operation Error Schemas | Accepted |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-22
|
last_updated: 2026-06-22-22
|
||||||
---
|
---
|
||||||
|
|
||||||
# alknet-call
|
# alknet-call
|
||||||
@@ -21,6 +21,7 @@ Structured RPC over QUIC: operations, request/response, streaming subscriptions,
|
|||||||
| [001](../../decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alknet/call` |
|
| [001](../../decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alknet/call` |
|
||||||
| [002](../../decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | CallAdapter implements ProtocolHandler |
|
| [002](../../decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | CallAdapter implements ProtocolHandler |
|
||||||
| [003](../../decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-call depends on alknet-core and irpc |
|
| [003](../../decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-call depends on alknet-core and irpc |
|
||||||
|
| [013](../../decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Adapter traits defined in Rust; TS is reference/browser adaptation |
|
||||||
| [004](../../decisions/004-auth-as-shared-core.md) | Auth as Shared Core | AuthContext passed to call handlers |
|
| [004](../../decisions/004-auth-as-shared-core.md) | Auth as Shared Core | AuthContext passed to call handlers |
|
||||||
| [005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | irpc provides framing and service dispatch |
|
| [005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | irpc provides framing and service dispatch |
|
||||||
| [006](../../decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | `alknet/call` ALPN, one ALPN per connection |
|
| [006](../../decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | `alknet/call` ALPN, one ALPN per connection |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-22
|
last_updated: 2026-06-22-22
|
||||||
---
|
---
|
||||||
|
|
||||||
# Call Protocol
|
# Call Protocol
|
||||||
@@ -105,6 +105,10 @@ Five event types carry request/response and subscription semantics:
|
|||||||
|
|
||||||
The `id` field carries the `requestId` for correlation.
|
The `id` field carries the `requestId` for correlation.
|
||||||
|
|
||||||
|
`call.completed` is sent only for subscriptions. A plain `call()` (request/response)
|
||||||
|
is complete after its single `call.responded`; no `call.completed` follows. The
|
||||||
|
`PendingRequestMap` entry for a `Call` is deleted on the first `call.responded`.
|
||||||
|
|
||||||
### `call.requested` Payload
|
### `call.requested` Payload
|
||||||
|
|
||||||
The `payload` of a `call.requested` event has this shape:
|
The `payload` of a `call.requested` event has this shape:
|
||||||
@@ -297,6 +301,7 @@ fn build_root_context(
|
|||||||
metadata: HashMap::new(), // fresh per request
|
metadata: HashMap::new(), // fresh per request
|
||||||
env: registration.scoped_env.clone()
|
env: registration.scoped_env.clone()
|
||||||
.unwrap_or_else(ScopedOperationEnv::empty), // from the bundle, empty for leaves
|
.unwrap_or_else(ScopedOperationEnv::empty), // from the bundle, empty for leaves
|
||||||
|
abort_policy: AbortPolicy::default(), // abort-dependents (ADR-016 Decision 6)
|
||||||
internal: false, // external call — ACL against caller identity
|
internal: false, // external call — ACL against caller identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-22
|
last_updated: 2026-06-22-22
|
||||||
---
|
---
|
||||||
|
|
||||||
# Operation Registry
|
# Operation Registry
|
||||||
@@ -116,12 +116,36 @@ pub struct OperationContext {
|
|||||||
pub capabilities: Capabilities,
|
pub capabilities: Capabilities,
|
||||||
pub metadata: HashMap<String, Value>,
|
pub metadata: HashMap<String, Value>,
|
||||||
pub env: OperationEnv,
|
pub env: OperationEnv,
|
||||||
|
/// Abort policy for this call's descendants (ADR-016 Decision 6).
|
||||||
|
/// Default `AbortDependents` — aborting this request aborts all
|
||||||
|
/// non-terminal descendants. `ContinueRunning` is an opt-in for
|
||||||
|
/// long-running work that should survive a parent's abort. Set by the
|
||||||
|
/// composing handler via `OperationEnv::invoke()` (or
|
||||||
|
/// `invoke_with_policy()`), not by the wire caller.
|
||||||
|
pub abort_policy: AbortPolicy,
|
||||||
/// Composition-origin flag. Set by `OperationEnv::invoke()` (true) or the
|
/// Composition-origin flag. Set by `OperationEnv::invoke()` (true) or the
|
||||||
/// `CallAdapter` dispatch path (false) — never by handlers. Module-private
|
/// `CallAdapter` dispatch path (false) — never by handlers. Module-private
|
||||||
/// for writes; read via `is_internal()`. See ADR-015.
|
/// for writes; read via `is_internal()`. See ADR-015.
|
||||||
pub(crate) internal: bool,
|
pub(crate) internal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Abort cascade policy for a call's descendants (ADR-016).
|
||||||
|
///
|
||||||
|
/// `AbortDependents` (default): aborting this call cascades to all
|
||||||
|
/// non-terminal descendants.
|
||||||
|
///
|
||||||
|
/// `ContinueRunning` (opt-in): descendants that have already started
|
||||||
|
/// continue to completion; descendants that haven't started are aborted;
|
||||||
|
/// no new descendants start.
|
||||||
|
pub enum AbortPolicy {
|
||||||
|
AbortDependents,
|
||||||
|
ContinueRunning,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AbortPolicy {
|
||||||
|
fn default() -> Self { Self::AbortDependents }
|
||||||
|
}
|
||||||
|
|
||||||
impl OperationContext {
|
impl OperationContext {
|
||||||
pub fn is_internal(&self) -> bool { self.internal }
|
pub fn is_internal(&self) -> bool { self.internal }
|
||||||
}
|
}
|
||||||
@@ -195,7 +219,45 @@ The CLI binary (or assembly layer) constructs the registry and passes it to the
|
|||||||
|
|
||||||
### OperationEnv
|
### OperationEnv
|
||||||
|
|
||||||
`OperationEnv` is the universal composition mechanism. A handler calls `context.env.invoke("fs", "readFile", input, &context)` and gets a `ResponseEnvelope` back — regardless of whether the operation runs locally, via an irpc service, or on a remote node.
|
The `OperationEnv` trait is the universal composition mechanism. A handler calls `context.env.invoke("fs", "readFile", input, &context)` and gets a `ResponseEnvelope` back — regardless of whether the operation runs locally, via an irpc service, or on a remote node.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// The composition dispatch trait. A handler composes child operations
|
||||||
|
/// through its `OperationContext.env` (which implements this trait).
|
||||||
|
///
|
||||||
|
/// This must remain a trait, not a concrete type — session-scoped
|
||||||
|
/// registries (OQ-19) depend on wrapping the global env via trait
|
||||||
|
/// layering. Making `OperationEnv` concrete or hardcoding the global
|
||||||
|
/// registry into the dispatch path would close the session-overlay
|
||||||
|
/// pattern.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait OperationEnv: Send + Sync {
|
||||||
|
/// Compose a child operation. The child's `OperationContext` is
|
||||||
|
/// constructed with `internal: true`, inheriting the parent's
|
||||||
|
/// composition authority as the child's caller identity. The abort
|
||||||
|
/// policy defaults to the parent's (ADR-016 Decision 6).
|
||||||
|
async fn invoke(
|
||||||
|
&self,
|
||||||
|
namespace: &str,
|
||||||
|
operation: &str,
|
||||||
|
input: Value,
|
||||||
|
parent: &OperationContext,
|
||||||
|
) -> ResponseEnvelope;
|
||||||
|
|
||||||
|
/// Compose a child with an explicit abort policy (ADR-016 Decision 6).
|
||||||
|
/// Use `AbortPolicy::ContinueRunning` for long-running work that
|
||||||
|
/// should survive a parent's abort. The default `invoke()` inherits
|
||||||
|
/// the parent's policy; this method overrides it for this child.
|
||||||
|
async fn invoke_with_policy(
|
||||||
|
&self,
|
||||||
|
namespace: &str,
|
||||||
|
operation: &str,
|
||||||
|
input: Value,
|
||||||
|
parent: &OperationContext,
|
||||||
|
policy: AbortPolicy,
|
||||||
|
) -> ResponseEnvelope;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
The `parent` parameter propagates the calling context: the nested call gets `parent_request_id: Some(parent.request_id)`, inherits `parent.handler_identity` as the caller identity, and is marked `internal: true`.
|
The `parent` parameter propagates the calling context: the nested call gets `parent_request_id: Some(parent.request_id)`, inherits `parent.handler_identity` as the caller identity, and is marked `internal: true`.
|
||||||
|
|
||||||
@@ -242,6 +304,9 @@ impl OperationEnv for LocalOperationEnv {
|
|||||||
metadata: HashMap::new(), // Fresh — does NOT propagate parent metadata (ADR-014)
|
metadata: HashMap::new(), // Fresh — does NOT propagate parent metadata (ADR-014)
|
||||||
env: registration.scoped_env.clone()
|
env: registration.scoped_env.clone()
|
||||||
.unwrap_or_else(ScopedOperationEnv::empty), // Child's own scoped env (empty for leaves)
|
.unwrap_or_else(ScopedOperationEnv::empty), // Child's own scoped env (empty for leaves)
|
||||||
|
// Abort policy: inherit the parent's policy by default (ADR-016).
|
||||||
|
// The parent handler can override via `invoke_with_policy()`.
|
||||||
|
abort_policy: parent.abort_policy.clone(),
|
||||||
internal: true, // Nested calls use handler authority
|
internal: true, // Nested calls use handler authority
|
||||||
};
|
};
|
||||||
self.registry.invoke(&name, input, context).await
|
self.registry.invoke(&name, input, context).await
|
||||||
@@ -283,7 +348,14 @@ These are read-only — no admin operations are exposed through the call protoco
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`services/schema` accepts `{ "name": "fs/readFile" }` and returns the full `OperationSpec` including input/output JSON Schemas and declared `error_schemas` (ADR-023). This enables client code generation: a client reading the schema can produce typed error enums instead of generic error handling.
|
`services/schema` accepts `{ "name": "fs/readFile" }` (no leading slash —
|
||||||
|
registry form, same as `OperationSpec.name`) and returns the full
|
||||||
|
`OperationSpec` including input/output JSON Schemas and declared
|
||||||
|
`error_schemas` (ADR-023). The `CallAdapter` normalizes the leading slash
|
||||||
|
from wire `operationId`s before lookup, so `services/schema` accepts both
|
||||||
|
`fs/readFile` and `/fs/readFile`. This enables client code generation: a
|
||||||
|
client reading the schema can produce typed error enums instead of generic
|
||||||
|
error handling.
|
||||||
|
|
||||||
### irpc Integration
|
### irpc Integration
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-21
|
last_updated: 2026-06-22-21
|
||||||
---
|
---
|
||||||
|
|
||||||
# alknet-core
|
# alknet-core
|
||||||
@@ -29,6 +29,7 @@ Core library for ALPN-based protocol dispatch. Every handler crate depends on al
|
|||||||
| [009](../../decisions/009-one-way-door-decision-framework.md) | One-Way Door Framework | Decision classification |
|
| [009](../../decisions/009-one-way-door-decision-framework.md) | One-Way Door Framework | Decision classification |
|
||||||
| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Endpoint, HandlerRegistry, accept loop |
|
| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Endpoint, HandlerRegistry, accept loop |
|
||||||
| [011](../../decisions/011-authcontext-structure.md) | AuthContext Structure | AuthContext fields and resolution flow |
|
| [011](../../decisions/011-authcontext-structure.md) | AuthContext Structure | AuthContext fields and resolution flow |
|
||||||
|
| [015](../../decisions/015-privilege-model-and-authority-context.md) | Privilege Model and Authority Context | Per-request identity on OperationContext; admin scope for config reload |
|
||||||
|
|
||||||
## Relevant Open Questions
|
## Relevant Open Questions
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-21
|
last_updated: 2026-06-22-21
|
||||||
---
|
---
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
@@ -115,19 +115,17 @@ pub struct AuthPolicy {
|
|||||||
/// Stored as strings to avoid russh dependency in core.
|
/// Stored as strings to avoid russh dependency in core.
|
||||||
pub authorized_fingerprints: HashSet<String>,
|
pub authorized_fingerprints: HashSet<String>,
|
||||||
|
|
||||||
/// Certificate authorities for certificate-based auth.
|
|
||||||
/// The exact structure is TBD — it will be defined when alknet-ssh
|
|
||||||
/// is implemented. For now, this is a placeholder that reserves
|
|
||||||
/// the field. alknet-ssh will define `CertAuthorityEntry` with
|
|
||||||
/// the necessary fields (public key, principals, options).
|
|
||||||
pub cert_authorities: Vec<CertAuthorityEntry>,
|
|
||||||
|
|
||||||
/// API keys for token-based auth.
|
/// API keys for token-based auth.
|
||||||
pub api_keys: Vec<ApiKeyEntry>,
|
pub api_keys: Vec<ApiKeyEntry>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`CertAuthorityEntry` is a placeholder type. Its fields will be defined when alknet-ssh is implemented and the certificate authority validation requirements are clear. For v1, `cert_authorities` will be an empty vector.
|
Certificate authority entries for cert-based auth will be added when
|
||||||
|
alknet-ssh is implemented. The `cert_authorities` field is omitted from v1
|
||||||
|
to avoid referencing an undefined type. Adding it back is additive (a new
|
||||||
|
field on `AuthPolicy` is non-breaking for existing config files that don't
|
||||||
|
use it). alknet-ssh will define `CertAuthorityEntry` with the necessary
|
||||||
|
fields (public key, principals, options).
|
||||||
|
|
||||||
This replaces the reference implementation's `AuthPolicy` which depended on `russh::keys::PublicKey`. The new version stores fingerprints as strings, not russh types. This removes the russh dependency from alknet-core.
|
This replaces the reference implementation's `AuthPolicy` which depended on `russh::keys::PublicKey`. The new version stores fingerprints as strings, not russh types. This removes the russh dependency from alknet-core.
|
||||||
|
|
||||||
@@ -217,7 +215,7 @@ Simplified from the reference implementation. Removes proxy-specific errors (now
|
|||||||
|
|
||||||
| Aspect | Reference | New Model |
|
| Aspect | Reference | New Model |
|
||||||
|--------|-----------|-----------|
|
|--------|-----------|-----------|
|
||||||
| StaticConfig fields | SSH host key, stealth, transport_mode, listeners, proxy | listen_addr, TLS cert/key, drain_timeout, rate limits |
|
| StaticConfig fields | SSH host key, stealth, transport_mode, listeners, proxy | listen_addr, TLS cert/key, drain_timeout |
|
||||||
| DynamicConfig.auth | `HashSet<PublicKey>` (russh types) | `HashSet<String>` (fingerprint strings) |
|
| DynamicConfig.auth | `HashSet<PublicKey>` (russh types) | `HashSet<String>` (fingerprint strings) |
|
||||||
| ListenerConfig | Enum with Stream/Http/Dns variants | Eliminated — single endpoint, ALPN dispatch |
|
| ListenerConfig | Enum with Stream/Http/Dns variants | Eliminated — single endpoint, ALPN dispatch |
|
||||||
| TransportMode | Tcp/Tls/Iroh | Eliminated — always QUIC+TLS |
|
| TransportMode | Tcp/Tls/Iroh | Eliminated — always QUIC+TLS |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-16
|
last_updated: 2026-06-22-16
|
||||||
---
|
---
|
||||||
|
|
||||||
# Core Types
|
# Core Types
|
||||||
@@ -57,6 +57,14 @@ pub struct Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Connection {
|
impl Connection {
|
||||||
|
/// Construct from a quinn connection (feature-gated on quinn).
|
||||||
|
#[cfg(feature = "quinn")]
|
||||||
|
pub fn from_quinn(conn: quinn::Connection) -> Self;
|
||||||
|
|
||||||
|
/// Construct from an iroh connection (feature-gated on iroh).
|
||||||
|
#[cfg(feature = "iroh")]
|
||||||
|
pub fn from_iroh(conn: iroh::Connection) -> Self;
|
||||||
|
|
||||||
pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), StreamError>;
|
pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), StreamError>;
|
||||||
pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), StreamError>;
|
pub async fn open_bi(&self) -> Result<(SendStream, RecvStream), StreamError>;
|
||||||
pub fn remote_alpn(&self) -> &[u8];
|
pub fn remote_alpn(&self) -> &[u8];
|
||||||
@@ -110,7 +118,7 @@ impl AsyncRead for RecvStream { ... }
|
|||||||
- `RecvStream` implements `AsyncRead`. Read bytes from the peer.
|
- `RecvStream` implements `AsyncRead`. Read bytes from the peer.
|
||||||
- These are concrete wrapper types that use internal enum dispatch to delegate to the appropriate QUIC stream type (quinn or iroh) in production, and to test mocks in tests.
|
- These are concrete wrapper types that use internal enum dispatch to delegate to the appropriate QUIC stream type (quinn or iroh) in production, and to test mocks in tests.
|
||||||
|
|
||||||
Since the endpoint supports both quinn and iroh connection sources (ADR-010), streams may come from either. `Connection::new()` wraps the appropriate stream source based on where the connection came from.
|
Since the endpoint supports both quinn and iroh connection sources (ADR-010), streams may come from either. `Connection::from_quinn()` / `Connection::from_iroh()` wrap the appropriate stream source based on where the connection came from.
|
||||||
|
|
||||||
## StreamError
|
## StreamError
|
||||||
|
|
||||||
@@ -138,6 +146,39 @@ When a handler encounters a `StreamError` and needs to return from `handle()`, i
|
|||||||
|
|
||||||
Handlers that manage multiple streams (SSH, call) may catch `StreamError::StreamClosed` per-stream and continue serving other streams on the same connection — only `ConnectionClosed` forces `handle()` to return.
|
Handlers that manage multiple streams (SSH, call) may catch `StreamError::StreamClosed` per-stream and continue serving other streams on the same connection — only `ConnectionClosed` forces `handle()` to return.
|
||||||
|
|
||||||
|
The mapping is provided as a `From` impl so handlers can use the `?` operator:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl From<StreamError> for HandlerError {
|
||||||
|
fn from(e: StreamError) -> Self {
|
||||||
|
match e {
|
||||||
|
StreamError::ConnectionClosed => HandlerError::ConnectionClosed,
|
||||||
|
StreamError::StreamClosed => {
|
||||||
|
HandlerError::StreamError(io::Error::new(
|
||||||
|
io::ErrorKind::ConnectionReset,
|
||||||
|
"stream closed",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
StreamError::Timeout => {
|
||||||
|
HandlerError::StreamError(io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
"stream timed out",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
StreamError::Internal(e) => HandlerError::StreamError(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This `From` impl is the canonical conversion — handler examples that use
|
||||||
|
`.await?` on `accept_bi()` / `open_bi()` rely on it. The `StreamError` →
|
||||||
|
`HandlerError::StreamError(io::Error)` mapping is lossy by design: the
|
||||||
|
distinction between stream-level and connection-level errors is preserved
|
||||||
|
in `StreamError`, but once a handler propagates via `HandlerError`, the
|
||||||
|
endpoint treats all variants as "close the connection" (one-ALPN-per-
|
||||||
|
connection, ADR-006).
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
| Decision | ADR | Summary |
|
| Decision | ADR | Summary |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-17
|
last_updated: 2026-06-22-17
|
||||||
---
|
---
|
||||||
|
|
||||||
# Endpoint
|
# Endpoint
|
||||||
@@ -124,7 +124,7 @@ fn dispatch(connection) {
|
|||||||
match handlers.get(alpn) {
|
match handlers.get(alpn) {
|
||||||
Some(handler) => {
|
Some(handler) => {
|
||||||
let auth = AuthContext::from_connection(&connection);
|
let auth = AuthContext::from_connection(&connection);
|
||||||
let conn = Connection::new(connection);
|
let conn = Connection::from_quinn(connection); // or from_iroh
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = handler.handle(conn, &auth).await {
|
if let Err(e) = handler.handle(conn, &auth).await {
|
||||||
// log error, connection closes
|
// log error, connection closes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-19
|
last_updated: 2026-06-22-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# alknet-vault
|
# alknet-vault
|
||||||
@@ -38,8 +38,10 @@ cross the network.
|
|||||||
| ADR | Title | Relevance |
|
| ADR | Title | Relevance |
|
||||||
|-----|-------|-----------|
|
|-----|-------|-----------|
|
||||||
| [003](../../decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-vault's standalone position |
|
| [003](../../decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-vault's standalone position |
|
||||||
|
| [006](../../decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | ALPN versioning pattern for potential `alknet/vault/v2` |
|
||||||
| [005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | VaultProtocol uses irpc directly |
|
| [005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | VaultProtocol uses irpc directly |
|
||||||
| [008](../../decisions/008-secret-service-integration.md) | Vault Integration Point | CLI-embedded, capability source |
|
| [008](../../decisions/008-secret-service-integration.md) | Vault Integration Point | CLI-embedded, capability source |
|
||||||
|
| [010](../../decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Ed25519 as default curve for TLS raw key identity |
|
||||||
| [014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Capabilities carry vault-derived material |
|
| [014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Capabilities carry vault-derived material |
|
||||||
| [018](../../decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies |
|
| [018](../../decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies |
|
||||||
| [019](../../decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer is the sole caller |
|
| [019](../../decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer is the sole caller |
|
||||||
@@ -101,6 +103,21 @@ the full list.
|
|||||||
depth measure, not the primary control — the primary control is that
|
depth measure, not the primary control — the primary control is that
|
||||||
`DerivedKey` never crosses the call protocol wire (ADR-014).
|
`DerivedKey` never crosses the call protocol wire (ADR-014).
|
||||||
|
|
||||||
|
## Known Source Drift
|
||||||
|
|
||||||
|
The vault crate carries over source from the POC. The following items are
|
||||||
|
known divergences between the current source and the spec. All must be
|
||||||
|
corrected during implementation sync. This table is the single source of
|
||||||
|
truth for drift tracking — if an item is fixed in source, update this table.
|
||||||
|
|
||||||
|
| # | Item | Current source behavior | Target behavior (per spec) | Source location | Spec reference |
|
||||||
|
|---|------|------------------------|-----------------------------|-----------------|----------------|
|
||||||
|
| 1 | IV generation | `rand::random()` | `OsRng` (CSPRNG) | `encryption.rs` L133 | [encryption.md → Security Constraints](encryption.md#security-constraints), [service.md → Security Constraints](service.md#security-constraints) |
|
||||||
|
| 2 | RwLock `unwrap()` | `unwrap()` on every `RwLock` acquisition (L142, 161, 182, 191, 196, 227, 264, 307, 340, 367) | `unwrap_or_else(\|e\| e.into_inner())` for poisoned lock recovery | `service.rs` (see line numbers) | [service.md → Security Constraints](service.md#security-constraints) |
|
||||||
|
| 3 | `CURRENT_KEY_VERSION` | `1` (HD-derived, but v1 is reserved for TS PBKDF2 legacy per ADR-020) | `2` (HD-derived, per ADR-020) | `encryption.rs` | [encryption.md → Key Versioning](encryption.md#key-versioning), [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) |
|
||||||
|
| 4 | `spawn()` return value | Returns a fresh, unspawned `VaultServiceActor` as the second tuple element (the spawned actor is consumed by `run`) | Either drop the second return value (return only `Client<VaultProtocol>`) or restructure so the returned actor is the one that was spawned | `service.rs` `VaultServiceActor::spawn()` | [service.md → Actor Dispatch](service.md#actor-dispatch) |
|
||||||
|
| 5 | `HashMap::clear` zeroization | `KeyCache::clear()` removes entries and relies on `CachedKey`'s `Drop` impl for zeroization | Verify `HashMap::clear()` actually drops values (it does, but worth a test) | `cache.rs` | [service.md → Security Constraints](service.md#security-constraints) |
|
||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
|
|
||||||
The vault re-exports its primary types from the crate root:
|
The vault re-exports its primary types from the crate root:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-19
|
last_updated: 2026-06-22-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mnemonic and Key Derivation
|
# Mnemonic and Key Derivation
|
||||||
@@ -201,6 +201,7 @@ Helper functions construct parameterized paths:
|
|||||||
```rust
|
```rust
|
||||||
pub fn device_path(index: u32) -> String; // m/74'/0'/0'/{index}'
|
pub fn device_path(index: u32) -> String; // m/74'/0'/0'/{index}'
|
||||||
pub fn site_password_path(site_hash: &str) -> String; // m/74'/1'/0'/{site_hash}'
|
pub fn site_password_path(site_hash: &str) -> String; // m/74'/1'/0'/{site_hash}'
|
||||||
|
pub fn encryption_path_for_version(version: u32) -> String; // m/74'/2'/0'/{version-2}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Path semantics
|
### Path semantics
|
||||||
@@ -214,14 +215,6 @@ pub fn site_password_path(site_hash: &str) -> String; // m/74'/1'/0'/{site_hash}
|
|||||||
| `m/74'/2'/0'/0'` | Encryption key for external credentials | AES-256-GCM | Credential encryption (v2, see [encryption.md](encryption.md)) |
|
| `m/74'/2'/0'/0'` | Encryption key for external credentials | AES-256-GCM | Credential encryption (v2, see [encryption.md](encryption.md)) |
|
||||||
| `m/44'/60'/0'/0/0` | Ethereum signing key | secp256k1 | Ethereum signing (feature-gated) |
|
| `m/44'/60'/0'/0/0` | Ethereum signing key | secp256k1 | Ethereum signing (feature-gated) |
|
||||||
|
|
||||||
Helper functions construct parameterized paths:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub fn device_path(index: u32) -> String; // m/74'/0'/0'/{index}'
|
|
||||||
pub fn site_password_path(site_hash: &str) -> String; // m/74'/1'/0'/{site_hash}'
|
|
||||||
pub fn encryption_path_for_version(version: u32) -> String; // m/74'/2'/0'/{version-2}'
|
|
||||||
```
|
|
||||||
|
|
||||||
`encryption_path_for_version` maps a key version to its derivation path
|
`encryption_path_for_version` maps a key version to its derivation path
|
||||||
(ADR-021). v2 (current) maps to `m/74'/2'/0'/0'` (which is `PATHS::ENCRYPTION`);
|
(ADR-021). v2 (current) maps to `m/74'/2'/0'/0'` (which is `PATHS::ENCRYPTION`);
|
||||||
v3 maps to `m/74'/2'/0'/1'`; etc. This is the rotation mechanism — each
|
v3 maps to `m/74'/2'/0'/1'`; etc. This is the rotation mechanism — each
|
||||||
|
|||||||
@@ -16,19 +16,27 @@ iroh's `ProtocolHandler` trait demonstrates this: it takes a bidirectional QUIC
|
|||||||
|
|
||||||
A single `ProtocolHandler` trait replaces both `StreamInterface` and `MessageInterface`:
|
A single `ProtocolHandler` trait replaces both `StreamInterface` and `MessageInterface`:
|
||||||
|
|
||||||
|
> **Note**: The signature below was revised by ADR-007. The `handle()` method
|
||||||
|
> now receives a `Connection` (not a `BiStream`) — see ADR-007 for the
|
||||||
|
> current authoritative signature. The original signature is retained here
|
||||||
|
> for historical context.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ProtocolHandler: Send + Sync + 'static {
|
pub trait ProtocolHandler: Send + Sync + 'static {
|
||||||
/// The ALPN string this handler claims (e.g. b"alknet/ssh")
|
/// The ALPN string this handler claims (e.g. b"alknet/ssh")
|
||||||
fn alpn(&self) -> &'static [u8];
|
fn alpn(&self) -> &'static [u8];
|
||||||
|
|
||||||
/// Handle an incoming bidirectional QUIC stream
|
/// Handle an incoming connection (revised by ADR-007 to receive
|
||||||
async fn handle(&self, stream: BiStream, auth: &AuthContext) -> Result<(), HandlerError>;
|
/// `Connection` instead of `BiStream`)
|
||||||
|
async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `alpn()` returns a static byte string — the handler's ALPN identifier
|
- `alpn()` returns a static byte string — the handler's ALPN identifier
|
||||||
- `handle()` receives a `BiStream` (a joined `(SendStream, RecvStream)` implementing `AsyncRead + AsyncWrite`) and an `AuthContext` carrying the authenticated identity, and returns `HandlerError` on failure
|
- `handle()` receives a `Connection` (revised by ADR-007 from the original
|
||||||
|
`BiStream`) and an `AuthContext` carrying the authenticated identity, and
|
||||||
|
returns `HandlerError` on failure
|
||||||
- Every handler manages its own wire format — no shared framing, no StreamInterface/MessageInterface split
|
- Every handler manages its own wire format — no shared framing, no StreamInterface/MessageInterface split
|
||||||
- The `ListenerConfig` enum is eliminated — ALPN advertisement configuration replaces it
|
- The `ListenerConfig` enum is eliminated — ALPN advertisement configuration replaces it
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ The workspace decomposes into the following crates:
|
|||||||
|
|
||||||
| Crate | Responsibility | Depends on |
|
| Crate | Responsibility | Depends on |
|
||||||
|-------|---------------|------------|
|
|-------|---------------|------------|
|
||||||
| `alknet-core` | ProtocolHandler trait, ALPN router, endpoint, BiStream, AuthContext, IdentityProvider, config, ArcSwap dynamic config | tokio, quinn, rustls, irpc |
|
| `alknet-core` | ProtocolHandler trait, ALPN router, endpoint, BiStream, AuthContext, IdentityProvider, config, ArcSwap dynamic config | tokio, quinn, rustls, irpc, iroh (feature-gated, added by ADR-010) |
|
||||||
| `alknet-vault` | Local key vault: BIP39/SLIP-0010/AES-GCM key derivation, encryption, VaultProtocol dispatch | (standalone, no alknet-core) |
|
| `alknet-vault` | Local key vault: BIP39/SLIP-0010/AES-GCM key derivation, encryption, VaultProtocol dispatch | (standalone, no alknet-core) |
|
||||||
| `alknet-ssh` | SshAdapter (russh, SOCKS5, port forwarding) | alknet-core, russh |
|
| `alknet-ssh` | SshAdapter (russh, SOCKS5, port forwarding) | alknet-core, russh |
|
||||||
| `alknet-call` | CallAdapter (JSON-RPC via irpc, operation registry, pub/sub, access control, call protocol client, adapter traits) | alknet-core, irpc |
|
| `alknet-call` | CallAdapter (JSON-RPC via irpc, operation registry, pub/sub, access control, call protocol client, adapter traits) | alknet-core, irpc |
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ The ALPN dispatch model simplifies this: every handler receives the same `AuthCo
|
|||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
|
> **Note**: The original text of this decision described the handler
|
||||||
|
> "enriching or replacing" the `AuthContext`. This was superseded by
|
||||||
|
> ADR-011, which made `AuthContext` immutable in `handle()` (passed as
|
||||||
|
> `&AuthContext`). Handlers resolve identity into a local variable and
|
||||||
|
> store it on `Connection` via `set_identity()`. The text below has been
|
||||||
|
> updated to reflect the ADR-011 model.
|
||||||
|
|
||||||
Authentication and identity resolution live in `alknet-core` as shared infrastructure. Each handler presents credentials differently, but all resolve through the same `IdentityProvider`:
|
Authentication and identity resolution live in `alknet-core` as shared infrastructure. Each handler presents credentials differently, but all resolve through the same `IdentityProvider`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -36,7 +43,7 @@ Auth resolution is **hybrid** — the endpoint resolves what it can, and handler
|
|||||||
|
|
||||||
1. **Endpoint-level resolution** (before `handle()` is called): If the TLS handshake provides a client certificate, the endpoint resolves the fingerprint to an `Identity` and passes it in `AuthContext`. This is the case for SSH (where the key exchange happens at the protocol level, but the TLS layer may also provide information).
|
1. **Endpoint-level resolution** (before `handle()` is called): If the TLS handshake provides a client certificate, the endpoint resolves the fingerprint to an `Identity` and passes it in `AuthContext`. This is the case for SSH (where the key exchange happens at the protocol level, but the TLS layer may also provide information).
|
||||||
|
|
||||||
2. **Handler-level resolution** (inside `handle()`): For protocols that carry credentials in application frames (AuthToken in the first call frame, Bearer header in HTTP), the handler extracts the credential from the stream and calls `IdentityProvider` to resolve it. The handler then enriches or replaces the partial `AuthContext` with the fully resolved `Identity`.
|
2. **Handler-level resolution** (inside `handle()`): For protocols that carry credentials in application frames (AuthToken in the first call frame, Bearer header in HTTP), the handler extracts the credential from the stream and calls `IdentityProvider` to resolve it. The handler then resolves the `Identity` into a local variable and stores it on the `Connection` via `set_identity()` for observability — it does **not** mutate the `AuthContext` (which is passed as `&AuthContext`, an immutable reference — see ADR-011). The per-request identity (for ACL) is resolved separately by the `CallAdapter` at `call.requested` time.
|
||||||
|
|
||||||
The `AuthContext` passed to `handle()` may be partial — containing only transport-level information if no TLS client certificate was provided. Handlers must not assume `AuthContext` contains a fully resolved `Identity`. Each handler knows its own credential extraction protocol and is responsible for completing authentication.
|
The `AuthContext` passed to `handle()` may be partial — containing only transport-level information if no TLS client certificate was provided. Handlers must not assume `AuthContext` contains a fully resolved `Identity`. Each handler knows its own credential extraction protocol and is responsible for completing authentication.
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ fn dispatch(connection) {
|
|||||||
match handler {
|
match handler {
|
||||||
Some(h) => {
|
Some(h) => {
|
||||||
auth = AuthContext::from_connection(&connection)
|
auth = AuthContext::from_connection(&connection)
|
||||||
conn = Connection::new(connection)
|
conn = Connection::from_quinn(connection) // or from_iroh
|
||||||
tokio::spawn(h.handle(conn, &auth))
|
tokio::spawn(h.handle(conn, &auth))
|
||||||
}
|
}
|
||||||
None => connection.close()
|
None => connection.close()
|
||||||
|
|||||||
@@ -113,15 +113,23 @@ enumerate the internal call tree.
|
|||||||
|
|
||||||
### 3. Handler identity is carried on OperationContext
|
### 3. Handler identity is carried on OperationContext
|
||||||
|
|
||||||
`OperationContext` carries both the caller's identity (who invoked me) and the
|
> **Note**: This decision's `handler_identity: Option<Identity>` type was
|
||||||
handler's identity (who am I acting as):
|
> superseded by ADR-022, which replaced `Identity` with
|
||||||
|
> `CompositionAuthority` — a declared authority bundle that is not a peer
|
||||||
|
> identity and is not resolvable through `IdentityProvider`. The core
|
||||||
|
> decision (authority switch, not ACL skip) holds unchanged. See ADR-022
|
||||||
|
> Decision 2 for the current type.
|
||||||
|
|
||||||
|
`OperationContext` carries both the caller's identity (who invoked me) and
|
||||||
|
the handler's identity (who am I acting as):
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub struct OperationContext {
|
pub struct OperationContext {
|
||||||
pub request_id: String,
|
pub request_id: String,
|
||||||
pub parent_request_id: Option<String>,
|
pub parent_request_id: Option<String>,
|
||||||
pub identity: Option<Identity>, // Caller's identity (inbound)
|
pub identity: Option<Identity>, // Caller's identity (inbound)
|
||||||
pub handler_identity: Option<Identity>, // Handler's identity (composition authority)
|
// Type changed to Option<CompositionAuthority> by ADR-022:
|
||||||
|
pub handler_identity: Option<CompositionAuthority>, // Handler's composition authority
|
||||||
pub capabilities: Capabilities,
|
pub capabilities: Capabilities,
|
||||||
pub metadata: HashMap<String, Value>,
|
pub metadata: HashMap<String, Value>,
|
||||||
pub env: OperationEnv,
|
pub env: OperationEnv,
|
||||||
@@ -273,11 +281,14 @@ Principle of least privilege.
|
|||||||
is more important than debuggability from the wire.
|
is more important than debuggability from the wire.
|
||||||
|
|
||||||
6. **The handler identity is a full `Identity` (with scopes), not a special
|
6. **The handler identity is a full `Identity` (with scopes), not a special
|
||||||
principal type.** This reuses the existing `Identity` type and `IdentityProvider`
|
principal type.** ~~This reuses the existing `Identity` type and
|
||||||
infrastructure (ADR-004). If handler identities need different resolution
|
`IdentityProvider` infrastructure (ADR-004).~~ **Superseded by ADR-022
|
||||||
semantics (e.g., not resolvable through `IdentityProvider`), a separate type
|
Decision 2**: composition authority is a declared authority bundle
|
||||||
may be needed. The assumption is that the existing identity infrastructure
|
(`CompositionAuthority`), not a peer `Identity`. It is not resolvable
|
||||||
suffices.
|
through `IdentityProvider` and does not represent an inbound caller. The
|
||||||
|
distinction is necessary because a handler is not a network peer — its
|
||||||
|
authority is declared by the assembly layer at registration, not resolved
|
||||||
|
from credentials.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ abort — the handler that composes the child knows that, not the wire caller.
|
|||||||
Putting the policy on the wire payload would give the wire caller control
|
Putting the policy on the wire payload would give the wire caller control
|
||||||
over internal composition behavior it can't see.
|
over internal composition behavior it can't see.
|
||||||
|
|
||||||
**Why not per-operation declaration**: ADR-016 Assumption 5 says the policy
|
**Why not per-operation declaration**: Assumption 5 says the policy
|
||||||
is per-call, not per-operation. The same operation may need
|
is per-call, not per-operation. The same operation may need
|
||||||
`abort-dependents` in one composition context and `continue-running` in
|
`abort-dependents` in one composition context and `continue-running` in
|
||||||
another. A static property on `OperationSpec` can't express that.
|
another. A static property on `OperationSpec` can't express that.
|
||||||
|
|||||||
@@ -113,10 +113,11 @@ pub async fn from_call(
|
|||||||
The adapter:
|
The adapter:
|
||||||
1. Calls `services/list` on the remote node → gets the list of `External`
|
1. Calls `services/list` on the remote node → gets the list of `External`
|
||||||
operations
|
operations
|
||||||
2. Calls `services/schema` for each → gets the input/output JSON Schemas
|
2. Calls `services/schema` for each → gets the input/output JSON Schemas and
|
||||||
|
declared error_schemas (ADR-023)
|
||||||
3. For each discovered operation, constructs an `(OperationSpec, Handler)` pair:
|
3. For each discovered operation, constructs an `(OperationSpec, Handler)` pair:
|
||||||
- The spec mirrors the remote operation's name, namespace, type, schemas,
|
- The spec mirrors the remote operation's name, namespace, type, schemas
|
||||||
and access control
|
(input, output, and error_schemas — ADR-023), and access control
|
||||||
- The handler sends `call.requested` through the `CallConnection` and awaits
|
- The handler sends `call.requested` through the `CallConnection` and awaits
|
||||||
`call.responded` (or streams for subscriptions)
|
`call.responded` (or streams for subscriptions)
|
||||||
4. The caller registers these pairs in their local registry
|
4. The caller registers these pairs in their local registry
|
||||||
@@ -272,8 +273,8 @@ same as `from_openapi` receives HTTP credentials.
|
|||||||
remote call failures are handled the same as local handler failures.
|
remote call failures are handled the same as local handler failures.
|
||||||
|
|
||||||
4. **`from_call`-registered operations mirror the remote spec.** The imported
|
4. **`from_call`-registered operations mirror the remote spec.** The imported
|
||||||
`OperationSpec` has the same name, namespace, type, schemas, and access
|
`OperationSpec` has the same name, namespace, type, schemas (input, output,
|
||||||
control as the remote operation. If the remote operation changes (new
|
and error_schemas per ADR-023), and access control as the remote operation. If the remote operation changes (new
|
||||||
schema, renamed), the imported spec is stale until re-import. The
|
schema, renamed), the imported spec is stale until re-import. The
|
||||||
assumption is that re-import happens on reconnection or is triggered
|
assumption is that re-import happens on reconnection or is triggered
|
||||||
explicitly. Hot-swapping imported specs is a two-way door.
|
explicitly. Hot-swapping imported specs is a two-way door.
|
||||||
|
|||||||
@@ -123,9 +123,9 @@ impl VaultServiceHandle {
|
|||||||
```
|
```
|
||||||
|
|
||||||
`decrypt` now derives the key at the path **indicated by
|
`decrypt` now derives the key at the path **indicated by
|
||||||
`encrypted.key_version`** — not always at `PATHS::ENCRYPTION`. This is
|
`encrypted.key_version`** — not always at `PATHS::ENCRYPTION`. This corrects
|
||||||
the fix for the W1 drift issue from the vault review: the current source
|
a source drift: the current source ignores `key_version` for key selection;
|
||||||
ignores `key_version` for key selection; the spec now makes it functional.
|
the spec now makes it functional.
|
||||||
|
|
||||||
### 3. `rotate` method
|
### 3. `rotate` method
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Proposed
|
Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -120,7 +120,8 @@ pub enum OperationProvenance {
|
|||||||
FromOpenAPI,
|
FromOpenAPI,
|
||||||
/// MCP forwarding stub (from_mcp), leaf — cannot compose.
|
/// MCP forwarding stub (from_mcp), leaf — cannot compose.
|
||||||
FromMCP,
|
FromMCP,
|
||||||
/// QUIC forwarding stub (from_call), leaf locally — cannot compose.
|
/// QUIC forwarding stub (from_call). Leaf in the local registry —
|
||||||
|
/// forwards calls to a remote node; cannot compose locally.
|
||||||
FromCall,
|
FromCall,
|
||||||
/// JSON Schema definition (from_jsonschema), no handler — schema only.
|
/// JSON Schema definition (from_jsonschema), no handler — schema only.
|
||||||
FromJsonSchema,
|
FromJsonSchema,
|
||||||
@@ -134,7 +135,7 @@ pub enum OperationProvenance {
|
|||||||
| `Local` | Yes | Yes — scopes set by assembly layer | External or Internal (assembly declares) | Trusted code |
|
| `Local` | Yes | Yes — scopes set by assembly layer | External or Internal (assembly declares) | Trusted code |
|
||||||
| `FromOpenAPI` | No (leaf) | No | Internal | HTTP endpoint trusted; handler is a forwarding stub |
|
| `FromOpenAPI` | No (leaf) | No | Internal | HTTP endpoint trusted; handler is a forwarding stub |
|
||||||
| `FromMCP` | No (leaf) | No | Internal | MCP server trusted; handler is a forwarding stub |
|
| `FromMCP` | No (leaf) | No | Internal | MCP server trusted; handler is a forwarding stub |
|
||||||
| `FromCall` | No (leaf locally) | No | Internal | Remote node trusted; handler is a forwarding stub |
|
| `FromCall` | No (leaf in local registry) | No | Internal | Remote node trusted; handler is a forwarding stub |
|
||||||
| `FromJsonSchema` | N/A (no handler) | No | N/A | N/A |
|
| `FromJsonSchema` | N/A (no handler) | No | N/A | N/A |
|
||||||
| `Session` | Yes (within sandbox) | Yes — scopes set by assembly layer at sandbox creation | Internal always | Untrusted code in sandbox |
|
| `Session` | Yes (within sandbox) | Yes — scopes set by assembly layer at sandbox creation | Internal always | Untrusted code in sandbox |
|
||||||
|
|
||||||
@@ -178,6 +179,24 @@ pub struct CompositionAuthority {
|
|||||||
/// handler can reach in composition.
|
/// handler can reach in composition.
|
||||||
pub resources: HashMap<String, Vec<String>>,
|
pub resources: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CompositionAuthority {
|
||||||
|
/// `None` — for leaves that don't compose (convenience for
|
||||||
|
/// `composition_authority: CompositionAuthority::none()`).
|
||||||
|
pub fn none() -> Option<Self> { None }
|
||||||
|
|
||||||
|
/// Construct a composition authority with the given label and scopes.
|
||||||
|
pub fn new(
|
||||||
|
label: &str,
|
||||||
|
scopes: impl IntoIterator<Item = String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
label: label.to_string(),
|
||||||
|
scopes: scopes.into_iter().collect(),
|
||||||
|
resources: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This supersedes ADR-015's Assumption 6. ADR-015's core decision (authority
|
This supersedes ADR-015's Assumption 6. ADR-015's core decision (authority
|
||||||
@@ -212,6 +231,23 @@ pub struct ScopedOperationEnv {
|
|||||||
/// parameterized-dispatch attack surface.
|
/// parameterized-dispatch attack surface.
|
||||||
pub allowed_operations: HashSet<String>,
|
pub allowed_operations: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ScopedOperationEnv {
|
||||||
|
/// Empty set — for leaves that don't compose (no reachable operations).
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self { allowed_operations: HashSet::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct from an iterable of operation names.
|
||||||
|
pub fn new(ops: impl IntoIterator<Item = String>) -> Self {
|
||||||
|
Self { allowed_operations: ops.into_iter().collect() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given operation name is reachable.
|
||||||
|
pub fn allows(&self, name: &str) -> bool {
|
||||||
|
self.allowed_operations.contains(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. The registration bundle carries all three
|
### 4. The registration bundle carries all three
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Proposed
|
Accepted
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-20
|
last_updated: 2026-06-22-20
|
||||||
---
|
---
|
||||||
|
|
||||||
# Alknet Overview
|
# Alknet Overview
|
||||||
@@ -222,7 +222,7 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questi
|
|||||||
- **OQ-01**: BiStream type definition (resolved: trait, Connection parameter — see ADR-007)
|
- **OQ-01**: BiStream type definition (resolved: trait, Connection parameter — see ADR-007)
|
||||||
- **OQ-02**: AuthContext resolution timing (resolved: hybrid — see ADR-004)
|
- **OQ-02**: AuthContext resolution timing (resolved: hybrid — see ADR-004)
|
||||||
- **OQ-03**: ALPN string naming convention (resolved: see ADR-006)
|
- **OQ-03**: ALPN string naming convention (resolved: see ADR-006)
|
||||||
- **OQ-04**: Dynamic handler registration at runtime vs static at startup (two-way door, defer to implementation)
|
- **OQ-04**: Dynamic handler registration (resolved: static at startup — see ADR-010)
|
||||||
- **OQ-08**: Vault integration point (resolved: CLI-embedded, assembly-layer only — see ADR-008, ADR-014, ADR-018, ADR-019)
|
- **OQ-08**: Vault integration point (resolved: CLI-embedded, assembly-layer only — see ADR-008, ADR-014, ADR-018, ADR-019)
|
||||||
- **OQ-16**: Safe vault operations for call protocol exposure (resolved: none for now — see ADR-014)
|
- **OQ-16**: Safe vault operations for call protocol exposure (resolved: none for now — see ADR-014)
|
||||||
- **OQ-20**: Encryption key derivation (resolved: HD derivation, not PBKDF2 — see ADR-020)
|
- **OQ-20**: Encryption key derivation (resolved: HD derivation, not PBKDF2 — see ADR-020)
|
||||||
@@ -234,11 +234,11 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questi
|
|||||||
| Failure | Behavior |
|
| Failure | Behavior |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| ALPN negotiation fails (no intersection) | TLS handshake fails — correct behavior, the client and server have no protocol in common |
|
| ALPN negotiation fails (no intersection) | TLS handshake fails — correct behavior, the client and server have no protocol in common |
|
||||||
| Handler `handle()` returns `HandlerError` | Endpoint logs the error, closes the QUIC stream. Other streams on the same connection are unaffected |
|
| Handler `handle()` returns `HandlerError` | Endpoint logs the error, closes the QUIC connection. Other connections are unaffected |
|
||||||
| Handler panics | The handler's task is caught by tokio's panic handling. The stream is closed. Other streams and connections are unaffected |
|
| Handler panics | The handler's task is caught by tokio's panic handling. The connection is dropped. Other connections are unaffected |
|
||||||
| `IdentityProvider` returns `None` | AuthContext is partial. If the handler requires authentication and cannot extract credentials from the stream, it closes the stream with an auth error |
|
| `IdentityProvider` returns `None` | AuthContext is partial. If the handler requires authentication and cannot extract credentials from the stream, it closes the connection with an auth error |
|
||||||
| Config reload fails | `ArcSwap<DynamicConfig>` keeps the previous valid config. Error is logged. No service interruption |
|
| Config reload fails | `ArcSwap<DynamicConfig>` keeps the previous valid config. Error is logged. No service interruption |
|
||||||
| BiStream read/write error | QUIC stream-level error. The handler detects this as an I/O error and returns from `handle()`. The connection itself may remain open for other streams |
|
| BiStream read/write error | QUIC stream-level error. The handler detects this as an I/O error and returns from `handle()`. The connection itself may remain open for other streams — but since each handler owns a full `Connection` (one ALPN per connection, ADR-006), a stream error typically causes the handler to return, closing the connection |
|
||||||
|
|
||||||
## What Stays from the Previous Implementation
|
## What Stays from the Previous Implementation
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user