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
+62 -17
View File
@@ -16,7 +16,11 @@
//!
//! The canonical API targets operations by `target.path` rather than a mandatory
//! object identifier. Object `ref` and object/interface validators are separate,
//! optional observation fields. Interface references are structured scope/name values observed
//! optional observation fields. This demo registers explicit `catalog`, `audit`
//! and `diagnostics` names at the service Object self-scope. A private [`Scope`]
//! lifetime and coherent [`ScopeObservation`] bind fetch and call to that Object;
//! successful calls carry its optional `scope_ref` identity precondition.
//! Interface references are structured scope/name values observed
//! separately from descriptors, and descriptors contain neither a Rust trait
//! name nor an opaque-reference `name` field. Application records, enums,
//! unions, entry paths, and JSON all encode through the schema-neutral [`Value`]
@@ -41,8 +45,8 @@ use std::io;
use wip_host::{
CallErrorKind, CallRequest, ExecutionContext, InterfaceRef, InterfaceRegistry, InterfaceTarget,
ObjectProvider, ProviderResult, PublishedObject, RegisteredInterface, Target, Validator,
WipObject,
ObjectProvider, ProviderResult, PublishedObject, RegisteredInterface, Scope, ScopeObservation,
Target, Validator, WipObject,
};
use wip_interface as wip;
use wip_interface::WipType;
@@ -136,14 +140,20 @@ pub struct DemoReport {
pub object_validator: Option<Vec<u8>>,
/// Structured interface references published by the object, in exposure order.
pub published_interfaces: Vec<InterfaceReference>,
/// Root-scoped reference assigned to the catalog descriptor.
/// Self-scoped reference assigned to the catalog descriptor.
pub catalog_interface_reference: InterfaceReference,
/// Root-scoped reference assigned to the audit descriptor.
/// Self-scoped reference assigned to the audit descriptor.
pub audit_interface_reference: InterfaceReference,
/// Descriptor format observed separately for the catalog reference.
pub catalog_descriptor_format: String,
/// Descriptor format observed separately for the audit reference.
pub audit_descriptor_format: String,
/// Scope Object ref observed together with the catalog Descriptor.
pub catalog_scope_ref: Option<String>,
/// Scope Object ref observed together with the audit Descriptor.
pub audit_scope_ref: Option<String>,
/// Membership rejection when the supplied scope Object ref is stale.
pub scope_mismatch: ProtocolErrorCode,
/// Typed successful application result decoded from a validated [`Value`].
pub published: PublishOutcome,
/// Typed application-domain rejection decoded from a validated [`Value`].
@@ -182,25 +192,34 @@ pub struct DemoReport {
/// application-value decoding fails unexpectedly.
pub fn run() -> Result<DemoReport, Box<dyn Error>> {
let mut registry = InterfaceRegistry::new();
let catalog = registry.register(CatalogDefinition, Some(Validator::new(CATALOG_VALIDATOR)))?;
let audit = registry.register(AuditDefinition, None)?;
let diagnostics = registry.register(DiagnosticsDefinition, None)?;
let scope = Scope::new(OBJECT_PATH)?;
let catalog = registry.register(
&scope,
"catalog",
CatalogDefinition,
Some(Validator::new(CATALOG_VALIDATOR)),
)?;
let audit = registry.register(&scope, "audit", AuditDefinition, None)?;
let diagnostics = registry.register(&scope, "diagnostics", DiagnosticsDefinition, None)?;
let provider = ApplicationProvider {
scope,
catalog: catalog.clone(),
audit: audit.clone(),
};
let object = provider.observe_object(OBJECT_PATH)?;
let catalog_response = registry
let catalog_response = provider
.fetch_interface(
&registry,
object
.interfaces
.first()
.ok_or_else(|| io::Error::other("catalog interface was not published"))?,
)?
.protocol_response()?;
let audit_response = registry
let audit_response = provider
.fetch_interface(
&registry,
object
.interfaces
.get(1)
@@ -319,14 +338,31 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
.protocol_error()
.code;
let interface_not_found = registry
.fetch_interface(&InterfaceReference {
scope: "/".into(),
name: "missing-interface".into(),
})
let interface_not_found = provider
.fetch_interface(
&registry,
&InterfaceReference {
scope: OBJECT_PATH.into(),
name: "missing-interface".into(),
},
)
.expect_err("unknown interface references must not resolve")
.code;
let mut stale_scope = request(
OBJECT_PATH,
catalog.reference(),
Some(CATALOG_VALIDATOR),
"status",
BTreeMap::new(),
);
stale_scope.interface.scope_ref = Some("replaced-service-instance".into());
let scope_mismatch = provider
.call_operation(stale_scope)
.expect_err("stale scope identity must reject before operation dispatch")
.protocol_error()
.code;
let precedence_failure = provider
.call_operation(CallRequest {
target: Target {
@@ -354,6 +390,9 @@ pub fn run() -> Result<DemoReport, Box<dyn Error>> {
audit_interface_reference: audit.reference().protocol_reference(),
catalog_descriptor_format: catalog_response.descriptor.format,
audit_descriptor_format: audit_response.descriptor.format,
catalog_scope_ref: catalog_response.scope_ref,
audit_scope_ref: audit_response.scope_ref,
scope_mismatch,
published,
rejected,
catalog_status,
@@ -417,7 +456,7 @@ fn request(
validator: Some(OBJECT_VALIDATOR.to_vec()),
},
interface: InterfaceTarget {
scope_ref: None,
scope_ref: Some("article-service-instance".into()),
reference: interface.protocol_reference(),
validator: interface_validator.map(<[u8]>::to_vec),
},
@@ -427,6 +466,7 @@ fn request(
}
struct ApplicationProvider {
scope: Scope,
catalog: RegisteredInterface<CatalogDefinition>,
audit: RegisteredInterface<AuditDefinition>,
}
@@ -443,7 +483,12 @@ impl ObjectProvider for ApplicationProvider {
if path != OBJECT_PATH {
return ProviderResult::NotFound;
}
let publication = PublishedObject::new(ApplicationService::new())
// The immutable demo has no mutable backend actors. A real backend
// must retain its transaction/lock across association and operation.
let service = ApplicationService::new();
let current = ScopeObservation::new(&self.scope, &service);
let publication = PublishedObject::new(service)
.bind_scope(current)
.expose(&self.catalog)
.expect("ApplicationService implements Catalog")
.expose(&self.audit)
@@ -28,10 +28,20 @@ fn public_crates_support_the_complete_qualified_end_to_end_scenario() {
);
assert_ne!(
report.catalog_interface_reference, report.audit_interface_reference,
"interfaces form an ordered set of distinct opaque references"
"interfaces form an ordered set of distinct scope/name references"
);
assert_eq!(report.catalog_interface_reference.scope, "/article-service");
assert_eq!(report.catalog_interface_reference.name, "catalog");
assert_eq!(report.audit_interface_reference.scope, "/article-service");
assert_eq!(report.audit_interface_reference.name, "audit");
assert_eq!(report.catalog_descriptor_format, INTERFACE_FORMAT_V1);
assert_eq!(report.audit_descriptor_format, INTERFACE_FORMAT_V1);
assert_eq!(
report.catalog_scope_ref.as_deref(),
Some("article-service-instance")
);
assert_eq!(report.audit_scope_ref, report.catalog_scope_ref);
assert_eq!(report.scope_mismatch, ProtocolErrorCode::InterfaceMismatch);
assert_eq!(
report.published,
PublishOutcome::Published("article-1".into())