@gleanwork/mcp-server-tester 2.0.0-beta.0 → 2.0.0-beta.1

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.
@@ -165,6 +165,9 @@ interface HostEvent {
165
165
  server?: string;
166
166
  arguments?: Record<string, unknown>;
167
167
  output?: string;
168
+ /** Explicit tool-result error status, absent when not observed. */
169
+ isError?: boolean;
170
+ rawName?: string;
168
171
  id?: string;
169
172
  }
170
173
 
@@ -165,6 +165,9 @@ interface HostEvent {
165
165
  server?: string;
166
166
  arguments?: Record<string, unknown>;
167
167
  output?: string;
168
+ /** Explicit tool-result error status, absent when not observed. */
169
+ isError?: boolean;
170
+ rawName?: string;
168
171
  id?: string;
169
172
  }
170
173
 
@@ -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, aY as HostType, aZ as HttpMCPConfig, a_ as IterationResult, G as Judge, F as JudgeConfig, a$ as JudgeExpectConfig, J as JudgeMatcherOptions, b0 as JudgeResult, b1 as JudgeValidatorConfig, L as LLMProvider, b2 as LLMToolCall, b4 as LoadDatasetOptions, b5 as MCPAuthConfig, b6 as MCPAuthFixtures, b7 as MCPClientCredentialsConfig, p as MCPConfig, b9 as MCPConformanceCheck, ba as MCPConformanceOptions, bb as MCPConformanceRaw, bc as MCPConformanceResult, bd as MCPConformanceResultData, be as MCPEvalData, bf as MCPEvalHistoricalSummary, bg as MCPEvalReporterConfig, bh as MCPEvalRunData, M as MCPFixtureApi, bi as MCPFixtureOptions, bj as MCPHostCapabilities, a as MCPHostConfig, b as MCPHostSimulationResult, bk as MCPHostSimulator, bl as MCPOAuthConfig, bm as MCPServerCapabilitiesData, bp as NormalizedToolResponse, O as OAuthSetupConfig, P as PatternValidatorOptions, bs as PlaywrightOAuthClientProviderConfig, bt as PredicateResult, bu as ProposeVariantsContext, bv as ProtectedResourceDiscoveryResult, bw as ProtectedResourceMetadata, bx as ProviderKind, by as RegexSanitizer, bz as ResultSource, R as RubricSpec, bC as SaveBaselineOptions, bD as SaveEvalRunComparisonOptions, bE as SaveServerComparisonOptions, bF as SchemaRegistry, S as SchemaValidatorOptions, bG as SerializedEvalDataset, bH as ServerComparisonOptions, bI as ServerComparisonResult, c as SizeValidatorOptions, d as SnapshotSanitizer, bJ as SnapshotSanitizers, bK as StdioMCPConfig, bN as StoredClientInfo, bQ as StoredEvalResultLoadOptions, bR as StoredEvalResultRef, bS as StoredEvalResultSaveOptions, bT as StoredEvalRunRef, bU as StoredOAuthState, bV as StoredServerMetadata, bW as StoredTokens, T as TextValidatorOptions, bY as TokenResult, g as ToolCallCountOptions, f as ToolCallExpectation, bZ as ToolMetadataOverride, b_ as ToolOverrideVariant, e as ToolPredicate, c0 as UsageMetrics, V as ValidationResult, c1 as VariantCandidateResult, c2 as VariantExperimentOptions, c3 as VariantExperimentReason, c4 as VariantExperimentResult, c5 as VariantExperimentRound, c6 as VariantImprovementProposal, c7 as VariantRecommendation } from '../index-aX_tCfTD.cjs';
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';
@@ -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, aY as HostType, aZ as HttpMCPConfig, a_ as IterationResult, G as Judge, F as JudgeConfig, a$ as JudgeExpectConfig, J as JudgeMatcherOptions, b0 as JudgeResult, b1 as JudgeValidatorConfig, L as LLMProvider, b2 as LLMToolCall, b4 as LoadDatasetOptions, b5 as MCPAuthConfig, b6 as MCPAuthFixtures, b7 as MCPClientCredentialsConfig, p as MCPConfig, b9 as MCPConformanceCheck, ba as MCPConformanceOptions, bb as MCPConformanceRaw, bc as MCPConformanceResult, bd as MCPConformanceResultData, be as MCPEvalData, bf as MCPEvalHistoricalSummary, bg as MCPEvalReporterConfig, bh as MCPEvalRunData, M as MCPFixtureApi, bi as MCPFixtureOptions, bj as MCPHostCapabilities, a as MCPHostConfig, b as MCPHostSimulationResult, bk as MCPHostSimulator, bl as MCPOAuthConfig, bm as MCPServerCapabilitiesData, bp as NormalizedToolResponse, O as OAuthSetupConfig, P as PatternValidatorOptions, bs as PlaywrightOAuthClientProviderConfig, bt as PredicateResult, bu as ProposeVariantsContext, bv as ProtectedResourceDiscoveryResult, bw as ProtectedResourceMetadata, bx as ProviderKind, by as RegexSanitizer, bz as ResultSource, R as RubricSpec, bC as SaveBaselineOptions, bD as SaveEvalRunComparisonOptions, bE as SaveServerComparisonOptions, bF as SchemaRegistry, S as SchemaValidatorOptions, bG as SerializedEvalDataset, bH as ServerComparisonOptions, bI as ServerComparisonResult, c as SizeValidatorOptions, d as SnapshotSanitizer, bJ as SnapshotSanitizers, bK as StdioMCPConfig, bN as StoredClientInfo, bQ as StoredEvalResultLoadOptions, bR as StoredEvalResultRef, bS as StoredEvalResultSaveOptions, bT as StoredEvalRunRef, bU as StoredOAuthState, bV as StoredServerMetadata, bW as StoredTokens, T as TextValidatorOptions, bY as TokenResult, g as ToolCallCountOptions, f as ToolCallExpectation, bZ as ToolMetadataOverride, b_ as ToolOverrideVariant, e as ToolPredicate, c0 as UsageMetrics, V as ValidationResult, c1 as VariantCandidateResult, c2 as VariantExperimentOptions, c3 as VariantExperimentReason, c4 as VariantExperimentResult, c5 as VariantExperimentRound, c6 as VariantImprovementProposal, c7 as VariantRecommendation } from '../index-aX_tCfTD.js';
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.0",
3
+ "version": "2.0.0-beta.1",
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,5 @@
1
+ anthropic==1.6.0
2
+ pyautogui==0.9.54
3
+ mss==10.2.0
4
+ Pillow==12.3.0
5
+ pyobjc-framework-Quartz==12.2.2; sys_platform == "darwin"
@@ -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())