hilos-agent 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,281 @@
1
+ // Installed before the first page script by `hilos-agent webmcp`.
2
+ //
3
+ // A native browser implementation wins when it exists. The fallback implements
4
+ // the current imperative WebMCP surface closely enough for sites to register,
5
+ // list, execute, abort, and retire page-scoped tools. The privileged hilos
6
+ // wrapper deliberately exposes no DOM, cookies, storage, or arbitrary eval —
7
+ // only bounded tool metadata and execution results.
8
+ (() => {
9
+ "use strict";
10
+
11
+ const TOOL_NAME = /^[A-Za-z0-9_.-]{1,128}$/;
12
+ const MAX_TOOLS = 64;
13
+ const MAX_SCHEMA_CHARS = 32 * 1024;
14
+ const MAX_RESULT_CHARS = 64 * 1024;
15
+
16
+ const copyJson = (value) => {
17
+ if (value === undefined) return undefined;
18
+ return JSON.parse(JSON.stringify(value));
19
+ };
20
+
21
+ const domError = (message, name) => {
22
+ try {
23
+ return new DOMException(message, name);
24
+ } catch {
25
+ const error = new Error(message);
26
+ error.name = name;
27
+ return error;
28
+ }
29
+ };
30
+
31
+ function installFallbackModelContext() {
32
+ if (
33
+ document.modelContext &&
34
+ typeof document.modelContext.registerTool === "function" &&
35
+ typeof document.modelContext.getTools === "function" &&
36
+ typeof document.modelContext.executeTool === "function"
37
+ ) {
38
+ return document.modelContext;
39
+ }
40
+
41
+ const tools = new Map();
42
+ const events = new EventTarget();
43
+ let ontoolchange = null;
44
+
45
+ const notify = () => {
46
+ const event = new Event("toolchange");
47
+ events.dispatchEvent(event);
48
+ if (typeof ontoolchange === "function") {
49
+ try {
50
+ ontoolchange.call(context, event);
51
+ } catch {
52
+ // A page listener cannot break the browser mediator.
53
+ }
54
+ }
55
+ };
56
+
57
+ const context = {
58
+ registerTool(tool, options = {}) {
59
+ try {
60
+ if (!tool || typeof tool !== "object") {
61
+ throw new TypeError("WebMCP tool must be an object");
62
+ }
63
+ const name = String(tool.name || "");
64
+ const description = String(tool.description || "");
65
+ if (!TOOL_NAME.test(name)) {
66
+ throw new TypeError("WebMCP tool name is invalid");
67
+ }
68
+ if (!description) {
69
+ throw new TypeError("WebMCP tool description is required");
70
+ }
71
+ if (typeof tool.execute !== "function") {
72
+ throw new TypeError("WebMCP tool execute callback is required");
73
+ }
74
+ if (tools.has(name)) {
75
+ throw domError(`A WebMCP tool named ${name} is already registered`, "InvalidStateError");
76
+ }
77
+ if (options.signal?.aborted) {
78
+ throw options.signal.reason || domError("Registration aborted", "AbortError");
79
+ }
80
+
81
+ const inputSchema = copyJson(tool.inputSchema);
82
+ const schemaText = JSON.stringify(inputSchema ?? {});
83
+ if (schemaText.length > MAX_SCHEMA_CHARS) {
84
+ throw new TypeError("WebMCP input schema is too large");
85
+ }
86
+ const entry = {
87
+ name,
88
+ title: typeof tool.title === "string" ? tool.title : "",
89
+ description,
90
+ inputSchema,
91
+ annotations: tool.annotations
92
+ ? {
93
+ readOnlyHint: tool.annotations.readOnlyHint === true,
94
+ untrustedContentHint: tool.annotations.untrustedContentHint === true,
95
+ }
96
+ : undefined,
97
+ execute: tool.execute,
98
+ origin: location.origin,
99
+ };
100
+ tools.set(name, entry);
101
+ notify();
102
+
103
+ if (options.signal) {
104
+ options.signal.addEventListener(
105
+ "abort",
106
+ () => {
107
+ if (tools.get(name) !== entry) return;
108
+ tools.delete(name);
109
+ notify();
110
+ },
111
+ { once: true },
112
+ );
113
+ }
114
+ return Promise.resolve(undefined);
115
+ } catch (error) {
116
+ return Promise.reject(error);
117
+ }
118
+ },
119
+
120
+ async getTools() {
121
+ return [...tools.values()]
122
+ .sort((a, b) => a.name.localeCompare(b.name))
123
+ .map((tool) => ({
124
+ name: tool.name,
125
+ title: tool.title,
126
+ description: tool.description,
127
+ inputSchema: copyJson(tool.inputSchema),
128
+ window,
129
+ origin: tool.origin,
130
+ annotations: copyJson(tool.annotations),
131
+ }));
132
+ },
133
+
134
+ async executeTool(registeredTool, inputObject = {}, options = {}) {
135
+ if (!inputObject || typeof inputObject !== "object" || Array.isArray(inputObject)) {
136
+ throw new TypeError("WebMCP tool input must be an object");
137
+ }
138
+ if (options.signal?.aborted) {
139
+ throw options.signal.reason || domError("Execution aborted", "AbortError");
140
+ }
141
+ const name = String(registeredTool?.name || "");
142
+ const tool = tools.get(name);
143
+ if (!tool) throw domError(`WebMCP tool ${name} is not registered`, "NotFoundError");
144
+
145
+ const controller = new AbortController();
146
+ const onAbort = () => controller.abort(options.signal?.reason);
147
+ options.signal?.addEventListener("abort", onAbort, { once: true });
148
+ try {
149
+ const value = await tool.execute(copyJson(inputObject), { signal: controller.signal });
150
+ return JSON.stringify(value);
151
+ } finally {
152
+ options.signal?.removeEventListener("abort", onAbort);
153
+ }
154
+ },
155
+
156
+ addEventListener(...args) {
157
+ return events.addEventListener(...args);
158
+ },
159
+ removeEventListener(...args) {
160
+ return events.removeEventListener(...args);
161
+ },
162
+ dispatchEvent(...args) {
163
+ return events.dispatchEvent(...args);
164
+ },
165
+ get ontoolchange() {
166
+ return ontoolchange;
167
+ },
168
+ set ontoolchange(value) {
169
+ ontoolchange = typeof value === "function" ? value : null;
170
+ },
171
+ };
172
+
173
+ Object.defineProperty(document, "modelContext", {
174
+ configurable: false,
175
+ enumerable: true,
176
+ writable: false,
177
+ value: context,
178
+ });
179
+ return context;
180
+ }
181
+
182
+ const modelContext = installFallbackModelContext();
183
+
184
+ const source = () => ({
185
+ origin: location.origin,
186
+ pagePath: location.pathname || "/",
187
+ });
188
+
189
+ async function registeredTools() {
190
+ const tools = await modelContext.getTools();
191
+ return (Array.isArray(tools) ? tools : [])
192
+ .filter((tool) => tool && tool.origin === location.origin)
193
+ .slice(0, MAX_TOOLS);
194
+ }
195
+
196
+ const bridge = Object.freeze({
197
+ version: 1,
198
+ mode:
199
+ document.modelContext === modelContext &&
200
+ Object.prototype.hasOwnProperty.call(document, "modelContext")
201
+ ? "polyfill"
202
+ : "native",
203
+
204
+ async list() {
205
+ const tools = await registeredTools();
206
+ return {
207
+ ...source(),
208
+ tools: tools.map((tool) => {
209
+ let inputSchema = tool.inputSchema;
210
+ let schemaOmitted = false;
211
+ try {
212
+ // Chromium's first WebMCP implementation exposes RegisteredTool's
213
+ // schema as the draft's serialized JSON string; the current report
214
+ // exposes an object. Normalize both without passing site prose on.
215
+ if (typeof inputSchema === "string") inputSchema = JSON.parse(inputSchema);
216
+ if (JSON.stringify(inputSchema ?? {}).length > MAX_SCHEMA_CHARS) {
217
+ inputSchema = undefined;
218
+ schemaOmitted = true;
219
+ }
220
+ } catch {
221
+ inputSchema = undefined;
222
+ schemaOmitted = true;
223
+ }
224
+ return {
225
+ name: String(tool.name || "").slice(0, 128),
226
+ inputSchema: copyJson(inputSchema),
227
+ schemaOmitted,
228
+ annotations: tool.annotations
229
+ ? {
230
+ readOnlyHint: tool.annotations.readOnlyHint === true,
231
+ untrustedContentHint: tool.annotations.untrustedContentHint === true,
232
+ }
233
+ : undefined,
234
+ };
235
+ }),
236
+ };
237
+ },
238
+
239
+ async call(name, inputObject, timeoutMs = 30_000) {
240
+ const tools = await registeredTools();
241
+ const tool = tools.find((candidate) => candidate.name === name);
242
+ if (!tool) throw domError(`WebMCP tool ${name} is not registered`, "NotFoundError");
243
+
244
+ const controller = new AbortController();
245
+ const timer = setTimeout(
246
+ () => controller.abort(domError("WebMCP tool timed out", "TimeoutError")),
247
+ Math.max(1_000, Math.min(Number(timeoutMs) || 30_000, 60_000)),
248
+ );
249
+ try {
250
+ let result;
251
+ try {
252
+ result = await modelContext.executeTool(tool, inputObject, {
253
+ signal: controller.signal,
254
+ });
255
+ } catch (error) {
256
+ // Chromium's early draft expected a serialized input string. Only
257
+ // retry the pre-execution parse failure; an arbitrary tool failure
258
+ // might follow a side effect and must never be executed twice.
259
+ if (!/failed to parse input arguments/i.test(String(error?.message || error))) throw error;
260
+ result = await modelContext.executeTool(tool, JSON.stringify(inputObject), {
261
+ signal: controller.signal,
262
+ });
263
+ }
264
+ const resultJson = typeof result === "string" ? result : JSON.stringify(result);
265
+ if (resultJson.length > MAX_RESULT_CHARS) {
266
+ throw new RangeError("WebMCP tool result exceeds the 64 KB bridge limit");
267
+ }
268
+ return { ...source(), tool: name, resultJson };
269
+ } finally {
270
+ clearTimeout(timer);
271
+ }
272
+ },
273
+ });
274
+
275
+ Object.defineProperty(window, "__hilosWebMcpBridge", {
276
+ configurable: false,
277
+ enumerable: false,
278
+ writable: false,
279
+ value: bridge,
280
+ });
281
+ })();