210 lines
9.7 KiB
Markdown
210 lines
9.7 KiB
Markdown
---
|
|
id: call/registry/operation-context
|
|
name: Implement OperationContext, AbortPolicy, CompositionAuthority, and ScopedOperationEnv
|
|
status: completed
|
|
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
|
|
|
|
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. |