@goodandready/dsh-dsml-artifact-guard 0.1.2 → 0.1.4
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 +12 -0
- package/cordis.patch.yml +2 -5
- package/docs/README.ru.md +12 -0
- package/docs/README.zh.md +12 -0
- package/docs/design/DESIGN.md +36 -0
- package/lib/index.js +105 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -150,3 +150,15 @@ npm run check
|
|
|
150
150
|
## 📄 License
|
|
151
151
|
|
|
152
152
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
## Changed in v0.1.3
|
|
156
|
+
|
|
157
|
+
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
158
|
+
#9: `cordis.patch.yml` uses `config: {}` — schema defaults (`mode: audit`, provider/model ids) apply unless overridden in host config.
|
|
159
|
+
|
|
160
|
+
## Changed in v0.1.4
|
|
161
|
+
|
|
162
|
+
#11: schema `mode` default set to `sanitize` for out-of-the-box protection.
|
|
163
|
+
#11: robust open/close tag balancing in `sanitizeDsmlArtifacts` to handle prior closed blocks in prose.
|
|
164
|
+
#11: repo hygiene with tracked `package-lock.json` and design contract.
|
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
|
@@ -150,3 +150,15 @@ npm run check
|
|
|
150
150
|
## 📄 Лицензия
|
|
151
151
|
|
|
152
152
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
## Changed in v0.1.3
|
|
156
|
+
|
|
157
|
+
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
158
|
+
#9: `cordis.patch.yml` uses `config: {}` — schema defaults (`mode: audit`, provider/model ids) apply unless overridden in host config.
|
|
159
|
+
|
|
160
|
+
## Изменения в v0.1.4
|
|
161
|
+
|
|
162
|
+
#11: значение `mode` по умолчанию в схеме переведено на `sanitize` для защиты «из коробки».
|
|
163
|
+
#11: балансировка открывающих и закрывающих тегов в `sanitizeDsmlArtifacts` при наличии закрытых блоков в прозе.
|
|
164
|
+
#11: гигиена репозитория с отслеживаемым `package-lock.json` и дизайн-контрактом.
|
package/docs/README.zh.md
CHANGED
|
@@ -150,3 +150,15 @@ npm run check
|
|
|
150
150
|
## 📄 开源许可证
|
|
151
151
|
|
|
152
152
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
## Changed in v0.1.3
|
|
156
|
+
|
|
157
|
+
#8: wrap `llm/stream` in `ctx.effect` so unload unsubscribes.
|
|
158
|
+
#9: `cordis.patch.yml` uses `config: {}` — schema defaults (`mode: audit`, provider/model ids) apply unless overridden in host config.
|
|
159
|
+
|
|
160
|
+
## v0.1.4 变更说明
|
|
161
|
+
|
|
162
|
+
#11: 将 schema 中 `mode` 默认值设置为 `sanitize`,实现开箱即用的清理保护。
|
|
163
|
+
#11: 在 `sanitizeDsmlArtifacts` 中实现严格的开闭标签深度平衡,精准处理正文中已闭合标签与末尾泄漏标签。
|
|
164
|
+
#11: 纳入 `package-lock.json` 跟踪并完善设计契约。
|
|
@@ -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
|
+
}
|