dsh-perm-gate 5.0.2 → 5.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +2 -0
- package/CHANGELOG.ko.md +2 -0
- package/CHANGELOG.md +2 -0
- package/INSTALL.ja.md +15 -0
- package/INSTALL.ko.md +15 -0
- package/INSTALL.md +15 -0
- package/INSTALL.zh.md +15 -0
- package/README.de.md +487 -0
- package/README.es.md +489 -0
- package/README.fr.md +498 -0
- package/README.it.md +490 -0
- package/README.ja.md +18 -1
- package/README.ko.md +18 -1
- package/README.md +18 -1
- package/README.ru.md +478 -0
- package/README.zh.md +18 -1
- package/lib/client.js +788 -5
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
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)
|