Files
Hare a7186173b4 feat(fs): bind external scopes to current execution lineage
Add fail-closed Composition lifetime attestation for non-root scopes; preserve pinned deployment-root semantics and principal visibility. Fetch and dispatch bind optional published scope refs inside the refreshed backend boundary.

Validation: cargo test -p wip-fs-server (25 unit, 8 HTTP, 31 extension tests and 1 doc-test); Nix clippy for FS and public example with -D warnings; git diff --check.
2026-10-08 03:56:17 +09:00

2256 lines
76 KiB
Rust

//! External-crate contract tests: no Linux backend, private modules, or listener.
use std::collections::BTreeMap;
use std::sync::{Arc, Barrier, Mutex, TryLockError};
use std::time::{Duration, Instant};
use wip_fs_server::{
BackendEntry, Composition, DefaultInterfaces, EntryKind, FileRead, FilesystemBackend,
HttpAdapter, Projection, ProjectionPolicy, ResolvedEntry, ResourceLimits,
};
use wip_host::{
CancellationToken, EffectGuarantee, ExecutionContext, HostError, InterfaceRef,
InterfaceRegistry, ObjectProvider, OperationAttempt, ProviderResult, PublishedObject,
RegisteredInterface, Scope, Validator, WipObject,
};
use wip_http::{
DecodedResponse, Endpoint, decode_call_operation_response, decode_fetch_interface_response,
decode_observe_response, encode_call_operation_request, encode_fetch_interface_request,
encode_observe_request,
};
use wip_interface as wip;
use wip_protocol::{
CallOperationRequest, CallOperationResponse, FetchInterfaceRequest, FetchInterfaceResponse,
InterfaceDescriptor, InterfaceTarget, ObjectObservation, ObserveRequest, ProtocolErrorCode,
Target, Value,
};
const BACKEND_SECRET: &str = "/private/backend/credentials.key";
const POLICY_SECRET: &str = "policy-secret-tenant-42";
/// Application-defined capabilities, independent of built-in filesystem traits.
#[wip::interface]
pub trait MemoryOperations {
/// Read through the application capability.
/// # Returns
/// Current bytes.
fn read(&self) -> wip::OperationResult<wip::Bytes>;
/// Change the pinned target.
/// # Returns
/// Unit after changing the data.
fn change(
&self,
/// New data.
contents: wip::Bytes,
) -> wip::OperationResult<()>;
/// A principal-hidden operation.
/// # Returns
/// Unit.
fn hidden(&self) -> wip::OperationResult<()>;
/// A visible but principal-denied operation.
/// # Returns
/// Unit.
fn denied(&self) -> wip::OperationResult<()>;
/// Inject a typed backend failure with an explicit effect guarantee.
/// # Returns
/// Unit on success (this test implementation always fails).
fn fail(
&self,
/// Failure category and effect disposition.
mode: u64,
) -> wip::OperationResult<()>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Data {
bytes: Vec<u8>,
revision: u64,
hidden: bool,
}
struct Node {
id: u64,
kind: EntryKind,
data: Mutex<Data>,
calls: Mutex<Vec<&'static str>>,
}
#[derive(Clone)]
struct Entry {
path: String,
node: Arc<Node>,
validator: Validator,
hidden: bool,
}
impl Entry {
fn snapshot(path: String, node: Arc<Node>) -> Self {
let data = node.data.lock().unwrap();
let revision = data.revision;
let hidden = data.hidden;
drop(data);
let validator = Validator::new(format!("{}:{revision}", node.id).into_bytes());
Self {
path,
node,
validator,
hidden,
}
}
}
impl WipObject for Entry {
fn name(&self) -> &str {
self.path.rsplit('/').next().unwrap()
}
fn description(&self) -> Option<&str> {
Some("public memory entry")
}
fn reference(&self) -> Option<&str> {
(self.path == "/").then_some("entry-root")
}
fn validator(&self) -> Option<&Validator> {
Some(&self.validator)
}
}
impl BackendEntry for Entry {
fn kind(&self) -> EntryKind {
self.node.kind
}
}
#[derive(Debug)]
struct Seen {
stage: &'static str,
path: String,
deadline: Option<Instant>,
budget: Option<u64>,
token: CancellationToken,
}
#[derive(Default)]
struct Controls {
bump_before_execute: bool,
bump_ancestor: Option<String>,
hide_before_execute: bool,
hide_ancestor: Option<String>,
replace_ancestor: Option<String>,
replace_mapping: bool,
rollback: bool,
cancel_on_execute: bool,
cancel_on_publish: bool,
resolve_error: bool,
children_error: bool,
publish_error: bool,
execute_error: Option<EffectGuarantee>,
}
struct Shared {
nodes: Mutex<BTreeMap<String, Arc<Node>>>,
// Participating memory writers serialize validation, dispatch and disposition.
// Deliberate out-of-band mutation below simulates the documented writer limit.
transaction: Mutex<()>,
controls: Mutex<Controls>,
seen: Mutex<Vec<Seen>>,
}
impl Shared {
fn record(&self, stage: &'static str, path: &str, context: &ExecutionContext) {
self.seen.lock().unwrap().push(Seen {
stage,
path: path.into(),
deadline: context.deadline(),
budget: context.resource_budget(),
token: context.cancellation().clone(),
});
}
fn node(&self, path: &str) -> Arc<Node> {
self.nodes.lock().unwrap()[path].clone()
}
}
#[derive(Clone)]
struct MemoryBackend(Arc<Shared>, Arc<Node>);
impl MemoryBackend {
fn new(shared: Arc<Shared>) -> Self {
let root = shared.node("/");
Self(shared, root)
}
}
fn check_context(context: &ExecutionContext) -> Result<(), HostError> {
if context.cancellation().is_cancelled()
|| context
.deadline()
.is_some_and(|deadline| Instant::now() >= deadline)
|| context.resource_budget() == Some(0)
{
Err(HostError::resource_limit(
BACKEND_SECRET,
EffectGuarantee::NotCommitted,
))
} else {
Ok(())
}
}
impl FilesystemBackend for MemoryBackend {
type Entry = Entry;
fn resolve(
&self,
context: &ExecutionContext,
path: &str,
) -> ProviderResult<ResolvedEntry<Entry>> {
self.0.record("resolve", path, context);
if let Err(error) = check_context(context) {
return ProviderResult::Failure(error);
}
if self.0.controls.lock().unwrap().resolve_error {
return ProviderResult::Failure(HostError::internal(
BACKEND_SECRET,
EffectGuarantee::NotCommitted,
));
}
// One connected map snapshot, serialized with participating mutations.
let _transaction = self.0.transaction.lock().unwrap();
let nodes = self.0.nodes.lock().unwrap();
if !nodes
.get("/")
.is_some_and(|root| Arc::ptr_eq(root, &self.1))
{
return ProviderResult::NotFound;
}
let Some(node) = nodes.get(path) else {
return ProviderResult::NotFound;
};
let mut ancestors = Vec::new();
if path != "/" {
let prefixes = std::iter::once("/").chain(
path.match_indices('/')
.skip(1)
.map(|(index, _)| &path[..index]),
);
for prefix in prefixes {
let Some(parent) = nodes.get(prefix) else {
return ProviderResult::NotFound;
};
if parent.kind != EntryKind::Directory {
return ProviderResult::NotFound;
}
ancestors.push(Entry::snapshot(prefix.into(), parent.clone()));
}
}
ProviderResult::Found(ResolvedEntry::new(
ancestors,
Entry::snapshot(path.into(), node.clone()),
))
}
fn children(
&self,
context: &ExecutionContext,
entry: &Entry,
maximum: usize,
) -> Result<Vec<Entry>, HostError> {
self.0.record("children", &entry.path, context);
check_context(context)?;
if self.0.controls.lock().unwrap().children_error {
return Err(HostError::internal(
BACKEND_SECRET,
EffectGuarantee::NotCommitted,
));
}
let prefix = if entry.path == "/" {
"/".into()
} else {
format!("{}/", entry.path)
};
let nodes = self.0.nodes.lock().unwrap();
let children: Vec<_> = nodes
.iter()
.filter(|(path, _)| {
path.strip_prefix(&prefix)
.is_some_and(|rest| !rest.is_empty() && !rest.contains('/'))
})
.map(|(path, node)| Entry::snapshot(path.clone(), node.clone()))
.collect();
if children.len() > maximum {
return Err(HostError::resource_limit(
BACKEND_SECRET,
EffectGuarantee::NotCommitted,
));
}
Ok(children)
}
fn execute(
&self,
context: &ExecutionContext,
resolved: ResolvedEntry<Entry>,
operation: impl FnOnce(ResolvedEntry<Entry>) -> OperationAttempt,
) -> ProviderResult<OperationAttempt> {
let _lock = self.0.transaction.lock().unwrap();
let (ancestors, entry) = resolved.into_parts();
self.0.record("execute", &entry.path, context);
if let Err(error) = check_context(context) {
return ProviderResult::Failure(error);
}
let mut controls = self.0.controls.lock().unwrap();
if let Some(guarantee) = controls.execute_error {
return ProviderResult::Failure(HostError::internal(BACKEND_SECRET, guarantee));
}
if controls.bump_before_execute {
entry.node.data.lock().unwrap().revision += 1;
controls.bump_before_execute = false;
}
if let Some(path) = controls.bump_ancestor.take() {
self.0.node(&path).data.lock().unwrap().revision += 1;
}
if controls.hide_before_execute {
entry.node.data.lock().unwrap().hidden = true;
}
if let Some(path) = controls.hide_ancestor.take() {
self.0.node(&path).data.lock().unwrap().hidden = true;
}
if let Some(path) = controls.replace_ancestor.take() {
self.0.nodes.lock().unwrap().insert(
path,
Arc::new(Node {
id: 1000,
kind: EntryKind::Directory,
data: Mutex::new(Data {
bytes: b"replacement ancestor".to_vec(),
revision: 1,
hidden: false,
}),
calls: Mutex::new(Vec::new()),
}),
);
}
if controls.replace_mapping {
// The callback must still receive the original capability, not this decoy.
self.0.nodes.lock().unwrap().insert(
entry.path.clone(),
Arc::new(Node {
id: 999,
kind: EntryKind::Regular,
data: Mutex::new(Data {
bytes: b"decoy".to_vec(),
revision: 1,
hidden: false,
}),
calls: Mutex::new(vec![]),
}),
);
controls.replace_mapping = false;
}
if controls.cancel_on_execute {
context.cancellation().cancel();
}
let rollback = controls.rollback;
drop(controls);
// Verify lineage identity/membership within the same transaction. The
// callback cannot see a substituted ancestor, nor a redirected target.
let nodes = self.0.nodes.lock().unwrap();
if !nodes
.get("/")
.is_some_and(|root| Arc::ptr_eq(root, &self.1))
{
return ProviderResult::NotFound;
}
for ancestor in &ancestors {
if !nodes
.get(&ancestor.path)
.is_some_and(|node| Arc::ptr_eq(node, &ancestor.node))
{
return ProviderResult::NotFound;
}
}
drop(nodes);
let refreshed_ancestors = ancestors
.into_iter()
.map(|ancestor| Entry::snapshot(ancestor.path, ancestor.node))
.collect();
let before = entry.node.data.lock().unwrap().clone();
let refreshed = Entry::snapshot(entry.path.clone(), entry.node.clone());
let attempt = operation(ResolvedEntry::new(refreshed_ancestors, refreshed));
let attempt = match attempt {
OperationAttempt::Started(Ok(mut result)) => {
result.validator = Some(
Entry::snapshot(entry.path, entry.node.clone())
.validator
.as_bytes()
.to_vec(),
);
OperationAttempt::Started(Ok(result))
}
failed @ OperationAttempt::Started(Err(_)) if rollback => {
*entry.node.data.lock().unwrap() = before;
// The guarantee is upgraded only after the data is restored.
failed.confirm_rollback()
}
other => other,
};
ProviderResult::Found(attempt)
}
}
// Local wrapper avoids orphan rules and implements both external and local traits.
struct Object {
entry: Entry,
custom: InterfaceRef,
shared: Arc<Shared>,
context: ExecutionContext,
}
impl WipObject for Object {
fn name(&self) -> &str {
self.entry.name()
}
fn description(&self) -> Option<&str> {
self.entry.description()
}
fn reference(&self) -> Option<&str> {
// Public representation identity need not be the backend entry ref.
// ScopeObservation must capture this published Object's ref.
(self.entry.path == "/").then_some("memory-root")
}
fn validator(&self) -> Option<&Validator> {
self.entry.validator()
}
fn requires_validator(&self, interface: &InterfaceRef, operation: &str) -> bool {
interface == &self.custom && matches!(operation, "read" | "change")
}
}
impl Object {
fn invoked(&self, operation: &'static str) {
assert!(
matches!(
self.shared.transaction.try_lock(),
Err(TryLockError::WouldBlock)
),
"typed dispatch escaped the backend lock"
);
self.shared.record("typed", &self.entry.path, &self.context);
self.entry.node.calls.lock().unwrap().push(operation);
}
fn change_data(&self, bytes: Vec<u8>) {
let mut data = self.entry.node.data.lock().unwrap();
data.bytes = bytes;
data.revision += 1;
}
}
impl MemoryOperations for Object {
fn read(&self) -> wip::OperationResult<wip::Bytes> {
self.invoked("custom.read");
Ok(wip::Bytes(
self.entry.node.data.lock().unwrap().bytes.clone(),
))
}
fn change(&self, contents: wip::Bytes) -> wip::OperationResult<()> {
self.invoked("custom.change");
self.change_data(contents.0);
Ok(())
}
fn hidden(&self) -> wip::OperationResult<()> {
self.invoked("custom.hidden");
Ok(())
}
fn denied(&self) -> wip::OperationResult<()> {
self.invoked("custom.denied");
Ok(())
}
fn fail(&self, mode: u64) -> wip::OperationResult<()> {
self.invoked("custom.fail");
let guarantee = if mode < 2 {
EffectGuarantee::NotCommitted
} else {
self.change_data(b"uncertain effects".to_vec());
EffectGuarantee::Unknown
};
let diagnostic = format!("{BACKEND_SECRET}: {POLICY_SECRET}");
Err(if mode.is_multiple_of(2) {
HostError::internal(diagnostic, guarantee)
} else {
HostError::resource_limit(diagnostic, guarantee)
})
}
}
impl FileRead for Object {
fn read(&self, offset: Option<u64>, length: Option<u64>) -> wip::OperationResult<wip::Bytes> {
self.invoked("default.read");
let data = self.entry.node.data.lock().unwrap();
let start = usize::try_from(offset.unwrap_or(0))
.unwrap_or(usize::MAX)
.min(data.bytes.len());
let count = usize::try_from(length.unwrap_or(u64::MAX)).unwrap_or(usize::MAX);
Ok(wip::Bytes(
data.bytes[start..start.saturating_add(count).min(data.bytes.len())].to_vec(),
))
}
}
struct MemoryComposition {
defaults: DefaultInterfaces,
custom: RegisteredInterface<MemoryOperationsDefinition>,
shared: Arc<Shared>,
}
impl Composition<Entry> for MemoryComposition {
type Object = Object;
fn publish(
&self,
entry: Entry,
context: &ExecutionContext,
) -> Result<PublishedObject<Object>, HostError> {
self.shared.record("publish", &entry.path, context);
let controls = self.shared.controls.lock().unwrap();
if controls.publish_error {
return Err(HostError::internal(
format!("{BACKEND_SECRET}: {POLICY_SECRET}"),
EffectGuarantee::NotCommitted,
));
}
if controls.cancel_on_publish {
context.cancellation().cancel();
}
drop(controls);
let path = entry.path.clone();
let kind = entry.kind();
let mut publication = PublishedObject::new(Object {
entry,
custom: self.custom.reference().clone(),
shared: self.shared.clone(),
context: context.clone(),
});
// Entry conditions add, replace, or hide the default capability.
if kind == EntryKind::Regular && !matches!(path.as_str(), "/replacement" | "/metadata") {
publication = publication.expose(&self.defaults.read).unwrap();
}
if matches!(
path.as_str(),
"/added" | "/replacement" | "/secret/nested/data"
) {
publication = publication.expose(&self.custom).unwrap();
}
Ok(publication)
}
}
#[derive(Clone, Copy, Debug)]
enum Principal {
Admin,
Limited,
}
struct Policy {
custom: InterfaceRef,
read: InterfaceRef,
hide_read: bool,
invalid_selection: bool,
panic_authorize: bool,
}
impl ProjectionPolicy<Entry, Object> for Policy {
type Principal = Principal;
fn object_visible(&self, principal: &Principal, entry: &Entry) -> bool {
!entry.hidden && (!matches!(principal, Principal::Limited) || entry.path != "/secret")
}
fn interface_visible(&self, principal: &Principal, interface: &InterfaceRef) -> bool {
!(matches!(principal, Principal::Limited) && self.hide_read && interface == &self.read)
}
fn interfaces(
&self,
principal: &Principal,
entry: &Entry,
available: &[InterfaceRef],
) -> Vec<InterfaceRef> {
if self.invalid_selection && entry.path == "/plain" {
return vec![self.custom.clone()];
}
let mut selected = available.to_vec();
if matches!(principal, Principal::Limited) {
selected.reverse();
}
if matches!(principal, Principal::Limited) && entry.path == "/policy-metadata" {
selected.clear();
}
selected
}
fn operation_visible(
&self,
principal: &Principal,
interface: &InterfaceRef,
operation: &str,
) -> bool {
!(matches!(principal, Principal::Limited)
&& interface == &self.custom
&& operation == "hidden")
}
fn authorize(
&self,
principal: &Principal,
_: &Object,
interface: &InterfaceRef,
operation: &str,
) -> bool {
if self.panic_authorize {
panic!("{POLICY_SECRET}");
}
!(matches!(principal, Principal::Limited)
&& interface == &self.custom
&& operation == "denied")
}
}
#[test]
fn observation_and_fetch_policy_panics_remain_sanitized_read_failures() {
struct PanickingPolicy(bool);
impl ProjectionPolicy<Entry, Object> for PanickingPolicy {
type Principal = ();
fn object_visible(&self, _: &(), _: &Entry) -> bool {
if self.0 {
panic!("{POLICY_SECRET}");
}
true
}
fn interface_visible(&self, _: &(), _: &InterfaceRef) -> bool {
if !self.0 {
panic!("{POLICY_SECRET}");
}
true
}
}
for panic_object in [true, false] {
let fixture = Fixture::new();
let mut registry = InterfaceRegistry::new();
let scope = Scope::new("/").unwrap();
let defaults = DefaultInterfaces::register(&mut registry, &scope).unwrap();
let custom = registry
.register(
&scope,
"memory.operations",
MemoryOperationsDefinition,
None,
)
.unwrap();
let reference = custom.reference().protocol_reference();
let engine = Projection::new(
MemoryBackend::new(fixture.shared.clone()),
registry,
MemoryComposition {
defaults,
custom,
shared: fixture.shared.clone(),
},
PanickingPolicy(panic_object),
fixture.limits,
);
let view = engine.view(&());
let error = view
.observe(
&ExecutionContext::new(),
&ObserveRequest {
path: "/added".into(),
depth: 0,
},
)
.unwrap_err();
assert_eq!(error.code, ProtocolErrorCode::Internal);
assert!(!error.message.contains(POLICY_SECRET));
if !panic_object {
let error = view
.fetch_interface(&FetchInterfaceRequest {
interface: reference,
})
.unwrap_err();
assert_eq!(error.code, ProtocolErrorCode::Internal);
assert!(!error.message.contains(POLICY_SECRET));
}
}
}
type Engine = Projection<MemoryBackend, MemoryComposition, Policy>;
struct Fixture {
engine: Engine,
shared: Arc<Shared>,
custom: wip_protocol::InterfaceReference,
read: wip_protocol::InterfaceReference,
limits: ResourceLimits,
// Client-known descriptors acquired coherently before execution fault injection.
descriptors: BTreeMap<(bool, wip_protocol::InterfaceReference), InterfaceDescriptor>,
}
impl Fixture {
fn new() -> Self {
Self::configured(false, false, false, None, ResourceLimits::default())
}
fn configured(
hide_read: bool,
invalid_selection: bool,
panic_authorize: bool,
interface_validator: Option<Validator>,
limits: ResourceLimits,
) -> Self {
let mut nodes = BTreeMap::new();
for (index, (path, kind)) in [
("/", EntryKind::Directory),
("/plain", EntryKind::Regular),
("/added", EntryKind::Regular),
("/replacement", EntryKind::Regular),
("/metadata", EntryKind::Regular),
("/policy-metadata", EntryKind::Regular),
("/unsupported", EntryKind::Unsupported),
("/link", EntryKind::Symlink),
("/secret", EntryKind::Directory),
("/secret/nested", EntryKind::Directory),
("/secret/nested/data", EntryKind::Regular),
]
.into_iter()
.enumerate()
{
nodes.insert(
path.into(),
Arc::new(Node {
id: index as u64,
kind,
data: Mutex::new(Data {
bytes: b"hello memory".to_vec(),
revision: 1,
hidden: false,
}),
calls: Mutex::new(vec![]),
}),
);
}
let shared = Arc::new(Shared {
nodes: Mutex::new(nodes),
transaction: Mutex::new(()),
controls: Mutex::new(Controls::default()),
seen: Mutex::new(vec![]),
});
let mut registry = InterfaceRegistry::new();
let scope = Scope::new("/").unwrap();
let defaults = DefaultInterfaces::register(&mut registry, &scope).unwrap();
let custom = registry
.register(
&scope,
"memory.operations",
MemoryOperationsDefinition,
interface_validator,
)
.unwrap();
let read_ref = defaults.read.reference().clone();
let custom_ref = custom.reference().clone();
let policy = Policy {
custom: custom_ref.clone(),
read: read_ref.clone(),
hide_read,
invalid_selection,
panic_authorize,
};
let composition = MemoryComposition {
defaults,
custom,
shared: shared.clone(),
};
let engine = Projection::new(
MemoryBackend::new(shared.clone()),
registry,
composition,
policy,
limits,
);
let mut descriptors = BTreeMap::new();
if !invalid_selection {
for principal in [Principal::Admin, Principal::Limited] {
for reference in [&read_ref, &custom_ref] {
let reference = reference.protocol_reference();
if let Ok(observation) = engine
.view(&principal)
.fetch_interface(&fetch_request(&reference))
{
descriptors.insert(
(matches!(principal, Principal::Admin), reference),
observation.descriptor,
);
}
}
}
}
shared.seen.lock().unwrap().clear();
Self {
engine,
shared,
custom: custom_ref.protocol_reference(),
read: read_ref.protocol_reference(),
limits,
descriptors,
}
}
fn observe(&self, principal: &Principal, path: &str, depth: u32) -> ObjectObservation {
self.engine
.view(principal)
.observe(&ExecutionContext::new(), &observe_request(path, depth))
.unwrap()
}
fn descriptor(
&self,
principal: &Principal,
reference: &wip_protocol::InterfaceReference,
) -> InterfaceDescriptor {
self.descriptors[&(matches!(principal, Principal::Admin), reference.clone())].clone()
}
fn request(
&self,
path: &str,
reference: &wip_protocol::InterfaceReference,
operation: &str,
validator: Option<Vec<u8>>,
) -> CallOperationRequest {
CallOperationRequest {
target: Target {
path: path.into(),
validator,
},
interface: InterfaceTarget {
scope_ref: None,
reference: reference.clone(),
validator: None,
},
operation: operation.into(),
arguments: BTreeMap::new(),
}
}
fn http_call(
&self,
principal: &Principal,
request: &CallOperationRequest,
descriptor: &InterfaceDescriptor,
context: &ExecutionContext,
) -> (
http::Response<Vec<u8>>,
DecodedResponse<CallOperationResponse>,
) {
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let view = self.engine.view(principal);
let response = HttpAdapter::new(&view, &endpoint, self.limits).handle_http_with_context(
encode_call_operation_request(&endpoint, request, descriptor, self.limits.wire)
.unwrap(),
context,
);
let decoded =
decode_call_operation_response(request, descriptor, &response, self.limits.wire)
.unwrap();
(response, decoded)
}
}
#[test]
fn ancestor_visibility_refresh_inside_boundary_prevents_descendant_invocation_and_effects() {
for (target, ancestor) in [
("/added", "/"),
("/secret/nested/data", "/"),
("/secret/nested/data", "/secret"),
("/secret/nested/data", "/secret/nested"),
] {
for operation in ["denied", "read", "change"] {
let fixture = Fixture::new();
let observation = fixture.observe(&Principal::Admin, target, 0);
let descriptor = fixture.descriptor(&Principal::Admin, &fixture.custom);
let mut request = fixture.request(
target,
&fixture.custom,
operation,
observation.object.validator,
);
if operation == "change" {
request
.arguments
.insert("contents".into(), Value::Bytes(b"must not commit".to_vec()));
}
let leaf = fixture.shared.node(target);
let before = leaf.data.lock().unwrap().clone();
fixture.shared.controls.lock().unwrap().hide_ancestor = Some(ancestor.into());
let (wire, response) = fixture.http_call(
&Principal::Admin,
&request,
&descriptor,
&ExecutionContext::new(),
);
wire_error(response, ProtocolErrorCode::NotFound);
assert!(leaf.calls.lock().unwrap().is_empty());
assert_eq!(*leaf.data.lock().unwrap(), before);
assert!(!String::from_utf8_lossy(wire.body()).contains(BACKEND_SECRET));
let error = fixture
.engine
.view(&Principal::Admin)
.observe(&ExecutionContext::new(), &observe_request(target, 0))
.unwrap_err();
assert_eq!(error.code, ProtocolErrorCode::NotFound);
// The entire target resolves once, not as unrelated path prefixes.
let seen = fixture.shared.seen.lock().unwrap();
assert!(
seen.iter()
.filter(|event| event.stage == "resolve")
.all(|event| event.path == target)
);
}
}
}
#[test]
fn replaced_visible_ancestors_reject_without_redirecting_or_dispatching_the_pinned_target() {
for (target, ancestor) in [
("/added", "/"),
("/secret/nested/data", "/secret"),
("/secret/nested/data", "/secret/nested"),
] {
let fixture = Fixture::new();
let observation = fixture.observe(&Principal::Admin, target, 0);
let descriptor = fixture.descriptor(&Principal::Admin, &fixture.custom);
let mut request = fixture.request(
target,
&fixture.custom,
"change",
observation.object.validator,
);
request
.arguments
.insert("contents".into(), Value::Bytes(b"must not commit".to_vec()));
let leaf = fixture.shared.node(target);
let original_ancestor = fixture.shared.node(ancestor);
let before = leaf.data.lock().unwrap().clone();
fixture.shared.controls.lock().unwrap().replace_ancestor = Some(ancestor.into());
let (_, response) = fixture.http_call(
&Principal::Admin,
&request,
&descriptor,
&ExecutionContext::new(),
);
wire_error(response, ProtocolErrorCode::NotFound);
assert!(leaf.calls.lock().unwrap().is_empty());
assert_eq!(*leaf.data.lock().unwrap(), before);
assert!(!Arc::ptr_eq(
&original_ancestor,
&fixture.shared.node(ancestor)
));
assert!(
fixture
.shared
.node(ancestor)
.calls
.lock()
.unwrap()
.is_empty()
);
// A replacement is visible but cannot authorize a previously resolved lineage.
assert!(!fixture.shared.node(ancestor).data.lock().unwrap().hidden);
}
}
fn observe_request(path: &str, depth: u32) -> ObserveRequest {
ObserveRequest {
path: path.into(),
depth,
}
}
fn fetch_request(reference: &wip_protocol::InterfaceReference) -> FetchInterfaceRequest {
FetchInterfaceRequest {
interface: reference.clone(),
}
}
fn success<T: std::fmt::Debug>(response: DecodedResponse<T>) -> T {
match response {
DecodedResponse::Success(value) => value,
other => panic!("expected success, got {other:?}"),
}
}
fn wire_error<T: std::fmt::Debug>(response: DecodedResponse<T>, expected: ProtocolErrorCode) {
match response {
DecodedResponse::ProtocolFailure {
error,
status_mismatch,
} => {
assert_eq!(error.code, expected);
assert!(status_mismatch.is_none(), "noncanonical HTTP error status");
}
other => panic!("expected {expected:?}, got {other:?}"),
}
}
fn no_secrets(response: &http::Response<Vec<u8>>) {
let body = std::str::from_utf8(response.body()).unwrap();
assert!(
!body.contains(BACKEND_SECRET),
"backend path leaked: {body}"
);
assert!(
!body.contains(POLICY_SECRET),
"policy diagnostic leaked: {body}"
);
assert!(
response.body().len() < 1024,
"failure diagnostic was not bounded"
);
}
#[test]
fn external_composition_adds_replaces_and_hides_default_interfaces_by_entry_condition() {
let fixture = Fixture::new();
assert!(Arc::ptr_eq(&fixture.engine.backend().0, &fixture.shared));
for (path, expected) in [
("/plain", vec![fixture.read.clone()]),
("/added", vec![fixture.read.clone(), fixture.custom.clone()]),
("/replacement", vec![fixture.custom.clone()]),
("/metadata", vec![]),
("/unsupported", vec![]),
("/link", vec![]),
("/", vec![]),
] {
let observation = fixture.observe(&Principal::Admin, path, 0);
assert_eq!(observation.object.interfaces, expected, "{path}");
assert_eq!(
observation.object.description.as_deref(),
Some("public memory entry")
);
assert!(observation.children.is_none());
assert_eq!(
observation.object.r#ref.as_deref(),
(path == "/").then_some("memory-root")
);
for reference in &observation.object.interfaces {
let fetched = fixture
.engine
.view(&Principal::Admin)
.fetch_interface(&fetch_request(reference))
.unwrap();
assert_eq!(&fetched.interface, reference);
fetched.descriptor.validate().unwrap();
}
}
let request = fixture.request("/added", &fixture.read, "read", None);
assert_eq!(
fixture
.engine
.view(&Principal::Admin)
.call_operation(request)
.unwrap()
.result,
Value::Bytes(b"hello memory".to_vec())
);
let observation = fixture.observe(&Principal::Admin, "/replacement", 0);
let request = fixture.request(
"/replacement",
&fixture.custom,
"read",
observation.object.validator,
);
assert_eq!(
fixture
.engine
.view(&Principal::Admin)
.call_operation(request)
.unwrap()
.result,
Value::Bytes(b"hello memory".to_vec())
);
let request = fixture.request("/replacement", &fixture.read, "read", None);
assert_eq!(
fixture
.engine
.view(&Principal::Admin)
.call_operation(request)
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::InterfaceMismatch
);
}
#[test]
fn principal_views_hide_children_and_guessed_descendants_for_observation_and_calls() {
let fixture = Fixture::new();
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let context = ExecutionContext::new();
for principal in [Principal::Admin, Principal::Limited] {
let view = fixture.engine.view(&principal);
let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits);
let request = observe_request("/", 3);
let response = adapter.handle_http_with_context(
encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(),
&context,
);
let tree =
success(decode_observe_response(&request, &response, fixture.limits.wire).unwrap());
let secret = tree
.children
.unwrap()
.into_iter()
.find(|child| child.object.name == "secret");
match principal {
Principal::Admin => assert_eq!(
secret.unwrap().children.unwrap()[0]
.children
.as_ref()
.unwrap()[0]
.object
.name,
"data"
),
Principal::Limited => assert!(secret.is_none()),
}
}
fixture.shared.seen.lock().unwrap().clear();
for path in ["/secret", "/secret/nested", "/secret/nested/data"] {
let view = fixture.engine.view(&Principal::Limited);
assert_eq!(
view.observe(&context, &observe_request(path, 0))
.unwrap_err()
.code,
ProtocolErrorCode::NotFound
);
assert_eq!(
view.observe_object(path).unwrap_err().protocol_error().code,
ProtocolErrorCode::NotFound
);
let request = fixture.request(path, &fixture.custom, "denied", None);
let (response, decoded) = fixture.http_call(
&Principal::Limited,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&context,
);
wire_error(decoded, ProtocolErrorCode::NotFound);
no_secrets(&response);
}
assert!(
fixture
.shared
.seen
.lock()
.unwrap()
.iter()
.all(|seen| seen.stage == "resolve"),
"hidden lineage was published, enumerated, or dispatched"
);
assert!(
fixture
.shared
.node("/secret/nested/data")
.calls
.lock()
.unwrap()
.is_empty()
);
// A principal does not mutate the other principal's descriptor or tree.
assert_eq!(
fixture
.observe(&Principal::Admin, "/secret/nested/data", 0)
.object
.name,
"data"
);
}
#[test]
fn projected_descriptor_and_dispatch_distinguish_hidden_missing_and_denied_operations() {
let fixture = Fixture::new();
let admin = fixture.descriptor(&Principal::Admin, &fixture.custom);
let limited = fixture.descriptor(&Principal::Limited, &fixture.custom);
assert!(
admin
.operations
.iter()
.any(|operation| operation.name == "hidden")
);
assert!(
!limited
.operations
.iter()
.any(|operation| operation.name == "hidden")
);
assert!(
limited
.operations
.iter()
.any(|operation| operation.name == "denied")
);
assert_eq!(
fixture
.observe(&Principal::Limited, "/added", 0)
.object
.interfaces,
vec![fixture.custom.clone(), fixture.read.clone()]
);
let view = fixture.engine.view(&Principal::Limited);
for (operation, code) in [
("hidden", ProtocolErrorCode::OperationNotFound),
("absent", ProtocolErrorCode::OperationNotFound),
("denied", ProtocolErrorCode::PermissionDenied),
] {
let request = fixture.request("/added", &fixture.custom, operation, None);
assert_eq!(
view.call_operation(request.clone())
.unwrap_err()
.protocol_error()
.code,
code
);
if operation != "absent" {
// Encode against the full descriptor to simulate a guessed hidden operation.
let (response, decoded) = fixture.http_call(
&Principal::Limited,
&request,
&admin,
&ExecutionContext::new(),
);
wire_error(decoded, code);
no_secrets(&response);
}
}
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
let request = fixture.request("/added", &fixture.custom, "denied", None);
fixture
.engine
.view(&Principal::Admin)
.call_operation(request)
.unwrap();
assert_eq!(
*fixture.shared.node("/added").calls.lock().unwrap(),
vec!["custom.denied"]
);
assert_eq!(
fixture.descriptor(&Principal::Admin, &fixture.custom),
admin
);
}
#[test]
fn interface_visibility_filters_publication_and_fetch_together() {
let fixture = Fixture::configured(true, false, false, None, ResourceLimits::default());
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let view = fixture.engine.view(&Principal::Limited);
let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits);
assert_eq!(
fixture
.observe(&Principal::Limited, "/added", 0)
.object
.interfaces,
vec![fixture.custom.clone()]
);
assert!(
fixture
.observe(&Principal::Limited, "/plain", 0)
.object
.interfaces
.is_empty()
);
let tree = fixture.observe(&Principal::Limited, "/", 1);
for child in tree.children.unwrap() {
for reference in child.object.interfaces {
let request = fetch_request(&reference);
let response = adapter.handle_http_with_context(
encode_fetch_interface_request(&endpoint, &request, fixture.limits.wire).unwrap(),
&ExecutionContext::new(),
);
let fetched: FetchInterfaceResponse = success(
decode_fetch_interface_response(&request, &response, fixture.limits.wire).unwrap(),
);
assert_eq!(fetched.interface, reference);
}
}
let request = fetch_request(&fixture.read);
let response = adapter.handle_http_with_context(
encode_fetch_interface_request(&endpoint, &request, fixture.limits.wire).unwrap(),
&ExecutionContext::new(),
);
wire_error(
decode_fetch_interface_response(&request, &response, fixture.limits.wire).unwrap(),
ProtocolErrorCode::InterfaceNotFound,
);
let request = fixture.request("/added", &fixture.read, "read", None);
assert_eq!(
view.call_operation(request)
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::InterfaceMismatch
);
assert_eq!(
fixture
.observe(&Principal::Admin, "/added", 0)
.object
.interfaces,
vec![fixture.read.clone(), fixture.custom.clone()]
);
assert!(
fixture
.engine
.view(&Principal::Admin)
.fetch_interface(&fetch_request(&fixture.read))
.is_ok()
);
}
#[test]
fn entry_specific_policy_selects_a_subset_without_globally_hiding_the_descriptor() {
let fixture = Fixture::new();
let admin = fixture.observe(&Principal::Admin, "/policy-metadata", 0);
let limited = fixture.observe(&Principal::Limited, "/policy-metadata", 0);
assert_eq!(admin.object.interfaces, vec![fixture.read.clone()]);
assert!(limited.object.interfaces.is_empty());
assert_eq!(admin.object.validator, limited.object.validator);
let request = fixture.request("/policy-metadata", &fixture.read, "read", None);
let (response, decoded) = fixture.http_call(
&Principal::Limited,
&request,
&fixture.descriptor(&Principal::Limited, &fixture.read),
&ExecutionContext::new(),
);
wire_error(decoded, ProtocolErrorCode::InterfaceMismatch);
no_secrets(&response);
assert!(
fixture
.shared
.node("/policy-metadata")
.calls
.lock()
.unwrap()
.is_empty()
);
fixture
.engine
.view(&Principal::Admin)
.call_operation(request)
.unwrap();
assert_eq!(
*fixture
.shared
.node("/policy-metadata")
.calls
.lock()
.unwrap(),
vec!["default.read"]
);
}
#[test]
fn http_observe_fetch_and_call_round_trip_uses_only_client_observed_publications() {
for principal in [Principal::Admin, Principal::Limited] {
let fixture = Fixture::new();
let context = ExecutionContext::new();
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let view = fixture.engine.view(&principal);
let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits);
let observe = observe_request("/added", 0);
let response = adapter.handle_http_with_context(
encode_observe_request(&endpoint, &observe, fixture.limits.wire).unwrap(),
&context,
);
let observed =
success(decode_observe_response(&observe, &response, fixture.limits.wire).unwrap())
.object;
let mut custom_descriptor = None;
for reference in observed.interfaces {
let fetch = fetch_request(&reference);
let response = adapter.handle_http_with_context(
encode_fetch_interface_request(&endpoint, &fetch, fixture.limits.wire).unwrap(),
&context,
);
let fetched = success(
decode_fetch_interface_response(&fetch, &response, fixture.limits.wire).unwrap(),
);
let application_interface = fetched
.descriptor
.operations
.iter()
.any(|operation| operation.name == "change");
let mut request = fixture.request(
"/added",
&fetched.interface,
"read",
if application_interface {
observed.validator.clone()
} else {
None
},
);
request.interface.validator = fetched.validator;
if !application_interface {
request.arguments.insert("offset".into(), Value::Integer(6));
request.arguments.insert("length".into(), Value::Integer(6));
}
let response = adapter.handle_http_with_context(
encode_call_operation_request(
&endpoint,
&request,
&fetched.descriptor,
fixture.limits.wire,
)
.unwrap(),
&context,
);
let result = success(
decode_call_operation_response(
&request,
&fetched.descriptor,
&response,
fixture.limits.wire,
)
.unwrap(),
);
assert_eq!(
result.result,
Value::Bytes(if application_interface {
b"hello memory".to_vec()
} else {
b"memory".to_vec()
})
);
assert_eq!(result.validator, observed.validator);
if application_interface {
assert_eq!(
fetched
.descriptor
.operations
.iter()
.any(|operation| operation.name == "hidden"),
matches!(principal, Principal::Admin)
);
custom_descriptor = Some((fetched.interface, fetched.descriptor));
}
}
let (reference, descriptor) = custom_descriptor.unwrap();
let mut request =
fixture.request("/added", &reference, "change", observed.validator.clone());
request
.arguments
.insert("contents".into(), Value::Bytes(b"HTTP commit".to_vec()));
let response = adapter.handle_http_with_context(
encode_call_operation_request(&endpoint, &request, &descriptor, fixture.limits.wire)
.unwrap(),
&context,
);
let result = success(
decode_call_operation_response(&request, &descriptor, &response, fixture.limits.wire)
.unwrap(),
);
assert_eq!(result.result, Value::Unit);
assert_ne!(result.validator, observed.validator);
let response = adapter.handle_http_with_context(
encode_observe_request(&endpoint, &observe, fixture.limits.wire).unwrap(),
&context,
);
let committed =
success(decode_observe_response(&observe, &response, fixture.limits.wire).unwrap());
assert_eq!(result.validator, committed.object.validator);
assert_eq!(
fixture.shared.node("/added").data.lock().unwrap().bytes,
b"HTTP commit"
);
}
}
#[test]
fn policy_cannot_add_an_unimplemented_interface() {
let fixture = Fixture::configured(false, true, false, None, ResourceLimits::default());
let view = fixture.engine.view(&Principal::Admin);
assert_eq!(
view.observe(&ExecutionContext::new(), &observe_request("/plain", 0))
.unwrap_err()
.code,
ProtocolErrorCode::Internal
);
let request = fixture.request("/plain", &fixture.custom, "denied", None);
// This deliberately invalid publication cannot supply a fetch response.
// Use the application's authored schema solely to encode the test request.
let (response, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&MemoryOperationsDefinition::descriptor(),
&ExecutionContext::new(),
);
wire_error(decoded, ProtocolErrorCode::Internal);
no_secrets(&response);
assert!(
fixture
.shared
.node("/plain")
.calls
.lock()
.unwrap()
.is_empty()
);
}
#[test]
fn validators_are_required_per_qualified_operation_and_preconditions_precede_permission() {
let fixture = Fixture::new();
let view = fixture.engine.view(&Principal::Limited);
let validator = fixture
.observe(&Principal::Limited, "/added", 0)
.object
.validator;
// The same operation name needs a validator on only one interface.
view.call_operation(fixture.request("/added", &fixture.read, "read", None))
.unwrap();
for operation in ["read", "change"] {
let request = fixture.request("/added", &fixture.custom, operation, None);
assert_eq!(
view.call_operation(request)
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::ValidatorRequired
);
}
for (operation, supplied, code) in [
(
"denied",
Some(b"stale".to_vec()),
ProtocolErrorCode::ValidatorMismatch,
),
(
"hidden",
validator.clone(),
ProtocolErrorCode::OperationNotFound,
),
(
"denied",
validator.clone(),
ProtocolErrorCode::PermissionDenied,
),
] {
let request = fixture.request("/added", &fixture.custom, operation, supplied);
assert_eq!(
view.call_operation(request)
.unwrap_err()
.protocol_error()
.code,
code
);
}
let mut request = fixture.request("/added", &fixture.custom, "denied", validator.clone());
request
.arguments
.insert("unexpected".into(), Value::String(POLICY_SECRET.into()));
assert_eq!(
view.call_operation(request)
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::InvalidArguments
);
view.call_operation(fixture.request("/added", &fixture.custom, "read", validator))
.unwrap();
assert_eq!(
*fixture.shared.node("/added").calls.lock().unwrap(),
vec!["default.read", "custom.read"]
);
}
#[test]
fn registered_interface_validators_are_independent_call_preconditions() {
let fixture = Fixture::configured(
false,
false,
false,
Some(Validator::new(b"schema-1".to_vec())),
ResourceLimits::default(),
);
let fetched = fixture
.engine
.view(&Principal::Admin)
.fetch_interface(&fetch_request(&fixture.custom))
.unwrap();
assert_eq!(fetched.validator, Some(b"schema-1".to_vec()));
let view = fixture.engine.view(&Principal::Limited);
let mut request = fixture.request("/added", &fixture.custom, "hidden", None);
assert_eq!(
view.call_operation(request.clone())
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::InterfaceValidatorRequired
);
request.interface.validator = Some(b"schema-old".to_vec());
assert_eq!(
view.call_operation(request.clone())
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::InterfaceValidatorMismatch
);
request.interface.validator = fetched.validator;
assert_eq!(
view.call_operation(request)
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::OperationNotFound
);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
}
#[test]
fn backend_refreshes_validators_under_execution_lock_before_dispatch() {
let fixture = Fixture::new();
let validator = fixture
.observe(&Principal::Admin, "/added", 0)
.object
.validator;
fixture.shared.controls.lock().unwrap().bump_before_execute = true;
let mut request = fixture.request("/added", &fixture.custom, "change", validator);
request
.arguments
.insert("contents".into(), Value::Bytes(b"should not run".to_vec()));
let (response, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&ExecutionContext::new(),
);
wire_error(decoded, ProtocolErrorCode::ValidatorMismatch);
no_secrets(&response);
let node = fixture.shared.node("/added");
assert!(node.calls.lock().unwrap().is_empty());
assert_eq!(node.data.lock().unwrap().bytes, b"hello memory");
assert_ne!(
fixture
.observe(&Principal::Admin, "/added", 0)
.object
.validator,
request.target.validator
);
}
#[test]
fn operation_keeps_the_resolved_target_and_returns_its_committed_post_validator() {
let fixture = Fixture::new();
let original = fixture.shared.node("/added");
let validator = fixture
.observe(&Principal::Admin, "/added", 0)
.object
.validator;
fixture.shared.controls.lock().unwrap().replace_mapping = true;
let mut request = fixture.request("/added", &fixture.custom, "change", validator.clone());
request
.arguments
.insert("contents".into(), Value::Bytes(b"committed".to_vec()));
let (_, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&ExecutionContext::new(),
);
let result = success(decoded);
assert_eq!(result.result, Value::Unit);
assert_ne!(result.validator, validator);
assert_eq!(
result.validator,
Some(
Entry::snapshot("/added".into(), original.clone())
.validator
.as_bytes()
.to_vec()
)
);
assert_eq!(original.data.lock().unwrap().bytes, b"committed");
assert_eq!(*original.calls.lock().unwrap(), vec!["custom.change"]);
let decoy = fixture.shared.node("/added");
assert!(!Arc::ptr_eq(&original, &decoy));
assert_eq!(decoy.data.lock().unwrap().bytes, b"decoy");
assert!(decoy.calls.lock().unwrap().is_empty());
}
#[test]
fn concurrent_conditional_calls_are_serialized_across_validation_and_commit() {
let fixture = Fixture::new();
let validator = fixture
.observe(&Principal::Admin, "/added", 0)
.object
.validator;
let barrier = Barrier::new(2);
let results = std::thread::scope(|scope| {
let mut workers = Vec::new();
for bytes in [b"first".to_vec(), b"second".to_vec()] {
let mut request =
fixture.request("/added", &fixture.custom, "change", validator.clone());
request
.arguments
.insert("contents".into(), Value::Bytes(bytes));
let engine = &fixture.engine;
let barrier = &barrier;
workers.push(scope.spawn(move || {
barrier.wait();
engine.view(&Principal::Admin).call_operation(request)
}));
}
workers
.into_iter()
.map(|worker| worker.join().unwrap())
.collect::<Vec<_>>()
});
assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
assert_eq!(
results
.iter()
.filter_map(|result| result.as_ref().err())
.next()
.unwrap()
.protocol_error()
.code,
ProtocolErrorCode::ValidatorMismatch
);
assert_eq!(
*fixture.shared.node("/added").calls.lock().unwrap(),
vec!["custom.change"]
);
let returned = results.into_iter().find_map(Result::ok).unwrap().validator;
assert_eq!(
returned,
fixture
.observe(&Principal::Admin, "/added", 0)
.object
.validator
);
}
#[test]
fn explicit_effect_guarantees_and_confirmed_rollback_control_failure_classification() {
for rollback in [false, true] {
for mode in 0..4 {
let fixture = Fixture::new();
fixture.shared.controls.lock().unwrap().rollback = rollback;
let before = fixture.shared.node("/added").data.lock().unwrap().clone();
let before_validator = fixture
.observe(&Principal::Admin, "/added", 0)
.object
.validator;
let mut request = fixture.request("/added", &fixture.custom, "fail", None);
request
.arguments
.insert("mode".into(), Value::Integer(mode));
let (response, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&ExecutionContext::new(),
);
let code = if mode >= 2 && !rollback {
ProtocolErrorCode::OperationOutcomeUnknown
} else if mode % 2 == 0 {
ProtocolErrorCode::Internal
} else {
ProtocolErrorCode::ResourceLimitExceeded
};
wire_error(decoded, code);
no_secrets(&response);
let after = fixture.shared.node("/added").data.lock().unwrap().clone();
assert_eq!(
*fixture.shared.node("/added").calls.lock().unwrap(),
vec!["custom.fail"]
);
if rollback || mode < 2 {
assert_eq!(after, before, "mode={mode}, rollback={rollback}");
assert_eq!(
fixture
.observe(&Principal::Admin, "/added", 0)
.object
.validator,
before_validator
);
} else {
assert_eq!(after.bytes, b"uncertain effects");
assert_ne!(after.revision, before.revision);
}
}
}
}
#[test]
fn backend_boundary_failures_never_upgrade_unknown_effects() {
for (guarantee, code) in [
(EffectGuarantee::NotCommitted, ProtocolErrorCode::Internal),
(
EffectGuarantee::Unknown,
ProtocolErrorCode::OperationOutcomeUnknown,
),
] {
let fixture = Fixture::new();
fixture.shared.controls.lock().unwrap().execute_error = Some(guarantee);
let request = fixture.request("/added", &fixture.custom, "denied", None);
let (response, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&ExecutionContext::new(),
);
wire_error(decoded, code);
no_secrets(&response);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
}
}
#[test]
fn backend_and_composition_secret_diagnostics_are_sanitized_on_observe_and_call() {
for stage in ["resolve", "children", "publish"] {
let fixture = Fixture::new();
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
{
let mut controls = fixture.shared.controls.lock().unwrap();
match stage {
"resolve" => controls.resolve_error = true,
"children" => controls.children_error = true,
"publish" => controls.publish_error = true,
_ => unreachable!(),
}
}
let view = fixture.engine.view(&Principal::Admin);
let request = observe_request("/", 1);
assert_eq!(
view.observe(&ExecutionContext::new(), &request)
.unwrap_err()
.code,
ProtocolErrorCode::Internal
);
let response = HttpAdapter::new(&view, &endpoint, fixture.limits).handle_http_with_context(
encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(),
&ExecutionContext::new(),
);
wire_error(
decode_observe_response(&request, &response, fixture.limits.wire).unwrap(),
ProtocolErrorCode::Internal,
);
no_secrets(&response);
if stage != "children" {
let request = fixture.request("/added", &fixture.custom, "denied", None);
let (response, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&ExecutionContext::new(),
);
wire_error(decoded, ProtocolErrorCode::Internal);
no_secrets(&response);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
}
}
}
#[test]
fn policy_panics_and_guessed_operation_diagnostics_do_not_leak_to_http() {
let fixture = Fixture::configured(false, false, true, None, ResourceLimits::default());
let request = fixture.request("/added", &fixture.custom, "denied", None);
let (response, decoded) = fixture.http_call(
&Principal::Limited,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&ExecutionContext::new(),
);
wire_error(decoded, ProtocolErrorCode::OperationOutcomeUnknown);
no_secrets(&response);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
let fixture = Fixture::new();
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let view = fixture.engine.view(&Principal::Limited);
// This valid metadata names a nonexistent operation; bypass client-side
// descriptor validation to exercise server diagnostics for untrusted names.
let body = format!(
r#"{{"target":{{"path":"/added"}},"interface":{{"reference":{{"scope":"{}","name":"{}"}}}},"operation":"{}","arguments":{{}}}}"#,
fixture.custom.scope, fixture.custom.name, POLICY_SECRET
);
let request = http::Request::builder()
.method(http::Method::POST)
.uri(endpoint.route(wip_http::Route::CallOperation).as_str())
.header(http::header::CONTENT_TYPE, wip_http::JSON_CONTENT_TYPE)
.body(body.into_bytes())
.unwrap();
let response = HttpAdapter::new(&view, &endpoint, fixture.limits)
.handle_http_with_context(request, &ExecutionContext::new());
no_secrets(&response);
assert!(
std::str::from_utf8(response.body())
.unwrap()
.contains("operation_not_found")
);
}
#[test]
fn http_context_reaches_resolve_children_publication_boundary_and_typed_operation_unchanged() {
let fixture = Fixture::new();
let token = CancellationToken::new();
let deadline = Instant::now() + Duration::from_secs(60);
let context = ExecutionContext::new()
.with_cancellation(token.clone())
.with_deadline(deadline)
.with_resource_budget(77);
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let view = fixture.engine.view(&Principal::Admin);
let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits);
let request = observe_request("/", 1);
let response = adapter.handle_http_with_context(
encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(),
&context,
);
success(decode_observe_response(&request, &response, fixture.limits.wire).unwrap());
let request = fixture.request("/added", &fixture.read, "read", None);
let (_, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.read),
&context,
);
success(decoded);
let seen = fixture.shared.seen.lock().unwrap();
for stage in ["resolve", "children", "publish", "execute", "typed"] {
assert!(
seen.iter().any(|record| record.stage == stage),
"missing {stage}"
);
}
for record in seen.iter() {
assert_eq!(record.deadline, Some(deadline), "{}", record.stage);
assert_eq!(record.budget, Some(77), "{}", record.stage);
assert!(!record.token.is_cancelled());
}
token.cancel();
assert!(
seen.iter().all(|record| record.token.is_cancelled()),
"a context stage received an unrelated cancellation token"
);
}
#[test]
fn cancelled_elapsed_and_empty_budget_requests_reject_without_backend_work() {
for condition in ["cancelled", "deadline", "budget"] {
let fixture = Fixture::new();
let token = CancellationToken::new();
let context = match condition {
"cancelled" => {
token.cancel();
ExecutionContext::new().with_cancellation(token)
}
"deadline" => {
ExecutionContext::new().with_deadline(Instant::now() - Duration::from_secs(1))
}
"budget" => ExecutionContext::new().with_resource_budget(0),
_ => unreachable!(),
};
let view = fixture.engine.view(&Principal::Admin);
let request = observe_request("/added", 0);
assert_eq!(
view.observe(&context, &request).unwrap_err().code,
ProtocolErrorCode::ResourceLimitExceeded
);
let call = fixture.request("/added", &fixture.read, "read", None);
assert_eq!(
view.call_operation_with_context(&context, call.clone())
.unwrap_err()
.protocol_error()
.code,
ProtocolErrorCode::ResourceLimitExceeded
);
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let adapter = HttpAdapter::new(&view, &endpoint, fixture.limits);
let response = adapter.handle_http_with_context(
encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(),
&context,
);
if condition == "deadline" {
assert_eq!(response.status(), http::StatusCode::GATEWAY_TIMEOUT);
assert!(response.body().is_empty());
} else {
wire_error(
decode_observe_response(&request, &response, fixture.limits.wire).unwrap(),
ProtocolErrorCode::ResourceLimitExceeded,
);
let (_, decoded) = fixture.http_call(
&Principal::Admin,
&call,
&fixture.descriptor(&Principal::Admin, &fixture.read),
&context,
);
wire_error(decoded, ProtocolErrorCode::ResourceLimitExceeded);
}
assert!(fixture.shared.seen.lock().unwrap().is_empty());
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
}
}
#[test]
fn cancellation_or_visibility_change_inside_boundary_prevents_typed_dispatch() {
for condition in ["execute", "publish", "visibility"] {
let fixture = Fixture::new();
{
let mut controls = fixture.shared.controls.lock().unwrap();
match condition {
"execute" => controls.cancel_on_execute = true,
"publish" => controls.cancel_on_publish = true,
"visibility" => controls.hide_before_execute = true,
_ => unreachable!(),
}
}
let request = fixture.request("/added", &fixture.custom, "denied", None);
let context = ExecutionContext::new();
let (response, decoded) = fixture.http_call(
&Principal::Admin,
&request,
&fixture.descriptor(&Principal::Admin, &fixture.custom),
&context,
);
wire_error(
decoded,
if condition == "visibility" {
ProtocolErrorCode::NotFound
} else {
ProtocolErrorCode::ResourceLimitExceeded
},
);
no_secrets(&response);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
if condition != "visibility" {
assert!(context.cancellation().is_cancelled());
}
}
}
#[test]
fn observation_depth_node_and_context_budgets_fail_without_truncating_the_tree() {
let limits = ResourceLimits {
max_observation_depth: 1,
max_observation_nodes: 2,
..ResourceLimits::default()
};
let fixture = Fixture::configured(false, false, false, None, limits);
let view = fixture.engine.view(&Principal::Admin);
assert_eq!(
view.observe(&ExecutionContext::new(), &observe_request("/", 2))
.unwrap_err()
.code,
ProtocolErrorCode::ResourceLimitExceeded
);
assert!(
fixture.shared.seen.lock().unwrap().is_empty(),
"depth limit should precede resolution"
);
assert_eq!(
view.observe(&ExecutionContext::new(), &observe_request("/", 1))
.unwrap_err()
.code,
ProtocolErrorCode::ResourceLimitExceeded
);
assert!(
view.observe(&ExecutionContext::new(), &observe_request("/added", 1))
.unwrap()
.children
.unwrap()
.is_empty()
);
let fixture = Fixture::new();
let view = fixture.engine.view(&Principal::Limited);
let context = ExecutionContext::new().with_resource_budget(2);
assert_eq!(
view.observe(&context, &observe_request("/", 1))
.unwrap_err()
.code,
ProtocolErrorCode::ResourceLimitExceeded
);
let endpoint = Endpoint::parse("https://example.test/extensions").unwrap();
let request = observe_request("/", 1);
let response = HttpAdapter::new(&view, &endpoint, fixture.limits).handle_http_with_context(
encode_observe_request(&endpoint, &request, fixture.limits.wire).unwrap(),
&context,
);
wire_error(
decode_observe_response(&request, &response, fixture.limits.wire).unwrap(),
ProtocolErrorCode::ResourceLimitExceeded,
);
no_secrets(&response);
}
#[test]
fn root_scope_fetch_and_call_use_the_current_boundary_snapshot() {
let fixture = Fixture::new();
let view = fixture.engine.view(&Principal::Admin);
let fetched = view
.fetch_interface(&fetch_request(&fixture.custom))
.unwrap();
assert_eq!(fetched.scope_ref.as_deref(), Some("memory-root"));
assert!(
fixture
.observe(&Principal::Admin, "/", 0)
.object
.interfaces
.is_empty()
);
let mut request = fixture.request("/added", &fixture.custom, "denied", None);
request.interface.scope_ref = Some("wrong-root".into());
assert_eq!(
view.call_operation(request.clone()).unwrap_err().kind(),
wip_host::CallErrorKind::InterfaceMismatch
);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
// Ordinary state updates do not replace the publication lifetime or ref.
fixture.shared.node("/").data.lock().unwrap().revision += 1;
request.interface.scope_ref = fetched.scope_ref;
view.call_operation(request).unwrap();
// Fetch cannot attest a stale prelookup: visibility is refreshed under the
// same boundary even though resolve initially sees a visible root.
fixture.shared.controls.lock().unwrap().hide_before_execute = true;
assert_eq!(
view.fetch_interface(&fetch_request(&fixture.custom))
.unwrap_err()
.code,
ProtocolErrorCode::InterfaceNotFound
);
}
#[test]
fn root_scope_replacement_during_call_rejects_the_prelookup() {
let fixture = Fixture::new();
let view = fixture.engine.view(&Principal::Admin);
// Simulate a retained non-root lineage whose root is replaced before call.
fixture.shared.controls.lock().unwrap().replace_ancestor = Some("/".into());
let request = fixture.request("/added", &fixture.custom, "denied", None);
assert_eq!(
view.call_operation(request).unwrap_err().kind(),
wip_host::CallErrorKind::NotFound
);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
assert_eq!(
view.fetch_interface(&fetch_request(&fixture.custom))
.unwrap_err()
.code,
ProtocolErrorCode::InterfaceNotFound
);
let fixture = Fixture::new();
fixture.shared.controls.lock().unwrap().replace_ancestor = Some("/".into());
assert_eq!(
fixture
.engine
.view(&Principal::Admin)
.fetch_interface(&fetch_request(&fixture.custom))
.unwrap_err()
.code,
ProtocolErrorCode::InterfaceNotFound
);
}
#[path = "support/scoped.rs"]
mod scoped;
#[test]
fn nonroot_registrations_fail_closed_without_composition_lifetime_attestation() {
let fixture = Fixture::new();
let mut registry = InterfaceRegistry::new();
let root = Scope::new("/").unwrap();
let local = Scope::new("/added").unwrap();
let defaults = DefaultInterfaces::register(&mut registry, &root).unwrap();
let custom = registry
.register(
&local,
"memory.operations",
MemoryOperationsDefinition,
None,
)
.unwrap();
let reference = custom.reference().protocol_reference();
let policy = Policy {
custom: custom.reference().clone(),
read: defaults.read.reference().clone(),
hide_read: false,
invalid_selection: false,
panic_authorize: false,
};
let engine = Projection::new(
MemoryBackend::new(fixture.shared.clone()),
registry,
MemoryComposition {
defaults,
custom,
shared: fixture.shared.clone(),
},
policy,
fixture.limits,
);
let view = engine.view(&Principal::Admin);
let object = view.observe_object("/added").unwrap();
assert!(!object.interfaces.contains(&reference));
assert_eq!(
view.fetch_interface(&fetch_request(&reference))
.unwrap_err()
.code,
ProtocolErrorCode::InterfaceNotFound
);
let request = fixture.request("/added", &reference, "denied", None);
assert_eq!(
view.call_operation(request).unwrap_err().kind(),
wip_host::CallErrorKind::InterfaceMismatch
);
assert!(
fixture
.shared
.node("/added")
.calls
.lock()
.unwrap()
.is_empty()
);
}