docs(arch): call-completion — ADR-028 peer-scoped filtering + client-and-adapters spec + tasks

Resolves the four gap-analysis decisions (DC-1..4) blocking the alknet-call
client/adapter surface specced in ADR-017:

- ADR-028 (new): locks the one-way door for DC-1 — CallClient registry is
  default-deny (remote_safe: bool on HandlerRegistration, default false across
  all provenance); share-global is an explicit trusted-peer opt-in; filtering
  is a dispatch-time read over the single Layer-0 registry, not a copy.
- client-and-adapters.md (new spec): operationally fills the gap ADR-017 left
  to implementation — CallClient, from_call, from_jsonschema, OperationAdapter
  trait, adapter location map, no-env-vars invariant, exchange-of-operations
  pattern. Keeps call-protocol.md and operation-registry.md under the
  700-line split threshold.
- ADR-017 amended: records DC-2/3/4 v1 defaults (auto-on-reconnect,
  error-on-collision, Result error type) and points DC-1 at ADR-028.
- OQ-25..28 (new): two-way-door remainders (remote_safe shape, AdapterError
  variants, re-import trigger, namespace collision) with v1 defaults recorded.
- Index/cross-ref updates across READMEs and the two existing call specs.

Tasks: 6 task files under tasks/call/ decomposing the completion work along
the gap-analysis priority order — remote-safe-marking (one-way door, first)
→ call-client (phase-risk) → from-call → operation-adapter-trait →
from-jsonschema (parallel with call-client) → review-completion. Graph
validated with taskgraph; parallelism designed in (from-jsonschema runs
concurrent with call-client/from-call once the trait lands).
This commit is contained in:
2026-06-26 12:25:13 +00:00
parent 6940d9858d
commit 2649e068e5
14 changed files with 1817 additions and 11 deletions

View File

@@ -0,0 +1,125 @@
---
id: call/client/operation-adapter-trait
name: Define OperationAdapter async trait + AdapterError enum (ADR-017 §5, DC-4/OQ-26)
status: pending
depends_on: [call/registry/handler-registration]
scope: narrow
risk: low
impact: project
level: implementation
---
## Description
Define the `OperationAdapter` async trait and the `AdapterError` crate-level
enum in `src/client/adapter.rs` (or `src/registry/adapter.rs` — pick the
module that keeps the trait near the types it produces). This is the #3 gap
(enabling, not blocking) — `from_call` can be built as a free function before
the trait exists, but the trait is needed before `alknet-http`'s
`from_openapi`/`from_mcp` adapters can be built. Small, standalone, unblocks
`alknet-http` Phase 1.
### The trait (ADR-017 §5)
```rust
#[async_trait]
pub trait OperationAdapter: Send + Sync {
async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>;
}
```
The trait is **async** because `from_call` requires async discovery
(`services/list` + `services/schema` over a QUIC connection). Sync adapters
(`from_openapi`, `from_mcp` reading a static spec) trivially satisfy an async
trait — their `import()` bodies contain no `.await` points. This is locked by
ADR-017 §5; the async/sync question is decided.
The return type is `Vec<HandlerRegistration>` (not `(OperationSpec, Handler)`
pairs) — ADR-022 changed the registration API to the bundle shape, and
adapters must produce bundles. Adapter convenience methods construct bundles
with `composition_authority: None` and `scoped_env: None` for the leaf ops they
produce.
The `to_*` adapters (`to_openapi`, `to_mcp`) are outbound projections, not
`OperationAdapter` implementations — they consume the registry, they don't
produce entries for it (ADR-017 §5). Do not implement `to_*` here.
### AdapterError (DC-4, OQ-26)
ADR-017 §5 showed `async fn import(&self) -> Vec<HandlerRegistration>` with no
error type. A real implementation needs to handle failures. The trait returns
`Result<Vec<HandlerRegistration>, AdapterError>` where `AdapterError` is a
crate-level enum covering the failure modes real implementations hit:
- `DiscoveryFailed``from_call` remote unreachable / `services/list` failed
- `SchemaParse``from_openapi` / `from_jsonschema` couldn't parse the spec
- `Transport` — underlying transport error (QUIC for `from_call`, HTTP for
`from_openapi`/`from_mcp`)
- `Unauthorized` — HTTP 401 for `from_openapi`/`from_mcp`, auth rejected for
`from_call`
- `Conflict` — namespace collision in `from_call` (DC-3); reuse for other
adapter collisions
The exact variant set is the two-way-door remainder (OQ-26); the *presence* of
an error type is recorded in `client-and-adapters.md`. Pick the variants
above as the v1 set; add a `#[non_exhaustive]` so `alknet-http`'s adapters can
extend without breaking match arms. Use `thiserror::Error` for the derive
(consistent with the crate's existing error types).
### Where the trait lives
The trait lives in **alknet-call** (where the types — `HandlerRegistration`,
`OperationSpec`, `Handler` — live). The *implementations* live where their
transport dependencies live (the adapter location map, client-and-adapters.md):
- `FromCall` — QUIC-backed (in `alknet-call`, task `call/client/from_call`)
- `FromJsonSchema` — pure parse, no transport (in `alknet-call`, task
`call/client/from-jsonschema`)
- `FromOpenAPI` — HTTP-backed (in `alknet-http`, separate Phase 0)
- `FromMCP` — MCP streamable-HTTP-backed (in `alknet-http`, feature-gated,
separate Phase 0)
Do not implement `FromOpenAPI`/`FromMCP` here — those are `alknet-http` tasks.
This task defines the trait + error; `from_call` and `from_jsonschema`
implement it (in their tasks).
### Implementations registered in this task
Optionally implement a trivial `FromJsonSchema` adapter in this task if it
falls out naturally (it's a pure-parse adapter with no transport — see
`call/client/from-jsonschema`). If it doesn't fall out naturally, leave it for
the `from-jsonschema` task; the trait + error alone satisfy this task's
acceptance criteria.
## Acceptance Criteria
- [ ] `OperationAdapter` trait defined: `async fn import(&self) -> Result<Vec<HandlerRegistration>, AdapterError>`
- [ ] Trait is `#[async_trait]` (async — ADR-017 §5, locked)
- [ ] `AdapterError` enum defined with `#[non_exhaustive]` and `thiserror::Error`
- [ ] `AdapterError` variants: `DiscoveryFailed`, `SchemaParse`, `Transport`, `Unauthorized`, `Conflict`
- [ ] Trait + error are `pub` and re-exported from `lib.rs`
- [ ] Trait is located in alknet-call (where the types live), not alknet-http
- [ ] Doc comments link to ADR-017 §5 and client-and-adapters.md
- [ ] Unit test: a trivial test adapter implementing the trait compiles and returns Ok
- [ ] Unit test: a test adapter returning `Err(AdapterError::SchemaParse)` compiles
- [ ] `cargo test -p alknet-call` succeeds
- [ ] `cargo clippy -p alknet-call --all-targets` succeeds with no warnings
## References
- docs/architecture/crates/call/client-and-adapters.md — OperationAdapter trait §, Adapter Location Map §
- docs/architecture/decisions/017-call-protocol-client-and-adapter-contract.md — ADR-017 §5 (the trait contract), Amendments (DC-4 resolution)
- docs/architecture/open-questions.md — OQ-26 (AdapterError variants, two-way-door remainder)
- docs/research/alknet-call-completion/gap-analysis.md — DC-4, implementation priority #3
## Notes
> The trait is async because from_call needs async discovery; sync adapters
> (from_openapi reading a static spec) trivially satisfy it. The trait lives
> in alknet-call (where the types live); implementations live with their
> transport deps (from_call/from_jsonschema here, from_openapi/from_mcp in
> alknet-http). The AdapterError variants are the two-way-door remainder
> (OQ-26) — `#[non_exhaustive]` lets alknet-http extend without breaking. This
> task is small and standalone; it unblocks alknet-http Phase 1's adapter
> implementations. The to_* adapters are projections, not OperationAdapter
> impls — don't implement them here.