Files
alknet/tasks/call/registry/operation-context.md

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
call/registry/operation-spec
core/core-types
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 with call.requested event's id field. For wire calls, this is the client-generated ID. For composed calls, generated by OperationEnv::invoke() via generate_request_id() (UUID v4 or parent_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 sent call.requested. For internal calls, the parent handler's handler_identity (propagated through OperationEnv::invoke()).
  • handler_identity: the composition authority of the handler processing this call. None for 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 through OperationEnv::invoke() — nested calls get fresh metadata. The tracing link is parent_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 calls context.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). Default AbortDependents. ContinueRunning is opt-in for long-running work. Set by the composing handler via invoke(), not by the wire caller.
  • deadline: for this call and all descendants. Set by build_root_context to now + 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: when true, this call originated from composition (a handler calling another operation via OperationEnv), not from a wire request. This switches the authority context: ACL runs against handler_identity, not identity. Module-private for writes; read via is_internal(). Only set by OperationEnv::invoke() (true) or CallAdapter dispatch 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

  • OperationContext struct with all 10 fields
  • internal field is pub(crate) (module-private for writes)
  • is_internal() method exposes read access
  • AbortPolicy enum with AbortDependents, ContinueRunning
  • Default for AbortPolicy returns AbortDependents
  • CompositionAuthority struct with label, scopes, resources
  • CompositionAuthority::none() returns None
  • CompositionAuthority::new(label, scopes) constructor
  • CompositionAuthority::as_identity() produces synthetic Identity for ACL
  • ScopedOperationEnv struct with allowed set
  • ScopedOperationEnv::empty(), new(), allows() methods
  • generate_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-call succeeds
  • cargo clippy -p alknet-call succeeds 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 internal field 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.