feat: bind Host interfaces to scope Object lifetimes

Register explicit scope/name coordinates independently of Definition type. Require provider-attested current scope snapshots for fetch and dispatch, compare optional scope_ref at membership precedence, and retain the same Descriptor through result validation.

Migrate FS pinned-root boundaries and public self-scope example; document provider atomicity/lifecycle and root-only FS support. Add lifecycle, ref, replacement-race, root-lineage and compile-time exposure regressions.

Validation: cargo fmt --all --check; git diff --check; cargo check --workspace --all-targets; cargo test --workspace --all-targets and --doc (232 passing test-harness cases including doctests); nix develop --command cargo clippy --workspace --all-targets -- -D warnings; RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --no-deps. Narrow wip-host suite also passed.
This commit is contained in:
2026-10-08 03:18:13 +09:00
parent 8ecc307fc9
commit 755e6aeb88
15 changed files with 1585 additions and 248 deletions
+60 -25
View File
@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
use wip_host::{
CallError, CallErrorKind, CallRequest, CancellationToken, ExecutionContext, InterfaceRef,
InterfaceRegistry, InterfaceTarget, ObjectProvider, OperationAttempt, ProviderResult,
PublishedObject, RegisteredInterface, Target, Validator, WipObject,
PublishedObject, RegisteredInterface, Scope, ScopeObservation, Target, Validator, WipObject,
};
use wip_interface::{
CodecError, EffectGuarantee, HostError, OperationResult, TypeDeclaration, TypeExpr, Value,
@@ -17,9 +17,16 @@ use wip_protocol::ProtocolErrorCode;
#[test]
fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_tokens() {
let mut registry = InterfaceRegistry::new();
let read = registry.register(ReaderDefinition, None).unwrap();
let write = registry.register(WriterDefinition, None).unwrap();
let hidden = registry.register(HiddenDefinition, None).unwrap();
let root = Scope::new("/").unwrap();
let read = registry
.register(&root, "reader", ReaderDefinition, None)
.unwrap();
let write = registry
.register(&root, "writer::#日本語", WriterDefinition, None)
.unwrap();
let hidden = registry
.register(&root, "hidden", HiddenDefinition, None)
.unwrap();
let publication = || {
PublishedObject::new(Document::open("document", None, Rc::new(Cell::new(0))))
.expose(&read)
@@ -61,8 +68,9 @@ fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_to
.interfaces
.is_empty()
);
let current = ScopeObservation::new(&root, &Document::open("", None, Rc::new(Cell::new(0))));
let full = registry
.fetch_interface(&read.reference().protocol_reference())
.fetch_interface(&read.reference().protocol_reference(), &current)
.unwrap();
let filtered = full
.clone()
@@ -71,7 +79,7 @@ fn principal_projection_orders_subsets_and_rejects_unimplemented_or_duplicate_to
assert_eq!(full.descriptor.operations.len(), 3);
assert_eq!(
registry
.fetch_interface(&read.reference().protocol_reference())
.fetch_interface(&read.reference().protocol_reference(), &current)
.unwrap(),
full
);
@@ -309,6 +317,7 @@ impl Broken for Document {
}
struct TestProvider {
root: Scope,
reader: RegisteredInterface<ReaderDefinition>,
writer: RegisteredInterface<WriterDefinition>,
broken: RegisteredInterface<BrokenDefinition>,
@@ -326,11 +335,13 @@ struct TestProvider {
impl TestProvider {
fn new(
root: Scope,
reader: RegisteredInterface<ReaderDefinition>,
writer: RegisteredInterface<WriterDefinition>,
broken: RegisteredInterface<BrokenDefinition>,
) -> Self {
Self {
root,
reader,
writer,
broken,
@@ -362,7 +373,10 @@ impl ObjectProvider for TestProvider {
self.observed_cancelled
.set(context.cancellation().is_cancelled());
self.resolutions.set(self.resolutions.get() + 1);
let scope_object = Document::open("", None, Rc::clone(&self.calls));
let current_scope = ScopeObservation::new(&self.root, &scope_object);
let (document, interfaces) = match path {
"/" => (scope_object, 0),
"/document" => (
Document::strict(
"document",
@@ -420,6 +434,7 @@ impl ObjectProvider for TestProvider {
3 => publication.expose(&self.broken).unwrap(),
_ => unreachable!(),
};
let publication = publication.bind_scope(current_scope);
ProviderResult::Found(operation(&publication))
}
@@ -461,13 +476,25 @@ fn registry_and_provider() -> (
RegisteredInterface<HiddenDefinition>,
) {
let mut registry = InterfaceRegistry::new();
let root = Scope::new("/").unwrap();
let reader = registry
.register(ReaderDefinition, Some(Validator::new(b"schema-v1")))
.register(
&root,
"reader",
ReaderDefinition,
Some(Validator::new(b"schema-v1")),
)
.unwrap();
let writer = registry.register(WriterDefinition, None).unwrap();
let hidden = registry.register(HiddenDefinition, None).unwrap();
let broken = registry.register(BrokenDefinition, None).unwrap();
let provider = TestProvider::new(reader, writer, broken);
let writer = registry
.register(&root, "writer::#日本語", WriterDefinition, None)
.unwrap();
let hidden = registry
.register(&root, "hidden", HiddenDefinition, None)
.unwrap();
let broken = registry
.register(&root, "broken", BrokenDefinition, None)
.unwrap();
let provider = TestProvider::new(root, reader, writer, broken);
(registry, provider, hidden)
}
@@ -505,8 +532,8 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
let reference = provider.reader.reference();
assert_ne!(reference.as_str(), "Reader");
let observation = registry
.fetch_interface(&reference.protocol_reference())
let observation = provider
.fetch_interface(&registry, &reference.protocol_reference())
.unwrap();
assert_eq!(observation.reference, *reference);
assert_eq!(observation.descriptor, ReaderDefinition::descriptor());
@@ -518,8 +545,8 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
assert_eq!(response.validator.as_deref(), Some(b"schema-v1".as_slice()));
response.descriptor.validate().unwrap();
let mut invalid_observation = registry
.fetch_interface(&reference.protocol_reference())
let mut invalid_observation = provider
.fetch_interface(&registry, &reference.protocol_reference())
.unwrap();
let duplicate_operation = invalid_observation.descriptor.operations[0].clone();
invalid_observation
@@ -529,11 +556,14 @@ fn registry_returns_atomic_canonical_observation_without_rewriting_descriptor()
let invalid_response = invalid_observation.protocol_response().unwrap_err();
assert_eq!(invalid_response.code, ProtocolErrorCode::Internal);
let error = registry
.fetch_interface(&wip_protocol::InterfaceReference {
scope: "/".into(),
name: "unknown".into(),
})
let error = provider
.fetch_interface(
&registry,
&wip_protocol::InterfaceReference {
scope: "/".into(),
name: "unknown".into(),
},
)
.unwrap_err();
assert_eq!(error.code, ProtocolErrorCode::InterfaceNotFound);
}
@@ -1146,14 +1176,13 @@ fn execution_context_is_propagated_and_preflight_prevents_invocation() {
}
#[test]
fn legacy_registry_boundaries_copy_names_verbatim_and_match_exact_scope() {
fn registry_boundaries_preserve_names_verbatim_and_match_exact_scope() {
let (registry, provider, _) = registry_and_provider();
let legacy = provider.writer.reference();
let reference = legacy.protocol_reference();
assert_eq!(reference.scope, "/");
assert_eq!(reference.name, legacy.as_str());
// The registry's colon-delimited name remains opaque, not parsed.
assert!(reference.name.starts_with("wip-interface:"));
assert_eq!(reference.name, "writer::#日本語");
assert_eq!(registry.resolve(&reference).as_ref(), Some(legacy));
let nested = wip_protocol::InterfaceReference {
scope: "/open".into(),
@@ -1161,7 +1190,10 @@ fn legacy_registry_boundaries_copy_names_verbatim_and_match_exact_scope() {
};
assert!(registry.resolve(&nested).is_none());
assert_eq!(
registry.fetch_interface(&nested).unwrap_err().code,
provider
.fetch_interface(&registry, &nested)
.unwrap_err()
.code,
ProtocolErrorCode::InterfaceNotFound
);
let malformed = wip_protocol::InterfaceReference {
@@ -1169,7 +1201,10 @@ fn legacy_registry_boundaries_copy_names_verbatim_and_match_exact_scope() {
name: reference.name,
};
assert_eq!(
registry.fetch_interface(&malformed).unwrap_err().code,
provider
.fetch_interface(&registry, &malformed)
.unwrap_err()
.code,
ProtocolErrorCode::InvalidRequest
);
}