feat(http): connection-local Layer 2 overlay for browser-registered ops (ADR-024/034/044)

Enforce AccessControl on overlay ops in OverlayOperationEnv::invoke_with_policy
(alknet-call) so the hub's calls to browser-registered ops are gated by the
browser's AccessControl — matching OperationRegistry::invoke semantics for
internal composition (caller identity = parent handler_identity.as_identity()).

Add src/websocket/overlay.rs with 19 integration tests covering the connection-
local overlay acceptance criteria: browser ops land in the per-CallConnection
overlay (not PeerCompositeEnv), no PeerId for the browser, register_imported()/
register_imported_all() populate the overlay, hub outgoing calls route through
overlay_env() (not PeerRef::Specific), PeerRef::Specific('browser-X') routes to
NOT_FOUND, AccessControl gates hub calls (allowed/forbidden/default), overlay is
per-connection isolated and dropped on WS close, WS close aborts in-flight calls
with ADR-016 cascade, bidirectionality, and browser-with-no-ops use-case scoping.
This commit is contained in:
2026-07-01 19:45:36 +00:00
parent 86752ba242
commit ad279693ce
3 changed files with 712 additions and 0 deletions

View File

@@ -27,6 +27,7 @@ use crate::protocol::wire::ResponseEnvelope;
use crate::registry::context::{generate_request_id, AbortPolicy, OperationContext, ScopedPeerEnv};
use crate::registry::env::OperationEnv;
use crate::registry::registration::{Handler, HandlerRegistration};
use crate::registry::spec::AccessResult;
const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(30);
@@ -309,6 +310,7 @@ impl OperationEnv for OverlayOperationEnv {
let handler: Handler;
let composition_authority;
let scoped_env;
let access_control;
{
let overlay = self.overlay.read();
let Some(registration) = overlay.get(&name) else {
@@ -320,6 +322,19 @@ impl OperationEnv for OverlayOperationEnv {
.scoped_env
.clone()
.unwrap_or_else(ScopedPeerEnv::empty);
access_control = registration.spec.access_control.clone();
}
let caller_identity = if parent.internal {
parent
.handler_identity
.as_ref()
.and_then(|ca| ca.as_identity())
} else {
parent.identity.clone()
};
if let AccessResult::Forbidden(message) = access_control.check(caller_identity.as_ref()) {
return ResponseEnvelope::forbidden(parent.request_id.clone(), message);
}
let context = OperationContext {