@ai-agent-forge/plugin-desktop-macos-input 0.0.0-stage → 0.88.3

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/MANUAL.md ADDED
@@ -0,0 +1,16 @@
1
+ # desktop-macos-input 能力手册(AI 用)
2
+
3
+ macOS 桌面真实输入:CGEvent HID 级鼠标/键盘注入(与物理硬件事件同一队列)+ AX 元素动作。与 desktop-macos-observe 配对(observe 拿元素坐标 → execute → reobserve)。
4
+
5
+ ## 真实度(重要)
6
+
7
+ - **鼠标**:CGEventPost(kCGHIDEventTap)——用户态最接近真实硬件的档位,行为信号层面与物理鼠标同队列。
8
+ - **键盘**:System Events keystroke/keyCode——同为 HID 注入。
9
+ - 这是"反自动化检测/原生对话框/无 DOM 页面"场景的真实操作通道;CDP 系工具(Playwright)生成的事件与之不同源。
10
+
11
+ ## 使用要点
12
+
13
+ 1. **TCC**:辅助功能授权对象是 `/usr/bin/osascript`;-25201/-25211 让用户加白(见 observe 手册)。
14
+ 2. **坐标输入前先聚焦目标窗口**,执行期间用户碰键鼠会打断。
15
+ 3. 锁屏时注入无效(窗口不在 AX 树,事件落空)——先确认解锁。
16
+ 4. 打字中文等非 ASCII 文本走 AX set value 或剪贴板粘贴路径比逐键注入可靠;逐键注入适合 ASCII 与快捷键。
package/README.md CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @agent-forge/plugin-desktop-macos-input
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Bounded macOS desktop input capability plugin for [Agent Forge](https://github.com/agent-forge/agent-forge), structured as the macOS counterpart of
4
+ `plugins/desktop-windows-input`. It registers `desktop_execute` — focus, click, type, key, scroll — backed by the macOS Accessibility tree through a
5
+ JXA driver (`osascript -l JavaScript`). A readonly host mode registers no input tools at all, and the shipped facade deliberately exposes no replay
6
+ method, so `desktop_replay` never registers out of the box.
7
+
8
+ Action surface: click accepts observed control identities (AXPress with a CGEvent coordinate-click fallback) and — click-only — explicit
9
+ `coordinates` targets (integer, non-negative screen coordinates); key uses a minimal SendKeys-aligned grammar (`+` shift, `^` control, `%` option,
10
+ `{ENTER}`-style special keys) translated to System Events keystroke/keyCode; scroll posts CGEvent scroll-wheel events. High-risk actions (uac,
11
+ password, admin, payment, external-submit) stay behind the shared `requiresDesktopUserConfirmation` fail-closed gate and always return `needs_user`.
12
+ Every action re-observes so results carry fresh state.
13
+
14
+ On non-macOS platforms importing the package does not throw; every tool reports an explicit `unsupported` result. A missing Accessibility
15
+ (assistive access) grant is reported fail-closed as the probe reason `accessibility-permission-required`.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ # from a local checkout
21
+ agent-forge plugin install ./plugins/desktop-macos-input
22
+
23
+ # from npm
24
+ agent-forge plugin install @agent-forge/plugin-desktop-macos-input
25
+ ```
26
+
27
+ The package ships two manifests: `agent-forge.json` (ecosystem manifest for the deterministic plugin resolver, `platform.os: ["macos"]`) and
28
+ `plugin.json` (host-load manifest consumed by `loadCapabilityPluginFromManifest`, entry `./dist/entry.js`).
29
+
30
+ ## Library surface
31
+
32
+ `import { DesktopMacosInputAdapter, MacosAccessibilityDriver } from "@agent-forge/plugin-desktop-macos-input";`
33
+
34
+ - `DesktopMacosInputAdapter` — production input facade (no replay method).
35
+ - `MacosAccessibilityDriver(options?)` — JXA driver; supports `command`/`timeoutMs`/`spawnSync`/`platform` injection for brokers and tests.
36
+ - `createDesktopInputCapabilityFactoryV1(runtime)` — registers `desktop_execute` (plus `desktop_replay` only when the injected runtime exposes a
37
+ replay method) on a `CapabilityAPI`.
38
+ - Default export of `dist/entry.js` — the precomposed factory used by external plugin loading.
39
+
40
+ The session/observation/projection contract and the confirmation gate are deliberate inlined copies shared with
41
+ `@agent-forge/plugin-desktop-macos-observe` so both packages stay self-contained.
42
+
43
+ ## Real-host status
44
+
45
+ The driver and adapter are contract-tested with an injected fake `spawnSync` (canned JSON, no real desktop). Real darwin behavior — the JXA
46
+ enumeration and action scripts, Accessibility permission classification, keystroke/keyCode delivery and CGEvent clicks — is verified only by the
47
+ explicitly gated test (`AGENT_FORGE_MACOS_AX_INPUT_REAL_TEST=1` on macOS, scroll-only to stay reversible) and has not been validated on a real
48
+ macOS host as of this change.
49
+
50
+ ## Development
51
+
52
+ ```sh
53
+ npm install --ignore-scripts # repo root, creates the workspace symlink
54
+ npm run build # plugins/desktop-macos-input (tsgo)
55
+ npm test # vitest; the entry fidelity test requires `npm run build` first
56
+ ```
@@ -0,0 +1,31 @@
1
+ {
2
+ "packageFormatVersion": 1,
3
+ "id": "agent-forge.plugin.desktop-macos-input",
4
+ "pluginVersion": "0.88.3",
5
+ "apiVersion": "1",
6
+ "minHostVersion": "0.84.4",
7
+ "capabilityRefs": [
8
+ {
9
+ "id": "desktop-session",
10
+ "version": 1
11
+ },
12
+ {
13
+ "id": "desktop-uia",
14
+ "version": 1
15
+ },
16
+ {
17
+ "id": "desktop-input",
18
+ "version": 1
19
+ }
20
+ ],
21
+ "platform": {
22
+ "os": [
23
+ "macos"
24
+ ],
25
+ "runtime": [
26
+ {
27
+ "name": "node"
28
+ }
29
+ ]
30
+ }
31
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Bounded desktop input capability factory, mirroring the Windows package's
3
+ * `capability.ts` (migrated from agent-forge
4
+ * `src/profile/runtime-capabilities-desktop-input.ts`,
5
+ * `desktopInputFactory`, D-075 S3 extraction). Differences from the Windows
6
+ * copy:
7
+ * - The runtime type is the macOS full-control adapter from `types.ts`.
8
+ * - The target union gains the `coordinates` variant (`x`/`y` integer,
9
+ * non-negative screen coordinates); the coordinate target is accepted for
10
+ * the `click` action only — any other action kind with a coordinate target
11
+ * is rejected as `desktop_input_invalid` (same ruling the Windows package
12
+ * implements for its coordinate click).
13
+ * Tool names, parameters, budgets, result shapes, the readonly-mode refusal
14
+ * to register input tools, and the optional replay registration when the
15
+ * runtime exposes a replay method are unchanged; the shipped macOS facade
16
+ * exposes no replay, so only `desktop_execute` registers out of the box.
17
+ */
18
+ import type { CapabilityPluginSyncFactory } from "@agent-forge/plugin-sdk";
19
+ import type { DesktopFullControlAdapterV1 } from "./types.ts";
20
+ export declare function createDesktopInputCapabilityFactoryV1(runtime: DesktopFullControlAdapterV1): CapabilityPluginSyncFactory;
21
+ //# sourceMappingURL=capability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAc3E,OAAO,KAAK,EACX,2BAA2B,EAI3B,MAAM,YAAY,CAAC;AA6TpB,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,2BAA2B,GAClC,2BAA2B,CA0N7B","sourcesContent":["/**\n * Bounded desktop input capability factory, mirroring the Windows package's\n * `capability.ts` (migrated from agent-forge\n * `src/profile/runtime-capabilities-desktop-input.ts`,\n * `desktopInputFactory`, D-075 S3 extraction). Differences from the Windows\n * copy:\n * - The runtime type is the macOS full-control adapter from `types.ts`.\n * - The target union gains the `coordinates` variant (`x`/`y` integer,\n * non-negative screen coordinates); the coordinate target is accepted for\n * the `click` action only — any other action kind with a coordinate target\n * is rejected as `desktop_input_invalid` (same ruling the Windows package\n * implements for its coordinate click).\n * Tool names, parameters, budgets, result shapes, the readonly-mode refusal\n * to register input tools, and the optional replay registration when the\n * runtime exposes a replay method are unchanged; the shipped macOS facade\n * exposes no replay, so only `desktop_execute` registers out of the box.\n */\nimport type { CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport {\n\tDESKTOP_MAX_POSTCONDITIONS_V1,\n\tDESKTOP_MAX_REPLAY_ACTIONS_V1,\n\tDESKTOP_MAX_STRING_CHARS_V1,\n\tDESKTOP_MAX_TYPE_TEXT_CHARS_V1,\n\tdesktopBoundedString,\n\tdesktopFailure,\n\tdesktopRequiredString,\n\tdesktopResultWithinBudget,\n\tprojectDesktopObservation,\n} from \"./projection.ts\";\nimport { signalFrom, toolInvocation } from \"./shared.ts\";\nimport type {\n\tDesktopFullControlAdapterV1,\n\tDesktopOperationRequestV1,\n\tDesktopPostconditionV1,\n\tDesktopReplayRequestV1,\n} from \"./types.ts\";\n\nfunction desktopCoordinates(input: Record<string, unknown>): { readonly x: number; readonly y: number } {\n\tconst x = input.x;\n\tconst y = input.y;\n\tif (typeof x !== \"number\" || typeof y !== \"number\" || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0)\n\t\tthrow new Error(\"desktop-target-invalid\");\n\treturn { x, y };\n}\n\nfunction desktopTarget(input: unknown): DesktopOperationRequestV1[\"action\"][\"target\"] {\n\tif (input === null || typeof input !== \"object\" || Array.isArray(input)) throw new Error(\"desktop-target-invalid\");\n\tconst target = input as Record<string, unknown>;\n\tif (target.kind === \"window\") {\n\t\tdesktopExactKeys(target, [\"kind\", \"windowId\"]);\n\t\treturn { kind: \"window\", windowId: desktopRequiredString(target.windowId, \"windowId\") };\n\t}\n\tif (target.kind === \"control\") {\n\t\tdesktopExactKeys(target, [\"kind\", \"controlId\", \"windowId\"]);\n\t\treturn {\n\t\t\tkind: \"control\",\n\t\t\tcontrolId: desktopRequiredString(target.controlId, \"controlId\"),\n\t\t\t...(target.windowId === undefined ? {} : { windowId: desktopRequiredString(target.windowId, \"windowId\") }),\n\t\t};\n\t}\n\tif (target.kind === \"coordinates\") {\n\t\tdesktopExactKeys(target, [\"kind\", \"x\", \"y\"]);\n\t\treturn { kind: \"coordinates\", ...desktopCoordinates(target) };\n\t}\n\tthrow new Error(\"desktop-target-invalid\");\n}\n\nfunction desktopExactKeys(value: Record<string, unknown>, allowed: readonly string[]): void {\n\tif (Object.keys(value).some((key) => !allowed.includes(key))) throw new Error(\"desktop-input-invalid\");\n}\n\nfunction desktopPostconditions(input: unknown): readonly DesktopPostconditionV1[] | undefined {\n\tif (input === undefined) return undefined;\n\tif (!Array.isArray(input) || input.length > DESKTOP_MAX_POSTCONDITIONS_V1)\n\t\tthrow new Error(\"desktop-postconditions-invalid\");\n\treturn input.map((entry) => {\n\t\tif (entry === null || typeof entry !== \"object\" || Array.isArray(entry))\n\t\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t\tconst value = entry as Record<string, unknown>;\n\t\tif (value.kind === \"window-exists\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"windowId\"]);\n\t\t\treturn { kind: \"window-exists\", windowId: desktopRequiredString(value.windowId, \"windowId\") } as const;\n\t\t}\n\t\tif (value.kind === \"control-exists\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"controlId\"]);\n\t\t\treturn { kind: \"control-exists\", controlId: desktopRequiredString(value.controlId, \"controlId\") } as const;\n\t\t}\n\t\tif (value.kind === \"focused\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"controlId\"]);\n\t\t\treturn { kind: \"focused\", controlId: desktopRequiredString(value.controlId, \"controlId\") } as const;\n\t\t}\n\t\tif (value.kind === \"property-equals\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"target\", \"property\", \"expected\"]);\n\t\t\tif (value.property !== \"enabled\" && value.property !== \"value\" && value.property !== \"name\")\n\t\t\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t\t\tif (\n\t\t\t\t(value.property === \"enabled\" && typeof value.expected !== \"boolean\") ||\n\t\t\t\t(value.property !== \"enabled\" && typeof value.expected !== \"string\")\n\t\t\t)\n\t\t\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t\t\treturn {\n\t\t\t\tkind: \"property-equals\",\n\t\t\t\ttarget: desktopTarget(value.target),\n\t\t\t\tproperty: value.property,\n\t\t\t\texpected:\n\t\t\t\t\ttypeof value.expected === \"string\"\n\t\t\t\t\t\t? desktopBoundedString(value.expected, \"postcondition expected\")\n\t\t\t\t\t\t: value.expected,\n\t\t\t} as DesktopPostconditionV1;\n\t\t}\n\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t});\n}\n\nfunction desktopExecuteRequest(input: Record<string, unknown>, operationId: string): DesktopOperationRequestV1 {\n\tdesktopExactKeys(input, [\"sessionId\", \"action\", \"postconditions\"]);\n\tconst actionInput = input.action;\n\tif (actionInput === null || typeof actionInput !== \"object\" || Array.isArray(actionInput))\n\t\tthrow new Error(\"desktop-action-invalid\");\n\tconst action = actionInput as Record<string, unknown>;\n\tdesktopExactKeys(action, [\"kind\", \"target\", \"text\", \"key\"]);\n\tif (\n\t\taction.kind !== \"focus\" &&\n\t\taction.kind !== \"click\" &&\n\t\taction.kind !== \"type\" &&\n\t\taction.kind !== \"key\" &&\n\t\taction.kind !== \"scroll\"\n\t)\n\t\tthrow new Error(\"desktop-action-invalid\");\n\tconst target = desktopTarget(action.target);\n\tif (target === undefined) throw new Error(\"desktop-target-invalid\");\n\t// Coordinate targets are a click-only affordance: pointing elsewhere is a\n\t// caller contract violation, not a fallback path.\n\tif (target.kind === \"coordinates\" && action.kind !== \"click\") throw new Error(\"desktop-coordinates-click-only\");\n\tconst text =\n\t\taction.kind === \"type\" || action.kind === \"scroll\" ? desktopBoundedString(action.text, \"text\") : undefined;\n\tif (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\tconst key = action.kind === \"key\" ? desktopBoundedString(action.key, \"key\") : undefined;\n\tif (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\tconst postconditions = desktopPostconditions(input.postconditions);\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tsessionId: desktopRequiredString(input.sessionId, \"sessionId\"),\n\t\taction: {\n\t\t\tversion: 1,\n\t\t\tkind: action.kind,\n\t\t\ttarget,\n\t\t\t...(text === undefined ? {} : { text }),\n\t\t\t...(key === undefined ? {} : { key }),\n\t\t},\n\t\t...(postconditions === undefined ? {} : { postconditions }),\n\t\t// Input plugin operations always request a fresh observation; callers may\n\t\t// not turn this off because target identity otherwise goes stale.\n\t\treobserveAfterAction: true,\n\t};\n}\n\nfunction desktopVerification(\n\tvalue: unknown,\n): { readonly status: \"passed\" | \"failed\" | \"unknown\"; readonly checks: readonly string[] } | undefined {\n\tif (value === undefined) return undefined;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"desktop-result-invalid\");\n\tconst verification = value as Record<string, unknown>;\n\tif (\n\t\t(verification.status !== \"passed\" && verification.status !== \"failed\" && verification.status !== \"unknown\") ||\n\t\t!Array.isArray(verification.checks) ||\n\t\tverification.checks.length > DESKTOP_MAX_POSTCONDITIONS_V1\n\t)\n\t\tthrow new Error(\"desktop-result-invalid\");\n\treturn {\n\t\tstatus: verification.status,\n\t\tchecks: verification.checks.map((check) => desktopBoundedString(check, \"verification check\")),\n\t};\n}\n\nfunction desktopExecuteResult(value: unknown, request: DesktopOperationRequestV1) {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"desktop-result-invalid\");\n\tconst result = value as Record<string, unknown>;\n\tif (\n\t\tresult.version !== 1 ||\n\t\tresult.operationId !== request.operationId ||\n\t\t(result.status !== \"succeeded\" &&\n\t\t\tresult.status !== \"failed\" &&\n\t\t\tresult.status !== \"cancelled\" &&\n\t\t\tresult.status !== \"needs_user\" &&\n\t\t\tresult.status !== \"unsupported\")\n\t)\n\t\tthrow new Error(\"desktop-result-invalid\");\n\tconst observation = result.observation === undefined ? undefined : projectDesktopObservation(result.observation);\n\tif (\n\t\tresult.status === \"succeeded\" &&\n\t\t(observation === undefined || observation.observation.sessionId !== request.sessionId)\n\t) {\n\t\treturn desktopFailure(\n\t\t\trequest.operationId,\n\t\t\t\"verification_unavailable\",\n\t\t\t\"Desktop action did not produce a fresh observation\",\n\t\t);\n\t}\n\tconst base = {\n\t\tversion: 1 as const,\n\t\toperationId: request.operationId,\n\t\tstatus: result.status,\n\t\tsummary: result.status === \"succeeded\" ? \"Desktop action completed\" : \"Desktop action could not be completed\",\n\t\t...(observation === undefined ? {} : observation),\n\t\t...(desktopVerification(result.verification) === undefined\n\t\t\t? {}\n\t\t\t: { verification: desktopVerification(result.verification) }),\n\t};\n\tif (result.status === \"succeeded\") return desktopResultWithinBudget(base);\n\tconst error = result.error;\n\tif (error === null || typeof error !== \"object\" || Array.isArray(error)) throw new Error(\"desktop-result-invalid\");\n\tconst errorValue = error as Record<string, unknown>;\n\tconst causeCode =\n\t\tresult.status === \"needs_user\" && typeof errorValue.causeCode === \"string\"\n\t\t\t? desktopBoundedString(errorValue.causeCode, \"error causeCode\")\n\t\t\t: undefined;\n\treturn desktopResultWithinBudget({\n\t\t...base,\n\t\terror: {\n\t\t\tversion: 1 as const,\n\t\t\tcode: result.status === \"needs_user\" ? \"needs_user\" : \"desktop_action_failed\",\n\t\t\tmessage: result.status === \"needs_user\" ? \"Desktop action requires user action\" : \"Desktop action failed\",\n\t\t\t...(causeCode === undefined ? {} : { causeCode }),\n\t\t},\n\t});\n}\n\nfunction desktopReplayActionInput(raw: unknown): DesktopOperationRequestV1[\"action\"] {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) throw new Error(\"desktop-action-invalid\");\n\tconst action = raw as Record<string, unknown>;\n\tdesktopExactKeys(action, [\"kind\", \"target\", \"text\", \"key\"]);\n\tif (\n\t\taction.kind !== \"focus\" &&\n\t\taction.kind !== \"click\" &&\n\t\taction.kind !== \"type\" &&\n\t\taction.kind !== \"key\" &&\n\t\taction.kind !== \"scroll\"\n\t)\n\t\tthrow new Error(\"desktop-action-invalid\");\n\tconst target = desktopTarget(action.target);\n\tif (target === undefined) throw new Error(\"desktop-target-invalid\");\n\tif (target.kind === \"coordinates\" && action.kind !== \"click\") throw new Error(\"desktop-coordinates-click-only\");\n\tconst text =\n\t\taction.kind === \"type\" || action.kind === \"scroll\" ? desktopBoundedString(action.text, \"text\") : undefined;\n\tif (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\tconst key = action.kind === \"key\" ? desktopBoundedString(action.key, \"key\") : undefined;\n\tif (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\treturn {\n\t\tversion: 1,\n\t\tkind: action.kind,\n\t\ttarget,\n\t\t...(text === undefined ? {} : { text }),\n\t\t...(key === undefined ? {} : { key }),\n\t};\n}\n\nfunction desktopReplayRequest(input: Record<string, unknown>, operationId: string): DesktopReplayRequestV1 {\n\tdesktopExactKeys(input, [\"sessionId\", \"actions\", \"stopOnFailure\"]);\n\tif (\n\t\t!Array.isArray(input.actions) ||\n\t\tinput.actions.length === 0 ||\n\t\tinput.actions.length > DESKTOP_MAX_REPLAY_ACTIONS_V1\n\t)\n\t\tthrow new Error(\"desktop-replay-actions-invalid\");\n\tconst sessionId = desktopRequiredString(input.sessionId, \"sessionId\");\n\tconst operations = input.actions.map((raw, index) => {\n\t\tconst action = desktopReplayActionInput(raw);\n\t\treturn {\n\t\t\tversion: 1 as const,\n\t\t\toperationId: `${operationId}-${index}`,\n\t\t\tsessionId,\n\t\t\taction,\n\t\t\t// Replay always re-observes so per-action results carry fresh state.\n\t\t\treobserveAfterAction: true,\n\t\t} satisfies DesktopOperationRequestV1;\n\t});\n\treturn {\n\t\tversion: 1,\n\t\tsessionId,\n\t\toperations,\n\t\tstopOnFailure: input.stopOnFailure === undefined ? true : input.stopOnFailure === true,\n\t};\n}\n\nfunction desktopReplayResult(value: unknown, operationId: string): unknown {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"desktop-result-invalid\");\n\tconst result = value as Record<string, unknown>;\n\tif (\n\t\tresult.version !== 1 ||\n\t\t(result.status !== \"succeeded\" &&\n\t\t\tresult.status !== \"failed\" &&\n\t\t\tresult.status !== \"cancelled\" &&\n\t\t\tresult.status !== \"needs_user\") ||\n\t\t!Array.isArray(result.results)\n\t)\n\t\tthrow new Error(\"desktop-result-invalid\");\n\tconst results = result.results.map((entry) => {\n\t\tif (entry === null || typeof entry !== \"object\" || Array.isArray(entry))\n\t\t\tthrow new Error(\"desktop-result-invalid\");\n\t\tconst record = entry as Record<string, unknown>;\n\t\tif (\n\t\t\trecord.version !== 1 ||\n\t\t\ttypeof record.operationId !== \"string\" ||\n\t\t\t(record.status !== \"succeeded\" &&\n\t\t\t\trecord.status !== \"failed\" &&\n\t\t\t\trecord.status !== \"cancelled\" &&\n\t\t\t\trecord.status !== \"needs_user\" &&\n\t\t\t\trecord.status !== \"unsupported\")\n\t\t)\n\t\t\tthrow new Error(\"desktop-result-invalid\");\n\t\treturn {\n\t\t\toperationId: desktopBoundedString(record.operationId, \"replay operationId\"),\n\t\t\tstatus: record.status,\n\t\t\tsummary: desktopBoundedString(record.summary, \"replay summary\"),\n\t\t};\n\t});\n\tconst last = result.results[result.results.length - 1] as Record<string, unknown> | undefined;\n\tconst observation =\n\t\tlast?.observation === undefined ? undefined : projectDesktopObservation(last.observation).observation;\n\tconst needsUser = result.results.find((entry) => (entry as Record<string, unknown>).status === \"needs_user\") as\n\t\t| Record<string, unknown>\n\t\t| undefined;\n\tconst causeCode =\n\t\tneedsUser !== undefined &&\n\t\tneedsUser.error !== null &&\n\t\ttypeof needsUser.error === \"object\" &&\n\t\t\"causeCode\" in needsUser.error &&\n\t\ttypeof (needsUser.error as Record<string, unknown>).causeCode === \"string\"\n\t\t\t? desktopBoundedString((needsUser.error as Record<string, unknown>).causeCode, \"replay causeCode\")\n\t\t\t: undefined;\n\treturn desktopResultWithinBudget({\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: result.status,\n\t\tsummary: result.status === \"succeeded\" ? \"Desktop replay completed\" : \"Desktop replay stopped before completion\",\n\t\tresults,\n\t\t...(observation === undefined ? {} : { observation }),\n\t\t...(result.status === \"needs_user\" && causeCode !== undefined\n\t\t\t? {\n\t\t\t\t\terror: {\n\t\t\t\t\t\tversion: 1 as const,\n\t\t\t\t\t\tcode: \"needs_user\",\n\t\t\t\t\t\tmessage: \"Desktop replay requires user action\",\n\t\t\t\t\t\tcauseCode,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {}),\n\t});\n}\n\nexport function createDesktopInputCapabilityFactoryV1(\n\truntime: DesktopFullControlAdapterV1,\n): CapabilityPluginSyncFactory {\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"desktop_execute\",\n\t\t\tlabel: \"Execute desktop action\",\n\t\t\tdescription:\n\t\t\t\t\"Execute one bounded Accessibility focus, click, type, key, or scroll action against an observed target (click also accepts screen coordinates) and return a fresh bounded observation\",\n\t\t\tparameters: Type.Object(\n\t\t\t\t{\n\t\t\t\t\tsessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\taction: Type.Object(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkind: Type.Union([\n\t\t\t\t\t\t\t\tType.Literal(\"focus\"),\n\t\t\t\t\t\t\t\tType.Literal(\"click\"),\n\t\t\t\t\t\t\t\tType.Literal(\"type\"),\n\t\t\t\t\t\t\t\tType.Literal(\"key\"),\n\t\t\t\t\t\t\t\tType.Literal(\"scroll\"),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\ttarget: Type.Union([\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window\"),\n\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control\"),\n\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\twindowId: Type.Optional(\n\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"coordinates\"),\n\t\t\t\t\t\t\t\t\t\tx: Type.Integer({ minimum: 0 }),\n\t\t\t\t\t\t\t\t\t\ty: Type.Integer({ minimum: 0 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\ttext: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t\tkey: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t),\n\t\t\t\t\tpostconditions: Type.Optional(\n\t\t\t\t\t\tType.Array(\n\t\t\t\t\t\t\tType.Union([\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window-exists\"),\n\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control-exists\"),\n\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"focused\"),\n\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"property-equals\"),\n\t\t\t\t\t\t\t\t\t\ttarget: Type.Union([\n\t\t\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window\"),\n\t\t\t\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control\"),\n\t\t\t\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t\t\twindowId: Type.Optional(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\t\tproperty: Type.Union([\n\t\t\t\t\t\t\t\t\t\t\tType.Literal(\"enabled\"),\n\t\t\t\t\t\t\t\t\t\t\tType.Literal(\"value\"),\n\t\t\t\t\t\t\t\t\t\t\tType.Literal(\"name\"),\n\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\t\texpected: Type.Union([\n\t\t\t\t\t\t\t\t\t\t\tType.Boolean(),\n\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t{ maxItems: DESKTOP_MAX_POSTCONDITIONS_V1 },\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{ additionalProperties: false },\n\t\t\t),\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\tconst signal = signalFrom(third);\n\t\t\t\tif (signal?.aborted)\n\t\t\t\t\treturn desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\");\n\t\t\t\tlet request: DesktopOperationRequestV1;\n\t\t\t\ttry {\n\t\t\t\t\trequest = desktopExecuteRequest(input, operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_input_invalid\", \"Desktop action input is invalid\");\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = runtime.execute(request);\n\t\t\t\t\treturn signal?.aborted\n\t\t\t\t\t\t? desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\")\n\t\t\t\t\t\t: desktopExecuteResult(result, request);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_adapter_failed\", \"The selected desktop adapter failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst replay = runtime.replay;\n\t\tif (replay === undefined) return;\n\t\tapi.registerTool({\n\t\t\tname: \"desktop_replay\",\n\t\t\tlabel: \"Replay desktop actions\",\n\t\t\tdescription:\n\t\t\t\t\"Replay a bounded sequence of desktop actions against one session; stops at the first failure, cancellation, or user-confirmation pause by default\",\n\t\t\tparameters: Type.Object(\n\t\t\t\t{\n\t\t\t\t\tsessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\tactions: Type.Array(\n\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tkind: Type.Union([\n\t\t\t\t\t\t\t\t\tType.Literal(\"focus\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"click\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"type\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"key\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"scroll\"),\n\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\ttarget: Type.Union([\n\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window\"),\n\t\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control\"),\n\t\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\twindowId: Type.Optional(\n\t\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"coordinates\"),\n\t\t\t\t\t\t\t\t\t\t\tx: Type.Integer({ minimum: 0 }),\n\t\t\t\t\t\t\t\t\t\t\ty: Type.Integer({ minimum: 0 }),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\ttext: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t\t\tkey: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t),\n\t\t\t\t\t\t{ minItems: 1, maxItems: DESKTOP_MAX_REPLAY_ACTIONS_V1 },\n\t\t\t\t\t),\n\t\t\t\t\tstopOnFailure: Type.Optional(Type.Boolean()),\n\t\t\t\t},\n\t\t\t\t{ additionalProperties: false },\n\t\t\t),\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\tconst signal = signalFrom(third);\n\t\t\t\tif (signal?.aborted)\n\t\t\t\t\treturn desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\");\n\t\t\t\tlet request: DesktopReplayRequestV1;\n\t\t\t\ttry {\n\t\t\t\t\trequest = desktopReplayRequest(input, operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_input_invalid\", \"Desktop replay input is invalid\");\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = replay(request);\n\t\t\t\t\treturn signal?.aborted\n\t\t\t\t\t\t? desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\")\n\t\t\t\t\t\t: desktopReplayResult(result, operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_adapter_failed\", \"The selected desktop adapter failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n"]}
@@ -0,0 +1,456 @@
1
+ import { Type } from "typebox";
2
+ import { DESKTOP_MAX_POSTCONDITIONS_V1, DESKTOP_MAX_REPLAY_ACTIONS_V1, DESKTOP_MAX_STRING_CHARS_V1, DESKTOP_MAX_TYPE_TEXT_CHARS_V1, desktopBoundedString, desktopFailure, desktopRequiredString, desktopResultWithinBudget, projectDesktopObservation, } from "./projection.js";
3
+ import { signalFrom, toolInvocation } from "./shared.js";
4
+ function desktopCoordinates(input) {
5
+ const x = input.x;
6
+ const y = input.y;
7
+ if (typeof x !== "number" || typeof y !== "number" || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0)
8
+ throw new Error("desktop-target-invalid");
9
+ return { x, y };
10
+ }
11
+ function desktopTarget(input) {
12
+ if (input === null || typeof input !== "object" || Array.isArray(input))
13
+ throw new Error("desktop-target-invalid");
14
+ const target = input;
15
+ if (target.kind === "window") {
16
+ desktopExactKeys(target, ["kind", "windowId"]);
17
+ return { kind: "window", windowId: desktopRequiredString(target.windowId, "windowId") };
18
+ }
19
+ if (target.kind === "control") {
20
+ desktopExactKeys(target, ["kind", "controlId", "windowId"]);
21
+ return {
22
+ kind: "control",
23
+ controlId: desktopRequiredString(target.controlId, "controlId"),
24
+ ...(target.windowId === undefined ? {} : { windowId: desktopRequiredString(target.windowId, "windowId") }),
25
+ };
26
+ }
27
+ if (target.kind === "coordinates") {
28
+ desktopExactKeys(target, ["kind", "x", "y"]);
29
+ return { kind: "coordinates", ...desktopCoordinates(target) };
30
+ }
31
+ throw new Error("desktop-target-invalid");
32
+ }
33
+ function desktopExactKeys(value, allowed) {
34
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
35
+ throw new Error("desktop-input-invalid");
36
+ }
37
+ function desktopPostconditions(input) {
38
+ if (input === undefined)
39
+ return undefined;
40
+ if (!Array.isArray(input) || input.length > DESKTOP_MAX_POSTCONDITIONS_V1)
41
+ throw new Error("desktop-postconditions-invalid");
42
+ return input.map((entry) => {
43
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
44
+ throw new Error("desktop-postcondition-invalid");
45
+ const value = entry;
46
+ if (value.kind === "window-exists") {
47
+ desktopExactKeys(value, ["kind", "windowId"]);
48
+ return { kind: "window-exists", windowId: desktopRequiredString(value.windowId, "windowId") };
49
+ }
50
+ if (value.kind === "control-exists") {
51
+ desktopExactKeys(value, ["kind", "controlId"]);
52
+ return { kind: "control-exists", controlId: desktopRequiredString(value.controlId, "controlId") };
53
+ }
54
+ if (value.kind === "focused") {
55
+ desktopExactKeys(value, ["kind", "controlId"]);
56
+ return { kind: "focused", controlId: desktopRequiredString(value.controlId, "controlId") };
57
+ }
58
+ if (value.kind === "property-equals") {
59
+ desktopExactKeys(value, ["kind", "target", "property", "expected"]);
60
+ if (value.property !== "enabled" && value.property !== "value" && value.property !== "name")
61
+ throw new Error("desktop-postcondition-invalid");
62
+ if ((value.property === "enabled" && typeof value.expected !== "boolean") ||
63
+ (value.property !== "enabled" && typeof value.expected !== "string"))
64
+ throw new Error("desktop-postcondition-invalid");
65
+ return {
66
+ kind: "property-equals",
67
+ target: desktopTarget(value.target),
68
+ property: value.property,
69
+ expected: typeof value.expected === "string"
70
+ ? desktopBoundedString(value.expected, "postcondition expected")
71
+ : value.expected,
72
+ };
73
+ }
74
+ throw new Error("desktop-postcondition-invalid");
75
+ });
76
+ }
77
+ function desktopExecuteRequest(input, operationId) {
78
+ desktopExactKeys(input, ["sessionId", "action", "postconditions"]);
79
+ const actionInput = input.action;
80
+ if (actionInput === null || typeof actionInput !== "object" || Array.isArray(actionInput))
81
+ throw new Error("desktop-action-invalid");
82
+ const action = actionInput;
83
+ desktopExactKeys(action, ["kind", "target", "text", "key"]);
84
+ if (action.kind !== "focus" &&
85
+ action.kind !== "click" &&
86
+ action.kind !== "type" &&
87
+ action.kind !== "key" &&
88
+ action.kind !== "scroll")
89
+ throw new Error("desktop-action-invalid");
90
+ const target = desktopTarget(action.target);
91
+ if (target === undefined)
92
+ throw new Error("desktop-target-invalid");
93
+ // Coordinate targets are a click-only affordance: pointing elsewhere is a
94
+ // caller contract violation, not a fallback path.
95
+ if (target.kind === "coordinates" && action.kind !== "click")
96
+ throw new Error("desktop-coordinates-click-only");
97
+ const text = action.kind === "type" || action.kind === "scroll" ? desktopBoundedString(action.text, "text") : undefined;
98
+ if (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
99
+ throw new Error("desktop-type-text-budget");
100
+ const key = action.kind === "key" ? desktopBoundedString(action.key, "key") : undefined;
101
+ if (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
102
+ throw new Error("desktop-type-text-budget");
103
+ const postconditions = desktopPostconditions(input.postconditions);
104
+ return {
105
+ version: 1,
106
+ operationId,
107
+ sessionId: desktopRequiredString(input.sessionId, "sessionId"),
108
+ action: {
109
+ version: 1,
110
+ kind: action.kind,
111
+ target,
112
+ ...(text === undefined ? {} : { text }),
113
+ ...(key === undefined ? {} : { key }),
114
+ },
115
+ ...(postconditions === undefined ? {} : { postconditions }),
116
+ // Input plugin operations always request a fresh observation; callers may
117
+ // not turn this off because target identity otherwise goes stale.
118
+ reobserveAfterAction: true,
119
+ };
120
+ }
121
+ function desktopVerification(value) {
122
+ if (value === undefined)
123
+ return undefined;
124
+ if (value === null || typeof value !== "object" || Array.isArray(value))
125
+ throw new Error("desktop-result-invalid");
126
+ const verification = value;
127
+ if ((verification.status !== "passed" && verification.status !== "failed" && verification.status !== "unknown") ||
128
+ !Array.isArray(verification.checks) ||
129
+ verification.checks.length > DESKTOP_MAX_POSTCONDITIONS_V1)
130
+ throw new Error("desktop-result-invalid");
131
+ return {
132
+ status: verification.status,
133
+ checks: verification.checks.map((check) => desktopBoundedString(check, "verification check")),
134
+ };
135
+ }
136
+ function desktopExecuteResult(value, request) {
137
+ if (value === null || typeof value !== "object" || Array.isArray(value))
138
+ throw new Error("desktop-result-invalid");
139
+ const result = value;
140
+ if (result.version !== 1 ||
141
+ result.operationId !== request.operationId ||
142
+ (result.status !== "succeeded" &&
143
+ result.status !== "failed" &&
144
+ result.status !== "cancelled" &&
145
+ result.status !== "needs_user" &&
146
+ result.status !== "unsupported"))
147
+ throw new Error("desktop-result-invalid");
148
+ const observation = result.observation === undefined ? undefined : projectDesktopObservation(result.observation);
149
+ if (result.status === "succeeded" &&
150
+ (observation === undefined || observation.observation.sessionId !== request.sessionId)) {
151
+ return desktopFailure(request.operationId, "verification_unavailable", "Desktop action did not produce a fresh observation");
152
+ }
153
+ const base = {
154
+ version: 1,
155
+ operationId: request.operationId,
156
+ status: result.status,
157
+ summary: result.status === "succeeded" ? "Desktop action completed" : "Desktop action could not be completed",
158
+ ...(observation === undefined ? {} : observation),
159
+ ...(desktopVerification(result.verification) === undefined
160
+ ? {}
161
+ : { verification: desktopVerification(result.verification) }),
162
+ };
163
+ if (result.status === "succeeded")
164
+ return desktopResultWithinBudget(base);
165
+ const error = result.error;
166
+ if (error === null || typeof error !== "object" || Array.isArray(error))
167
+ throw new Error("desktop-result-invalid");
168
+ const errorValue = error;
169
+ const causeCode = result.status === "needs_user" && typeof errorValue.causeCode === "string"
170
+ ? desktopBoundedString(errorValue.causeCode, "error causeCode")
171
+ : undefined;
172
+ return desktopResultWithinBudget({
173
+ ...base,
174
+ error: {
175
+ version: 1,
176
+ code: result.status === "needs_user" ? "needs_user" : "desktop_action_failed",
177
+ message: result.status === "needs_user" ? "Desktop action requires user action" : "Desktop action failed",
178
+ ...(causeCode === undefined ? {} : { causeCode }),
179
+ },
180
+ });
181
+ }
182
+ function desktopReplayActionInput(raw) {
183
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
184
+ throw new Error("desktop-action-invalid");
185
+ const action = raw;
186
+ desktopExactKeys(action, ["kind", "target", "text", "key"]);
187
+ if (action.kind !== "focus" &&
188
+ action.kind !== "click" &&
189
+ action.kind !== "type" &&
190
+ action.kind !== "key" &&
191
+ action.kind !== "scroll")
192
+ throw new Error("desktop-action-invalid");
193
+ const target = desktopTarget(action.target);
194
+ if (target === undefined)
195
+ throw new Error("desktop-target-invalid");
196
+ if (target.kind === "coordinates" && action.kind !== "click")
197
+ throw new Error("desktop-coordinates-click-only");
198
+ const text = action.kind === "type" || action.kind === "scroll" ? desktopBoundedString(action.text, "text") : undefined;
199
+ if (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
200
+ throw new Error("desktop-type-text-budget");
201
+ const key = action.kind === "key" ? desktopBoundedString(action.key, "key") : undefined;
202
+ if (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
203
+ throw new Error("desktop-type-text-budget");
204
+ return {
205
+ version: 1,
206
+ kind: action.kind,
207
+ target,
208
+ ...(text === undefined ? {} : { text }),
209
+ ...(key === undefined ? {} : { key }),
210
+ };
211
+ }
212
+ function desktopReplayRequest(input, operationId) {
213
+ desktopExactKeys(input, ["sessionId", "actions", "stopOnFailure"]);
214
+ if (!Array.isArray(input.actions) ||
215
+ input.actions.length === 0 ||
216
+ input.actions.length > DESKTOP_MAX_REPLAY_ACTIONS_V1)
217
+ throw new Error("desktop-replay-actions-invalid");
218
+ const sessionId = desktopRequiredString(input.sessionId, "sessionId");
219
+ const operations = input.actions.map((raw, index) => {
220
+ const action = desktopReplayActionInput(raw);
221
+ return {
222
+ version: 1,
223
+ operationId: `${operationId}-${index}`,
224
+ sessionId,
225
+ action,
226
+ // Replay always re-observes so per-action results carry fresh state.
227
+ reobserveAfterAction: true,
228
+ };
229
+ });
230
+ return {
231
+ version: 1,
232
+ sessionId,
233
+ operations,
234
+ stopOnFailure: input.stopOnFailure === undefined ? true : input.stopOnFailure === true,
235
+ };
236
+ }
237
+ function desktopReplayResult(value, operationId) {
238
+ if (value === null || typeof value !== "object" || Array.isArray(value))
239
+ throw new Error("desktop-result-invalid");
240
+ const result = value;
241
+ if (result.version !== 1 ||
242
+ (result.status !== "succeeded" &&
243
+ result.status !== "failed" &&
244
+ result.status !== "cancelled" &&
245
+ result.status !== "needs_user") ||
246
+ !Array.isArray(result.results))
247
+ throw new Error("desktop-result-invalid");
248
+ const results = result.results.map((entry) => {
249
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
250
+ throw new Error("desktop-result-invalid");
251
+ const record = entry;
252
+ if (record.version !== 1 ||
253
+ typeof record.operationId !== "string" ||
254
+ (record.status !== "succeeded" &&
255
+ record.status !== "failed" &&
256
+ record.status !== "cancelled" &&
257
+ record.status !== "needs_user" &&
258
+ record.status !== "unsupported"))
259
+ throw new Error("desktop-result-invalid");
260
+ return {
261
+ operationId: desktopBoundedString(record.operationId, "replay operationId"),
262
+ status: record.status,
263
+ summary: desktopBoundedString(record.summary, "replay summary"),
264
+ };
265
+ });
266
+ const last = result.results[result.results.length - 1];
267
+ const observation = last?.observation === undefined ? undefined : projectDesktopObservation(last.observation).observation;
268
+ const needsUser = result.results.find((entry) => entry.status === "needs_user");
269
+ const causeCode = needsUser !== undefined &&
270
+ needsUser.error !== null &&
271
+ typeof needsUser.error === "object" &&
272
+ "causeCode" in needsUser.error &&
273
+ typeof needsUser.error.causeCode === "string"
274
+ ? desktopBoundedString(needsUser.error.causeCode, "replay causeCode")
275
+ : undefined;
276
+ return desktopResultWithinBudget({
277
+ version: 1,
278
+ operationId,
279
+ status: result.status,
280
+ summary: result.status === "succeeded" ? "Desktop replay completed" : "Desktop replay stopped before completion",
281
+ results,
282
+ ...(observation === undefined ? {} : { observation }),
283
+ ...(result.status === "needs_user" && causeCode !== undefined
284
+ ? {
285
+ error: {
286
+ version: 1,
287
+ code: "needs_user",
288
+ message: "Desktop replay requires user action",
289
+ causeCode,
290
+ },
291
+ }
292
+ : {}),
293
+ });
294
+ }
295
+ export function createDesktopInputCapabilityFactoryV1(runtime) {
296
+ return (api) => {
297
+ if (api.host.mode === "readonly")
298
+ return;
299
+ api.registerTool({
300
+ name: "desktop_execute",
301
+ label: "Execute desktop action",
302
+ description: "Execute one bounded Accessibility focus, click, type, key, or scroll action against an observed target (click also accepts screen coordinates) and return a fresh bounded observation",
303
+ parameters: Type.Object({
304
+ sessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
305
+ action: Type.Object({
306
+ kind: Type.Union([
307
+ Type.Literal("focus"),
308
+ Type.Literal("click"),
309
+ Type.Literal("type"),
310
+ Type.Literal("key"),
311
+ Type.Literal("scroll"),
312
+ ]),
313
+ target: Type.Union([
314
+ Type.Object({
315
+ kind: Type.Literal("window"),
316
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
317
+ }, { additionalProperties: false }),
318
+ Type.Object({
319
+ kind: Type.Literal("control"),
320
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
321
+ windowId: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 })),
322
+ }, { additionalProperties: false }),
323
+ Type.Object({
324
+ kind: Type.Literal("coordinates"),
325
+ x: Type.Integer({ minimum: 0 }),
326
+ y: Type.Integer({ minimum: 0 }),
327
+ }, { additionalProperties: false }),
328
+ ]),
329
+ text: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
330
+ key: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
331
+ }, { additionalProperties: false }),
332
+ postconditions: Type.Optional(Type.Array(Type.Union([
333
+ Type.Object({
334
+ kind: Type.Literal("window-exists"),
335
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
336
+ }, { additionalProperties: false }),
337
+ Type.Object({
338
+ kind: Type.Literal("control-exists"),
339
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
340
+ }, { additionalProperties: false }),
341
+ Type.Object({
342
+ kind: Type.Literal("focused"),
343
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
344
+ }, { additionalProperties: false }),
345
+ Type.Object({
346
+ kind: Type.Literal("property-equals"),
347
+ target: Type.Union([
348
+ Type.Object({
349
+ kind: Type.Literal("window"),
350
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
351
+ }, { additionalProperties: false }),
352
+ Type.Object({
353
+ kind: Type.Literal("control"),
354
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
355
+ windowId: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 })),
356
+ }, { additionalProperties: false }),
357
+ ]),
358
+ property: Type.Union([
359
+ Type.Literal("enabled"),
360
+ Type.Literal("value"),
361
+ Type.Literal("name"),
362
+ ]),
363
+ expected: Type.Union([
364
+ Type.Boolean(),
365
+ Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
366
+ ]),
367
+ }, { additionalProperties: false }),
368
+ ]), { maxItems: DESKTOP_MAX_POSTCONDITIONS_V1 })),
369
+ }, { additionalProperties: false }),
370
+ execute: async (first, second, third) => {
371
+ const { input, operationId } = toolInvocation(first, second);
372
+ const signal = signalFrom(third);
373
+ if (signal?.aborted)
374
+ return desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled");
375
+ let request;
376
+ try {
377
+ request = desktopExecuteRequest(input, operationId);
378
+ }
379
+ catch {
380
+ return desktopFailure(operationId, "desktop_input_invalid", "Desktop action input is invalid");
381
+ }
382
+ try {
383
+ const result = runtime.execute(request);
384
+ return signal?.aborted
385
+ ? desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled")
386
+ : desktopExecuteResult(result, request);
387
+ }
388
+ catch {
389
+ return desktopFailure(operationId, "desktop_adapter_failed", "The selected desktop adapter failed");
390
+ }
391
+ },
392
+ });
393
+ const replay = runtime.replay;
394
+ if (replay === undefined)
395
+ return;
396
+ api.registerTool({
397
+ name: "desktop_replay",
398
+ label: "Replay desktop actions",
399
+ description: "Replay a bounded sequence of desktop actions against one session; stops at the first failure, cancellation, or user-confirmation pause by default",
400
+ parameters: Type.Object({
401
+ sessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
402
+ actions: Type.Array(Type.Object({
403
+ kind: Type.Union([
404
+ Type.Literal("focus"),
405
+ Type.Literal("click"),
406
+ Type.Literal("type"),
407
+ Type.Literal("key"),
408
+ Type.Literal("scroll"),
409
+ ]),
410
+ target: Type.Union([
411
+ Type.Object({
412
+ kind: Type.Literal("window"),
413
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
414
+ }, { additionalProperties: false }),
415
+ Type.Object({
416
+ kind: Type.Literal("control"),
417
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
418
+ windowId: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 })),
419
+ }, { additionalProperties: false }),
420
+ Type.Object({
421
+ kind: Type.Literal("coordinates"),
422
+ x: Type.Integer({ minimum: 0 }),
423
+ y: Type.Integer({ minimum: 0 }),
424
+ }, { additionalProperties: false }),
425
+ ]),
426
+ text: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
427
+ key: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
428
+ }, { additionalProperties: false }), { minItems: 1, maxItems: DESKTOP_MAX_REPLAY_ACTIONS_V1 }),
429
+ stopOnFailure: Type.Optional(Type.Boolean()),
430
+ }, { additionalProperties: false }),
431
+ execute: async (first, second, third) => {
432
+ const { input, operationId } = toolInvocation(first, second);
433
+ const signal = signalFrom(third);
434
+ if (signal?.aborted)
435
+ return desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled");
436
+ let request;
437
+ try {
438
+ request = desktopReplayRequest(input, operationId);
439
+ }
440
+ catch {
441
+ return desktopFailure(operationId, "desktop_input_invalid", "Desktop replay input is invalid");
442
+ }
443
+ try {
444
+ const result = replay(request);
445
+ return signal?.aborted
446
+ ? desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled")
447
+ : desktopReplayResult(result, operationId);
448
+ }
449
+ catch {
450
+ return desktopFailure(operationId, "desktop_adapter_failed", "The selected desktop adapter failed");
451
+ }
452
+ },
453
+ });
454
+ };
455
+ }
456
+ //# sourceMappingURL=capability.js.map