dsh-perm-gate 5.0.2 → 5.0.4

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/README.es.md ADDED
@@ -0,0 +1,489 @@
1
+ # dsh-perm-gate
2
+
3
+ - [English README](./README.md)
4
+ - [中文 README](./README.zh.md)
5
+ - [日本語 README](./README.ja.md)
6
+ - [한국어 README](./README.ko.md)
7
+ - [Français README](./README.fr.md)
8
+ - [Deutsch README](./README.de.md)
9
+ - [Italiano README](./README.it.md)
10
+ - [Русский README](./README.ru.md)
11
+ - [Español README](./README.es.md)
12
+ - [Installation guide](./INSTALL.md)
13
+ - [中文安装指南](./INSTALL.zh.md)
14
+ - [日本語インストールガイド](./INSTALL.ja.md)
15
+ - [한국어 설치 안내](./INSTALL.ko.md)
16
+ - [Guide d'installation](./INSTALL.fr.md)
17
+ - [Installationsanleitung](./INSTALL.de.md)
18
+ - [Guida all'installazione](./INSTALL.it.md)
19
+ - [Руководство по установке](./INSTALL.ru.md)
20
+ - [Guía de instalación](./INSTALL.es.md)
21
+ - [Changelog](./CHANGELOG.md)
22
+ - [日本語 changelog](./CHANGELOG.ja.md)
23
+ - [한국어 changelog](./CHANGELOG.ko.md)
24
+ - [Français changelog](./CHANGELOG.fr.md)
25
+ - [Deutsch changelog](./CHANGELOG.de.md)
26
+ - [Italiano changelog](./CHANGELOG.it.md)
27
+ - [Русский changelog](./CHANGELOG.ru.md)
28
+ - [Español changelog](./CHANGELOG.es.md)
29
+
30
+ > **Nota de compatibilidad:** la v2.0.0 incluye diccionarios `ja` / `ko`, pero el DSH oficial solo
31
+ > expone `zh` / `en` a través de `LocaleRuntime` (`LOCALE_IDS = ["zh", "en"]`). En un DSH sin
32
+ > modificar, seleccionar `ja` / `ko` falla con `locale "<id>" is not registered`. Use un fork de
33
+ > DSH que actualice `LOCALE_IDS` (locale-settings.ts) y las etiquetas `LOCALES`
34
+ > (client/index.ts), y recompile.
35
+
36
+ > **▼ Compatibilidad de versiones de DSH**
37
+ >
38
+ > Dos líneas de DSH se atienden desde dos ramas de larga vida, cada una con su propia serie de
39
+ > versiones, su `engines.dsh` y su dist-tag de npm ([esquema de releases](./RELEASING.md)):
40
+ >
41
+ > | Versión de DSH | Rama | Versión | Tag de npm |
42
+ > | --- | --- | --- | --- |
43
+ > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
44
+ > | 0.1.2-alpha.1+ (incl. 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x` es un rango) |
45
+ > | 0.2.0-rc.1 (línea 0.2.0) | `compat/0.2.0` | `5.x` | `@dsh-0.2.0` |
46
+ >
47
+ > El número de serie sigue la **línea de DSH** (`1.x` = DSH ≤ 0.1.1, `2.x` = DSH 0.1.2+), y los
48
+ > mayores se cercan entre sí: una instalación `^1.x` nunca resuelve una versión `2.x`, y
49
+ > viceversa. `engines.dsh` declara la misma separación, pero DSH nunca lo lee: son los rangos y
50
+ > los dist-tags lo que mantienen un DSH antiguo en `1.x`.
51
+ >
52
+ > `@deepseek-ai/dsh-client-runtime` fue **eliminado** en `0.1.2-alpha.1` — no se limitó a mudarse.
53
+ > La línea `legacy` sigue llegando a `ctx.slots` a través de él; `main` obtiene la misma
54
+ > declaración de `@deepseek-ai/dsh-client-ui-renderer/client`. Dos costuras sensibles a la versión
55
+ > se manejan con sondeos de capacidad en lugar de comprobaciones de versión: (1) el registro de
56
+ > ajustes usa `register`, presente en ambas líneas (`installSection` es una adición, no un
57
+ > reemplazo); (2) `effectivePolicy` es un método **privado** del servicio de aprobación de usuario
58
+ > en ambas, así que se lee tras un sondeo `typeof` y degrada a «política desconocida» cuando falta
59
+ > o lanza un error.
60
+
61
+ Versión **2.6.0** — véase el [changelog](./CHANGELOG.es.md).
62
+
63
+ Una única puerta de permisos autosuficiente, determinista en primer término y fail-closed para
64
+ DeepSeek Harness.
65
+
66
+ `dsh-perm-gate` decide cada llamada a una herramienta mediante una cadena de prioridad fija:
67
+
68
+ | Etapa | Decisión | Qué es |
69
+ | ---- | ---- | ---- |
70
+ | **P0** | `deny` | denegación dura determinista: material de credenciales, mutación de rutas protegidas, shell peligroso |
71
+ | **P1** | `allow` | una **concesión de sesión** precisa y acotada |
72
+ | **P2** | `deny/allow/ask` | cadena de reglas estática: primero la lista negra, luego allow, luego ask |
73
+ | **P3** | `allow/deny/ask` | clasificador semántico LLM opcional (**desactivado** por defecto) |
74
+ | **P4** | `ask` | el seam oficial de aprobación |
75
+
76
+ Estrictamente fail-closed: una decisión P0 nunca es anulada por una concesión, una regla, el
77
+ clasificador o un humano.
78
+
79
+ ## Por qué
80
+
81
+ El ecosistema de seguridad de DSH reparte esto entre varios plugins (`dsh-permission-rules`,
82
+ `dsh-auto-mode`, `dsh-auto-review`, `dsh-movein-permissions`). `dsh-perm-gate` fusiona la puerta +
83
+ la aprobación + el clasificador (opcional) en un solo paquete, con una única pista de auditoría y
84
+ sin acoplamiento de versiones entre plugins.
85
+
86
+ ## Funcionalidades
87
+
88
+ - **Lista blanca / negra de comandos** — emparejamiento sobre una **descomposición argv** (no una
89
+ cadena cruda), con descenso recursivo en `sh -c`/`bash -c`, detección de tuberías, comprobación
90
+ de destinos de redirección y reconocimiento de recursivo/forzado (`rm -rf`).
91
+ - **Prioridad del deny** — una regla deny que coincida vence a cualquier regla allow.
92
+ - **Concesiones de sesión** — concesiones precisas `(herramienta, huella canónica)` con `TTL` +
93
+ `maxUses`; relanzar con un destino distinto nunca reutiliza la autoridad. Los sub-agentes
94
+ heredan pero no pueden acuñar.
95
+ - **Motor de reglas de funciones puras** — compilación glob/regex con cota de ReDoS, fallo ruidoso
96
+ ante reglas malformadas y caché de compilación por hash de la fuente.
97
+ - **Auditoría** — cada decisión se registra como un evento `{ignorable:true}` con su `callId`; la
98
+ razón visible para el modelo coincide con el desenlace registrado.
99
+ - **Nivel 自动审查** (`permissive`, además de `permissive-full`) — un **modo de aprobación
100
+ independiente** (aparte de los niveles de solo lectura, acceso completo y lista blanca) que no
101
+ es ni «autoaprobación» ni confianza ciega. El frontend expone un **único interruptor**
102
+ (`permissive`); las cuatro estrategias de backend son **combinables** y las dirige la
103
+ configuración del plugin — siempre fail-closed frente a P0. El selector de permisos y la fila de
104
+ ajustes lo muestran ambos bajo la etiqueta de producto 自动审查, sin icono. La variante
105
+ `permissive-full` conserva el mismo comportamiento de aprobación pero elimina la sandbox de
106
+ archivos integrada, que de otro modo niega las tuberías con nombre que `git clone` / Cygwin /
107
+ ConPTY necesitan.
108
+ - **Respuesta automática a las escaladas de sandbox** (`trustEscalation`) — una escalada de
109
+ sandbox se pide desde *dentro* del cuerpo de la herramienta shell / pwsh / edit, tras
110
+ `tools/pre-execute`, de modo que la puerta nunca la vio y una llamada que había permitido
111
+ automáticamente seguía pidiéndole aprobar la ampliación. Con esta estrategia activada, la
112
+ llamada exacta que la puerta despejó (reconocida por `callId`) se responde aquí.
113
+ - **`llmAssist` con riesgo graduado** — un LLM propio compatible con OpenAI califica cada `ask`
114
+ como `safe` / `risky:<categoría>`; las categorías duras (borrado, credenciales, remoto, sistema,
115
+ masivo) **siempre preguntan**, lo neutro alimenta el aprendizaje de veredictos, y todo fallo
116
+ permanece fail-closed.
117
+ - **Aprendizaje de veredictos** — los `ask` de riesgo neutro que el humano aprueba y que realmente
118
+ se ejecutan se acumulan; superado el umbral, *la misma operación exacta* (emparejada por huella)
119
+ se auto-permite.
120
+ - **Flujo de eventos de decisión** — cada decisión se añade a un flujo JSONL y la mitad de
121
+ navegador lo muestra como una franja de aviso sobre la entrada de conversación, más una pestaña
122
+ de registros de aprobación (de lo más reciente a lo más antiguo) en la vista de conversación.
123
+
124
+ Una **lista negra de palabras clave deny predefinida** (heredada de los `DEFAULT_DENY_KEYWORDS` de
125
+ dsh-approval-gate: `rm -rf`, `push --force`, `drop table`, `mkfs`, `git reset --hard`,
126
+ `docker system prune`, …) veta toda llamada cuyo texto contenga una palabra clave — subcadena sin
127
+ distinguir mayúsculas, aplicada antes de la lista blanca, las concesiones y el LLM. Es editable
128
+ como lista en la tarjeta de ajustes (las entradas predefinidas van etiquetadas, y una
129
+ restauración de un clic recupera el preajuste); sin definir o vacía aplica el preajuste — la lista
130
+ negra nunca se apaga en silencio.
131
+
132
+ ## Instalación
133
+
134
+ Requiere una instalación existente de
135
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
136
+
137
+ ```sh
138
+ dsh plugin --profile web add dsh-perm-gate
139
+ ```
140
+
141
+ Los pasos completos de instalación, actualización, migración y resolución de problemas viven en la
142
+ [Guía de instalación](./INSTALL.es.md) — también disponible en
143
+ [English](./INSTALL.md) / [中文](./INSTALL.zh.md) / [日本語](./INSTALL.ja.md) /
144
+ [한국어](./INSTALL.ko.md) / [Français](./INSTALL.fr.md) / [Deutsch](./INSTALL.de.md) /
145
+ [Italiano](./INSTALL.it.md) / [Русский](./INSTALL.ru.md).
146
+
147
+ ## Configuración
148
+
149
+ Añada el plugin al `cordis.yml`:
150
+
151
+ ```yaml
152
+ - id: dsh-perm-gate
153
+ name: dsh-perm-gate
154
+ config:
155
+ rulesFile: ./permissions.yaml # optional; defaults to $DSH_HOME/perm-gate/rules.yml
156
+ dshHome: $DSH_HOME # root pinned for protected-target checks
157
+ defaultAction: ask # allow | ask | deny
158
+ gatePresets: [permissive, permissive-full] # tiers where the gate is active (default)
159
+ sessionSweep: true # hourly cleanup of archived/dead sessions' gate data
160
+ ```
161
+
162
+ ### Barrido de sesiones
163
+
164
+ Al arrancar y cada hora, la puerta lee el almacén de espacios de trabajo de DSH
165
+ (`$DSH_HOME/storages/workspace.json`, de solo lectura) y clasifica cada sesión para la que
166
+ conserva datos de la cadena de autorización. Las sesiones que DSH archivó
167
+ (`global.archivedSessionIds`) o que ya no rastrea en absoluto ven sus eventos de decisión
168
+ retirados de `$DSH_HOME/perm-gate/events.jsonl` y sus ficheros de instantánea previos al cambio
169
+ eliminados de `$DSH_HOME/perm-gate/snapshots/` — historia que la página de revisión ya no puede
170
+ alcanzar, por datos que el propio harness considera desaparecidos. Las sesiones vivas no se
171
+ tocan, las filas no atribuibles (id de sesión vacío) nunca se eliminan, y cualquier fallo es
172
+ fail-open: la pasada se salta y se reintenta una hora después. Fije `sessionSweep: false` para
173
+ desactivarlo; `workspaceStoreFile` reemplaza la ruta del almacén. Restaurar una sesión archivada
174
+ no restaura su historia barrida.
175
+
176
+ ### Fichero de reglas
177
+
178
+ ```yaml
179
+ permissions:
180
+ defaultAction: ask
181
+ deny:
182
+ - command: [rm#recursive]
183
+ reason: no recursive rm
184
+ - command: [ssh]
185
+ reason: no direct ssh
186
+ - paths: [.dsh/**]
187
+ reason: protect harness metadata
188
+ allow:
189
+ - command: [pnpm, node]
190
+ reason: dev tools
191
+ - command: [curl, wget]
192
+ args: ["https://*.example.com/*"]
193
+ reason: allowed endpoint
194
+ ask:
195
+ - command: [bash, sh]
196
+ reason: ask shells
197
+ ```
198
+
199
+ Una entrada de comando `word#flag` empareja la palabra del comando (`word`) con el modificador
200
+ `recursive` o `force` — así, `rm#recursive` coincide con `rm -rf`, `env rm -rf` y
201
+ `sh -c "rm -rf /"`.
202
+
203
+ ### Política de red (opt-in)
204
+
205
+ Un proxy local HTTP/CONNECT que juzga el tráfico saliente de los **subprocesos de shell** según
206
+ el mismo fichero de reglas, más una vía de aprobación para destinos que ninguna regla cubre.
207
+ **Desactivado por defecto** — activarlo enlaza un puerto de loopback y reescribe el entorno proxy
208
+ de los procesos hijos, así que nunca se enciende implícitamente.
209
+
210
+ ```yaml
211
+ - id: dsh-perm-gate
212
+ config:
213
+ networkEnabled: false # master switch (default false)
214
+ networkMode: whitelist # deny-all | whitelist | allow-all
215
+ networkUnlisted: ask # ask | deny — unlisted target handling
216
+ networkUnattributed: allow # allow | deny — traffic with no shell attribution
217
+ networkInjectEnv: true # rewrite HTTP(S)_PROXY/ALL_PROXY for children
218
+ networkAskTimeoutMs: 120000 # approval wait before failing closed
219
+ networkGrantTtlMs: 1800000 # how long one approval covers its target
220
+ ```
221
+
222
+ **Comportamiento por niveles.** Nada llega a la red sin una regla allow. Un destino no listado se
223
+ escala al seam de aprobación interactivo, planteado en nombre del comando de shell que abrió la
224
+ conexión; aprobar amplía el alcance de ese destino durante la sesión. Una regla `deny` nunca se
225
+ escala: la aprobación puede ampliar lo que un destino no listado puede alcanzar, pero jamás puede
226
+ anular una regla que dice no.
227
+
228
+ **El límite — léalo antes de fiarse de esto.** El proxy es una capa de política *cooperativa*,
229
+ no una frontera de aplicación. Solo ve el tráfico de clientes que leen el entorno de proxy:
230
+
231
+ | Cliente | ¿Cubierto? |
232
+ |--------|----------|
233
+ | `curl`, `wget`, `git`, Go `net/http`, Python `requests` | sí |
234
+ | **Node.js `http`/`https`/`fetch`** | **no — conecta directamente** |
235
+ | Java (sin flags de proxy `-D`), .NET `HttpClient` | no |
236
+ | Sockets en crudo, TCP propio | no |
237
+ | DNS, QUIC/HTTP3, protocolos no HTTP | no |
238
+ | Conexiones a una IP literal | no |
239
+
240
+ Así que un comando de shell como `node -e "require('http').get('http://host/')"` no se
241
+ intercepta. Trátelo como una barrera contra accidentes y un lugar donde declarar una intención,
242
+ no como una sandbox hermética.
243
+
244
+ El tráfico de red **propio** de DSH — las herramientas de red integradas y el transporte LLM — se
245
+ deja deliberadamente al margen. Esas conexiones no llevan atribución de shell, y
246
+ `networkUnattributed: allow` (el valor por defecto) las deja pasar sin revisión: revisarlas
247
+ permitiría al host bloquearse *a sí mismo*, lo cual es un fallo peor que un bloqueo omitido.
248
+ Fije `networkUnattributed: deny` solo si sabe que los clientes de su host ignoran el entorno de
249
+ proxy.
250
+
251
+ El estado en vivo se consulta en `GET /api/dsh-perm-gate/network` (modo, bind, puerto, viveza del
252
+ proxy, estado de inyección de entorno, contadores de bloqueos, bloqueos recientes).
253
+
254
+ ## El nivel 自动审查 (valor de máquina `permissive`)
255
+
256
+ 自动审查 es un **nivel de aprobación independiente** en el selector de permisos de DSH, en
257
+ paralelo a Solo lectura / Escritura de espacio de trabajo / Acceso completo / Lista blanca. No es
258
+ una «autoaprobación» genérica y nunca acuña autoridad ciega: solo estrecha o ensancha el seam
259
+ *antes* del paso humano/LLM, mientras la denegación dura P0 se mantiene monótona e innegociable
260
+ **dentro del ámbito propio de la puerta**.
261
+
262
+ > **P0 tiene alcance, no es global.** La puerta actúa solo cuando el preajuste de permisos de la
263
+ > sesión es uno de los `gatePresets` (por defecto `permissive` / `permissive-full`). Con
264
+ > cualquier otro preajuste — Solo lectura, Escritura de espacio de trabajo o Acceso completo —
265
+ > **toda** la puerta se retira, incluida la denegación dura P0, porque la política propia del
266
+ > nivel elegido gobierna esa sesión. Es deliberado (véase `gatePresets` en la tabla de
267
+ > configuración), pero significa que «P0 es innegociable» se cumple *dentro* de los niveles de la
268
+ > puerta, no en todos los niveles. Una retirada no es silenciosa: la puerta registra un evento
269
+ > `stand-down` por transición sesión/preajuste y el navegador muestra una franja persistente
270
+ > **GATE OFF** sobre la entrada. Fije `gatePresets: ['*']` para volver a hacer P0 global.
271
+
272
+ **Se envían dos variantes**, porque el `sandbox` y el `approval` de un preajuste son perillas
273
+ independientes y acoplarlas forzaba un mal intercambio:
274
+
275
+ | Etiqueta del selector | Valor de máquina | sandbox | approval |
276
+ |--------------|---------------|---------|----------|
277
+ | 自动审查 | `permissive` | `workspace-write` | `ask` |
278
+ | 自动审查(高权限) | `permissive-full` | `danger-full-access` | `ask` |
279
+
280
+ El nivel sencillo conserva la sandbox de archivos integrada. Esa sandbox también niega las
281
+ tuberías con nombre que un proceso hijo necesita para arrancar, de modo que `git clone`, el
282
+ `sh.exe` de MSYS2/Cygwin y ConPTY fallan bajo ella con `Win32 error 5` /
283
+ `couldn't create signal pipe`. Como la puerta solo está activa **en los niveles listados en
284
+ `gatePresets`**, querer la puerta implicaba aceptar esa restricción. 自动审查(高权限) elimina el
285
+ acoplamiento: idéntico comportamiento de aprobación, sin restricción de sandbox de archivos. La
286
+ descripción propia del nivel enuncia el intercambio sin rodeos — el flujo de trabajo es más
287
+ fluido, las aprobaciones siguen aplicándose por llamada, pero no queda **ninguna sandbox de
288
+ sistema como respaldo**. Ambos están en los `gatePresets` por defecto, así que cualquiera le da la
289
+ cadena completa P0–P4 — la puerta lee solo el **nombre** del preajuste, nunca el modo de sandbox.
290
+
291
+ La etiqueta del selector es una **cadena de producto suministrada por el host**, no una entrada de
292
+ diccionario por locale: DSH renderiza el `name:` del nivel de un plugin literalmente en ambas
293
+ superficies de permisos (la fila por defecto de los ajustes generales y el selector del composer)
294
+ y solo aporta sus propias etiquetas localizadas para los tres valores integrados, de modo que
295
+ `cordis.patch.yml` envía la etiqueta china para cada sesión.
296
+
297
+ El **icono** es otra historia. El mapa de glifos del composer está cerrado, y su propio comentario
298
+ enuncia la regla: *host-configured names outside the design set get none.* `permissive` es un
299
+ valor integrado, así que 自动审查 ya tiene un glifo escudo+ojo; `permissive-full` recibe el mismo
300
+ glifo solo porque `npx dsh-perm-gate-patch-glyph` lo añade a ese mapa. Ese parche edita un paquete
301
+ **del host**, así que se pierde en cada actualización de DSH — véase
302
+ [Tras una actualización de DSH](./INSTALL.es.md).
303
+
304
+ En `cordis.yml`:
305
+
306
+ ```yaml
307
+ - id: dsh-perm-gate
308
+ name: dsh-perm-gate
309
+ config:
310
+ rulesFile: ./permissions.yaml
311
+ defaultAction: ask
312
+ permissive: true # single front-facing switch (independent tier on)
313
+ permissiveStrategies: # backend, combinable
314
+ trustAutoAllow: true # in-scope safe ops auto-allow; dangerous/unknown ask
315
+ alwaysConfirm: false # every crossing asks; allow-controls add repeat-allow / wl-migrate buttons
316
+ llmAssist: false # LLM classify first, human fallback on ask/failure
317
+ trustEscalation: true # a cleared call's own sandbox escalation needs no prompt
318
+ ```
319
+
320
+ `trustAutoAllow` es el nivel intermedio de base (un allow por regla pasa automáticamente).
321
+ `alwaysConfirm` muestra el panel de aprobación en cada cruce; sus «controles allow» añaden dos
322
+ botones extendidos — **volver a permitir este tipo en esta sesión** (una concesión de sesión
323
+ acotada) y **permitir cada ocurrencia** (que persiste la palabra del comando en la lista blanca
324
+ `allow` del `permissions.yaml` vía `approveAllowEverywhere`). `llmAssist` consulta un LLM real
325
+ configurable (`classifierEndpoint` / `classifierModel`, cualquier API compatible con OpenAI) para
326
+ decidir automáticamente un `ask`, y recae en el seam humano ante `ask`/error — siempre
327
+ fail-closed. `trustEscalation` (activo por defecto mientras el nivel esté activo) responde una
328
+ escalada `sandbox_permissions` planteada desde dentro de una llamada que la puerta ya permitió;
329
+ véase abajo. Con `permissive` desactivado, la puerta se comporta exactamente como antes.
330
+
331
+ ### Escalada de sandbox: por qué un veredicto `safe` seguía mostrando la pregunta
332
+
333
+ Una llamada a una herramienta puede plantear **dos aprobaciones independientes**. La puerta posee
334
+ la primera — su propio `ask`, en la cascada `tools/pre-execute`. La segunda llega de
335
+ `approveEscalation` **dentro del cuerpo de la herramienta**, en el momento de `tools/execute`,
336
+ siempre que el modelo haya pasado `sandbox_permissions` + `justification`; para entonces
337
+ `tools/pre-execute` ya está resuelto, de modo que el allow de la puerta nunca llega a ella. Una
338
+ llamada que el LLM calificó `safe` y que la puerta auto-permitía seguía mostrando, pues, una
339
+ pregunta pidiendo aprobar la ampliación de sandbox.
340
+
341
+ `trustEscalation` cierra esa brecha. La puerta recuerda cada llamada que permitió positivamente
342
+ (indexada por la `callId` del host, que la petición de escalada repite) y responde ella misma la
343
+ escalada con `allowed-once`. Solo se aplica cuando se cumple **todo**:
344
+
345
+ - el nivel 自动审查 está activo y `trustEscalation` está activo;
346
+ - la petición lleva una `callId` que la puerta despejó, con nombre de herramienta coincidente;
347
+ - la razón es una escalada reconocida que nombra `workspace-write` o `danger-full-access`.
348
+
349
+ Todo lo demás — una razón no reconocida, otra llamada, una llamada que la puerta preguntó o
350
+ denegó, el paso a través `approval: never` — se delega al humano sin cambios, de modo que un
351
+ futuro cambio de redacción de DSH falla cerrado en vez de abierto. La respuesta automática queda
352
+ registrada en el flujo de eventos (`verdict: "escalation-auto"`, `mode: <objetivo>`). Apague el
353
+ interruptor para mantener la ampliación de sandbox bajo control humano mientras los demás allow
354
+ siguen siendo automáticos.
355
+
356
+ ### llmAssist con riesgo graduado, aprendizaje de veredictos y el flujo de eventos
357
+
358
+ Con `llmAssist` activo, el LLM configurado califica un `ask` cada vez con un protocolo
359
+ estructurado. La calificación ocurre **dentro de la cascada `tools/pre-execute` de la puerta,
360
+ antes de que la decisión vuelva al host**: un veredicto `safe` delega la llamada directamente, de
361
+ modo que el panel de aprobación nunca aparece; solo un veredicto realmente incierto llega hasta
362
+ usted. En la tarjeta de ajustes se pueden elegir dos fuentes de receptor: una **API propia**
363
+ (cualquier endpoint compatible con OpenAI — `classifierEndpoint` / `classifierModel` /
364
+ `classifierApiKey`, con preajustes que incluyen Xiaomi MiMo `https://api.xiaomimimo.com/v1`), o el
365
+ **grupo de modelos del host DSH** (el servicio `llm` configurado de la sesión, vía
366
+ `agentDefaultModel.currentSelection`, opcionalmente sustituido con `classifierProvider` /
367
+ `classifierModel`). Un botón de **prueba de salud** (`POST /api/dsh-perm-gate/health`) ejecuta una
368
+ completación mínima e informa de la latencia.
369
+
370
+ - `safe` → la llamada se auto-permite (auditada como fuente `classifier`); no se muestra panel.
371
+ - `risky` + una **categoría dura** (`deletion`, `credential`, `remote`, `system`, `bulk`) → la
372
+ llamada **conserva el ask humano**. El clasificador **nunca deniega**: la vía de denegación
373
+ pertenece solo a las capas deterministas (denegación dura P0, la lista negra de palabras clave
374
+ deny, las reglas `deny:` explícitas), de modo que una categoría mal calificada siempre queda
375
+ negociable en lugar de un bloqueo sin apelación. Las categorías duras se distinguen de
376
+ `neutral` de una manera que importa: **nunca se aprenden**, así que aprobaciones humanas
377
+ repetidas nunca pueden sedimentarlas en un auto-allow.
378
+ (Denegar por palabra del modelo se midió en vivo: un benigno `git commit -F …` calificado
379
+ `remote` produjo un auto-deny sin panel y sin concesión para reintentar.)
380
+ - `risky:neutral` → con `riskLearning` activado (tarjeta de Ajustes, desactivado por defecto),
381
+ cada aprobación humana que realmente se ejecuta (saldada mediante el evento `tools/result` del
382
+ host) cuenta hacia una clave `tool|category`; cuando el contador alcanza `riskThreshold`
383
+ (3 por defecto) **y** la huella de operación de la nueva llamada (palabra del comando + basename
384
+ del destino) coincide con una muestra confirmada, la misma operación exacta se auto-permite. Un
385
+ destino distinto nunca reutiliza esa autoridad. Con la sedimentación (`riskSediment`, activada
386
+ por defecto) las muestras confirmadas de una clave que alcanzó el umbral se vuelven **reglas
387
+ allow deterministas**: un acierto exacto permite sin más, sin otra llamada al LLM — incluso con
388
+ `llmAssist` desactivado — y las reglas sedimentadas son visibles y gestionables (terminar /
389
+ quitar) en la tarjeta de ajustes.
390
+ - Los tiempos de espera agotados (`riskTimeoutMs`, 20 s por defecto, un reintento), los fallos de
391
+ transporte y las salidas fuera de protocolo dejan el ask intacto — la puerta nunca adivina.
392
+
393
+ El estado de aprendizaje persiste en un JSON propiedad del plugin
394
+ (`$DSH_HOME/perm-gate/learning.json`, o `learningFile`), nunca en su fichero de reglas YAML. Cada
395
+ decisión se añade a `$DSH_HOME/perm-gate/events.jsonl` (o `eventsFile`) y se sirve en
396
+ `GET /api/dsh-perm-gate/events?sessionId=&since=`; la mitad de navegador la consulta y muestra la
397
+ última decisión como franja de aviso sobre la entrada de conversación (los ask siguen visibles
398
+ hasta el siguiente evento) y enumera las decisiones de toda la sesión, de lo más reciente, en la
399
+ pestaña **Approvals** de la vista de conversación.
400
+
401
+ Los ficheros afectados por cada decisión se copian en instantáneas antes de que el cambio aterrice
402
+ (≤ 5 ficheros, ≤ 256 KB cada uno) bajo `$DSH_HOME/perm-gate/snapshots/`; en la pestaña
403
+ **Approvals**, cada chip de fichero abre un diff de líneas (`GET /api/dsh-perm-gate/diff`) con una
404
+ acción de **revert** que entrega una instrucción de restauración en la conversación
405
+ (`POST /api/dsh-perm-gate/revert`). Una barra de inventario de instantáneas las borra por sesión o
406
+ por completo (`GET /api/dsh-perm-gate/snapshots-stats` / `POST /api/dsh-perm-gate/snapshots-clear`).
407
+
408
+ Un `ask` que la puerta enruta a un humano se rastrea hasta que el humano responde: un observador
409
+ pasivo `approval/request` registra el desenlace cerrado (`allowed-once` → **aprobado**,
410
+ `rejected` → **rechazado**, `cancelled` → **cancelado**, `unavailable` → una denegación, pues no
411
+ existía canal de aprobación), con `tools/result` saldando el mismo ask como respaldo cuando el
412
+ observador no puede correlacionarlo. Las aprobaciones informan del progreso de aprendizaje
413
+ posterior (`n`/umbral), y la franja de aviso etiqueta los tres estados terminales.
414
+
415
+ 自动审查 y 自动审查(高权限) dibujan ambas el glifo escudo+ojo en el selector — la primera del
416
+ mapa integrado de DSH, la segunda del parche del host que describe la guía de instalación. Sin
417
+ ese parche, el segundo nivel es solo texto en todas las superficies; su etiqueta y su control no
418
+ se ven afectados.
419
+
420
+ ### Un nivel de sesión seleccionable
421
+
422
+ `cordis.patch.yml` añade un preajuste `permissive` (`sandbox: workspace-write`, `approval: ask`,
423
+ nombre **自动审查**) entre Escritura de espacio de trabajo y Acceso completo. El parche del bundle
424
+ de DSH reemplaza todo el mapa `permission.config.presets` en lugar de fusionar por clave, así que
425
+ el fichero también redeclara los tres integrados (`read-only` / `workspace-write` /
426
+ `danger-full-access`, de `@deepseek-ai/dsh-base/cordis.patch.yml`);
427
+ `test/patch-presets.spec.ts` fija ese conjunto de claves. Así, el selector de permisos de sesión
428
+ ofrece 自动审查 como un nivel de aprobación independiente y seleccionable, no un modo genérico de
429
+ «autoaprobación».
430
+
431
+ La puerta está activa **solo en los niveles listados en `gatePresets`** (por defecto
432
+ `['permissive', 'permissive-full']`, los dos niveles que añade este plugin). En cualquier otro
433
+ nivel — Solo lectura, Escritura de espacio de trabajo, Acceso completo, `custom` — el flujo de
434
+ decisión de la puerta no se ejecuta en absoluto: ni allow, ni ask, ni deny, ni denegación dura
435
+ P0, ni veto de palabras clave deny, ni evento de auditoría. La política propia del nivel elegido
436
+ gobierna la llamada, y ese es el punto: el `danger-full-access` integrado se define como «acceso
437
+ completo sin avisos de aprobación», así que rebasarlo con un ask (allí irresponsable — el seam de
438
+ aprobación rechaza antes de que corra cualquier respondedor, produciendo
439
+ `the user rejected tool "..."` sin panel) o con una denegación dura contradeciría en silencio el
440
+ nivel que el usuario eligió. `gatePresets: ['*']` vuelve a hacer global la puerta (denegación dura
441
+ incluida); dentro de un nivel activo, un `ask` sigue degradándose a paso transparente cuando la
442
+ política de aprobación efectiva de la sesión es `never` — por eso ambos niveles 自动审查 declaran
443
+ `approval: ask`.
444
+
445
+ ### Configurable en la interfaz
446
+
447
+ El nivel también se ajusta en tiempo de ejecución desde **Ajustes → Plugins → 自动审查** (una
448
+ página `settings.plugins.tab` renderizada por el cliente de navegador del plugin): un interruptor
449
+ conmuta `permissive`, y cuatro conmutadores editan las `permissiveStrategies` de backend. El host
450
+ lee el espacio de nombres en vivo, de modo que un cambio se aplica en la siguiente llamada a
451
+ herramienta sin reiniciar. Esta es una clase de aprobación independiente, NO un modo genérico de
452
+ «autoaprobación».
453
+
454
+ ### Prueba de reglas (dry-run)
455
+
456
+ La misma página lleva un panel de **prueba de reglas**: escriba un nombre de herramienta y un
457
+ comando, pulse Probar, y la puerta juzga esa llamada contra el conjunto de reglas que tiene
458
+ cargado en ese momento — sin ejecutar nada y sin escribir regla alguna. Obtiene el veredicto, la
459
+ regla emparejada (índice y acción), las dimensiones que esa regla restringe, y la razón.
460
+
461
+ Informa tanto del veredicto efectivo (toda la cadena P0 → P1 → P2 → P3 → P4) *como* de la
462
+ respuesta propia de la capa de reglas, que no son lo mismo: una denegación dura P0 o una palabra
463
+ clave deny de preajuste saltan antes de la cadena de reglas y no dejan tras de sí índice de regla
464
+ alguno, de modo que el panel dice «ninguna regla coincidió» en vez de nombrar una regla ajena. Una
465
+ nota de «0 reglas cargadas» significa que la ruta `rulesFile` no resolvió nada.
466
+
467
+ El panel habla con `POST /api/dsh-perm-gate/dry-run`, que es **de solo lectura por construcción**
468
+ — no tiene forma de escritura alguna, así que probar una regla nunca podrá cambiarla.
469
+
470
+ ## CLI
471
+
472
+ Dry-run de una llamada contra un fichero de reglas (sin harness):
473
+
474
+ ```sh
475
+ dsh-perm-gate --rules permissions.yaml --tool bash --args '{"command":"pnpm install"}'
476
+ dsh-perm-gate --rules permissions.yaml --list
477
+ ```
478
+
479
+ ## Desarrollo
480
+
481
+ ```sh
482
+ npm run typecheck
483
+ npm test
484
+ npm run build
485
+ ```
486
+
487
+ ## Licencia
488
+
489
+ [MIT](./LICENSE)