9.7 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||
|---|---|---|---|---|---|---|---|---|---|
| call/registry/operation-context | Implement OperationContext, AbortPolicy, CompositionAuthority, and ScopedOperationEnv | completed |
|
broad | high | component | implementation |
Description
Implement the operation context types in src/registry/context.rs. This is
the highest-density task in the call crate — OperationContext has 10 fields,
each tied to an ADR. The authority-switch semantics (internal: true → ACL
against handler_identity, not identity) is where ADR-015, ADR-022, and
ADR-024 converge.
Read ADR-015, ADR-022, and ADR-024 before starting this task.
OperationContext
pub struct OperationContext {
pub request_id: String,
pub parent_request_id: Option<String>,
pub identity: Option<Identity>, // Caller's identity (inbound)
pub handler_identity: Option<CompositionAuthority>, // Handler's composition authority (ADR-022)
pub capabilities: Capabilities,
pub metadata: HashMap<String, Value>,
pub scoped_env: ScopedOperationEnv, // Reachability set (data, ADR-022)
pub env: Arc<dyn OperationEnv + Send + Sync>, // Composition dispatch trait (ADR-024)
pub abort_policy: AbortPolicy, // ADR-016 Decision 6
pub deadline: Option<Instant>,
pub(crate) internal: bool, // Module-private for writes (ADR-015)
}
Field-by-field:
request_id: correlates withcall.requestedevent'sidfield. For wire calls, this is the client-generated ID. For composed calls, generated byOperationEnv::invoke()viagenerate_request_id()(UUID v4 orparent_id + "-" + counter). Deterministic IDs must not be used — they collide across concurrent invocations, corrupting PendingRequestMap and the abort-cascade tree.parent_request_id: set when this call was initiated by another operation (via OperationEnv). Records the agency chain — the call tree is the principal→agent chain (ADR-015).identity: the authenticated caller (from IdentityProvider) — inbound auth (who is calling me). For external calls, who sentcall.requested. For internal calls, the parent handler'shandler_identity(propagated throughOperationEnv::invoke()).handler_identity: the composition authority of the handler processing this call.Nonefor leaves (FromOpenAPI/FromMCP/FromCall) — they don't compose.Some(...)for Local/Session ops. For internal calls (internal: true), ACL checks against this authority (ADR-015, ADR-022). This is NOT a peer Identity — it's a declared authority bundle set at registration.capabilities: outbound credentials the handler may use (decrypted API keys, scoped vault access). From the registration bundle (ADR-022).metadata: request-scoped context (tracing IDs, connection info). Must not hold secret material (ADR-014). Does not propagate throughOperationEnv::invoke()— nested calls get fresh metadata. The tracing link isparent_request_id, not metadata propagation.scoped_env: the reachability set — operations this handler may compose. Populated from the registration bundle (ADR-022). This is data (a struct), not a dispatch trait.None/empty for leaves.env: the composition dispatch trait (Arc<dyn OperationEnv + Send + Sync>). A handler callscontext.env.invoke(...)to compose children. This is a trait object, not a concrete struct — enables registry layering (ADR-024).abort_policy: for this call's descendants (ADR-016 Decision 6). DefaultAbortDependents.ContinueRunningis opt-in for long-running work. Set by the composing handler viainvoke(), not by the wire caller.deadline: for this call and all descendants. Set bybuild_root_contexttonow + CallAdapter.default_timeout(default 30s). Composed calls inherit the parent's deadline (children do NOT get a fresh 30s).None= unbounded (long-running subscriptions).internal: whentrue, this call originated from composition (a handler calling another operation via OperationEnv), not from a wire request. This switches the authority context: ACL runs againsthandler_identity, notidentity. Module-private for writes; read viais_internal(). Only set byOperationEnv::invoke()(true) orCallAdapterdispatch path (false).
AbortPolicy
pub enum AbortPolicy {
AbortDependents, // default — abort cascades to all non-terminal descendants
ContinueRunning, // opt-in — started descendants continue, unstarted aborted
}
impl Default for AbortPolicy {
fn default() -> Self { Self::AbortDependents }
}
CompositionAuthority
pub struct CompositionAuthority {
pub label: String, // e.g., "agent-chat" — not a peer id
pub scopes: Vec<String>, // e.g., ["llm:call", "fs:read"]
pub resources: HashMap<String, Vec<String>>, // e.g., {"service": ["vastai"]}
}
impl CompositionAuthority {
pub fn none() -> Option<Self> { None } // Convenience for leaves
pub fn new(label: &str, scopes: impl IntoIterator<Item = String>) -> Self { ... }
pub fn as_identity(&self) -> Option<Identity> { ... } // Synthetic Identity for ACL
}
The declared authority the handler operates under when composing children.
None for leaves. This replaces ADR-015's handler_identity: Identity — it's
not a peer identity, it's a declared authority bundle. See ADR-022.
as_identity() produces a synthetic Identity from the authority (label as
id, scopes, resources) for ACL checking against AccessControl.
ScopedOperationEnv
pub struct ScopedOperationEnv {
allowed: HashSet<String>, // operation names this handler may reach
}
impl ScopedOperationEnv {
pub fn empty() -> Self;
pub fn new(ops: impl IntoIterator<Item = impl Into<String>>) -> Self;
pub fn allows(&self, name: &str) -> bool; // is this op in the reachability set?
}
The reachability set — the operations this handler may reach via env.invoke().
Populated from the registration bundle (ADR-022). This is data, not a dispatch
trait. The reachability check in OperationEnv::invoke() consults
scoped_env.allows(&name). None/empty for leaves.
OperationContext methods
impl OperationContext {
pub fn is_internal(&self) -> bool { self.internal }
}
The internal field is pub(crate) — only OperationEnv::invoke() and the
CallAdapter dispatch path can set it. Handlers read via is_internal().
generate_request_id
pub(crate) fn generate_request_id() -> String {
// UUID v4 — must be unique across concurrent invocations
// Deterministic IDs (e.g., format!("env-{name}")) MUST NOT be used
}
Use the uuid crate (already a dependency). This is module-internal — called
by OperationEnv::invoke() for composed calls.
Acceptance Criteria
OperationContextstruct with all 10 fieldsinternalfield ispub(crate)(module-private for writes)is_internal()method exposes read accessAbortPolicyenum with AbortDependents, ContinueRunningDefault for AbortPolicyreturnsAbortDependentsCompositionAuthoritystruct with label, scopes, resourcesCompositionAuthority::none()returnsNoneCompositionAuthority::new(label, scopes)constructorCompositionAuthority::as_identity()produces synthetic Identity for ACLScopedOperationEnvstruct with allowed setScopedOperationEnv::empty(),new(),allows()methodsgenerate_request_id()produces UUID v4 (unique, non-deterministic)- Unit test: ScopedOperationEnv::allows (in set → true, not in set → false)
- Unit test: CompositionAuthority::as_identity produces correct Identity
- Unit test: AbortPolicy default is AbortDependents
cargo test -p alknet-callsucceedscargo clippy -p alknet-callsucceeds with no warnings
References
- docs/architecture/crates/call/operation-registry.md — OperationContext, AbortPolicy, CompositionAuthority, ScopedOperationEnv
- docs/architecture/decisions/015-privilege-model-and-authority-context.md — ADR-015 (internal flag, authority switch)
- docs/architecture/decisions/016-abort-cascade-for-nested-calls.md — ADR-016 (AbortPolicy)
- docs/architecture/decisions/022-handler-registration-provenance-and-composition-authority.md — ADR-022 (CompositionAuthority, ScopedOperationEnv)
- docs/architecture/decisions/024-operation-registry-layering.md — ADR-024 (env as trait object)
Notes
Read ADR-015, ADR-022, and ADR-024 before starting. This is the highest-density task in the call crate. OperationContext has 10 fields, each tied to an ADR. The authority-switch semantics (internal: true → ACL against handler_identity, not identity) is where three ADRs converge. The
internalfield is module-private for writes — only OperationEnv::invoke() and the CallAdapter dispatch path set it. Metadata does NOT propagate through composition (security constraint, ADR-014). Request IDs must be unique (UUID v4) — deterministic IDs corrupt PendingRequestMap and abort-cascade tree.
Summary
Implemented OperationContext (10 fields, internal pub(crate) + is_internal()),
AbortPolicy (AbortDependents default), CompositionAuthority (none/new/as_identity
for ACL), ScopedOperationEnv (empty/new/allows), and generate_request_id (UUID v4)
in registry/context.rs. Added minimal OperationEnv trait in registry/env.rs
(invoke/invoke_with_policy/contains) so the env field compiles — the operation-env
task will expand with LocalOperationEnv and CompositeOperationEnv. 37 unit tests
pass; clippy clean. Merged to develop.