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.
This commit is contained in:
204
tasks/call/registry/operation-context.md
Normal file
204
tasks/call/registry/operation-context.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
id: call/registry/operation-context
|
||||
name: Implement OperationContext, AbortPolicy, CompositionAuthority, and ScopedOperationEnv
|
||||
status: pending
|
||||
depends_on: [call/registry/operation-spec, core/core-types]
|
||||
scope: broad
|
||||
risk: high
|
||||
impact: component
|
||||
level: 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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
Reference in New Issue
Block a user