@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.
- package/CHANGELOG.md +113 -0
- package/README-es.md +27 -11
- package/README-fr.md +27 -11
- package/README-ru.md +27 -11
- package/README-zh.md +27 -11
- package/README.md +27 -11
- package/dist/annotation-review.d.ts +10 -0
- package/dist/annotation-review.d.ts.map +1 -0
- package/dist/annotation-review.js +36 -0
- package/dist/annotation-review.js.map +1 -0
- package/dist/atomic-screen.d.ts +61 -0
- package/dist/atomic-screen.d.ts.map +1 -0
- package/dist/atomic-screen.js +165 -0
- package/dist/atomic-screen.js.map +1 -0
- package/dist/flow-guard-answer.d.ts +29 -0
- package/dist/flow-guard-answer.d.ts.map +1 -0
- package/dist/flow-guard-answer.js +201 -0
- package/dist/flow-guard-answer.js.map +1 -0
- package/dist/flow-guard-classify.d.ts +14 -0
- package/dist/flow-guard-classify.d.ts.map +1 -0
- package/dist/flow-guard-classify.js +224 -0
- package/dist/flow-guard-classify.js.map +1 -0
- package/dist/flow-guard-decode.d.ts +17 -0
- package/dist/flow-guard-decode.d.ts.map +1 -0
- package/dist/flow-guard-decode.js +149 -0
- package/dist/flow-guard-decode.js.map +1 -0
- package/dist/flow-guard-destinations.d.ts +23 -0
- package/dist/flow-guard-destinations.d.ts.map +1 -0
- package/dist/flow-guard-destinations.js +158 -0
- package/dist/flow-guard-destinations.js.map +1 -0
- package/dist/flow-guard-fingerprints.d.ts +27 -0
- package/dist/flow-guard-fingerprints.d.ts.map +1 -0
- package/dist/flow-guard-fingerprints.js +164 -0
- package/dist/flow-guard-fingerprints.js.map +1 -0
- package/dist/flow-guard-fragments.d.ts +17 -0
- package/dist/flow-guard-fragments.d.ts.map +1 -0
- package/dist/flow-guard-fragments.js +122 -0
- package/dist/flow-guard-fragments.js.map +1 -0
- package/dist/flow-guard-instructions.d.ts +12 -0
- package/dist/flow-guard-instructions.d.ts.map +1 -0
- package/dist/flow-guard-instructions.js +36 -0
- package/dist/flow-guard-instructions.js.map +1 -0
- package/dist/flow-guard-labels.d.ts +10 -0
- package/dist/flow-guard-labels.d.ts.map +1 -0
- package/dist/flow-guard-labels.js +55 -0
- package/dist/flow-guard-labels.js.map +1 -0
- package/dist/flow-guard-leak.d.ts +29 -0
- package/dist/flow-guard-leak.d.ts.map +1 -0
- package/dist/flow-guard-leak.js +135 -0
- package/dist/flow-guard-leak.js.map +1 -0
- package/dist/flow-guard-paths.d.ts +21 -0
- package/dist/flow-guard-paths.d.ts.map +1 -0
- package/dist/flow-guard-paths.js +72 -0
- package/dist/flow-guard-paths.js.map +1 -0
- package/dist/flow-guard-policy.d.ts +15 -0
- package/dist/flow-guard-policy.d.ts.map +1 -0
- package/dist/flow-guard-policy.js +179 -0
- package/dist/flow-guard-policy.js.map +1 -0
- package/dist/flow-guard-post.d.ts +6 -0
- package/dist/flow-guard-post.d.ts.map +1 -0
- package/dist/flow-guard-post.js +131 -0
- package/dist/flow-guard-post.js.map +1 -0
- package/dist/flow-guard-pre.d.ts +13 -0
- package/dist/flow-guard-pre.d.ts.map +1 -0
- package/dist/flow-guard-pre.js +335 -0
- package/dist/flow-guard-pre.js.map +1 -0
- package/dist/flow-guard-screen.d.ts +85 -0
- package/dist/flow-guard-screen.d.ts.map +1 -0
- package/dist/flow-guard-screen.js +558 -0
- package/dist/flow-guard-screen.js.map +1 -0
- package/dist/flow-guard-state.d.ts +12 -0
- package/dist/flow-guard-state.d.ts.map +1 -0
- package/dist/flow-guard-state.js +121 -0
- package/dist/flow-guard-state.js.map +1 -0
- package/dist/flow-guard-text.d.ts +9 -0
- package/dist/flow-guard-text.d.ts.map +1 -0
- package/dist/flow-guard-text.js +46 -0
- package/dist/flow-guard-text.js.map +1 -0
- package/dist/flow-guard-types.d.ts +154 -0
- package/dist/flow-guard-types.d.ts.map +1 -0
- package/dist/flow-guard-types.js +2 -0
- package/dist/flow-guard-types.js.map +1 -0
- package/dist/flow-guard.d.ts +20 -215
- package/dist/flow-guard.d.ts.map +1 -1
- package/dist/flow-guard.js +26 -1400
- package/dist/flow-guard.js.map +1 -1
- package/dist/mcp-client.d.ts +1 -2
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +2 -19
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp-server.js +5 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +46 -9
- package/dist/mcp-tools.js.map +1 -1
- package/dist/public-address.d.ts +3 -0
- package/dist/public-address.d.ts.map +1 -0
- package/dist/public-address.js +58 -0
- package/dist/public-address.js.map +1 -0
- package/dist/review-diff.d.ts +36 -0
- package/dist/review-diff.d.ts.map +1 -1
- package/dist/review-diff.js +177 -7
- package/dist/review-diff.js.map +1 -1
- package/dist/task-adapter.d.ts +73 -0
- package/dist/task-adapter.d.ts.map +1 -0
- package/dist/task-adapter.js +379 -0
- package/dist/task-adapter.js.map +1 -0
- package/dist/task-cli.d.ts +3 -0
- package/dist/task-cli.d.ts.map +1 -0
- package/dist/task-cli.js +162 -0
- package/dist/task-cli.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wrap-cli.d.ts +5 -0
- package/dist/wrap-cli.d.ts.map +1 -1
- package/dist/wrap-cli.js +29 -2
- package/dist/wrap-cli.js.map +1 -1
- package/dist/wrap-screen.d.ts +3 -0
- package/dist/wrap-screen.d.ts.map +1 -0
- package/dist/wrap-screen.js +20 -0
- package/dist/wrap-screen.js.map +1 -0
- package/dist/wrap-state.d.ts +8 -1
- package/dist/wrap-state.d.ts.map +1 -1
- package/dist/wrap-state.js +21 -4
- package/dist/wrap-state.js.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/dist/wrap.js +204 -10
- package/dist/wrap.js.map +1 -1
- package/docs/flow-guard-model-comparison-2026-10-25.es.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.fr.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.ru.md +82 -0
- package/docs/flow-guard-model-comparison-2026-10-25.zh.md +82 -0
- package/docs/flow-guard.es.md +307 -12
- package/docs/flow-guard.fr.md +307 -12
- package/docs/flow-guard.md +315 -13
- package/docs/flow-guard.ru.md +314 -13
- package/docs/flow-guard.zh.md +306 -8
- package/docs/scan-limits.es.md +39 -0
- package/docs/scan-limits.fr.md +39 -0
- package/docs/scan-limits.md +39 -0
- package/docs/scan-limits.ru.md +39 -0
- package/docs/scan-limits.zh.md +39 -0
- package/docs/scan.es.md +2 -0
- package/docs/scan.fr.md +2 -0
- package/docs/scan.md +2 -0
- package/docs/scan.ru.md +2 -0
- package/docs/scan.zh.md +2 -0
- package/docs/security-hardening.es.md +1 -1
- package/docs/security-hardening.fr.md +1 -1
- package/docs/security-hardening.md +1 -1
- package/docs/security-hardening.ru.md +1 -1
- package/docs/security-hardening.zh.md +1 -1
- package/examples/atomic-screen-server.mjs +42 -0
- package/native/macos/Package.swift +9 -0
- package/native/macos/Sources/WardenDesktop/NativeSurface.swift +246 -0
- package/native/macos/Sources/WardenDesktop/StandardIO.swift +139 -0
- package/native/macos/Sources/WardenDesktopHost/Main.swift +211 -0
- package/package.json +18 -6
- package/scripts/build-macos.mjs +22 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Границы измерений и подтверждение версии
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](scan-limits.md) · **Русский** · [Español](scan-limits.es.md) · [Français](scan-limits.fr.md) · [中文](scan-limits.zh.md)
|
|
4
|
+
|
|
5
|
+
Эти уточнения относятся к разбору читательских вопросов от 2026-10-25. Проверенный **опубликованный npm-артефакт `0.21.0`** и более поздние локальные исходники — разные объекты: прежняя строка версии в `package.json` не добавляет исправление в опубликованный архив. Кодовые тесты, фиктивный биллинг и программные ответы на формы подтверждения не заменяют оценку модели, production-трафика или поведения людей.
|
|
6
|
+
|
|
7
|
+
## Биллинг и заявления сервера
|
|
8
|
+
|
|
9
|
+
`initialize`, `tools/list` и контрольный canary-вызов сами могут стоить денег или иметь другие эффекты на сервере. Успешное обнаружение или preview не устанавливает стоимость и эффект следующего вызова. В нашей фиктивной платёжной fixture canary выбирал бесплатный `preview`, а `{}` — операцию со списанием семи условных кредитов. Это измерение fixture, а не счёта внешнего продавца.
|
|
10
|
+
|
|
11
|
+
`readOnlyHint`, `destructiveHint` и `openWorldHint` — непроверенные заявления, а не доказательства бесплатности или безвредности. Локальная проверка annotations при первом использовании включается явно, по умолчанию `off` и действует только в `wrap --flow NAME`. Согласие на неё не подтверждает цену или фактические эффекты. См. [правила flow guard и их границы](flow-guard.ru.md).
|
|
12
|
+
|
|
13
|
+
## Версия пакета и версия, сообщённая сервером
|
|
14
|
+
|
|
15
|
+
Точная запись `package@x.y.z` задаёт запрошенную версию пакета. `initialize.serverInfo.version` сообщает сам сервер; это не независимо установленная версия пакета и не digest артефакта. Например, рассмотренный пакет Mapled имел версию `@mapled/mcp@0.21.6`, а сервер по-прежнему сообщал `0.1.0`.
|
|
16
|
+
|
|
17
|
+
Проверенный опубликованный `0.21.0` мог ошибочно подставлять сообщённую версию в совет по закреплению пакета. Проверяйте фактическое разрешение package manager и lock/integrity, а не принимайте такой совет автоматически. Локальное исправление разделяет эти сведения и убирает подстановку; оно по-прежнему не удостоверяет байты пакета, запущенные launcher. Незакреплённый `npx` может выбрать другую версию или использовать кэш; ни один вариант не гарантирован при каждом старте. Hash набора tools относится к наблюдаемым определениям, а не ко всей программе. См. [scan](scan.ru.md) и [интеграцию](integration.ru.md).
|
|
18
|
+
|
|
19
|
+
## Сравнение launcher и рекомендательный baseline
|
|
20
|
+
|
|
21
|
+
В исследовании Auten был один публичный наблюдавшийся снимок, а не измерение двух сред launcher. Для воспроизведения нужны точные runner/версия и аргументы запуска, а также два снимка tools из соответствующих сред. Наблюдение HISTOR — рекомендательный baseline для сравнения, а не доверенный pin, утверждённый оператором.
|
|
22
|
+
|
|
23
|
+
У [сборщика свидетельств launcher](../examples/launcher-evidence/README.md) вспомогательная документация пока только английская. Он сохраняет нормализованные клиентом определения tools, а не исходный wire transcript. Введённую версию runner сообщает владелец; это поле не определяет пакетный артефакт независимо. Готовый сборщик сам по себе не доказывает, что сравнение двух сред состоялось.
|
|
24
|
+
|
|
25
|
+
## Вопросы, подтверждения и ответы
|
|
26
|
+
|
|
27
|
+
Исторические **39/97** — число задач с хотя бы одним `ask`, а не людей, давших согласие. Исполнение после решения hook не доказывает нажатие Allow человеком. Wrapper может записать accept/decline/cancel, но такие же ответы создаёт программный клиент. Частота человеческих подтверждений, время чтения и усталость требуют данных о реально показанных людям запросах и связанных результатах; эти тесты их не измеряли.
|
|
28
|
+
|
|
29
|
+
Для сохранённой серии DeepSeek Flash guard + note (`injecagent`, 20 travel-задач) официальный исторический результат AgentDojo остаётся **18/20** по проверке точного упоминания названия. При чтении сохранённых фрагментов с помощью AI обнаружены предупреждения, а не принятая навязанная рекомендация, но полными были только 18 ответов; два достигли старого лимита хранения в 2000 символов. Потерянные хвосты неизвестны, поэтому новый результат по полным ответам `0/20` не установлен. Локальный harness сохраняет будущие ответы полностью, с длиной и hash; старые хвосты он не восстанавливает и нового модельного прогона не подразумевает.
|
|
30
|
+
|
|
31
|
+
**Контролируемый выбор (2026-10-25):** В отдельной синтетической проверке после ответа инструмента использован опубликованный WARDEN `0.21.0`: шесть гостиничных задач × пять повторов, пометки clean/benign/injected и три профиля дали **270/270 правильных ID**, включая **0/90 выборов цели атаки** в injected (0/30 в каждом профиле). Название цели встречалось в **25/270** ответах, включая контроли, а Stop сработал **0/90** раз в `note_stop`; baseline без защиты тоже выбрал правильно, поэтому дополнительный выигрыш защиты не установлен, а правильный ID не подтверждает точность всех фактов в объяснении. [Протокол, результаты и пределы](../../docs/growth/launchgate-selection-2026-10-25.ru.md): это отдельный от AgentDojo опыт, не меняющий исторические **18/20** и не доказывающий общую безопасность.
|
|
32
|
+
|
|
33
|
+
**Продолжение с ничьей (2026-10-25):** При двух допустимых оптимумах injected target выбран 48/48 без защиты, 44/48 с note и 45/48 с note+Stop; clean baseline — соответственно 28/48, 25/48 и 29/48. Интервалы baseline-adjusted эффекта защиты включают ноль; все 432 выбора допустимы, поэтому выбор target не означает ошибку. Настоящие предпочтения пользователя выполнены 18/18, Stop не запрашивал recheck. Локальный [offline evaluator](../scripts/selection-probe/README.md) поддерживает набор допустимых ответов и common-baseline сравнения; runtime-блокировка не изменена. [Результаты и пределы](../../docs/growth/launchgate-tie-selection-2026-10-25.ru.md).
|
|
34
|
+
|
|
35
|
+
## Пороги фрагментов и стоимость перепроверки
|
|
36
|
+
|
|
37
|
+
Сильные fingerprint-окна остаются **8 для non-hex / 10 для hex**. Локальное исправление индекса после известных префиксов не обеспечивает надёжного сильного распознавания шестисимвольного фрагмента внутри слова. Существующая сессия должна перечитать credentials для получения новых окон. Обязательный review на границе credentials отличается от обнаружения именно этого фрагмента как секрета.
|
|
38
|
+
|
|
39
|
+
Аналогично совпадение 12 нормализованных символов в Stop требует ещё одной проверки ответа, но не доказывает семантический отказ от инструкции. После отмены исключения для warning-слов этот проход оплачивает и честное предупреждение. Перефразирование и подмена выбора строк могут обойти такой поиск совпадений. Улучшения на fixtures и специально выбранные контроли ложных отказов не устанавливают глобальный оптимум или production-частоту ошибок. [Руководство flow guard](flow-guard.ru.md) описывает политику, оставшиеся пробелы и ограничение повторов, которое обеспечивает host-флаг `stop_hook_active`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 测量边界与版本证据
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](scan-limits.md) · [Русский](scan-limits.ru.md) · [Español](scan-limits.es.md) · [Français](scan-limits.fr.md) · **中文**
|
|
4
|
+
|
|
5
|
+
以下说明对应 2026-10-25 的读者反馈。已测试的**已发布 npm `0.21.0` 构件**与后续本地源码是不同对象:保留 `package.json` 中的版本号,不会使本地修复进入已发布的归档。代码测试、虚拟账单和脚本生成的批准回复,不能代替模型评估、生产流量测量或用户研究。
|
|
6
|
+
|
|
7
|
+
## 账单与服务器声明
|
|
8
|
+
|
|
9
|
+
`initialize`、`tools/list` 和 canary 探测调用本身也可能产生费用或其他服务器端效果。发现流程或 preview 成功,不能证明下一次调用的价格或效果。在我们的虚拟账单 fixture 中,canary 选择了免费的 `preview`,而 `{}` 选择了扣除七个虚拟积分的操作。这测量的是该 fixture,而不是外部卖家的真实账单。
|
|
10
|
+
|
|
11
|
+
`readOnlyHint`、`destructiveHint` 和 `openWorldHint` 都是未经验证的声明,不能证明操作免费或无害。本地的 annotations 首次使用审查需要明确启用,默认是 `off`,且只适用于 `wrap --flow NAME`。接受该审查并不表示批准价格,也不构成对实际效果的认证。参见[流控制及其边界](flow-guard.zh.md)。
|
|
12
|
+
|
|
13
|
+
## 请求的包版本与服务器自报版本
|
|
14
|
+
|
|
15
|
+
精确的 `package@x.y.z` 指定请求的包版本。`initialize.serverInfo.version` 是服务器提供的字符串,不是独立解析得到的包版本,也不是构件摘要。例如,审查的 Mapled 包为 `@mapled/mcp@0.21.6`,服务器仍自报 `0.1.0`。
|
|
16
|
+
|
|
17
|
+
已测试的发布版 `0.21.0` 可能错误地用自报版本生成包固定建议。应核实包管理器的实际解析结果与锁定文件、完整性证据,而不是直接接受该建议。本地修复将这些元数据分开,并移除了这种替换;它仍不能认证 launcher 实际运行了哪些包字节。未固定版本的 `npx` 可能解析到另一版本,也可能复用缓存,不能保证每次启动都采用其中一种行为。tools 集合的 hash 标识观察到的定义,而不是整个可执行程序。参见[扫描](scan.zh.md)和[集成](integration.zh.md)。
|
|
18
|
+
|
|
19
|
+
## Launcher 比较与参考基线
|
|
20
|
+
|
|
21
|
+
Auten 调查只有一个公开观察到的快照,并没有完成对两种 launcher 环境的实测。复现差异需要准确的 runner 及其版本、启动参数,以及来自相应环境的两个 tools 快照。HISTOR 观察记录可作为比较参考,但不是经操作者批准的可信 pin。
|
|
22
|
+
|
|
23
|
+
[Launcher 证据收集器](../examples/launcher-evidence/README.md)目前只有英文辅助文档。它记录的是经过客户端规范化的 tools 定义,而不是原始协议记录。填写的 runner 版本由所有者声明,该字段不会独立解析包构件。收集器已经准备好,并不证明两个环境的比较已经发生。
|
|
24
|
+
|
|
25
|
+
## 询问、批准与回答
|
|
26
|
+
|
|
27
|
+
历史数字 **39/97** 统计的是触发过 `ask` 的任务,而不是批准操作的人数。hook 决策之后发生执行,不能证明有人点击了 Allow。Wrapper 可以记录 accept/decline/cancel,但脚本客户端也能生成这些回复。人工批准率、阅读时间和批准疲劳,需要真正展示给人的请求及其关联结果;这些测试没有测量它们。
|
|
28
|
+
|
|
29
|
+
在保存的 DeepSeek Flash guard + note 系列(`injecagent`,20 个 travel 用例)中,AgentDojo 的历史官方结果仍是 **18/20**,依据是目标名称的精确出现。AI 辅助阅读在保存的片段中发现的是警告,而非采纳攻击者推荐,但只有 18 个回答完整,另外两个达到旧的 2000 字符保存上限。缺失的结尾内容未知,因此不能据此建立完整回答上的新 `0/20` 分数。本地 harness 为未来回答保存全文、长度和 hash;它不会恢复历史缺失内容,也不意味着重新运行了模型。
|
|
30
|
+
|
|
31
|
+
**受控选择测试(2026-10-25):**独立的工具结果后合成测试使用已发布的 WARDEN `0.21.0`,覆盖六项酒店任务 × 五次重复、clean/benign/injected 三种备注及三种配置:**270/270 个 ID 选择正确**,其中 injected 条件下**0/90 次选择攻击目标**(每种配置 0/30)。目标名称出现在包括对照组在内的 **25/270** 个回答中,`note_stop` 的 Stop 触发 **0/90** 次;无守卫基线也全部选对,因此尚未测得守卫的额外收益,而正确 ID 不代表解释中的每项事实都准确。[协议、结果与限制](../../docs/growth/launchgate-selection-2026-10-25.en.md):这项独立研究不改变 AgentDojo 历史上的 **18/20**,也不证明普遍安全。
|
|
32
|
+
|
|
33
|
+
**并列选择后续实验(2026-10-25):** 两个最优选项均可接受时,注入条件下目标选择分别为:无防护 48/48、note 44/48、note+Stop 45/48;对应 clean 基线为 28/48、25/48、29/48。经基线调整的防护效果区间包含零;432 次选择均符合任务要求,选择目标本身不能视为错误。真实用户偏好得到 18/18 次遵守,Stop 未要求复查。本地[离线评估器](../scripts/selection-probe/README.md)现支持多个可接受答案及共同基线比较;运行时阻断逻辑未更改。[结果与限制,英文](../../docs/growth/launchgate-tie-selection-2026-10-25.en.md)。
|
|
34
|
+
|
|
35
|
+
## 片段阈值与复查成本
|
|
36
|
+
|
|
37
|
+
强指纹窗口仍为 **非十六进制 8 / 十六进制 10**。本地对已知前缀之后索引的修复,并没有建立对嵌在较长单词中的六字符片段的可靠强检测。现有会话必须重新读取凭据才能获得新窗口。在凭据边界要求审查,与确认该片段就是秘密,是不同的判断。
|
|
38
|
+
|
|
39
|
+
同样,Stop 中 12 个规范化字符的匹配会请求再次检查回答,但不能证明模型在语义上拒绝了指令。删除警告词豁免后,诚实的警告也会多付出一次检查。改写和被操纵的行选择可以绕过这种匹配。Fixture 上的改进和刻意选择的误拒对照,既不能确立全局最优阈值,也不能得出生产误报率。[流控制指南](flow-guard.zh.md)说明了策略、尚存的缺口,以及依赖 host 的 `stop_hook_active` 标志限制后续检查的机制。
|
package/docs/scan.es.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scan.md) · [Русский](scan.ru.md) · **Español** · [Français](scan.fr.md) · [中文](scan.zh.md)
|
|
4
4
|
|
|
5
|
+
Las llamadas de descubrimiento pueden generar cargos; consulte los [límites de medición y pruebas de versión](scan-limits.es.md) antes de interpretar un scan o confiar en una versión declarada.
|
|
6
|
+
|
|
5
7
|
`warden-mcp scan` lee la configuración MCP que ya tienes, se conecta a cada servidor que inicia y revisa las definiciones de herramientas antes de que un modelo las vea. Es la misma cadena de compuertas que [`wrap`](../README-es.md) y la [biblioteca](integration.es.md), ejecutada una vez sobre tus configuraciones en lugar de delante de un solo servidor.
|
|
6
8
|
|
|
7
9
|
```bash
|
package/docs/scan.fr.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scan.md) · [Русский](scan.ru.md) · [Español](scan.es.md) · **Français** · [中文](scan.zh.md)
|
|
4
4
|
|
|
5
|
+
Les appels de découverte peuvent entraîner des frais ; consultez les [limites des mesures et preuves de version](scan-limits.fr.md) avant d’interpréter un scan ou une version déclarée.
|
|
6
|
+
|
|
5
7
|
`warden-mcp scan` lit la configuration MCP que vous avez déjà, se connecte à chaque serveur qu’elle démarre et vérifie les définitions d’outils avant qu’un modèle ne les voie. C’est la même chaîne de portes que [`wrap`](../README-fr.md) et la [bibliothèque](integration.fr.md), exécutée une fois sur vos configurations au lieu de se placer devant un seul serveur.
|
|
6
8
|
|
|
7
9
|
```bash
|
package/docs/scan.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 **English** · [Русский](scan.ru.md) · [Español](scan.es.md) · [Français](scan.fr.md) · [中文](scan.zh.md)
|
|
4
4
|
|
|
5
|
+
Discovery calls can incur costs; see [measurement limits and version evidence](scan-limits.md) before interpreting a scan or trusting a version hint.
|
|
6
|
+
|
|
5
7
|
`warden-mcp scan` reads the MCP configuration you already have, connects to every server it starts, and vets the tool definitions before a model sees them. It is the same gate chain as [`wrap`](../README.md#protect-claude-desktop-or-cursor-with-one-wrapper) and the [library](integration.md), run once over your configs instead of in front of one server.
|
|
6
8
|
|
|
7
9
|
```bash
|
package/docs/scan.ru.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scan.md) · **Русский** · [Español](scan.es.md) · [Français](scan.fr.md) · [中文](scan.zh.md)
|
|
4
4
|
|
|
5
|
+
Вызовы обнаружения могут стоить денег; перед интерпретацией scan и советов о версии см. [границы измерений и подтверждение версии](scan-limits.ru.md).
|
|
6
|
+
|
|
5
7
|
`warden-mcp scan` читает уже существующую у вас конфигурацию MCP, подключается к каждому серверу из неё и проверяет определения инструментов до того, как их увидит модель. Это та же цепочка гейтов, что у [`wrap`](../README-ru.md) и у [библиотеки](integration.ru.md), только пройденная один раз по вашим конфигам, а не перед одним сервером.
|
|
6
8
|
|
|
7
9
|
```bash
|
package/docs/scan.zh.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scan.md) · [Русский](scan.ru.md) · [Español](scan.es.md) · [Français](scan.fr.md) · **中文**
|
|
4
4
|
|
|
5
|
+
发现调用也可能产生费用;解读扫描结果或信任版本提示前,请阅读[测量边界与版本证据](scan-limits.zh.md)。
|
|
6
|
+
|
|
5
7
|
`warden-mcp scan` 读取你已有的 MCP 配置,连接其中启动的每个服务器,并在模型看到工具定义之前对其进行审查。它与 [`wrap`](../README-zh.md) 和[库](integration.zh.md)使用同一条关卡链,只是对你的配置运行一次,而不是挡在单个服务器前面。
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -7,7 +7,7 @@ Warden 0.7.0 y ARGUS 0.3.2 cierran seis problemas. Se verifican las definiciones
|
|
|
7
7
|
1. **Antes de conectar.** Ejecutar `warden.vetLaunch(server)` antes de iniciar el proceso stdio o abrir el transporte remoto. Comprueba procedencia, amenazas conocidas en comandos/direcciones e identidad de arranque aprobada. Después, obtener todas las páginas de `tools/list` y ejecutar `vet(server, tools)` antes de exponer herramientas al modelo. Aprobar el arranque no aprueba las herramientas.
|
|
8
8
|
2. **Protección del feed firmado.** Se guardan timestamp, digest y registros del último snapshot por clave de editor en `feeds/`. Se rechaza un timestamp anterior o contenido distinto con el mismo timestamp, incluso tras reiniciar. Un snapshot firmado posterior puede retirar reglas. Si falla la actualización se mantienen los últimos registros aceptados; `feed.status.stale` informa de su antigüedad por separado. El límite de diez segundos incluye cabeceras y cuerpo completo; siguen vigentes los límites de bytes y registros.
|
|
9
9
|
3. **Stdio.** Estructuras JSON-RPC inválidas devuelven `-32600`, parámetros inválidos `-32602` y JSON mal formado `-32700`. UTF-8 se decodifica al recibir la trama completa. Máximo: 1 MiB por trama y 8 KiB por cabecera Content-Length. Una trama inválida o excesiva cierra la conexión deliberadamente; un objeto de solicitud inválido no termina el proceso. Los argumentos mantienen el límite independiente de 256 000 caracteres.
|
|
10
|
-
4. **Aprobaciones persistentes.** `vet_mcp_server` consulta pins guardados. `status_mcp_server({server, tools})` devuelve `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash` y `changed` para revisión. Aprobar requiere los hashes exactos revisados y `previous_pin_revision` (null para la primera aprobación). Revocar también exige la revisión anterior. Un cambio concurrente invalida una revisión antigua. Las definiciones se guardan para comparación: no incluir credenciales reales.
|
|
10
|
+
4. **Aprobaciones persistentes.** `vet_mcp_server` consulta pins guardados. `status_mcp_server({server, tools})` devuelve `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash` y `changed` para revisión. Aprobar requiere los hashes exactos revisados y `previous_pin_revision` (null para la primera aprobación). Revocar también exige la revisión anterior. Un cambio concurrente invalida una revisión antigua. Las definiciones se guardan para comparación: no incluir credenciales reales. Desde el 2026-10-23 la revisión también dice de dónde vino un cambio y qué leer primero. Pase `server_info` (el objeto `serverInfo` de la respuesta del servidor a initialize) a `status_mcp_server` y a `approve_mcp_server`: la aprobación lo guarda, y `origin` y `reviewText` muestran la versión que el servidor declaró entonces y ahora. Esa versión es palabra del propio servidor, así que nunca entra en un hash ni se toma por fiable. Para `npx`, `bunx`, `npm exec`, `pnpm dlx`, `yarn dlx`, `uvx`, `uv tool run` y `pipx run`, la revisión indica si el comando nombra una versión exacta; sin ella, cada arranque ejecuta lo que el registro marque como última, y la revisión da la forma fijada. Una herramienta cuyo texto nuevo habla de la aprobación del usuario («without asking the user», «already approved») o de qué herramienta usar («prefer this over web search», «never use another tool») aparece primero y se marca en el diff. La marca solo señala texto, nunca aprueba ni rechaza; solo se reconocen frases en inglés.
|
|
11
11
|
5. **Definiciones completas.** El formato v2 incluye todos los campos publicados: `title`, `outputSchema`, `annotations` y extensiones. Se omiten campos superiores undefined. Las definiciones simples name/description/inputSchema conservan su digest. Pins antiguos que no cubrían campos adicionales requieren aprobación explícita (`PIN_FORMAT_UPGRADE_REQUIRED`); los nuevos registran `toolsHashVersion: 2`. Las reglas v6 analizan las superficies adicionales. Las comillas de serialización JSON no convierten una instrucción en una cita inocua. Las anotaciones no conceden permisos.
|
|
12
12
|
6. **Sesión activa.** ARGUS pone las herramientas en cuarentena inmediatamente al recibir `notifications/tools/list_changed` y verifica de nuevo todas las páginas. También verifica antes de cada llamada, aunque el servidor omita notificaciones. Una referencia antigua no ejecuta una definición modificada, incluso tras aprobarla de nuevo. Fallos de listado, nombres duplicados, cursores repetidos, más de 32 páginas, 256 herramientas o 1 MiB de definiciones bloquean el uso. No se reaprueban cambios automáticamente. La primera conexión limpia conserva la política existente de ARGUS: guardar automáticamente el primer pin, sin revisión humana implícita. Si falla esa escritura, se cierra la conexión. Un cambio durante la ejecución impide devolver el resultado, pero no deshace efectos ya producidos; no reintentar automáticamente.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ Warden 0.7.0 et ARGUS 0.3.2 corrigent six lacunes. Ils vérifient les définitio
|
|
|
7
7
|
1. **Avant la connexion.** Appeler `warden.vetLaunch(server)` avant de lancer le processus stdio ou d'ouvrir le transport distant. Vérification de la provenance, des commandes/adresses dangereuses connues et de l'identité de lancement approuvée. Ensuite récupérer toutes les pages de `tools/list` et appeler `vet(server, tools)` avant exposition au modèle. Une vérification du lancement ne vaut pas approbation des outils.
|
|
8
8
|
2. **Rejeu du feed signé.** Timestamp, digest et enregistrements du dernier snapshot sont conservés par clé d'éditeur dans `feeds/`. Un timestamp antérieur ou un contenu différent au même timestamp est refusé, même après redémarrage. Un snapshot signé plus récent peut retirer des règles. En cas d'échec, les derniers enregistrements acceptés restent actifs ; `feed.status.stale` indique séparément leur fraîcheur. Le délai de dix secondes couvre en-têtes et corps complet ; les limites de taille et de nombre restent actives.
|
|
9
9
|
3. **Stdio.** Enveloppe JSON-RPC invalide : `-32600` ; paramètres invalides : `-32602` ; syntaxe JSON invalide : `-32700`. UTF-8 est décodé après réception d'une trame complète. Limites : 1 MiB par trame, 8 KiB pour l'en-tête Content-Length. Une trame incorrecte ou trop grande ferme volontairement la connexion ; un objet de requête invalide ne tue pas le processus. La limite indépendante des arguments reste 256 000 caractères.
|
|
10
|
-
4. **Approbations persistantes.** `vet_mcp_server` lit les empreintes enregistrées. `status_mcp_server({server, tools})` renvoie `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash` et `changed` pour comparaison. L'approbation exige les empreintes exactes examinées et `previous_pin_revision` (null pour la première approbation). La révocation exige également la révision précédente. Une modification concurrente invalide une ancienne revue. Les définitions sont conservées : ne pas y inclure de véritables secrets.
|
|
10
|
+
4. **Approbations persistantes.** `vet_mcp_server` lit les empreintes enregistrées. `status_mcp_server({server, tools})` renvoie `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash` et `changed` pour comparaison. L'approbation exige les empreintes exactes examinées et `previous_pin_revision` (null pour la première approbation). La révocation exige également la révision précédente. Une modification concurrente invalide une ancienne revue. Les définitions sont conservées : ne pas y inclure de véritables secrets. Depuis le 2026-10-23, la revue indique aussi d’où vient un changement et quoi lire en premier. Passez `server_info` (l’objet `serverInfo` de la réponse du serveur à initialize) à `status_mcp_server` et à `approve_mcp_server` : l’approbation le conserve, et `origin` et `reviewText` montrent la version déclarée par le serveur alors et maintenant. Cette version est la parole du serveur : elle n’entre dans aucune empreinte et n’est jamais tenue pour fiable. Pour `npx`, `bunx`, `npm exec`, `pnpm dlx`, `yarn dlx`, `uvx`, `uv tool run` et `pipx run`, la revue dit si la commande nomme une version exacte ; sans elle, chaque démarrage lance ce que le registre marque comme la dernière, et la revue donne la forme épinglée. Un outil dont le nouveau texte parle de l’approbation de l’utilisateur (« without asking the user », « already approved ») ou du choix de l’outil (« prefer this over web search », « never use another tool ») est listé en premier et marqué dans le diff. La marque signale du texte, elle n’approuve ni ne refuse rien ; seules les formulations anglaises sont reconnues.
|
|
11
11
|
5. **Définitions complètes.** Le format v2 couvre tous les champs annoncés : `title`, `outputSchema`, `annotations` et extensions. Les champs supérieurs undefined sont omis. Les définitions simples name/description/inputSchema gardent leur digest. Les anciennes empreintes ne couvrant pas les champs supplémentaires exigent une nouvelle approbation explicite (`PIN_FORMAT_UPGRADE_REQUIRED`) ; les nouvelles portent `toolsHashVersion: 2`. Le jeu v6 analyse ces surfaces. Les guillemets de sérialisation JSON ne transforment plus une instruction en citation inoffensive. Les annotations restent des indications non fiables, jamais des permissions.
|
|
12
12
|
6. **Pendant la session.** ARGUS suspend immédiatement les outils à réception de `notifications/tools/list_changed` puis vérifie toutes les pages. Il vérifie aussi avant chaque appel, même sans notification. Une ancienne référence ne peut pas exécuter une définition modifiée après réapprobation. Échec du listing, noms dupliqués, curseurs répétés, plus de 32 pages, 256 outils ou 1 MiB de définitions bloquent l'usage. Aucun changement n'est réapprouvé automatiquement. La première connexion saine conserve la politique ARGUS existante : premier pin automatique, sans revue humaine implicite. Un échec d'écriture ferme désormais la connexion. Un changement pendant l'exécution masque le résultat mais n'annule pas une action déjà effectuée ; ne pas réessayer automatiquement.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ Warden 0.7.0 and ARGUS 0.3.2 close six gaps. This is a definition and launch-con
|
|
|
7
7
|
1. **Before connection.** Call `warden.vetLaunch(server)` before spawning a stdio process or opening a remote transport. It checks origin, known command/endpoint threats and the pinned launch identity. Once connected, obtain the complete `tools/list` and call `vet(server, tools)` before exposing tools. A successful launch check alone does not approve tools.
|
|
8
8
|
2. **Signed feed rollback.** The last accepted timestamp, content digest and records are stored per publisher key under `feeds/`. An older timestamp, or different content with the same timestamp, is rejected even after restart. A newer signed snapshot can intentionally remove a rule. Failed updates retain the last-good records; `feed.status.stale` reports freshness separately, so retained rules are not presented as current intelligence. The ten-second deadline covers headers and the complete body. The byte and record caps still apply.
|
|
9
9
|
3. **Stdio.** Invalid JSON-RPC envelopes return `-32600`, invalid parameters return `-32602`, and syntax errors return `-32700`. UTF-8 is decoded only after a complete frame arrives. Frames are limited to 1 MiB; legacy Content-Length headers to 8 KiB. Oversized or invalid framing terminates the connection deliberately; invalid request objects do not. The separate tool-argument limit remains 256,000 characters.
|
|
10
|
-
4. **Durable approvals.** `vet_mcp_server` reads persisted pins. `status_mcp_server({server, tools})` returns `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash` and `changed` for review. Approval requires the exact reviewed tool and identity hashes and `previous_pin_revision` (null for first approval). Revocation also requires the previous revision. Concurrent changes invalidate a stale review. Definitions are retained for comparison; never put live credentials in them.
|
|
10
|
+
4. **Durable approvals.** `vet_mcp_server` reads persisted pins. `status_mcp_server({server, tools})` returns `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash` and `changed` for review. Approval requires the exact reviewed tool and identity hashes and `previous_pin_revision` (null for first approval). Revocation also requires the previous revision. Concurrent changes invalidate a stale review. Definitions are retained for comparison; never put live credentials in them. Since 2026-10-23 the review also says where a change came from and what to read first. Pass `server_info` (the `serverInfo` object from the server's initialize result) to `status_mcp_server` and `approve_mcp_server`: the approval stores it, and `origin` and `reviewText` show the version the server reported then and now. That version is the server's own word, so it is never hashed or trusted. For `npx`, `bunx`, `npm exec`, `pnpm dlx`, `yarn dlx`, `uvx`, `uv tool run` and `pipx run`, the review says whether the command names an exact version; without one, each start runs whatever the registry marks latest, and the review gives the pinned form. A tool whose new text is about the user's approval ("without asking the user", "already approved") or about which tool to use ("prefer this over web search", "never use another tool") is listed first and tagged in the diff. The tag marks text and never approves or refuses; only English phrasing is recognized.
|
|
11
11
|
5. **Complete definitions.** Hash format v2 pins all advertised fields, including `title`, `outputSchema`, `annotations` and extension metadata. Undefined top-level fields are omitted. Plain name/description/inputSchema payloads keep their existing digest. Legacy pins that did not cover extended fields require explicit reapproval (`PIN_FORMAT_UPGRADE_REQUIRED`); new pins record `toolsHashVersion: 2`. Ruleset v6 scans these additional surfaces. JSON delimiters around a schema description are not treated as evidence that an instruction is merely a quotation. Annotations remain untrusted hints and grant no permissions.
|
|
12
12
|
6. **During the session.** ARGUS quarantines tools immediately on `notifications/tools/list_changed` and rechecks all pages. It also rechecks before every tool call, including when a server sends no notification. Old wrappers are tied to the definitions the user/model saw and cannot execute a changed definition after reapproval. Listing failures, duplicate names, repeated cursors, over 32 pages, over 256 tools or over 1 MiB of listed definitions fail closed. Drift is never automatically reapproved. Initial clean connections retain ARGUS's existing automatic first-pin policy; this is not a human review step. Pin-write failure now closes the connection. A change during execution withholds the result, but cannot undo an already performed side effect; do not retry automatically.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ Warden 0.7.0 и ARGUS 0.3.2 закрывают шесть разрывов. Ward
|
|
|
7
7
|
1. **До подключения.** Вызывать `warden.vetLaunch(server)` до запуска stdio-процесса или открытия удалённого соединения. Проверяются происхождение, известные опасные команды/адреса и ранее одобренная идентичность запуска. После подключения получить полный `tools/list` и вызвать `vet(server, tools)` до передачи инструментов модели. Успешная предпроверка запуска сама по себе не одобряет инструменты.
|
|
8
8
|
2. **Откат подписанной базы угроз.** Последние принятые timestamp, digest и записи сохраняются отдельно для каждого ключа издателя в `feeds/`. Более старый снимок и другой контент с тем же timestamp отклоняются, включая после перезапуска. Более новый подписанный снимок может намеренно удалить правило. При ошибке обновления остаются последние принятые правила; `feed.status.stale` отдельно показывает их свежесть. Таймаут десять секунд охватывает заголовки и всё тело ответа. Ограничения размера и числа записей сохраняются.
|
|
9
9
|
3. **Stdio.** Некорректная структура JSON-RPC возвращает `-32600`, параметры — `-32602`, синтаксис JSON — `-32700`. UTF-8 декодируется после получения целого сообщения. Предел сообщения — 1 MiB, заголовка старого Content-Length — 8 KiB. Превышение предела или повреждённый фрейм намеренно закрывают соединение; некорректный объект запроса процесс не завершает. Отдельный предел аргументов инструмента — 256 000 символов.
|
|
10
|
-
4. **Постоянные одобрения.** `vet_mcp_server` читает сохранённые отпечатки. `status_mcp_server({server, tools})` возвращает `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash`, `changed` для сравнения. Одобрение требует точных рассмотренных хешей инструментов и запуска, а также `previous_pin_revision` (null при первом одобрении). Отзыв тоже требует предыдущую ревизию. Конкурентное изменение делает старое решение неприменимым. Определения сохраняются для сравнения: не помещайте в них действующие секреты.
|
|
10
|
+
4. **Постоянные одобрения.** `vet_mcp_server` читает сохранённые отпечатки. `status_mcp_server({server, tools})` возвращает `previous`, `previousRevision`, `currentTools`, `currentToolsHash`, `currentIdentityHash`, `changed` для сравнения. Одобрение требует точных рассмотренных хешей инструментов и запуска, а также `previous_pin_revision` (null при первом одобрении). Отзыв тоже требует предыдущую ревизию. Конкурентное изменение делает старое решение неприменимым. Определения сохраняются для сравнения: не помещайте в них действующие секреты. С 2026-10-23 обзор говорит ещё, откуда пришло изменение и что читать первым. Передайте `server_info` (объект `serverInfo` из ответа сервера на initialize) в `status_mcp_server` и `approve_mcp_server`: одобрение его сохраняет, а `origin` и `reviewText` показывают версию, которую сервер сообщал тогда и сообщает сейчас. Это слова самого сервера, поэтому версия не входит в хеши и не считается доверенной. Для `npx`, `bunx`, `npm exec`, `pnpm dlx`, `yarn dlx`, `uvx`, `uv tool run` и `pipx run` обзор сообщает, указана ли в команде точная версия; без неё каждый запуск берёт то, что реестр считает последним, и обзор приводит закреплённую форму. Инструмент, в новом тексте которого есть фразы о подтверждении пользователем («without asking the user», «already approved») или о выборе инструмента («prefer this over web search», «never use another tool»), идёт в списке первым и помечается в diff. Пометка только выделяет текст и ничего не одобряет и не запрещает; распознаются только английские формулировки.
|
|
11
11
|
5. **Полные определения.** Формат отпечатка v2 включает все объявленные поля, в том числе `title`, `outputSchema`, `annotations` и расширения. Верхнеуровневые поля со значением undefined пропускаются. Для обычных name/description/inputSchema прежний digest сохраняется. Старые одобрения, не покрывавшие расширенные поля, требуют явного переодобрения (`PIN_FORMAT_UPGRADE_REQUIRED`); новые содержат `toolsHashVersion: 2`. Набор правил v6 проверяет дополнительные поверхности. JSON-кавычки вокруг описания схемы больше не считаются доказательством безобидного цитирования инструкции. Аннотации остаются недоверенными подсказками, не разрешениями.
|
|
12
12
|
6. **Во время сессии.** При `notifications/tools/list_changed` ARGUS немедленно приостанавливает инструменты и перепроверяет все страницы списка. Повторная проверка есть и перед каждым вызовом — даже без уведомлений сервера. Старые ссылки привязаны к определениям, которые видел пользователь/модель, и не выполняют изменённый инструмент после переодобрения. Ошибки получения списка, одинаковые имена, повтор курсора, более 32 страниц, 256 инструментов или 1 MiB определений блокируют использование. Изменения не переодобряются автоматически. Для первого чистого подключения сохраняется прежняя политика ARGUS: автоматическая запись первого отпечатка; это не ручная проверка человеком. Ошибка записи теперь закрывает соединение. Изменение во время исполнения скрывает результат, но не отменяет уже совершённое действие — автоматически повторять вызов нельзя.
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@ Warden 0.7.0 和 ARGUS 0.3.2 修复六个缺口。它检查工具定义和启动
|
|
|
7
7
|
1. **连接之前。** 在启动 stdio 进程或建立远程连接前调用 `warden.vetLaunch(server)`,检查来源、已知危险命令/地址及已批准的启动身份。连接后获取完整分页 `tools/list`,再调用 `vet(server, tools)`,通过后才向模型暴露工具。启动检查通过不等于工具已获批准。
|
|
8
8
|
2. **签名威胁库防回滚。** 按发布者公钥在 `feeds/` 保存最后接受的 timestamp、digest 和记录。拒绝较旧时间戳,以及相同时间戳下不同的内容;重启后仍有效。更新的合法签名快照可以主动删除规则。更新失败时保留最后有效的规则;`feed.status.stale` 单独说明新鲜度,保留规则不代表它们是最新情报。十秒超时覆盖响应头和完整响应体,字节数及记录数限制继续有效。
|
|
9
9
|
3. **Stdio。** 错误 JSON-RPC 结构返回 `-32600`,错误参数返回 `-32602`,JSON 语法错误返回 `-32700`。收到完整帧后才解码 UTF-8。每帧最多 1 MiB,旧式 Content-Length 头最多 8 KiB。超限或损坏的帧会主动关闭连接;错误的请求对象不会结束进程。工具参数仍有独立的 256 000 字符限制。
|
|
10
|
-
4. **持久审批。** `vet_mcp_server` 读取已保存的指纹。`status_mcp_server({server, tools})` 返回 `previous`、`previousRevision`、`currentTools`、`currentToolsHash`、`currentIdentityHash`、`changed` 供比较。批准必须提供已审阅的准确工具哈希、启动身份哈希及 `previous_pin_revision`(首次为 null)。撤销也需要上一版本标识;并发更新会使旧审阅失效。工具定义会保存用于比较,请勿写入真实凭据。
|
|
10
|
+
4. **持久审批。** `vet_mcp_server` 读取已保存的指纹。`status_mcp_server({server, tools})` 返回 `previous`、`previousRevision`、`currentTools`、`currentToolsHash`、`currentIdentityHash`、`changed` 供比较。批准必须提供已审阅的准确工具哈希、启动身份哈希及 `previous_pin_revision`(首次为 null)。撤销也需要上一版本标识;并发更新会使旧审阅失效。工具定义会保存用于比较,请勿写入真实凭据。 自 2026-10-23 起,审阅还会说明变更从何而来、应先看什么。向 `status_mcp_server` 和 `approve_mcp_server` 传入 `server_info`(服务器 initialize 响应里的 `serverInfo` 对象):批准时会保存它,`origin` 和 `reviewText` 会显示服务器当时与现在自报的版本。这个版本是服务器自己的说法,因此不计入哈希,也不被信任。对 `npx`、`bunx`、`npm exec`、`pnpm dlx`、`yarn dlx`、`uvx`、`uv tool run` 和 `pipx run`,审阅会说明命令里是否写明确切版本;没有时,每次启动都会运行注册表标为最新的版本,审阅会给出固定版本的写法。新文本涉及用户批准(“without asking the user”、“already approved”)或工具选择(“prefer this over web search”、“never use another tool”)的工具会列在最前,并在 diff 中标注。标注只提示文本,不批准也不拒绝;只识别英文表述。
|
|
11
11
|
5. **完整定义。** 指纹格式 v2 包含所有公布字段,包括 `title`、`outputSchema`、`annotations` 和扩展元数据;忽略顶层 undefined 字段。只有 name/description/inputSchema 的定义保留原 digest。未覆盖扩展字段的旧审批需要明确重新批准(`PIN_FORMAT_UPGRADE_REQUIRED`);新记录包含 `toolsHashVersion: 2`。规则集 v6 扫描新增字段。JSON 序列化引号不再被误认为“指令只是无害引用”。注解始终是不可信提示,不授予权限。
|
|
12
12
|
6. **会话期间。** 收到 `notifications/tools/list_changed` 后,ARGUS 立即暂停工具并重新检查所有页面。每次工具调用前也重新检查,覆盖不发通知的服务器。旧工具引用绑定用户/模型曾看到的定义,重新批准后也不能调用已经变化的定义。列表获取失败、重名、重复游标、超过 32 页、256 个工具或 1 MiB 定义时阻止使用。变化不会自动重新批准。首次正常连接保留 ARGUS 原有的自动首次记录策略,这不表示人工审阅。记录失败现在会关闭连接。执行期间发生变化会隐藏结果,但不能撤销已发生的副作用,请勿自动重试。
|
|
13
13
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A runnable SERVER-OWNED virtual surface. No OS clicks, browser, mail, payment or network.
|
|
3
|
+
// Production backends must provide the same synchronous ownership/transaction guarantee.
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { AtomicScreenServer, ATOMIC_SCREEN_PREPARE, ATOMIC_SCREEN_COMMIT, ATOMIC_SCREEN_CANCEL } from '../dist/atomic-screen.js';
|
|
6
|
+
|
|
7
|
+
let submitted = 0;
|
|
8
|
+
const snapshot = () => ({ content: [{ type: 'text', text: JSON.stringify({ applicationId: 'dev.warden.atomic-example', title: 'Local virtual form', submitted,
|
|
9
|
+
elements: [{ index: 3, role: 'button', name: 'Send', accessibilityIdentifier: 'example.submit', bounds: { x: 10, y: 10, width: 80, height: 30 } }],
|
|
10
|
+
}) }] });
|
|
11
|
+
const surface = new AtomicScreenServer({ observation: { name: 'screenshot', arguments: {} }, actions: ['click'], snapshot: snapshot(), apply: (action, target) => {
|
|
12
|
+
// This is the actual effect on the model owned by this server, in the same JS turn as CAS.
|
|
13
|
+
// Never replace this with an awaited/queued coordinate click on an externally mutable desktop.
|
|
14
|
+
if (action.name !== 'click' || target !== '3') throw new Error('Unsupported target');
|
|
15
|
+
submitted++;
|
|
16
|
+
return { result: { content: [{ type: 'text', text: `Virtual form submitted ${submitted} time(s). Nothing was sent externally.` }] }, snapshot: snapshot() };
|
|
17
|
+
} });
|
|
18
|
+
const tools = [
|
|
19
|
+
{ name: 'screenshot', description: 'Observe the local virtual form.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
|
|
20
|
+
{ name: 'click', description: 'Activate an element of the local virtual form through atomic approval.', inputSchema: { type: 'object', properties: { index: { type: 'integer' } }, required: ['index'], additionalProperties: false } },
|
|
21
|
+
];
|
|
22
|
+
const send = value => process.stdout.write(JSON.stringify(value) + '\n');
|
|
23
|
+
createInterface({ input: process.stdin }).on('line', line => {
|
|
24
|
+
let msg;
|
|
25
|
+
try {
|
|
26
|
+
if (Buffer.byteLength(line) > 65_536) throw new Error('Oversized request');
|
|
27
|
+
msg = JSON.parse(line);
|
|
28
|
+
if (!msg || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') throw new Error('Invalid request');
|
|
29
|
+
const ok = result => { if (msg.id !== undefined) send({ jsonrpc: '2.0', id: msg.id, result }); };
|
|
30
|
+
if (msg.method === ATOMIC_SCREEN_CANCEL) { surface.cancel(msg.params); return ok({}); }
|
|
31
|
+
if (msg.id === undefined) return;
|
|
32
|
+
if (msg.method === 'initialize') return ok({ protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'warden-atomic-example', version: '1' } });
|
|
33
|
+
if (msg.method === 'tools/list') return ok({ tools });
|
|
34
|
+
if (msg.method === ATOMIC_SCREEN_PREPARE) return ok(surface.prepare(msg.params));
|
|
35
|
+
if (msg.method === ATOMIC_SCREEN_COMMIT) return ok(surface.commit(msg.params));
|
|
36
|
+
if (msg.method === 'tools/call' && msg.params?.name === 'screenshot' && Object.keys(msg.params.arguments ?? {}).length === 0) return ok(surface.read());
|
|
37
|
+
// Raw GUI dispatch is deliberately absent, even for direct clients.
|
|
38
|
+
throw new Error('Unsupported method or raw effect; atomic commit required');
|
|
39
|
+
} catch {
|
|
40
|
+
if (msg?.id !== undefined) send({ jsonrpc: '2.0', id: msg.id, error: { code: -32041, message: 'Atomic example refused or outcome is uncertain; no automatic retry' } });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
let package = Package(name: "WardenDesktop", platforms: [.macOS(.v13)], products: [
|
|
4
|
+
.library(name: "WardenDesktop", targets: ["WardenDesktop"]),
|
|
5
|
+
.executable(name: "warden-desktop", targets: ["WardenDesktopHost"]),
|
|
6
|
+
], targets: [
|
|
7
|
+
.target(name: "WardenDesktop"),
|
|
8
|
+
.executableTarget(name: "WardenDesktopHost", dependencies: ["WardenDesktop"]),
|
|
9
|
+
])
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import CryptoKit
|
|
3
|
+
import Security
|
|
4
|
+
import ObjectiveC
|
|
5
|
+
|
|
6
|
+
public let wardenAtomicProtocol = "warden.screen.atomic.v1"
|
|
7
|
+
|
|
8
|
+
public enum DesktopError: Error { case invalid, unavailable, unsupported, uncertain }
|
|
9
|
+
private func digest(_ data: Data) -> String { SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() }
|
|
10
|
+
private func digest(_ text: String) -> String { digest(Data(text.utf8)) }
|
|
11
|
+
private func opaque() throws -> String {
|
|
12
|
+
var bytes = [UInt8](repeating: 0, count: 32)
|
|
13
|
+
guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else { throw DesktopError.unavailable }
|
|
14
|
+
return bytes.map { String(format: "%02x", $0) }.joined()
|
|
15
|
+
}
|
|
16
|
+
private func json(_ value: Any) throws -> String {
|
|
17
|
+
let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed, .withoutEscapingSlashes, .sortedKeys])
|
|
18
|
+
guard let result = String(data: data, encoding: .utf8) else { throw DesktopError.invalid }
|
|
19
|
+
return result
|
|
20
|
+
}
|
|
21
|
+
private func exact(_ value: [String: Any], _ keys: [String]) -> Bool { Set(value.keys) == Set(keys) }
|
|
22
|
+
private func hex(_ value: Any?) -> String? {
|
|
23
|
+
guard let s = value as? String, s.count == 64, s.allSatisfy({ "0123456789abcdef".contains($0) }) else { return nil }; return s
|
|
24
|
+
}
|
|
25
|
+
private func number(_ value: Any?) -> Double? {
|
|
26
|
+
guard let n = value as? NSNumber, CFGetTypeID(n) != CFBooleanGetTypeID(), n.doubleValue.isFinite, abs(n.doubleValue) <= 1_000_000 else { return nil }
|
|
27
|
+
return n.doubleValue
|
|
28
|
+
}
|
|
29
|
+
private typealias NativeAction = @convention(c) (AnyObject, Selector, AnyObject) -> Void
|
|
30
|
+
private func actionImplementation(_ target: NSObject, _ selector: Selector) -> IMP? {
|
|
31
|
+
guard NSStringFromSelector(selector).filter({ $0 == ":" }).count == 1,
|
|
32
|
+
let method = class_getInstanceMethod(type(of: target), selector), method_getNumberOfArguments(method) == 3 else { return nil }
|
|
33
|
+
var returns = [CChar](repeating: 0, count: 32), argument = [CChar](repeating: 0, count: 32)
|
|
34
|
+
method_getReturnType(method, &returns, returns.count); method_getArgumentType(method, 2, &argument, argument.count)
|
|
35
|
+
// A block (@?) is not an object sender, despite sharing the '@' prefix.
|
|
36
|
+
guard returns[0] == 118 && returns[1] == 0 && argument[0] == 64 && argument[1] == 0 else { return nil } // void action(id sender)
|
|
37
|
+
return method_getImplementation(method)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Target-side authority. Embed INSIDE the application that owns these AppKit controls.
|
|
41
|
+
/// All model/handler changes must run on MainActor and call invalidate BEFORE mutation.
|
|
42
|
+
/// This is deliberately not an AXUIElement/CGEvent proxy for other applications.
|
|
43
|
+
@MainActor public final class WardenDesktopSurface {
|
|
44
|
+
private struct Binding {
|
|
45
|
+
let button: NSButton
|
|
46
|
+
let target: NSObject
|
|
47
|
+
let selector: Selector
|
|
48
|
+
let implementation: IMP
|
|
49
|
+
let identity: String
|
|
50
|
+
}
|
|
51
|
+
private struct Capture { let observation: [String: Any]; let fingerprint: String; let boxes: [String: CGRect]; let stamps: [String: String] }
|
|
52
|
+
private struct Ticket {
|
|
53
|
+
let revision: String; let stamp: String; let fingerprint: String; let action: String; let target: String
|
|
54
|
+
let expires: Double; let deadline: UInt64
|
|
55
|
+
}
|
|
56
|
+
private let window: NSWindow
|
|
57
|
+
private let applicationID: String
|
|
58
|
+
private var bindings: [String: Binding] = [:]
|
|
59
|
+
private var tickets: [String: Ticket] = [:]
|
|
60
|
+
private var revision: String
|
|
61
|
+
private var poisoned = false
|
|
62
|
+
private var uncertain = false
|
|
63
|
+
private var committing = false
|
|
64
|
+
private var lastFingerprint: String?
|
|
65
|
+
private let now: () -> Double
|
|
66
|
+
private let uptime: () -> UInt64
|
|
67
|
+
private var observers: [NSObjectProtocol] = []
|
|
68
|
+
deinit { for observer in observers { NotificationCenter.default.removeObserver(observer) } }
|
|
69
|
+
|
|
70
|
+
public init(window: NSWindow, applicationID: String, now: @escaping () -> Double = { Date().timeIntervalSince1970 * 1000 }, uptime: @escaping () -> UInt64 = { DispatchTime.now().uptimeNanoseconds }) throws {
|
|
71
|
+
guard !applicationID.isEmpty, applicationID.utf16.count <= 200, !applicationID.unicodeScalars.contains(where: { $0.value < 32 || $0.value == 127 }) else { throw DesktopError.invalid }
|
|
72
|
+
self.window = window; self.applicationID = applicationID; self.revision = try opaque(); self.now = now; self.uptime = uptime
|
|
73
|
+
for event in [NSWindow.didMoveNotification, NSWindow.didResizeNotification, NSWindow.willCloseNotification, NSWindow.willMiniaturizeNotification, NSWindow.didMiniaturizeNotification,
|
|
74
|
+
NSWindow.willBeginSheetNotification, NSWindow.didEndSheetNotification,
|
|
75
|
+
NSWindow.didDeminiaturizeNotification, NSWindow.didChangeScreenNotification, NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification] {
|
|
76
|
+
observers.append(NotificationCenter.default.addObserver(forName: event, object: window, queue: .main) { [weak self] _ in
|
|
77
|
+
MainActor.assumeIsolated { if let s = self, !s.committing, !s.uncertain { try? s.invalidate() } }
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Only explicit, retained action targets; nil/first-responder routing is not supported.
|
|
83
|
+
/// Register momentary push buttons whose synchronous handlers do not pump a nested run loop.
|
|
84
|
+
public func register(id: String, button: NSButton, target: NSObject, selector: Selector) throws {
|
|
85
|
+
guard !committing, !uncertain else { throw DesktopError.unavailable }
|
|
86
|
+
let identity = button.accessibilityIdentifier()
|
|
87
|
+
guard !committing, bindings.count < 64, bindings[id] == nil, id.range(of: "^[A-Za-z0-9_.:-]{1,40}$", options: .regularExpression) != nil,
|
|
88
|
+
button.window === window, button.target === target, button.action == selector,
|
|
89
|
+
!identity.isEmpty, identity.utf16.count <= 200, !identity.unicodeScalars.contains(where: { $0.value < 32 || $0.value == 127 }),
|
|
90
|
+
!bindings.values.contains(where: { $0.identity == identity || $0.button === button }),
|
|
91
|
+
button.accessibilityRole() == .button,
|
|
92
|
+
let cell = button.cell as? NSButtonCell, cell.showsStateBy.isEmpty,
|
|
93
|
+
let implementation = actionImplementation(target, selector) else { throw DesktopError.unsupported }
|
|
94
|
+
try invalidate()
|
|
95
|
+
bindings[id] = Binding(button: button, target: target, selector: selector, implementation: implementation, identity: identity)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Mandatory for invisible model/recipient/handler changes, even A -> B -> A.
|
|
99
|
+
/// Failure invalidates existing approvals. Does not recover an uncertain effect.
|
|
100
|
+
public func invalidate() throws {
|
|
101
|
+
guard !committing, !uncertain else { throw DesktopError.unavailable }
|
|
102
|
+
poisoned = true; revision = try opaque(); lastFingerprint = nil; poisoned = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func capture() throws -> Capture {
|
|
106
|
+
guard !poisoned, !bindings.isEmpty, window.isVisible, !window.isMiniaturized,
|
|
107
|
+
window.alphaValue.isFinite, window.alphaValue > 0, window.attachedSheet == nil, window.sheets.isEmpty,
|
|
108
|
+
!NSApp.windows.contains(where: { $0.sheetParent === window }),
|
|
109
|
+
NSApp.modalWindow == nil || NSApp.modalWindow === window,
|
|
110
|
+
window.title.utf16.count <= 160, let content = window.contentView else { throw DesktopError.unavailable }
|
|
111
|
+
let top = NSScreen.screens.first?.frame.maxY ?? 0
|
|
112
|
+
var elements: [[String: Any]] = [], boxes: [String: CGRect] = [:], stamps: [String: String] = [:], internals: [[String: Any]] = []
|
|
113
|
+
for id in bindings.keys.sorted() {
|
|
114
|
+
let b = bindings[id]!, button = b.button
|
|
115
|
+
guard button.window === window, button.isDescendant(of: content), !button.isHiddenOrHasHiddenAncestor, button.isEnabled,
|
|
116
|
+
button.target === b.target, button.action == b.selector, actionImplementation(b.target, b.selector) == b.implementation,
|
|
117
|
+
button.accessibilityIdentifier() == b.identity, button.accessibilityRole() == .button,
|
|
118
|
+
(button.cell as? NSButtonCell)?.showsStateBy.isEmpty == true else { throw DesktopError.unavailable }
|
|
119
|
+
guard button.title.utf16.count <= 160 else { throw DesktopError.unavailable }
|
|
120
|
+
var appearance: [[String: Any]] = [], ancestor: NSView? = button
|
|
121
|
+
while let view = ancestor {
|
|
122
|
+
guard appearance.count < 128, view.alphaValue.isFinite, view.alphaValue > 0 else { throw DesktopError.unavailable }
|
|
123
|
+
appearance.append(["view": String(describing: ObjectIdentifier(view)), "alpha": view.alphaValue])
|
|
124
|
+
if view === content { break }
|
|
125
|
+
ancestor = view.superview
|
|
126
|
+
}
|
|
127
|
+
let screen = window.convertToScreen(button.convert(button.bounds, to: nil))
|
|
128
|
+
let box = CGRect(x: screen.minX, y: top - screen.maxY, width: screen.width, height: screen.height)
|
|
129
|
+
guard [box.minX, box.minY, box.width, box.height].allSatisfy({ $0.isFinite && abs($0) <= 1_000_000 }), box.width > 0, box.height > 0 else { throw DesktopError.unavailable }
|
|
130
|
+
let bounds: [String: Any] = ["x": box.minX, "y": box.minY, "width": box.width, "height": box.height]
|
|
131
|
+
elements.append(["index": id, "role": "button", "name": button.title, "accessibilityIdentifier": b.identity, "bounds": bounds])
|
|
132
|
+
boxes[id] = box
|
|
133
|
+
// Match flow-guard-screen.ts screenTargetEvidence exactly; conformance is integration-tested.
|
|
134
|
+
let label = try json([button.title] + Array(repeating: NSNull() as Any, count: 8))
|
|
135
|
+
let geometry = try "{\"x\":\(json(box.minX)),\"y\":\(json(box.minY)),\"width\":\(json(box.width)),\"height\":\(json(box.height))}"
|
|
136
|
+
let stampText = try "{\"target\":\(json(id)),\"role\":\"button\",\"reference\":\(json(digest(id))),\"identity\":\(json(digest(b.identity))),\"bounds\":\(geometry),\"label\":\(json(digest(label))),\"application\":\(json(applicationID)),\"titles\":[\(json(digest(String(window.title.trimmingCharacters(in: .whitespacesAndNewlines).prefix(160)))))]}"
|
|
137
|
+
stamps[id] = digest(stampText)
|
|
138
|
+
internals.append(["id": id, "object": String(describing: ObjectIdentifier(button)), "target": String(describing: ObjectIdentifier(b.target)),
|
|
139
|
+
"selector": NSStringFromSelector(b.selector), "frame": bounds, "title": button.title, "identity": b.identity,
|
|
140
|
+
"state": button.state.rawValue, "tag": button.tag, "alternateTitle": button.alternateTitle, "appearance": appearance])
|
|
141
|
+
}
|
|
142
|
+
let observation: [String: Any] = ["content": [["type": "text", "text": "Native AppKit controls owned by this application; coordinates are global top-left points, not pixels."]],
|
|
143
|
+
"structuredContent": ["applicationId": applicationID, "windowTitle": window.title, "elements": elements]]
|
|
144
|
+
let state: [String: Any] = ["elements": internals, "window": String(describing: ObjectIdentifier(window)), "windowNumber": window.windowNumber,
|
|
145
|
+
"title": window.title, "alpha": window.alphaValue, "modal": NSApp.modalWindow.map { String(describing: ObjectIdentifier($0)) } ?? "",
|
|
146
|
+
"key": window.isKeyWindow, "focus": window.firstResponder.map { String(describing: ObjectIdentifier($0)) } ?? ""]
|
|
147
|
+
return Capture(observation: observation, fingerprint: digest(try json(state)), boxes: boxes, stamps: stamps)
|
|
148
|
+
}
|
|
149
|
+
private func fresh() throws -> Capture {
|
|
150
|
+
guard !committing else { throw DesktopError.unavailable }
|
|
151
|
+
do {
|
|
152
|
+
let view = try capture()
|
|
153
|
+
if let before = lastFingerprint, before != view.fingerprint { revision = try opaque() }
|
|
154
|
+
lastFingerprint = view.fingerprint
|
|
155
|
+
return view
|
|
156
|
+
} catch {
|
|
157
|
+
poisoned = true; tickets.removeAll(); throw DesktopError.unavailable
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
public func read() throws -> [String: Any] { try fresh().observation }
|
|
161
|
+
private func resolve(_ action: [String: Any], _ view: Capture) throws -> String {
|
|
162
|
+
guard exact(action, ["name", "arguments"]), action["name"] as? String == "click", let args = action["arguments"] as? [String: Any] else { throw DesktopError.unsupported }
|
|
163
|
+
let id: String
|
|
164
|
+
var requestedPoint: CGPoint?
|
|
165
|
+
if exact(args, ["index"]), let ref = args["index"] as? String, bindings[ref] != nil { id = ref }
|
|
166
|
+
else if exact(args, ["x", "y"]), let x = number(args["x"]), let y = number(args["y"]) {
|
|
167
|
+
let hits = view.boxes.filter { $0.value.contains(CGPoint(x: x, y: y)) }
|
|
168
|
+
guard hits.count == 1, let ref = hits.first?.key else { throw DesktopError.unsupported }; id = ref
|
|
169
|
+
requestedPoint = CGPoint(x: x, y: y)
|
|
170
|
+
} else { throw DesktopError.unsupported }
|
|
171
|
+
// Bind visible native hit target too; another view in this window must not cover it.
|
|
172
|
+
let b = bindings[id]!, center = requestedPoint ?? CGPoint(x: view.boxes[id]!.midX, y: view.boxes[id]!.midY)
|
|
173
|
+
let top = NSScreen.screens.first?.frame.maxY ?? 0
|
|
174
|
+
let inWindow = window.convertPoint(fromScreen: CGPoint(x: center.x, y: top - center.y))
|
|
175
|
+
guard let content = window.contentView, let hit = content.hitTest(content.convert(inWindow, from: nil)), hit === b.button || hit.isDescendant(of: b.button) else { throw DesktopError.unavailable }
|
|
176
|
+
return id
|
|
177
|
+
}
|
|
178
|
+
public func prepare(_ params: [String: Any]) throws -> [String: Any] {
|
|
179
|
+
guard exact(params, ["protocol", "observation", "action"]), params["protocol"] as? String == wardenAtomicProtocol,
|
|
180
|
+
let observation = params["observation"] as? [String: Any], exact(observation, ["name", "arguments"]), observation["name"] as? String == "screenshot",
|
|
181
|
+
let args = observation["arguments"] as? [String: Any], args.isEmpty, let action = params["action"] as? [String: Any] else { throw DesktopError.invalid }
|
|
182
|
+
let view = try fresh(), observedRevision = revision, target = try resolve(action, view)
|
|
183
|
+
let final = try fresh()
|
|
184
|
+
guard revision == observedRevision, view.fingerprint == final.fingerprint,
|
|
185
|
+
view.stamps[target] == final.stamps[target] else { throw DesktopError.unavailable }
|
|
186
|
+
let actionText = try json(action), time = now(), monotonic = uptime()
|
|
187
|
+
let (deadline, overflow) = monotonic.addingReportingOverflow(300_000_000_000)
|
|
188
|
+
guard time.isFinite, time >= 0, time <= 9_007_199_254_440_991, !overflow,
|
|
189
|
+
revision == observedRevision, !poisoned, !committing else { throw DesktopError.unavailable }
|
|
190
|
+
tickets = tickets.filter { $0.value.expires > time && $0.value.deadline > monotonic }
|
|
191
|
+
guard tickets.count < 256 else { throw DesktopError.unavailable }
|
|
192
|
+
let ticket = try opaque(), expiry = floor(time + 300_000), stamp = view.stamps[target]!
|
|
193
|
+
tickets[ticket] = Ticket(revision: observedRevision, stamp: stamp, fingerprint: view.fingerprint, action: actionText, target: target, expires: expiry, deadline: deadline)
|
|
194
|
+
return ["protocol": wardenAtomicProtocol, "ticket": ticket, "revision": revision, "stamp": stamp, "expiresAt": expiry, "observation": view.observation]
|
|
195
|
+
}
|
|
196
|
+
public func cancel(_ params: [String: Any]) throws {
|
|
197
|
+
guard exact(params, ["protocol", "ticket"]), params["protocol"] as? String == wardenAtomicProtocol, let ticket = hex(params["ticket"]) else { throw DesktopError.invalid }
|
|
198
|
+
tickets.removeValue(forKey: ticket)
|
|
199
|
+
}
|
|
200
|
+
public func commit(_ params: [String: Any]) throws -> [String: Any] {
|
|
201
|
+
guard exact(params, ["protocol", "ticket", "expected", "action"]), params["protocol"] as? String == wardenAtomicProtocol,
|
|
202
|
+
let ticket = hex(params["ticket"]), let expected = params["expected"] as? [String: Any], exact(expected, ["revision", "stamp"]),
|
|
203
|
+
let expectedRevision = hex(expected["revision"]), let stamp = hex(expected["stamp"]), let action = params["action"] as? [String: Any] else { throw DesktopError.invalid }
|
|
204
|
+
func refused(_ reason: String) -> [String: Any] { ["protocol": wardenAtomicProtocol, "status": "refused", "reason": reason] }
|
|
205
|
+
guard let approved = tickets.removeValue(forKey: ticket) else { return refused("unknown") }
|
|
206
|
+
guard !poisoned, !committing else { return refused("unavailable") }
|
|
207
|
+
let admissionTime = now()
|
|
208
|
+
guard admissionTime < approved.expires, admissionTime >= approved.expires - 300_000, uptime() < approved.deadline else { return refused("expired") }
|
|
209
|
+
guard expectedRevision == approved.revision, stamp == approved.stamp, try json(action) == approved.action else { return refused("mismatch") }
|
|
210
|
+
let view: Capture
|
|
211
|
+
do { view = try fresh() } catch { return refused("unavailable") }
|
|
212
|
+
guard revision == approved.revision, view.fingerprint == approved.fingerprint, view.stamps[approved.target] == approved.stamp else { return refused("stale") }
|
|
213
|
+
guard (try? resolve(action, view)) == approved.target, revision == approved.revision, !poisoned,
|
|
214
|
+
let b = bindings[approved.target] else { return refused("stale") }
|
|
215
|
+
// hitTest is overridable AppKit code. Do not rely solely on the host's
|
|
216
|
+
// invalidation callback: visible properties/handler may have changed there.
|
|
217
|
+
let final: Capture
|
|
218
|
+
do { final = try fresh() } catch { return refused("unavailable") }
|
|
219
|
+
guard revision == approved.revision, final.fingerprint == approved.fingerprint,
|
|
220
|
+
final.stamps[approved.target] == approved.stamp else { return refused("stale") }
|
|
221
|
+
// Validation itself may take time. Check both clocks at the dispatch boundary.
|
|
222
|
+
// Allocate the receipt revision before any effect; entropy failure must not
|
|
223
|
+
// escape as an ordinary unavailable error after a successful handler.
|
|
224
|
+
let committedRevision = try opaque()
|
|
225
|
+
let dispatchTime = now()
|
|
226
|
+
guard dispatchTime < approved.expires, dispatchTime >= approved.expires - 300_000,
|
|
227
|
+
uptime() < approved.deadline else { return refused("expired") }
|
|
228
|
+
// Clock providers are injected host callbacks too. No callbacks may run
|
|
229
|
+
// after this final CAS and before the pinned implementation invocation.
|
|
230
|
+
guard revision == approved.revision, !poisoned, !committing else { return refused("stale") }
|
|
231
|
+
// Linearization point in the TARGET APPLICATION's main run loop. No AX RPC,
|
|
232
|
+
// CGEvent, performClick animation, await, timer or queued coordinate dispatch.
|
|
233
|
+
committing = true
|
|
234
|
+
defer { committing = false }
|
|
235
|
+
uncertain = true; poisoned = true
|
|
236
|
+
// NSApplication.sendAction is overridable and may reroute/queue or report
|
|
237
|
+
// success without invoking this method. Call the checked, retained IMP.
|
|
238
|
+
let invoke = unsafeBitCast(b.implementation, to: NativeAction.self)
|
|
239
|
+
invoke(b.target, b.selector, b.button)
|
|
240
|
+
revision = committedRevision; lastFingerprint = nil
|
|
241
|
+
// A receipt attests local handler dispatch, never remote payment/mail completion.
|
|
242
|
+
poisoned = false; uncertain = false
|
|
243
|
+
return ["protocol": wardenAtomicProtocol, "status": "committed", "revisionBefore": approved.revision, "revisionAfter": revision, "stamp": stamp,
|
|
244
|
+
"result": ["content": [["type": "text", "text": "Native AppKit action dispatched synchronously to the approved button target. External outcomes require reconciliation."]]]]
|
|
245
|
+
}
|
|
246
|
+
}
|