@goodandready/dsh-dsml-artifact-guard 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -4
- package/cordis.patch.yml +2 -5
- package/docs/README.ru.md +30 -4
- package/docs/README.zh.md +29 -3
- package/docs/design/DESIGN.md +36 -0
- package/lib/index.js +105 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/🌐_DSH_Hub-goodandready.app-ff4500.svg?style=for-the-badge&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
<a href="docs/README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
|
@@ -64,15 +74,15 @@ graph TD
|
|
|
64
74
|
Audit["Audit Mode<br/>(Emits ctx.logger warning only)"]
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
Turn -->|
|
|
77
|
+
Turn -->|llm/stream hook| Hook
|
|
68
78
|
Hook --> ScopeCheck
|
|
69
79
|
ScopeCheck -->|No| PassThrough
|
|
70
80
|
ScopeCheck -->|Yes| Buffer
|
|
71
81
|
PassThrough --> ChatUI
|
|
72
82
|
Buffer --> Detector
|
|
73
83
|
Detector -->|No Artifact| ChatUI
|
|
74
|
-
Detector -->|Artifact
|
|
75
|
-
Detector -->|Artifact
|
|
84
|
+
Detector -->|Artifact detected: sanitize| Sanitize --> ChatUI
|
|
85
|
+
Detector -->|Artifact detected: audit| Audit --> ChatUI
|
|
76
86
|
```
|
|
77
87
|
|
|
78
88
|
---
|
|
@@ -150,3 +160,19 @@ npm run check
|
|
|
150
160
|
## 📄 License
|
|
151
161
|
|
|
152
162
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## Changed in v0.1.3
|
|
166
|
+
|
|
167
|
+
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
168
|
+
#9: `cordis.patch.yml` uses `config: {}` — schema defaults (`mode: audit`, provider/model ids) apply unless overridden in host config.
|
|
169
|
+
|
|
170
|
+
## Changed in v0.1.4
|
|
171
|
+
|
|
172
|
+
#11: schema `mode` default set to `sanitize` for out-of-the-box protection.
|
|
173
|
+
#11: robust open/close tag balancing in `sanitizeDsmlArtifacts` to handle prior closed blocks in prose.
|
|
174
|
+
#11: repo hygiene with tracked `package-lock.json` and design contract.
|
|
175
|
+
|
|
176
|
+
## Changed in v0.1.5
|
|
177
|
+
|
|
178
|
+
#15: fix Mermaid diagram edge label syntax on GitHub and add mandatory repository support block.
|
package/cordis.patch.yml
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
# Host-only DSH bundle.
|
|
1
|
+
# Host-only DSH bundle. Defaults live in Config schema (mode=sanitize).
|
|
2
2
|
- insert:
|
|
3
3
|
- id: dsh-dsml-artifact-guard
|
|
4
4
|
name: '@goodandready/dsh-dsml-artifact-guard'
|
|
5
|
-
config:
|
|
6
|
-
mode: sanitize
|
|
7
|
-
providerId: opencode-go
|
|
8
|
-
modelId: deepseek-v4-flash
|
|
5
|
+
config: {}
|
package/docs/README.ru.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge
|
|
15
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/🌐_DSH_Hub-goodandready.app-ff4500.svg?style=for-the-badge&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
|
@@ -64,15 +74,15 @@ graph TD
|
|
|
64
74
|
Audit["Режим Audit<br/>(Логирование через ctx.logger)"]
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
Turn
|
|
77
|
+
Turn -->|Хук llm/stream| Hook
|
|
68
78
|
Hook --> ScopeCheck
|
|
69
79
|
ScopeCheck -->|Нет| PassThrough
|
|
70
80
|
ScopeCheck -->|Да| Buffer
|
|
71
81
|
PassThrough --> ChatUI
|
|
72
82
|
Buffer --> Detector
|
|
73
83
|
Detector -->|Нет артефакта| ChatUI
|
|
74
|
-
Detector -->|Артефакт
|
|
75
|
-
Detector -->|Артефакт
|
|
84
|
+
Detector -->|Артефакт найден: sanitize| Sanitize --> ChatUI
|
|
85
|
+
Detector -->|Артефакт найден: audit| Audit --> ChatUI
|
|
76
86
|
```
|
|
77
87
|
|
|
78
88
|
---
|
|
@@ -150,3 +160,19 @@ npm run check
|
|
|
150
160
|
## 📄 Лицензия
|
|
151
161
|
|
|
152
162
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## Changed in v0.1.3
|
|
166
|
+
|
|
167
|
+
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
168
|
+
#9: `cordis.patch.yml` uses `config: {}` — schema defaults (`mode: audit`, provider/model ids) apply unless overridden in host config.
|
|
169
|
+
|
|
170
|
+
## Изменения в v0.1.4
|
|
171
|
+
|
|
172
|
+
#11: значение `mode` по умолчанию в схеме переведено на `sanitize` для защиты «из коробки».
|
|
173
|
+
#11: балансировка открывающих и закрывающих тегов в `sanitizeDsmlArtifacts` при наличии закрытых блоков в прозе.
|
|
174
|
+
#11: гигиена репозитория с отслеживаемым `package-lock.json` и дизайн-контрактом.
|
|
175
|
+
|
|
176
|
+
## Изменения в v0.1.5
|
|
177
|
+
|
|
178
|
+
#15: исправлен синтаксис стрелок диаграммы Mermaid на GitHub и добавлен обязательный блок поддержки репозитория.
|
package/docs/README.zh.md
CHANGED
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<table align="center">
|
|
25
|
+
<tr>
|
|
26
|
+
<td align="center">
|
|
27
|
+
⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
|
|
28
|
+
<br><br>
|
|
29
|
+
🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
24
34
|
</div>
|
|
25
35
|
|
|
26
36
|
---
|
|
@@ -64,15 +74,15 @@ graph TD
|
|
|
64
74
|
Audit["审计模式 (Audit)<br/>(仅记录日志不改动文本)"]
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
Turn -->|
|
|
77
|
+
Turn -->|llm/stream 拦截| Hook
|
|
68
78
|
Hook --> ScopeCheck
|
|
69
79
|
ScopeCheck -->|未命中| PassThrough
|
|
70
80
|
ScopeCheck -->|命中| Buffer
|
|
71
81
|
PassThrough --> ChatUI
|
|
72
82
|
Buffer --> Detector
|
|
73
83
|
Detector -->|无异常| ChatUI
|
|
74
|
-
Detector
|
|
75
|
-
Detector
|
|
84
|
+
Detector -->|检出残留: sanitize| Sanitize --> ChatUI
|
|
85
|
+
Detector -->|检出残留: audit| Audit --> ChatUI
|
|
76
86
|
```
|
|
77
87
|
|
|
78
88
|
---
|
|
@@ -150,3 +160,19 @@ npm run check
|
|
|
150
160
|
## 📄 开源许可证
|
|
151
161
|
|
|
152
162
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## Changed in v0.1.3
|
|
166
|
+
|
|
167
|
+
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
168
|
+
#9: `cordis.patch.yml` uses `config: {}` — schema defaults (`mode: audit`, provider/model ids) apply unless overridden in host config.
|
|
169
|
+
|
|
170
|
+
## v0.1.4 变更说明
|
|
171
|
+
|
|
172
|
+
#11: 将 schema 中 `mode` 默认值设置为 `sanitize`,实现开箱即用的清理保护。
|
|
173
|
+
#11: 在 `sanitizeDsmlArtifacts` 中实现严格的开闭标签深度平衡,精准处理正文中已闭合标签与末尾泄漏标签。
|
|
174
|
+
#11: 纳入 `package-lock.json` 跟踪并完善设计契约。
|
|
175
|
+
|
|
176
|
+
## v0.1.5 变更说明
|
|
177
|
+
|
|
178
|
+
#15: 修复 GitHub 上 Mermaid 流程图边标签语法解析错误,并补全标准项目支持引导区。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Design Contract: dsh-dsml-artifact-guard
|
|
2
|
+
|
|
3
|
+
## 1. Product Purpose & User Scenarios
|
|
4
|
+
- **Purpose**: Host-only streaming guard for DeepSeek Harness that intercepts assistant stream chunks and strips leaked DSML closing tags (`</|DSML|parameter>`, `</|DSML|invoke>`, `</|DSML|tool_calls>`) emitted by upstream providers before text reaches the user interface.
|
|
5
|
+
- **Primary Scenarios**:
|
|
6
|
+
- Assistant responds through an upstream gateway exhibiting DSML leakage (e.g. `opencode-go` + `deepseek-v4-flash`). Leaked protocol closing tags at the end of the response are stripped automatically.
|
|
7
|
+
- User and assistant discuss DSML syntax or generate markdown/code examples with DSML tags. Legitimate open tags and their matching closing tags are preserved intact (fail-open).
|
|
8
|
+
- Out-of-scope providers/models pass through with zero buffering or regex evaluation.
|
|
9
|
+
|
|
10
|
+
## 2. User Surfaces
|
|
11
|
+
- **Web UI**: UI отсутствует на текущем этапе (host-only middleware plugin).
|
|
12
|
+
- **DSH Configuration**:
|
|
13
|
+
- `mode`: `"sanitize"` (default, strips tags), `"audit"` (logs to `ctx.logger.info`), `"disabled"` (bypasses).
|
|
14
|
+
- `providerId`: target provider (default `"opencode-go"`).
|
|
15
|
+
- `modelId`: target model (default `"deepseek-v4-flash"`).
|
|
16
|
+
- **Documentation**:
|
|
17
|
+
- Full trilingual documentation: `README.md` (EN), `docs/README.ru.md` (RU), `docs/README.zh.md` (ZH).
|
|
18
|
+
|
|
19
|
+
## 3. Streaming & Buffering Contract
|
|
20
|
+
- **Synchronous Hook Contract**: The `llm/stream` listener returns an `AsyncIterable` synchronously. Returning a `Promise` breaks the Cordis event dispatcher.
|
|
21
|
+
- **Sliding Buffer (KEEP = 96)**: Preserves the terminal window across chunk splits.
|
|
22
|
+
- **Tag Balancing**: Tracks opening and closing DSML tags across the stream so that only orphaned closing tags at the terminal tail are sanitized, leaving balanced code blocks untouched.
|
|
23
|
+
|
|
24
|
+
## 4. Do / Don't
|
|
25
|
+
- **Do**:
|
|
26
|
+
- Preserve fail-open guarantees for all user prose.
|
|
27
|
+
- Dispose hook registration with `ctx.effect`.
|
|
28
|
+
- Pass non-target provider/model streams directly through.
|
|
29
|
+
- **Don't**:
|
|
30
|
+
- Do not make the stream hook async.
|
|
31
|
+
- Do not drop legitimate DSML discussion in chat.
|
|
32
|
+
- Do not add heavy client-side UI for simple host middleware.
|
|
33
|
+
|
|
34
|
+
## 5. Decisions Log
|
|
35
|
+
- **2026-09-06**: Added `ctx.effect` lifecycle wrapper and switched patch to schema-driven defaults (`#8, #9`).
|
|
36
|
+
- **2026-09-12**: Changed schema default mode to `sanitize` for out-of-the-box protection, implemented robust open/close tag balancing across streaming chunks, and tracked `package-lock.json` (`#11`).
|
package/lib/index.js
CHANGED
|
@@ -1 +1,105 @@
|
|
|
1
|
-
import Schema from '@deepseek-ai/schemastery';
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
|
|
3
|
+
const TAIL = /(?:<\/|DSML|parameter>\s*)?(?:<\/|DSML|invoke>\s*)?<\/|DSML|tool_calls>\s*$/u;
|
|
4
|
+
const OPEN_TAG = /<|DSML|(?:tool_calls|invoke|parameter)[^>]*>/gu;
|
|
5
|
+
const CLOSE_TAG = /<\/|DSML|(?:tool_calls|invoke|parameter)>/gu;
|
|
6
|
+
const KEEP = 96;
|
|
7
|
+
const SCAN_BUFFER_SAFE = 64;
|
|
8
|
+
|
|
9
|
+
export const name = '@goodandready/dsh-dsml-artifact-guard';
|
|
10
|
+
export const inject = ['llm'];
|
|
11
|
+
|
|
12
|
+
export const Config = Schema.object({
|
|
13
|
+
mode: Schema.string().default('sanitize'),
|
|
14
|
+
providerId: Schema.string().default('opencode-go'),
|
|
15
|
+
modelId: Schema.string().default('deepseek-v4-flash'),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function matchesScope(o, c) {
|
|
19
|
+
return (o?.providerId ?? o?.provider?.id ?? o?.provider) == c.providerId
|
|
20
|
+
&& (o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model) == c.modelId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact } = {}) {
|
|
24
|
+
let p = '';
|
|
25
|
+
let i;
|
|
26
|
+
let scanBuffer = '';
|
|
27
|
+
let totalOpen = 0;
|
|
28
|
+
let totalClose = 0;
|
|
29
|
+
|
|
30
|
+
const countTags = (str) => {
|
|
31
|
+
if (!str) return;
|
|
32
|
+
const opens = str.match(OPEN_TAG);
|
|
33
|
+
if (opens) totalOpen += opens.length;
|
|
34
|
+
const closes = str.match(CLOSE_TAG);
|
|
35
|
+
if (closes) totalClose += closes.length;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const flush = async function* (end) {
|
|
39
|
+
if (!p) return;
|
|
40
|
+
let text = p;
|
|
41
|
+
if (end) {
|
|
42
|
+
if (scanBuffer) {
|
|
43
|
+
countTags(scanBuffer);
|
|
44
|
+
scanBuffer = '';
|
|
45
|
+
}
|
|
46
|
+
if (TAIL.test(text)) {
|
|
47
|
+
const tailMatch = text.match(TAIL);
|
|
48
|
+
const tailCloseCount = tailMatch ? (tailMatch[0].match(CLOSE_TAG) || []).length : 0;
|
|
49
|
+
const closedBeforeTail = totalClose - tailCloseCount;
|
|
50
|
+
const unclosedBeforeTail = totalOpen - closedBeforeTail;
|
|
51
|
+
if (unclosedBeforeTail <= 0) {
|
|
52
|
+
onArtifact?.({ mode, removed: 1 });
|
|
53
|
+
if (mode === 'sanitize') text = text.replace(TAIL, '');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (text) yield { type: 'text-delta', index: i, text };
|
|
58
|
+
p = '';
|
|
59
|
+
i = undefined;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for await (const c of s) {
|
|
63
|
+
if (c?.type === 'text-delta' && typeof c.text === 'string') {
|
|
64
|
+
if (p && i !== c.index) yield* flush(false);
|
|
65
|
+
i = c.index;
|
|
66
|
+
p += c.text;
|
|
67
|
+
|
|
68
|
+
scanBuffer += c.text;
|
|
69
|
+
if (scanBuffer.length > SCAN_BUFFER_SAFE) {
|
|
70
|
+
const safe = scanBuffer.slice(0, scanBuffer.length - SCAN_BUFFER_SAFE);
|
|
71
|
+
scanBuffer = scanBuffer.slice(scanBuffer.length - SCAN_BUFFER_SAFE);
|
|
72
|
+
countTags(safe);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (p.length > KEEP) {
|
|
76
|
+
const n = p.length - KEEP;
|
|
77
|
+
yield { type: 'text-delta', index: i, text: p.slice(0, n) };
|
|
78
|
+
p = p.slice(n);
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (c?.type === 'finish') {
|
|
83
|
+
yield* flush(true);
|
|
84
|
+
yield c;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
yield* flush(false);
|
|
88
|
+
yield c;
|
|
89
|
+
}
|
|
90
|
+
yield* flush(true);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function apply(ctx, c = {}) {
|
|
94
|
+
const e = Config(c) ?? {};
|
|
95
|
+
// #8: dispose with ctx.effect so unload unsubscribes llm/stream
|
|
96
|
+
ctx.effect(() => ctx.on('llm/stream', (o, next) => {
|
|
97
|
+
const s = next(o);
|
|
98
|
+
return e.mode === 'disabled' || !matchesScope(o, e)
|
|
99
|
+
? s
|
|
100
|
+
: sanitizeDsmlArtifacts(s, {
|
|
101
|
+
mode: e.mode,
|
|
102
|
+
onArtifact: (x) => ctx.logger?.info?.('DSML closing-tag artifact detected', x),
|
|
103
|
+
});
|
|
104
|
+
}));
|
|
105
|
+
}
|