dsh-perm-gate 3.0.0 → 4.1.0-beta.1
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/INSTALL.ja.md +8 -14
- package/INSTALL.ko.md +9 -15
- package/INSTALL.md +8 -16
- package/INSTALL.zh.md +7 -12
- package/README.ja.md +2 -3
- package/README.ko.md +2 -3
- package/README.md +1 -2
- package/README.zh.md +2 -3
- package/lib/client.js +160 -51
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +3 -0
- package/lib/config.js +37 -1
- package/lib/events.d.ts +35 -0
- package/lib/events.js +128 -0
- package/lib/index.js +44 -86
- package/package.json +10 -9
- package/docs/plan-rules-to-settings.md +0 -217
package/INSTALL.ja.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` バージョン **2.
|
|
15
|
+
`dsh-perm-gate` バージョン **2.4.1**。判定チェーン、ルールファイル形式、自动审查
|
|
16
16
|
ティアについては [日本語 README](./README.ja.md) を参照してください。
|
|
17
17
|
|
|
18
18
|
## 必要条件
|
|
@@ -23,20 +23,19 @@
|
|
|
23
23
|
- インストール先の profile。ブラウザ側は `web` profile 向けにのみビルドされます
|
|
24
24
|
(`package.json` の `dsh.client.platform = "web"`)。
|
|
25
25
|
|
|
26
|
-
## DSH
|
|
26
|
+
## DSH のバージョンに合う tag を選ぶ
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
単一ビルドが DSH 両ラインをカバーするため、どちらの tag を選んでも動作するコードが
|
|
29
|
+
インストールされます —— tag を残しておくのは、あなたがピン留めするバージョンが
|
|
30
|
+
各ラインで意味を持つようにするためです。
|
|
31
31
|
|
|
32
32
|
| DSH のバージョン | インストール方法 |
|
|
33
33
|
|-----------------|-----------------|
|
|
34
|
-
| `0.1.2-alpha.1`
|
|
35
|
-
| `0.1.5-rc.1` 以上 | `dsh plugin --profile web add github:drscrewdriver/dsh-perm-gate#sync/0.1.5-from-main`(3.x ライン;npm の dist-tag は未公開 —— `@latest` は同じコードの 2.x パッケージングをインストールします) |
|
|
34
|
+
| `0.1.2-alpha.1` 以上(`0.1.5-rc.2` を含む) | `dsh plugin --profile web add dsh-perm-gate`(tag `latest`) |
|
|
36
35
|
| `0.1.1-rc.2` まで | `dsh plugin --profile web add dsh-perm-gate@legacy` |
|
|
37
36
|
|
|
38
|
-
DSH は `engines.dsh` を強制しないため、tag
|
|
39
|
-
|
|
37
|
+
DSH は `engines.dsh` を強制しないため、tag が選択メカニズムであり、互換性ゲート
|
|
38
|
+
そのものではありません。単一ビルドが両ラインをカバーする理由と tag の公開方法については、
|
|
40
39
|
[RELEASING.md](./RELEASING.md) を参照してください。
|
|
41
40
|
|
|
42
41
|
## 公式 CLI でインストール
|
|
@@ -73,11 +72,6 @@ profile の `cordis.yml` にプラグインを追加します:
|
|
|
73
72
|
defaultAction: ask # allow | ask | deny
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
2.6.0 から、ルール本体は `dsh-perm-gate-rules` settings 名前空間に保存され、変更の
|
|
77
|
-
たびにゲートがその場で再コンパイルされます。上記の YAML ファイルは任意になりました。
|
|
78
|
-
名前空間が未設定の間だけ読み込まれ、プラグインが最初にルールを書き込む際(許可リスト
|
|
79
|
-
の承認、または設定 UI での編集)にファイルのルールが自動的に取り込まれます。
|
|
80
|
-
|
|
81
75
|
まずは [examples/permissions.example.yaml](./examples/permissions.example.yaml)
|
|
82
76
|
から始め、profile を再読込してください。
|
|
83
77
|
|
package/INSTALL.ko.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` 버전 **2.
|
|
15
|
+
`dsh-perm-gate` 버전 **2.4.1**. 결정 체인, 규칙 파일 형식, 自动审查 티어는
|
|
16
16
|
[한국어 README](./README.ko.md)를 참고하세요.
|
|
17
17
|
|
|
18
18
|
## 요구 사항
|
|
@@ -23,21 +23,20 @@
|
|
|
23
23
|
- 설치할 profile. 브라우저 절반은 `web` profile용으로만 빌드됩니다
|
|
24
24
|
(`package.json`의 `dsh.client.platform = "web"`).
|
|
25
25
|
|
|
26
|
-
## DSH 버전에 맞는
|
|
26
|
+
## DSH 버전에 맞는 tag 선택
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
단일 빌드는 두 DSH 라인을 모두 커버하므로, 어느 tag를 골라도 작동하는 코드가
|
|
29
|
+
설치됩니다 —— tag를 유지하는 이유는 당신이 고정하는 버전이 각 라인에서 의미 있게
|
|
30
|
+
유지되도록 하기 위함입니다.
|
|
31
31
|
|
|
32
32
|
| DSH 버전 | 설치 방법 |
|
|
33
33
|
|----------|-----------|
|
|
34
|
-
| `0.1.2-alpha.1`
|
|
35
|
-
| `0.1.5-rc.1` 이상 | `dsh plugin --profile web add github:drscrewdriver/dsh-perm-gate#sync/0.1.5-from-main` (3.x 라인; npm dist-tag 미게시 —— `@latest`는 동일 코드의 2.x 패키징을 설치) |
|
|
34
|
+
| `0.1.2-alpha.1` 이상 (`0.1.5-rc.2` 포함) | `dsh plugin --profile web add dsh-perm-gate` (tag `latest`) |
|
|
36
35
|
| `0.1.1-rc.2` 이하 | `dsh plugin --profile web add dsh-perm-gate@legacy` |
|
|
37
36
|
|
|
38
|
-
DSH는 `engines.dsh`를 강제하지 않으므로, tag
|
|
39
|
-
|
|
40
|
-
참고하세요.
|
|
37
|
+
DSH는 `engines.dsh`를 강제하지 않으므로, tag는 선택 메커니즘이지 호환성 게이트가
|
|
38
|
+
아닙니다. 단일 아티팩트가 두 라인을 커버하는 이유와 tag가 게시되는 방식에 대해서는
|
|
39
|
+
[RELEASING.md](./RELEASING.md)를 참고하세요.
|
|
41
40
|
|
|
42
41
|
## 공식 CLI로 설치
|
|
43
42
|
|
|
@@ -74,11 +73,6 @@ profile의 `cordis.yml`에 플러그인을 추가합니다:
|
|
|
74
73
|
defaultAction: ask # allow | ask | deny
|
|
75
74
|
```
|
|
76
75
|
|
|
77
|
-
2.6.0부터 규칙 본체는 `dsh-perm-gate-rules` settings 네임스페이스에 저장되며, 변경 시
|
|
78
|
-
마다 게이트가 그 자리에서 다시 컴파일됩니다. 위의 YAML 파일은 선택 사항이 되었습니다.
|
|
79
|
-
네임스페이스가 설정되지 않은 동안에만 읽히며, 플러그인이 처음으로 규칙을 쓸 때(허용
|
|
80
|
-
목록 승인 또는 설정 UI에서의 편집) 파일의 규칙이 자동으로 흡수됩니다.
|
|
81
|
-
|
|
82
76
|
[examples/permissions.example.yaml](./examples/permissions.example.yaml)에서 시작한 뒤
|
|
83
77
|
profile을 다시 불러오세요.
|
|
84
78
|
|
package/INSTALL.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` version **2.
|
|
15
|
+
`dsh-perm-gate` version **2.4.1**. Continue with the [README](./README.md) for the
|
|
16
16
|
decision chain, the rules file format and the 自动审查 tier.
|
|
17
17
|
|
|
18
18
|
## Requirements
|
|
@@ -23,21 +23,19 @@ decision chain, the rules file format and the 自动审查 tier.
|
|
|
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`).
|
|
25
25
|
|
|
26
|
-
## Pick the
|
|
26
|
+
## Pick the tag for your DSH version
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
points at the same tree).
|
|
28
|
+
One build serves both DSH lines, so either tag installs working code — the tag
|
|
29
|
+
exists so a version you pin stays meaningful per line.
|
|
31
30
|
|
|
32
31
|
| Your DSH | Install |
|
|
33
32
|
|----------|---------|
|
|
34
|
-
| `0.1.2-alpha.1`
|
|
35
|
-
| `0.1.5-rc.1` or newer | `dsh plugin --profile web add github:drscrewdriver/dsh-perm-gate#sync/0.1.5-from-main` (the 3.x line; no npm dist-tag yet — `@latest` installs the same code with 2.x packaging) |
|
|
33
|
+
| `0.1.2-alpha.1` or newer (incl. `0.1.5-rc.2`) | `dsh plugin --profile web add dsh-perm-gate` (tag `latest`) |
|
|
36
34
|
| up to `0.1.1-rc.2` | `dsh plugin --profile web add dsh-perm-gate@legacy` |
|
|
37
35
|
|
|
38
|
-
DSH does not enforce `engines.dsh`, so tags
|
|
39
|
-
|
|
40
|
-
lines and tags are
|
|
36
|
+
DSH does not enforce `engines.dsh`, so the tags are the selection mechanism rather
|
|
37
|
+
than a compatibility gate. See [RELEASING.md](./RELEASING.md) for why one artifact
|
|
38
|
+
covers both lines and how the tags are published.
|
|
41
39
|
|
|
42
40
|
## Install with the official CLI
|
|
43
41
|
|
|
@@ -75,12 +73,6 @@ Add the plugin to your profile's `cordis.yml`:
|
|
|
75
73
|
gatePresets: [permissive] # tiers where the gate is active at all (default)
|
|
76
74
|
```
|
|
77
75
|
|
|
78
|
-
Since 2.6.0 the rules themselves are stored in the `dsh-perm-gate-rules` settings
|
|
79
|
-
namespace, and every change recompiles the gate in place. The YAML file above is
|
|
80
|
-
optional: it is only read while the namespace is untouched, and the first rules
|
|
81
|
-
write from the plugin (an allowlist approval, or an edit in the settings UI)
|
|
82
|
-
absorbs the file's rules automatically.
|
|
83
|
-
|
|
84
76
|
Start from [examples/permissions.example.yaml](./examples/permissions.example.yaml),
|
|
85
77
|
then reload the profile.
|
|
86
78
|
|
package/INSTALL.zh.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
`dsh-perm-gate` 版本 **2.
|
|
15
|
+
`dsh-perm-gate` 版本 **2.4.1**。裁决链、规则文件格式与自动审查档位请见
|
|
16
16
|
[中文 README](./README.zh.md)。
|
|
17
17
|
|
|
18
18
|
## 前置条件
|
|
@@ -23,19 +23,18 @@
|
|
|
23
23
|
- 需要一个目标 profile —— 浏览器端只为 `web` profile 构建(`package.json`
|
|
24
24
|
里 `dsh.client.platform = "web"`)。
|
|
25
25
|
|
|
26
|
-
## 按你的 DSH
|
|
26
|
+
## 按你的 DSH 版本选择 tag
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
单一构建同时覆盖两条 DSH 线,所以任一 tag 安装的代码都能工作 —— 保留 tag
|
|
29
|
+
是为了让你锁定的版本在每条线上都有意义。
|
|
30
30
|
|
|
31
31
|
| 你的 DSH | 安装方式 |
|
|
32
32
|
|----------|----------|
|
|
33
|
-
| `0.1.2-alpha.1`
|
|
34
|
-
| `0.1.5-rc.1` 或更高 | `dsh plugin --profile web add github:drscrewdriver/dsh-perm-gate#sync/0.1.5-from-main`(3.x 线;npm 尚无对应 dist-tag —— `@latest` 安装的是相同代码的 2.x 打包) |
|
|
33
|
+
| `0.1.2-alpha.1` 或更高(含 `0.1.5-rc.2`) | `dsh plugin --profile web add dsh-perm-gate`(tag `latest`) |
|
|
35
34
|
| 不高于 `0.1.1-rc.2` | `dsh plugin --profile web add dsh-perm-gate@legacy` |
|
|
36
35
|
|
|
37
|
-
DSH 不强制 `engines.dsh`,因此 tag
|
|
38
|
-
|
|
36
|
+
DSH 不强制 `engines.dsh`,因此 tag 是选择机制而非兼容性关卡。
|
|
37
|
+
详见 [RELEASING.md](./RELEASING.md) 了解为何单一制品覆盖两条线以及 tag 的发布方式。
|
|
39
38
|
|
|
40
39
|
## 用官方 CLI 安装
|
|
41
40
|
|
|
@@ -71,10 +70,6 @@ dsh plugin --profile web add .
|
|
|
71
70
|
defaultAction: ask # allow | ask | deny
|
|
72
71
|
```
|
|
73
72
|
|
|
74
|
-
自 2.6.0 起,规则本体存储在 `dsh-perm-gate-rules` settings 命名空间中,每次变更都会
|
|
75
|
-
就地重编译闸门。上面的 YAML 文件变为可选:只有命名空间未被配置时才会读取它,插件
|
|
76
|
-
发起的首次规则写入(白名单批准或设置界面编辑)会自动收编文件中的规则。
|
|
77
|
-
|
|
78
73
|
可从 [examples/permissions.example.yaml](./examples/permissions.example.yaml) 起步,
|
|
79
74
|
然后重载 profile。
|
|
80
75
|
|
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
|
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/lib/client.js
CHANGED
|
@@ -405,7 +405,6 @@ window.__ModuleLoader__.load({
|
|
|
405
405
|
* learning.json store the gate reads); actions POST back. Best-effort: a
|
|
406
406
|
* missing route renders the empty state, never an error.
|
|
407
407
|
*/
|
|
408
|
-
const POLL_MS$2 = 5e3;
|
|
409
408
|
const itemStyle = {
|
|
410
409
|
display: "flex",
|
|
411
410
|
alignItems: "center",
|
|
@@ -435,6 +434,8 @@ window.__ModuleLoader__.load({
|
|
|
435
434
|
};
|
|
436
435
|
function SedimentSection({ t }) {
|
|
437
436
|
const [body, setBody] = (0, react.useState)(null);
|
|
437
|
+
/** Latest loader, so a mutation can refresh the list without owning the fetch. */
|
|
438
|
+
const reloadRef = (0, react.useRef)(() => {});
|
|
438
439
|
(0, react.useEffect)(() => {
|
|
439
440
|
let alive = true;
|
|
440
441
|
const load = () => {
|
|
@@ -444,11 +445,18 @@ window.__ModuleLoader__.load({
|
|
|
444
445
|
if (alive) setBody({});
|
|
445
446
|
});
|
|
446
447
|
};
|
|
448
|
+
reloadRef.current = load;
|
|
447
449
|
load();
|
|
448
|
-
const
|
|
450
|
+
const onVisible = () => {
|
|
451
|
+
if (document.visibilityState === "visible") load();
|
|
452
|
+
};
|
|
453
|
+
window.addEventListener("focus", onVisible);
|
|
454
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
449
455
|
return () => {
|
|
450
456
|
alive = false;
|
|
451
|
-
|
|
457
|
+
reloadRef.current = () => {};
|
|
458
|
+
window.removeEventListener("focus", onVisible);
|
|
459
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
452
460
|
};
|
|
453
461
|
}, []);
|
|
454
462
|
const post = (payload) => {
|
|
@@ -456,6 +464,8 @@ window.__ModuleLoader__.load({
|
|
|
456
464
|
method: "POST",
|
|
457
465
|
headers: { "content-type": "application/json" },
|
|
458
466
|
body: JSON.stringify(payload)
|
|
467
|
+
}).then(() => {
|
|
468
|
+
reloadRef.current();
|
|
459
469
|
}).catch(() => {});
|
|
460
470
|
};
|
|
461
471
|
const threshold = body?.threshold ?? 3;
|
|
@@ -2009,6 +2019,95 @@ window.__ModuleLoader__.load({
|
|
|
2009
2019
|
const body = await res.json();
|
|
2010
2020
|
return Array.isArray(body.events) ? body.events : [];
|
|
2011
2021
|
}
|
|
2022
|
+
/** The host's push route (see src/events.ts); the browser half prefers it to polling. */
|
|
2023
|
+
const STREAM_ROUTE = "/api/dsh-perm-gate/stream";
|
|
2024
|
+
/** Stream attempts before the route is written off as unsupported and polling takes over. */
|
|
2025
|
+
const STREAM_RETRIES = 3;
|
|
2026
|
+
const STREAM_RETRY_MS = 3e3;
|
|
2027
|
+
/**
|
|
2028
|
+
* Degraded path only: a host predating the stream route still gets a servable
|
|
2029
|
+
* strip. Deliberately slower than the old 2 s tick — it now also pauses while
|
|
2030
|
+
* the tab is hidden, so a backgrounded page costs nothing.
|
|
2031
|
+
*/
|
|
2032
|
+
const FALLBACK_POLL_MS = 5e3;
|
|
2033
|
+
/**
|
|
2034
|
+
* Follow one session's decision feed until the returned close is called.
|
|
2035
|
+
*
|
|
2036
|
+
* Prefers the host's `text/event-stream` route, which pushes each decision as
|
|
2037
|
+
* the gate appends it — no timer, no repeated re-read of the event log. When
|
|
2038
|
+
* the stream is unavailable (an older host, a proxy that buffers it) this
|
|
2039
|
+
* degrades to polling the JSON route, gated on page visibility.
|
|
2040
|
+
*
|
|
2041
|
+
* @param sessionId - the session whose decisions to follow.
|
|
2042
|
+
* @param getSince - cursor supplier, read at connect and on every fallback poll.
|
|
2043
|
+
* @param onBatch - receives the backlog, then live batches.
|
|
2044
|
+
* @returns the close: ends the stream and any fallback timer.
|
|
2045
|
+
*/
|
|
2046
|
+
function followEvents(sessionId, getSince, onBatch) {
|
|
2047
|
+
let closed = false;
|
|
2048
|
+
let source = null;
|
|
2049
|
+
let pollTimer = null;
|
|
2050
|
+
let retries = 0;
|
|
2051
|
+
let polls = 0;
|
|
2052
|
+
const stopPolling = () => {
|
|
2053
|
+
if (pollTimer === null) return;
|
|
2054
|
+
clearInterval(pollTimer);
|
|
2055
|
+
pollTimer = null;
|
|
2056
|
+
};
|
|
2057
|
+
const poll = () => {
|
|
2058
|
+
if (closed) return;
|
|
2059
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
2060
|
+
const backlog = polls === 0;
|
|
2061
|
+
polls += 1;
|
|
2062
|
+
fetchEvents(sessionId, getSince()).then((events) => {
|
|
2063
|
+
if (!closed) onBatch({
|
|
2064
|
+
events,
|
|
2065
|
+
backlog
|
|
2066
|
+
});
|
|
2067
|
+
}).catch(() => {});
|
|
2068
|
+
};
|
|
2069
|
+
const startPolling = () => {
|
|
2070
|
+
if (closed || pollTimer !== null) return;
|
|
2071
|
+
polls = 0;
|
|
2072
|
+
poll();
|
|
2073
|
+
pollTimer = setInterval(poll, FALLBACK_POLL_MS);
|
|
2074
|
+
};
|
|
2075
|
+
const connect = () => {
|
|
2076
|
+
if (closed) return;
|
|
2077
|
+
const query = `?sessionId=${encodeURIComponent(sessionId)}&since=${getSince()}`;
|
|
2078
|
+
const next = new EventSource(`${STREAM_ROUTE}${query}`);
|
|
2079
|
+
source = next;
|
|
2080
|
+
next.onopen = () => {
|
|
2081
|
+
retries = 0;
|
|
2082
|
+
stopPolling();
|
|
2083
|
+
};
|
|
2084
|
+
next.onmessage = (message) => {
|
|
2085
|
+
try {
|
|
2086
|
+
const body = JSON.parse(message.data);
|
|
2087
|
+
if (Array.isArray(body.events)) onBatch({
|
|
2088
|
+
events: body.events,
|
|
2089
|
+
backlog: body.backlog === true
|
|
2090
|
+
});
|
|
2091
|
+
} catch {}
|
|
2092
|
+
};
|
|
2093
|
+
next.onerror = () => {
|
|
2094
|
+
if (source === next) source = null;
|
|
2095
|
+
next.close();
|
|
2096
|
+
if (closed) return;
|
|
2097
|
+
retries += 1;
|
|
2098
|
+
startPolling();
|
|
2099
|
+
if (retries <= STREAM_RETRIES) setTimeout(connect, STREAM_RETRY_MS);
|
|
2100
|
+
};
|
|
2101
|
+
};
|
|
2102
|
+
if (typeof EventSource === "undefined") startPolling();
|
|
2103
|
+
else connect();
|
|
2104
|
+
return () => {
|
|
2105
|
+
closed = true;
|
|
2106
|
+
source?.close();
|
|
2107
|
+
source = null;
|
|
2108
|
+
stopPolling();
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2012
2111
|
/** Resolve the current session id from slot props (top level, nested, or hook). */
|
|
2013
2112
|
function resolveSessionId(props) {
|
|
2014
2113
|
const direct = props?.sessionId;
|
|
@@ -2195,15 +2294,15 @@ window.__ModuleLoader__.load({
|
|
|
2195
2294
|
* dsh-perm-gate (a simplified version of the review-feed pattern demonstrated
|
|
2196
2295
|
* by dsh-approval-gate).
|
|
2197
2296
|
*
|
|
2198
|
-
*
|
|
2199
|
-
*
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2297
|
+
* Follows the host's decision feed (`GET /api/dsh-perm-gate/stream`, an SSE
|
|
2298
|
+
* push; see `followEvents` for the older-host polling fallback) and shows the
|
|
2299
|
+
* latest decision above the conversation input: auto-allows (green) and denies
|
|
2300
|
+
* (red) auto-dismiss after a few seconds; an ask (amber) stays until the next
|
|
2301
|
+
* event because it needs the human's attention.
|
|
2202
2302
|
*
|
|
2203
2303
|
* Everything is best-effort: no session id, a missing route, or any fetch
|
|
2204
2304
|
* failure simply means "render nothing". No @deepseek-ai value imports.
|
|
2205
2305
|
*/
|
|
2206
|
-
const POLL_MS$1 = 2e3;
|
|
2207
2306
|
const AUTO_HIDE_MS = 4e3;
|
|
2208
2307
|
/** Localized tag for a manual terminal state; other kinds keep their short tag. */
|
|
2209
2308
|
function manualTagKey(kind) {
|
|
@@ -2224,35 +2323,27 @@ window.__ModuleLoader__.load({
|
|
|
2224
2323
|
setNotice(null);
|
|
2225
2324
|
if (sessionId === null) return;
|
|
2226
2325
|
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;
|
|
2326
|
+
const onBatch = (batch) => {
|
|
2327
|
+
if (!alive || batch.events.length === 0) return;
|
|
2328
|
+
const last = batch.events[batch.events.length - 1];
|
|
2329
|
+
if (last === void 0) return;
|
|
2330
|
+
if (last.id > sinceRef.current) sinceRef.current = last.id;
|
|
2331
|
+
if (batch.backlog) {
|
|
2249
2332
|
shownIdRef.current = sinceRef.current;
|
|
2333
|
+
return;
|
|
2250
2334
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2335
|
+
if (last.id <= shownIdRef.current) return;
|
|
2336
|
+
shownIdRef.current = last.id;
|
|
2337
|
+
setNotice(last);
|
|
2338
|
+
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
2339
|
+
if (!presentation(last.kind).sticky) hideTimerRef.current = setTimeout(() => {
|
|
2340
|
+
setNotice(null);
|
|
2341
|
+
}, AUTO_HIDE_MS);
|
|
2342
|
+
};
|
|
2343
|
+
const close = followEvents(sessionId, () => sinceRef.current, onBatch);
|
|
2253
2344
|
return () => {
|
|
2254
2345
|
alive = false;
|
|
2255
|
-
|
|
2346
|
+
close();
|
|
2256
2347
|
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
2257
2348
|
};
|
|
2258
2349
|
}, [sessionId]);
|
|
@@ -2335,7 +2426,8 @@ window.__ModuleLoader__.load({
|
|
|
2335
2426
|
* Approval-history view — the `conversation.view` face of dsh-perm-gate,
|
|
2336
2427
|
* following the session-scoped review-page pattern demonstrated by
|
|
2337
2428
|
* dsh-approval-gate: a timeline of every gate decision in the current
|
|
2338
|
-
* conversation, newest first,
|
|
2429
|
+
* conversation, newest first, loaded once and then extended by the host's push
|
|
2430
|
+
* feed (see `followEvents`).
|
|
2339
2431
|
*
|
|
2340
2432
|
* Beyond the decision timeline this view owns the review data plane: a snapshot
|
|
2341
2433
|
* inventory bar (with session/all clearing), per-event file chips, and a diff
|
|
@@ -2347,7 +2439,6 @@ window.__ModuleLoader__.load({
|
|
|
2347
2439
|
* routes. Everything is best-effort: no session id or any fetch failure renders
|
|
2348
2440
|
* the empty / error state. No @deepseek-ai value imports.
|
|
2349
2441
|
*/
|
|
2350
|
-
const POLL_MS = 5e3;
|
|
2351
2442
|
/** Tag text per event kind, keyed into the plugin dictionary. */
|
|
2352
2443
|
const TAG_KEYS = {
|
|
2353
2444
|
auto: "history.tag.auto",
|
|
@@ -2761,29 +2852,47 @@ window.__ModuleLoader__.load({
|
|
|
2761
2852
|
setEvents(null);
|
|
2762
2853
|
setError(null);
|
|
2763
2854
|
setStats(null);
|
|
2855
|
+
if (sessionId === null) {
|
|
2856
|
+
setEvents([]);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2764
2859
|
let alive = true;
|
|
2765
|
-
|
|
2860
|
+
let cursor = 0;
|
|
2861
|
+
let closeStream = null;
|
|
2862
|
+
fetchSnapshotStats(sessionId).then((next) => {
|
|
2863
|
+
if (alive) setStats(next);
|
|
2864
|
+
});
|
|
2865
|
+
/**
|
|
2866
|
+
* Decisions landing after the initial load. Both the stream's pushes and the
|
|
2867
|
+
* fallback's ticks carry only events past the cursor, so they merge the same
|
|
2868
|
+
* way; a batch is also the point where the snapshot inventory can have grown.
|
|
2869
|
+
*/
|
|
2870
|
+
const onBatch = (batch) => {
|
|
2871
|
+
if (!alive || batch.events.length === 0) return;
|
|
2872
|
+
for (const ev of batch.events) if (ev.id > cursor) cursor = ev.id;
|
|
2873
|
+
setEvents((current) => {
|
|
2874
|
+
const known = new Set((current ?? []).map((ev) => ev.id));
|
|
2875
|
+
const added = batch.events.filter((ev) => !known.has(ev.id));
|
|
2876
|
+
return added.length === 0 ? current : [...added, ...current ?? []].sort((a, b) => b.id - a.id);
|
|
2877
|
+
});
|
|
2878
|
+
setError(null);
|
|
2766
2879
|
fetchSnapshotStats(sessionId).then((next) => {
|
|
2767
2880
|
if (alive) setStats(next);
|
|
2768
2881
|
});
|
|
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
2882
|
};
|
|
2782
|
-
|
|
2783
|
-
|
|
2883
|
+
fetchEvents(sessionId, 0).then((loaded) => {
|
|
2884
|
+
if (!alive) return;
|
|
2885
|
+
setEvents([...loaded].sort((a, b) => b.id - a.id));
|
|
2886
|
+
setError(null);
|
|
2887
|
+
cursor = loaded.reduce((max, ev) => ev.id > max ? ev.id : max, 0);
|
|
2888
|
+
}).catch((e) => {
|
|
2889
|
+
if (alive) setError(String(e?.message ?? e));
|
|
2890
|
+
}).finally(() => {
|
|
2891
|
+
if (alive) closeStream = followEvents(sessionId, () => cursor, onBatch);
|
|
2892
|
+
});
|
|
2784
2893
|
return () => {
|
|
2785
2894
|
alive = false;
|
|
2786
|
-
|
|
2895
|
+
closeStream?.();
|
|
2787
2896
|
};
|
|
2788
2897
|
}, [sessionId]);
|
|
2789
2898
|
/** Whether one event has a snapshot covering this file (chip is clickable). */
|