fix(worker): remove Wip prefix from gateway tool names (T-712)
Expose Discover, Inspect, and Call in WIP mode and update schema descriptions and current documentation. Preserve execution, argument schemas, internal types, and ordinary Tools mode. Add a registered-tool discovery/inspection/call regression. Validation: cargo test -p worker --lib wip::tests:: (43 passed); cargo test -p worker (827 unit + 110 integration passed); root cargo check; cargo fmt --all -- --check; git diff --check HEAD.
This commit is contained in:
@@ -2426,7 +2426,7 @@ fn one() -> u32 {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||||
struct WipInspectInput {
|
struct WipInspectInput {
|
||||||
/// Opaque interface reference returned by WipDiscover.
|
/// Opaque interface reference returned by Discover.
|
||||||
interface: String,
|
interface: String,
|
||||||
/// Explicitly supersede the cached descriptor observation.
|
/// Explicitly supersede the cached descriptor observation.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -2435,11 +2435,11 @@ struct WipInspectInput {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||||
struct WipCallInput {
|
struct WipCallInput {
|
||||||
/// Exact object path returned by WipDiscover.
|
/// Exact object path returned by Discover.
|
||||||
path: String,
|
path: String,
|
||||||
/// Exact interface reference selected from that object.
|
/// Exact interface reference selected from that object.
|
||||||
interface: String,
|
interface: String,
|
||||||
/// Exact operation name from WipInspect.
|
/// Exact operation name from Inspect.
|
||||||
operation: String,
|
operation: String,
|
||||||
/// Original operation arguments. Compatibility operations accept the original tool object here.
|
/// Original operation arguments. Compatibility operations accept the original tool object here.
|
||||||
arguments: Json,
|
arguments: Json,
|
||||||
@@ -2521,7 +2521,7 @@ fn wip_tool_definitions(runtime: Arc<WipRuntime>) -> Vec<ToolDefinition> {
|
|||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema = schemars::schema_for!(WipDiscoverInput);
|
let schema = schemars::schema_for!(WipDiscoverInput);
|
||||||
(
|
(
|
||||||
ToolMeta::new("WipDiscover")
|
ToolMeta::new("Discover")
|
||||||
.description("Explore the authorized WIP Worldspace and stateful Known Space. Start at `/`; use reset after reconnect or authority changes.")
|
.description("Explore the authorized WIP Worldspace and stateful Known Space. Start at `/`; use reset after reconnect or authority changes.")
|
||||||
.input_schema(serde_json::to_value(schema).expect("WIP discover schema serializes")),
|
.input_schema(serde_json::to_value(schema).expect("WIP discover schema serializes")),
|
||||||
Arc::new(WipDiscoverTool {
|
Arc::new(WipDiscoverTool {
|
||||||
@@ -2532,8 +2532,8 @@ fn wip_tool_definitions(runtime: Arc<WipRuntime>) -> Vec<ToolDefinition> {
|
|||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema = schemars::schema_for!(WipInspectInput);
|
let schema = schemars::schema_for!(WipInspectInput);
|
||||||
(
|
(
|
||||||
ToolMeta::new("WipInspect")
|
ToolMeta::new("Inspect")
|
||||||
.description("Fetch and validate one descriptor returned by WipDiscover. Inspect before calling an operation.")
|
.description("Fetch and validate one descriptor returned by Discover. Inspect before calling an operation.")
|
||||||
.input_schema(serde_json::to_value(schema).expect("WIP inspect schema serializes")),
|
.input_schema(serde_json::to_value(schema).expect("WIP inspect schema serializes")),
|
||||||
Arc::new(WipInspectTool {
|
Arc::new(WipInspectTool {
|
||||||
runtime: Arc::clone(&inspect_runtime),
|
runtime: Arc::clone(&inspect_runtime),
|
||||||
@@ -2543,7 +2543,7 @@ fn wip_tool_definitions(runtime: Arc<WipRuntime>) -> Vec<ToolDefinition> {
|
|||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema = schemars::schema_for!(WipCallInput);
|
let schema = schemars::schema_for!(WipCallInput);
|
||||||
(
|
(
|
||||||
ToolMeta::new("WipCall")
|
ToolMeta::new("Call")
|
||||||
.description("Invoke an operation only from fresh object/interface observations. Stale validators and invalid original JSON Schema inputs fail closed; unknown write outcomes must not be retried automatically.")
|
.description("Invoke an operation only from fresh object/interface observations. Stale validators and invalid original JSON Schema inputs fail closed; unknown write outcomes must not be retried automatically.")
|
||||||
.input_schema(serde_json::to_value(schema).expect("WIP call schema serializes")),
|
.input_schema(serde_json::to_value(schema).expect("WIP call schema serializes")),
|
||||||
Arc::new(WipCallTool {
|
Arc::new(WipCallTool {
|
||||||
@@ -3093,12 +3093,74 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|definition| definition.name)
|
.map(|definition| definition.name)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
["WipCall", "WipDiscover", "WipInspect"]
|
["Call", "Discover", "Inspect"]
|
||||||
);
|
);
|
||||||
let metrics = runtime.metrics();
|
let metrics = runtime.metrics();
|
||||||
assert!(metrics.ordinary_schema_bytes > metrics.wip_schema_bytes);
|
assert!(metrics.ordinary_schema_bytes > metrics.wip_schema_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn renamed_gateway_tools_discover_inspect_and_call_through_registered_surface() {
|
||||||
|
let calls = Arc::new(AtomicUsize::new(0));
|
||||||
|
let mut engine = Engine::<_, Mutable, ()>::new_annotated(DummyClient);
|
||||||
|
engine.register_tool(echo_definition("Echo".into(), Arc::clone(&calls)));
|
||||||
|
install_wip_mode(&mut engine, None, "worker-a".into()).unwrap();
|
||||||
|
let tools = engine.tool_server_handle();
|
||||||
|
tools.flush_pending();
|
||||||
|
|
||||||
|
let discovered = transport_output_json(
|
||||||
|
tools
|
||||||
|
.call_tool(
|
||||||
|
"Discover",
|
||||||
|
r#"{"path":"/","depth":2}"#,
|
||||||
|
ToolExecutionContext::direct(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let object = discovered["known_space"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|item| item["path"] == "/tools/Echo")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(object["state"], "fresh");
|
||||||
|
let interface = object["object"]["interfaces"][0].as_str().unwrap();
|
||||||
|
let inspected = transport_output_json(
|
||||||
|
tools
|
||||||
|
.call_tool(
|
||||||
|
"Inspect",
|
||||||
|
&json!({"interface": interface}).to_string(),
|
||||||
|
ToolExecutionContext::direct(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(inspected["interface"], interface);
|
||||||
|
assert_eq!(inspected["state"], "fresh");
|
||||||
|
assert_eq!(inspected["descriptor"]["operations"][0]["name"], "call");
|
||||||
|
|
||||||
|
let arguments = json!({"message": "hello"});
|
||||||
|
let output = tools
|
||||||
|
.call_tool(
|
||||||
|
"Call",
|
||||||
|
&json!({
|
||||||
|
"path": object["path"],
|
||||||
|
"interface": interface,
|
||||||
|
"operation": "call",
|
||||||
|
"arguments": arguments,
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
ToolExecutionContext::direct(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(output.summary, arguments.to_string());
|
||||||
|
assert!(output.content.is_none());
|
||||||
|
assert!(output.attachments.is_empty());
|
||||||
|
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mount_collision_and_native_replacement_are_explicit() {
|
fn mount_collision_and_native_replacement_are_explicit() {
|
||||||
struct Native;
|
struct Native;
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ Ticket/Objective/Merge Request routes retain their T-698 contracts.
|
|||||||
|
|
||||||
## First-Workspace example
|
## First-Workspace example
|
||||||
|
|
||||||
1. `WipDiscover(path="/", depth=1)` finds `repositories`, `workdirs` and
|
1. `Discover(path="/", depth=1)` finds `repositories`, `workdirs` and
|
||||||
`workdir-attachments` when permitted.
|
`workdir-attachments` when permitted.
|
||||||
2. Inspect the returned Repository collection Interface and call `list` with a
|
2. Inspect the returned Repository collection Interface and call `list` with a
|
||||||
bounded limit. An empty registered/visible catalog explicitly returns an empty
|
bounded limit. An empty registered/visible catalog explicitly returns an empty
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ The implementation uses the crates.io `wip-client`, `wip-protocol`, and `wip-htt
|
|||||||
|
|
||||||
A WIP Worker exposes exactly these generic LLM tools instead of exposing every enabled ordinary tool twice:
|
A WIP Worker exposes exactly these generic LLM tools instead of exposing every enabled ordinary tool twice:
|
||||||
|
|
||||||
- `WipDiscover`: observe an object path and bounded descendants, beginning at `/`.
|
- `Discover`: observe an object path and bounded descendants, beginning at `/`.
|
||||||
- `WipInspect`: fetch the descriptor for an opaque interface reference returned by discovery.
|
- `Inspect`: fetch the descriptor for an opaque interface reference returned by discovery.
|
||||||
- `WipCall`: call an operation using fresh object and interface observations.
|
- `Call`: call an operation using fresh object and interface observations.
|
||||||
|
|
||||||
The compatibility projection mounts enabled ordinary tools at:
|
The compatibility projection mounts enabled ordinary tools at:
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ Each object has one opaque interface reference:
|
|||||||
yoi.tool/<exact-tool-registration-name>/v1
|
yoi.tool/<exact-tool-registration-name>/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
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. Native input is decoded with the published `wip-http` descriptor-bound codec, so named and composite types are resolved, an integral JSON number remains a WIP `Number` when declared as such, and WIP `Bytes` use the codec's canonical RFC 4648 base64 JSON representation. 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, `Call.arguments` is the original ordinary tool argument object. Native projections instead receive a JSON object keyed by the descriptor's declared parameter names. Native input is decoded with the published `wip-http` descriptor-bound codec, so named and composite types are resolved, an integral JSON number remains a WIP `Number` when declared as such, and WIP `Bytes` use the codec's canonical RFC 4648 base64 JSON representation. 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.
|
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.
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ Object resolution and operations are separate contracts. One mounted `WipProject
|
|||||||
- A native projection replaces a compatibility projection either by the original same-route semantic capability or by an explicit registry capability claim owned by a mounted native route.
|
- A native projection replaces a compatibility projection either by the original same-route semantic capability or by an explicit registry capability claim owned by a mounted native route.
|
||||||
- Conflicting native claims fail startup. When native is selected, the claimed compatibility tool is hidden rather than exposed through a second entry. Normal Tool mode is unaffected because it does not install the WIP registry.
|
- Conflicting native claims fail startup. When native is selected, the claimed compatibility tool is hidden rather than exposed through a second entry. Normal Tool mode is unaffected because it does not install the WIP registry.
|
||||||
|
|
||||||
`WipInspect` renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation.
|
`Inspect` renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation.
|
||||||
|
|
||||||
The built-in native route migration is:
|
The built-in native route migration is:
|
||||||
|
|
||||||
@@ -103,13 +103,13 @@ The Worldspace contains only the operations registered from enabled Features and
|
|||||||
|
|
||||||
Filesystem scope, provider Workdir capability checks, Backend Workspace authority, Feature enablement, and input-specific restrictions stay in the original tool implementation and are checked again at invocation. `ask` permissions remain denied fail-closed because the runtime has no approval protocol. The gateway does not mint authority or expose authentication material.
|
Filesystem scope, provider Workdir capability checks, Backend Workspace authority, Feature enablement, and input-specific restrictions stay in the original tool implementation and are checked again at invocation. `ask` permissions remain denied fail-closed because the runtime has no approval protocol. The gateway does not mint authority or expose authentication material.
|
||||||
|
|
||||||
Pre/post Engine history still records one bounded `WipCall` result, while the compatibility result retains the original `ToolOutput`, including attachments and normal Engine output pruning. WIP call audit records retain the route, operation, request identity, and terminal classification.
|
Pre/post Engine history still records one bounded `Call` result, while the compatibility result retains the original `ToolOutput`, including attachments and normal Engine output pruning. WIP call audit records retain the route, operation, request identity, and terminal classification.
|
||||||
|
|
||||||
## Client lifetime, restoration, and compaction
|
## Client lifetime, restoration, and compaction
|
||||||
|
|
||||||
The stateful `wip-client::Client` is owned by one Worker WIP runtime. Sessions are keyed by the canonical endpoint and an opaque security-context identity derived by the Host from Workspace, Worker, and session identity. Authentication credentials are not included in that identity and are never emitted to descriptors, tool output, or history.
|
The stateful `wip-client::Client` is owned by one Worker WIP runtime. Sessions are keyed by the canonical endpoint and an opaque security-context identity derived by the Host from Workspace, Worker, and session identity. Authentication credentials are not included in that identity and are never emitted to descriptors, tool output, or history.
|
||||||
|
|
||||||
Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `WipDiscover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority.
|
Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `Discover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority.
|
||||||
|
|
||||||
Saved Session/history entries remain append-only evidence and are never rewritten or replayed as calls. In particular, historical `/features/<feature>/...` text stays displayable but is not a current route alias. Passing such a legacy path to discovery or call returns `NotFound` with guidance to rediscover from `/`; the Host does not translate it, resolve it under another namespace, or expose old and new routes in parallel.
|
Saved Session/history entries remain append-only evidence and are never rewritten or replayed as calls. In particular, historical `/features/<feature>/...` text stays displayable but is not a current route alias. Passing such a legacy path to discovery or call returns `NotFound` with guidance to rediscover from `/`; the Host does not translate it, resolve it under another namespace, or expose old and new routes in parallel.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user