Files
alknet/tasks/call/registry/operation-context.md
glm-5.2 098fd8b9b9 tasks: decompose vault, core, call crates into 28 atomic implementation tasks
Break down the three initial crates (alknet-vault, alknet-core, alknet-call)
into dependency-ordered task files for implementation agents.

Structure:
- tasks/vault/ (10 tasks) — drift fixes from ADR-025/026 refactor, review,
  spec sync. Vault is independent and can run fully in parallel with core/call.
- tasks/core/ (6 tasks) — crate init, core types, config, auth, endpoint,
  review. Core is foundational; call depends on it.
- tasks/call/ (12 tasks) — split into registry/ and protocol/ topic subdirs
  reflecting the two subsystems. CallAdapter is the merge point.

Key decisions:
- Drifts 3+9+10 grouped as one task (key-versioning-rotation) — the complete
  ADR-021 rotation feature that doesn't compile in pieces
- Reviews injected at end of each crate phase (vault, core, call)
- Vault spec-sync task removes the drift table and bumps doc status to stable
- ACME deferred in core/endpoint (noted as TODO; X509 manual certs for now)
- OperationEnv kept as a trait (load-bearing for ADR-024 layering)

Validated: 28 tasks, no cycles, 11 generations of parallel work.
Critical path runs through call (11 tasks). Vault completes by generation 4.
6 high-risk tasks identified (21%): irpc-removal, endpoint, operation-context,
operation-env, call-adapter, abort-cascade.
2026-06-23 12:41:47 +00:00

9.2 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 pending
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

To be filled on completion