feat(cf-006/cf-007): per-call opener identity for open-op hooks; ADR-016 code-list sweep

Batch-fixes every remaining open alkcall-side finding from downstream
consumers so 0.7.0 is the only release they need to absorb.

- CF-006 (the CF-005 corollary): run_open_wrapper derives a per-call
  AuthContext — the opener's dispatch-resolved identity (the same
  identity the ACL gate and cap check saw) overlaid onto the
  install-time context — and passes it to both the establisher and
  the pump handler. Identity-less calls keep the install-time
  identity (no synthetic-anonymous rewrite); transport-truthful
  fields are never rewritten. Signatures unchanged — behavior-only;
  identical on per-connection registries, hub-forwarded opens now
  show the end client. Gates:
  open_wrapper_overlays_per_call_identity_on_install_time_auth +
  open_wrapper_keeps_install_time_identity_when_call_identityless.
- CF-007 (alkhttp review 006 Part C doc drift): ADR-016 amended to
  the eight-code list — ALREADY_EXISTS + CONNECTION_CLOSED in the
  Context, §3 table, and from_openapi collision rule; new §2a
  documents the undelivered-vs-ambiguous write-failure distinction.
- ChannelPlan type doc now states the Send + Sync payload constraint
  (alktunnels POC F-1 re-derived it by compiler error).
- Ledger: CF-006 and CF-007 filed + resolved; CF-005's corollary
  note points at CF-006; the Open section is empty. ADR-049 gains
  the per-call-identity note. Changelog 0.7.0 covers the batch.

Sweep result (all downstream reviews): no other open alkcall items —
alktunnels W3/F-2 are downstream-by-design, alktty R3/P14 are closed
constraints, alknet has none; OQ-24/37/39/40/41 stay
deferred-by-design (no consumer pull yet).

Verification: cargo test (631) + --all-features (648), clippy
(all-targets, all-features, -D warnings), fmt --check, doc
--no-deps, wasm32 check, semver-checks (no update required),
publish --dry-run.
This commit is contained in:
2026-09-07 11:26:12 +00:00
parent db5530ed34
commit ed741c34e0
5 changed files with 324 additions and 79 deletions
+35 -8
View File
@@ -6,10 +6,12 @@ this crate adheres to [Semantic Versioning](https://semver.org/).
## [0.7.0] - 2026-09-07
The connect-side caller-identity seam (CF-005): a connect-side serving
op can now authenticate the transport-authenticated peer by key-based
identity (mTLS/QUIC), not only by payload `auth_token`. The wire
surface is unchanged.
The connect-side caller-identity seam (CF-005) plus the full sweep of
every open alkcall finding from downstream consumers (CF-006, CF-007):
a connect-side serving op can now authenticate the
transport-authenticated peer by key-based identity (mTLS/QUIC), the
open-op establisher/pump handler see the per-call opener identity,
and the ADR-016 code list is current. The wire surface is unchanged.
### Added
@@ -39,11 +41,36 @@ surface is unchanged.
`AccessControl::check` fails closed (`FORBIDDEN`) — unchanged. The
identity resolution is process-local (`set_identity` on the
internal channel-0 `Connection`); nothing new crosses the transport.
Regression gates: the four `cf005_*` tests in
- **The open-op establisher and pump handler see the per-call opener
identity (CF-006 — the CF-005 corollary).** `run_open_wrapper`
derives a per-call `AuthContext` — the opener's dispatch-resolved
identity overlaid onto the install-time context — and passes it to
both the establisher (ADR-049 §1) and the pump handler
(`OpenHandler`). Identity-less calls keep the install-time
identity; transport-truthful fields (`alpn`, `remote_addr`,
`tls_client_fingerprint`) are never rewritten. Signatures are
unchanged — behavior-only; on a per-connection registry (ADR-047
§4) this is identical to before, and on hub-forwarded opens the
establisher now sees the end client instead of the hub.
- **ADR-016 amended: the protocol-code list is eight codes (CF-007 —
alkhttp review 006 Part C doc drift).** `ALREADY_EXISTS`
(non-retryable, `op/register` collision) and `CONNECTION_CLOSED`
(retryable, provably-undelivered call) are now in the Context
paragraph, the §3 table, and the `from_openapi` collision rule; §2a
documents the undelivered-vs-ambiguous write-failure distinction.
Doc-only.
- **`ChannelPlan` type doc carries the `Send + Sync` payload
constraint** (alktunnels POC F-1 — a non-`Sync` boxed handle needs
a wrapper; documented so the next consumer doesn't re-derive it by
compiler error).
- Regression gates: the four `cf005_*` tests in
`src/channels/client.rs` (transport propagation, override
precedence, identity-less failure, token fallback + precedence).
Ledger: `docs/reviews/consumer-findings-ledger.md` CF-005 →
resolved. Spec: ADR-022 §connect-side-serving.
precedence, identity-less failure, token fallback + precedence) and
the two open-op identity-overlay tests in
`src/channels/operations.rs`. Ledger: CF-005, CF-006, CF-007 →
resolved (the ledger's Open section is now empty). Specs: ADR-022
§connect-side-serving, ADR-016 (code list), ADR-049 establisher
doc note.
## [0.6.0] - 2026-09-07
@@ -2,23 +2,30 @@
## Status
Accepted (amended by ADR-021 — protocol-level code list extended to six)
Accepted (amended by ADR-021 — protocol-level code list extended to six;
amended 2026-09-07 — extended to eight, see §2a below)
## Context
The `OperationSpec` in alknet-call has `input_schema` and `output_schema` but
no `error_schemas`. The `call.error` payload (call-protocol.md L128134)
carries a `code` and `message`, where `code` is one of six infrastructure
carries a `code` and `message`, where `code` is one of eight infrastructure
codes: `NOT_FOUND`, `FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`,
`INTERNAL`, `TIMEOUT`.
`INTERNAL`, `TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`.
These six codes cover **protocol-level failures** — the call protocol
These eight codes cover **protocol-level failures** — the call protocol
itself can always fail to find an operation, deny access, reject bad input,
reject the wrong dispatch method for the operation type, time out, or hit
an internal error. They are emitted by the dispatch machinery (the registry,
reject the wrong dispatch method for the operation type, time out, hit an
internal error, reject a registration collision, or report a provably
undelivered call. They are emitted by the dispatch machinery (the registry,
the adapter), not by operation handlers. `INVALID_OPERATION_TYPE` was added
by ADR-021 (streaming handler for subscriptions — `invoke()` called on a
`Subscription`, or `invoke_streaming()` on a `Query`/`Mutation`).
`ALREADY_EXISTS` was added by the ADR-022 collision sub-amendment
(2026-09-04 — `op/register` rejects a peer-announce collision; non-retryable,
registration is state). `CONNECTION_CLOSED` was added by the consumer
findings ledger CF-001 (2026-08-30 — write failures on provably undelivered
`call.requested` frames; **retryable** — see §2a).
But operations also have **domain-level failures** that are not covered:
@@ -186,7 +193,7 @@ optional-array convention.
### 3. Protocol-level vs operation-level error codes
The six existing codes are **protocol-level** — emitted by the dispatch
The eight existing codes are **protocol-level** — emitted by the dispatch
machinery, not by handlers:
| Code | Emitted by | Meaning |
@@ -197,6 +204,20 @@ machinery, not by handlers:
| `INVALID_OPERATION_TYPE` | Registry / `OperationEnv` | Wrong dispatch path for the operation's type (`invoke()` on a `Subscription`, `invoke_streaming()` on a `Query`/`Mutation`, or `OperationEnv::invoke()` on a `Subscription` during composition — ADR-021) |
| `INTERNAL` | Registry / Adapter | Handler panic, unhandled error, connection failure |
| `TIMEOUT` | Adapter | Request timed out |
| `ALREADY_EXISTS` | Registry (`op/register` collision gate) | A peer-announce or import collides with an existing registration (`replace: false`, or a serving-side name — ADR-022). Non-retryable without `replace: true`. |
| `CONNECTION_CLOSED` | Client write path | The `call.requested` frame could not be written — the call is provably undelivered (CF-001). `retryable: true` — the only protocol code a caller may auto-retry. Mid-publish and completed-frame write failures stay `INTERNAL` (delivery ambiguous). |
#### 2a. `CONNECTION_CLOSED` — the retryable undelivered-call code (CF-001)
The write-failure mapping distinguishes **provably undelivered** from
**delivery-ambiguous**: a failed write of the *request* frame means the
producer never saw the call — reconnect/retry is safe. A failed write
*after* delivery started (mid-publish, completed frame) is ambiguous —
retry unsafe — and stays `INTERNAL`. The producer-side `fail_all(...)` on
connection close also stays `INTERNAL` (the callee cannot know what the
caller received). The code is additive to the wire vocabulary; consumers
treat unknown codes per their existing policy, with the `retryable` flag
as the machine-readable signal.
Operation-level domain codes are emitted by **handlers** — the operation's
own logic determines what went wrong. They are declared in `error_schemas`
@@ -247,9 +268,9 @@ accordingly.
```
**Normative rule (review #002 W20)**: `from_openapi` must not produce error
codes that collide with the six protocol-level codes (`NOT_FOUND`,
codes that collide with the protocol-level codes (`NOT_FOUND`,
`FORBIDDEN`, `INVALID_INPUT`, `INVALID_OPERATION_TYPE`, `INTERNAL`,
`TIMEOUT`). The adapter prefixes
`TIMEOUT`, `ALREADY_EXISTS`, `CONNECTION_CLOSED`). The adapter prefixes
imported error codes with `HTTP_` and the status number (e.g., `HTTP_404`,
`HTTP_429`) to avoid collision. This is a requirement for the adapter, not
a naming convention — the `from_openapi` example above was previously shown
@@ -410,6 +431,10 @@ enum instead of a generic `Result<Output, string>`.
- ADR-021: Streaming handler for subscriptions (amends this ADR's
protocol-level code list — `INVALID_OPERATION_TYPE` added as the sixth
protocol-level code)
- ADR-022 (collision sub-amendment 2026-09-04): `ALREADY_EXISTS` — the
`op/register` collision rejection code
- Consumer findings ledger CF-001 (2026-08-30): `CONNECTION_CLOSED`
the retryable provably-undelivered-call code
- docs/sdd_process.md L19, L423 (Safe Exit protocol — the general principle
of making failure typed and declared)
- TypeScript reference: `/workspace/@alkdev/operations/src/types.ts`
@@ -332,6 +332,15 @@ implementation detail; the wire surface is unchanged from §1/§3):
`Fn(Value, AuthContext) -> BoxFuture<'static,
Result<Establishment, EstablishmentError>>`; the `Connection`
belongs exclusively to the `OpenHandler` (unchanged).
**Identity semantics (CF-005 corollary, 2026-09-07):** the
`AuthContext` the establisher and the pump handler receive is the
**per-call** context — the opener's dispatch-resolved identity (the
same identity the ACL gate and the per-identity cap check saw)
overlaid onto the install-time context; transport-truthful fields
(`alpn`, `remote_addr`, `tls_client_fingerprint`) stay
install-time. On a per-connection registry (ADR-047 §4) this is
identical to the install-time context; on hub-forwarded opens the
establisher sees the end client, not the hub.
2. **The bound is the earlier of the dispatch deadline and the
per-registration timeout.** §2 names the dispatch deadline "when
the `OperationContext` carries one, else the crate constant";
+64 -2
View File
@@ -78,11 +78,73 @@ Format: date | found-in (alkhttp context) | severity | status.
consumer can `set_identity` before wrapping).
- The corollary (`register_openable`'s closed-over
establishment-time `AuthContext` vs the per-call opener identity)
remains open as a design follow-up — it affects the establisher /
pump handler seam, not the ACL gate; tracked separately.
was resolved in the same batch (2026-09-07, CF-006 below).
- **Status:** resolved — 2026-09-07 (regression gates: `cf005_*`
tests in `src/channels/client.rs`).
### CF-006 — open-op establisher/pump handler receive the install-time `AuthContext`, never the per-call opener's identity (CF-005 corollary) (2026-09-07) — RESOLVED 2026-09-07
- **Found in:** the CF-005 verification pass (same alktunnels
reverse-flow POC W1 material; the ledger's CF-005 entry named it as
a "design follow-up, tracked separately"). `run_open_wrapper`
(`src/channels/operations.rs`) closed over the
`install_channel_zero`-time `AuthContext` and passed `auth.clone()`
verbatim to both the establisher (ADR-049 §1) and the pump handler
(`OpenHandler`'s 4th parameter). The per-call opener's identity —
the same identity the ACL gate and the per-identity cap check saw —
reached neither hook. On a per-connection registry (ADR-047 §4)
install-time identity IS the caller's, so the gap was invisible
there; it shows on hub-forwarded opens (the establisher saw the
hub, not the end client) and on the connect-side serving path
(token- or `ServingConfig.identity`-resolved callers). alktty
review 002 R3 independently recorded the same seam (closed
per-connection by design, with the "registry must stay
per-connection" constraint).
- **Impact:** an establisher doing per-principal authorization or
ownership checks on the backend could not see the actual opener;
the pump handler tagging sessions by principal saw the hub.
- **Fix (2026-09-07):** `run_open_wrapper` derives a **per-call
`AuthContext`** — the opener's dispatch-resolved identity overlaid
onto the install-time context — and passes it to both the
establisher and the pump handler. When the call has no resolved
identity, the install-time identity is kept (anonymous-keep; no
synthetic-`anonymous` rewrite of a real install-time identity).
Transport-truthful fields (`alpn`, `remote_addr`,
`tls_client_fingerprint`) are never rewritten. Signatures are
unchanged (`OpenEstablisher`, `OpenHandler` still take
`AuthContext`) — behavior-only, additive; downstream code compiles
unchanged. Regression gates:
`open_wrapper_overlays_per_call_identity_on_install_time_auth` +
`open_wrapper_keeps_install_time_identity_when_call_identityless`
in `src/channels/operations.rs`.
- **Also in this batch:** `ChannelPlan`'s type doc now spells out the
`Send + Sync` payload constraint (alktunnels POC F-1 re-derived it
by compiler error — a doc line prevents the next consumer repeating
that).
- **Status:** resolved — 2026-09-07.
### CF-007 — ADR-016's protocol-code list is stale: omits `ALREADY_EXISTS` and `CONNECTION_CLOSED` (2026-09-07) — RESOLVED 2026-09-07
- **Found in:** alkhttp review 006, Part C
(`alkhttp/docs/reviews/006-alkcall-0.3.0-consequence-review.md`
L207233, L319324 — "upstream doc drift … flagged for the next
alkcall doc pass"). ADR-016 said "six codes" and its table listed
only the original six, while the wire vocabulary had grown:
`ALREADY_EXISTS` (ADR-022 collision sub-amendment, 0.3.0) and
`CONNECTION_CLOSED` (CF-001, 0.4.x, the retryable
provably-undelivered-call code). Downstream consumers already
handle both; the authoritative ADR was the thing that was wrong.
- **Fix (2026-09-07):** ADR-016 amended — Status notes the 2026-09-07
extension to eight; the Context paragraph and the §3 table carry
both codes with their semantics (`ALREADY_EXISTS` non-retryable,
registration state; `CONNECTION_CLOSED` retryable — the only
protocol code a caller may auto-retry), a §2a subsection documents
the undelivered-vs-ambiguous write-failure distinction, the
`from_openapi` collision rule lists all eight, and the
cross-references cite the two amendments. Doc-only; the wire
surface is unchanged.
- **Status:** resolved — 2026-09-07.
---
### CF-004 — `services_schema_handler` discloses Internal/ACL-restricted op specs — no visibility or AccessControl check (2026-08-30) — RESOLVED 2026-08-31
+182 -60
View File
@@ -362,6 +362,15 @@ pub type OpenHandler = Arc<
/// The plan is **process-local**: establisher → wrapper → handler on
/// the producing side. Nothing crosses the transport that isn't
/// already the open op's input — the wire surface is unchanged.
///
/// **Payload constraint (alktunnels POC F-1):** `Arc<dyn Any + Send +
/// Sync>` requires the concrete plan payload to be `Send + Sync`. A
/// dialed-socket handle with boxed non-`Sync` halves (e.g.
/// `Box<dyn AsyncRead>` without a `Sync` bound) does not satisfy it —
/// wrap such handles in a `Sync` adapter or hold them unboxed. The
/// bound is correct (the plan crosses the wrapper task boundary);
/// this note exists so the next consumer doesn't re-derive it by
/// compiler error.
pub type ChannelPlan = Arc<dyn std::any::Any + Send + Sync>;
/// The establishment-phase result (ADR-049 §1, as filled by ADR-049
@@ -441,8 +450,13 @@ impl EstablishmentError {
/// is spawned. On failure the wrapper tears down the just-allocated
/// channel and replies `channel:open_failed` with `details.reason`.
///
/// The establisher takes the open op's `input` (params) and the peer's
/// `AuthContext`. It deliberately does **not** receive the channel's
/// The establisher takes the open op's `input` (params) and the
/// **per-call** `AuthContext`: the opener's resolved identity (the
/// same identity the ACL gate and the per-identity cap check saw —
/// CF-005 corollary, 2026-09-07) overlaid onto the install-time
/// context; the transport-truthful fields (`alpn`, `remote_addr`,
/// `tls_client_fingerprint`) are install-time. It deliberately does
/// **not** receive the channel's
/// `Connection`: the channel `BiStream` is yield-once
/// (`ChannelBidiStreamSource`) and belongs exclusively to the pump
/// handler (the `OpenHandler`); the establisher is pre-data-plane (its
@@ -524,9 +538,13 @@ impl ChannelCore {
/// overlay registry, Layer 2 per ADR-019 — this is the
/// per-connection registration the §4 amendment blesses). The
/// `auth` is the peer's `AuthContext`, captured at
/// `install_channel_zero` time and closed over by the wrapper so
/// the ALPN handler receives it without the wrapper having to
/// reach into `OperationContext` for it.
/// `install_channel_zero` time and closed over by the wrapper: it
/// provides the transport-truthful fields (`alpn`,
/// `remote_addr`, `tls_client_fingerprint`), while the identity
/// field is overridden **per call** — the opener's resolved
/// identity (the same identity the ACL gate and the cap check
/// saw; CF-005 corollary, 2026-09-07), falling back to the
/// install-time identity when the call has none.
///
/// **Op type:** the spec's `op_type` determines the
/// `HandlerKind`. For `Query`/`Mutation` the wrapper is a `Once`
@@ -639,15 +657,15 @@ impl ChannelCore {
}
}
/// The opener identity for the per-identity cap check. The direct
/// caller (the peer that opened this channels connection) is the
/// identity whose cap is consulted; `forwarded_for` is metadata and
/// is NOT consulted (ADR-026, ADR-047 §7). Falls back to a synthetic
/// "anonymous" identity when the call has no resolved identity (the
/// cap still applies — anonymous opens count against the anonymous
/// identity's cap, which is 256 by default).
fn opener_identity_from_context(ctx: &OperationContext) -> Identity {
ctx.identity.clone().unwrap_or_else(|| Identity {
/// The opener identity for the per-identity cap check (CF-005
/// corollary): `Some` is the dispatch-resolved caller identity; `None`
/// falls back to a synthetic "anonymous" identity. The direct caller
/// (the peer that opened this channels connection) is the identity
/// whose cap is consulted; `forwarded_for` is metadata and is NOT
/// consulted (ADR-026, ADR-047 §7). Anonymous opens count against the
/// anonymous identity's cap, which is 256 by default.
fn opener_identity_or_anonymous(call_identity: Option<Identity>) -> Identity {
call_identity.unwrap_or_else(|| Identity {
id: "anonymous".to_string(),
scopes: vec![],
resources: Default::default(),
@@ -743,9 +761,24 @@ fn establishment_timeout_call_error() -> CallError {
///
/// `input` is the open op's input (params), passed through to the
/// establisher (when registered) and the ALPN's `OpenHandler`.
/// `opener_id` is the `PeerId` recorded in the opener ledger (the
/// direct caller's identity id). `opener_identity` is the identity the
/// cap was reserved against (un-reserved on failure paths).
/// `call_identity` is the dispatch-resolved caller identity — `Some`
/// when the call carried one (the same identity the ACL gate saw),
/// `None` when identity-less. The cap is checked against it (or the
/// synthetic `anonymous` when `None`) and it is overlaid onto the
/// per-call auth when present.
///
/// The `auth` handed to the establisher and the pump handler is the
/// **per-call** context (CF-005 corollary): the opener's resolved
/// identity overlaid onto the install-time context. When the call has
/// no resolved identity, the install-time identity is kept (the
/// anonymous-keep case). Transport-truthful fields (`alpn`,
/// `remote_addr`, `tls_client_fingerprint`) are never rewritten. In
/// the per-connection-registry deployment (ADR-047 §4) the
/// install-time identity IS the caller's, so this is
/// behavior-identical there; the difference shows on hub-forwarded
/// opens (the establisher sees the end client, not the hub) and on
/// the connect-side serving path (token- or
/// `ServingConfig.identity`-resolved callers).
#[allow(clippy::too_many_arguments)]
async fn run_open_wrapper(
manager: &ChannelManager,
@@ -755,16 +788,24 @@ async fn run_open_wrapper(
auth: &AuthContext,
alpn: &str,
input: Value,
opener_id: String,
opener_identity: Identity,
call_identity: Option<Identity>,
request_id: String,
deadline: Option<Instant>,
) -> ResponseEnvelope {
let mut per_call_auth = auth.clone();
if let Some(identity) = &call_identity {
per_call_auth.identity = Some(identity.clone());
}
let opener_identity = opener_identity_or_anonymous(call_identity);
if let Err(channel_err) = policy.check_open(&opener_identity) {
return ResponseEnvelope::error(request_id, map_channel_error_to_call_error(&channel_err));
}
let channel_id = match manager.open_channel(alpn, opener_id, None).await {
let channel_id = match manager
.open_channel(alpn, opener_identity.id.clone(), None)
.await
{
Ok((id, send, recv)) => {
// The establishment phase (ADR-049 §1): awaited bounded,
// before the reply and before the pump handler is spawned.
@@ -775,7 +816,7 @@ async fn run_open_wrapper(
let bound = establishment_bound(hook.timeout, deadline);
let establishment = tokio::time::timeout(
bound,
(hook.establisher)(input.clone(), auth.clone()),
(hook.establisher)(input.clone(), per_call_auth.clone()),
)
.await;
match establishment {
@@ -817,7 +858,7 @@ async fn run_open_wrapper(
Arc::clone(&accepted),
);
let channel_conn = Connection::from_source(source, alpn.as_bytes().to_vec());
let raw_task = open_handler(input, plan, channel_conn, auth.clone());
let raw_task = open_handler(input, plan, channel_conn, per_call_auth);
let teardown_manager = manager.clone();
let teardown_policy = Arc::clone(policy);
@@ -948,8 +989,6 @@ fn make_open_handler_once(
let alpn = alpn.clone();
Box::pin(async move {
let request_id = ctx.request_id.clone();
let opener_identity = opener_identity_from_context(&ctx);
let opener_id = opener_identity.id.clone();
run_open_wrapper(
&manager,
&policy,
@@ -958,8 +997,7 @@ fn make_open_handler_once(
&auth,
&alpn,
input,
opener_id,
opener_identity,
ctx.identity.clone(),
request_id,
ctx.deadline,
)
@@ -993,8 +1031,6 @@ fn make_open_handler_stream(
let alpn = alpn.clone();
Box::pin(futures::stream::once(async move {
let request_id = ctx.request_id.clone();
let opener_identity = opener_identity_from_context(&ctx);
let opener_id = opener_identity.id.clone();
run_open_wrapper(
&manager,
&policy,
@@ -1003,8 +1039,7 @@ fn make_open_handler_stream(
&auth,
&alpn,
input,
opener_id,
opener_identity,
ctx.identity.clone(),
request_id,
ctx.deadline,
)
@@ -1357,17 +1392,15 @@ mod tests {
}
#[test]
fn opener_identity_from_context_returns_identity_when_present() {
fn opener_identity_or_anonymous_returns_identity_when_present() {
let ctx = test_context("opener-1");
let id = opener_identity_from_context(&ctx);
let id = opener_identity_or_anonymous(ctx.identity.clone());
assert_eq!(id.id, "alice");
}
#[test]
fn opener_identity_from_context_falls_back_to_anonymous() {
let mut ctx = test_context("opener-2");
ctx.identity = None;
let id = opener_identity_from_context(&ctx);
fn opener_identity_or_anonymous_falls_back_to_anonymous() {
let id = opener_identity_or_anonymous(None);
assert_eq!(id.id, "anonymous");
}
@@ -1415,6 +1448,106 @@ mod tests {
assert!(spawned.load(Ordering::SeqCst));
}
/// CF-005 corollary — the establisher and the pump handler see the
/// **per-call** opener identity (the identity the ACL gate and cap
/// check saw), not the install-time snapshot. The install-time
/// context here carries a stale identity (`stale-install`); the
/// dispatch context resolves `alice`. Both hooks must report
/// `alice` while `alpn` stays the install-time transport value.
#[tokio::test]
async fn open_wrapper_overlays_per_call_identity_on_install_time_auth() {
let manager = make_manager().await;
let policy = super::super::policy::default_policy();
let seen_establisher = Arc::new(parking_lot::Mutex::new(None::<String>));
let seen_handler = Arc::new(parking_lot::Mutex::new(None::<String>));
let seen_establisher_cl = Arc::clone(&seen_establisher);
let seen_handler_cl = Arc::clone(&seen_handler);
let establisher: OpenEstablisher = Arc::new(move |_input, auth| {
let seen = Arc::clone(&seen_establisher_cl);
Box::pin(async move {
*seen.lock() = auth.identity.as_ref().map(|i| i.id.clone());
Ok::<_, EstablishmentError>(Establishment::default())
})
});
let open_handler: OpenHandler = Arc::new(move |_input, _plan, _conn, auth| {
*seen_handler_cl.lock() = auth.identity.as_ref().map(|i| i.id.clone());
tokio::spawn(async {})
});
let mut install_auth = AuthContext::anonymous(b"alk/tty");
install_auth.identity = Some(Identity {
id: "stale-install".to_string(),
scopes: vec![],
resources: HashMap::new(),
});
let handler = make_open_handler_once(
manager.clone(),
policy,
Some(EstablisherHook {
establisher,
timeout: None,
}),
open_handler,
install_auth,
"alk/tty".to_string(),
);
let env = handler(json!({}), test_context("open-per-call-1")).await;
env.result.expect("open should succeed");
assert_eq!(
seen_establisher.lock().as_deref(),
Some("alice"),
"establisher saw the per-call opener identity"
);
assert_eq!(
seen_handler.lock().as_deref(),
Some("alice"),
"pump handler saw the per-call opener identity"
);
}
/// CF-005 corollary (fallback leg) — when the call has no resolved
/// identity, the anonymous opener keeps the install-time identity
/// (no overlay). The install-time identity here is `bob`; the
/// handler must see `bob`, not the synthetic `anonymous`.
#[tokio::test]
async fn open_wrapper_keeps_install_time_identity_when_call_identityless() {
let manager = make_manager().await;
let policy = super::super::policy::default_policy();
let seen_handler = Arc::new(parking_lot::Mutex::new(None::<String>));
let seen_handler_cl = Arc::clone(&seen_handler);
let open_handler: OpenHandler = Arc::new(move |_input, _plan, _conn, auth| {
*seen_handler_cl.lock() = auth.identity.as_ref().map(|i| i.id.clone());
tokio::spawn(async {})
});
let mut install_auth = AuthContext::anonymous(b"alk/tty");
install_auth.identity = Some(Identity {
id: "bob".to_string(),
scopes: vec![],
resources: HashMap::new(),
});
let handler = make_open_handler_once(
manager.clone(),
policy,
None,
open_handler,
install_auth,
"alk/tty".to_string(),
);
let mut ctx = test_context("open-per-call-2");
ctx.identity = None;
let env = handler(json!({}), ctx).await;
env.result.expect("open should succeed");
assert_eq!(
seen_handler.lock().as_deref(),
Some("bob"),
"identity-less call keeps the install-time identity"
);
}
#[tokio::test]
async fn make_open_handler_stream_success_path() {
let manager = make_manager().await;
@@ -1628,11 +1761,11 @@ mod tests {
let open_handler: OpenHandler =
Arc::new(|_input, _plan, _conn, _auth| tokio::spawn(async {}));
let auth = AuthContext::anonymous(b"alk/call");
let opener_id = Identity {
let opener = Some(Identity {
id: "alice".to_string(),
scopes: vec![],
resources: HashMap::new(),
};
});
let env = run_open_wrapper(
&manager,
&policy,
@@ -1641,8 +1774,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
opener_id.id.clone(),
opener_id,
opener,
"req-cap-deny".to_string(),
None,
)
@@ -1705,8 +1837,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-est-fail".to_string(),
None,
)
@@ -1764,8 +1895,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-est-timeout".to_string(),
None,
)
@@ -1809,8 +1939,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-est-deadline".to_string(),
Some(Instant::now() + Duration::from_millis(50)),
)
@@ -1844,8 +1973,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-est-ok".to_string(),
None,
)
@@ -1884,8 +2012,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-accepted".to_string(),
None,
)
@@ -1916,8 +2043,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-not-accepted".to_string(),
None,
)
@@ -2129,8 +2255,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-plan-flow".to_string(),
None,
)
@@ -2183,8 +2308,7 @@ mod tests {
&auth,
"alk/tty",
json!({ "resource": "same" }),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-race-a".to_string(),
None,
),
@@ -2196,8 +2320,7 @@ mod tests {
&auth,
"alk/tty",
json!({ "resource": "same" }),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-race-b".to_string(),
None,
)
@@ -2233,8 +2356,7 @@ mod tests {
&auth,
"alk/tty",
json!({}),
"alice".to_string(),
identity("alice"),
Some(identity("alice")),
"req-no-plan".to_string(),
None,
)