fix: resolve target-qualified ancestor interfaces through live publications

Add a provider-owned routing hint without granting authority. Fetch checks provider ownership and re-resolves one coherent target/scope publication; default self/shared scope behavior remains unchanged.

Validation: 7 ancestor regression tests, 901 Worker unit and 110 integration tests, root cargo check, formatting and diff checks passed.
This commit is contained in:
2026-10-09 02:22:18 +09:00
parent f9d4c97f1e
commit 37a13e3566
2 changed files with 110 additions and 1 deletions
+29 -1
View File
@@ -288,6 +288,13 @@ pub trait WipSubtreeProvider: Send + Sync {
/// looking up a mutable ancestor after resolving the target. Self-scoped
/// publishers can use `WipPublication::self_scoped` on their pinned snapshot.
async fn publication(&self, path: &str) -> Result<Option<WipPublication>, ProtocolError>;
/// Routing hint for target-qualified local Interface names in an ancestor
/// scope. This grants no authority: Host re-resolves one coherent current
/// publication, checks the exact reference, and forbids escaping this owner.
/// Self/shared-scope providers retain the ordinary scope Object route.
fn interface_target(&self, reference: &InterfaceReference) -> Option<String> {
Some(reference.scope.clone())
}
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError>;
}
@@ -1248,8 +1255,29 @@ impl WipHost {
reference
.validate()
.map_err(|e| protocol_error(ProtocolErrorCode::InvalidRequest, e.to_string()))?;
let target = if let Some(mount) = self.subtree(&reference.scope) {
let target = mount.provider.interface_target(reference).ok_or_else(|| {
protocol_error(
ProtocolErrorCode::InterfaceNotFound,
"interface is not published",
)
})?;
if wip_protocol::validate_path(&target).is_err()
|| !self
.subtree(&target)
.is_some_and(|owner| owner.root == mount.root)
{
return Err(protocol_error(
ProtocolErrorCode::InterfaceNotFound,
"interface target is outside its provider",
));
}
target
} else {
reference.scope.clone()
};
if let Some(publication) = self
.publication_for_interface_live(&reference.scope, reference)
.publication_for_interface_live(&target, reference)
.await?
&& publication.projection.interface == *reference
&& let Some(scope) = publication.scope
+81
View File
@@ -538,3 +538,84 @@ async fn same_name_scope_replacement_or_loss_of_ref_rejects_old_metadata_without
);
}
}
struct TargetQualifiedProvider {
inner: Arc<AncestorProvider>,
target_hint: Mutex<Option<String>>,
}
#[async_trait]
impl WipSubtreeProvider for TargetQualifiedProvider {
fn interface_target(&self, _: &InterfaceReference) -> Option<String> {
self.target_hint.lock().unwrap().clone()
}
async fn publication(&self, path: &str) -> Result<Option<WipPublication>, ProtocolError> {
self.inner.publication(path).await
}
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError> {
self.inner.children(path).await
}
}
#[tokio::test]
async fn ancestor_interface_target_hint_fetches_one_current_publication_and_never_grants_authority()
{
let (_, inner) = fixture("/github", true, false);
let provider = Arc::new(TargetQualifiedProvider {
inner: inner.clone(),
target_hint: Mutex::new(Some(inner.target_path.clone())),
});
let mut mounts = WipMountRegistry::new();
mounts.allocate_namespace("scope", "github").unwrap();
mounts
.mount(projection(
"/github",
"/github",
"read",
inner.calls.clone(),
))
.unwrap();
mounts
.mount_subtree(WipSubtreeMount {
root: "/github".into(),
provider: provider.clone(),
})
.unwrap();
let host = WipHost::new(mounts);
let selected = reference("/github", "read");
let fetched = host.fetch_interface_live(&selected).await.unwrap();
assert_eq!(fetched.scope_ref, Some("live-scope".into()));
assert_eq!(
inner.state.lock().unwrap().lookups,
[inner.target_path.clone()]
);
// A hint must not fabricate a Descriptor or restore a deleted scope lifetime.
inner.state.lock().unwrap().visible = false;
assert_eq!(
host.fetch_interface_live(&selected).await.unwrap_err().code,
ProtocolErrorCode::InterfaceNotFound
);
inner.state.lock().unwrap().visible = true;
let forged = reference("/github", "not-published");
assert_eq!(
host.fetch_interface_live(&forged).await.unwrap_err().code,
ProtocolErrorCode::InterfaceNotFound
);
for hint in [
None,
Some("/outside"),
Some("/github-extra/item"),
Some("/github/../item"),
Some("/github/missing"),
] {
*provider.target_hint.lock().unwrap() = hint.map(str::to_owned);
inner.state.lock().unwrap().lookups.clear();
assert_eq!(
host.fetch_interface_live(&selected).await.unwrap_err().code,
ProtocolErrorCode::InterfaceNotFound
);
if hint != Some("/github/missing") {
assert!(inner.state.lock().unwrap().lookups.is_empty());
}
}
assert_eq!(inner.calls.load(Ordering::SeqCst), 0);
}