Phase 0a — ADRs (9 new): - ADR-026: Transport/interface separation (three-layer model) - ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI) - ADR-028: Auth as irpc service (AuthProtocol behind feature flag) - ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core) - ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle) - ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware) - ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries) - ADR-033: OperationEnv universal composition (three dispatch paths) - ADR-034: Head/worker terminology (replace hub/spoke) Phase 0b — New spec documents (7): - identity.md, services.md, interface.md, configuration.md, storage.md, flowgraph.md, secret-service.md Updated existing docs: - auth.md: reference identity.md for canonical definitions, add AuthProtocol - open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25 - README.md: add all new docs, ADRs 026-034 Marked 19 architecture tasks as completed.
130 lines
5.5 KiB
Markdown
130 lines
5.5 KiB
Markdown
# ADR-033: OperationEnv as Universal Composition Mechanism
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
The `@alkdev/operations` TypeScript package defines `OperationEnv` as a
|
|
universal composition mechanism. A handler receives `context.env[namespace][op](input)`
|
|
and can invoke any registered operation regardless of whether it runs locally, in
|
|
an irpc service on the same cluster, or on a remote node via call protocol.
|
|
|
|
The research documents define three dispatch paths:
|
|
1. **Local dispatch** — direct function call through the operation registry
|
|
2. **Service dispatch** — irpc protocol call to a service backend
|
|
3. **Remote dispatch** — call protocol `EventEnvelope` to a remote node
|
|
|
|
Without a formal decision, irpc services could be seen as a replacement for
|
|
OperationEnv or for the call protocol. They are not — irpc is one dispatch
|
|
backend for OperationEnv, not a replacement for anything. The call protocol is
|
|
another dispatch backend. OperationEnv unifies them from the handler's
|
|
perspective.
|
|
|
|
The three communication patterns in the system (ADR-032) are:
|
|
- Domain events (Honker streams) — internal to the owning service
|
|
- irpc service calls — synchronous, in-cluster
|
|
- Call protocol events — asynchronous, cross-node
|
|
|
|
irpc services and call protocol operations serve different scopes but must
|
|
compose cleanly through OperationEnv.
|
|
|
|
## Decision
|
|
|
|
**OperationEnv is the universal composition mechanism that all operation
|
|
handlers receive. It provides namespace + operation name → invoke with input,
|
|
return output, regardless of dispatch path.**
|
|
|
|
### OperationEnv Behavioral Contract
|
|
|
|
```rust
|
|
// The behavioral contract: given a namespace and operation name, invoke the
|
|
// operation with the given input and return the output. The handler neither
|
|
// knows nor cares whether the dispatch is local, via irpc, or via call protocol.
|
|
pub trait OperationEnv: Send + Sync {
|
|
fn invoke(&self, namespace: &str, operation: &str, input: Value) -> ResponseEnvelope;
|
|
}
|
|
```
|
|
|
|
The Rust implementation may use typed method dispatch or a registry behind the
|
|
scenes, but the handler-facing API must preserve this contract.
|
|
|
|
### Three Dispatch Paths
|
|
|
|
OperationEnv resolves each call to one of three dispatch backends:
|
|
|
|
| Path | Mechanism | Serialization | Scope |
|
|
|------|-----------|---------------|-------|
|
|
| Local | Direct function call through registry | None (in-process) | Same process |
|
|
| Service | irpc protocol enum dispatch | postcard (binary) | Same cluster |
|
|
| Remote | Call protocol `EventEnvelope` | JSON | Cross-node |
|
|
|
|
All three produce the same `ResponseEnvelope`. The handler always calls
|
|
`context.env.invoke("secrets", "derive", input)` and gets a `ResponseEnvelope`
|
|
back.
|
|
|
|
### Service Assembly
|
|
|
|
The deployment topology determines which dispatch path each operation uses:
|
|
|
|
```rust
|
|
// Minimal deployment (single node, all local)
|
|
let env = OperationEnv::local(local_registry);
|
|
|
|
// Production deployment (mix of local and remote)
|
|
let env = OperationEnv::new()
|
|
.local("auth", auth_registry) // Auth runs locally
|
|
.local("config", config_registry) // Config runs locally
|
|
.service("secrets", secret_irpc_client) // Secret service via irpc
|
|
.remote("worker-1", call_protocol_conn) // Worker-1 operations via call protocol
|
|
```
|
|
|
|
### irpc Services Are One Dispatch Backend
|
|
|
|
irpc services (`AuthProtocol`, `SecretProtocol`, `ConfigProtocol`) define the
|
|
wire format for in-cluster communication. They are Rust-to-Rust, type-safe,
|
|
and efficient. But they are not a replacement for OperationEnv or for the call
|
|
protocol. They are one dispatch backend.
|
|
|
|
An irpc service can be exposed as a call protocol operation:
|
|
`/head/auth/verify` receives a call protocol event and internally calls
|
|
`AuthProtocol::VerifyPubkey` via irpc. The layers compose:
|
|
|
|
```
|
|
Call Protocol (Layer 3, external, JSON)
|
|
└── irpc Service (Layer 3, internal, postcard)
|
|
└── Honker Streams (Domain events, within service boundary)
|
|
```
|
|
|
|
### Adapters Map to OperationEnv
|
|
|
|
HTTP (`POST /v1/{namespace}/{op}`), MCP (`tools/call`), DNS
|
|
(`{op}.{namespace}.alk.dev TXT?`), and call protocol
|
|
(`/call.requested`) all resolve through OperationEnv. This is what makes
|
|
operations universally composable across all interfaces.
|
|
|
|
## Consequences
|
|
|
|
- **Positive**: Handlers compose through a single interface. Adding a new
|
|
dispatch path (e.g., a new irpc service) doesn't change handler code.
|
|
- **Positive**: irpc and call protocol coexist naturally. The handler doesn't
|
|
know which path was taken.
|
|
- **Positive**: Adapters (MCP, HTTP, DNS) map to operations through the same
|
|
OperationEnv interface. One handler, multiple dispatch paths.
|
|
- **Positive**: Deployment topology determines dispatch, not code. Same handler
|
|
works locally, in-cluster, or cross-node.
|
|
- **Negative**: OperationEnv is a new abstraction that must coexist with the
|
|
existing call protocol handler pattern. The registry currently maps paths to
|
|
handlers; OperationEnv adds namespace-aware composition on top.
|
|
- **Negative**: The `@alkdev/operations` TypeScript `HashMap<String,
|
|
HashMap<String, fn>>` model needs idiomatic Rust translation. The behavioral
|
|
contract must match, but the implementation can differ.
|
|
|
|
## References
|
|
|
|
- [research/services.md](../../research/services.md) — OperationContext, OperationEnv
|
|
- [research/integration-plan.md](../../research/integration-plan.md) — Phase 1.5, OperationEnv wiring
|
|
- [ADR-032](032-event-boundary-discipline.md) — Event boundary discipline
|
|
- [ADR-024](024-bidirectional-call-protocol.md) — Bidirectional call protocol
|
|
- [ADR-025](025-handler-spec-separation.md) — Handler/spec separation |