@aimarket/warden 0.6.0 → 0.8.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 +24 -1
- package/README-es.md +57 -6
- package/README-fr.md +57 -6
- package/README-ru.md +57 -6
- package/README-zh.md +57 -6
- package/README.md +57 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-rpc.d.ts +3 -1
- package/dist/mcp-rpc.d.ts.map +1 -1
- package/dist/mcp-rpc.js +32 -8
- package/dist/mcp-rpc.js.map +1 -1
- package/dist/mcp-server.js +50 -44
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tools.d.ts +1 -12
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +92 -18
- package/dist/mcp-tools.js.map +1 -1
- package/dist/pin-store.d.ts +13 -0
- package/dist/pin-store.d.ts.map +1 -0
- package/dist/pin-store.js +44 -0
- package/dist/pin-store.js.map +1 -0
- package/dist/pinning.d.ts +4 -3
- package/dist/pinning.d.ts.map +1 -1
- package/dist/pinning.js +16 -3
- package/dist/pinning.js.map +1 -1
- package/dist/state.d.ts +7 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +61 -0
- package/dist/state.js.map +1 -0
- package/dist/static-scan.d.ts +2 -2
- package/dist/static-scan.d.ts.map +1 -1
- package/dist/static-scan.js +107 -8
- package/dist/static-scan.js.map +1 -1
- package/dist/threat-feed.d.ts +9 -0
- package/dist/threat-feed.d.ts.map +1 -1
- package/dist/threat-feed.js +50 -7
- package/dist/threat-feed.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wrap-cli.d.ts +21 -0
- package/dist/wrap-cli.d.ts.map +1 -0
- package/dist/wrap-cli.js +158 -0
- package/dist/wrap-cli.js.map +1 -0
- package/dist/wrap-state.d.ts +19 -0
- package/dist/wrap-state.d.ts.map +1 -0
- package/dist/wrap-state.js +62 -0
- package/dist/wrap-state.js.map +1 -0
- package/dist/wrap-wire.d.ts +13 -0
- package/dist/wrap-wire.d.ts.map +1 -0
- package/dist/wrap-wire.js +88 -0
- package/dist/wrap-wire.js.map +1 -0
- package/dist/wrap.d.ts +3 -0
- package/dist/wrap.d.ts.map +1 -0
- package/dist/wrap.js +316 -0
- package/dist/wrap.js.map +1 -0
- package/docs/gates.es.md +224 -0
- package/docs/gates.fr.md +228 -0
- package/docs/gates.md +219 -0
- package/docs/gates.ru.md +225 -0
- package/docs/gates.zh.md +178 -0
- package/docs/security-hardening.es.md +40 -0
- package/docs/security-hardening.fr.md +40 -0
- package/docs/security-hardening.md +40 -0
- package/docs/security-hardening.ru.md +40 -0
- package/docs/security-hardening.zh.md +40 -0
- package/package.json +7 -4
package/docs/gates.ru.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Цепочка гейтов
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](gates.md) · **Русский** · [Español](gates.es.md) · [Français](gates.fr.md) · [中文](gates.zh.md)
|
|
4
|
+
|
|
5
|
+
> [Изменения безопасности 0.7.0 и миграция](security-hardening.ru.md).
|
|
6
|
+
|
|
7
|
+
`Warden.vet(server, tools)` прогоняет упорядоченную цепочку и возвращает один вердикт. Эта страница —
|
|
8
|
+
вся процедура принятия решения: на что смотрит каждый гейт, что он может заблокировать и как
|
|
9
|
+
складывается итоговое число.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
static-scan → threat-feed → origin → pinning
|
|
13
|
+
(бесплатно) (бесплатно (бесплатно) (бесплатно)
|
|
14
|
+
после load)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Порядок — от самого дешёвого и самого локального. Ни один гейт в цепочке не делает сетевых запросов:
|
|
18
|
+
единственная загрузка, которую WARDEN вообще выполняет, — это `ThreatFeed.load(url)`, и её вызываете
|
|
19
|
+
вы сами, до проверки.
|
|
20
|
+
|
|
21
|
+
## Как собирается вердикт
|
|
22
|
+
|
|
23
|
+
Каждый гейт возвращает `{ findings, score, fatal? }`. Цепочка:
|
|
24
|
+
|
|
25
|
+
1. прогоняет все гейты по порядку, накапливая находки (каждый гейт видит `prior`);
|
|
26
|
+
2. перемножает оценки гейтов — composite score это **произведение**, поэтому один плохой гейт тянет
|
|
27
|
+
сервер вниз, а не усредняется тремя хорошими;
|
|
28
|
+
3. блокирует, если какой-то гейт вернул `fatal` или если какая-то не-advisory находка достигает
|
|
29
|
+
`policy.blockAtSeverity`;
|
|
30
|
+
4. обрывает цепочку **только** на явном `fatal`. Блокирующая, но не фатальная находка позволяет
|
|
31
|
+
остальным гейтам отчитаться, чтобы запись о том, *почему*, осталась полной.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const SEVERITY_RANK = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Если `policy.blockAtSeverity` не один из этих пяти ключей, конструктор пишет предупреждение и
|
|
38
|
+
откатывается на `"high"`. Опечатка здесь раньше была худшим из возможных отказов: `rank >= undefined`
|
|
39
|
+
ложно при любом сравнении, поэтому неправильно написанный порог молча отключал блокировку целиком.
|
|
40
|
+
|
|
41
|
+
### Две оси: severity и tier
|
|
42
|
+
|
|
43
|
+
Severity отвечает на вопрос *сколько внимания это заслуживает*. **Tier** отвечает на вопрос *дефект
|
|
44
|
+
ли это вообще* — и он является данными на находке (`advisory: true`), а не следствием severity.
|
|
45
|
+
|
|
46
|
+
Находка с `advisory` сообщается, никогда не блокирует и никогда не стоит инструмента — при **любом**
|
|
47
|
+
`blockAtSeverity`. Инструмент, чья схема принимает `api_key`, стоит того, чтобы на него указали, и не
|
|
48
|
+
является дефектом; выражение этого через понижение severity снова делало бы его блокирующим для всех,
|
|
49
|
+
кто ужесточил порог.
|
|
50
|
+
|
|
51
|
+
## static-scan
|
|
52
|
+
|
|
53
|
+
Локальное regex-сканирование `name`, `description` и `inputSchema` каждого инструмента. 26 правил в
|
|
54
|
+
наборе **v7**: 15 `block`, 11 `advise`, и у 15 из них есть контекстный **guard** — именованная
|
|
55
|
+
проверка, решающая, действительно ли совпадение — то, что правило ищет. См.
|
|
56
|
+
[полевой обзор](mcp-survey.ru.md) — прогон по 1 108 серверам, на котором калибровался v4.
|
|
57
|
+
|
|
58
|
+
**v5: текст приводится к обычному виду до того, как его читает любое правило.** NFKC превращает
|
|
59
|
+
широкие буквы, лигатуры и другие совместимые формы в обычные; невидимые символы внутри слова
|
|
60
|
+
выбрасываются; блок Unicode-тегов (невидимые копии ASCII, в которых можно спрятать целое предложение)
|
|
61
|
+
расшифровывается; внутри слова, где латиница смешана с кириллицей или греческим, буквы-двойники
|
|
62
|
+
заменяются латинскими, а слово целиком на одном алфавите не трогается. Английское правило больше не
|
|
63
|
+
обходится широкими буквами, пробелом нулевой ширины, тегами или кириллической `о` — на каком бы языке
|
|
64
|
+
ни был окружающий текст. Идентификатор свёртки публикуется как `fold` и входит в дайджест. Два правила
|
|
65
|
+
скрытых нагрузок читают **сырой** текст (`raw: true`): они ищут именно то, что свёртка убирает.
|
|
66
|
+
|
|
67
|
+
Смысл регэксп-таблица не читает: формулировка на языке, для которого правила не написаны, вне её.
|
|
68
|
+
v5 добавляет то, что от языка не зависит: свёртку, пару `TOOL_DEF_SECRET_EXFIL` (хранилище секретов и
|
|
69
|
+
внешний адрес в пределах 100 символов; только уведомляет — её единственное срабатывание на 10 645
|
|
70
|
+
живых серверах оказалось честным), блок тегов и изоляты направления в `TOOL_DEF_HIDDEN_UNICODE`, а
|
|
71
|
+
HISTOR показывает каждый внешний адрес, впервые появившийся в определениях сервера. Распознавание по
|
|
72
|
+
смыслу — задача классификатора, а не этой таблицы.
|
|
73
|
+
|
|
74
|
+
Ещё v5 убирает три измеренных ложных срабатывания: «send the user to https://…» (переход человека,
|
|
75
|
+
guard `navigation`), «keep calling … without asking the user» (самостоятельная работа, guard
|
|
76
|
+
`autonomy`) и соединитель нулевой ширины внутри эмодзи. На корпусе из 10 645 серверов v5 блокирует 56
|
|
77
|
+
там, где v4 блокировал 63, и не блокирует ни одного, которого не блокировал v4.
|
|
78
|
+
|
|
79
|
+
**v7** убирает три ложных срабатывания, которые ruleset v6 ещё давал на закоммиченном корпусе
|
|
80
|
+
2026-10-01 ([полевое обследование](mcp-survey.ru.md)). «Private key/value memory» — хранилище
|
|
81
|
+
ключ-значение, а не закрытый ключ (guard `keyValue`: составное слово через косую черту или дефис и
|
|
82
|
+
следом слово-хранилище). «Find … without asking the user for ids» — инструмент сам находит
|
|
83
|
+
идентификатор (guard `autonomy` теперь принимает и глагол поиска, если весь объект просьбы —
|
|
84
|
+
идентификатор). «The key is read from the MCP connection's X-API-Key header» — сервер описывает
|
|
85
|
+
собственную аутентификацию (guard `ownAuthHeader`: пассив «is read from», заголовок запроса, не чужой,
|
|
86
|
+
ключ никуда не передаётся дальше). Кроме того, `autonomy` больше не освобождает «without asking the
|
|
87
|
+
user», если объект — согласие: «keep retrying the transfer without asking the user for approval» снова
|
|
88
|
+
блокируется. На этом корпусе v7 блокирует 3 сервера там, где v6 блокировал 6, остальные находки не
|
|
89
|
+
меняются.
|
|
90
|
+
|
|
91
|
+
Каждое правило объявляет, на каких из этих трёх **поверхностей** оно работает; 17 из 26 включают
|
|
92
|
+
имя. Четыре, которые не включают, — это правила, завязанные на существительное
|
|
93
|
+
(`TOOL_DEF_SECRET_REQUEST`, `TOOL_DEF_CREDENTIAL_PARAM`, `TOOL_DEF_ENV_REFERENCE`, `TOOL_DEF_SECRET_EXFIL`): имя — это
|
|
94
|
+
идентификатор, `api_key` и `private_key` — его обычные части, и блокировать
|
|
95
|
+
`sign_with_private_key` означало бы повторить ошибку калибровки v1 на новой поверхности. Правила,
|
|
96
|
+
завязанные на фразу, требуют пробелов и потому не могут совпасть с `snake_case` вовсе, а два
|
|
97
|
+
правила о скрытой нагрузке — про символы, которые в имени не легитимны никогда: эти работают везде.
|
|
98
|
+
|
|
99
|
+
До v3 имя не сканировалось **ничем** — то есть инъекционная фраза, zero-width символ или
|
|
100
|
+
base64-блоб в первом поле, которое читает модель, не попадали в отчёт совсем.
|
|
101
|
+
|
|
102
|
+
Оценка гейта — `1 − штраф(худшая блокирующая severity)`; advisory-срабатывания на неё не влияют.
|
|
103
|
+
|
|
104
|
+
| худшая блокирующая severity | нет | info | low | medium | high | critical |
|
|
105
|
+
|---|---|---|---|---|---|---|
|
|
106
|
+
| оценка гейта | 1 | 1 | 0.9 | 0.7 | 0.4 | 0 |
|
|
107
|
+
|
|
108
|
+
| Код | Severity | Tier | Имя? | Что ловит |
|
|
109
|
+
|---|---|---|---|---|
|
|
110
|
+
| `TOOL_DEF_INJECTION` | critical / high | block | ✅ | «ignore all previous instructions», «do not tell the user», теги `<system>`, ссылки на developer prompt |
|
|
111
|
+
| `TOOL_DEF_SECRET_REQUEST` | critical | block | — | `private_key`, `seed_phrase`/`mnemonic`, пути `~/.ssh` |
|
|
112
|
+
| `TOOL_DEF_SECRET_HARVEST` | critical | block | ✅ | инструмент, чья заявленная задача — читать/выгружать/раскрывать секреты |
|
|
113
|
+
| `TOOL_DEF_EXFIL` | critical / high | block | ✅ | «post to https://…», «forward it to…», «exfiltrate», формулировки про upload на хост |
|
|
114
|
+
| `TOOL_DEF_HIDDEN_UNICODE` | high | block | ✅ | zero-width и bidi-управляющие символы — текст, которого рецензент не видит |
|
|
115
|
+
| `TOOL_DEF_BASE64_BLOB` | high | block | ✅ | цепочка base64 от 120 символов в имени, описании или схеме |
|
|
116
|
+
| `TOOL_DEF_DATA_URL` | high | block | ✅ | URL вида `data:…;base64,` и `javascript:` |
|
|
117
|
+
| `TOOL_DEF_CREDENTIAL_PARAM` | medium / low | advise | — | схема или описание просит `api_key`, `password`, `secret`, bearer-токены |
|
|
118
|
+
| `TOOL_DEF_ENV_REFERENCE` | medium | advise | — | `.env`, «environment variables» |
|
|
119
|
+
| `TOOL_DEF_IMPERATIVE` | low / info | advise | ✅ | «you must», «instead of» — формулировки в форме промпта, сами по себе ничего не доказывающие |
|
|
120
|
+
|
|
121
|
+
`staticScanRuleset()` возвращает каждое правило вместе с **исходником регулярки, флагами и поверхностями**, чтобы
|
|
122
|
+
третья сторона могла перезапустить ровно то же правило, плюс `{ version, digest }`, где digest —
|
|
123
|
+
sha256 по канонической форме RFC 8785 отсортированного списка правил. Сортировка — сравнением
|
|
124
|
+
code-unit, никогда не `localeCompare`: зависящая от локали коллация давала бы для одной и той же
|
|
125
|
+
таблицы разный digest на по-разному настроенном хосте, а это ровно то расхождение, для обнаружения
|
|
126
|
+
которого digest и существует.
|
|
127
|
+
|
|
128
|
+
## threat-feed
|
|
129
|
+
|
|
130
|
+
Сопоставляет идентичность сервера и определения инструментов с записями `ThreatRecord` — 11
|
|
131
|
+
встроенных плюс то, что добавил подписанный feed (см. [контракт feed](threat-feed.ru.md)).
|
|
132
|
+
|
|
133
|
+
- Любое совпадение ⇒ оценка гейта **0**.
|
|
134
|
+
- `fatal` **только** для записи `critical`, совпавшей с *сервером*. Критическое совпадение на одном
|
|
135
|
+
*инструменте* не фатально: остальная цепочка всё равно отчитывается, а вина остаётся привязанной к
|
|
136
|
+
этому инструменту — именно это позволяет в основном нормальному серверу продолжать работать с одним
|
|
137
|
+
изолированным инструментом.
|
|
138
|
+
- `ThreatRecord.scope` выбирает поверхность: `server` (id/name/url/command/args), `tool`
|
|
139
|
+
(name/description/inputSchema) или `any` — значение по умолчанию, когда запись его не указывает.
|
|
140
|
+
|
|
141
|
+
Встроенные коды: `THREAT_TYPOSQUAT`, `THREAT_CRYPTO_DRAINER`, `THREAT_SEED_PHRASE`,
|
|
142
|
+
`THREAT_SSH_KEY_READ`, `THREAT_ENV_EXFIL`, `THREAT_DESTRUCTIVE_CMD`, `THREAT_FORK_BOMB`.
|
|
143
|
+
|
|
144
|
+
## origin
|
|
145
|
+
|
|
146
|
+
Объявил ли оператор этот сервер, или он пришёл из удалённого каталога (заполнено
|
|
147
|
+
`McpServerRef.catalog`)?
|
|
148
|
+
|
|
149
|
+
| `allowUnknownServers` | находка | оценка | fatal |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| `false` (fail-closed) | `SERVER_UNDECLARED`, high | 0 | да |
|
|
152
|
+
| `true` | `SERVER_UNDECLARED`, info | 1 | нет |
|
|
153
|
+
|
|
154
|
+
Раньше этот переключатель означал «ещё нет оценки репутации», чего не могло выполнить ни одно
|
|
155
|
+
развёртывание: оракулу никто никогда не передавал trust-рёбра, поэтому каждый сервер возвращался
|
|
156
|
+
непоручённым, и `false` блокировал их все. Происхождение из каталога — факт, которым хост уже
|
|
157
|
+
располагает локально, он не требует сети и не может привести к дедлоку.
|
|
158
|
+
|
|
159
|
+
## pinning
|
|
160
|
+
|
|
161
|
+
Сравнивает текущие определения инструментов со снимком, который подтвердил пользователь. Хеш — sha256
|
|
162
|
+
по канонической форме RFC 8785 набора определений; та же канонизация, что у подписи feed, а не вторая
|
|
163
|
+
сериализация.
|
|
164
|
+
|
|
165
|
+
| Ситуация | Код | Severity | Оценка | Fatal |
|
|
166
|
+
|---|---|---|---|---|
|
|
167
|
+
| Pin ещё нет (первый контакт) | `TOOL_DEF_UNPINNED` | info | 0.9 | нет |
|
|
168
|
+
| Хеш отличается от pin | `TOOL_DEF_DRIFT` | high | 0 | при `pinToolDefs` |
|
|
169
|
+
| У определений нет канонической формы (без pin) | `TOOL_DEF_UNCANONICAL` | medium | 0.5 | нет |
|
|
170
|
+
| У определений нет канонической формы (с pin) | `TOOL_DEF_UNCANONICAL` | high | 0 | при `pinToolDefs` |
|
|
171
|
+
|
|
172
|
+
Первый контакт стоит 0.1, а не блокировки: чистый объявленный сервер без pin получает ровно **0.9**, а
|
|
173
|
+
`TOOL_DEF_UNPINNED` имеет severity `info` намеренно — при `blockAtSeverity: "info"` блокирующий первый
|
|
174
|
+
взгляд сделал бы любой сервер навсегда непригодным, ведь ничего нельзя зафиксировать до того, как это
|
|
175
|
+
хоть раз подтвердили.
|
|
176
|
+
|
|
177
|
+
`warden.approve(server, tools)` записывает pin через ваш `PinStore`. Операция идемпотентна.
|
|
178
|
+
|
|
179
|
+
## Разбиение по инструментам
|
|
180
|
+
|
|
181
|
+
`allowedTools` / `blockedTools` делят объявленные инструменты:
|
|
182
|
+
|
|
183
|
+
- инструмент **заблокирован**, если не-advisory находка называет его (`finding.tool`) и достигает
|
|
184
|
+
порога;
|
|
185
|
+
- все остальные инструменты разрешены;
|
|
186
|
+
- чувствительные инструменты (`policy.sensitiveToolPatterns`) остаются *разрешёнными* — они помечены,
|
|
187
|
+
чтобы цикл вашего агента мог требовать подтверждение на каждый вызов во время работы. См.
|
|
188
|
+
`classifyTools` / `isSensitiveTool`.
|
|
189
|
+
|
|
190
|
+
## Как добавить свой гейт
|
|
191
|
+
|
|
192
|
+
`WardenGate` — интерфейс из трёх строк, а `new Warden({ gates, policy, log })` принимает цепочку
|
|
193
|
+
напрямую, поэтому свой гейт можно вставить без форка:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { Warden, StaticScanGate, ThreatGate, OriginGate, PinningGate } from "@aimarket/warden";
|
|
197
|
+
import type { WardenGate, WardenGateInput, WardenGateResult } from "@aimarket/warden";
|
|
198
|
+
|
|
199
|
+
class DenyByPublisher implements WardenGate {
|
|
200
|
+
readonly name = "publisher-allowlist";
|
|
201
|
+
async evaluate(input: WardenGateInput): Promise<WardenGateResult> {
|
|
202
|
+
const ok = ALLOWED.has(input.server.name);
|
|
203
|
+
return ok
|
|
204
|
+
? { findings: [], score: 1 }
|
|
205
|
+
: { findings: [{ gate: this.name, severity: "high", code: "PUBLISHER_UNKNOWN",
|
|
206
|
+
message: `${input.server.name} не входит в список разрешённых издателей` }],
|
|
207
|
+
score: 0, fatal: true };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const warden = new Warden({
|
|
212
|
+
gates: [new StaticScanGate(), new ThreatGate(feed), new DenyByPublisher(), new OriginGate(), new PinningGate(store)],
|
|
213
|
+
policy,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Два правила для гейта, который вы пишете сами: **никогда не заявляйте, что удалённый сервис
|
|
218
|
+
недоступен, если вы действительно не отправили запрос** (`test/no-phantom-gate.test.ts` следит за этим
|
|
219
|
+
на поставляемых гейтах) и возвращайте оценку, которую можете защитить — гейт, который ничего не
|
|
220
|
+
измерил, должен вернуть `1`, а не «нейтральные» 0.6, иначе он штрафует каждый сервер за измерение,
|
|
221
|
+
которого не проводил.
|
|
222
|
+
|
|
223
|
+
## Гейты в режиме wrap
|
|
224
|
+
|
|
225
|
+
`vetLaunch` вызывается до запуска процесса. `initialize.instructions` проходит существующий static-scan и удаляется при блокирующей находке. `tools/list` собирает не более 32 страниц, 256 уникальных инструментов и 1 MiB, затем вызывает `vet`; клиент получает проверенные определения одной страницей. `list_changed` немедленно включает карантин; уведомление уходит клиенту только после успешной проверки. Перед каждым вызовом список перечитывается, а определение сравнивается с последним показанным клиенту. Изменение во время проверки запрещает отправку; во время выполнения скрывает ответ. Сквозные сообщения ограничены отдельно — 32 MiB. Таймаут внутреннего запроса списка — 10 секунд. При EOF закрывается stdin дочернего, через 5 секунд отправляется SIGTERM, перед истечением 10 секунд — SIGKILL. Встречные запросы и прочие сообщения сохраняют ID и исходный JSON. Политика, TOFU, ручное одобрение и ограничения audit-only описаны в README.
|
package/docs/gates.zh.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# 门控链
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](gates.md) · [Русский](gates.ru.md) · [Español](gates.es.md) · [Français](gates.fr.md) · **中文**
|
|
4
|
+
|
|
5
|
+
> [0.7.0 安全更新与迁移](security-hardening.zh.md).
|
|
6
|
+
|
|
7
|
+
`Warden.vet(server, tools)` 会按顺序跑完一条门控链,并返回单一裁定。本页就是完整的判定过程:每个门控看什么、
|
|
8
|
+
它可以阻止什么,以及最后那个数字是怎么算出来的。
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
static-scan → threat-feed → origin → pinning
|
|
12
|
+
(免费) (load 之后 (免费) (免费)
|
|
13
|
+
免费)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
顺序是「最便宜、最本地的先来」。链上没有任何环节发起网络请求——WARDEN 唯一会发出的下载是
|
|
17
|
+
`ThreatFeed.load(url)`,而那是你自己在审查之前调用的。
|
|
18
|
+
|
|
19
|
+
## 裁定是怎么拼出来的
|
|
20
|
+
|
|
21
|
+
每个门控返回 `{ findings, score, fatal? }`。整条链:
|
|
22
|
+
|
|
23
|
+
1. 按顺序运行每个门控,累积发现(每个门控都能看到 `prior`);
|
|
24
|
+
2. 把各门控的评分**相乘**——综合评分是乘积,所以一个坏门控会把整台服务器拉下来,而不是被三个好门控平均掉;
|
|
25
|
+
3. 若任一门控返回 `fatal`,或任一非 advisory 的发现达到 `policy.blockAtSeverity`,则阻止;
|
|
26
|
+
4. **只有**遇到显式 `fatal` 才短路。会阻止但不 fatal 的发现仍让后续门控继续报告,这样「为什么」的记录才完整。
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const SEVERITY_RANK = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
如果 `policy.blockAtSeverity` 不是这五个键之一,构造函数会写一条警告并回退到 `"high"`。这里的一个拼写错误
|
|
33
|
+
曾是最糟糕的故障模式:`rank >= undefined` 在任何比较中都为 `false`,于是一个拼错的阈值会悄无声息地把阻止功能
|
|
34
|
+
整个关掉。
|
|
35
|
+
|
|
36
|
+
### 两个维度:严重级别与层级
|
|
37
|
+
|
|
38
|
+
严重级别回答的是*这值得多少注意*。**层级**(tier)回答的是*这到底算不算缺陷*——而它是发现上的一项数据
|
|
39
|
+
(`advisory: true`),不是严重级别的推论。
|
|
40
|
+
|
|
41
|
+
带 `advisory` 的发现会被报告,但在**任何** `blockAtSeverity` 下都不会阻止连接、也不会牺牲任何工具。一个 schema
|
|
42
|
+
里接受 `api_key` 的工具值得被指出来,但它不是缺陷;用「降低严重级别」来表达这一点,会让它对任何收紧阈值的人重新
|
|
43
|
+
变成阻止性的。
|
|
44
|
+
|
|
45
|
+
## static-scan
|
|
46
|
+
|
|
47
|
+
对每个工具的 `name`、`description` 与 `inputSchema` 做本地正则扫描。规则集 **v7** 共 26 条规则(其中 15 条带有上下文 **guard**——一项具名检查,用于判断某次命中是否真是该规则要找的东西;参见[实地普查](mcp-survey.zh.md),即校准 v4 的 1 108 台服务器实测):
|
|
48
|
+
15 条 `block`,11 条 `advise`。
|
|
49
|
+
|
|
50
|
+
**v5:任何规则读取文本之前,先对文本做归一化。** NFKC 把全角字母、连字和其他兼容形式变成普通字母;单词内部的不可见字符被删除;Unicode 标签区块(ASCII 的不可见副本,能藏下一整句话)被解码;在拉丁字母与西里尔或希腊字母混写的单词里,形近字母被换成拉丁字母,而整词只用一种文字写成的单词保持不变。于是,无论周围文本是什么语言,英文规则都不能再被全角字母、词内零宽空格、标签字符或西里尔字母 `о` 绕过。归一化以 `fold` 的形式与规则一起发布,并计入 digest。两条隐藏载荷规则读取**原始**文本(`raw: true`)。
|
|
51
|
+
|
|
52
|
+
正则表格读不懂含义:用规则没有覆盖的语言写成的说法不在它的范围内。v5 增加了与语言无关的部分:归一化;`TOOL_DEF_SECRET_EXFIL` 组合(机密存放位置与外部地址相距不超过 100 个字符;仅提示,因为它在 10 645 台真实服务器上唯一的命中是正当用途);`TOOL_DEF_HIDDEN_UNICODE` 纳入标签区块和双向隔离符;HISTOR 会标出服务器定义中新出现的任何外部地址。按含义识别属于分类器的工作,而不是这张表的工作。
|
|
53
|
+
|
|
54
|
+
v5 还消除了三类实测误报:“send the user to https://…”(引导用户跳转,guard `navigation`)、“keep calling … without asking the user”(自主运行,guard `autonomy`),以及表情符号序列中的零宽连接符。在 10 645 台服务器的语料上,v4 阻止 63 台,v5 阻止 56 台,且没有新增任何 v4 未阻止的服务器。
|
|
55
|
+
|
|
56
|
+
**v7** 消除了规则集 v6 在已提交的 2026-10-01 语料上仍有的三处误报([实地普查](mcp-survey.zh.md))。“Private key/value memory” 指键值存储,而非私钥(guard `keyValue`:以斜杠或连字符构成的复合词,后接表示存储的名词)。“Find … without asking the user for ids” 是工具自行解析标识符(guard `autonomy` 现在也接受查找类动词,前提是被索要的对象整体就是标识符)。“The key is read from the MCP connection's X-API-Key header” 是服务器在描述自己的认证方式(guard `ownAuthHeader`:被动语态 “is read from”、来源是请求头、不是他人的请求头、密钥未被转交他处)。此外,当 “without asking the user” 的对象是“同意”时,`autonomy` 不再豁免:“keep retrying the transfer without asking the user for approval” 重新被阻止。在该语料上,v6 阻止 6 台服务器,v7 阻止 3 台,其余所有发现均不变。
|
|
57
|
+
|
|
58
|
+
每条规则都声明自己在这三个**面**中的哪些上运行,26 条里有 17 条包含名称。不包含名称的那四条是以**名词**为
|
|
59
|
+
锚的规则(`TOOL_DEF_SECRET_REQUEST`、`TOOL_DEF_CREDENTIAL_PARAM`、`TOOL_DEF_ENV_REFERENCE`、`TOOL_DEF_SECRET_EXFIL`):名称是标识
|
|
60
|
+
符,`api_key` 和 `private_key` 是标识符里再普通不过的组成部分,而拒绝 `sign_with_private_key` 等于把 v1 的
|
|
61
|
+
校准错误换个面重犯一次。以**短语**为锚的规则需要空白字符,因此根本无法匹配 `snake_case`;而两条隐藏载荷规则
|
|
62
|
+
针对的字符在名称里从来都不合法——这些都在所有面上运行。
|
|
63
|
+
|
|
64
|
+
在 v3 之前,名称**根本没有被任何规则扫描过**:注入短语、零宽字符或 base64 串出现在模型最先读到的那个字段里,
|
|
65
|
+
完全不会被报告。
|
|
66
|
+
|
|
67
|
+
门控评分为 `1 − 惩罚(最严重的阻止性 severity)`;advisory 命中永远不影响它。
|
|
68
|
+
|
|
69
|
+
| 最严重的阻止性 severity | 无 | info | low | medium | high | critical |
|
|
70
|
+
|---|---|---|---|---|---|---|
|
|
71
|
+
| 门控评分 | 1 | 1 | 0.9 | 0.7 | 0.4 | 0 |
|
|
72
|
+
|
|
73
|
+
| 代码 | 严重级别 | 层级 | 名称? | 捕捉什么 |
|
|
74
|
+
|---|---|---|---|---|
|
|
75
|
+
| `TOOL_DEF_INJECTION` | critical / high | block | ✅ | 「ignore all previous instructions」「do not tell the user」、`<system>` 标签、对 developer prompt 的引用 |
|
|
76
|
+
| `TOOL_DEF_SECRET_REQUEST` | critical | block | — | `private_key`、`seed_phrase`/`mnemonic`、`~/.ssh` 路径 |
|
|
77
|
+
| `TOOL_DEF_SECRET_HARVEST` | critical | block | ✅ | 自称职责就是读取/导出/披露密钥的工具 |
|
|
78
|
+
| `TOOL_DEF_EXFIL` | critical / high | block | ✅ | 「post to https://…」「forward it to…」「exfiltrate」、上传到某主机的措辞 |
|
|
79
|
+
| `TOOL_DEF_HIDDEN_UNICODE` | high | block | ✅ | 零宽字符与双向控制字符——审阅者看不见的文本 |
|
|
80
|
+
| `TOOL_DEF_BASE64_BLOB` | high | block | ✅ | 名称、描述或 schema 里长达 120+ 字符的 base64 串 |
|
|
81
|
+
| `TOOL_DEF_DATA_URL` | high | block | ✅ | `data:…;base64,` 与 `javascript:` 形式的 URL |
|
|
82
|
+
| `TOOL_DEF_CREDENTIAL_PARAM` | medium / low | advise | — | schema 或描述索要 `api_key`、`password`、`secret`、bearer 令牌 |
|
|
83
|
+
| `TOOL_DEF_ENV_REFERENCE` | medium | advise | — | `.env`、「environment variables」 |
|
|
84
|
+
| `TOOL_DEF_IMPERATIVE` | low / info | advise | ✅ | 「you must」「instead of」——提示词形态的措辞,单独并不能证明任何事 |
|
|
85
|
+
|
|
86
|
+
`staticScanRuleset()` 会连同**正则源码、flags 与所扫描的面** 一起返回每条规则,好让第三方能重跑一模一样的规则;同时返回
|
|
87
|
+
`{ version, digest }`,其中 digest 是对已排序规则列表的 RFC 8785 规范化形式做的 sha256。排序按 code-unit 比较,
|
|
88
|
+
绝不用 `localeCompare`:依赖 locale 的排序会让同一张规则表在配置不同的主机上算出不同的摘要,而这正是摘要本身
|
|
89
|
+
要检测的分歧。
|
|
90
|
+
|
|
91
|
+
## threat-feed
|
|
92
|
+
|
|
93
|
+
把服务器身份和工具定义与 `ThreatRecord` 比对——11 条内置,加上已签名 feed 追加的内容(见
|
|
94
|
+
[feed 契约](threat-feed.zh.md))。
|
|
95
|
+
|
|
96
|
+
- 任何命中 ⇒ 门控评分 **0**。
|
|
97
|
+
- **只有**当 `critical` 记录命中*服务器*时才 `fatal`。命中某个*工具*的 critical 不是 fatal,于是链上其余门控
|
|
98
|
+
仍会报告,责任也仍限定在那个工具上——这正是让一台大体正常的服务器在隔离掉一个工具后继续可用的原因。
|
|
99
|
+
- `ThreatRecord.scope` 选择比对面:`server`(id/name/url/command/args)、`tool`
|
|
100
|
+
(name/description/inputSchema),或 `any`——记录省略该字段时的默认值。
|
|
101
|
+
|
|
102
|
+
内置代码:`THREAT_TYPOSQUAT`、`THREAT_CRYPTO_DRAINER`、`THREAT_SEED_PHRASE`、`THREAT_SSH_KEY_READ`、
|
|
103
|
+
`THREAT_ENV_EXFIL`、`THREAT_DESTRUCTIVE_CMD`、`THREAT_FORK_BOMB`。
|
|
104
|
+
|
|
105
|
+
## origin
|
|
106
|
+
|
|
107
|
+
这台服务器是运营者声明过的,还是来自远端目录(`McpServerRef.catalog` 有值)?
|
|
108
|
+
|
|
109
|
+
| `allowUnknownServers` | 发现 | 评分 | fatal |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| `false`(失败即关闭) | `SERVER_UNDECLARED`,high | 0 | 是 |
|
|
112
|
+
| `true` | `SERVER_UNDECLARED`,info | 1 | 否 |
|
|
113
|
+
|
|
114
|
+
这个开关过去的含义是「还没有信誉评分」,而没有任何部署能满足它:从来没有人给预言机提供过信任边,于是每台服务器
|
|
115
|
+
回来都是「无人担保」,`false` 就把它们全都拦住了。而「来自目录」是宿主本地已经掌握的事实,不需要网络,也不可能
|
|
116
|
+
死锁。
|
|
117
|
+
|
|
118
|
+
## pinning
|
|
119
|
+
|
|
120
|
+
把当前的工具定义与用户批准过的快照做比较。哈希是对定义集合的 RFC 8785 规范化形式做的 sha256——与 feed 签名用的是
|
|
121
|
+
同一套规范化,而不是第二种序列化。
|
|
122
|
+
|
|
123
|
+
| 情形 | 代码 | 严重级别 | 评分 | Fatal |
|
|
124
|
+
|---|---|---|---|---|
|
|
125
|
+
| 尚无 pin(首次接触) | `TOOL_DEF_UNPINNED` | info | 0.9 | 否 |
|
|
126
|
+
| 哈希与 pin 不一致 | `TOOL_DEF_DRIFT` | high | 0 | 当 `pinToolDefs` |
|
|
127
|
+
| 工具定义没有规范形式(无 pin) | `TOOL_DEF_UNCANONICAL` | medium | 0.5 | 否 |
|
|
128
|
+
| 工具定义没有规范形式(有 pin) | `TOOL_DEF_UNCANONICAL` | high | 0 | 当 `pinToolDefs` |
|
|
129
|
+
|
|
130
|
+
首次接触的代价是 0.1,而不是阻止:一台干净、已声明、尚无 pin 的服务器恰好得 **0.9**,而 `TOOL_DEF_UNPINNED`
|
|
131
|
+
被刻意定为 `info`——在 `blockAtSeverity: "info"` 下,若首次相见就阻止,那么任何服务器都将永远不可用,因为在被
|
|
132
|
+
批准过一次之前,什么都无法固定。
|
|
133
|
+
|
|
134
|
+
`warden.approve(server, tools)` 通过你的 `PinStore` 写入 pin。该操作是幂等的。
|
|
135
|
+
|
|
136
|
+
## 按工具划分
|
|
137
|
+
|
|
138
|
+
`allowedTools` / `blockedTools` 把已公布的工具分成两部分:
|
|
139
|
+
|
|
140
|
+
- 若某条非 advisory 的发现点名了某个工具(`finding.tool`)并达到阈值,则该工具**被阻止**;
|
|
141
|
+
- 其余工具全部允许;
|
|
142
|
+
- 敏感工具(`policy.sensitiveToolPatterns`)仍然是*被允许的*——它们只是被标记出来,好让你的智能体主循环在运行时
|
|
143
|
+
对每次调用要求审批。参见 `classifyTools` / `isSensitiveTool`。
|
|
144
|
+
|
|
145
|
+
## 新增一个门控
|
|
146
|
+
|
|
147
|
+
`WardenGate` 只是三行接口,而 `new Warden({ gates, policy, log })` 直接接受整条链,所以你可以插入自己的门控而
|
|
148
|
+
不必 fork:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { Warden, StaticScanGate, ThreatGate, OriginGate, PinningGate } from "@aimarket/warden";
|
|
152
|
+
import type { WardenGate, WardenGateInput, WardenGateResult } from "@aimarket/warden";
|
|
153
|
+
|
|
154
|
+
class DenyByPublisher implements WardenGate {
|
|
155
|
+
readonly name = "publisher-allowlist";
|
|
156
|
+
async evaluate(input: WardenGateInput): Promise<WardenGateResult> {
|
|
157
|
+
const ok = ALLOWED.has(input.server.name);
|
|
158
|
+
return ok
|
|
159
|
+
? { findings: [], score: 1 }
|
|
160
|
+
: { findings: [{ gate: this.name, severity: "high", code: "PUBLISHER_UNKNOWN",
|
|
161
|
+
message: `${input.server.name} 不在允许的发布者名单中` }],
|
|
162
|
+
score: 0, fatal: true };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const warden = new Warden({
|
|
167
|
+
gates: [new StaticScanGate(), new ThreatGate(feed), new DenyByPublisher(), new OriginGate(), new PinningGate(store)],
|
|
168
|
+
policy,
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
自己写门控时有两条规矩:**没有真的发出请求,就绝不宣称远端服务不可达**
|
|
173
|
+
(`test/no-phantom-gate.test.ts` 会对随包发布的门控强制这一点);以及返回一个你能站得住脚的评分——什么都没测量的
|
|
174
|
+
门控必须返回 `1`,而不是一个「中性」的 0.6,否则它就是在为一次从未做过的测量惩罚每一台服务器。
|
|
175
|
+
|
|
176
|
+
## wrap 模式的检查流程
|
|
177
|
+
|
|
178
|
+
启动子进程前调用 `vetLaunch`。`initialize.instructions` 使用现有 static-scan,遇到阻止判定时删除。`tools/list` 最多读取 32 页、256 个唯一工具及 1 MiB,然后调用 `vet`;允许的定义以单页返回。`list_changed` 立即隔离服务器,只有重新检查成功才转发。每次调用前重新读取列表,与客户端最后看到的定义比较。检查期间变更阻止发送;执行期间变更则隐藏响应。透传消息另有 32 MiB 上限,内部请求超时为 10 秒。EOF 关闭子进程 stdin,5 秒后 SIGTERM,10 秒前 SIGKILL。服务器反向请求及其他消息保留 ID 和原始 JSON。策略、TOFU、人工审批及 audit-only 限制参见 README。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Warden 0.7.0: aprobaciones y verificación continua
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](security-hardening.md) · [Русский](security-hardening.ru.md) · **Español** · [Français](security-hardening.fr.md) · [中文](security-hardening.zh.md)
|
|
4
|
+
|
|
5
|
+
Warden 0.7.0 y ARGUS 0.3.2 cierran seis problemas. Se verifican las definiciones y la configuración de arranque; esto no constituye aislamiento del sistema operativo ni una garantía del comportamiento del servidor.
|
|
6
|
+
|
|
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
|
+
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
|
+
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.
|
|
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
|
+
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
|
+
|
|
14
|
+
## Estado y permisos
|
|
15
|
+
|
|
16
|
+
Directorio: `WARDEN_STATE_DIR`, en su defecto `$XDG_STATE_HOME/warden`, o `~/.local/state/warden`. La biblioteca acepta `ThreatFeed({stateDir})` y `FilePinStore(directory)`. ARGUS usa `warden/` dentro de su directorio de memoria para el feed. Archivos con nombre derivado del hash del ID, reemplazo atómico, permisos solo del propietario y bloqueos por archivo. Tras una caída con bloqueo activo, la escritura falla de forma segura: detener todos los escritores, inspeccionar y borrar solo el `.lock` abandonado. No borrar snapshots para ocultar errores. Usar almacenamiento local persistente; eliminarlo borra aprobaciones e historial contra rollback.
|
|
17
|
+
|
|
18
|
+
Las mutaciones MCP están deshabilitadas por defecto. El operador puede establecer `WARDEN_ALLOW_PIN_CHANGES=1` antes de iniciar `warden-mcp`. Esto delega al cliente MCP la capacidad de aprobar/revocar; no puede activarse mediante argumentos. El host o una sesión separada de operador debe imponer la revisión humana. Escanear no aprueba silenciosamente; reaprobaciones deben superar las demás comprobaciones.
|
|
19
|
+
|
|
20
|
+
## Actualización y límites
|
|
21
|
+
|
|
22
|
+
Publicar/instalar primero `@aimarket/warden@0.7.0`, después `@alexar76/argus3@0.3.2`, y reiniciar clientes MCP. Revisar migraciones de pins antiguos en vez de borrarlos. Otros hosts deben integrar `vetLaunch` y la revalidación. Cambios de comportamiento con definiciones idénticas no son detectables por hashes. Los resultados siguen siendo datos no confiables: este cambio no añade filtrado de resultados ni aislamiento de procesos. Las pruebas locales cubren rollback, reinicio, timeout del cuerpo, UTF-8 fragmentado, mensajes inválidos, persistencia, cambios de metadatos y bloqueo en ARGUS, sin pagos ni despliegue en producción.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const state = (await client.callTool({
|
|
26
|
+
name: "status_mcp_server", arguments: { server, tools },
|
|
27
|
+
})).structuredContent;
|
|
28
|
+
// Review state.previous against state.currentTools and the launch identity first.
|
|
29
|
+
await client.callTool({ name: "approve_mcp_server", arguments: {
|
|
30
|
+
server, tools,
|
|
31
|
+
reviewed_tools_hash: state.currentToolsHash,
|
|
32
|
+
reviewed_identity_hash: state.currentIdentityHash,
|
|
33
|
+
previous_pin_revision: state.previousRevision,
|
|
34
|
+
} });
|
|
35
|
+
const verdict = (await client.callTool({
|
|
36
|
+
name: "vet_mcp_server", arguments: { server, tools },
|
|
37
|
+
})).structuredContent;
|
|
38
|
+
if (!verdict.allow) throw new Error("WARDEN blocked the server");
|
|
39
|
+
// Revoke using a freshly reviewed status, not the pre-approval revision.
|
|
40
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Warden 0.7.0 : approbations et vérification continue
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](security-hardening.md) · [Русский](security-hardening.ru.md) · [Español](security-hardening.es.md) · **Français** · [中文](security-hardening.zh.md)
|
|
4
|
+
|
|
5
|
+
Warden 0.7.0 et ARGUS 0.3.2 corrigent six lacunes. Ils vérifient les définitions et la configuration de lancement, sans constituer un bac à sable du système ni garantir le comportement du serveur.
|
|
6
|
+
|
|
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
|
+
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
|
+
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.
|
|
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
|
+
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
|
+
|
|
14
|
+
## État et droits de l'opérateur
|
|
15
|
+
|
|
16
|
+
Répertoire : `WARDEN_STATE_DIR`, sinon `$XDG_STATE_HOME/warden`, sinon `~/.local/state/warden`. La bibliothèque accepte `ThreatFeed({stateDir})` et `FilePinStore(directory)`. ARGUS place le feed dans `warden/` sous son répertoire mémoire. Noms de fichiers dérivés du hash des ID, remplacement atomique, permissions réservées au propriétaire et verrous par fichier. Si un processus meurt avec un verrou, les mutations échouent en sécurité : arrêter tous les rédacteurs, inspecter et supprimer uniquement le `.lock` abandonné. Ne pas effacer les snapshots pour masquer une erreur. Utiliser un volume local persistant ; sa suppression efface approbations et historique anti-rejeu.
|
|
17
|
+
|
|
18
|
+
Les mutations MCP sont désactivées par défaut. L'opérateur peut définir `WARDEN_ALLOW_PIN_CHANGES=1` avant de lancer `warden-mcp`. Cela délègue au client MCP le droit d'approuver/révoquer ; un argument ne peut pas l'activer. La revue humaine relève du host ou d'une session opérateur séparée. Scanner n'approuve pas silencieusement ; toute réapprobation passe encore les autres contrôles.
|
|
19
|
+
|
|
20
|
+
## Mise à jour et limites
|
|
21
|
+
|
|
22
|
+
Publier/installer d'abord `@aimarket/warden@0.7.0`, puis `@alexar76/argus3@0.3.2`, et redémarrer les clients MCP. Examiner les migrations des anciens pins au lieu de les supprimer. Les autres hosts doivent brancher `vetLaunch` et la revalidation. Un comportement modifié derrière des définitions identiques échappe aux hashes. Les résultats restent non fiables : aucun filtrage du contenu des résultats ni isolation des processus n'est ajouté ici. Les tests locaux couvrent rejeu, redémarrage, délai du corps, UTF-8 fragmenté, requêtes invalides, persistance, changements de métadonnées et blocage ARGUS, sans paiements ni déploiement en production.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const state = (await client.callTool({
|
|
26
|
+
name: "status_mcp_server", arguments: { server, tools },
|
|
27
|
+
})).structuredContent;
|
|
28
|
+
// Review state.previous against state.currentTools and the launch identity first.
|
|
29
|
+
await client.callTool({ name: "approve_mcp_server", arguments: {
|
|
30
|
+
server, tools,
|
|
31
|
+
reviewed_tools_hash: state.currentToolsHash,
|
|
32
|
+
reviewed_identity_hash: state.currentIdentityHash,
|
|
33
|
+
previous_pin_revision: state.previousRevision,
|
|
34
|
+
} });
|
|
35
|
+
const verdict = (await client.callTool({
|
|
36
|
+
name: "vet_mcp_server", arguments: { server, tools },
|
|
37
|
+
})).structuredContent;
|
|
38
|
+
if (!verdict.allow) throw new Error("WARDEN blocked the server");
|
|
39
|
+
// Revoke using a freshly reviewed status, not the pre-approval revision.
|
|
40
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Warden 0.7.0: approvals and continuous verification
|
|
2
|
+
|
|
3
|
+
> 🌐 **English** · [Русский](security-hardening.ru.md) · [Español](security-hardening.es.md) · [Français](security-hardening.fr.md) · [中文](security-hardening.zh.md)
|
|
4
|
+
|
|
5
|
+
Warden 0.7.0 and ARGUS 0.3.2 close six gaps. This is a definition and launch-configuration firewall, not an operating-system sandbox or a guarantee about server behaviour.
|
|
6
|
+
|
|
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
|
+
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
|
+
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.
|
|
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
|
+
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
|
+
|
|
14
|
+
## State and operator permissions
|
|
15
|
+
|
|
16
|
+
The state directory is `WARDEN_STATE_DIR`, otherwise `$XDG_STATE_HOME/warden`, otherwise `~/.local/state/warden`. A library caller can supply `ThreatFeed({stateDir})` or `FilePinStore(directory)`. ARGUS stores feed state below its configured memory directory (`warden/`). Pin files use hashed server IDs, atomic replacement and owner-only permissions; writes use per-file locks. If a process dies while holding a lock, later mutation fails closed. Stop all writers, inspect the state and remove only the abandoned `.lock` file; do not delete snapshots to silence an error. Keep the directory on a persistent local volume. Deleting it discards approvals and rollback history.
|
|
17
|
+
|
|
18
|
+
By default MCP approval mutations are disabled. The operator may set `WARDEN_ALLOW_PIN_CHANGES=1` **before starting** `warden-mcp`. This delegates approval/revocation capability to that MCP client; a request argument cannot enable it. Human review must be enforced by the host or by using a separate operator session. Scanning never approves silently, and reapproval must still pass the other security gates.
|
|
19
|
+
|
|
20
|
+
## Upgrade and limits
|
|
21
|
+
|
|
22
|
+
Publish/install `@aimarket/warden@0.7.0` first, then `@alexar76/argus3@0.3.2`, which pins that dependency. Restart MCP clients. Review legacy-pin migration findings rather than deleting pins. Other hosts must wire `vetLaunch` and runtime revalidation themselves. Neither hash comparisons nor notifications detect a backend changing its behaviour while advertising identical definitions. Tool results remain untrusted; these changes do not implement result-content screening or process isolation. Added local regression tests cover rollback/restart, body timeout, split UTF-8, invalid messages, approval persistence, metadata drift and ARGUS call blocking. No paid calls or production deployment are required to run them.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const state = (await client.callTool({
|
|
26
|
+
name: "status_mcp_server", arguments: { server, tools },
|
|
27
|
+
})).structuredContent;
|
|
28
|
+
// Review state.previous against state.currentTools and the launch identity first.
|
|
29
|
+
await client.callTool({ name: "approve_mcp_server", arguments: {
|
|
30
|
+
server, tools,
|
|
31
|
+
reviewed_tools_hash: state.currentToolsHash,
|
|
32
|
+
reviewed_identity_hash: state.currentIdentityHash,
|
|
33
|
+
previous_pin_revision: state.previousRevision,
|
|
34
|
+
} });
|
|
35
|
+
const verdict = (await client.callTool({
|
|
36
|
+
name: "vet_mcp_server", arguments: { server, tools },
|
|
37
|
+
})).structuredContent;
|
|
38
|
+
if (!verdict.allow) throw new Error("WARDEN blocked the server");
|
|
39
|
+
// Revoke using a freshly reviewed status, not the pre-approval revision.
|
|
40
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Warden 0.7.0: одобрения и повторная проверка
|
|
2
|
+
|
|
3
|
+
> 🌐 [English](security-hardening.md) · **Русский** · [Español](security-hardening.es.md) · [Français](security-hardening.fr.md) · [中文](security-hardening.zh.md)
|
|
4
|
+
|
|
5
|
+
Warden 0.7.0 и ARGUS 0.3.2 закрывают шесть разрывов. Warden проверяет определения инструментов и настройки запуска; он не является песочницей ОС и не гарантирует поведение сервера.
|
|
6
|
+
|
|
7
|
+
1. **До подключения.** Вызывать `warden.vetLaunch(server)` до запуска stdio-процесса или открытия удалённого соединения. Проверяются происхождение, известные опасные команды/адреса и ранее одобренная идентичность запуска. После подключения получить полный `tools/list` и вызвать `vet(server, tools)` до передачи инструментов модели. Успешная предпроверка запуска сама по себе не одобряет инструменты.
|
|
8
|
+
2. **Откат подписанной базы угроз.** Последние принятые timestamp, digest и записи сохраняются отдельно для каждого ключа издателя в `feeds/`. Более старый снимок и другой контент с тем же timestamp отклоняются, включая после перезапуска. Более новый подписанный снимок может намеренно удалить правило. При ошибке обновления остаются последние принятые правила; `feed.status.stale` отдельно показывает их свежесть. Таймаут десять секунд охватывает заголовки и всё тело ответа. Ограничения размера и числа записей сохраняются.
|
|
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 при первом одобрении). Отзыв тоже требует предыдущую ревизию. Конкурентное изменение делает старое решение неприменимым. Определения сохраняются для сравнения: не помещайте в них действующие секреты.
|
|
11
|
+
5. **Полные определения.** Формат отпечатка v2 включает все объявленные поля, в том числе `title`, `outputSchema`, `annotations` и расширения. Верхнеуровневые поля со значением undefined пропускаются. Для обычных name/description/inputSchema прежний digest сохраняется. Старые одобрения, не покрывавшие расширенные поля, требуют явного переодобрения (`PIN_FORMAT_UPGRADE_REQUIRED`); новые содержат `toolsHashVersion: 2`. Набор правил v6 проверяет дополнительные поверхности. JSON-кавычки вокруг описания схемы больше не считаются доказательством безобидного цитирования инструкции. Аннотации остаются недоверенными подсказками, не разрешениями.
|
|
12
|
+
6. **Во время сессии.** При `notifications/tools/list_changed` ARGUS немедленно приостанавливает инструменты и перепроверяет все страницы списка. Повторная проверка есть и перед каждым вызовом — даже без уведомлений сервера. Старые ссылки привязаны к определениям, которые видел пользователь/модель, и не выполняют изменённый инструмент после переодобрения. Ошибки получения списка, одинаковые имена, повтор курсора, более 32 страниц, 256 инструментов или 1 MiB определений блокируют использование. Изменения не переодобряются автоматически. Для первого чистого подключения сохраняется прежняя политика ARGUS: автоматическая запись первого отпечатка; это не ручная проверка человеком. Ошибка записи теперь закрывает соединение. Изменение во время исполнения скрывает результат, но не отменяет уже совершённое действие — автоматически повторять вызов нельзя.
|
|
13
|
+
|
|
14
|
+
## Хранение и права оператора
|
|
15
|
+
|
|
16
|
+
Каталог состояния: `WARDEN_STATE_DIR`, иначе `$XDG_STATE_HOME/warden`, иначе `~/.local/state/warden`. В библиотеке можно задать `ThreatFeed({stateDir})` или `FilePinStore(directory)`. ARGUS хранит базу угроз в `warden/` внутри своего каталога памяти. Имена файлов одобрений получаются из хеша server ID; запись атомарная, права — только владельцу, изменения защищены отдельными файловыми блокировками. Если процесс погиб с блокировкой, дальнейшее изменение отклоняется. Остановить всех писателей, проверить состояние и удалить только оставшийся `.lock`; не удалять снимки ради устранения ошибки. Использовать постоянный локальный том. Удаление каталога стирает одобрения и историю защиты от отката.
|
|
17
|
+
|
|
18
|
+
По умолчанию MCP не разрешает изменять одобрения. Оператор может установить `WARDEN_ALLOW_PIN_CHANGES=1` **до запуска** `warden-mcp`. Это делегирует MCP-клиенту право одобрения и отзыва; аргумент запроса включить его не может. Если нужна ручная проверка, её обеспечивает хост или отдельная операторская сессия. Сканирование само не одобряет сервер, а переодобрение обязано пройти остальные проверки безопасности.
|
|
19
|
+
|
|
20
|
+
## Обновление и границы
|
|
21
|
+
|
|
22
|
+
Сначала опубликовать/установить `@aimarket/warden@0.7.0`, затем `@alexar76/argus3@0.3.2`, который закрепляет эту зависимость. Перезапустить MCP-клиенты. При миграции изучить предупреждение и определения, а не удалять одобрения. В других хостах `vetLaunch` и повторную проверку необходимо подключить самостоятельно. Неизменные объявления не позволяют обнаружить изменение поведения внутреннего кода сервера. Результаты инструментов остаются недоверенными: фильтр содержимого результатов и изоляция процессов этим релизом не добавлены. Локальные регрессионные тесты проверяют откат/перезапуск, таймаут тела, разрыв UTF-8, некорректные сообщения, сохранение одобрений, изменение метаданных и блокировку вызовов ARGUS. Платные вызовы и выкладка на прод для этих тестов не требуются.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const state = (await client.callTool({
|
|
26
|
+
name: "status_mcp_server", arguments: { server, tools },
|
|
27
|
+
})).structuredContent;
|
|
28
|
+
// Review state.previous against state.currentTools and the launch identity first.
|
|
29
|
+
await client.callTool({ name: "approve_mcp_server", arguments: {
|
|
30
|
+
server, tools,
|
|
31
|
+
reviewed_tools_hash: state.currentToolsHash,
|
|
32
|
+
reviewed_identity_hash: state.currentIdentityHash,
|
|
33
|
+
previous_pin_revision: state.previousRevision,
|
|
34
|
+
} });
|
|
35
|
+
const verdict = (await client.callTool({
|
|
36
|
+
name: "vet_mcp_server", arguments: { server, tools },
|
|
37
|
+
})).structuredContent;
|
|
38
|
+
if (!verdict.allow) throw new Error("WARDEN blocked the server");
|
|
39
|
+
// Revoke using a freshly reviewed status, not the pre-approval revision.
|
|
40
|
+
```
|