@gleanwork/mcp-server-tester 2.0.0-beta.0 → 2.0.0-beta.2
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.
- package/dist/cli/index.js +3854 -1219
- package/dist/fixtures/mcp.d.ts +3 -0
- package/dist/fixtures/mcp.js +2 -2
- package/dist/fixtures/mcp.js.map +1 -1
- package/dist/{index-aX_tCfTD.d.cts → index-CwFHKIoN.d.cts} +70 -1
- package/dist/{index-aX_tCfTD.d.ts → index-CwFHKIoN.d.ts} +70 -1
- package/dist/index.cjs +5077 -2542
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5091 -2557
- package/dist/index.js.map +1 -1
- package/dist/reporters/mcpReporter.d.cts +3 -0
- package/dist/reporters/mcpReporter.d.ts +3 -0
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +7 -3
- package/scripts/cowork-requirements.txt +5 -0
- package/scripts/cowork_computer_use.py +409 -0
package/dist/types/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AuthType, K as BuiltInRubric, N as BuiltInSanitizer, Q as CLIConfig, W as CLIOAuthClientConfig, X as CLIOAuthResult, Y as CLIOutputFormat, Z as CaseComparisonResult, _ as ClientCredentialsConfig, $ as CompareEvalRunsOptions, a0 as ComparisonOutcome, a1 as ContentBlock, a2 as CreateMCPClientOptions, a3 as CustomJudgeExecutor, a4 as CustomJudgeResult, a9 as EvalCase, aa as EvalCaseComparison, ab as EvalCaseComparisonOutcome, ac as EvalCaseRequest, l as EvalCaseResult, ae as EvalContext, o as EvalDataset, ag as EvalExpectBlock, ah as EvalExpectationResult, ak as EvalMode, ao as EvalRunComparisonLabels, ap as EvalRunComparisonResult, aq as EvalRunMetadata, ar as EvalRunnerOptions, r as EvalRunnerResult, ay as ExpectationBreakdown, az as ExpectationResultMap, aA as ExpectationType, aB as ExperimentMetric, aJ as FieldRemovalSanitizer,
|
|
1
|
+
export { A as AuthType, K as BuiltInRubric, N as BuiltInSanitizer, Q as CLIConfig, W as CLIOAuthClientConfig, X as CLIOAuthResult, Y as CLIOutputFormat, Z as CaseComparisonResult, _ as ClientCredentialsConfig, $ as CompareEvalRunsOptions, a0 as ComparisonOutcome, a1 as ContentBlock, a2 as CreateMCPClientOptions, a3 as CustomJudgeExecutor, a4 as CustomJudgeResult, a9 as EvalCase, aa as EvalCaseComparison, ab as EvalCaseComparisonOutcome, ac as EvalCaseRequest, l as EvalCaseResult, ae as EvalContext, o as EvalDataset, ag as EvalExpectBlock, ah as EvalExpectationResult, ak as EvalMode, ao as EvalRunComparisonLabels, ap as EvalRunComparisonResult, aq as EvalRunMetadata, ar as EvalRunnerOptions, r as EvalRunnerResult, ay as ExpectationBreakdown, az as ExpectationResultMap, aA as ExpectationType, aB as ExperimentMetric, aJ as FieldRemovalSanitizer, aT as HostDiagnostics, a_ as HostType, a$ as HttpMCPConfig, b0 as IterationResult, G as Judge, F as JudgeConfig, b1 as JudgeExpectConfig, J as JudgeMatcherOptions, b2 as JudgeResult, b3 as JudgeValidatorConfig, L as LLMProvider, b4 as LLMToolCall, b6 as LoadDatasetOptions, b7 as MCPAuthConfig, b8 as MCPAuthFixtures, b9 as MCPClientCredentialsConfig, p as MCPConfig, bb as MCPConformanceCheck, bc as MCPConformanceOptions, bd as MCPConformanceRaw, be as MCPConformanceResult, bf as MCPConformanceResultData, bg as MCPEvalData, bh as MCPEvalHistoricalSummary, bi as MCPEvalReporterConfig, bj as MCPEvalRunData, M as MCPFixtureApi, bk as MCPFixtureOptions, bl as MCPHostCapabilities, a as MCPHostConfig, b as MCPHostSimulationResult, bm as MCPHostSimulator, bn as MCPOAuthConfig, bo as MCPServerCapabilitiesData, br as NormalizedToolResponse, O as OAuthSetupConfig, P as PatternValidatorOptions, bu as PlaywrightOAuthClientProviderConfig, bv as PredicateResult, bw as ProposeVariantsContext, bx as ProtectedResourceDiscoveryResult, by as ProtectedResourceMetadata, bz as ProviderKind, bA as RegexSanitizer, bB as ResultSource, R as RubricSpec, bE as SaveBaselineOptions, bF as SaveEvalRunComparisonOptions, bG as SaveServerComparisonOptions, bH as SchemaRegistry, S as SchemaValidatorOptions, bI as SerializedEvalDataset, bJ as ServerComparisonOptions, bK as ServerComparisonResult, c as SizeValidatorOptions, d as SnapshotSanitizer, bL as SnapshotSanitizers, bM as StdioMCPConfig, bP as StoredClientInfo, bS as StoredEvalResultLoadOptions, bT as StoredEvalResultRef, bU as StoredEvalResultSaveOptions, bV as StoredEvalRunRef, bW as StoredOAuthState, bX as StoredServerMetadata, bY as StoredTokens, T as TextValidatorOptions, b_ as TokenResult, g as ToolCallCountOptions, f as ToolCallExpectation, b$ as ToolMetadataOverride, c0 as ToolOverrideVariant, e as ToolPredicate, c2 as UsageMetrics, V as ValidationResult, c3 as VariantCandidateResult, c4 as VariantExperimentOptions, c5 as VariantExperimentReason, c6 as VariantExperimentResult, c7 as VariantExperimentRound, c8 as VariantImprovementProposal, c9 as VariantRecommendation } from '../index-CwFHKIoN.cjs';
|
|
2
2
|
import 'zod';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
import '@modelcontextprotocol/sdk/client/index.js';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AuthType, K as BuiltInRubric, N as BuiltInSanitizer, Q as CLIConfig, W as CLIOAuthClientConfig, X as CLIOAuthResult, Y as CLIOutputFormat, Z as CaseComparisonResult, _ as ClientCredentialsConfig, $ as CompareEvalRunsOptions, a0 as ComparisonOutcome, a1 as ContentBlock, a2 as CreateMCPClientOptions, a3 as CustomJudgeExecutor, a4 as CustomJudgeResult, a9 as EvalCase, aa as EvalCaseComparison, ab as EvalCaseComparisonOutcome, ac as EvalCaseRequest, l as EvalCaseResult, ae as EvalContext, o as EvalDataset, ag as EvalExpectBlock, ah as EvalExpectationResult, ak as EvalMode, ao as EvalRunComparisonLabels, ap as EvalRunComparisonResult, aq as EvalRunMetadata, ar as EvalRunnerOptions, r as EvalRunnerResult, ay as ExpectationBreakdown, az as ExpectationResultMap, aA as ExpectationType, aB as ExperimentMetric, aJ as FieldRemovalSanitizer,
|
|
1
|
+
export { A as AuthType, K as BuiltInRubric, N as BuiltInSanitizer, Q as CLIConfig, W as CLIOAuthClientConfig, X as CLIOAuthResult, Y as CLIOutputFormat, Z as CaseComparisonResult, _ as ClientCredentialsConfig, $ as CompareEvalRunsOptions, a0 as ComparisonOutcome, a1 as ContentBlock, a2 as CreateMCPClientOptions, a3 as CustomJudgeExecutor, a4 as CustomJudgeResult, a9 as EvalCase, aa as EvalCaseComparison, ab as EvalCaseComparisonOutcome, ac as EvalCaseRequest, l as EvalCaseResult, ae as EvalContext, o as EvalDataset, ag as EvalExpectBlock, ah as EvalExpectationResult, ak as EvalMode, ao as EvalRunComparisonLabels, ap as EvalRunComparisonResult, aq as EvalRunMetadata, ar as EvalRunnerOptions, r as EvalRunnerResult, ay as ExpectationBreakdown, az as ExpectationResultMap, aA as ExpectationType, aB as ExperimentMetric, aJ as FieldRemovalSanitizer, aT as HostDiagnostics, a_ as HostType, a$ as HttpMCPConfig, b0 as IterationResult, G as Judge, F as JudgeConfig, b1 as JudgeExpectConfig, J as JudgeMatcherOptions, b2 as JudgeResult, b3 as JudgeValidatorConfig, L as LLMProvider, b4 as LLMToolCall, b6 as LoadDatasetOptions, b7 as MCPAuthConfig, b8 as MCPAuthFixtures, b9 as MCPClientCredentialsConfig, p as MCPConfig, bb as MCPConformanceCheck, bc as MCPConformanceOptions, bd as MCPConformanceRaw, be as MCPConformanceResult, bf as MCPConformanceResultData, bg as MCPEvalData, bh as MCPEvalHistoricalSummary, bi as MCPEvalReporterConfig, bj as MCPEvalRunData, M as MCPFixtureApi, bk as MCPFixtureOptions, bl as MCPHostCapabilities, a as MCPHostConfig, b as MCPHostSimulationResult, bm as MCPHostSimulator, bn as MCPOAuthConfig, bo as MCPServerCapabilitiesData, br as NormalizedToolResponse, O as OAuthSetupConfig, P as PatternValidatorOptions, bu as PlaywrightOAuthClientProviderConfig, bv as PredicateResult, bw as ProposeVariantsContext, bx as ProtectedResourceDiscoveryResult, by as ProtectedResourceMetadata, bz as ProviderKind, bA as RegexSanitizer, bB as ResultSource, R as RubricSpec, bE as SaveBaselineOptions, bF as SaveEvalRunComparisonOptions, bG as SaveServerComparisonOptions, bH as SchemaRegistry, S as SchemaValidatorOptions, bI as SerializedEvalDataset, bJ as ServerComparisonOptions, bK as ServerComparisonResult, c as SizeValidatorOptions, d as SnapshotSanitizer, bL as SnapshotSanitizers, bM as StdioMCPConfig, bP as StoredClientInfo, bS as StoredEvalResultLoadOptions, bT as StoredEvalResultRef, bU as StoredEvalResultSaveOptions, bV as StoredEvalRunRef, bW as StoredOAuthState, bX as StoredServerMetadata, bY as StoredTokens, T as TextValidatorOptions, b_ as TokenResult, g as ToolCallCountOptions, f as ToolCallExpectation, b$ as ToolMetadataOverride, c0 as ToolOverrideVariant, e as ToolPredicate, c2 as UsageMetrics, V as ValidationResult, c3 as VariantCandidateResult, c4 as VariantExperimentOptions, c5 as VariantExperimentReason, c6 as VariantExperimentResult, c7 as VariantExperimentRound, c8 as VariantImprovementProposal, c9 as VariantRecommendation } from '../index-CwFHKIoN.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
import '@playwright/test';
|
|
4
4
|
import '@modelcontextprotocol/sdk/client/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gleanwork/mcp-server-tester",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.2",
|
|
4
4
|
"description": "Playwright-based testing and evaluation framework for MCP servers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"import": "./dist/index.js",
|
|
29
29
|
"require": "./dist/index.cjs"
|
|
30
30
|
},
|
|
31
|
+
"./cowork-runtime": "./scripts/cowork_computer_use.py",
|
|
32
|
+
"./cowork-requirements": "./scripts/cowork-requirements.txt",
|
|
31
33
|
"./types": {
|
|
32
34
|
"types": "./dist/types/index.d.ts",
|
|
33
35
|
"import": "./dist/types/index.js",
|
|
@@ -54,12 +56,14 @@
|
|
|
54
56
|
"mcp-server-tester": "./dist/cli/index.js"
|
|
55
57
|
},
|
|
56
58
|
"files": [
|
|
57
|
-
"dist"
|
|
59
|
+
"dist",
|
|
60
|
+
"scripts/cowork_computer_use.py",
|
|
61
|
+
"scripts/cowork-requirements.txt"
|
|
58
62
|
],
|
|
59
63
|
"scripts": {
|
|
60
64
|
"build": "npm run build:ui && tsup && npm run build:copy-ui",
|
|
61
65
|
"build:copy-ui": "cp -r src/reporters/ui-dist dist/reporters/",
|
|
62
|
-
"build:ui": "tsx src/reporters/build-ui.ts",
|
|
66
|
+
"build:ui": "node --import tsx src/reporters/build-ui.ts",
|
|
63
67
|
"dev": "tsup --watch",
|
|
64
68
|
"dev:ui": "tsx src/reporters/build-ui.ts --watch",
|
|
65
69
|
"format": "prettier --write \"**/*.{ts,tsx,json,md}\"",
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Submit one Claude Cowork query through a bounded screenshot/action loop.
|
|
3
|
+
|
|
4
|
+
The process stops immediately after the first submit key. MST owns native-session
|
|
5
|
+
correlation, terminal validation, response extraction, and telemetry collection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import asyncio
|
|
12
|
+
import base64
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
DISPLAY_WIDTH = 1280
|
|
24
|
+
DISPLAY_HEIGHT = 800
|
|
25
|
+
DEFAULT_MODEL = "claude-sonnet-4-6"
|
|
26
|
+
DEFAULT_MAX_ACTIONS = 24
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def log(message: str) -> None:
|
|
30
|
+
print(f"[mst:cowork-cu] {message}", file=sys.stderr, flush=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def screenshot() -> dict[str, Any]:
|
|
34
|
+
import mss
|
|
35
|
+
from PIL import Image
|
|
36
|
+
|
|
37
|
+
with mss.MSS() as capture:
|
|
38
|
+
monitor = capture.monitors[1]
|
|
39
|
+
raw = capture.grab(monitor)
|
|
40
|
+
image = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX")
|
|
41
|
+
image = image.resize((DISPLAY_WIDTH, DISPLAY_HEIGHT), Image.Resampling.LANCZOS)
|
|
42
|
+
buffer = io.BytesIO()
|
|
43
|
+
image.save(buffer, format="PNG")
|
|
44
|
+
return {
|
|
45
|
+
"type": "image",
|
|
46
|
+
"source": {
|
|
47
|
+
"type": "base64",
|
|
48
|
+
"media_type": "image/png",
|
|
49
|
+
"data": base64.standard_b64encode(buffer.getvalue()).decode("ascii"),
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def screen_point(coordinate: list[int]) -> tuple[int, int]:
|
|
55
|
+
import pyautogui
|
|
56
|
+
|
|
57
|
+
width, height = pyautogui.size() # Logical points, not Retina capture pixels.
|
|
58
|
+
return (
|
|
59
|
+
int(coordinate[0] * width / DISPLAY_WIDTH),
|
|
60
|
+
int(coordinate[1] * height / DISPLAY_HEIGHT),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def type_query(text: str) -> None:
|
|
65
|
+
"""Unicode keyboard events; line breaks must not submit the composer."""
|
|
66
|
+
import Quartz
|
|
67
|
+
import pyautogui
|
|
68
|
+
|
|
69
|
+
for line_index, line in enumerate(text.split("\n")):
|
|
70
|
+
if line_index:
|
|
71
|
+
pyautogui.hotkey("shift", "enter")
|
|
72
|
+
for start in range(0, len(line), 16):
|
|
73
|
+
chunk = line[start:start + 16]
|
|
74
|
+
units = len(chunk.encode("utf-16-le")) // 2
|
|
75
|
+
for down in (True, False):
|
|
76
|
+
event = Quartz.CGEventCreateKeyboardEvent(None, 0, down)
|
|
77
|
+
Quartz.CGEventKeyboardSetUnicodeString(event, units, chunk)
|
|
78
|
+
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
|
|
79
|
+
time.sleep(0.01)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def execute_action(action: dict[str, Any]) -> tuple[Any, bool]:
|
|
83
|
+
import pyautogui
|
|
84
|
+
|
|
85
|
+
name = action.get("action")
|
|
86
|
+
if name == "screenshot":
|
|
87
|
+
return screenshot(), False
|
|
88
|
+
if name in {
|
|
89
|
+
"left_click",
|
|
90
|
+
"right_click",
|
|
91
|
+
"middle_click",
|
|
92
|
+
"double_click",
|
|
93
|
+
"triple_click",
|
|
94
|
+
}:
|
|
95
|
+
x, y = screen_point(action["coordinate"])
|
|
96
|
+
button = {
|
|
97
|
+
"right_click": "right",
|
|
98
|
+
"middle_click": "middle",
|
|
99
|
+
}.get(name, "left")
|
|
100
|
+
clicks = {
|
|
101
|
+
"double_click": 2,
|
|
102
|
+
"triple_click": 3,
|
|
103
|
+
}.get(name, 1)
|
|
104
|
+
pyautogui.click(x, y, clicks=clicks, button=button)
|
|
105
|
+
return f"{name} completed", False
|
|
106
|
+
if name == "cursor_position":
|
|
107
|
+
x, y = pyautogui.position()
|
|
108
|
+
return {"x": int(x), "y": int(y)}, False
|
|
109
|
+
if name == "mouse_move":
|
|
110
|
+
x, y = screen_point(action["coordinate"])
|
|
111
|
+
pyautogui.moveTo(x, y)
|
|
112
|
+
return "mouse_move completed", False
|
|
113
|
+
if name == "left_click_drag":
|
|
114
|
+
start_x, start_y = screen_point(action["start_coordinate"])
|
|
115
|
+
end_x, end_y = screen_point(action["coordinate"])
|
|
116
|
+
pyautogui.moveTo(start_x, start_y)
|
|
117
|
+
pyautogui.dragTo(end_x, end_y, duration=0.4, button="left")
|
|
118
|
+
return "drag completed", False
|
|
119
|
+
if name == "type":
|
|
120
|
+
type_query(action["text"])
|
|
121
|
+
return "text entered", False
|
|
122
|
+
if name == "key":
|
|
123
|
+
text = str(action["text"])
|
|
124
|
+
aliases = {"return": "enter", "super": "command", "cmd": "command", "ctrl": "ctrl"}
|
|
125
|
+
keys = [aliases.get(part.strip().lower(), part.strip().lower()) for part in text.split("+")]
|
|
126
|
+
if len(keys) == 1:
|
|
127
|
+
pyautogui.press(keys[0])
|
|
128
|
+
else:
|
|
129
|
+
pyautogui.hotkey(*keys)
|
|
130
|
+
submitted = any(key in {"enter", "return"} for key in keys)
|
|
131
|
+
return f"key completed: {text}", submitted
|
|
132
|
+
if name == "scroll":
|
|
133
|
+
x, y = screen_point(action.get("coordinate", [DISPLAY_WIDTH // 2, DISPLAY_HEIGHT // 2]))
|
|
134
|
+
amount = int(action.get("scroll_amount", 3))
|
|
135
|
+
direction = action.get("scroll_direction", "down")
|
|
136
|
+
pyautogui.moveTo(x, y)
|
|
137
|
+
pyautogui.scroll(-amount if direction == "down" else amount)
|
|
138
|
+
return "scroll completed", False
|
|
139
|
+
if name == "wait":
|
|
140
|
+
time.sleep(min(float(action.get("duration", 1)), 5.0))
|
|
141
|
+
return "wait completed", False
|
|
142
|
+
raise RuntimeError(f"unsupported Computer Use action: {name}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
TOKEN_FIELDS = (
|
|
146
|
+
"input_tokens", "output_tokens", "cache_creation_input_tokens", "cache_read_input_tokens",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ComputerUseDriverError(RuntimeError):
|
|
151
|
+
def __init__(self, message: str, telemetry: dict[str, Any]):
|
|
152
|
+
super().__init__(message)
|
|
153
|
+
self.telemetry = telemetry
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Telemetry:
|
|
157
|
+
"""Only completed API responses and allowlisted scalar data; never request content."""
|
|
158
|
+
|
|
159
|
+
def __init__(self) -> None:
|
|
160
|
+
self.started = time.monotonic()
|
|
161
|
+
self.models: list[str] = []
|
|
162
|
+
self.responses = 0
|
|
163
|
+
self.usage: dict[str, int | float] = {}
|
|
164
|
+
self.coverage = {field: 0 for field in TOKEN_FIELDS}
|
|
165
|
+
self.actions = 0
|
|
166
|
+
self.attempted = 0
|
|
167
|
+
self.executed = 0
|
|
168
|
+
self.refused = 0
|
|
169
|
+
|
|
170
|
+
def observe(self, response: Any) -> None:
|
|
171
|
+
self.responses += 1
|
|
172
|
+
model = getattr(response, "model", None)
|
|
173
|
+
secrets = [value for key, value in os.environ.items()
|
|
174
|
+
if value and re.search(r"token|key|secret|password|authorization", key, re.I)]
|
|
175
|
+
if (isinstance(model, str) and re.fullmatch(r"claude-[a-z0-9][a-z0-9.-]{0,99}", model)
|
|
176
|
+
and not any(secret in model for secret in secrets) and model not in self.models):
|
|
177
|
+
self.models.append(model)
|
|
178
|
+
usage = getattr(response, "usage", None)
|
|
179
|
+
for field in TOKEN_FIELDS:
|
|
180
|
+
value = getattr(usage, field, None)
|
|
181
|
+
if (type(value) in (int, float) and 0 <= value <= 9007199254740991
|
|
182
|
+
and math.isfinite(value) and value == int(value)):
|
|
183
|
+
total = self.usage.get(field, 0) + value
|
|
184
|
+
if total <= 9007199254740991:
|
|
185
|
+
self.usage[field] = total
|
|
186
|
+
self.coverage[field] += 1
|
|
187
|
+
|
|
188
|
+
def snapshot(self, accounting: str) -> dict[str, Any]:
|
|
189
|
+
return {
|
|
190
|
+
"accounting": accounting,
|
|
191
|
+
"response_models": list(self.models),
|
|
192
|
+
"planner_response_count": self.responses,
|
|
193
|
+
"usage": dict(self.usage),
|
|
194
|
+
"usage_observation_counts": dict(self.coverage),
|
|
195
|
+
"duration_ms": max(0, (time.monotonic() - self.started) * 1000),
|
|
196
|
+
"action_count": self.actions,
|
|
197
|
+
"attempted_action_count": self.attempted,
|
|
198
|
+
"executed_action_count": self.executed,
|
|
199
|
+
"refused_action_count": self.refused,
|
|
200
|
+
# Anthropic Messages usage does not supply authoritative dollar cost.
|
|
201
|
+
"cost": {"status": "unavailable"},
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def run(query: str, max_actions: int, mode: str) -> dict[str, Any]:
|
|
206
|
+
telemetry = Telemetry()
|
|
207
|
+
try:
|
|
208
|
+
result = await run_driver(query, max_actions, mode, telemetry)
|
|
209
|
+
result["telemetry"] = telemetry.snapshot("complete")
|
|
210
|
+
return result
|
|
211
|
+
except Exception as error:
|
|
212
|
+
raise ComputerUseDriverError(str(error), telemetry.snapshot("partial")) from error
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def run_driver(query: str, max_actions: int, mode: str, telemetry: Telemetry) -> dict[str, Any]:
|
|
216
|
+
try:
|
|
217
|
+
import anthropic
|
|
218
|
+
except ImportError as error:
|
|
219
|
+
raise RuntimeError("Install the Computer Use dependencies: pip install anthropic pyautogui mss Pillow") from error
|
|
220
|
+
|
|
221
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
222
|
+
if not api_key:
|
|
223
|
+
raise RuntimeError("ANTHROPIC_API_KEY is required for the Computer Use submission driver")
|
|
224
|
+
|
|
225
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
226
|
+
model = os.environ.get("MST_COWORK_CUA_MODEL", DEFAULT_MODEL)
|
|
227
|
+
log(f"starting driver with model={model}, max_actions={max_actions}")
|
|
228
|
+
subprocess.run(["open", "-a", "Claude"], check=False, capture_output=True)
|
|
229
|
+
log("requested Claude Desktop launch/focus")
|
|
230
|
+
time.sleep(float(os.environ.get("MST_COWORK_CUA_START_DELAY", "3")))
|
|
231
|
+
|
|
232
|
+
tools = [{
|
|
233
|
+
"type": "computer_20251124",
|
|
234
|
+
"name": "computer",
|
|
235
|
+
"display_width_px": DISPLAY_WIDTH,
|
|
236
|
+
"display_height_px": DISPLAY_HEIGHT,
|
|
237
|
+
}]
|
|
238
|
+
if mode == "hitl":
|
|
239
|
+
messages: list[dict[str, Any]] = [{
|
|
240
|
+
"role": "user",
|
|
241
|
+
"content": (
|
|
242
|
+
"Inspect the currently visible Claude Desktop Cowork task. If a human-in-the-loop "
|
|
243
|
+
"approval or choice prompt is blocking progress, choose the first visible option. "
|
|
244
|
+
"Do not submit a new query, do not change account settings, and do not approve a "
|
|
245
|
+
"prompt unless it is the visible task's first option. If no such prompt is visible, "
|
|
246
|
+
"stop without taking an action.\n\nTask to locate (do not type or submit this text):\n" + query
|
|
247
|
+
),
|
|
248
|
+
}]
|
|
249
|
+
system = (
|
|
250
|
+
"You are a bounded HITL resolver. Use screenshots and Computer Use actions only. "
|
|
251
|
+
"Choose the first visible option when a task approval/choice prompt is present. "
|
|
252
|
+
"Never submit a query or change account settings. If no HITL prompt is visible, stop."
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
tools.append({
|
|
256
|
+
"name": "fill_query",
|
|
257
|
+
"description": (
|
|
258
|
+
"Insert the unchanged original evaluation query into the focused "
|
|
259
|
+
"empty Cowork task composer. First locate and focus that composer using a screenshot. "
|
|
260
|
+
"This tool takes no text: the harness supplies the exact text. It may run only once. "
|
|
261
|
+
"After it succeeds, use computer key Enter to submit, never click Send."
|
|
262
|
+
),
|
|
263
|
+
"input_schema": {"type": "object", "properties": {}, "additionalProperties": False},
|
|
264
|
+
})
|
|
265
|
+
messages = [{
|
|
266
|
+
"role": "user",
|
|
267
|
+
"content": (
|
|
268
|
+
"Control Claude Desktop. Open Cowork, create a fresh task, and focus its empty "
|
|
269
|
+
"prompt composer. Call fill_query with no arguments. It inserts the original "
|
|
270
|
+
"query for you. Do not reconstruct or type the query yourself. Then press "
|
|
271
|
+
"unmodified Enter once to submit. Do not wait for or read the answer."
|
|
272
|
+
),
|
|
273
|
+
}]
|
|
274
|
+
system = (
|
|
275
|
+
"You are a bounded desktop submission operator. Use screenshots and Computer Use "
|
|
276
|
+
"actions to open/focus Claude, select Cowork, create a fresh task, and focus its composer. "
|
|
277
|
+
"Use fill_query, not computer type, to insert the query. After fill_query succeeds, "
|
|
278
|
+
"submit with unmodified Enter, never by clicking Send. Never submit twice. Stop after "
|
|
279
|
+
"submission. Do not approve permissions or change account settings."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Ground the first action in a current screenshot, not an assumed layout.
|
|
283
|
+
messages[0]["content"] = [{"type": "text", "text": messages[0]["content"]}, screenshot()]
|
|
284
|
+
hitl_action_taken = False
|
|
285
|
+
actions_executed = 0
|
|
286
|
+
typed_query = False
|
|
287
|
+
for action_number in range(1, max_actions + 1):
|
|
288
|
+
log(f"requesting Computer Use plan {action_number}/{max_actions}")
|
|
289
|
+
response = client.beta.messages.create(
|
|
290
|
+
model=model,
|
|
291
|
+
max_tokens=1024,
|
|
292
|
+
system=system,
|
|
293
|
+
tools=tools,
|
|
294
|
+
messages=messages,
|
|
295
|
+
betas=["computer-use-2025-11-24"],
|
|
296
|
+
)
|
|
297
|
+
telemetry.observe(response)
|
|
298
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
299
|
+
tool_results: list[dict[str, Any]] = []
|
|
300
|
+
for block in response.content:
|
|
301
|
+
if getattr(block, "type", None) != "tool_use":
|
|
302
|
+
continue
|
|
303
|
+
if actions_executed >= max_actions:
|
|
304
|
+
raise RuntimeError("Computer Use action budget exhausted; no further actions executed")
|
|
305
|
+
actions_executed += 1
|
|
306
|
+
telemetry.actions += 1
|
|
307
|
+
tool_name = getattr(block, "name", "computer")
|
|
308
|
+
action = block.input
|
|
309
|
+
action_name = action.get('action', 'unknown')
|
|
310
|
+
refusal = None
|
|
311
|
+
if mode == "hitl" and (tool_name != "computer" or action_name not in {"screenshot", "wait", "mouse_move", "cursor_position", "left_click", "scroll"}):
|
|
312
|
+
telemetry.refused += 1
|
|
313
|
+
raise RuntimeError("HITL cannot type, press keys, drag, or submit tasks")
|
|
314
|
+
if mode == "submit":
|
|
315
|
+
if tool_name == "fill_query":
|
|
316
|
+
if typed_query:
|
|
317
|
+
refusal = "The query is already entered. Do not fill again. Use unmodified Enter to submit."
|
|
318
|
+
elif block.input != {}:
|
|
319
|
+
refusal = "fill_query takes no arguments; the harness owns the original text."
|
|
320
|
+
else:
|
|
321
|
+
action = {"action": "type", "text": query}
|
|
322
|
+
action_name = "type"
|
|
323
|
+
elif tool_name != "computer":
|
|
324
|
+
refusal = "Unknown tool. Use computer for navigation and fill_query for text entry."
|
|
325
|
+
elif action_name == "type":
|
|
326
|
+
refusal = "Do not reconstruct the query. Focus the empty composer, then call fill_query with no arguments."
|
|
327
|
+
elif action_name == "key" and any(k.strip().lower() in {"enter", "return"} for k in str(action.get("text", "")).split("+")):
|
|
328
|
+
if not typed_query or str(action.get("text", "")).lower() not in {"enter", "return"}:
|
|
329
|
+
refusal = "Only an unmodified Enter after fill_query succeeds may submit."
|
|
330
|
+
elif typed_query and action_name not in {"screenshot", "wait", "cursor_position"}:
|
|
331
|
+
refusal = "The query is entered. Only screenshots or the single Enter submission are allowed."
|
|
332
|
+
if refusal:
|
|
333
|
+
telemetry.refused += 1
|
|
334
|
+
# A rejected proposal has no desktop side effects. Let the planner correct it
|
|
335
|
+
# within the same action budget; never retry a failed physical text entry.
|
|
336
|
+
tool_results.append({"type": "tool_result", "tool_use_id": block.id, "content": refusal, "is_error": True})
|
|
337
|
+
continue
|
|
338
|
+
log(f"executing action {actions_executed}: {action_name}")
|
|
339
|
+
if mode == "hitl" and action_name not in {
|
|
340
|
+
"screenshot",
|
|
341
|
+
"wait",
|
|
342
|
+
"mouse_move",
|
|
343
|
+
"cursor_position",
|
|
344
|
+
}:
|
|
345
|
+
hitl_action_taken = True
|
|
346
|
+
telemetry.attempted += 1
|
|
347
|
+
result, submitted = execute_action(action)
|
|
348
|
+
telemetry.executed += 1
|
|
349
|
+
if tool_name == "fill_query":
|
|
350
|
+
typed_query = True
|
|
351
|
+
if submitted and mode != "hitl":
|
|
352
|
+
log("submission boundary reached; stopping immediately")
|
|
353
|
+
return {
|
|
354
|
+
"status": "submitted",
|
|
355
|
+
"action_count": actions_executed,
|
|
356
|
+
"model": model,
|
|
357
|
+
"submission_action": {"action": "key", "text": "enter"},
|
|
358
|
+
}
|
|
359
|
+
if isinstance(result, dict) and result.get("type") == "image":
|
|
360
|
+
tool_content: Any = [result]
|
|
361
|
+
else:
|
|
362
|
+
tool_content = str(result)
|
|
363
|
+
tool_results.append({
|
|
364
|
+
"type": "tool_result",
|
|
365
|
+
"tool_use_id": block.id,
|
|
366
|
+
"content": tool_content,
|
|
367
|
+
})
|
|
368
|
+
if not tool_results:
|
|
369
|
+
if mode == "hitl":
|
|
370
|
+
log("no HITL action was needed")
|
|
371
|
+
return {"status": "hitl_checked", "action_count": actions_executed, "model": model}
|
|
372
|
+
raise RuntimeError("Computer Use planner stopped before submitting the Cowork query")
|
|
373
|
+
messages.append({"role": "user", "content": tool_results})
|
|
374
|
+
|
|
375
|
+
if mode == "hitl" and not hitl_action_taken:
|
|
376
|
+
log("no visible HITL prompt found within the bounded check")
|
|
377
|
+
return {
|
|
378
|
+
"status": "hitl_checked",
|
|
379
|
+
"action_count": actions_executed,
|
|
380
|
+
"prompt_found": False,
|
|
381
|
+
"model": model,
|
|
382
|
+
}
|
|
383
|
+
if mode == "hitl":
|
|
384
|
+
raise RuntimeError(
|
|
385
|
+
f"Computer Use HITL check exceeded {max_actions} actions after attempting a visible prompt"
|
|
386
|
+
)
|
|
387
|
+
raise RuntimeError(f"Computer Use submission exceeded {max_actions} actions without submitting")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def main() -> int:
|
|
391
|
+
parser = argparse.ArgumentParser()
|
|
392
|
+
parser.add_argument("query")
|
|
393
|
+
parser.add_argument("--max-actions", type=int, default=DEFAULT_MAX_ACTIONS)
|
|
394
|
+
parser.add_argument("--mode", choices=["submit", "hitl"], default="submit")
|
|
395
|
+
args = parser.parse_args()
|
|
396
|
+
try:
|
|
397
|
+
print(json.dumps(asyncio.run(run(args.query, args.max_actions, args.mode))), flush=True)
|
|
398
|
+
return 0
|
|
399
|
+
except Exception as error:
|
|
400
|
+
log(f"driver failed: {error}")
|
|
401
|
+
result = {"status": "failed", "error": str(error)}
|
|
402
|
+
if isinstance(error, ComputerUseDriverError):
|
|
403
|
+
result["telemetry"] = error.telemetry
|
|
404
|
+
print(json.dumps(result), flush=True)
|
|
405
|
+
return 1
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
sys.exit(main())
|