diff --git a/README.md b/README.md index e28d86f..0cebbf9 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ Any document in the tree matching these origins (and allowed to use `tools` perm Once tools are registered, in-page agents can discover and invoke them using `getTools()` and `executeTool()`. -Calling `document.modelContext.getTools()` returns a promise that resolves with an array of `RegisteredTool` dictionary objects. Each object contains the tool's `name`, `description`, `inputSchema`, `origin`, and owner `window`. By default, `getTools()` only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the `fromOrigins` option. This array only supports secure origins. +Calling `document.modelContext.getTools()` returns a promise that resolves with an array of [`Tool`](https://webmachinelearning.github.io/webmcp/#tool) objects. Each `Tool` exposes the tool's `name`, `title`, `description`, `inputSchema`, `annotations`, `origin`, and owner `window`. By default, `getTools()` only returns tools registered by documents same-origin with the caller in the frame tree. To retrieve cross-origin tools, you must explicitly list their origins in the `fromOrigins` option. This array only supports secure origins. ```js // Discover tools exposed by same-origin frames in the tree (default) @@ -372,7 +372,7 @@ const crossOriginTools = await document.modelContext.getTools({ }); ``` -An agent executes a discovered `RegisteredTool` by passing the tool dictionary and input arguments along to `document.modelContext.executeTool()`. The browser securely mediates the execution, ensuring the [`exposedTo`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextregistertooloptions-exposedto) and [`fromOrigins`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextgettooloptions-fromorigins) agree, and the tool runs in the tool owner's execution context: +An agent executes a discovered `Tool` by passing it and input arguments along to `document.modelContext.executeTool()`. The browser securely mediates the execution, ensuring the [`exposedTo`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextregistertooloptions-exposedto) and [`fromOrigins`](https://webmachinelearning.github.io/webmcp/#dom-modelcontextgettooloptions-fromorigins) agree, and the tool runs in the tool owner's execution context. `Tool` objects can only be obtained from `getTools()`, and each one refers to the specific tool registration it was returned for: if that tool is unregistered, executing the `Tool` fails, even if a new tool is later registered under the same name. ```js const tools = await document.modelContext.getTools(); diff --git a/index.bs b/index.bs index bd81b6d..c0fe9fe 100644 --- a/index.bs +++ b/index.bs @@ -153,8 +153,8 @@ A model context is a [=struct=] with the following [=struct/items=]:
ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=].
-1. If |toolMap|[|toolName|] does not [=map/exist=], then run |completionSteps| given null and
+1. If |toolMap|[|toolId|] does not [=map/exist=], then run |completionSteps| given null and
[=tool execution error/tool not found=], and abort these steps.
This protects us against a race between tool unregistration and execution. While tool - existence is protected from this race, tool unregistration followed by a quick - re-registration of a tool with the same |toolName| but input schema is not protected - against.
+This protects against a race between tool unregistration and execution: the tool might have + been unregistered after {{ModelContext/executeTool()}} checked for its existence [=in + parallel=], but before these steps run.
-This might result in |inputArguments| for an old tool being applied to the [=tool - definition/input schema=] of a newer tool, and causing whatever error that might cause, when issue #92 is resolved.
+Because tools are looked up by |toolId| rather than by name, this also protects against the + tool being unregistered and quickly re-registered under the same name. The re-registered tool + has a different |toolId|, so a caller holding a {{Tool}} for the old tool fails to execute, + instead of silently executing the new tool with |inputArguments| intended for the old one.
Document=].
@@ -669,8 +682,8 @@ The {{ModelContext}} interface provides methods for web applications to register
[Exposed=Window, SecureContext]
interface ModelContext : EventTarget {
PromiseExecutes a tool on the document it was registered on. Returns a promise that resolves to the stringified result of the tool's execution.
-tool is a {{RegisteredTool}}, often retrieved by - {{ModelContext/getTools()}}. Callers pass a JavaScript object in as the +
tool is a {{Tool}}, retrieved by {{ModelContext/getTools()}}. It + identifies the exact tool registration that {{ModelContext/getTools()}} returned, so if that + tool has since been unregistered, execution fails, even if a different tool has since been + registered under the same name. Callers pass a JavaScript object in as the {{ModelContext/executeTool(tool)/inputObject}} argument for the tool; it's expected to match the - schema described by the tool's {{RegisteredTool/inputSchema}}. If the argument is omitted, the - tool receives an empty object.
+ schema described by the tool's {{Tool/inputSchema}}. If the argument is omitted, the tool + receives an empty object. @@ -745,7 +760,8 @@ The registerTool(tool, optionsregisterTool(tool, options. -1. If |tool map|[|tool name|] [=map/exists=], then return [=a promise rejected with=] an +1. If |tool map|'s [=map/values=] [=list/contains=] a [=tool definition=] whose [=tool + definition/name=] is |tool name|, then return [=a promise rejected with=] an {{InvalidStateError}} {{DOMException}}. Note: Similarly, script run while serializing |tool|'s {{ModelContextTool/inputSchema}} can @@ -810,6 +827,8 @@ The registerTool(tool, optionsregisterTool(tool, optionsregisterTool(tool, optionsgetTools(options) method stepsModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool
map=].
- 1. [=map/For each=] tool name → |tool definition| of |targetToolMap|:
+ 1. [=list/For each=] |tool definition| of |targetToolMap|'s [=map/values=]:
1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool
definition/exposed origins=], and |callerOrigin| returns false, then
@@ -941,41 +963,45 @@ The getTools(options) method steps
1. [=Queue a global task=] on the [=webmcp task source=] given |global| to run the following
steps:
- 1. Let |tools| be an empty [=list=] of {{RegisteredTool}} dictionaries.
+ 1. Let |tools| be an empty [=list=] of {{Tool}} objects.
1. [=list/For each=] (|tool definition|, |targetDocument|) of |discoveredTools|:
- 1. Let |registeredTool| be a new {{RegisteredTool}} dictionary, with the following fields:
+ 1. Let |newTool| be a [=new=] {{Tool}} object created in [=this=]'s [=relevant realm=],
+ whose members are set as follows:
+
+ : [=Tool/tool id=]
+ :: |tool definition|'s [=tool definition/tool id=]
- : {{RegisteredTool/name}}
+ : [=Tool/name=]
:: |tool definition|'s [=tool definition/name=]
- : {{RegisteredTool/title}}
+ : [=Tool/title=]
:: |tool definition|'s [=tool definition/title=] if it is non-null; otherwise the empty
string.
- Issue(#224): Consider not defaulting to the empty string, and just excluding this
- member, which will result in {{undefined}}.
+ Issue(#224): Consider not defaulting to the empty string.
- : {{RegisteredTool/description}}
+ : [=Tool/description=]
:: |tool definition|'s [=tool definition/description=]
- : {{RegisteredTool/inputSchema}}
+ : [=Tool/input schema=]
:: the result of [=parse a JSON string to a JavaScript value=] given |tool
definition|'s [=tool definition/input schema=], if |tool definition|'s
- [=tool definition/input schema=] is not the empty string; otherwise undefined.
+ [=tool definition/input schema=] is not the empty string; otherwise null.
Note: This will never throw an exception, because the string stored in the tool
definition is always a valid JSON string.
- : {{RegisteredTool/window}}
+ : [=Tool/window=]
:: |targetDocument|'s [=relevant global object=]
- : {{RegisteredTool/origin}}
- :: [=serialization of an origin|serialized=] |targetDocument|'s [=Document/origin=].
+ : [=Tool/origin=]
+ :: |targetDocument|'s [=Document/origin=]
- : {{RegisteredTool/annotations}}
- :: if |tool definition|'s [=tool definition/annotations=] is not null, a
+ : [=Tool/annotations=]
+ :: null if |tool definition|'s [=tool definition/annotations=] is null. Otherwise, the
+ result of [=converted to a JavaScript value|converting to a JavaScript value=] a
{{ToolAnnotations}} dictionary whose {{ToolAnnotations/readOnlyHint}} is |tool
definition|'s [=tool definition/annotations=]'s [=annotations/read-only hint=],
{{ToolAnnotations/untrustedContentHint}} is |tool definition|'s [=tool
@@ -985,10 +1011,10 @@ The getTools(options) method steps
{{ToolAnnotations/debugging}} is |tool definition|'s [=tool
definition/annotations=]'s [=annotations/debugging=].
- 1. [=list/Append=] |registeredTool| to |tools|.
+ 1. [=list/Append=] |newTool| to |tools|.
- 1. [=list/Sort in ascending order=] |tools|, with |a| being less than |b| if
- |a|["{{RegisteredTool/name}}"] is [=code unit less than=] |b|["{{RegisteredTool/name}}"].
+ 1. [=list/Sort in ascending order=] |tools|, with |a| being less than |b| if |a|'s [=Tool/name=]
+ is [=code unit less than=] |b|'s [=Tool/name=].
1. [=Resolve=] |promise| with |tools|.
@@ -1009,16 +1035,12 @@ The executeTool(tool, inputObject
1. If |callerDocument| is not [=allowed to use=] the "{{tools}}" feature, then return [=a promise
rejected with=] a "{{NotAllowedError}}" {{DOMException}}.
-1. Let |expectedTargetOriginURL| be the result of [=URL parser|parsing=] |tool|'s
- {{RegisteredTool/origin}}.
-
-1. If |expectedTargetOriginURL| is failure, or |expectedTargetOriginURL|'s [=url/origin=] is an
- [=opaque origin=], then return [=a promise rejected with=] a "{{NotSupportedError}}"
- {{DOMException}}.
+1. Let |expectedTargetOrigin| be |tool|'s [=Tool/origin=].
-1. Let |expectedTargetOrigin| be |expectedTargetOriginURL|'s [=url/origin=].
+1. If |expectedTargetOrigin| is an [=opaque origin=], then return [=a promise rejected with=] a
+ "{{NotSupportedError}}" {{DOMException}}.
-1. [=Assert=]: |expectedTargetOrigin| is not an [=opaque origin=].
+ Issue(#349): Allow the execution of tools registered by documents with an [=opaque origin=].
1. If |inputObject| was not given, then set |inputObject| to
[$OrdinaryObjectCreate$]({{%Object.prototype%}}).
@@ -1030,7 +1052,7 @@ The executeTool(tool, inputObject
1. Let |promise| be [=a new promise=] created in [=this=]'s [=relevant realm=].
-1. Let |targetWindow| be |tool|'s {{RegisteredTool/window}}.
+1. Let |targetWindow| be |tool|'s [=Tool/window=].
1. Let |targetDocument| be |targetWindow|'s [=associated Document|associated
Document=].
@@ -1078,24 +1100,31 @@ The executeTool(tool, inputObject
1. Let |targetToolMap| be |targetDocument|'s [=Document/associated ModelContext|associated
ModelContext=]'s [=ModelContext/internal context=]'s [=model context/tool map=].
- 1. Let |toolName| be |tool|'s {{RegisteredTool/name}}.
+ 1. Let |toolId| be |tool|'s [=Tool/tool id=].
- 1. If |targetToolMap|[|toolName|] does not [=map/exist=], then [=queue a global task=] on the
+ 1. If |targetToolMap|[|toolId|] does not [=map/exist=], then [=queue a global task=] on the
[=webmcp task source=] given |callerDocument|'s [=relevant global object=] to [=reject=]
|promise| with a "{{NotFoundError}}" {{DOMException}}, and abort these steps.
- 1. Let |tool definition| be |targetToolMap|[|toolName|].
+ 1. Let |tool definition| be |targetToolMap|[|toolId|].
+
+ 1. [=Assert=]: |tool definition|'s [=tool definition/name=] is |tool|'s [=Tool/name=].
1. If [=tool is exposed to an origin=] given |targetOrigin|, |tool definition|'s [=tool
definition/exposed origins=], and |callerOrigin| returns false, then [=queue a global task=]
on the [=webmcp task source=] given |callerDocument|'s [=relevant global object=] to
[=reject=] |promise| with a "{{NotFoundError}}" {{DOMException}}, and abort these steps.
- Note: Rejecting with "{{NotFoundError}}" across all three checks above—when |targetOrigin| does
- not match |expectedTargetOrigin|, when |toolName| does not exist, and when |tool definition|
- is not exposed to |callerOrigin|—prevents a cross-origin caller from using fabricated
- {{RegisteredTool}} dictionaries to probe |targetDocument|'s current origin or enumerate
- private tools not exposed to |callerOrigin|.
+ Note: Callers cannot fabricate {{Tool}} objects; they only get them from
+ {{ModelContext/getTools()}}. But the tools registered in |targetDocument| can change after
+ {{ModelContext/getTools()}} resolves: for example, the tool can be unregistered, or
+ unregistered and replaced by a new tool with the same name but a different [=tool
+ definition/tool id=].
+
+ Note: Rejecting with "{{NotFoundError}}" across all three checks above—when |targetOrigin|
+ does not match |expectedTargetOrigin|, when the tool identified by |toolId| does not exist,
+ and when |tool definition| is not exposed to |callerOrigin|—avoids revealing to the caller
+ which of these happened.
1. Let |completionSteps| be an algorithm that takes a [=string=]-or-null |result| and a
[=tool execution error=]-or-null |error|, and runs the following steps [=in parallel=]:
@@ -1139,7 +1168,7 @@ The executeTool(tool, inputObject
:: |targetDocument|
: [=pending tool execution/tool name=]
- :: |toolName|
+ :: |tool definition|'s [=tool definition/name=]
: [=pending tool execution/completion steps=]
:: |completionSteps|
@@ -1148,8 +1177,8 @@ The executeTool(tool, inputObject
[=traversable navigable/pending tool executions map=][|uuid|] to |execution|.
1. [=Queue a global task=] on the [=webmcp task source=] given |targetWindow| to run the [=tool
- execute steps=] given |toolName|, |targetDocument|, |inputArguments|, |completionSteps|,
- and |uuid|.
+ execute steps=] given |toolId|, |targetDocument|, |inputArguments|, |completionSteps|, and
+ |uuid|.
Note: Because documents only process tasks on their event loops when [=Document/fully
active=], if |targetDocument| is not [=Document/fully active=], this will simply queue the
@@ -1193,8 +1222,7 @@ callback ToolExecuteCallback = Promisetool["{{ModelContextTool/name}}"]A unique identifier for the tool. This is used by [=agents=] to reference the tool when - making tool calls. +
A name for the tool. This is used by [=agents=] to reference the tool when making tool calls.
tool["{{ModelContextTool/title}}"]tool["{{RegisteredTool/name}}"]
+ : tool.{{Tool/name}}
:: A unique identifier for the tool. It is the same value provided at tool registration, via
{{ModelContextTool/name}}.
- : tool["{{RegisteredTool/title}}"]
+ : tool.{{Tool/title}}
:: A human-readable label for the tool. It is the same value provided at tool registration, via
{{ModelContextTool/title}}.
- : tool["{{RegisteredTool/description}}"]
+ : tool.{{Tool/description}}
:: A natural language description of the tool's functionality. It is the same value provided at
tool registration, via {{ModelContextTool/description}}.
- : tool["{{RegisteredTool/inputSchema}}"]
+ : tool.{{Tool/inputSchema}}
:: A JSON Schema object describing the expected input parameters for the tool
- [[!JSON-SCHEMA]]. It is a deep copy of the schema provided at tool registration, via
- {{ModelContextTool/inputSchema}}.
-
- : tool["{{RegisteredTool/window}}"]
- :: The {{Window}} of the document that registered the tool.
-
- : tool["{{RegisteredTool/origin}}"]
- :: The origin of the document that registered the tool. This member is only meaningful when the
- tool is cross-origin, and the consumer of a tool cannot otherwise get the tool's origin from
- its {{RegisteredTool/window}}. For same-origin tools, this is the same as the tool's
- {{RegisteredTool/window}}'s {{WindowOrWorkerGlobalScope/origin}}, and the caller's own
+ [[!JSON-SCHEMA]], or null if the tool was registered without one. It is a deep copy of the
+ schema provided at tool registration, via {{ModelContextTool/inputSchema}}.
+
+ : tool.{{Tool/window}}
+ :: The {{Window}} of the document that registered the tool, or null if that document has since
+ been destroyed (e.g., because its <{iframe}> was removed).
+
+ : tool.{{Tool/origin}}
+ :: The origin of the document that registered the tool. This is only meaningful when the tool is
+ cross-origin, and the consumer of a tool cannot otherwise get the tool's origin from its
+ {{Tool/window}}. For same-origin tools, this is the same as the tool's {{Tool/window}}'s
+ {{WindowOrWorkerGlobalScope/origin}}, and the caller's own
{{Window}}.{{WindowOrWorkerGlobalScope/origin}}.
- : tool["{{RegisteredTool/annotations}}"]
- :: Optional annotations providing metadata about the tool. It matches
- {{ModelContextTool/annotations}}.
+ : tool.{{Tool/annotations}}
+ :: Annotations providing metadata about the tool, matching {{ModelContextTool/annotations}}, or
+ null if the tool was registered without any.