dsh-permission-rules 0.6.19 → 0.6.21
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 +12 -0
- package/README.es.md +2 -2
- package/README.hi.md +2 -2
- package/README.md +2 -2
- package/README.pt.md +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +24 -57
- package/lib/types/runtime.d.ts +18 -23
- package/lib/types/runtime.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/runtime.ts +26 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## v0.6.21 - 2026-09-10
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- The session-less host chain no longer survives an explicit reload, so a corrected rule takes effect without restarting the process. `hostLoaded` was cached for the lifetime of the mount, and that chain is deliberately neither a `byCwd` member nor watched, so neither `reloadAll()` nor `saveRuleFile()` ever reached it — and the settings page (which drives `reloadAll()`) is the one surface usable while no session has run a tool call, i.e. exactly the window in which this chain is what judges traffic. `invalidateHostChain()` now drops the cache and re-arms the one-shot warning, and is called from `reloadAll()`, `saveRuleFile()` and `onNetworkConfigChanged()` (the last because `rulesFile` and `fallbackPath` are inputs to the chain, so a config change must not reuse a chain built from the previous one). The new test asserts both the stale read and the refreshed one, and removing the invalidation makes it fail.
|
|
6
|
+
|
|
7
|
+
## v0.6.20 - 2026-09-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Corrected the mechanism by which this plugin participates in host networking (issue #22).** The five READMEs, `AGENTS.md` and the v0.6.19 note claimed that the process-wide injected proxy environment also placed the harness's own LLM transport under this policy. Measurement on 2026-09-10 (Node 22; `scripts/host-egress-probe.mjs`) shows it does not: the launcher installs undici's global dispatcher from the **launch environment** before the first plugin mounts, that dispatcher routes by its policy rather than by the environment, and Node samples the proxy environment at start — so a mid-process `process.env` write reaches neither `fetch` nor `node:http`. The injection covers spawned shell children, which is what it was always for. The READMEs' known-limitations bullet now says so, and the `llm/stream` annotation shipped in v0.6.19 is recorded as inert: it can only fire when a blocked child-process connection coincides within its window with an unrelated model failure, so it misattributes rather than diagnoses — it has been **removed in this release**, along with its registration, its block-window helper, the type import it needed, and its four tests.
|
|
12
|
+
|
|
1
13
|
## v0.6.19 - 2026-09-10
|
|
2
14
|
|
|
3
15
|
### Security
|
package/README.es.md
CHANGED
|
@@ -78,7 +78,7 @@ Una **política de red a nivel de proceso** estilo Codex: el tráfico de subproc
|
|
|
78
78
|
|
|
79
79
|
- **Emparejamiento** — `match.network` con `domains` / `ips` / `ports` / `schemes` (globs, comodines, CIDR, rangos de puertos; se aceptan puertos YAML numéricos). La extracción de candidatos URL en la ruta caliente `tools/pre-execute` se dispara sobre argumentos de herramientas web y URLs embebidas en texto de comandos bash/pwsh; los destinos de loopback pueden cortocircuitar reglas según la política `loopback`. Los literales IPv6 mapeados a IPv4 se normalizan a su forma IPv4 antes de comparar, y el proxy se conecta a las direcciones con las que se tomó la decisión, sin una segunda resolución DNS.
|
|
80
80
|
- **Auditoría** — las conexiones denegadas anexan `permissionRules/network` a la sesión propietaria (la misma puerta adaptativa `ignorable`), con contadores de bloqueo e intercepciones recientes en `/rules network` y la página de settings.
|
|
81
|
-
- **Diagnóstico** — las conexiones bloqueadas llevan un mensaje `[network: …]
|
|
81
|
+
- **Diagnóstico** — las conexiones bloqueadas llevan un mensaje `[network: …]` que nombra el destino bloqueado, el modo o la regla que decidió y la solución.
|
|
82
82
|
|
|
83
83
|
## Quick start
|
|
84
84
|
|
|
@@ -175,7 +175,7 @@ Todos los parámetros son campos Schemastery `Config` (modificables desde cordis
|
|
|
175
175
|
- **Los candidatos de ruta son heurísticos.** Solo las claves de argumento documentadas alimentan el emparejamiento de rutas, y el emparejamiento relativo al workspace es insensible a mayúsculas ASCII solo con `caseInsensitivePaths` activado.
|
|
176
176
|
- **Los globs son un subconjunto conservador.** Sin expansión de llaves — escribe dos patrones, o usa modo regex.
|
|
177
177
|
- **La guardia de backtracking de regex es estructural, no exhaustiva.** Prefiere el modo glob para archivos no confiables.
|
|
178
|
-
- **
|
|
178
|
+
- **Las peticiones salientes del propio host no pasan por este proxy.** El entorno de proxy inyectado cubre los **subprocesos de shell derivados** —que es para lo que sirve la inyección— y cualquier consumidor que lea los nombres de proxy en el momento de la petición. **No** cubre el tráfico basado en `fetch` del propio proceso del host: el launcher instala el dispatcher global de undici desde el **entorno de lanzamiento** antes de que se monte el primer plugin, ese dispatcher enruta por su política y no por el entorno, y Node muestrea el entorno de proxy al arrancar, así que un plugin que se monte después no puede redirigirlo. Consecuencias: los endpoints del proveedor **no** necesitan una regla allow, y `network.injectEnv: false` no saca al proceso del host de nada. Medido el 2026-09-10 en Node 22; reproducible con `scripts/host-egress-probe.mjs`.
|
|
179
179
|
|
|
180
180
|
## Collaborating with dsh-auto-review
|
|
181
181
|
|
package/README.hi.md
CHANGED
|
@@ -78,7 +78,7 @@ Codex-शैली की **प्रक्रिया-स्तरीय न
|
|
|
78
78
|
|
|
79
79
|
- **मिलान** — `match.network` `domains` / `ips` / `ports` / `schemes` के साथ (globs, वाइल्डकार्ड, CIDR, पोर्ट श्रेणियाँ; संख्यात्मक YAML पोर्ट स्वीकृत)। `tools/pre-execute` हॉट पथ पर URL-उम्मीदवार निष्कर्षण वेब-टूल तर्कों और bash/pwsh कमांड टेक्स्ट में एम्बेडेड URLs पर चलता है; लूपबैक लक्ष्य `loopback` नीति के अनुसार नियमों को शॉर्ट-सर्किट कर सकते हैं। IPv4-मैप्ड IPv6 लिटरल तुलना से पहले IPv4 रूप में सामान्यीकृत होते हैं, और प्रॉक्सी उन्हीं पतों पर कनेक्ट करता है जिन पर निर्णय लिया गया — दूसरा DNS रिज़ॉल्यूशन नहीं।
|
|
80
80
|
- **ऑडिट** — अस्वीकृत कनेक्शन स्वामी सत्र में `permissionRules/network` जोड़ते हैं (वही अनुकूली `ignorable` द्वार), `/rules network` और settings पृष्ठ में ब्लॉक काउंटर व हाल की अवरोधन के साथ।
|
|
81
|
-
- **निदान** — अवरुद्ध कनेक्शन `[network: …]` संदेश लेकर आते
|
|
81
|
+
- **निदान** — अवरुद्ध कनेक्शन `[network: …]` संदेश लेकर आते हैं जो अवरुद्ध लक्ष्य, निर्णय लेने वाला मोड या नियम, और समाधान बताता है।
|
|
82
82
|
|
|
83
83
|
## Quick start
|
|
84
84
|
|
|
@@ -175,7 +175,7 @@ dsh --profile web --dump-config | grep -A4 'id: permission-rules'
|
|
|
175
175
|
- **पथ उम्मीदवार अनुमानी हैं।** केवल दस्तावेज़ित तर्क कुंजियाँ पथ मिलान को खिलाती हैं, और कार्यक्षेत्र-सापेक्ष मिलान केवल `caseInsensitivePaths` चालू होने पर ASCII-केस-असंवेदी है।
|
|
176
176
|
- **globs एक रूढ़िवादी उपसमुच्चय हैं।** कोई ब्रेस विस्तार नहीं — दो पैटर्न लिखें, या regex मोड उपयोग करें।
|
|
177
177
|
- **regex बैकट्रैकिंग गार्ड संरचनात्मक है, संपूर्ण नहीं।** अविश्वसनीय फ़ाइलों के लिए glob मोड पसंद करें।
|
|
178
|
-
-
|
|
178
|
+
- **होस्ट के अपने आउटबाउंड अनुरोध इस प्रॉक्सी से नहीं जाते।** इंजेक्ट किया गया प्रॉक्सी वातावरण **उत्पन्न shell उप-प्रक्रियाओं** को कवर करता है — इंजेक्शन इसी के लिए है — और उन उपभोक्ताओं को जो अनुरोध के समय प्रॉक्सी नाम पढ़ते हैं। यह होस्ट प्रोसेस के अपने `fetch`-आधारित ट्रैफ़िक को **नहीं** कवर करता: launcher पहले प्लगइन के माउंट होने से पहले **लॉन्च वातावरण** से undici का ग्लोबल dispatcher स्थापित कर देता है, वह dispatcher वातावरण से नहीं बल्कि अपनी नीति से रूट करता है, और Node प्रॉक्सी वातावरण को प्रारंभ पर सैंपल करता है — इसलिए बाद में माउंट होने वाला प्लगइन उसे नहीं मोड़ सकता। परिणाम: प्रदाता endpoints के लिए allow नियम की **आवश्यकता नहीं** है, और `network.injectEnv: false` होस्ट प्रोसेस को किसी चीज़ से बाहर नहीं करता। 2026-09-10 को Node 22 पर मापा गया; `scripts/host-egress-probe.mjs` से दोहराएँ।
|
|
179
179
|
|
|
180
180
|
## Collaborating with dsh-auto-review
|
|
181
181
|
|
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ A Codex-style **process-level network policy**: shell subprocess traffic flows t
|
|
|
79
79
|
|
|
80
80
|
- **Matching** — `match.network` with `domains` / `ips` / `ports` / `schemes` (globs, wildcards, CIDRs, port ranges; numeric YAML ports are accepted). URL-candidate extraction on the `tools/pre-execute` hot path fires on web-tool arguments and URLs embedded in bash/pwsh command text; loopback targets can short-circuit rules per `loopback` policy. IPv4-mapped IPv6 literals are normalized to their IPv4 form before matching, and the proxy connects on the addresses the decision was made on — never a second DNS resolution.
|
|
81
81
|
- **Audit** — denied connections append `permissionRules/network` to the owning session (same adaptive `ignorable` gate), with block counters and recent interceptions in `/rules network` and the settings page.
|
|
82
|
-
- **Diagnosis** — blocked connections carry a `[network: …]` message
|
|
82
|
+
- **Diagnosis** — blocked connections carry a `[network: …]` message naming the blocked target, the mode or rule that decided, and the remediation.
|
|
83
83
|
|
|
84
84
|
## Quick start
|
|
85
85
|
|
|
@@ -176,7 +176,7 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). An id
|
|
|
176
176
|
- **Path candidates are heuristic.** Only the documented argument keys feed path matching, and workspace-relative matching is ASCII-case-insensitive only when `caseInsensitivePaths` is on.
|
|
177
177
|
- **Globs are a conservative subset.** No brace expansion — write two patterns, or use regex mode.
|
|
178
178
|
- **The regex backtracking guard is structural, not exhaustive.** Prefer glob mode for untrusted files.
|
|
179
|
-
- **
|
|
179
|
+
- **The host's own outbound requests do not go through this proxy.** The injected proxy environment covers **spawned shell children** — which is what the injection is for — and any consumer that reads the proxy names at request time. It does **not** cover the host process's own `fetch`-based traffic: the launcher installs undici's global dispatcher from the **launch environment** before the first plugin mounts, that dispatcher routes by its policy rather than by the environment, and Node samples the proxy environment at start — so a plugin mounting later cannot redirect it. Consequences: provider endpoints do **not** need an allow rule, and `network.injectEnv: false` does not take the host process out of anything. Measured 2026-09-10 on Node 22; reproduce with `scripts/host-egress-probe.mjs`.
|
|
180
180
|
|
|
181
181
|
## Collaborating with dsh-auto-review
|
|
182
182
|
|
package/README.pt.md
CHANGED
|
@@ -78,7 +78,7 @@ Uma **política de rede em nível de processo** estilo Codex: o tráfego de subp
|
|
|
78
78
|
|
|
79
79
|
- **Correspondência** — `match.network` com `domains` / `ips` / `ports` / `schemes` (globs, curingas, CIDR, faixas de porta; portas YAML numéricas são aceitas). A extração de candidatos URL na rota quente `tools/pre-execute` dispara sobre argumentos de ferramentas web e URLs embutidas em texto de comando bash/pwsh; destinos de loopback podem curto-circuitar regras conforme a política `loopback`. Literais IPv6 mapeados em IPv4 são normalizados para a forma IPv4 antes da comparação, e o proxy conecta nos endereços usados na decisão, sem uma segunda resolução DNS.
|
|
80
80
|
- **Auditoria** — conexões negadas anexam `permissionRules/network` à sessão proprietária (a mesma porta adaptativa `ignorable`), com contadores de bloqueio e intercepções recentes em `/rules network` e na página de settings.
|
|
81
|
-
- **Diagnóstico** — conexões bloqueadas trazem uma mensagem `[network: …]
|
|
81
|
+
- **Diagnóstico** — conexões bloqueadas trazem uma mensagem `[network: …]` nomeando o destino bloqueado, o modo ou a regra que decidiu e a solução.
|
|
82
82
|
|
|
83
83
|
## Quick start
|
|
84
84
|
|
|
@@ -175,7 +175,7 @@ Todos os parâmetros são campos Schemastery `Config` (alteráveis pelo cordis.y
|
|
|
175
175
|
- **Candidatos de caminho são heurísticos.** Somente as chaves de argumento documentadas alimentam a correspondência de caminho, e a correspondência relativa ao workspace é insensível a maiúsculas ASCII apenas com `caseInsensitivePaths` ativado.
|
|
176
176
|
- **Globs são um subconjunto conservador.** Sem expansão de chaves — escreva dois padrões, ou use o modo regex.
|
|
177
177
|
- **A guarda de backtracking de regex é estrutural, não exaustiva.** Prefira o modo glob para arquivos não confiáveis.
|
|
178
|
-
- **
|
|
178
|
+
- **As requisições de saída do próprio host não passam por este proxy.** O ambiente de proxy injetado cobre os **subprocessos de shell derivados** — que é para isso que a injeção serve — e qualquer consumidor que leia os nomes de proxy no momento da requisição. **Não** cobre o tráfego baseado em `fetch` do próprio processo do host: o launcher instala o dispatcher global do undici a partir do **ambiente de lançamento** antes de o primeiro plugin ser montado, esse dispatcher roteia pela sua política e não pelo ambiente, e o Node amostra o ambiente de proxy na inicialização, então um plugin montado depois não consegue redirecioná-lo. Consequências: os endpoints do provedor **não** precisam de uma regra allow, e `network.injectEnv: false` não tira o processo do host de nada. Medido em 2026-09-10 no Node 22; reproduza com `scripts/host-egress-probe.mjs`.
|
|
179
179
|
|
|
180
180
|
## Collaborating with dsh-auto-review
|
|
181
181
|
|
package/README.zh.md
CHANGED
|
@@ -78,7 +78,7 @@ Codex 风格的**进程级网络策略**:shell 子进程流量经内置本地
|
|
|
78
78
|
|
|
79
79
|
- **匹配** —— `match.network` 用 `domains` / `ips` / `ports` / `schemes`(glob、通配符、CIDR、端口范围;数值型 YAML 端口可接受)。`tools/pre-execute` 热路径上的 URL 候选抽取作用于 web 工具参数与嵌入 bash/pwsh 命令文本的 URL;回环目标可按 `loopback` 策略短路规则。IPv4 映射的 IPv6 字面量在匹配前归一化为 IPv4 形式;代理按裁决到的地址建连,不做二次 DNS 解析。
|
|
80
80
|
- **审计** —— 被拒连接向所属会话追加 `permissionRules/network`(同样的自适应 `ignorable` 门),块计数器与近期拦截在 `/rules network` 与设置页展示。
|
|
81
|
-
- **诊断** —— 被拦截的连接带 `[network: …]`
|
|
81
|
+
- **诊断** —— 被拦截的连接带 `[network: …]` 消息,点名被拦目标、做出裁决的模式或规则,以及处置办法。
|
|
82
82
|
|
|
83
83
|
## Quick start
|
|
84
84
|
|
|
@@ -175,7 +175,7 @@ dsh --profile web --dump-config | grep -A4 'id: permission-rules'
|
|
|
175
175
|
- **路径候选是启发式的。** 只有文档化的参数键参与路径匹配,且工作区相对匹配仅在 `caseInsensitivePaths` 开启时忽略 ASCII 大小写。
|
|
176
176
|
- **glob 是保守子集。** 无花括号展开——写两个模式,或用正则模式。
|
|
177
177
|
- **正则回溯守卫是结构性的、非穷尽的。** 对不可信文件优先用 glob 模式。
|
|
178
|
-
-
|
|
178
|
+
- **宿主自身的出站请求不经过本代理。** 注入的代理环境覆盖**派生的 shell 子进程**(这正是注入的用途),以及在请求时读取代理变量名的消费者;它**不覆盖**宿主进程自身基于 `fetch` 的流量——launcher 在第一个插件挂载之前就用**启动环境**装好了 undici 的全局 dispatcher,该 dispatcher 按策略而非按环境路由,且 Node 在启动时采样代理环境,因此后挂载的插件无法改变它。结论:供应商端点**不需要**允许规则,`network.injectEnv: false` 也不会把宿主进程移出任何东西。2026-09-10 实测于 Node 22;用 `scripts/host-egress-probe.mjs` 可复现。
|
|
179
179
|
|
|
180
180
|
## Collaborating with dsh-auto-review
|
|
181
181
|
|
package/lib/index.js
CHANGED
|
@@ -2575,13 +2575,6 @@ const WEB_TOOLS = ["web_fetch", "web_search"];
|
|
|
2575
2575
|
/** Stale in-flight entries are dropped after this many milliseconds (attribution is best-effort). */
|
|
2576
2576
|
const IN_FLIGHT_TTL_MS = 6e5;
|
|
2577
2577
|
/**
|
|
2578
|
-
* Clock slack for attributing a proxy block to a model call: a block recorded
|
|
2579
|
-
* within this window before the call started still counts, so millisecond
|
|
2580
|
-
* granularity or a listener ordering between the retry middleware and this
|
|
2581
|
-
* waterfall cannot hide the block that caused the failure.
|
|
2582
|
-
*/
|
|
2583
|
-
const LLM_BLOCK_SKEW_MS = 1e3;
|
|
2584
|
-
/**
|
|
2585
2578
|
* Chokidar polling interval used when the watched path lives on a filesystem
|
|
2586
2579
|
* whose change events are unreliable (WSL drvfs/9p mounts): polling costs a
|
|
2587
2580
|
* stat per interval, which is negligible for a rule file and far cheaper than
|
|
@@ -3121,7 +3114,10 @@ var PermissionRulesRuntime = class {
|
|
|
3121
3114
|
* connection is then judged exactly as before, against the loaded workspace
|
|
3122
3115
|
* chains). It is not watched either: the session-less window is the boot
|
|
3123
3116
|
* window, and the first tool call of any session takes over with the normal
|
|
3124
|
-
* per-workspace chain, which is watched.
|
|
3117
|
+
* per-workspace chain, which is watched. An EXPLICIT reload still drops this
|
|
3118
|
+
* cache ({@link invalidateHostChain}) — `/rules reload`, the settings-page
|
|
3119
|
+
* reload and a settings save all reach it — because during the boot window a
|
|
3120
|
+
* corrected rule otherwise had no effect short of restarting the process.
|
|
3125
3121
|
* @returns the configured chain, or undefined when it cannot be loaded (the mode default then applies).
|
|
3126
3122
|
*/
|
|
3127
3123
|
hostChain() {
|
|
@@ -3140,51 +3136,16 @@ var PermissionRulesRuntime = class {
|
|
|
3140
3136
|
};
|
|
3141
3137
|
}
|
|
3142
3138
|
/**
|
|
3143
|
-
*
|
|
3144
|
-
*
|
|
3145
|
-
*
|
|
3146
|
-
*
|
|
3147
|
-
*
|
|
3148
|
-
*
|
|
3149
|
-
* failure whose window contains a proxy block, so no turn fails silently on
|
|
3150
|
-
* a policy decision the user cannot see.
|
|
3151
|
-
* @param next - the downstream chain (the resolved adapter's stream).
|
|
3152
|
-
* @returns the same chunks, with any failure carrying the policy diagnosis.
|
|
3153
|
-
*/
|
|
3154
|
-
llmStream(next) {
|
|
3155
|
-
return this.diagnosedLlmStream(next);
|
|
3156
|
-
}
|
|
3157
|
-
/** Delegate one model call and enrich its failure with the proxy diagnosis, if any. */
|
|
3158
|
-
async *diagnosedLlmStream(next) {
|
|
3159
|
-
const startedAt = Date.now();
|
|
3160
|
-
try {
|
|
3161
|
-
yield* next();
|
|
3162
|
-
} catch (error) {
|
|
3163
|
-
const diagnosis = this.llmFailureDiagnosis(startedAt);
|
|
3164
|
-
if (diagnosis !== void 0) {
|
|
3165
|
-
this.ctx.logger.warn(`permission-rules: ${diagnosis}`);
|
|
3166
|
-
if (error instanceof Error && !error.message.includes("[network:")) error.message = `${error.message} — ${diagnosis}`;
|
|
3167
|
-
}
|
|
3168
|
-
throw error;
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
/**
|
|
3172
|
-
* The policy diagnosis for one failed model call, or undefined when the
|
|
3173
|
-
* proxy blocked nothing while it ran. The newest block in the window names
|
|
3174
|
-
* the target and the mode, and the remediation is the honest one: allow the
|
|
3175
|
-
* endpoint or stop injecting the policy environment into the host process.
|
|
3139
|
+
* Drop the cached session-less chain so the next proxy decision re-reads it,
|
|
3140
|
+
* and re-arm the one-shot warning so a file that has since become loadable
|
|
3141
|
+
* can report a failure again. Without this the cache was permanent: the
|
|
3142
|
+
* session-less chain is deliberately not a `byCwd` member and is not watched,
|
|
3143
|
+
* so neither `reloadAll()` nor a settings save ever reached it and a
|
|
3144
|
+
* corrected rule needed a process restart (issue #18 follow-up).
|
|
3176
3145
|
*/
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
const blocks = proxy.recentBlocks().filter((record) => record.time >= since - LLM_BLOCK_SKEW_MS);
|
|
3181
|
-
const newest = blocks[0];
|
|
3182
|
-
if (newest === void 0) return void 0;
|
|
3183
|
-
const target = `${newest.scheme ?? "https"}://${newest.domain}${newest.port !== void 0 ? `:${newest.port}` : ""}`;
|
|
3184
|
-
const marker = newest.action === "ask" ? "[network: blocked pending approval]" : "[network: denied]";
|
|
3185
|
-
const rule = newest.matched ? `, rule ${(newest.ruleIndex ?? 0) + 1}${newest.source !== "" ? ` (${newest.source})` : ""}` : ", no matching allow rule";
|
|
3186
|
-
const more = blocks.length > 1 ? ` (and ${blocks.length - 1} more block${blocks.length > 2 ? "s" : ""} in the same window)` : "";
|
|
3187
|
-
return `${marker} the local policy proxy blocked ${target} during this model call (mode ${newest.mode}${rule})${more} — the host process inherits the policy proxy environment (network.injectEnv), so the model transport is adjudicated like any other connection; add an allow rule for the provider endpoint or set network.injectEnv: false`;
|
|
3146
|
+
invalidateHostChain() {
|
|
3147
|
+
this.hostLoaded = void 0;
|
|
3148
|
+
this.hostChainWarned = false;
|
|
3188
3149
|
}
|
|
3189
3150
|
/** Mark one delegated shell execution as in-flight (newest attribution wins). */
|
|
3190
3151
|
markShell(exec) {
|
|
@@ -3306,6 +3267,7 @@ var PermissionRulesRuntime = class {
|
|
|
3306
3267
|
*/
|
|
3307
3268
|
async onNetworkConfigChanged() {
|
|
3308
3269
|
if (!this.config.network.enabled) return;
|
|
3270
|
+
this.invalidateHostChain();
|
|
3309
3271
|
await this.networkProxy?.close();
|
|
3310
3272
|
this.envRestore?.();
|
|
3311
3273
|
this.envRestore = void 0;
|
|
@@ -3387,8 +3349,15 @@ var PermissionRulesRuntime = class {
|
|
|
3387
3349
|
sourceOwner(path) {
|
|
3388
3350
|
for (const loaded of this.byCwd.values()) if (loaded.sources.includes(path) || !isAbsolute(this.config.rulesFile) && join(loaded.cwd, this.config.rulesFile) === path) return loaded.cwd;
|
|
3389
3351
|
}
|
|
3390
|
-
/**
|
|
3352
|
+
/**
|
|
3353
|
+
* Re-read every cached workspace chain and drop the session-less host chain
|
|
3354
|
+
* (the settings-page reload action). The host chain matters here because the
|
|
3355
|
+
* settings page is a web-client surface: it can be used while no session has
|
|
3356
|
+
* run a tool call, which is exactly the window in which the host chain is
|
|
3357
|
+
* what judges traffic.
|
|
3358
|
+
*/
|
|
3391
3359
|
reloadAll() {
|
|
3360
|
+
this.invalidateHostChain();
|
|
3392
3361
|
for (const cwd of [...this.byCwd.keys()]) this.reload(cwd);
|
|
3393
3362
|
}
|
|
3394
3363
|
/**
|
|
@@ -3425,6 +3394,7 @@ var PermissionRulesRuntime = class {
|
|
|
3425
3394
|
};
|
|
3426
3395
|
}
|
|
3427
3396
|
let reloaded = 0;
|
|
3397
|
+
this.invalidateHostChain();
|
|
3428
3398
|
for (const cwd of [...this.byCwd.keys()]) {
|
|
3429
3399
|
this.reload(cwd);
|
|
3430
3400
|
reloaded += 1;
|
|
@@ -3969,10 +3939,7 @@ async function apply(ctx, config) {
|
|
|
3969
3939
|
return next();
|
|
3970
3940
|
});
|
|
3971
3941
|
attachSettingsSection(ctx, runtime, config);
|
|
3972
|
-
if (resolved.network.enabled)
|
|
3973
|
-
await runtime.attachNetworkProxy();
|
|
3974
|
-
ctx.on("llm/stream", (_options, next) => runtime.llmStream(next));
|
|
3975
|
-
}
|
|
3942
|
+
if (resolved.network.enabled) await runtime.attachNetworkProxy();
|
|
3976
3943
|
ctx.inject(["systemPrompt"], (scope) => {
|
|
3977
3944
|
const systemPrompt = scope.get("systemPrompt");
|
|
3978
3945
|
if (systemPrompt?.context === void 0) return;
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Context } from '@deepseek-ai/cordis';
|
|
14
14
|
import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands';
|
|
15
|
-
import type { StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
16
15
|
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
17
16
|
import type { Config, ResolvedConfig } from './config.js';
|
|
18
17
|
import type { CompiledRuleset, RuleHit } from './rules.js';
|
|
@@ -261,32 +260,22 @@ export declare class PermissionRulesRuntime {
|
|
|
261
260
|
* connection is then judged exactly as before, against the loaded workspace
|
|
262
261
|
* chains). It is not watched either: the session-less window is the boot
|
|
263
262
|
* window, and the first tool call of any session takes over with the normal
|
|
264
|
-
* per-workspace chain, which is watched.
|
|
263
|
+
* per-workspace chain, which is watched. An EXPLICIT reload still drops this
|
|
264
|
+
* cache ({@link invalidateHostChain}) — `/rules reload`, the settings-page
|
|
265
|
+
* reload and a settings save all reach it — because during the boot window a
|
|
266
|
+
* corrected rule otherwise had no effect short of restarting the process.
|
|
265
267
|
* @returns the configured chain, or undefined when it cannot be loaded (the mode default then applies).
|
|
266
268
|
*/
|
|
267
269
|
private hostChain;
|
|
268
270
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* failure whose window contains a proxy block, so no turn fails silently on
|
|
276
|
-
* a policy decision the user cannot see.
|
|
277
|
-
* @param next - the downstream chain (the resolved adapter's stream).
|
|
278
|
-
* @returns the same chunks, with any failure carrying the policy diagnosis.
|
|
271
|
+
* Drop the cached session-less chain so the next proxy decision re-reads it,
|
|
272
|
+
* and re-arm the one-shot warning so a file that has since become loadable
|
|
273
|
+
* can report a failure again. Without this the cache was permanent: the
|
|
274
|
+
* session-less chain is deliberately not a `byCwd` member and is not watched,
|
|
275
|
+
* so neither `reloadAll()` nor a settings save ever reached it and a
|
|
276
|
+
* corrected rule needed a process restart (issue #18 follow-up).
|
|
279
277
|
*/
|
|
280
|
-
|
|
281
|
-
/** Delegate one model call and enrich its failure with the proxy diagnosis, if any. */
|
|
282
|
-
private diagnosedLlmStream;
|
|
283
|
-
/**
|
|
284
|
-
* The policy diagnosis for one failed model call, or undefined when the
|
|
285
|
-
* proxy blocked nothing while it ran. The newest block in the window names
|
|
286
|
-
* the target and the mode, and the remediation is the honest one: allow the
|
|
287
|
-
* endpoint or stop injecting the policy environment into the host process.
|
|
288
|
-
*/
|
|
289
|
-
private llmFailureDiagnosis;
|
|
278
|
+
private invalidateHostChain;
|
|
290
279
|
/** Mark one delegated shell execution as in-flight (newest attribution wins). */
|
|
291
280
|
private markShell;
|
|
292
281
|
/** Drop one shell execution when it settles. */
|
|
@@ -347,7 +336,13 @@ export declare class PermissionRulesRuntime {
|
|
|
347
336
|
};
|
|
348
337
|
/** The workspace a rule source belongs to (for the editor's file list), or undefined for the fallback. */
|
|
349
338
|
sourceOwner(path: string): string | undefined;
|
|
350
|
-
/**
|
|
339
|
+
/**
|
|
340
|
+
* Re-read every cached workspace chain and drop the session-less host chain
|
|
341
|
+
* (the settings-page reload action). The host chain matters here because the
|
|
342
|
+
* settings page is a web-client surface: it can be used while no session has
|
|
343
|
+
* run a tool call, which is exactly the window in which the host chain is
|
|
344
|
+
* what judges traffic.
|
|
345
|
+
*/
|
|
351
346
|
reloadAll(): void;
|
|
352
347
|
/**
|
|
353
348
|
* Validate and write one known rule file. The document must parse and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACjF,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-permission-rules",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.21",
|
|
4
4
|
"description": "Declarative Claude Code-style permission rules plus a Codex-style process-level network policy for DeepSeek Harness: ordered allow/deny/ask rules with tool-name, argument (glob/regex), workspace-path, and network-target (domain/ip/port/scheme) matching on the tools/pre-execute waterfall, a built-in local HTTP/CONNECT proxy governing shell subprocess traffic, three network modes mapped onto the official sandbox presets, full session-log audit, HMR rule reload, and a settings page with a rule editor, block counters, and recent interception lists.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
package/src/runtime.ts
CHANGED
|
@@ -18,7 +18,6 @@ import { dirname, isAbsolute, join, resolve, sep } from 'node:path'
|
|
|
18
18
|
import chokidar from 'chokidar'
|
|
19
19
|
import type { Context } from '@deepseek-ai/cordis'
|
|
20
20
|
import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'
|
|
21
|
-
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
|
22
21
|
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
|
|
23
22
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
24
23
|
import { resolveConfig } from './config.ts'
|
|
@@ -82,14 +81,6 @@ const WEB_TOOLS: readonly string[] = ['web_fetch', 'web_search']
|
|
|
82
81
|
/** Stale in-flight entries are dropped after this many milliseconds (attribution is best-effort). */
|
|
83
82
|
const IN_FLIGHT_TTL_MS = 10 * 60 * 1000
|
|
84
83
|
|
|
85
|
-
/**
|
|
86
|
-
* Clock slack for attributing a proxy block to a model call: a block recorded
|
|
87
|
-
* within this window before the call started still counts, so millisecond
|
|
88
|
-
* granularity or a listener ordering between the retry middleware and this
|
|
89
|
-
* waterfall cannot hide the block that caused the failure.
|
|
90
|
-
*/
|
|
91
|
-
const LLM_BLOCK_SKEW_MS = 1000
|
|
92
|
-
|
|
93
84
|
/**
|
|
94
85
|
* Chokidar polling interval used when the watched path lives on a filesystem
|
|
95
86
|
* whose change events are unreliable (WSL drvfs/9p mounts): polling costs a
|
|
@@ -613,7 +604,10 @@ export class PermissionRulesRuntime {
|
|
|
613
604
|
* connection is then judged exactly as before, against the loaded workspace
|
|
614
605
|
* chains). It is not watched either: the session-less window is the boot
|
|
615
606
|
* window, and the first tool call of any session takes over with the normal
|
|
616
|
-
* per-workspace chain, which is watched.
|
|
607
|
+
* per-workspace chain, which is watched. An EXPLICIT reload still drops this
|
|
608
|
+
* cache ({@link invalidateHostChain}) — `/rules reload`, the settings-page
|
|
609
|
+
* reload and a settings save all reach it — because during the boot window a
|
|
610
|
+
* corrected rule otherwise had no effect short of restarting the process.
|
|
617
611
|
* @returns the configured chain, or undefined when it cannot be loaded (the mode default then applies).
|
|
618
612
|
*/
|
|
619
613
|
private hostChain(): NetworkChain | undefined {
|
|
@@ -634,57 +628,16 @@ export class PermissionRulesRuntime {
|
|
|
634
628
|
}
|
|
635
629
|
|
|
636
630
|
/**
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* failure whose window contains a proxy block, so no turn fails silently on
|
|
644
|
-
* a policy decision the user cannot see.
|
|
645
|
-
* @param next - the downstream chain (the resolved adapter's stream).
|
|
646
|
-
* @returns the same chunks, with any failure carrying the policy diagnosis.
|
|
631
|
+
* Drop the cached session-less chain so the next proxy decision re-reads it,
|
|
632
|
+
* and re-arm the one-shot warning so a file that has since become loadable
|
|
633
|
+
* can report a failure again. Without this the cache was permanent: the
|
|
634
|
+
* session-less chain is deliberately not a `byCwd` member and is not watched,
|
|
635
|
+
* so neither `reloadAll()` nor a settings save ever reached it and a
|
|
636
|
+
* corrected rule needed a process restart (issue #18 follow-up).
|
|
647
637
|
*/
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
/** Delegate one model call and enrich its failure with the proxy diagnosis, if any. */
|
|
653
|
-
private async *diagnosedLlmStream(next: () => AsyncIterable<StreamChunk>): AsyncGenerator<StreamChunk> {
|
|
654
|
-
const startedAt = Date.now()
|
|
655
|
-
try {
|
|
656
|
-
yield* next()
|
|
657
|
-
} catch (error: unknown) {
|
|
658
|
-
const diagnosis = this.llmFailureDiagnosis(startedAt)
|
|
659
|
-
if (diagnosis !== undefined) {
|
|
660
|
-
this.ctx.logger.warn(`permission-rules: ${diagnosis}`)
|
|
661
|
-
// Keep the original error object (its class, code, and retry facts)
|
|
662
|
-
// and only extend the message, so the failure taxonomy is unchanged.
|
|
663
|
-
if (error instanceof Error && !error.message.includes('[network:')) error.message = `${error.message} — ${diagnosis}`
|
|
664
|
-
}
|
|
665
|
-
throw error
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* The policy diagnosis for one failed model call, or undefined when the
|
|
671
|
-
* proxy blocked nothing while it ran. The newest block in the window names
|
|
672
|
-
* the target and the mode, and the remediation is the honest one: allow the
|
|
673
|
-
* endpoint or stop injecting the policy environment into the host process.
|
|
674
|
-
*/
|
|
675
|
-
private llmFailureDiagnosis(since: number): string | undefined {
|
|
676
|
-
const proxy = this.networkProxy
|
|
677
|
-
if (proxy === undefined) return undefined
|
|
678
|
-
const blocks = proxy.recentBlocks().filter(record => record.time >= since - LLM_BLOCK_SKEW_MS)
|
|
679
|
-
const newest = blocks[0]
|
|
680
|
-
if (newest === undefined) return undefined
|
|
681
|
-
const target = `${newest.scheme ?? 'https'}://${newest.domain}${newest.port !== undefined ? `:${newest.port}` : ''}`
|
|
682
|
-
const marker = newest.action === 'ask' ? '[network: blocked pending approval]' : '[network: denied]'
|
|
683
|
-
const rule = newest.matched
|
|
684
|
-
? `, rule ${(newest.ruleIndex ?? 0) + 1}${newest.source !== '' ? ` (${newest.source})` : ''}`
|
|
685
|
-
: ', no matching allow rule'
|
|
686
|
-
const more = blocks.length > 1 ? ` (and ${blocks.length - 1} more block${blocks.length > 2 ? 's' : ''} in the same window)` : ''
|
|
687
|
-
return `${marker} the local policy proxy blocked ${target} during this model call (mode ${newest.mode}${rule})${more} — the host process inherits the policy proxy environment (network.injectEnv), so the model transport is adjudicated like any other connection; add an allow rule for the provider endpoint or set network.injectEnv: false`
|
|
638
|
+
private invalidateHostChain(): void {
|
|
639
|
+
this.hostLoaded = undefined
|
|
640
|
+
this.hostChainWarned = false
|
|
688
641
|
}
|
|
689
642
|
|
|
690
643
|
/** Mark one delegated shell execution as in-flight (newest attribution wins). */
|
|
@@ -807,6 +760,9 @@ export class PermissionRulesRuntime {
|
|
|
807
760
|
*/
|
|
808
761
|
async onNetworkConfigChanged(): Promise<void> {
|
|
809
762
|
if (!this.config.network.enabled) return
|
|
763
|
+
// The chain inputs (rulesFile, fallbackPath, …) may have changed with the
|
|
764
|
+
// config, so the session-less chain must be re-read rather than reused.
|
|
765
|
+
this.invalidateHostChain()
|
|
810
766
|
await this.networkProxy?.close()
|
|
811
767
|
this.envRestore?.()
|
|
812
768
|
this.envRestore = undefined
|
|
@@ -887,8 +843,15 @@ export class PermissionRulesRuntime {
|
|
|
887
843
|
return undefined
|
|
888
844
|
}
|
|
889
845
|
|
|
890
|
-
/**
|
|
846
|
+
/**
|
|
847
|
+
* Re-read every cached workspace chain and drop the session-less host chain
|
|
848
|
+
* (the settings-page reload action). The host chain matters here because the
|
|
849
|
+
* settings page is a web-client surface: it can be used while no session has
|
|
850
|
+
* run a tool call, which is exactly the window in which the host chain is
|
|
851
|
+
* what judges traffic.
|
|
852
|
+
*/
|
|
891
853
|
reloadAll(): void {
|
|
854
|
+
this.invalidateHostChain()
|
|
892
855
|
for (const cwd of [...this.byCwd.keys()]) this.reload(cwd)
|
|
893
856
|
}
|
|
894
857
|
|
|
@@ -923,6 +886,7 @@ export class PermissionRulesRuntime {
|
|
|
923
886
|
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
924
887
|
}
|
|
925
888
|
let reloaded = 0
|
|
889
|
+
this.invalidateHostChain()
|
|
926
890
|
for (const cwd of [...this.byCwd.keys()]) {
|
|
927
891
|
this.reload(cwd)
|
|
928
892
|
reloaded += 1
|
|
@@ -1455,14 +1419,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
1455
1419
|
return next()
|
|
1456
1420
|
})
|
|
1457
1421
|
attachSettingsSection(ctx, runtime, config)
|
|
1458
|
-
if (resolved.network.enabled)
|
|
1459
|
-
await runtime.attachNetworkProxy()
|
|
1460
|
-
// Issue #22: the injected proxy environment is process-wide, so the
|
|
1461
|
-
// harness's own model transport is adjudicated here too. Keep a model-call
|
|
1462
|
-
// failure attributable to the policy instead of leaving it a bare
|
|
1463
|
-
// transport error.
|
|
1464
|
-
ctx.on('llm/stream', (_options: GenerateOptions, next: () => AsyncIterable<StreamChunk>) => runtime.llmStream(next))
|
|
1465
|
-
}
|
|
1422
|
+
if (resolved.network.enabled) await runtime.attachNetworkProxy()
|
|
1466
1423
|
ctx.inject(['systemPrompt'], (scope) => {
|
|
1467
1424
|
const systemPrompt = scope.get('systemPrompt') as { context?: (entry: { name: string; order?: number; text: string }) => void } | undefined
|
|
1468
1425
|
if (systemPrompt?.context === undefined) return
|