1use serde_json::{Value, json};
23
24pub(crate) struct ToolSpec {
27 pub(crate) name: &'static str,
28 pub(crate) description: &'static str,
29 pub(crate) schema: Value,
30}
31
32pub(crate) fn specs() -> Vec<ToolSpec> {
38 let specs = vec![
39 ToolSpec {
40 name: "browser_close",
41 description: "Close the calling session's current tab",
42 schema: json!({
43 "$schema": "https://json-schema.org/draft/2020-12/schema",
44 "type": "object",
45 "properties": {},
46 "additionalProperties": false
47 }),
48 },
49 ToolSpec {
50 name: "browser_resize",
51 description: "Resize the browser window",
52 schema: json!({
53 "$schema": "https://json-schema.org/draft/2020-12/schema",
54 "type": "object",
55 "properties": {
56 "width": {
57 "type": "number",
58 "description": "Width of the browser window"
59 },
60 "height": {
61 "type": "number",
62 "description": "Height of the browser window"
63 }
64 },
65 "required": ["width", "height"],
66 "additionalProperties": false
67 }),
68 },
69 ToolSpec {
70 name: "browser_console_messages",
71 description: "Returns all console messages",
72 schema: json!({
73 "$schema": "https://json-schema.org/draft/2020-12/schema",
74 "type": "object",
75 "properties": {
76 "level": {
77 "default": "info",
78 "description": "Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\".",
79 "type": "string",
80 "enum": ["error", "warning", "info", "debug"]
81 },
82 "all": {
83 "description": "Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false.",
84 "type": "boolean"
85 },
86 "filename": {
87 "description": "File name to save the console messages to. Relative file names are resolved against the workspace root. If not provided, messages are returned as text.",
88 "type": "string"
89 }
90 },
91 "required": ["level"],
92 "additionalProperties": false
93 }),
94 },
95 ToolSpec {
96 name: "browser_handle_dialog",
97 description: "Handle a dialog. A page's dialog is held until it is answered, and every call that would drive that tab is refused until then; this is the way to answer or dismiss it.",
98 schema: json!({
99 "$schema": "https://json-schema.org/draft/2020-12/schema",
100 "type": "object",
101 "properties": {
102 "accept": {
103 "type": "boolean",
104 "description": "Whether to accept the dialog."
105 },
106 "promptText": {
107 "description": "The text of the prompt in case of a prompt dialog.",
108 "type": "string"
109 }
110 },
111 "required": ["accept"],
112 "additionalProperties": false
113 }),
114 },
115 ToolSpec {
116 name: "browser_emulate_media",
117 description: "Emulate CSS media features for the page, for example switch between the light and dark color scheme. Omitted parameters are left unchanged; null clears an override.",
118 schema: json!({
119 "$schema": "https://json-schema.org/draft/2020-12/schema",
120 "type": "object",
121 "properties": {
122 "colorScheme": {
123 "description": "Emulates the prefers-color-scheme media feature",
124 "anyOf": [
125 { "type": "string", "enum": ["light", "dark"] },
126 { "type": "null" }
127 ]
128 },
129 "reducedMotion": {
130 "description": "Emulates the prefers-reduced-motion media feature",
131 "anyOf": [
132 { "type": "string", "enum": ["reduce", "no-preference"] },
133 { "type": "null" }
134 ]
135 },
136 "forcedColors": {
137 "description": "Emulates the forced-colors media feature",
138 "anyOf": [
139 { "type": "string", "enum": ["active", "none"] },
140 { "type": "null" }
141 ]
142 },
143 "contrast": {
144 "description": "Emulates the prefers-contrast media feature",
145 "anyOf": [
146 { "type": "string", "enum": ["more", "no-preference"] },
147 { "type": "null" }
148 ]
149 },
150 "media": {
151 "description": "Changes the CSS media type of the page",
152 "anyOf": [
153 { "type": "string", "enum": ["screen", "print"] },
154 { "type": "null" }
155 ]
156 }
157 },
158 "additionalProperties": false
159 }),
160 },
161 ToolSpec {
162 name: "browser_evaluate",
163 description: "Evaluate JavaScript expression on page or element",
164 schema: json!({
165 "$schema": "https://json-schema.org/draft/2020-12/schema",
166 "type": "object",
167 "properties": {
168 "element": {
169 "description": "Human-readable element description used to obtain permission to interact with the element",
170 "type": "string"
171 },
172 "target": {
173 "description": "Exact target element reference from the page snapshot, or a unique element selector",
174 "type": "string"
175 },
176 "function": {
177 "type": "string",
178 "description": "() => { /* code */ } or (element) => { /* code */ } when element is provided"
179 },
180 "filename": {
181 "description": "File name to save the result to. Relative file names are resolved against the workspace root. If not provided, result is returned as text.",
182 "type": "string"
183 }
184 },
185 "required": ["function"],
186 "additionalProperties": false
187 }),
188 },
189 ToolSpec {
190 name: "browser_file_upload",
191 description: "Upload one or multiple files",
192 schema: json!({
193 "$schema": "https://json-schema.org/draft/2020-12/schema",
194 "type": "object",
195 "properties": {
196 "paths": {
197 "description": "The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.",
198 "type": "array",
199 "items": { "type": "string" }
200 }
201 },
202 "additionalProperties": false
203 }),
204 },
205 ToolSpec {
206 name: "browser_drop",
207 description: "Drop files or MIME-typed data onto an element, as if dragged from outside the page. At least one of \"paths\" or \"data\" must be provided.",
208 schema: json!({
209 "$schema": "https://json-schema.org/draft/2020-12/schema",
210 "type": "object",
211 "properties": {
212 "element": {
213 "description": "Human-readable element description used to obtain permission to interact with the element",
214 "type": "string"
215 },
216 "target": {
217 "type": "string",
218 "description": "Exact target element reference from the page snapshot, or a unique element selector"
219 },
220 "paths": {
221 "description": "Absolute paths to files to drop onto the element.",
222 "type": "array",
223 "items": { "type": "string" }
224 },
225 "data": {
226 "description": "Data to drop, as a map of MIME type to string value (e.g. {\"text/plain\": \"hello\", \"text/uri-list\": \"https://example.com\"}).",
227 "type": "object",
228 "propertyNames": { "type": "string" },
229 "additionalProperties": { "type": "string" }
230 }
231 },
232 "required": ["target"],
233 "additionalProperties": false
234 }),
235 },
236 ToolSpec {
237 name: "browser_find",
238 description: "Search the accessibility snapshot of the current page for text or a regular expression. Returns matching snapshot nodes with a few lines of surrounding context (like search snippets), each shown under its path from the root of the tree, which is cheaper than capturing the whole snapshot when you only need to locate an element and its ref.",
239 schema: json!({
240 "$schema": "https://json-schema.org/draft/2020-12/schema",
241 "type": "object",
242 "properties": {
243 "text": {
244 "description": "Plain text to search for in the page snapshot (case-insensitive substring match). Provide either text or regex, not both.",
245 "type": "string"
246 },
247 "regex": {
248 "description": "Regular expression to search for in the page snapshot. Matching is case-sensitive by default; wrap the pattern in slashes to add flags, e.g. \"/error/i\" for case-insensitive. Provide either text or regex, not both.",
249 "type": "string"
250 },
251 "filename": {
252 "description": "Save results to a file instead of returning them in the response. Relative file names are resolved against the workspace root.",
253 "type": "string"
254 }
255 },
256 "additionalProperties": false
257 }),
258 },
259 ToolSpec {
260 name: "browser_fill_form",
261 description: "Fill multiple form fields",
262 schema: json!({
263 "$schema": "https://json-schema.org/draft/2020-12/schema",
264 "type": "object",
265 "properties": {
266 "fields": {
267 "type": "array",
268 "items": {
269 "type": "object",
270 "properties": {
271 "element": {
272 "description": "Human-readable element description used to obtain permission to interact with the element",
273 "type": "string"
274 },
275 "target": {
276 "type": "string",
277 "description": "Exact target element reference from the page snapshot, or a unique element selector"
278 },
279 "name": {
280 "type": "string",
281 "description": "Human-readable field name"
282 },
283 "type": {
284 "type": "string",
285 "enum": ["textbox", "checkbox", "radio", "combobox", "slider"],
286 "description": "Type of the field"
287 },
288 "value": {
289 "type": "string",
290 "description": "Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option."
291 }
292 },
293 "required": ["target", "name", "type", "value"],
294 "additionalProperties": false
295 },
296 "description": "Fields to fill in"
297 }
298 },
299 "required": ["fields"],
300 "additionalProperties": false
301 }),
302 },
303 ToolSpec {
304 name: "browser_press_key",
305 description: "Press a key on the keyboard",
306 schema: json!({
307 "$schema": "https://json-schema.org/draft/2020-12/schema",
308 "type": "object",
309 "properties": {
310 "key": {
311 "type": "string",
312 "description": "Name of the key to press or a character to generate, such as `ArrowLeft` or `a`"
313 }
314 },
315 "required": ["key"],
316 "additionalProperties": false
317 }),
318 },
319 ToolSpec {
320 name: "browser_type",
321 description: "Type text into editable element",
322 schema: json!({
323 "$schema": "https://json-schema.org/draft/2020-12/schema",
324 "type": "object",
325 "properties": {
326 "element": {
327 "description": "Human-readable element description used to obtain permission to interact with the element",
328 "type": "string"
329 },
330 "target": {
331 "type": "string",
332 "description": "Exact target element reference from the page snapshot, or a unique element selector"
333 },
334 "text": {
335 "type": "string",
336 "description": "Text to type into the element"
337 },
338 "submit": {
339 "description": "Whether to submit entered text (press Enter after)",
340 "type": "boolean"
341 },
342 "slowly": {
343 "description": "Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.",
344 "type": "boolean"
345 }
346 },
347 "required": ["target", "text"],
348 "additionalProperties": false
349 }),
350 },
351 ToolSpec {
352 name: "browser_navigate",
353 description: "Navigate to a URL",
354 schema: json!({
355 "$schema": "https://json-schema.org/draft/2020-12/schema",
356 "type": "object",
357 "properties": {
358 "url": {
359 "type": "string",
360 "description": "The URL to navigate to"
361 }
362 },
363 "required": ["url"],
364 "additionalProperties": false
365 }),
366 },
367 ToolSpec {
368 name: "browser_navigate_back",
369 description: "Go back to the previous page in the history",
370 schema: json!({
371 "$schema": "https://json-schema.org/draft/2020-12/schema",
372 "type": "object",
373 "properties": {},
374 "additionalProperties": false
375 }),
376 },
377 ToolSpec {
378 name: "browser_network_requests",
379 description: "Returns a numbered list of network requests since loading the page. Use browser_network_request with the number to get full details.",
380 schema: json!({
381 "$schema": "https://json-schema.org/draft/2020-12/schema",
382 "type": "object",
383 "properties": {
384 "static": {
385 "default": false,
386 "description": "Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.",
387 "type": "boolean"
388 },
389 "filter": {
390 "description": "Only return requests whose URL matches this regexp (e.g. \"/api/.*user\").",
391 "type": "string"
392 },
393 "filename": {
394 "description": "File name to save the network requests to. Relative file names are resolved against the workspace root. If not provided, requests are returned as text.",
395 "type": "string"
396 }
397 },
398 "required": ["static"],
399 "additionalProperties": false
400 }),
401 },
402 ToolSpec {
403 name: "browser_network_request",
404 description: "Returns full details (headers and body) of a single network request, or a single part if `part` is set. Use the number from browser_network_requests.",
405 schema: json!({
406 "$schema": "https://json-schema.org/draft/2020-12/schema",
407 "type": "object",
408 "properties": {
409 "index": {
410 "type": "integer",
411 "minimum": 1,
412 "maximum": 9_007_199_254_740_991_u64,
413 "description": "1-based index of the request, as printed by browser_network_requests."
414 },
415 "part": {
416 "description": "Return only this part of the request. Omit to return full details.",
417 "type": "string",
418 "enum": ["request-headers", "request-body", "response-headers", "response-body"]
419 },
420 "filename": {
421 "description": "File name to save the result to. Relative file names are resolved against the workspace root. If not provided, output is returned as text.",
422 "type": "string"
423 }
424 },
425 "required": ["index"],
426 "additionalProperties": false
427 }),
428 },
429 ToolSpec {
430 name: "browser_run_code_unsafe",
431 description: "Run a Playwright code snippet. Unsafe: executes arbitrary JavaScript in the Playwright server process and is RCE-equivalent.",
432 schema: json!({
433 "$schema": "https://json-schema.org/draft/2020-12/schema",
434 "type": "object",
435 "properties": {
436 "code": {
437 "description": "A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }`",
438 "type": "string"
439 },
440 "filename": {
441 "description": "Load code from the specified file. Relative file names are resolved against the workspace root. If both code and filename are provided, code will be ignored.",
442 "type": "string"
443 }
444 },
445 "additionalProperties": false
446 }),
447 },
448 ToolSpec {
449 name: "browser_take_screenshot",
450 description: "Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.",
451 schema: json!({
452 "$schema": "https://json-schema.org/draft/2020-12/schema",
453 "type": "object",
454 "properties": {
455 "element": {
456 "description": "Human-readable element description used to obtain permission to interact with the element",
457 "type": "string"
458 },
459 "target": {
460 "description": "Exact target element reference from the page snapshot, or a unique element selector",
461 "type": "string"
462 },
463 "type": {
464 "description": "Image format for the screenshot. If unset, inferred from the filename extension, otherwise png.",
465 "type": "string",
466 "enum": ["png", "jpeg", "webp"]
467 },
468 "filename": {
469 "description": "File name to save the screenshot to. Relative file names are resolved against the workspace root. If not specified, the screenshot is saved into the output directory as `page-{timestamp}.{png|jpeg|webp}`.",
470 "type": "string"
471 },
472 "fullPage": {
473 "description": "When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.",
474 "type": "boolean"
475 },
476 "scale": {
477 "default": "css",
478 "description": "Image resolution scale. \"css\" produces a screenshot sized in CSS pixels (smaller, consistent across devices). \"device\" produces a high-resolution screenshot using device pixels (larger, accounts for the device pixel ratio). Default is css.",
479 "type": "string",
480 "enum": ["css", "device"]
481 }
482 },
483 "required": ["scale"],
484 "additionalProperties": false
485 }),
486 },
487 ToolSpec {
488 name: "browser_snapshot",
489 description: "Capture accessibility snapshot of the current page, this is better than \
495 screenshot. A ref belongs to the snapshot that minted it, and only to \
496 an element that was visible and pointer-interactive at that moment. A \
497 page that keeps replacing its elements retires refs before the next \
498 call, so drive it with a unique role/name selector such as \
499 role=button[name=\"Save\"], scoped when names repeat, which resolves at \
500 call time; once the page has settled, take a full-page snapshot with no \
501 target and its refs hold. A target-scoped snapshot answers with that \
502 subtree alone, so refs outside it do not resolve until a full-page \
503 snapshot.",
504 schema: json!({
505 "$schema": "https://json-schema.org/draft/2020-12/schema",
506 "type": "object",
507 "properties": {
508 "target": {
509 "description": "Exact target element reference from the page snapshot, or a unique element selector",
510 "type": "string"
511 },
512 "filename": {
513 "description": "Save snapshot to a file instead of returning it in the response. Relative file names are resolved against the workspace root.",
514 "type": "string"
515 },
516 "depth": {
517 "description": "Limit the depth of the snapshot tree",
518 "type": "number"
519 },
520 "boxes": {
521 "description": "Include each element's bounding box as [box=x,y,width,height] in the snapshot. Coordinates are viewport-relative, in CSS pixels (Element.getBoundingClientRect)",
522 "type": "boolean"
523 }
524 },
525 "additionalProperties": false
526 }),
527 },
528 ToolSpec {
529 name: "browser_click",
530 description: "Perform click on a web page",
531 schema: json!({
532 "$schema": "https://json-schema.org/draft/2020-12/schema",
533 "type": "object",
534 "properties": {
535 "element": {
536 "description": "Human-readable element description used to obtain permission to interact with the element",
537 "type": "string"
538 },
539 "target": {
540 "type": "string",
541 "description": "Exact target element reference from the page snapshot, or a unique element selector"
542 },
543 "doubleClick": {
544 "description": "Whether to perform a double click instead of a single click",
545 "type": "boolean"
546 },
547 "button": {
548 "description": "Button to click, defaults to left",
549 "type": "string",
550 "enum": ["left", "right", "middle"]
551 },
552 "modifiers": {
553 "description": "Modifier keys to press",
554 "type": "array",
555 "items": {
556 "type": "string",
557 "enum": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"]
558 }
559 },
560 "force": {
565 "description": "Click through the actionability gate with a real mouse event at the element's current position, instead of waiting for it to be visible, stable, enabled and uncovered.",
566 "type": "boolean"
567 }
568 },
569 "required": ["target"],
570 "additionalProperties": false
571 }),
572 },
573 ToolSpec {
574 name: "browser_drag",
575 description: "Perform drag and drop between two elements",
576 schema: json!({
577 "$schema": "https://json-schema.org/draft/2020-12/schema",
578 "type": "object",
579 "properties": {
580 "startElement": {
581 "description": "Human-readable source element description used to obtain the permission to interact with the element",
582 "type": "string"
583 },
584 "startTarget": {
585 "type": "string",
586 "description": "Exact target element reference from the page snapshot, or a unique element selector"
587 },
588 "endElement": {
589 "description": "Human-readable target element description used to obtain the permission to interact with the element",
590 "type": "string"
591 },
592 "endTarget": {
593 "type": "string",
594 "description": "Exact target element reference from the page snapshot, or a unique element selector"
595 }
596 },
597 "required": ["startTarget", "endTarget"],
598 "additionalProperties": false
599 }),
600 },
601 ToolSpec {
602 name: "browser_hover",
603 description: "Hover over element on page",
604 schema: json!({
605 "$schema": "https://json-schema.org/draft/2020-12/schema",
606 "type": "object",
607 "properties": {
608 "element": {
609 "description": "Human-readable element description used to obtain permission to interact with the element",
610 "type": "string"
611 },
612 "target": {
613 "type": "string",
614 "description": "Exact target element reference from the page snapshot, or a unique element selector"
615 }
616 },
617 "required": ["target"],
618 "additionalProperties": false
619 }),
620 },
621 ToolSpec {
622 name: "browser_select_option",
623 description: "Select an option in a dropdown",
624 schema: json!({
625 "$schema": "https://json-schema.org/draft/2020-12/schema",
626 "type": "object",
627 "properties": {
628 "element": {
629 "description": "Human-readable element description used to obtain permission to interact with the element",
630 "type": "string"
631 },
632 "target": {
633 "type": "string",
634 "description": "Exact target element reference from the page snapshot, or a unique element selector"
635 },
636 "values": {
637 "type": "array",
638 "items": { "type": "string" },
639 "description": "Array of values to select in the dropdown. This can be a single value or multiple values."
640 }
641 },
642 "required": ["target", "values"],
643 "additionalProperties": false
644 }),
645 },
646 ToolSpec {
647 name: "browser_tabs",
648 description: "List, create, close, or select a browser tab.",
649 schema: json!({
650 "$schema": "https://json-schema.org/draft/2020-12/schema",
651 "type": "object",
652 "properties": {
653 "action": {
654 "type": "string",
655 "enum": ["list", "new", "close", "select"],
656 "description": "Operation to perform"
657 },
658 "tab": {
659 "description": "The tab's page id, printed first in its row by browser_tabs list. Used for close/select; another session's tab is refused, and an id no tab carries answers with the tabs open now.",
660 "type": "string"
661 },
662 "url": {
663 "description": "URL to navigate to in the new tab, used for new.",
664 "type": "string"
665 }
666 },
667 "required": ["action"],
668 "additionalProperties": false
669 }),
670 },
671 ToolSpec {
672 name: "browser_wait_for",
673 description: "Wait for text to appear or disappear or a specified time to pass",
674 schema: json!({
675 "$schema": "https://json-schema.org/draft/2020-12/schema",
676 "type": "object",
677 "properties": {
678 "time": {
679 "description": "The time to wait in seconds, at most 30",
680 "type": "number"
681 },
682 "text": {
683 "description": "The text to wait for",
684 "type": "string"
685 },
686 "textGone": {
687 "description": "The text to wait for to disappear",
688 "type": "string"
689 },
690 "expression": {
693 "description": "A JavaScript expression, evaluated repeatedly until it is truthy. Use this to wait for a condition no text can state, e.g. `document.querySelectorAll('.row').length > 3`.",
694 "type": "string"
695 }
696 },
697 "additionalProperties": false
698 }),
699 },
700 ToolSpec {
705 name: "browser_click_and_capture",
706 description: "Click an element and return the network responses the click triggered - url, status and body - so a click whose meaning is its request does not need two calls and a poll in between.",
707 schema: json!({
708 "$schema": "https://json-schema.org/draft/2020-12/schema",
709 "type": "object",
710 "properties": {
711 "target": {
712 "type": "string",
713 "description": "Exact target element reference from the page snapshot, or a unique element selector"
714 },
715 "element": {
716 "description": "Human-readable element description used to obtain permission to interact with the element",
717 "type": "string"
718 },
719 "settleMs": {
720 "description": "How long to keep collecting responses after the click before answering. Defaults to 1500 ms.",
721 "type": "number"
722 }
723 },
724 "required": ["target"],
725 "additionalProperties": false
726 }),
727 },
728 ToolSpec {
729 name: "browser_form_state",
730 description: "Read a form's state: every field's current value, whether it is marked invalid, and the text of the control itself - which is where a committed value lives for a picker that keeps it outside its input.",
731 schema: json!({
732 "$schema": "https://json-schema.org/draft/2020-12/schema",
733 "type": "object",
734 "properties": {
735 "target": {
736 "type": "string",
737 "description": "Exact target element reference from the page snapshot, or a unique element selector, for a form or a container of fields. Omit to read every form on the page."
738 },
739 "element": {
740 "description": "Human-readable element description used to obtain permission to interact with the element",
741 "type": "string"
742 }
743 },
744 "additionalProperties": false
745 }),
746 },
747 ToolSpec {
748 name: "browser_challenge_state",
749 description: "Report each CAPTCHA challenge iframe in the top-level document: its vendor, presence, visibility (its own and its ancestors' computed display, opacity and filter included), whether it is on screen, whether a click at its centre would reach it, and its rect. `live` is true only when the frame is present, visible, on screen and hit-testable, and `challengePresent` says whether any challenge iframe was found at all.\n\nThis probe reads the top-level document, not inside it: a challenge in an iframe, frame, object or embed document, or in a shadow root, is not classified, and a closed shadow root cannot be seen at all. `unseen` names every region it did not read (each unmatched iframe, every frame, object and embed, every open shadow root) and `complete` is false whenever that list is non-empty, so an empty challenge list beside unseen entries is not 'no CAPTCHA here'. Presence is not liveness: Google's EXPIRED overlay is a reCAPTCHA iframe parked off screen and hidden, and it is still present with a real rect. When a challenge is present but none is live, reload the page and probe again; if it stays hidden, hand off to the person rather than clicking blind.",
750 schema: json!({
751 "$schema": "https://json-schema.org/draft/2020-12/schema",
752 "type": "object",
753 "properties": {},
754 "additionalProperties": false
755 }),
756 },
757 ToolSpec {
758 name: "browser_hand_off",
759 description: "Ask the person at the client to act in the browser - solve a CAPTCHA, sign in, confirm something only they can - and wait for their word. **A visual artifact - an HTML mockup, a PNG - is raised here**: navigate to it first, then hand off the tab it landed on; a document - Markdown, PDF, plain text - goes through `documents__hand_off`, never this tool. The prompt sits with no timeout, like any other ask; it returns once they are done, send you a message about the page, or decline. The raise shows the page this session has been driving, exactly as it stands, and their answer does not close it: prepare the page first, and the person acts on the very page you prepared.",
760 schema: json!({
761 "$schema": "https://json-schema.org/draft/2020-12/schema",
762 "type": "object",
763 "properties": {
764 "reason": {
765 "type": "string",
766 "description": "What needs doing, in the session's own words. It is shown on the prompt, so name the page and the act rather than the tool."
767 },
768 "tab": {
769 "type": "string",
770 "description": "The page id of one of this session's tabs, from browser_tabs list: the 32 hex characters it prints first in the tab's row, naming the tab the person is asked to look at. A tool list that still shows `tab` as a number was recorded before forge addressed tabs by page id, and neither a restart nor a resume refreshes it - only a fresh session in this slot does."
771 }
772 },
773 "required": ["reason", "tab"],
774 "additionalProperties": false
775 }),
776 },
777 ToolSpec {
781 name: "browser_record_start",
782 description: "Record the page's viewport into a silent 1280x720 MP4 at 30 frames a second, laying the page out at that size for the take. The file stays on the machine the browser runs on. Call browser_record_stop to finish it; the recording stops itself at 180 seconds.",
783 schema: json!({
784 "$schema": "https://json-schema.org/draft/2020-12/schema",
785 "type": "object",
786 "properties": {},
787 "additionalProperties": false
788 }),
789 },
790 ToolSpec {
791 name: "browser_record_stop",
792 description: "Stop the running recording, answer the file it wrote with its length and size, and put the controller's own view back.",
793 schema: json!({
794 "$schema": "https://json-schema.org/draft/2020-12/schema",
795 "type": "object",
796 "properties": {},
797 "additionalProperties": false
798 }),
799 },
800 ToolSpec {
803 name: "browser_end_host",
804 description: "End the browser host this session's profile resolves to and let the next call start a fresh one. Use it when page calls keep failing on a host that browser_tabs can still list, especially after a popup wedged the browser. The profile is the org's, or a project's override, so every session using it loses its tabs, including sessions in other projects; the profile's data is kept. Any lead whose project resolves to that profile can call it - a worker asks its lead instead.",
805 schema: json!({
806 "$schema": "https://json-schema.org/draft/2020-12/schema",
807 "type": "object",
808 "properties": {},
809 "additionalProperties": false
810 }),
811 },
812 ];
813 specs
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819
820 #[test]
825 fn no_tool_names_a_profile_any_more() {
826 for spec in specs() {
827 let properties = spec.schema["properties"].as_object().expect("a schema");
828 assert!(!properties.contains_key("profile"), "{} still takes a profile", spec.name);
829 }
830 }
831
832 #[test]
836 fn a_hand_off_names_the_tab_it_wants_seen() {
837 let spec =
838 specs().into_iter().find(|spec| spec.name == "browser_hand_off").expect("the tool");
839 let required = spec.schema["required"].as_array().expect("required");
840 assert!(required.iter().any(|value| value == "tab"), "the tab is required");
841 assert!(
842 spec.schema["properties"]["tab"]["description"]
843 .as_str()
844 .expect("a description")
845 .contains("one of this session's tabs")
846 );
847 assert_eq!(
848 spec.schema["properties"]["tab"]["type"], "string",
849 "the page id names the tab, not a host number",
850 );
851 assert!(
852 !specs().iter().any(|spec| spec.name == "browser_profile_delete"),
853 "profiles are config-owned; there is no delete verb",
854 );
855 }
856
857 #[test]
863 fn a_hand_off_routes_visual_artifacts_and_names_the_stale_catalogue() {
864 let spec =
865 specs().into_iter().find(|spec| spec.name == "browser_hand_off").expect("the tool");
866 for phrase in [
867 "A visual artifact - an HTML mockup, a PNG - is raised here",
868 "goes through `documents__hand_off`, never this tool",
869 ] {
870 assert!(
871 spec.description.contains(phrase),
872 "the routing rule is stated: {}",
873 spec.description,
874 );
875 }
876 let tab = spec.schema["properties"]["tab"]["description"].as_str().expect("a description");
877 assert!(tab.contains("32 hex characters"), "the page id's shape is stated: {tab}");
878 assert!(
879 tab.contains("fresh session in this slot"),
880 "the stale list's one recovery is named: {tab}",
881 );
882 }
883
884 #[test]
887 fn the_tabs_tool_addresses_a_tab_by_its_page_id() {
888 let spec = specs().into_iter().find(|spec| spec.name == "browser_tabs").expect("the tool");
889 let properties = spec.schema["properties"].as_object().expect("a schema");
890 assert!(!properties.contains_key("index"), "the host's number is not an address");
891 assert_eq!(properties["tab"]["type"], "string", "the page id is");
892 assert!(
893 properties["tab"]["description"].as_str().expect("a description").contains("page id"),
894 "and the description says which id it is",
895 );
896 }
897
898 #[test]
903 fn the_end_host_door_states_its_scope_and_its_caller() {
904 let spec =
905 specs().into_iter().find(|spec| spec.name == "browser_end_host").expect("the tool");
906 for phrase in [
907 "The profile is the org's, or a project's override",
908 "including sessions in other projects",
909 "Any lead whose project resolves to that profile can call it",
910 "a worker asks its lead instead",
911 ] {
912 assert!(
913 spec.description.contains(phrase),
914 "the description states `{phrase}`: {}",
915 spec.description,
916 );
917 }
918 }
919
920 #[test]
926 fn the_advertised_const_is_the_spec_tables_names() {
927 let mut table: Vec<&str> = specs().iter().map(|spec| spec.name).collect();
928 let mut advertised = forge_primitives::browser::ADVERTISED_TOOLS.to_vec();
929 table.sort_unstable();
930 advertised.sort_unstable();
931 assert_eq!(table, advertised, "the const carries every advertised name and nothing else");
932 }
933
934 #[test]
938 fn the_surface_is_the_captures_tools_and_the_additions() {
939 let names: Vec<&str> = specs().iter().map(|spec| spec.name).collect();
940 assert_eq!(names.len(), 32, "25 upstream tools and seven additions: {names:?}");
941 for upstream in [
942 "browser_close",
943 "browser_resize",
944 "browser_console_messages",
945 "browser_handle_dialog",
946 "browser_emulate_media",
947 "browser_evaluate",
948 "browser_file_upload",
949 "browser_drop",
950 "browser_find",
951 "browser_fill_form",
952 "browser_press_key",
953 "browser_type",
954 "browser_navigate",
955 "browser_navigate_back",
956 "browser_network_requests",
957 "browser_network_request",
958 "browser_run_code_unsafe",
959 "browser_take_screenshot",
960 "browser_snapshot",
961 "browser_click",
962 "browser_drag",
963 "browser_hover",
964 "browser_select_option",
965 "browser_tabs",
966 "browser_wait_for",
967 ] {
968 assert!(names.contains(&upstream), "{upstream} is missing from the surface");
969 }
970 assert!(
971 names.contains(&"browser_click_and_capture")
972 && names.contains(&"browser_form_state")
973 && names.contains(&"browser_challenge_state")
974 && names.contains(&"browser_hand_off")
975 && names.contains(&"browser_record_start")
976 && names.contains(&"browser_record_stop")
977 && names.contains(&"browser_end_host"),
978 "the seven beyond-upstream tools are on the surface: {names:?}",
979 );
980 }
981
982 #[test]
986 fn every_tool_is_a_closed_object_schema_with_upstreams_prefix() {
987 for spec in specs() {
988 assert!(spec.name.starts_with("browser_"), "{} is not a browser tool", spec.name);
989 assert_eq!(spec.schema["type"], "object", "{} is not an object schema", spec.name);
990 assert_eq!(
991 spec.schema["additionalProperties"],
992 json!(false),
993 "{} accepts arguments it does not declare",
994 spec.name,
995 );
996 assert!(
997 !spec.description.is_empty(),
998 "{} has no description for the model to read",
999 spec.name,
1000 );
1001 }
1002 }
1003
1004 #[test]
1010 fn the_snapshot_description_states_a_refs_lifetime() {
1011 let snapshot = specs()
1012 .into_iter()
1013 .find(|spec| spec.name == "browser_snapshot")
1014 .expect("browser_snapshot");
1015 assert!(
1016 snapshot.description.starts_with("Capture accessibility snapshot"),
1017 "upstream's first sentence is kept: {}",
1018 snapshot.description,
1019 );
1020 for phrase in [
1021 "A ref belongs to the snapshot that minted it",
1022 "only to an element that was visible and pointer-interactive",
1023 "drive it with a unique role/name selector",
1024 "scoped when names repeat",
1025 "take a full-page snapshot with no target and its refs hold",
1026 "A target-scoped snapshot answers with that subtree alone",
1027 "refs outside it do not resolve until a full-page snapshot",
1028 ] {
1029 assert!(
1030 snapshot.description.contains(phrase),
1031 "the description says {phrase:?}: {}",
1032 snapshot.description,
1033 );
1034 }
1035 let selector = snapshot
1036 .description
1037 .find("drive it with a unique role/name selector")
1038 .expect("the selector route is stated");
1039 let settled = snapshot
1040 .description
1041 .find("take a full-page snapshot with no target and its refs hold")
1042 .expect("the settled route");
1043 assert!(
1044 selector < settled,
1045 "the route for a still-changing page comes first: {}",
1046 snapshot.description,
1047 );
1048 }
1049
1050 #[test]
1053 fn the_added_arguments_ride_tools_upstream_already_has() {
1054 let specs = specs();
1055 let click = specs.iter().find(|spec| spec.name == "browser_click").expect("browser_click");
1056 assert_eq!(
1057 click.schema["properties"]["force"]["type"],
1058 json!("boolean"),
1059 "the click carries `force`: {}",
1060 click.schema,
1061 );
1062 assert_eq!(click.schema["required"], json!(["target"]), "and requires what upstream does");
1063
1064 let wait =
1065 specs.iter().find(|spec| spec.name == "browser_wait_for").expect("browser_wait_for");
1066 assert_eq!(
1067 wait.schema["properties"]["expression"]["type"],
1068 json!("string"),
1069 "the wait carries `expression`: {}",
1070 wait.schema,
1071 );
1072 }
1073}