@aimarket/warden 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README-es.md +27 -11
  3. package/README-fr.md +27 -11
  4. package/README-ru.md +27 -11
  5. package/README-zh.md +27 -11
  6. package/README.md +27 -11
  7. package/dist/annotation-review.d.ts +10 -0
  8. package/dist/annotation-review.d.ts.map +1 -0
  9. package/dist/annotation-review.js +36 -0
  10. package/dist/annotation-review.js.map +1 -0
  11. package/dist/atomic-screen.d.ts +61 -0
  12. package/dist/atomic-screen.d.ts.map +1 -0
  13. package/dist/atomic-screen.js +165 -0
  14. package/dist/atomic-screen.js.map +1 -0
  15. package/dist/flow-guard-answer.d.ts +29 -0
  16. package/dist/flow-guard-answer.d.ts.map +1 -0
  17. package/dist/flow-guard-answer.js +201 -0
  18. package/dist/flow-guard-answer.js.map +1 -0
  19. package/dist/flow-guard-classify.d.ts +14 -0
  20. package/dist/flow-guard-classify.d.ts.map +1 -0
  21. package/dist/flow-guard-classify.js +224 -0
  22. package/dist/flow-guard-classify.js.map +1 -0
  23. package/dist/flow-guard-decode.d.ts +17 -0
  24. package/dist/flow-guard-decode.d.ts.map +1 -0
  25. package/dist/flow-guard-decode.js +149 -0
  26. package/dist/flow-guard-decode.js.map +1 -0
  27. package/dist/flow-guard-destinations.d.ts +23 -0
  28. package/dist/flow-guard-destinations.d.ts.map +1 -0
  29. package/dist/flow-guard-destinations.js +158 -0
  30. package/dist/flow-guard-destinations.js.map +1 -0
  31. package/dist/flow-guard-fingerprints.d.ts +27 -0
  32. package/dist/flow-guard-fingerprints.d.ts.map +1 -0
  33. package/dist/flow-guard-fingerprints.js +164 -0
  34. package/dist/flow-guard-fingerprints.js.map +1 -0
  35. package/dist/flow-guard-fragments.d.ts +17 -0
  36. package/dist/flow-guard-fragments.d.ts.map +1 -0
  37. package/dist/flow-guard-fragments.js +122 -0
  38. package/dist/flow-guard-fragments.js.map +1 -0
  39. package/dist/flow-guard-instructions.d.ts +12 -0
  40. package/dist/flow-guard-instructions.d.ts.map +1 -0
  41. package/dist/flow-guard-instructions.js +36 -0
  42. package/dist/flow-guard-instructions.js.map +1 -0
  43. package/dist/flow-guard-labels.d.ts +10 -0
  44. package/dist/flow-guard-labels.d.ts.map +1 -0
  45. package/dist/flow-guard-labels.js +55 -0
  46. package/dist/flow-guard-labels.js.map +1 -0
  47. package/dist/flow-guard-leak.d.ts +29 -0
  48. package/dist/flow-guard-leak.d.ts.map +1 -0
  49. package/dist/flow-guard-leak.js +135 -0
  50. package/dist/flow-guard-leak.js.map +1 -0
  51. package/dist/flow-guard-paths.d.ts +21 -0
  52. package/dist/flow-guard-paths.d.ts.map +1 -0
  53. package/dist/flow-guard-paths.js +72 -0
  54. package/dist/flow-guard-paths.js.map +1 -0
  55. package/dist/flow-guard-policy.d.ts +15 -0
  56. package/dist/flow-guard-policy.d.ts.map +1 -0
  57. package/dist/flow-guard-policy.js +179 -0
  58. package/dist/flow-guard-policy.js.map +1 -0
  59. package/dist/flow-guard-post.d.ts +6 -0
  60. package/dist/flow-guard-post.d.ts.map +1 -0
  61. package/dist/flow-guard-post.js +131 -0
  62. package/dist/flow-guard-post.js.map +1 -0
  63. package/dist/flow-guard-pre.d.ts +13 -0
  64. package/dist/flow-guard-pre.d.ts.map +1 -0
  65. package/dist/flow-guard-pre.js +335 -0
  66. package/dist/flow-guard-pre.js.map +1 -0
  67. package/dist/flow-guard-screen.d.ts +85 -0
  68. package/dist/flow-guard-screen.d.ts.map +1 -0
  69. package/dist/flow-guard-screen.js +558 -0
  70. package/dist/flow-guard-screen.js.map +1 -0
  71. package/dist/flow-guard-state.d.ts +12 -0
  72. package/dist/flow-guard-state.d.ts.map +1 -0
  73. package/dist/flow-guard-state.js +121 -0
  74. package/dist/flow-guard-state.js.map +1 -0
  75. package/dist/flow-guard-text.d.ts +9 -0
  76. package/dist/flow-guard-text.d.ts.map +1 -0
  77. package/dist/flow-guard-text.js +46 -0
  78. package/dist/flow-guard-text.js.map +1 -0
  79. package/dist/flow-guard-types.d.ts +154 -0
  80. package/dist/flow-guard-types.d.ts.map +1 -0
  81. package/dist/flow-guard-types.js +2 -0
  82. package/dist/flow-guard-types.js.map +1 -0
  83. package/dist/flow-guard.d.ts +20 -215
  84. package/dist/flow-guard.d.ts.map +1 -1
  85. package/dist/flow-guard.js +26 -1400
  86. package/dist/flow-guard.js.map +1 -1
  87. package/dist/mcp-client.d.ts +1 -2
  88. package/dist/mcp-client.d.ts.map +1 -1
  89. package/dist/mcp-client.js +2 -19
  90. package/dist/mcp-client.js.map +1 -1
  91. package/dist/mcp-server.js +5 -0
  92. package/dist/mcp-server.js.map +1 -1
  93. package/dist/mcp-tools.d.ts.map +1 -1
  94. package/dist/mcp-tools.js +46 -9
  95. package/dist/mcp-tools.js.map +1 -1
  96. package/dist/public-address.d.ts +3 -0
  97. package/dist/public-address.d.ts.map +1 -0
  98. package/dist/public-address.js +58 -0
  99. package/dist/public-address.js.map +1 -0
  100. package/dist/review-diff.d.ts +36 -0
  101. package/dist/review-diff.d.ts.map +1 -1
  102. package/dist/review-diff.js +177 -7
  103. package/dist/review-diff.js.map +1 -1
  104. package/dist/task-adapter.d.ts +73 -0
  105. package/dist/task-adapter.d.ts.map +1 -0
  106. package/dist/task-adapter.js +379 -0
  107. package/dist/task-adapter.js.map +1 -0
  108. package/dist/task-cli.d.ts +3 -0
  109. package/dist/task-cli.d.ts.map +1 -0
  110. package/dist/task-cli.js +162 -0
  111. package/dist/task-cli.js.map +1 -0
  112. package/dist/types.d.ts +9 -0
  113. package/dist/types.d.ts.map +1 -1
  114. package/dist/wrap-cli.d.ts +5 -0
  115. package/dist/wrap-cli.d.ts.map +1 -1
  116. package/dist/wrap-cli.js +29 -2
  117. package/dist/wrap-cli.js.map +1 -1
  118. package/dist/wrap-screen.d.ts +3 -0
  119. package/dist/wrap-screen.d.ts.map +1 -0
  120. package/dist/wrap-screen.js +20 -0
  121. package/dist/wrap-screen.js.map +1 -0
  122. package/dist/wrap-state.d.ts +8 -1
  123. package/dist/wrap-state.d.ts.map +1 -1
  124. package/dist/wrap-state.js +21 -4
  125. package/dist/wrap-state.js.map +1 -1
  126. package/dist/wrap.d.ts.map +1 -1
  127. package/dist/wrap.js +204 -10
  128. package/dist/wrap.js.map +1 -1
  129. package/docs/flow-guard-model-comparison-2026-10-25.es.md +82 -0
  130. package/docs/flow-guard-model-comparison-2026-10-25.fr.md +82 -0
  131. package/docs/flow-guard-model-comparison-2026-10-25.md +82 -0
  132. package/docs/flow-guard-model-comparison-2026-10-25.ru.md +82 -0
  133. package/docs/flow-guard-model-comparison-2026-10-25.zh.md +82 -0
  134. package/docs/flow-guard.es.md +307 -12
  135. package/docs/flow-guard.fr.md +307 -12
  136. package/docs/flow-guard.md +315 -13
  137. package/docs/flow-guard.ru.md +314 -13
  138. package/docs/flow-guard.zh.md +306 -8
  139. package/docs/scan-limits.es.md +39 -0
  140. package/docs/scan-limits.fr.md +39 -0
  141. package/docs/scan-limits.md +39 -0
  142. package/docs/scan-limits.ru.md +39 -0
  143. package/docs/scan-limits.zh.md +39 -0
  144. package/docs/scan.es.md +2 -0
  145. package/docs/scan.fr.md +2 -0
  146. package/docs/scan.md +2 -0
  147. package/docs/scan.ru.md +2 -0
  148. package/docs/scan.zh.md +2 -0
  149. package/docs/security-hardening.es.md +1 -1
  150. package/docs/security-hardening.fr.md +1 -1
  151. package/docs/security-hardening.md +1 -1
  152. package/docs/security-hardening.ru.md +1 -1
  153. package/docs/security-hardening.zh.md +1 -1
  154. package/examples/atomic-screen-server.mjs +42 -0
  155. package/native/macos/Package.swift +9 -0
  156. package/native/macos/Sources/WardenDesktop/NativeSurface.swift +246 -0
  157. package/native/macos/Sources/WardenDesktop/StandardIO.swift +139 -0
  158. package/native/macos/Sources/WardenDesktopHost/Main.swift +211 -0
  159. package/package.json +18 -6
  160. package/scripts/build-macos.mjs +22 -0
@@ -0,0 +1,558 @@
1
+ /** Flow guard: screen-control servers, whose clicks and keys move data without naming it. */
2
+ import { createHash } from 'node:crypto';
3
+ import { basename } from 'node:path';
4
+ import { isSecretPath } from './flow-guard-paths.js';
5
+ import { MAX_TEXT, responseStrings } from './flow-guard-text.js';
6
+ const SCREEN_WORDS = /(^|_|-)(screenshot|snapshot|screen|observe|accessibility|a11y|read_?page|page_?content|get_?(page|elements|ui|tree|screen|state)|elements|ui_?tree|dom|inspect)(_|-|$)/i;
7
+ const POINTER_WORDS = /(^|_|-)(click|tap|double_?click|right_?click|left_?click|middle_?click|triple_?click|press_?(button|element)|mouse|pointer|touch|drag|drop|swipe|long_?press)(_|-|$)/i;
8
+ // `type` in known control names, not arbitrary nouns such as `get_cuisine_type`.
9
+ const TYPE_WORDS = /(^|_|-)(type_?text|input_?text|insert_?text|enter_?text|fill)(_|-|$)|^type(_|-|$)|^(browser|screen|computer|desktop|keyboard)[_-]type$/i;
10
+ const KEY_WORDS = /(^|_|-)(key|keys|keypress|key_?press|hotkey|shortcut|keyboard|paste)(_|-|$)/i;
11
+ const keyControl = (name) => KEY_WORDS.test(name) || TYPE_WORDS.test(name);
12
+ const GENERIC_CONTROL = /^(computer|screen_control|desktop_control|browser_control)$/i;
13
+ // Exempt only complete observation names, never a substring such as `screenshot_then_submit`.
14
+ const READ_CONTROL_ACTIONS = new Set(['screenshot', 'snapshot', 'observe', 'accessibility', 'a11y', 'read_page',
15
+ 'page_content', 'get_page', 'get_elements', 'get_ui', 'get_tree', 'get_screen', 'get_state', 'elements', 'ui_tree', 'dom', 'inspect']);
16
+ const PASSIVE_KEY_ACTIONS = new Set(['key', 'keys', 'keypress', 'key_press', 'press_key', 'press_keys', 'hotkey', 'shortcut', 'keyboard']);
17
+ export const screenOperation = (name) => name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[.-]/g, '_').toLowerCase();
18
+ const SEND_TARGET = /\b(send|post|publish|submit|share|reply|comment|tweet|forward|upload|invite|pay|payment|buy|purchase|check\s?out|place\s+order|order\s+now|transfer|donate|sign\s?in|log\s?in)\b/i;
19
+ const DESTROY_TARGET = /\b(delete|remove|discard|erase|wipe|empty\s+trash|uninstall|revoke)\b/i;
20
+ const PASTE_KEYS = /\b(cmd|command|meta|super|win|ctrl|control)\s*[+-]\s*v\b|\bshift\s*[+-]\s*insert\b/i;
21
+ const REF_ARGS = new Set(['ref', 'element_ref', 'elementref', 'index', 'idx', 'element_index', 'elementindex', 'element_id', 'elementid', 'id', 'node_id', 'nodeid', 'mark']);
22
+ const TARGET_TEXT_ARGS = new Set(['element', 'label', 'name', 'text', 'title', 'target', 'description', 'selector', 'button', 'aria_label', 'arialabel']);
23
+ const KEY_TEXT_ARGS = new Set(['key', 'keys', 'text', 'combo', 'combination', 'shortcut', 'sequence', 'value', 'chars', 'input']);
24
+ const ID_KEYS = ['index', 'idx', 'id', 'ref', 'element_id', 'elementId', 'node_id', 'nodeId', 'mark'];
25
+ const LABEL_KEYS = ['name', 'label', 'text', 'title', 'aria_label', 'ariaLabel', 'description', 'value', 'innerText'];
26
+ const TITLE_KEYS = ['windowTitle', 'window_title', 'pageTitle', 'page_title', 'title'];
27
+ const ACCESSIBILITY_ID_KEYS = ['accessibilityIdentifier', 'accessibility_id', 'accessibilityId', 'automationId', 'automation_id', 'AXIdentifier', 'identifier'];
28
+ const TEXT_INPUT_ROLES = new Set(['textbox', 'input', 'textfield', 'text_field', 'entry', 'searchbox', 'textarea', 'combobox']);
29
+ const GUI_NAV_KEYS = new Set(['tab', 'shift+tab', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright', 'home', 'end', 'pageup', 'pagedown', 'escape']);
30
+ // Best-effort explanation vocabulary, never an allow-list. Unknown words still require approval.
31
+ const SCREEN_ACTION_ALIASES = Object.fromEntries([
32
+ ['send', 'senden envoyer enviar wyślij отправить 发送 送信 إرسال'],
33
+ ['submit', 'absenden soumettre zatwierdź отправка 提交'],
34
+ ['pay', 'bezahlen payer pagar zapłać оплатить 支付'],
35
+ ['share', 'teilen partager compartir udostępnij поделиться 分享'],
36
+ ['delete', 'löschen supprimer eliminar usuń удалить 删除'],
37
+ ['discard', 'verwerfen abandonner descartar odrzuć отменить 丢弃'],
38
+ ].flatMap(([verb, words]) => words.split(' ').map(word => [word, verb])));
39
+ // Playwright's accessibility snapshot (`- button "Send" [ref=e14]`), the `[ref_14]` form, numbered element lists
40
+ // (`14: button "Send"`, `1 'Issues' tab`) and `Page Title:` / `window:` lines.
41
+ const REF_LINE = /(?:^|\n)[ \t]*-?[ \t]*([a-z][\w-]{0,30})[ \t]+"([^"\n]{1,160})"[^\n]*?\[ref=([\w-]{1,24})\]/gi;
42
+ const REF_TOKEN_LINE = /(?:^|\n)[ \t]*-?[ \t]*([a-z][\w-]{0,30})[ \t]+"([^"\n]{1,160})"[^\n]*?\[(ref_\d{1,6})\]/gi;
43
+ const INDEX_LINE = /(?:^|[\n,;])[ \t]*(?:elements?:)?[ \t]*\[?(\d{1,4})\]?[ \t]*[:.)\]=-]?[ \t]*(?:([a-z][\w-]{0,30})[ \t]+)?["'“‘]([^"'”’\n]{1,160})["'”’](?:[ \t]+([a-z][\w-]{0,30}))?/gi;
44
+ const TITLE_LINE = /(?:^|\n)[ \t]*-?[ \t]*(?:page[ \t]+title|window[ \t]+title|active[ \t]+window|window|title)[ \t]*[:=][ \t]*["']?([^\n"']{1,160})/gi;
45
+ export const MAX_SCREENS = 8;
46
+ const MAX_REFS = 500;
47
+ const MAX_TITLES = 6;
48
+ export function screenServer(toolName) {
49
+ return (toolName.split('__')[1] || toolName).slice(0, 100);
50
+ }
51
+ function refKey(v) {
52
+ return String(v).trim().toLowerCase().slice(0, 40);
53
+ }
54
+ const ROLE = (v) => (typeof v === 'string' && /^[a-z][\w-]{0,30}$/i.test(v) ? v.toLowerCase() : '');
55
+ const screenObject = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
56
+ const screenNumber = (v) => typeof v === 'number' && Number.isFinite(v) && Math.abs(v) <= 10_000_000;
57
+ const screenHash = (v) => createHash('sha256').update(v).digest('hex');
58
+ const guiIdentity = (v) => typeof v === 'string' && v.length > 0 && v.length <= 200 && !/[\x00-\x1f\x7f]/.test(v);
59
+ const guiFields = (o, fields) => Object.keys(o).every(k => fields.includes(k));
60
+ function guiOrigin(v, policy = false) {
61
+ if (typeof v !== 'string' || v.length > 2048)
62
+ return undefined;
63
+ try {
64
+ const u = new URL(v);
65
+ if (!['https:', 'http:'].includes(u.protocol) || u.username || u.password)
66
+ return undefined;
67
+ if (policy && (u.pathname !== '/' || u.search || u.hash))
68
+ return undefined;
69
+ return u.origin;
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ function guiKey(v) {
76
+ if (typeof v !== 'string' && !(Array.isArray(v) && v.length > 0 && v.every(k => typeof k === 'string')))
77
+ return undefined;
78
+ const raw = Array.isArray(v) ? v.join('+') : v;
79
+ if (raw.length > 40 || /[^a-z+ ]/i.test(raw))
80
+ return undefined;
81
+ const key = raw.trim().toLowerCase().replace(/ *\+ */g, '+');
82
+ return GUI_NAV_KEYS.has(key) ? key : undefined;
83
+ }
84
+ /** Invalid GUI policy is strict as a whole: a typo must never create a broader permission. */
85
+ export function parseGuiPolicy(value) {
86
+ const strict = { mode: 'strict', maxSnapshotAgeMs: 5000, allow: [] };
87
+ const invalid = { ...strict, invalid: true };
88
+ const o = screenObject(value);
89
+ if (o?.mode === 'strict' && guiFields(o, ['mode', 'maxSnapshotAgeMs', 'allow']))
90
+ return strict;
91
+ if (!o || !guiFields(o, ['mode', 'maxSnapshotAgeMs', 'allow']) || o.mode !== 'policy')
92
+ return invalid;
93
+ const age = o.maxSnapshotAgeMs ?? 5000;
94
+ if (!Number.isInteger(age) || age < 100 || age > 30_000 || !Array.isArray(o.allow) || o.allow.length > 64)
95
+ return invalid;
96
+ const allow = [];
97
+ for (const raw of o.allow) {
98
+ const r = screenObject(raw), target = screenObject(r?.target), context = screenObject(r?.context);
99
+ if (!r || !target || !context || !guiFields(r, ['tool', 'action', 'target', 'context', 'keys', 'maxTextLength'])
100
+ || !guiFields(target, ['role', 'accessibilityId']) || !guiFields(context, ['origin', 'application'])
101
+ || typeof r.tool !== 'string' || !/^mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_.-]+$/.test(r.tool)
102
+ || typeof r.action !== 'string' || !['click', 'type', 'key'].includes(r.action) || !guiIdentity(target.accessibilityId)
103
+ || typeof target.role !== 'string' || !/^[a-z][\w-]{0,30}$/.test(target.role))
104
+ return invalid;
105
+ const origin = guiOrigin(context.origin, true), application = context.application;
106
+ if ((context.origin !== undefined && !origin) || (application !== undefined && (typeof application !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,199}$/.test(application)))
107
+ || (!origin && application === undefined))
108
+ return invalid;
109
+ const keys = guiKey(r.keys);
110
+ if (r.action === 'key' ? typeof r.keys !== 'string' || !keys || r.maxTextLength !== undefined : r.keys !== undefined)
111
+ return invalid;
112
+ if (r.action === 'type' ? !Number.isInteger(r.maxTextLength) || r.maxTextLength < 1 || r.maxTextLength > 4096 : r.maxTextLength !== undefined)
113
+ return invalid;
114
+ allow.push({ tool: r.tool, action: r.action, target: { role: target.role, accessibilityId: target.accessibilityId },
115
+ context: { ...(origin ? { origin } : {}), ...(typeof application === 'string' ? { application } : {}) },
116
+ ...(keys ? { keys } : {}), ...(r.action === 'type' ? { maxTextLength: r.maxTextLength } : {}) });
117
+ }
118
+ return { mode: 'policy', maxSnapshotAgeMs: age, allow };
119
+ }
120
+ function screenPoint(v) {
121
+ const o = screenObject(v);
122
+ if (o && screenNumber(o.x) && screenNumber(o.y))
123
+ return { x: o.x, y: o.y };
124
+ if (Array.isArray(v) && v.length === 2 && screenNumber(v[0]) && screenNumber(v[1]))
125
+ return { x: v[0], y: v[1] };
126
+ return undefined;
127
+ }
128
+ function screenBox(v) {
129
+ const o = screenObject(v);
130
+ if (!o)
131
+ return undefined; // Bare arrays have incompatible xywh/xyxy conventions; do not guess.
132
+ const x = o.x ?? o.left, y = o.y ?? o.top;
133
+ if (!screenNumber(x) || !screenNumber(y))
134
+ return undefined;
135
+ if ((o.left !== undefined && o.left !== x) || (o.top !== undefined && o.top !== y))
136
+ return undefined;
137
+ const width = o.width ?? (screenNumber(o.right) ? o.right - x : undefined);
138
+ const height = o.height ?? (screenNumber(o.bottom) ? o.bottom - y : undefined);
139
+ if (!screenNumber(width) || !screenNumber(height) || width <= 0 || height <= 0 || !screenNumber(x + width) || !screenNumber(y + height))
140
+ return undefined;
141
+ if ((o.right !== undefined && o.right !== x + width) || (o.bottom !== undefined && o.bottom !== y + height))
142
+ return undefined;
143
+ return { x, y, width, height };
144
+ }
145
+ function screenAction(text) {
146
+ const words = screenOperation(text.normalize('NFKC')).split(/[^\p{L}\p{N}]+/u);
147
+ const verb = words.join(' ').match(SEND_TARGET)?.[1]?.toLowerCase().replace(/\s+/g, ' ');
148
+ const destroy = words.join(' ').match(DESTROY_TARGET)?.[1]?.toLowerCase().replace(/\s+/g, ' ');
149
+ const aliases = words
150
+ .map(word => Object.hasOwn(SCREEN_ACTION_ALIASES, word) ? SCREEN_ACTION_ALIASES[word] : undefined);
151
+ return { verb: verb ?? aliases.find(v => v && v !== 'delete' && v !== 'discard'), destroy: destroy ?? aliases.find(v => v === 'delete' || v === 'discard') };
152
+ }
153
+ function screenElement(o, role, form) {
154
+ const boxes = ['bounds', 'boundingBox', 'bounding_box', 'rect'].filter(k => o[k] !== undefined).map(k => screenBox(o[k]));
155
+ const bounds = boxes.length && boxes.every(b => b && JSON.stringify(b) === JSON.stringify(boxes[0])) ? boxes[0] : undefined;
156
+ const center = boxes.length ? undefined : screenPoint(o.center);
157
+ const identifier = ACCESSIBILITY_ID_KEYS.map(k => typeof o[k] === 'string' ? o[k].slice(0, 200) : '').join(' ');
158
+ const action = screenAction(screenOperation(identifier));
159
+ const identities = [...new Set(ACCESSIBILITY_ID_KEYS.filter(k => o[k] !== undefined).map(k => guiIdentity(o[k]) ? o[k] : ''))];
160
+ const identityHash = identities.length === 1 && identities[0] ? screenHash(identities[0]) : undefined;
161
+ const idKey = ID_KEYS.find(k => typeof o[k] === 'string' || typeof o[k] === 'number');
162
+ const reference = idKey ? String(o[idKey]) : '';
163
+ const referenceHash = reference.length > 0 && reference.length <= 40 ? screenHash(reference) : undefined;
164
+ const labelHash = screenHash(JSON.stringify(LABEL_KEYS.map(k => typeof o[k] === 'string' ? o[k] : null)));
165
+ const formRef = typeof o.formId === 'string' || typeof o.formId === 'number' ? refKey(o.formId) : typeof o.form === 'string' || typeof o.form === 'number' ? refKey(o.form) : form;
166
+ return { ...(role ? { role } : {}), ...(bounds ? { bounds } : {}), ...(center ? { center } : {}),
167
+ ...(o.focused === true ? { focused: true } : {}), ...(typeof o.multiline === 'boolean' ? { multiline: o.multiline } : {}),
168
+ ...(formRef ? { form: formRef } : {}), ...(action.verb ? { identifierVerb: action.verb } : {}),
169
+ ...(action.destroy ? { identifierDestroy: action.destroy } : {}),
170
+ ...(o.type === 'submit' && ['button', 'input', 'submit'].includes(role) ? { submit: true } : {}),
171
+ ...(identityHash ? { identityHash } : {}), ...(referenceHash ? { referenceHash } : {}), labelHash };
172
+ }
173
+ function coordinateRef(input, view) {
174
+ const points = ['coordinate', 'coordinates', 'position'].filter(k => input[k] !== undefined).map(k => screenPoint(input[k]));
175
+ if (input.x !== undefined || input.y !== undefined)
176
+ points.push(screenPoint(input));
177
+ if (!points.length || !points.every(p => p && p.x === points[0]?.x && p.y === points[0]?.y))
178
+ return undefined;
179
+ const p = points[0];
180
+ const hits = Object.entries(view?.elements ?? {}).filter(([, e]) => {
181
+ if (e.ambiguous)
182
+ return false;
183
+ const b = e.bounds;
184
+ return b ? p.x >= b.x && p.y >= b.y && p.x < b.x + b.width && p.y < b.y + b.height
185
+ : e.center?.x === p.x && e.center?.y === p.y;
186
+ });
187
+ return hits.length === 1 ? hits[0][0] : undefined; // Overlaps have no reliable stacking order.
188
+ }
189
+ function explainScreenElement(out, ref, view) {
190
+ const e = Object.hasOwn(view.elements ?? {}, ref) ? view.elements?.[ref] : undefined;
191
+ if (e?.ambiguous)
192
+ return;
193
+ out.ref = ref;
194
+ const label = Object.hasOwn(view.refs, ref) ? view.refs[ref] : '';
195
+ const action = screenAction(label ?? '');
196
+ out.verb ??= action.verb;
197
+ out.destroy ??= action.destroy;
198
+ if (!out.verb && !out.destroy && (e?.identifierVerb || e?.identifierDestroy || e?.submit)) {
199
+ out.verb = e.identifierVerb ?? (e.submit ? 'submit' : undefined);
200
+ out.destroy = e.identifierDestroy;
201
+ out.evidence = e.submit && !e.identifierVerb && !e.identifierDestroy ? 'semantic' : 'identifier';
202
+ }
203
+ }
204
+ /** The elements and titles a screen tool's result shows, or undefined when the result is not a screen. A screen
205
+ * tool's result always replaces the server's last view, even when it is only an image: a stale list must not
206
+ * label the next click. Numbered lists also count from pointer/keyboard action results; acknowledgments alone
207
+ * keep the previous view, while an image-only action result clears it. `[ref=…]` lines and JSON nodes with a role
208
+ * count from any tool, since they are what accessibility trees look like. */
209
+ export function screenFromResult(toolName, response, text, input = {}) {
210
+ const tool = screenOperation(toolName.split('__').slice(2).join('__') || toolName);
211
+ const action = typeof input.action === 'string' ? screenOperation(input.action) : '';
212
+ const screenTool = SCREEN_WORDS.test(tool) || (GENERIC_CONTROL.test(tool) && SCREEN_WORDS.test(action));
213
+ const controlTool = POINTER_WORDS.test(tool) || keyControl(tool) || POINTER_WORDS.test(action) || keyControl(action);
214
+ const refs = Object.create(null);
215
+ const elements = Object.create(null);
216
+ const focusedRefs = new Set();
217
+ const conflictingRefs = new Set();
218
+ const origins = new Set(), applications = new Set();
219
+ const titles = [];
220
+ let structural = false;
221
+ let image = false;
222
+ let truncated = text.length >= MAX_TEXT;
223
+ const add = (key, role, label) => {
224
+ if (Object.keys(refs).length >= MAX_REFS || !label.trim())
225
+ return;
226
+ const ref = refKey(key), value = `${role ? role + ' ' : ''}"${label.trim().slice(0, 160)}"`;
227
+ if (Object.hasOwn(refs, ref) && refs[ref] !== value)
228
+ conflictingRefs.add(ref);
229
+ refs[ref] = value;
230
+ };
231
+ const body = text.slice(0, MAX_TEXT);
232
+ for (const m of body.matchAll(REF_LINE)) {
233
+ structural = true;
234
+ add(m[3], (m[1] ?? '').toLowerCase(), m[2] ?? '');
235
+ }
236
+ for (const m of body.matchAll(REF_TOKEN_LINE)) {
237
+ structural = true;
238
+ add(m[3], (m[1] ?? '').toLowerCase(), m[2] ?? '');
239
+ }
240
+ if (screenTool || controlTool)
241
+ for (const m of body.matchAll(INDEX_LINE)) {
242
+ structural = true;
243
+ add(m[1], ROLE(m[2]) || ROLE(m[4]), m[3] ?? '');
244
+ }
245
+ // JSON element lists, given as objects or as JSON in a text block.
246
+ const nodes = [response];
247
+ for (const t of responseStrings(response)) {
248
+ const s = t.trim();
249
+ if ((s.startsWith('{') || s.startsWith('[')) && s.length < MAX_TEXT) {
250
+ try {
251
+ nodes.push(JSON.parse(s));
252
+ }
253
+ catch { /* not JSON */ }
254
+ }
255
+ }
256
+ // Only document-level context: an element/link URL cannot authorize the page containing it.
257
+ for (const n of nodes) {
258
+ const root = screenObject(n);
259
+ for (const scope of [root, screenObject(root?.structuredContent)]) {
260
+ if (!scope)
261
+ continue;
262
+ for (const k of ['url', 'pageUrl', 'page_url', 'documentUrl'])
263
+ if (scope[k] !== undefined)
264
+ origins.add(guiOrigin(scope[k]) ?? '');
265
+ for (const k of ['applicationId', 'bundleId'])
266
+ if (scope[k] !== undefined)
267
+ applications.add(guiIdentity(scope[k]) ? scope[k] : '');
268
+ }
269
+ }
270
+ let budget = 20_000;
271
+ const walk = (v, depth, form) => {
272
+ if (budget-- <= 0 || depth > 8) {
273
+ truncated = true;
274
+ return;
275
+ }
276
+ if (!v || typeof v !== 'object')
277
+ return;
278
+ if (Array.isArray(v)) {
279
+ for (const x of v)
280
+ walk(x, depth + 1, form);
281
+ return;
282
+ }
283
+ const o = v;
284
+ if (o.type === 'image')
285
+ image = true;
286
+ const idKey = ID_KEYS.find(k => typeof o[k] === 'string' || typeof o[k] === 'number');
287
+ const labelKey = LABEL_KEYS.find(k => typeof o[k] === 'string' && o[k].trim());
288
+ const role = ROLE(o.role) || ROLE(o.type) || ROLE(o.tag);
289
+ const elementKey = idKey ? refKey(o[idKey]) : undefined;
290
+ const childForm = role === 'form' && elementKey ? elementKey : form;
291
+ if (elementKey && (role || screenTool) && (Object.hasOwn(elements, elementKey) || Object.keys(elements).length < MAX_REFS)) {
292
+ const element = screenElement(o, role, role === 'form' ? undefined : form);
293
+ if (Object.hasOwn(elements, elementKey) && JSON.stringify(elements[elementKey]) !== JSON.stringify(element))
294
+ elements[elementKey] = { ambiguous: true };
295
+ else
296
+ elements[elementKey] = element;
297
+ if (o.role !== undefined)
298
+ structural = true;
299
+ }
300
+ else if (elementKey && (role || screenTool))
301
+ truncated = true;
302
+ for (const k of ['focusedRef', 'focusedElementId', 'activeElementId']) {
303
+ if (typeof o[k] === 'string' || typeof o[k] === 'number')
304
+ focusedRefs.add(refKey(o[k]));
305
+ }
306
+ if (idKey && labelKey && (role || screenTool)) {
307
+ if (o.role !== undefined)
308
+ structural = true;
309
+ add(o[idKey], role, o[labelKey]);
310
+ }
311
+ else if (!idKey) {
312
+ for (const k of TITLE_KEYS)
313
+ if (typeof o[k] === 'string' && o[k].trim() && titles.length < MAX_TITLES)
314
+ titles.push(o[k].trim().slice(0, 160));
315
+ }
316
+ for (const x of Object.values(o))
317
+ if (x && typeof x === 'object')
318
+ walk(x, depth + 1, childForm);
319
+ };
320
+ for (const n of nodes)
321
+ walk(n, 0);
322
+ for (const ref of conflictingRefs)
323
+ if (Object.hasOwn(elements, ref))
324
+ elements[ref] = { ambiguous: true };
325
+ for (const m of body.matchAll(TITLE_LINE)) {
326
+ if (titles.length >= MAX_TITLES)
327
+ break;
328
+ titles.push((m[1] ?? '').trim().slice(0, 160));
329
+ }
330
+ if (!screenTool && !structural && !(controlTool && (image || titles.length)))
331
+ return undefined;
332
+ for (const [ref, e] of Object.entries(elements))
333
+ if (e.focused)
334
+ focusedRefs.add(ref);
335
+ const focused = focusedRefs.size === 1 ? [...focusedRefs][0] : undefined;
336
+ return { refs, titles, ...(Object.keys(elements).length ? { elements } : {}),
337
+ ...(origins.size > 1 || origins.has('') || applications.size > 1 || applications.has('') ? { contextAmbiguous: true } : {}),
338
+ ...(!truncated ? { policyComplete: true } : {}),
339
+ ...(origins.size === 1 && !origins.has('') ? { origin: [...origins][0] } : {}),
340
+ ...(applications.size === 1 && !applications.has('') ? { application: [...applications][0] } : {}),
341
+ ...(focused && Object.hasOwn(elements, focused) && !elements[focused]?.ambiguous ? { focused } : {}) };
342
+ }
343
+ /** A pointer or keyboard call to an MCP screen server, read against that server's last screen. */
344
+ export function screenCall(toolName, input, state) {
345
+ if (!toolName.startsWith('mcp__'))
346
+ return undefined;
347
+ const tool = screenOperation(toolName.split('__').slice(2).join('__') || toolName);
348
+ const action = typeof input.action === 'string' ? screenOperation(input.action) : '';
349
+ const pointer = POINTER_WORDS.test(tool) || POINTER_WORDS.test(action);
350
+ const key = keyControl(tool) || keyControl(action);
351
+ if (!pointer && !key) {
352
+ if (GENERIC_CONTROL.test(tool) && !(READ_CONTROL_ACTIONS.has(action) && Object.keys(input).length === 1))
353
+ return { guarded: 'control' };
354
+ return undefined;
355
+ }
356
+ const out = {};
357
+ const view = state.screens?.[screenServer(toolName)];
358
+ if (pointer) {
359
+ // No label, role, coordinate or caller-supplied description establishes the actual effect of a click.
360
+ // Gate even familiar labels: "Inbox" can be a submit button, or a page can change after the snapshot.
361
+ out.guarded = 'pointer';
362
+ const targets = [];
363
+ const requestedRefs = new Set();
364
+ for (const [k, v] of Object.entries(input)) {
365
+ const name = k.toLowerCase();
366
+ if (TARGET_TEXT_ARGS.has(name) && typeof v === 'string')
367
+ targets.push(v.slice(0, 300));
368
+ if (REF_ARGS.has(name) && (typeof v === 'string' || typeof v === 'number'))
369
+ requestedRefs.add(refKey(v));
370
+ }
371
+ const hit = coordinateRef(input, view);
372
+ const ref = requestedRefs.size === 1 ? [...requestedRefs][0] : requestedRefs.size === 0 ? hit : undefined;
373
+ if (ref && view && (hit === undefined || hit === ref)) {
374
+ explainScreenElement(out, ref, view);
375
+ if (hit === ref)
376
+ out.via = 'coordinates';
377
+ }
378
+ for (const t of targets) {
379
+ const action = screenAction(t);
380
+ out.verb ??= action.verb;
381
+ out.destroy ??= action.destroy;
382
+ }
383
+ }
384
+ if (key) {
385
+ const typed = keyTexts(input);
386
+ const paste = /(^|_|-)paste(_|-|$)/i.test(tool) || /^paste$/i.test(action) || typed.some(t => PASTE_KEYS.test(t));
387
+ // What the clipboard holds is not in the call. After a credential read it may be the credential.
388
+ if (paste)
389
+ out.paste = [...state.private].reverse().find(p => p.secret);
390
+ if (!out.guarded) {
391
+ if (paste)
392
+ out.guarded = 'paste';
393
+ else if (TYPE_WORDS.test(tool) || TYPE_WORDS.test(action))
394
+ out.guarded = 'text';
395
+ else if (typed.some(t => /\b(enter|return|numpadenter)\b|[\r\n]/i.test(t)))
396
+ out.guarded = 'enter';
397
+ else if (!(PASSIVE_KEY_ACTIONS.has(tool) || GENERIC_CONTROL.test(tool)) || !passiveShortcut(input))
398
+ out.guarded = 'keyboard';
399
+ }
400
+ if (out.guarded === 'enter' && view?.focused) {
401
+ explainScreenElement(out, view.focused, view);
402
+ out.via = 'focus';
403
+ const focused = view.elements?.[view.focused];
404
+ const chord = typed.some(t => /\b(cmd|command|meta|ctrl|control)[+-]enter\b/i.test(t));
405
+ if (!out.verb && !out.destroy && focused?.form && TEXT_INPUT_ROLES.has(focused.role ?? '') && (focused.multiline === false || (focused.multiline === true && chord))) {
406
+ const buttons = Object.entries(view.elements ?? {}).filter(([, e]) => !e.ambiguous && e.form === focused.form && (e.role === 'button' || e.submit));
407
+ const submits = buttons.map(([ref]) => { const result = {}; explainScreenElement(result, ref, view); return result; }).filter(e => e.verb);
408
+ if (submits.length === 1) {
409
+ out.verb = submits[0].verb;
410
+ out.evidence = 'form';
411
+ }
412
+ }
413
+ }
414
+ }
415
+ return out;
416
+ }
417
+ /** Operator-selected observation schema only. Never infer read authority from tool annotations. */
418
+ export function screenObservation(toolName, input) {
419
+ const name = screenOperation(toolName.split('__').slice(2).join('__') || toolName);
420
+ return READ_CONTROL_ACTIONS.has(name) || ['browser_snapshot', 'browser_take_screenshot', 'browser_screenshot', 'get_accessibility_tree', 'accessibility_snapshot', 'computer_screenshot'].includes(name) ? Object.keys(input).length === 0
421
+ : GENERIC_CONTROL.test(name) && Object.keys(input).length === 1 && typeof input.action === 'string' && READ_CONTROL_ACTIONS.has(screenOperation(input.action));
422
+ }
423
+ /** Equality evidence for a new observation, not permission and not an attestation of GUI effects. */
424
+ export function screenTargetEvidence(toolName, input, view) {
425
+ if (!view?.policyComplete || view.contextAmbiguous || !view.elements || !(view.origin || view.application || view.titles.length))
426
+ return;
427
+ const operation = guiOperation(toolName, input);
428
+ if (!operation)
429
+ return;
430
+ const keys = ['key', 'keys', 'combo', 'combination', 'shortcut'], texts = ['text', 'value'];
431
+ const allowed = operation === 'click' ? ['x', 'y', 'coordinate', 'coordinates', 'position'] : operation === 'key' ? keys : texts;
432
+ if (Object.keys(input).some(key => key !== 'action' && !REF_ARGS.has(key.toLowerCase()) && !allowed.includes(key)))
433
+ return;
434
+ if (operation !== 'click') {
435
+ const values = (operation === 'key' ? keys : texts).filter(key => input[key] !== undefined);
436
+ if (values.length !== 1)
437
+ return;
438
+ const value = input[values[0]];
439
+ if (operation === 'type' ? typeof value !== 'string' : !(typeof value === 'string' || Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string')))
440
+ return;
441
+ if (operation === 'key') {
442
+ const chord = (Array.isArray(value) ? value.join('+') : value).trim().toLowerCase().replace(/ *\+ */g, '+');
443
+ if (chord.length > 64 || !/^(?:(?:cmd|command|meta|ctrl|control|alt|option|shift|super|win)\+)*(?:[a-z0-9]|enter|return|numpadenter|insert|backspace|delete|space|tab|escape|home|end|pageup|pagedown|arrowup|arrowdown|arrowleft|arrowright|f(?:[1-9]|1\d|2[0-4]))$/.test(chord))
444
+ return;
445
+ }
446
+ }
447
+ const call = screenCall(toolName, input, { v: 1, untrusted: [], private: [], notified: false, full: false });
448
+ if (!call)
449
+ return;
450
+ const refs = Object.entries(input).filter(([key]) => REF_ARGS.has(key.toLowerCase()));
451
+ if (refs.length > 1)
452
+ return;
453
+ const raw = refs[0]?.[1];
454
+ if (refs.length && !(typeof raw === 'string' && /^[A-Za-z0-9_.:-]{1,40}$/.test(raw) || typeof raw === 'number' && Number.isSafeInteger(raw)))
455
+ return;
456
+ const ref = refs.length ? refKey(raw) : undefined;
457
+ const hasCoordinates = ['x', 'y', 'coordinate', 'coordinates', 'position'].some(k => input[k] !== undefined);
458
+ for (const key of ['coordinate', 'coordinates', 'position']) {
459
+ const value = input[key];
460
+ if (value !== undefined && !(Array.isArray(value) ? value.length === 2 && value.every(screenNumber) : screenObject(value) && Object.keys(value).every(k => ['x', 'y'].includes(k))))
461
+ return;
462
+ }
463
+ const hit = hasCoordinates ? coordinateRef(input, view) : undefined;
464
+ if (hasCoordinates && (!hit || ref && hit !== ref))
465
+ return;
466
+ const pointer = call.guarded === 'pointer';
467
+ const target = ref ?? hit ?? (!pointer ? view.focused : undefined);
468
+ if (!target || !pointer && target !== view.focused)
469
+ return;
470
+ const e = Object.hasOwn(view.elements, target) ? view.elements[target] : undefined;
471
+ if (!e || e.ambiguous || !e.role || !e.bounds || !e.referenceHash || ref && e.referenceHash !== screenHash(String(raw)))
472
+ return;
473
+ if (e.identityHash && Object.values(view.elements).filter(other => other.identityHash === e.identityHash).length !== 1)
474
+ return;
475
+ return { target, stamp: screenHash(JSON.stringify({ target, role: e.role, reference: e.referenceHash, identity: e.identityHash,
476
+ bounds: e.bounds, label: e.labelHash, form: e.form, submit: e.submit,
477
+ origin: view.origin, application: view.application, titles: view.titles.map(screenHash), ...(!pointer ? { focus: view.focused } : {}) })) };
478
+ }
479
+ export function screenTargetStamp(toolName, input, view) {
480
+ return screenTargetEvidence(toolName, input, view)?.stamp;
481
+ }
482
+ export function screenResponseEvidence(actionTool, action, observationTool, observation, response) {
483
+ const o = screenObject(response);
484
+ if (o?.isError || o?.error !== undefined)
485
+ return;
486
+ const view = screenFromResult(observationTool, response, responseStrings(response).join('\n'), observation);
487
+ return view ? screenTargetEvidence(actionTool, action, { at: new Date().toISOString(), tool: observationTool, ...view }) : undefined;
488
+ }
489
+ /** Derive equality evidence from this response itself, never a concurrently replaced cached view. */
490
+ export function screenResponseStamp(actionTool, action, observationTool, observation, response) {
491
+ return screenResponseEvidence(actionTool, action, observationTool, observation, response)?.stamp;
492
+ }
493
+ /** The only keyboard exemptions are an exact copy/select-all chord in a simple known schema. Extra fields,
494
+ * malformed arrays and sequences never turn into a harmless chord by dropping values during extraction. */
495
+ function passiveShortcut(input) {
496
+ const fields = Object.entries(input).filter(([k]) => k !== 'action');
497
+ if (fields.length !== 1)
498
+ return false;
499
+ const [name, value] = fields[0];
500
+ if (!['key', 'keys', 'combo', 'combination', 'shortcut'].includes(name))
501
+ return false;
502
+ if (input.action !== undefined && (typeof input.action !== 'string' || !PASSIVE_KEY_ACTIONS.has(screenOperation(input.action))))
503
+ return false;
504
+ if (typeof value !== 'string' && !(Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string')))
505
+ return false;
506
+ const raw = Array.isArray(value) ? value.join('+') : value;
507
+ if (raw.length > 64 || /[^a-z+ ]/i.test(raw))
508
+ return false;
509
+ const chord = raw.trim().toLowerCase()
510
+ .replace(/\s*\+\s*/g, '+').replace(/^(command|meta|super|win)\+/, 'cmd+').replace(/^control\+/, 'ctrl+');
511
+ return /^(cmd|ctrl)\+[ac]$/.test(chord);
512
+ }
513
+ function keyTexts(input) {
514
+ return Object.entries(input).filter(([k]) => KEY_TEXT_ARGS.has(k.toLowerCase()))
515
+ .map(([, v]) => (typeof v === 'string' ? v : Array.isArray(v) ? v.filter(x => typeof x === 'string').join('+') : ''))
516
+ .filter(Boolean).map(t => t.slice(0, 2000));
517
+ }
518
+ export function guiOperation(toolName, input) {
519
+ const operation = (name) => {
520
+ const simple = name.replace(/^(browser|screen|computer|desktop|keyboard)_/, '');
521
+ if (/^(click|left_click|tap)$/.test(simple))
522
+ return 'click';
523
+ if (/^(type|type_text|input_text|insert_text|enter_text|fill)$/.test(simple))
524
+ return 'type';
525
+ if (/^(key|keys|key_press|keypress|press_key|press_keys|hotkey|shortcut|keyboard)$/.test(simple))
526
+ return 'key';
527
+ return undefined;
528
+ };
529
+ const name = screenOperation(toolName.split('__').slice(2).join('__'));
530
+ const action = typeof input.action === 'string' ? operation(screenOperation(input.action)) : undefined;
531
+ const tool = GENERIC_CONTROL.test(name) ? action : operation(name);
532
+ return tool && (input.action === undefined || action === tool) ? tool : undefined;
533
+ }
534
+ /** A credential read that a screen server's calls reveal: a credential path typed (into an open dialog, a
535
+ * terminal, an address bar) or a window or page title naming a credential file. Only the file name is kept. */
536
+ export function screenSecretRead(toolName, input, view) {
537
+ if (!toolName.startsWith('mcp__'))
538
+ return undefined;
539
+ const tool = screenOperation(toolName.split('__').slice(2).join('__') || toolName);
540
+ const action = typeof input.action === 'string' ? screenOperation(input.action) : '';
541
+ const name = (path) => { const b = basename(path.replace(/\\/g, '/')); return /^[A-Za-z0-9_.+-]{1,60}$/.test(b) ? b : 'a credential file'; };
542
+ if (keyControl(tool) || keyControl(action)) {
543
+ for (const t of keyTexts(input)) {
544
+ for (const raw of t.split(/[\s"'`]+/)) {
545
+ const token = raw.replace(/[,;:)]+$/, '');
546
+ if (/[/~.]/.test(token) && isSecretPath(token))
547
+ return `typed path ${name(token)}`;
548
+ }
549
+ }
550
+ }
551
+ for (const title of view?.titles ?? []) {
552
+ for (const token of title.split(/[\s"'()|·–—]+/))
553
+ if (token && isSecretPath(token))
554
+ return `screen title naming ${name(token)}`;
555
+ }
556
+ return undefined;
557
+ }
558
+ //# sourceMappingURL=flow-guard-screen.js.map