Files
alknet/docs/architecture/decisions/033-operationenv-irpc-call-protocol.md
glm-5.1 d3633b7839 docs: complete Phase 0 architecture — spec updates, review fixes, and link portability
Update four existing specs (overview, server, napi-and-pubsub, call-protocol) to
reflect Phase 0 decisions: three-layer model, IdentityProvider, ForwardingPolicy,
OperationEnv, static/dynamic config split. Review all 9 Phase 0a ADRs (026-034)
for consistency. Fix 4 critical issues from architecture review: missing OQ-SVC-05
in open-questions.md, deprecated hub terminology, undefined AuthService and noq
terms. Replace inline OQ text with cross-references per format rules. Add
ConfigServiceImpl definition to configuration.md. Port absolute workspace paths
to project-relative links by copying referenced docs (feasibility, certbot,
fail2ban, event_source_types) into docs/research/.
2026-06-07 11:27:52 +00:00

5.7 KiB

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

// 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:

// 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