feat: migrate protocol and HTTP to structured interface references

T-20: identify interfaces by exact scope/name pairs; add optional opaque scope_ref metadata and separate shape validation from path-dependent scope compatibility. Preserve call error precedence, strict JSON, validators and transport boundaries. Mechanically migrate consumers and document API/wire compatibility and deferred lifecycle semantics.

Validation: cargo fmt --all --check; git diff --check; cargo check --workspace --all-targets; cargo test -p wip-protocol -p wip-http; cargo test --workspace (221 passing tests including doctests); cargo test --workspace --all-targets; nix develop --command cargo clippy --workspace --all-targets -- -D warnings; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps. All pass. cargo/rustc 1.95.0; development-shell Clippy/rustc 1.97.1.
This commit is contained in:
2026-10-07 18:29:51 +09:00
parent bf5fa54f44
commit 8ecc307fc9
34 changed files with 2124 additions and 263 deletions
+7 -5
View File
@@ -195,13 +195,15 @@ curl -sS -X POST http://127.0.0.1:8080/wip/v1/observe \
--data '{"path":"/","depth":2}'
```
A regular-file observation returns the opaque read interface reference and an object
validator. Retrieve that descriptor with:
A regular-file observation returns a structured read interface reference and an
object validator. Built-in legacy registrations use scope `/` and copy the opaque
registration string verbatim into `name`; they do not emit `scope_ref`. Copy the
complete reference from the observation to retrieve that descriptor:
```console
curl -sS -X POST http://127.0.0.1:8080/wip/v1/fetch_interface \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{"interface":"<read-interface-from-observation>"}'
--data '{"interface":{"scope":"/","name":"<read-interface-name-from-observation>"}}'
```
Then call a bounded range read:
@@ -211,7 +213,7 @@ curl -sS -X POST http://127.0.0.1:8080/wip/v1/call_operation \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{
"target":{"path":"/notes.txt","validator":"<base64-validator>"},
"interface":{"reference":"<read-interface>"},
"interface":{"reference":{"scope":"/","name":"<read-interface-name>"}},
"operation":"read",
"arguments":{"offset":0,"length":4096}
}'
@@ -225,7 +227,7 @@ curl -sS -X POST http://127.0.0.1:8080/wip/v1/call_operation \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
--data '{
"target":{"path":"/notes.txt","validator":"<base64-validator>"},
"interface":{"reference":"<write-interface>"},
"interface":{"reference":{"scope":"/","name":"<write-interface-name>"}},
"operation":"replace",
"arguments":{"contents":"dXBkYXRlZA=="}
}'
+2 -1
View File
@@ -827,7 +827,7 @@ mod tests {
projection: &DefaultProjection,
path: &str,
validator: Option<Vec<u8>>,
interface: String,
interface: wip_protocol::InterfaceReference,
operation: &str,
arguments: BTreeMap<String, Value>,
) -> Result<wip_protocol::CallOperationResponse, wip_host::CallError> {
@@ -837,6 +837,7 @@ mod tests {
validator,
},
interface: InterfaceTarget {
scope_ref: None,
reference: interface,
validator: None,
},
+4 -1
View File
@@ -98,6 +98,7 @@ fn real_listener_serves_three_core_interactions_and_write() {
validator: file.validator.clone(),
},
interface: InterfaceTarget {
scope_ref: None,
reference: file.interfaces[0].clone(),
validator: None,
},
@@ -141,6 +142,7 @@ fn real_listener_serves_three_core_interactions_and_write() {
validator: file.validator,
},
interface: InterfaceTarget {
scope_ref: None,
reference: file.interfaces[1].clone(),
validator: None,
},
@@ -412,7 +414,7 @@ fn call_argument_decoding_does_not_mask_missing_target() {
let config = ServerConfig::for_root(temporary.path());
let server = WipFsServer::new(config).expect("build server");
let endpoint = Endpoint::parse("http://localhost/wip").expect("endpoint");
let body = br#"{"target":{"path":"/missing"},"interface":{"reference":"unknown"},"operation":"missing","arguments":{"bytes":"not-base64"}}"#;
let body = br#"{"target":{"path":"/missing"},"interface":{"reference":{"scope":"/","name":"unknown"}},"operation":"missing","arguments":{"bytes":"not-base64"}}"#;
let request = http::Request::builder()
.method(http::Method::POST)
.uri(endpoint.route(Route::CallOperation).as_str())
@@ -490,6 +492,7 @@ fn read_response_size_failure_is_transport_local() {
validator: file.validator,
},
interface: InterfaceTarget {
scope_ref: None,
reference: file.interfaces[0].clone(),
validator: None,
},
+17 -12
View File
@@ -576,7 +576,7 @@ fn observation_and_fetch_policy_panics_remain_sanitized_read_failures() {
let mut registry = InterfaceRegistry::new();
let defaults = DefaultInterfaces::register(&mut registry).unwrap();
let custom = registry.register(MemoryOperationsDefinition, None).unwrap();
let reference = custom.reference().as_str().to_owned();
let reference = custom.reference().protocol_reference();
let engine = Projection::new(
MemoryBackend(fixture.shared.clone()),
registry,
@@ -616,8 +616,8 @@ type Engine = Projection<MemoryBackend, MemoryComposition, Policy>;
struct Fixture {
engine: Engine,
shared: Arc<Shared>,
custom: String,
read: String,
custom: wip_protocol::InterfaceReference,
read: wip_protocol::InterfaceReference,
limits: ResourceLimits,
}
impl Fixture {
@@ -696,8 +696,8 @@ impl Fixture {
limits,
),
shared,
custom: custom_ref.as_str().into(),
read: read_ref.as_str().into(),
custom: custom_ref.protocol_reference(),
read: read_ref.protocol_reference(),
limits,
}
}
@@ -707,7 +707,11 @@ impl Fixture {
.observe(&ExecutionContext::new(), &observe_request(path, depth))
.unwrap()
}
fn descriptor(&self, principal: &Principal, reference: &str) -> InterfaceDescriptor {
fn descriptor(
&self,
principal: &Principal,
reference: &wip_protocol::InterfaceReference,
) -> InterfaceDescriptor {
self.engine
.view(principal)
.fetch_interface(&fetch_request(reference))
@@ -717,7 +721,7 @@ impl Fixture {
fn request(
&self,
path: &str,
reference: &str,
reference: &wip_protocol::InterfaceReference,
operation: &str,
validator: Option<Vec<u8>>,
) -> CallOperationRequest {
@@ -727,7 +731,8 @@ impl Fixture {
validator,
},
interface: InterfaceTarget {
reference: reference.into(),
scope_ref: None,
reference: reference.clone(),
validator: None,
},
operation: operation.into(),
@@ -866,9 +871,9 @@ fn observe_request(path: &str, depth: u32) -> ObserveRequest {
depth,
}
}
fn fetch_request(reference: &str) -> FetchInterfaceRequest {
fn fetch_request(reference: &wip_protocol::InterfaceReference) -> FetchInterfaceRequest {
FetchInterfaceRequest {
interface: reference.into(),
interface: reference.clone(),
}
}
fn success<T: std::fmt::Debug>(response: DecodedResponse<T>) -> T {
@@ -1804,8 +1809,8 @@ fn policy_panics_and_guessed_operation_diagnostics_do_not_leak_to_http() {
// 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":"{}"}},"operation":"{}","arguments":{{}}}}"#,
fixture.custom, POLICY_SECRET
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)