dsh-perm-gate 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/INSTALL.ja.md +6 -6
- package/INSTALL.ko.md +4 -4
- package/INSTALL.md +7 -7
- package/INSTALL.zh.md +5 -5
- package/README.ja.md +2 -3
- package/README.ko.md +2 -3
- package/README.md +2 -3
- package/README.zh.md +2 -3
- package/dsh.plugin.json +2 -2
- package/lib/client.js +162 -89
- package/lib/client.js.map +1 -1
- package/lib/events.d.ts +35 -0
- package/lib/events.js +128 -0
- package/lib/index.js +6 -1
- package/package.json +10 -9
- package/docs/plan-rules-to-settings.md +0 -217
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.0.0] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **This branch is the DSH 0.1.5 line: version series `3.x`, `engines.dsh`
|
|
13
|
+
`>=0.1.5-rc.1 <0.2.0-0`, npm dist-tag `dsh-0.1.5`.** It is a **packaging delta only** —
|
|
14
|
+
the code is `main`'s, aligned to `main@42dabae`; the major bump is the fence, so a `^2.x`
|
|
15
|
+
install never resolves `3.x` and vice versa.
|
|
16
|
+
- `dsh.plugin.json` now carries the same version and range as `package.json` (it still said
|
|
17
|
+
`2.6.0` / `>=0.1.2-alpha.1`), and the README version reference follows.
|
|
18
|
+
- **`3.x` is not on npm yet.** No `3.x` version and no `dsh-0.1.5` dist-tag have been
|
|
19
|
+
published, so `dsh-perm-gate@dsh-0.1.5` does not resolve; until the release is cut, a
|
|
20
|
+
`0.1.5-rc.x` host installs this branch directly
|
|
21
|
+
(`github:drscrewdriver/dsh-perm-gate#sync/0.1.5-from-main`) or takes `@latest`.
|
|
22
|
+
|
|
8
23
|
## [2.6.0] - 2026-09-17
|
|
9
24
|
|
|
10
25
|
### Added
|
package/INSTALL.ja.md
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` バージョン **
|
|
15
|
+
`dsh-perm-gate` バージョン **3.0.0**。判定チェーン、ルールファイル形式、自动审查
|
|
16
16
|
ティアについては [日本語 README](./README.ja.md) を参照してください。
|
|
17
17
|
|
|
18
18
|
## 必要条件
|
|
19
19
|
|
|
20
20
|
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) がインストール済みであること。
|
|
21
|
-
- Node.js **>=
|
|
21
|
+
- Node.js **>= 24**(`package.json` の `engines` を参照)。
|
|
22
22
|
- `dsh` CLI が `PATH` に存在すること。
|
|
23
23
|
- インストール先の profile。ブラウザ側は `web` profile 向けにのみビルドされます
|
|
24
24
|
(`package.json` の `dsh.client.platform = "web"`)。
|
|
@@ -73,10 +73,10 @@ profile の `cordis.yml` にプラグインを追加します:
|
|
|
73
73
|
defaultAction: ask # allow | ask | deny
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
ルール本体は `dsh-perm-gate-rules` settings 名前空間に保存され、変更のたびにゲートが
|
|
77
|
+
その場で再コンパイルされます。上記の YAML ファイルは任意です。名前空間が未設定の間
|
|
78
|
+
だけ読み込まれ、プラグインが最初にルールを書き込む際(許可リストの承認、または設定
|
|
79
|
+
UI での編集)にファイルのルールが自動的に取り込まれます。
|
|
80
80
|
|
|
81
81
|
まずは [examples/permissions.example.yaml](./examples/permissions.example.yaml)
|
|
82
82
|
から始め、profile を再読込してください。
|
package/INSTALL.ko.md
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` 버전 **
|
|
15
|
+
`dsh-perm-gate` 버전 **3.0.0**. 결정 체인, 규칙 파일 형식, 自动审查 티어는
|
|
16
16
|
[한국어 README](./README.ko.md)를 참고하세요.
|
|
17
17
|
|
|
18
18
|
## 요구 사항
|
|
19
19
|
|
|
20
20
|
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)가 설치되어 있어야 합니다.
|
|
21
|
-
- Node.js **>=
|
|
21
|
+
- Node.js **>= 24** (`package.json`의 `engines` 참고).
|
|
22
22
|
- `dsh` CLI가 `PATH`에 있어야 합니다.
|
|
23
23
|
- 설치할 profile. 브라우저 절반은 `web` profile용으로만 빌드됩니다
|
|
24
24
|
(`package.json`의 `dsh.client.platform = "web"`).
|
|
@@ -74,8 +74,8 @@ profile의 `cordis.yml`에 플러그인을 추가합니다:
|
|
|
74
74
|
defaultAction: ask # allow | ask | deny
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
규칙 본체는 `dsh-perm-gate-rules` settings 네임스페이스에 저장되며, 변경 시마다
|
|
78
|
+
게이트가 그 자리에서 다시 컴파일됩니다. 위의 YAML 파일은 선택 사항입니다.
|
|
79
79
|
네임스페이스가 설정되지 않은 동안에만 읽히며, 플러그인이 처음으로 규칙을 쓸 때(허용
|
|
80
80
|
목록 승인 또는 설정 UI에서의 편집) 파일의 규칙이 자동으로 흡수됩니다.
|
|
81
81
|
|
package/INSTALL.md
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` version **
|
|
15
|
+
`dsh-perm-gate` version **3.0.0**. Continue with the [README](./README.md) for the
|
|
16
16
|
decision chain, the rules file format and the 自动审查 tier.
|
|
17
17
|
|
|
18
18
|
## Requirements
|
|
19
19
|
|
|
20
20
|
- An existing [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) installation.
|
|
21
|
-
- Node.js **>=
|
|
21
|
+
- Node.js **>= 24** (see `engines` in `package.json`).
|
|
22
22
|
- The `dsh` CLI on your `PATH`.
|
|
23
23
|
- A profile to install into — the browser half only ships for the `web` profile
|
|
24
24
|
(`dsh.client.platform = "web"` in `package.json`).
|
|
@@ -75,11 +75,11 @@ Add the plugin to your profile's `cordis.yml`:
|
|
|
75
75
|
gatePresets: [permissive] # tiers where the gate is active at all (default)
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
Rules are stored in the `dsh-perm-gate-rules` settings namespace, and every
|
|
79
|
+
change recompiles the gate in place. The YAML file above is optional: it is only
|
|
80
|
+
read while the namespace is untouched, and the first rules write from the plugin
|
|
81
|
+
(an allowlist approval, or an edit in the settings UI) absorbs the file's rules
|
|
82
|
+
automatically.
|
|
83
83
|
|
|
84
84
|
Start from [examples/permissions.example.yaml](./examples/permissions.example.yaml),
|
|
85
85
|
then reload the profile.
|
package/INSTALL.zh.md
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` 版本 **
|
|
15
|
+
`dsh-perm-gate` 版本 **3.0.0**。裁决链、规则文件格式与自动审查档位请见
|
|
16
16
|
[中文 README](./README.zh.md)。
|
|
17
17
|
|
|
18
18
|
## 前置条件
|
|
19
19
|
|
|
20
20
|
- 已安装 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)。
|
|
21
|
-
- Node.js **>=
|
|
21
|
+
- Node.js **>= 24**(见 `package.json` 的 `engines`)。
|
|
22
22
|
- `dsh` CLI 已在 `PATH` 中。
|
|
23
23
|
- 需要一个目标 profile —— 浏览器端只为 `web` profile 构建(`package.json`
|
|
24
24
|
里 `dsh.client.platform = "web"`)。
|
|
@@ -71,9 +71,9 @@ dsh plugin --profile web add .
|
|
|
71
71
|
defaultAction: ask # allow | ask | deny
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
规则本体存储在 `dsh-perm-gate-rules` settings 命名空间中,每次变更都会就地重编译
|
|
75
|
+
闸门。上面的 YAML 文件是可选的:只有命名空间未被配置时才会读取它,插件发起的首次
|
|
76
|
+
规则写入(白名单批准或设置界面编辑)会自动收编文件中的规则。
|
|
77
77
|
|
|
78
78
|
可从 [examples/permissions.example.yaml](./examples/permissions.example.yaml) 起步,
|
|
79
79
|
然后重载 profile。
|
package/README.ja.md
CHANGED
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
> | DSH バージョン | ブランチ | バージョン | npm タグ |
|
|
28
28
|
> | --- | --- | --- | --- |
|
|
29
29
|
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
|
|
30
|
-
> | 0.1.2-alpha.1
|
|
31
|
-
> | 0.1.5-rc.1+ | `sync/0.1.5-from-main`(=`compat/0.1.5`) | `3.x` | GitHub ref インストール(dist-tag 未公開) |
|
|
30
|
+
> | 0.1.2-alpha.1+(0.1.5-rc.2を含む) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x` は範囲) |
|
|
32
31
|
>
|
|
33
32
|
> バージョン系列は **DSH ライン**を追います(`1.x` = DSH ≤ 0.1.1、`2.x` = DSH 0.1.2+)。
|
|
34
33
|
> メジャー同士が互いを遮断するため、`^1.x` のインストールが `2.x` を解決することは
|
|
@@ -46,7 +45,7 @@
|
|
|
46
45
|
> メソッドであるため、`typeof` プロブで読み取り、欠落時やエラー時には「ポリシー
|
|
47
46
|
> 不明」にフォールバックします。
|
|
48
47
|
|
|
49
|
-
バージョン **2.
|
|
48
|
+
バージョン **2.4.1** — 変更履歴は [日本語 changelog](./CHANGELOG.ja.md) を参照。
|
|
50
49
|
|
|
51
50
|
DeepSeek Harness 向けの、単一・自己完結・決定論優先・fail-closed な権限ゲートです。
|
|
52
51
|
|
package/README.ko.md
CHANGED
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
> | DSH 버전 | 브랜치 | 버전 | npm 태그 |
|
|
28
28
|
> | --- | --- | --- | --- |
|
|
29
29
|
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
|
|
30
|
-
> | 0.1.2-alpha.1
|
|
31
|
-
> | 0.1.5-rc.1+ | `sync/0.1.5-from-main` (=`compat/0.1.5`) | `3.x` | GitHub ref 설치 (dist-tag 미게시) |
|
|
30
|
+
> | 0.1.2-alpha.1+ (0.1.5-rc.2 포함) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x`는 범위) |
|
|
32
31
|
>
|
|
33
32
|
> 버전 계열은 **DSH 라인**을 따릅니다(`1.x` = DSH ≤ 0.1.1, `2.x` = DSH 0.1.2+). 메이저
|
|
34
33
|
> 버전이 서로를 차단하므로 `^1.x` 설치가 `2.x`를 해석하는 일은 없고 그 반대도
|
|
@@ -44,7 +43,7 @@
|
|
|
44
43
|
> 모두에서 user-approval 서비스의 **개인** 메소드이므로 `typeof` 프로브를 통해
|
|
45
44
|
> 읽으며, 누락되거나 예외 발생 시 "정책_unknown"으로 후퇴합니다.
|
|
46
45
|
|
|
47
|
-
버전 **2.
|
|
46
|
+
버전 **2.4.1** — 변경 내역은 [한국어 changelog](./CHANGELOG.ko.md)를 참고하세요.
|
|
48
47
|
|
|
49
48
|
DeepSeek Harness용 단일·자족적·결정론 우선·fail-closed 권한 게이트입니다.
|
|
50
49
|
|
package/README.md
CHANGED
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
> | DSH version | Branch | Version | npm tag |
|
|
27
27
|
> | --- | --- | --- | --- |
|
|
28
28
|
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
|
|
29
|
-
> | 0.1.2-alpha.1
|
|
30
|
-
> | 0.1.5-rc.1+ | `sync/0.1.5-from-main` (=`compat/0.1.5`) | `3.x` | GitHub ref install (no dist-tag yet) |
|
|
29
|
+
> | 0.1.2-alpha.1+ (incl. 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x` is a range) |
|
|
31
30
|
>
|
|
32
31
|
> The series number tracks the **DSH line** (`1.x` = DSH ≤ 0.1.1, `2.x` = DSH 0.1.2+),
|
|
33
32
|
> and the majors fence each other: a `^1.x` install never resolves a `2.x` release and
|
|
@@ -43,7 +42,7 @@
|
|
|
43
42
|
> **private** method of the user-approval service on both, so it is read behind a
|
|
44
43
|
> `typeof` probe and degrades to “policy unknown” when absent or throwing.
|
|
45
44
|
|
|
46
|
-
Version **
|
|
45
|
+
Version **3.0.0** — see the [Changelog](./CHANGELOG.md).
|
|
47
46
|
|
|
48
47
|
A single, self-sufficient, deterministic-first, fail-closed permission gate for DeepSeek Harness.
|
|
49
48
|
|
package/README.zh.md
CHANGED
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
> | DSH 版本 | 分支 | 版本号 | npm 标签 |
|
|
26
26
|
> | --- | --- | --- | --- |
|
|
27
27
|
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
|
|
28
|
-
> | 0.1.2-alpha.1
|
|
29
|
-
> | 0.1.5-rc.1+ | `sync/0.1.5-from-main`(=`compat/0.1.5`) | `3.x` | GitHub ref 安装(暂无 dist-tag) |
|
|
28
|
+
> | 0.1.2-alpha.1+(含 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x` 是范围) |
|
|
30
29
|
>
|
|
31
30
|
> 版本序列号跟的是 **DSH 线**(`1.x` = DSH ≤ 0.1.1,`2.x` = DSH 0.1.2+),两条大版本互
|
|
32
31
|
> 相隔离:锁在 `^1.x` 的安装绝不会解析到 `2.x`,反之亦然。`engines.dsh` 表达同样的
|
|
@@ -40,7 +39,7 @@
|
|
|
40
39
|
> 线上都是 user-approval 服务的**私有**方法,因此通过 `typeof` 探测读取,缺失
|
|
41
40
|
> 或抛错时降级为「策略未知」。
|
|
42
41
|
|
|
43
|
-
版本 **2.
|
|
42
|
+
版本 **2.4.1** —— 变更见 [Changelog](./CHANGELOG.md)。
|
|
44
43
|
|
|
45
44
|
一个**单一自足、确定性优先、fail-closed** 的 DeepSeek Harness 权限门插件。
|
|
46
45
|
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-perm-gate",
|
|
3
3
|
"name": "dsh-perm-gate",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Deterministic-first, fail-closed permission gate for DeepSeek Harness 0.1.2+: a P0 hard-deny -> P1 precise session grant -> P2 static allow/deny rule chain -> P3 optional LLM classifier -> P4 ask pipeline over the tools/pre-execute waterfall, with a standalone CLI dry-run evaluator and an auditable decision log. It only vetoes with deny/ask and always delegates otherwise, and unknown or malformed rules fail loud instead of silently standing the gate down.",
|
|
7
7
|
"zh": "面向 DeepSeek Harness 0.1.2+ 的确定性优先、失败即拒的权限闸门:在 tools/pre-execute 瀑布上串起 P0 硬拒绝 → P1 精确会话授权 → P2 静态 allow/deny 规则链 → P3 可选 LLM 语义分类 → P4 ask,并附带独立 CLI 干跑评估器与可审计的决策日志。只以 deny/ask 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"engines": {
|
|
16
|
-
"dsh": ">=0.1.
|
|
16
|
+
"dsh": ">=0.1.5-rc.1 <0.2.0-0"
|
|
17
17
|
},
|
|
18
18
|
"components": {
|
|
19
19
|
"host": "lib/index.js",
|
package/lib/client.js
CHANGED
|
@@ -89,8 +89,6 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
"card.llmPresetHost": "DSH 模型组(选择后自动切换为会话模式)",
|
|
90
90
|
"card.networkEnabled": "网络拦截(networkEnabled)",
|
|
91
91
|
"card.networkEnabledHint": "开启后启动本地代理拦截子进程出站连接,按规则放行/阻断。关闭则零行为变更。",
|
|
92
|
-
"card.watch": "规则热重载(watch)",
|
|
93
|
-
"card.watchHint": "开启后自动监听规则文件变更并即时生效,无需手动 reload。",
|
|
94
92
|
"card.networkRebindNote": "切换后立即生效(自动重绑,无需重载插件)。",
|
|
95
93
|
"card.networkInjectEnv": "注入代理环境变量(networkInjectEnv)",
|
|
96
94
|
"card.networkInjectEnvHint": "开启后为子进程写入 HTTP(S)_PROXY / ALL_PROXY(并清空 NO_PROXY)。关闭则只启动监听、不改变任何进程环境。",
|
|
@@ -241,8 +239,6 @@ window.__ModuleLoader__.load({
|
|
|
241
239
|
"card.llmPresetHost": "DSH model groups (selecting one switches to the session receiver)",
|
|
242
240
|
"card.networkEnabled": "Network interception (networkEnabled)",
|
|
243
241
|
"card.networkEnabledHint": "When on, starts a local proxy that intercepts outbound subprocess connections and applies allow/deny rules. Off = zero behavior change.",
|
|
244
|
-
"card.watch": "Rule hot-reload (watch)",
|
|
245
|
-
"card.watchHint": "When on, automatically watches rule files for changes and applies them instantly without manual reload.",
|
|
246
242
|
"card.networkRebindNote": "Takes effect immediately — the proxy rebinds automatically, no plugin reload needed.",
|
|
247
243
|
"card.networkInjectEnv": "Inject proxy environment (networkInjectEnv)",
|
|
248
244
|
"card.networkInjectEnvHint": "Writes HTTP(S)_PROXY / ALL_PROXY for subprocesses (and clears NO_PROXY). Off = the listener runs but no process environment is changed.",
|
|
@@ -405,7 +401,6 @@ window.__ModuleLoader__.load({
|
|
|
405
401
|
* learning.json store the gate reads); actions POST back. Best-effort: a
|
|
406
402
|
* missing route renders the empty state, never an error.
|
|
407
403
|
*/
|
|
408
|
-
const POLL_MS$2 = 5e3;
|
|
409
404
|
const itemStyle = {
|
|
410
405
|
display: "flex",
|
|
411
406
|
alignItems: "center",
|
|
@@ -435,6 +430,8 @@ window.__ModuleLoader__.load({
|
|
|
435
430
|
};
|
|
436
431
|
function SedimentSection({ t }) {
|
|
437
432
|
const [body, setBody] = (0, react.useState)(null);
|
|
433
|
+
/** Latest loader, so a mutation can refresh the list without owning the fetch. */
|
|
434
|
+
const reloadRef = (0, react.useRef)(() => {});
|
|
438
435
|
(0, react.useEffect)(() => {
|
|
439
436
|
let alive = true;
|
|
440
437
|
const load = () => {
|
|
@@ -444,11 +441,18 @@ window.__ModuleLoader__.load({
|
|
|
444
441
|
if (alive) setBody({});
|
|
445
442
|
});
|
|
446
443
|
};
|
|
444
|
+
reloadRef.current = load;
|
|
447
445
|
load();
|
|
448
|
-
const
|
|
446
|
+
const onVisible = () => {
|
|
447
|
+
if (document.visibilityState === "visible") load();
|
|
448
|
+
};
|
|
449
|
+
window.addEventListener("focus", onVisible);
|
|
450
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
449
451
|
return () => {
|
|
450
452
|
alive = false;
|
|
451
|
-
|
|
453
|
+
reloadRef.current = () => {};
|
|
454
|
+
window.removeEventListener("focus", onVisible);
|
|
455
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
452
456
|
};
|
|
453
457
|
}, []);
|
|
454
458
|
const post = (payload) => {
|
|
@@ -456,6 +460,8 @@ window.__ModuleLoader__.load({
|
|
|
456
460
|
method: "POST",
|
|
457
461
|
headers: { "content-type": "application/json" },
|
|
458
462
|
body: JSON.stringify(payload)
|
|
463
|
+
}).then(() => {
|
|
464
|
+
reloadRef.current();
|
|
459
465
|
}).catch(() => {});
|
|
460
466
|
};
|
|
461
467
|
const threshold = body?.threshold ?? 3;
|
|
@@ -1923,7 +1929,7 @@ window.__ModuleLoader__.load({
|
|
|
1923
1929
|
})
|
|
1924
1930
|
]
|
|
1925
1931
|
}),
|
|
1926
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1932
|
+
value.networkEnabled === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1927
1933
|
style: sectionStyle,
|
|
1928
1934
|
children: [
|
|
1929
1935
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1954,39 +1960,7 @@ window.__ModuleLoader__.load({
|
|
|
1954
1960
|
})]
|
|
1955
1961
|
})
|
|
1956
1962
|
]
|
|
1957
|
-
}),
|
|
1958
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1959
|
-
style: sectionStyle,
|
|
1960
|
-
children: [
|
|
1961
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1962
|
-
style: labelStyle,
|
|
1963
|
-
children: t("card.watch")
|
|
1964
|
-
}),
|
|
1965
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1966
|
-
style: hintStyle,
|
|
1967
|
-
children: t("card.watchHint")
|
|
1968
|
-
}),
|
|
1969
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1970
|
-
style: {
|
|
1971
|
-
display: "flex",
|
|
1972
|
-
alignItems: "center",
|
|
1973
|
-
gap: "8px",
|
|
1974
|
-
marginTop: "6px"
|
|
1975
|
-
},
|
|
1976
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1977
|
-
type: "checkbox",
|
|
1978
|
-
checked: value.watch !== false,
|
|
1979
|
-
disabled: readonly,
|
|
1980
|
-
onChange: (event) => {
|
|
1981
|
-
scope.set("watch", event.currentTarget.checked);
|
|
1982
|
-
}
|
|
1983
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1984
|
-
style: { fontSize: "13px" },
|
|
1985
|
-
children: value.watch !== false ? "ON" : "OFF"
|
|
1986
|
-
})]
|
|
1987
|
-
})
|
|
1988
|
-
]
|
|
1989
|
-
}),
|
|
1963
|
+
}) : null,
|
|
1990
1964
|
!snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1991
1965
|
style: {
|
|
1992
1966
|
margin: "8px 0 0",
|
|
@@ -2009,6 +1983,95 @@ window.__ModuleLoader__.load({
|
|
|
2009
1983
|
const body = await res.json();
|
|
2010
1984
|
return Array.isArray(body.events) ? body.events : [];
|
|
2011
1985
|
}
|
|
1986
|
+
/** The host's push route (see src/events.ts); the browser half prefers it to polling. */
|
|
1987
|
+
const STREAM_ROUTE = "/api/dsh-perm-gate/stream";
|
|
1988
|
+
/** Stream attempts before the route is written off as unsupported and polling takes over. */
|
|
1989
|
+
const STREAM_RETRIES = 3;
|
|
1990
|
+
const STREAM_RETRY_MS = 3e3;
|
|
1991
|
+
/**
|
|
1992
|
+
* Degraded path only: a host predating the stream route still gets a servable
|
|
1993
|
+
* strip. Deliberately slower than the old 2 s tick — it now also pauses while
|
|
1994
|
+
* the tab is hidden, so a backgrounded page costs nothing.
|
|
1995
|
+
*/
|
|
1996
|
+
const FALLBACK_POLL_MS = 5e3;
|
|
1997
|
+
/**
|
|
1998
|
+
* Follow one session's decision feed until the returned close is called.
|
|
1999
|
+
*
|
|
2000
|
+
* Prefers the host's `text/event-stream` route, which pushes each decision as
|
|
2001
|
+
* the gate appends it — no timer, no repeated re-read of the event log. When
|
|
2002
|
+
* the stream is unavailable (an older host, a proxy that buffers it) this
|
|
2003
|
+
* degrades to polling the JSON route, gated on page visibility.
|
|
2004
|
+
*
|
|
2005
|
+
* @param sessionId - the session whose decisions to follow.
|
|
2006
|
+
* @param getSince - cursor supplier, read at connect and on every fallback poll.
|
|
2007
|
+
* @param onBatch - receives the backlog, then live batches.
|
|
2008
|
+
* @returns the close: ends the stream and any fallback timer.
|
|
2009
|
+
*/
|
|
2010
|
+
function followEvents(sessionId, getSince, onBatch) {
|
|
2011
|
+
let closed = false;
|
|
2012
|
+
let source = null;
|
|
2013
|
+
let pollTimer = null;
|
|
2014
|
+
let retries = 0;
|
|
2015
|
+
let polls = 0;
|
|
2016
|
+
const stopPolling = () => {
|
|
2017
|
+
if (pollTimer === null) return;
|
|
2018
|
+
clearInterval(pollTimer);
|
|
2019
|
+
pollTimer = null;
|
|
2020
|
+
};
|
|
2021
|
+
const poll = () => {
|
|
2022
|
+
if (closed) return;
|
|
2023
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
2024
|
+
const backlog = polls === 0;
|
|
2025
|
+
polls += 1;
|
|
2026
|
+
fetchEvents(sessionId, getSince()).then((events) => {
|
|
2027
|
+
if (!closed) onBatch({
|
|
2028
|
+
events,
|
|
2029
|
+
backlog
|
|
2030
|
+
});
|
|
2031
|
+
}).catch(() => {});
|
|
2032
|
+
};
|
|
2033
|
+
const startPolling = () => {
|
|
2034
|
+
if (closed || pollTimer !== null) return;
|
|
2035
|
+
polls = 0;
|
|
2036
|
+
poll();
|
|
2037
|
+
pollTimer = setInterval(poll, FALLBACK_POLL_MS);
|
|
2038
|
+
};
|
|
2039
|
+
const connect = () => {
|
|
2040
|
+
if (closed) return;
|
|
2041
|
+
const query = `?sessionId=${encodeURIComponent(sessionId)}&since=${getSince()}`;
|
|
2042
|
+
const next = new EventSource(`${STREAM_ROUTE}${query}`);
|
|
2043
|
+
source = next;
|
|
2044
|
+
next.onopen = () => {
|
|
2045
|
+
retries = 0;
|
|
2046
|
+
stopPolling();
|
|
2047
|
+
};
|
|
2048
|
+
next.onmessage = (message) => {
|
|
2049
|
+
try {
|
|
2050
|
+
const body = JSON.parse(message.data);
|
|
2051
|
+
if (Array.isArray(body.events)) onBatch({
|
|
2052
|
+
events: body.events,
|
|
2053
|
+
backlog: body.backlog === true
|
|
2054
|
+
});
|
|
2055
|
+
} catch {}
|
|
2056
|
+
};
|
|
2057
|
+
next.onerror = () => {
|
|
2058
|
+
if (source === next) source = null;
|
|
2059
|
+
next.close();
|
|
2060
|
+
if (closed) return;
|
|
2061
|
+
retries += 1;
|
|
2062
|
+
startPolling();
|
|
2063
|
+
if (retries <= STREAM_RETRIES) setTimeout(connect, STREAM_RETRY_MS);
|
|
2064
|
+
};
|
|
2065
|
+
};
|
|
2066
|
+
if (typeof EventSource === "undefined") startPolling();
|
|
2067
|
+
else connect();
|
|
2068
|
+
return () => {
|
|
2069
|
+
closed = true;
|
|
2070
|
+
source?.close();
|
|
2071
|
+
source = null;
|
|
2072
|
+
stopPolling();
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2012
2075
|
/** Resolve the current session id from slot props (top level, nested, or hook). */
|
|
2013
2076
|
function resolveSessionId(props) {
|
|
2014
2077
|
const direct = props?.sessionId;
|
|
@@ -2195,15 +2258,15 @@ window.__ModuleLoader__.load({
|
|
|
2195
2258
|
* dsh-perm-gate (a simplified version of the review-feed pattern demonstrated
|
|
2196
2259
|
* by dsh-approval-gate).
|
|
2197
2260
|
*
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2261
|
+
* Follows the host's decision feed (`GET /api/dsh-perm-gate/stream`, an SSE
|
|
2262
|
+
* push; see `followEvents` for the older-host polling fallback) and shows the
|
|
2263
|
+
* latest decision above the conversation input: auto-allows (green) and denies
|
|
2264
|
+
* (red) auto-dismiss after a few seconds; an ask (amber) stays until the next
|
|
2265
|
+
* event because it needs the human's attention.
|
|
2202
2266
|
*
|
|
2203
2267
|
* Everything is best-effort: no session id, a missing route, or any fetch
|
|
2204
2268
|
* failure simply means "render nothing". No @deepseek-ai value imports.
|
|
2205
2269
|
*/
|
|
2206
|
-
const POLL_MS$1 = 2e3;
|
|
2207
2270
|
const AUTO_HIDE_MS = 4e3;
|
|
2208
2271
|
/** Localized tag for a manual terminal state; other kinds keep their short tag. */
|
|
2209
2272
|
function manualTagKey(kind) {
|
|
@@ -2224,35 +2287,27 @@ window.__ModuleLoader__.load({
|
|
|
2224
2287
|
setNotice(null);
|
|
2225
2288
|
if (sessionId === null) return;
|
|
2226
2289
|
let alive = true;
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
const last = events[events.length - 1];
|
|
2234
|
-
if (last === void 0 || last.id <= shownIdRef.current) return;
|
|
2235
|
-
shownIdRef.current = last.id;
|
|
2236
|
-
sinceRef.current = last.id;
|
|
2237
|
-
setNotice(last);
|
|
2238
|
-
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
2239
|
-
if (!presentation(last.kind).sticky) hideTimerRef.current = setTimeout(() => {
|
|
2240
|
-
setNotice(null);
|
|
2241
|
-
}, AUTO_HIDE_MS);
|
|
2242
|
-
}).catch(() => {});
|
|
2243
|
-
}, POLL_MS$1);
|
|
2244
|
-
};
|
|
2245
|
-
fetchEvents(sessionId, 0).then((events) => {
|
|
2246
|
-
if (!alive) return;
|
|
2247
|
-
if (events.length > 0) {
|
|
2248
|
-
sinceRef.current = events[events.length - 1]?.id ?? 0;
|
|
2290
|
+
const onBatch = (batch) => {
|
|
2291
|
+
if (!alive || batch.events.length === 0) return;
|
|
2292
|
+
const last = batch.events[batch.events.length - 1];
|
|
2293
|
+
if (last === void 0) return;
|
|
2294
|
+
if (last.id > sinceRef.current) sinceRef.current = last.id;
|
|
2295
|
+
if (batch.backlog) {
|
|
2249
2296
|
shownIdRef.current = sinceRef.current;
|
|
2297
|
+
return;
|
|
2250
2298
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2299
|
+
if (last.id <= shownIdRef.current) return;
|
|
2300
|
+
shownIdRef.current = last.id;
|
|
2301
|
+
setNotice(last);
|
|
2302
|
+
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
2303
|
+
if (!presentation(last.kind).sticky) hideTimerRef.current = setTimeout(() => {
|
|
2304
|
+
setNotice(null);
|
|
2305
|
+
}, AUTO_HIDE_MS);
|
|
2306
|
+
};
|
|
2307
|
+
const close = followEvents(sessionId, () => sinceRef.current, onBatch);
|
|
2253
2308
|
return () => {
|
|
2254
2309
|
alive = false;
|
|
2255
|
-
|
|
2310
|
+
close();
|
|
2256
2311
|
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
2257
2312
|
};
|
|
2258
2313
|
}, [sessionId]);
|
|
@@ -2335,7 +2390,8 @@ window.__ModuleLoader__.load({
|
|
|
2335
2390
|
* Approval-history view — the `conversation.view` face of dsh-perm-gate,
|
|
2336
2391
|
* following the session-scoped review-page pattern demonstrated by
|
|
2337
2392
|
* dsh-approval-gate: a timeline of every gate decision in the current
|
|
2338
|
-
* conversation, newest first,
|
|
2393
|
+
* conversation, newest first, loaded once and then extended by the host's push
|
|
2394
|
+
* feed (see `followEvents`).
|
|
2339
2395
|
*
|
|
2340
2396
|
* Beyond the decision timeline this view owns the review data plane: a snapshot
|
|
2341
2397
|
* inventory bar (with session/all clearing), per-event file chips, and a diff
|
|
@@ -2347,7 +2403,6 @@ window.__ModuleLoader__.load({
|
|
|
2347
2403
|
* routes. Everything is best-effort: no session id or any fetch failure renders
|
|
2348
2404
|
* the empty / error state. No @deepseek-ai value imports.
|
|
2349
2405
|
*/
|
|
2350
|
-
const POLL_MS = 5e3;
|
|
2351
2406
|
/** Tag text per event kind, keyed into the plugin dictionary. */
|
|
2352
2407
|
const TAG_KEYS = {
|
|
2353
2408
|
auto: "history.tag.auto",
|
|
@@ -2761,29 +2816,47 @@ window.__ModuleLoader__.load({
|
|
|
2761
2816
|
setEvents(null);
|
|
2762
2817
|
setError(null);
|
|
2763
2818
|
setStats(null);
|
|
2819
|
+
if (sessionId === null) {
|
|
2820
|
+
setEvents([]);
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2764
2823
|
let alive = true;
|
|
2765
|
-
|
|
2824
|
+
let cursor = 0;
|
|
2825
|
+
let closeStream = null;
|
|
2826
|
+
fetchSnapshotStats(sessionId).then((next) => {
|
|
2827
|
+
if (alive) setStats(next);
|
|
2828
|
+
});
|
|
2829
|
+
/**
|
|
2830
|
+
* Decisions landing after the initial load. Both the stream's pushes and the
|
|
2831
|
+
* fallback's ticks carry only events past the cursor, so they merge the same
|
|
2832
|
+
* way; a batch is also the point where the snapshot inventory can have grown.
|
|
2833
|
+
*/
|
|
2834
|
+
const onBatch = (batch) => {
|
|
2835
|
+
if (!alive || batch.events.length === 0) return;
|
|
2836
|
+
for (const ev of batch.events) if (ev.id > cursor) cursor = ev.id;
|
|
2837
|
+
setEvents((current) => {
|
|
2838
|
+
const known = new Set((current ?? []).map((ev) => ev.id));
|
|
2839
|
+
const added = batch.events.filter((ev) => !known.has(ev.id));
|
|
2840
|
+
return added.length === 0 ? current : [...added, ...current ?? []].sort((a, b) => b.id - a.id);
|
|
2841
|
+
});
|
|
2842
|
+
setError(null);
|
|
2766
2843
|
fetchSnapshotStats(sessionId).then((next) => {
|
|
2767
2844
|
if (alive) setStats(next);
|
|
2768
2845
|
});
|
|
2769
|
-
if (sessionId === null) {
|
|
2770
|
-
setEvents([]);
|
|
2771
|
-
return;
|
|
2772
|
-
}
|
|
2773
|
-
fetchEvents(sessionId, 0).then((loaded) => {
|
|
2774
|
-
if (!alive) return;
|
|
2775
|
-
setEvents([...loaded].sort((a, b) => b.id - a.id));
|
|
2776
|
-
setError(null);
|
|
2777
|
-
}).catch((e) => {
|
|
2778
|
-
if (!alive) return;
|
|
2779
|
-
setError(String(e?.message ?? e));
|
|
2780
|
-
});
|
|
2781
2846
|
};
|
|
2782
|
-
|
|
2783
|
-
|
|
2847
|
+
fetchEvents(sessionId, 0).then((loaded) => {
|
|
2848
|
+
if (!alive) return;
|
|
2849
|
+
setEvents([...loaded].sort((a, b) => b.id - a.id));
|
|
2850
|
+
setError(null);
|
|
2851
|
+
cursor = loaded.reduce((max, ev) => ev.id > max ? ev.id : max, 0);
|
|
2852
|
+
}).catch((e) => {
|
|
2853
|
+
if (alive) setError(String(e?.message ?? e));
|
|
2854
|
+
}).finally(() => {
|
|
2855
|
+
if (alive) closeStream = followEvents(sessionId, () => cursor, onBatch);
|
|
2856
|
+
});
|
|
2784
2857
|
return () => {
|
|
2785
2858
|
alive = false;
|
|
2786
|
-
|
|
2859
|
+
closeStream?.();
|
|
2787
2860
|
};
|
|
2788
2861
|
}, [sessionId]);
|
|
2789
2862
|
/** Whether one event has a snapshot covering this file (chip is clickable). */
|