fix(worker): forward native WIP arguments by descriptor name

This commit is contained in:
2026-10-04 00:31:49 +09:00
parent 4a05aad508
commit e44637519e
2 changed files with 126 additions and 6 deletions
+124 -4
View File
@@ -872,10 +872,38 @@ impl WipRuntime {
arguments: Json,
execution: ToolExecutionContext,
) -> Result<ToolOutput, ToolError> {
let arguments = BTreeMap::from([(
"input".to_string(),
json_to_wip(&arguments).map_err(ToolError::InvalidArgument)?,
)]);
let arguments = match self
.host
.projection(&path)
.map(|projection| projection.kind)
{
Some(WipProjectionKind::Compatibility) => BTreeMap::from([(
"input".to_string(),
json_to_wip(&arguments).map_err(ToolError::InvalidArgument)?,
)]),
Some(WipProjectionKind::Native) => {
let Json::Object(arguments) = &arguments else {
return Err(ToolError::InvalidArgument(
"native WIP operation arguments must be a JSON object keyed by descriptor parameter name"
.into(),
));
};
arguments
.iter()
.map(|(name, value)| {
Ok((
name.clone(),
json_to_wip(value).map_err(ToolError::InvalidArgument)?,
))
})
.collect::<Result<BTreeMap<_, _>, ToolError>>()?
}
None => {
return Err(ToolError::InvalidArgument(format!(
"WIP target `{path}` is not published"
)));
}
};
let prepared = {
let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
let session = state.session.clone();
@@ -1784,6 +1812,98 @@ mod tests {
));
}
struct NativeConcat;
#[async_trait]
impl WipOperationHandler for NativeConcat {
async fn call(
&self,
operation: &str,
arguments: &BTreeMap<String, Value>,
_context: WipCallContext,
) -> Result<WipOperationOutput, WipOperationError> {
assert_eq!(operation, "concat");
let (Some(Value::String(left)), Some(Value::String(right))) =
(arguments.get("left"), arguments.get("right"))
else {
panic!("descriptor validation must run before native handler")
};
Ok(WipOperationOutput::native(Value::String(format!(
"{left}{right}"
))))
}
}
fn native_concat_projection() -> WipProjection {
WipProjection {
route: "/native/concat".into(),
capability: "native:concat".into(),
kind: WipProjectionKind::Native,
object: Object {
name: "concat".into(),
description: Some("native concat sample".into()),
interfaces: vec!["yoi.native/concat/v1".into()],
r#ref: Some("native:concat".into()),
validator: Some(vec![1]),
},
interface: "yoi.native/concat/v1".into(),
descriptor: InterfaceDescriptor {
format: INTERFACE_FORMAT_V1.into(),
documentation: None,
types: Vec::new(),
operations: vec![OperationDeclaration {
name: "concat".into(),
documentation: None,
parameters: vec![
ParameterDeclaration {
name: "left".into(),
required: true,
documentation: None,
r#type: TypeExpr::String,
},
ParameterDeclaration {
name: "right".into(),
required: true,
documentation: None,
r#type: TypeExpr::String,
},
],
returns: ReturnDeclaration {
documentation: None,
r#type: TypeExpr::String,
},
}],
},
interface_validator: Some(vec![1]),
handler: Arc::new(NativeConcat),
}
}
#[tokio::test]
async fn native_projection_accepts_descriptor_named_arguments_end_to_end() {
let mut registry = WipMountRegistry::new();
registry.mount(native_concat_projection()).unwrap();
let runtime =
WipRuntime::new(WipHost::new(registry), SecurityContext::new("worker-a"), 0).unwrap();
runtime.discover("/".into(), 2, false).await.unwrap();
runtime
.inspect("yoi.native/concat/v1".into(), false)
.await
.unwrap();
let output = runtime
.call(
"/native/concat".into(),
"yoi.native/concat/v1".into(),
"concat".into(),
json!({"left": "W", "right": "IP"}),
ToolExecutionContext::direct(),
)
.await
.unwrap();
assert_eq!(output.summary, "WIP operation completed");
assert_eq!(output.content.as_deref(), Some("\"WIP\""));
}
#[tokio::test]
async fn stateful_discover_inspect_call_round_trip_validates_original_schema() {
let calls = Arc::new(AtomicUsize::new(0));
+2 -2
View File
@@ -46,7 +46,7 @@ Each object has one opaque interface reference:
yoi.tool/<exact-tool-registration-name>/v1
```
Its single operation is `call`. `WipCall.arguments` is the original ordinary tool argument object. Tool argument IDs remain JSON values; the adapter never infers a domain object or Worldspace route from them.
Its single operation is `call`. For this compatibility route, `WipCall.arguments` is the original ordinary tool argument object. Native projections instead receive a JSON object keyed by the descriptor's declared parameter names. Tool argument IDs remain JSON values; the adapter never infers a domain object or Worldspace route from them.
The interface descriptor uses WIP `Json` for compatibility input and includes the exact original JSON Schema in descriptor documentation. Before execution, the Host validates the JSON value with that original schema and then delegates to the original async `Tool`. Constraints are therefore neither approximated nor silently dropped. A schema that cannot be compiled prevents WIP startup instead of creating a weaker projection.
@@ -61,7 +61,7 @@ The interface descriptor uses WIP `Json` for compatibility input and includes th
- A native projection replaces a compatibility projection only when both declare the same semantic capability (`tool:<registration-name>` for compatibility tools).
- When native is already selected, the compatibility tool is hidden rather than exposed through a second entry.
The unit sample `mount_collision_and_native_replacement_are_explicit` demonstrates independent native registration, deterministic replacement, and collision rejection.
The unit samples `mount_collision_and_native_replacement_are_explicit` and `native_projection_accepts_descriptor_named_arguments_end_to_end` demonstrate independent native registration, deterministic replacement, collision rejection, and a complete native invocation.
## Authority and execution