@goodandready/dsh-dsml-artifact-guard 0.2.7 → 0.2.9

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 CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.2.9] - 2026-10-07
11
+
12
+ ### Fixed
13
+ - **Settings Card Locale Snapshot Extraction**: safely extract active locale string from `LocaleSnapshot` object (`snap?.active`) returned by `ctx.locale.getLocale()` / `getSnapshot()`, eliminating fatal `TypeError: rawLang.startsWith is not a function` during settings card mount (#50, #57).
14
+ - **Russian Language Registration & Ownership**: removed embedded `ru` dictionary from the plugin bundle to prevent `(namespace, 'ru')` duplicate registration collisions in core; delegate Russian translations to `dsh-russian-lang` via `props.t` and `ctx.locale.bind(NS)` (#58).
15
+ - **Volatile Per-Field Settings Schema**: marked configuration fields volatile individually, enabling DSH to discover and generate settings form controls (#52).
16
+
17
+ ## [0.2.8] - 2026-09-26
18
+
19
+ ### Added
20
+ - **Universal DeepSeek Model Matching**: replaced strict single `providerId`/`modelId` pair with case-insensitive `modelPattern` RegExp (default: `deepseek`), automatically protecting all DeepSeek models regardless of provider gateway or vendor prefix (#48).
21
+ - **Target Providers Filter**: added optional `providers` array filter (empty = all providers) (#48).
22
+ - **Russian Localization**: added native `ru` translations in the Web UI settings card alongside English and Chinese (#48).
23
+
24
+ ### Changed
25
+ - **De-identification**: removed proprietary `opencode-go` default in public package configuration.
26
+ - **Backward Compatibility**: user configs with legacy `providerId` and `modelId` continue to work as an additional exact rule, with a deprecation notice and one-click clear button in the Web UI settings card.
27
+ - **Documentation**: added dedicated section explaining lack of DSML processing in core DSH 0.1.7-rc.2.
28
+
10
29
  ## [0.2.7] - 2026-09-25
11
30
 
12
31
  ### Fixed
package/README.md CHANGED
@@ -55,6 +55,14 @@ These leaked closing tags visually pollute the chat bubble, cause Markdown rende
55
55
 
56
56
  ---
57
57
 
58
+ ## 💡 Why Not in DSH Core?
59
+
60
+ As of DeepSeek Harness **0.1.7-rc.2**, the core upstream stream pipeline (`dsh-llm-pi-ai`, `dsh-llm-deepseek*`, etc.) does not perform terminal DSML protocol cleanup — there is zero mention or handling of `|DSML|` anywhere in the core engine. When upstream gateways (e.g. `commandcode`, `deepseek-official`, or custom proxies) leak protocol closing tags at the conclusion of an assistant turn, DSH forwards them directly to the frontend.
61
+
62
+ `dsh-dsml-artifact-guard` serves as the universal runtime guard across all DeepSeek models regardless of provider gateway or routing configuration.
63
+
64
+ ---
65
+
58
66
  ## 🏗️ Architecture
59
67
 
60
68
  ```mermaid
@@ -141,14 +149,14 @@ Restart your DeepSeek Harness instance.
141
149
 
142
150
  ## ⚙️ Configuration (`settings.yaml`)
143
151
 
144
- Configure provider and model targets in `settings.yaml` or through the Web UI:
152
+ Configure model matching and provider targets in `settings.yaml` or through the Web UI:
145
153
 
146
154
  ```yaml
147
155
  # settings.yaml
148
156
  dsh-dsml-artifact-guard:
149
157
  mode: sanitize
150
- providerId: "your-provider-id"
151
- modelId: "your-model-id"
158
+ modelPattern: "deepseek" # Case-insensitive RegExp matching all DeepSeek models
159
+ providers: [] # Empty = guard all providers; or e.g. ["commandcode", "deepseek-official"]
152
160
  ```
153
161
 
154
162
  ### Configuration Parameters
@@ -156,8 +164,10 @@ dsh-dsml-artifact-guard:
156
164
  | Parameter | Type | Default | Description |
157
165
  |:---|:---|:---|:---|
158
166
  | `mode` | `string` | `"sanitize"` | Operation mode: `"sanitize"` (strip tags), `"audit"` (log only), or `"disabled"` |
159
- | `providerId` | `string` | `"opencode-go"` | Target provider identifier exhibiting leaked tags |
160
- | `modelId` | `string` | `"deepseek-v4-flash"` | Target model identifier exhibiting leaked tags |
167
+ | `modelPattern` | `string` | `"deepseek"` | Case-insensitive RegExp matched against model IDs (e.g. `deepseek`, `deepseek-v4.*`, `deepseek/.*`) |
168
+ | `providers` | `string[]` | `[]` | Optional list of provider IDs. When empty, guards all providers |
169
+ | `providerId` | `string` | `undefined` | *(Deprecated)* Legacy exact provider identifier; maintained for backward compatibility |
170
+ | `modelId` | `string` | `undefined` | *(Deprecated)* Legacy exact model identifier; maintained for backward compatibility |
161
171
 
162
172
  ### HTTP Management Endpoints
163
173
 
package/README.ru.md CHANGED
@@ -55,6 +55,14 @@
55
55
 
56
56
  ---
57
57
 
58
+ ## 💡 Почему этого нет в ядре DSH?
59
+
60
+ По состоянию на DeepSeek Harness **0.1.7-rc.2**, ядро системы (`dsh-llm-pi-ai`, `dsh-llm-deepseek*` и др.) вообще не выполняет очистку завершающих тегов DSML — в ядровом коде нет ни единого упоминания `|DSML|`. Когда вышестоящие провайдеры (например, `commandcode`, `deepseek-official` или внешние шлюзы) отдают в потоке закрывающие теги протокола, DSH пропускает их прямо в интерфейс чата.
61
+
62
+ Плагин `dsh-dsml-artifact-guard` обеспечивает универсальную защиту для всех моделей DeepSeek независимо от конкретного шлюза или маршрутизатора.
63
+
64
+ ---
65
+
58
66
  ## 🏗️ Архитектура работы
59
67
 
60
68
  ```mermaid
package/README.zh.md CHANGED
@@ -55,6 +55,14 @@
55
55
 
56
56
  ---
57
57
 
58
+ ## 💡 为什么核心层未包含此功能?
59
+
60
+ 截至 DeepSeek Harness **0.1.7-rc.2**,核心上游流管道(`dsh-llm-pi-ai`、`dsh-llm-deepseek*` 等)均未对末端 DSML 协议标签进行任何清洗——核心引擎中完全不存在对 `|DSML|` 的处理。当上游网关(例如 `commandcode`、`deepseek-official` 或其他代理)在回复结尾泄漏协议闭合标签时,DSH 会将其直接传输到聊天前端。
61
+
62
+ `dsh-dsml-artifact-guard` 为所有 DeepSeek 模型提供通用的运行时防护,不受特定服务商网关或路由配置的限制。
63
+
64
+ ---
65
+
58
66
  ## 🏗️ 架构设计
59
67
 
60
68
  ```mermaid
package/lib/client.js CHANGED
@@ -41,8 +41,15 @@ window.__ModuleLoader__.load({
41
41
  modeSanitize: 'Sanitize (strip leaked closing tags)',
42
42
  modeAudit: 'Audit (log warnings only, preserve output)',
43
43
  modeDisabled: 'Disabled (bypass all streams)',
44
- providerId: 'Target Provider ID',
45
- modelId: 'Target Model ID',
44
+ modelPattern: 'Model Pattern (RegExp)',
45
+ modelPatternHint: 'Case-insensitive RegExp matched against model IDs (e.g. deepseek, deepseek-v4.*). Default: deepseek.',
46
+ providers: 'Target Providers (optional)',
47
+ providersPlaceholder: 'all providers (comma-separated, e.g. commandcode, deepseek-official)',
48
+ providersHint: 'Comma-separated list of provider IDs. Leave empty to guard all providers.',
49
+ legacyDetected: 'Legacy providerId / modelId detected: active as an additional rule.',
50
+ legacyClear: 'Clear legacy fields',
51
+ providerId: 'Target Provider ID (legacy)',
52
+ modelId: 'Target Model ID (legacy)',
46
53
  disabledNotice: '⚠️ Guard is currently disabled. DSML closing tags will reach the chat interface.',
47
54
  statusUnavailable: 'Settings service unavailable.',
48
55
  saving: 'Saving…',
@@ -67,8 +74,15 @@ window.__ModuleLoader__.load({
67
74
  modeSanitize: 'Sanitize 清洗(自动剥除残留闭合标签)',
68
75
  modeAudit: 'Audit 审计(仅在日志记录警告,不修改正文)',
69
76
  modeDisabled: 'Disabled 禁用(直通所有数据流)',
70
- providerId: '目标服务商标识 (Provider ID)',
71
- modelId: '目标模型标识 (Model ID)',
77
+ modelPattern: '模型正则匹配式 (RegExp)',
78
+ modelPatternHint: '对模型 ID 进行不区分大小写的正则表达式匹配(默认:deepseek)。支持任意服务商前缀。',
79
+ providers: '目标服务商(可选)',
80
+ providersPlaceholder: '所有服务商(以逗号分隔,例如 commandcode, deepseek-official)',
81
+ providersHint: '以逗号分隔的服务商 ID 列表。留空则保护所有服务商。',
82
+ legacyDetected: '检测到旧版 providerId / modelId 配置,并作为附加精确规则生效。',
83
+ legacyClear: '清除旧版配置',
84
+ providerId: '目标服务商标识 (Provider ID, 旧版)',
85
+ modelId: '目标模型标识 (Model ID, 旧版)',
72
86
  disabledNotice: '⚠️ 守卫当前已禁用,残留的 DSML 闭合标签将直接显示在聊天界面。',
73
87
  statusUnavailable: '配置服务不可用。',
74
88
  saving: '正在保存…',
@@ -77,10 +91,13 @@ window.__ModuleLoader__.load({
77
91
  saveError: '保存失败: {msg}',
78
92
  };
79
93
 
94
+
80
95
  const DEFAULTS = {
81
96
  mode: 'sanitize',
82
- providerId: 'opencode-go',
83
- modelId: 'deepseek-v4-flash',
97
+ modelPattern: 'deepseek',
98
+ providers: '',
99
+ providerId: '',
100
+ modelId: '',
84
101
  };
85
102
 
86
103
  function PluginCard({ ctx: _ctx, t, bare }) {
@@ -115,10 +132,38 @@ window.__ModuleLoader__.load({
115
132
  }
116
133
  const scope = scopeRef.current;
117
134
 
118
- const currentLang = ((typeof _ctx?.locale?.getLocale === 'function' ? _ctx.locale.getLocale() : null) || (typeof navigator !== 'undefined' && navigator.language) || 'en').startsWith('zh') ? 'zh' : 'en';
135
+ const rawLang = (() => {
136
+ try {
137
+ const snap = typeof _ctx?.locale?.getLocale === 'function'
138
+ ? _ctx.locale.getLocale()
139
+ : (typeof _ctx?.locale?.getSnapshot === 'function' ? _ctx.locale.getSnapshot() : null);
140
+ if (typeof snap?.active === 'string' && snap.active) return snap.active;
141
+ if (typeof snap === 'string' && snap) return snap;
142
+ } catch (err) {
143
+ /* Fallback if locale service getter fails */
144
+ }
145
+ if (typeof navigator !== 'undefined' && typeof navigator.language === 'string' && navigator.language) {
146
+ return navigator.language;
147
+ }
148
+ return 'en';
149
+ })();
150
+ const currentLang = rawLang.startsWith('zh') ? 'zh' : 'en';
119
151
  const dict = currentLang === 'zh' ? zh : en;
152
+
153
+ const tBound = React.useMemo(() => {
154
+ if (typeof t === 'function') return t;
155
+ if (typeof _ctx?.locale?.bind === 'function') {
156
+ try {
157
+ return _ctx.locale.bind(NS);
158
+ } catch (err) {
159
+ /* Locale bind fallback */
160
+ }
161
+ }
162
+ return null;
163
+ }, [t, _ctx]);
164
+
120
165
  const tt = (k, params) => {
121
- let str = (typeof t === 'function' ? t(k) : null) || dict[k] || en[k] || k;
166
+ let str = (typeof tBound === 'function' ? tBound(k) : null) || dict[k] || en[k] || k;
122
167
  if (params && typeof params === 'object') {
123
168
  for (const [pk, pv] of Object.entries(params)) {
124
169
  str = str.replace(new RegExp('\\{' + pk + '\\}', 'g'), String(pv));
@@ -137,22 +182,19 @@ window.__ModuleLoader__.load({
137
182
  const syncSnapshot = (snap) => {
138
183
  if (!snap || cancelled) return;
139
184
  const view = snap.view || snap.value || snap;
140
- if (typeof snap === 'object' && 'status' in snap) {
141
- if (snap.status === 'ready' && view) {
142
- setDraft({
143
- mode: view.mode || DEFAULTS.mode,
144
- providerId: view.providerId || DEFAULTS.providerId,
145
- modelId: view.modelId || DEFAULTS.modelId,
146
- });
147
- setStatus('ready');
148
- } else {
149
- setStatus(snap.status);
150
- }
151
- } else if (typeof view === 'object') {
185
+ if (typeof snap === 'object' && 'status' in snap && snap.status !== 'ready') {
186
+ setStatus(snap.status);
187
+ return;
188
+ }
189
+ if (typeof view === 'object' && view !== null) {
152
190
  setDraft({
153
191
  mode: view.mode || DEFAULTS.mode,
154
- providerId: view.providerId || DEFAULTS.providerId,
155
- modelId: view.modelId || DEFAULTS.modelId,
192
+ modelPattern: view.modelPattern !== undefined ? String(view.modelPattern) : DEFAULTS.modelPattern,
193
+ providers: Array.isArray(view.providers)
194
+ ? view.providers.join(', ')
195
+ : (typeof view.providers === 'string' ? view.providers : ''),
196
+ providerId: view.providerId ? String(view.providerId) : '',
197
+ modelId: view.modelId ? String(view.modelId) : '',
156
198
  });
157
199
  setStatus('ready');
158
200
  } else {
@@ -253,9 +295,16 @@ window.__ModuleLoader__.load({
253
295
  setMsg('');
254
296
  try {
255
297
  const failures = [];
256
- for (const key of ['mode', 'providerId', 'modelId']) {
298
+ const entries = [
299
+ ['mode', draft.mode || 'sanitize'],
300
+ ['modelPattern', (draft.modelPattern !== undefined && draft.modelPattern !== null) ? String(draft.modelPattern).trim() : 'deepseek'],
301
+ ['providers', draft.providers ? draft.providers.split(',').map(s => s.trim()).filter(Boolean) : []],
302
+ ['providerId', draft.providerId ? String(draft.providerId).trim() : ''],
303
+ ['modelId', draft.modelId ? String(draft.modelId).trim() : ''],
304
+ ];
305
+ for (const [key, val] of entries) {
257
306
  try {
258
- await scope.set(key, draft[key]);
307
+ await scope.set(key, val);
259
308
  } catch (err) {
260
309
  failures.push(key + ': ' + (err.message || String(err)));
261
310
  }
@@ -452,15 +501,16 @@ window.__ModuleLoader__.load({
452
501
  )
453
502
  ),
454
503
 
455
- // Provider ID Field
504
+ // Model Pattern Field
456
505
  React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
457
506
  React.createElement('label', {
458
507
  style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
459
- }, tt('providerId')),
508
+ }, tt('modelPattern')),
460
509
  React.createElement('input', {
461
510
  type: 'text',
462
- value: draft.providerId,
463
- onChange: (e) => setDraft(d => ({ ...d, providerId: e.target.value })),
511
+ value: draft.modelPattern,
512
+ placeholder: 'deepseek',
513
+ onChange: (e) => setDraft(d => ({ ...d, modelPattern: e.target.value })),
464
514
  style: {
465
515
  height: 34,
466
516
  borderRadius: 6,
@@ -470,18 +520,22 @@ window.__ModuleLoader__.load({
470
520
  padding: '0 10px',
471
521
  fontSize: 13,
472
522
  },
473
- })
523
+ }),
524
+ React.createElement('span', {
525
+ style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', lineHeight: '1.4' },
526
+ }, tt('modelPatternHint'))
474
527
  ),
475
528
 
476
- // Model ID Field
529
+ // Target Providers Field
477
530
  React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
478
531
  React.createElement('label', {
479
532
  style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' },
480
- }, tt('modelId')),
533
+ }, tt('providers')),
481
534
  React.createElement('input', {
482
535
  type: 'text',
483
- value: draft.modelId,
484
- onChange: (e) => setDraft(d => ({ ...d, modelId: e.target.value })),
536
+ value: draft.providers,
537
+ placeholder: tt('providersPlaceholder'),
538
+ onChange: (e) => setDraft(d => ({ ...d, providers: e.target.value })),
485
539
  style: {
486
540
  height: 34,
487
541
  borderRadius: 6,
@@ -491,7 +545,49 @@ window.__ModuleLoader__.load({
491
545
  padding: '0 10px',
492
546
  fontSize: 13,
493
547
  },
494
- })
548
+ }),
549
+ React.createElement('span', {
550
+ style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', lineHeight: '1.4' },
551
+ }, tt('providersHint'))
552
+ ),
553
+
554
+ // Legacy Provider/Model notice if detected
555
+ (draft.providerId || draft.modelId) && React.createElement('div', {
556
+ style: {
557
+ display: 'flex',
558
+ alignItems: 'flex-start',
559
+ justifyContent: 'space-between',
560
+ gap: 8,
561
+ padding: '8px 10px',
562
+ borderRadius: 6,
563
+ background: 'color-mix(in srgb, var(--dsw-alias-state-warning-primary) 10%, transparent)',
564
+ border: '1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 25%, transparent)',
565
+ fontSize: 12,
566
+ color: 'var(--dsw-alias-label-secondary)',
567
+ },
568
+ },
569
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 2 } },
570
+ React.createElement('span', null, tt('legacyDetected')),
571
+ React.createElement('span', { style: { fontFamily: 'monospace', fontSize: 11 } },
572
+ (draft.providerId ? 'provider: ' + draft.providerId : '') +
573
+ (draft.providerId && draft.modelId ? ' | ' : '') +
574
+ (draft.modelId ? 'model: ' + draft.modelId : '')
575
+ )
576
+ ),
577
+ React.createElement('button', {
578
+ type: 'button',
579
+ onClick: () => setDraft(d => ({ ...d, providerId: '', modelId: '' })),
580
+ style: {
581
+ background: 'transparent',
582
+ border: 'none',
583
+ color: 'var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))',
584
+ fontSize: 11,
585
+ cursor: 'pointer',
586
+ textDecoration: 'underline',
587
+ whiteSpace: 'nowrap',
588
+ padding: '2px 4px',
589
+ },
590
+ }, tt('legacyClear'))
495
591
  ),
496
592
 
497
593
  // Actions
package/lib/index.js CHANGED
@@ -9,20 +9,57 @@ const KEEP = 96;
9
9
  export const name = '@goodandready/dsh-dsml-artifact-guard';
10
10
  export const inject = ['llm', 'settings', 'webServer'];
11
11
 
12
- const rawConfig = Schema.object({
13
- mode: Schema.string().default('sanitize'),
14
- providerId: Schema.string().default('opencode-go'),
15
- modelId: Schema.string().default('deepseek-v4-flash'),
12
+ // DSH builds a namespace's settings form from the PER-FIELD volatile markers of the
13
+ // entry schema. Wrapping the object in .volatile() produced none of them, so no form was
14
+ // ever generated. Mark each user-editable field instead.
15
+ export const Config = Schema.object({
16
+ mode: Schema.string().default('sanitize').volatile().description('How to handle detected artifact markup'),
17
+ modelPattern: Schema.string().default('deepseek').volatile().description('Model name substring this plugin guards'),
18
+ providers: Schema.array(Schema.string()).default([]).volatile().description('Restrict guarding to these providers; empty means all'),
19
+ providerId: Schema.string().volatile().description('Legacy exact provider id match'),
20
+ modelId: Schema.string().volatile().description('Legacy exact model id match'),
16
21
  });
17
22
 
18
- export const Config = typeof rawConfig.volatile === 'function'
19
- ? rawConfig.volatile()
20
- : rawConfig.extra('volatile', true);
23
+ // Unwrap Volatile boxes before any caller reads a value.
24
+ export function plainConfig(value) {
25
+ if (value === null || typeof value !== 'object') return value;
26
+ if (Array.isArray(value)) return value.map(plainConfig);
27
+ if (typeof value.get === 'function') return plainConfig(value.get());
28
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainConfig(v)]));
29
+ }
21
30
 
22
31
  export function matchesScope(o, c) {
23
- const provider = o?.providerId ?? o?.provider?.id ?? o?.provider;
24
- const model = o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model;
25
- return provider == c.providerId && model == c.modelId;
32
+ if (!c || c.mode === 'disabled') return false;
33
+ const provider = String(o?.providerId ?? o?.provider?.id ?? o?.provider ?? '').trim();
34
+ const model = String(o?.modelId ?? o?.model?.id ?? o?.model?.name ?? o?.model ?? '').trim();
35
+ if (!model && !provider) return false;
36
+
37
+ // 1. Backward compatibility: if legacy exact providerId/modelId were specified and match
38
+ if (c.providerId && c.modelId && provider === c.providerId && model === c.modelId) {
39
+ return true;
40
+ }
41
+
42
+ // 2. Provider check: if c.providers is non-empty, provider must be included
43
+ const providersList = Array.isArray(c.providers)
44
+ ? c.providers.map(p => String(p).trim()).filter(Boolean)
45
+ : (typeof c.providers === 'string' && c.providers.trim() ? c.providers.split(',').map(p => p.trim()).filter(Boolean) : []);
46
+
47
+ if (providersList.length > 0) {
48
+ if (!providersList.includes(provider)) return false;
49
+ }
50
+
51
+ // 3. Model pattern check (case-insensitive regex)
52
+ const pattern = (c.modelPattern !== undefined && c.modelPattern !== null && String(c.modelPattern).trim() !== '')
53
+ ? String(c.modelPattern).trim()
54
+ : 'deepseek';
55
+
56
+ try {
57
+ const rx = new RegExp(pattern, 'i');
58
+ return rx.test(model);
59
+ } catch (err) {
60
+ /* Fallback to substring matching if regex syntax is invalid */
61
+ return model.toLowerCase().includes(pattern.toLowerCase());
62
+ }
26
63
  }
27
64
 
28
65
  export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact } = {}) {
@@ -109,25 +146,21 @@ export async function* sanitizeDsmlArtifacts(s, { mode = 'sanitize', onArtifact
109
146
 
110
147
  export function unwrapConfig(conf) {
111
148
  if (!conf) return {};
112
- if (typeof conf.get === 'function') {
149
+ // A Volatile reference (any field is now volatile) resolves through .get().
150
+ const readRef = (val) => {
113
151
  try {
114
- const val = conf.get();
115
- return (val && typeof val === 'object') ? val : {};
116
- } catch (err) {
117
- /* Volatile getter failed */
152
+ const inner = typeof val?.get === 'function' ? val.get() : val;
153
+ return inner && typeof inner === 'object' ? inner : {};
154
+ } catch {
118
155
  return {};
119
156
  }
120
- }
157
+ };
158
+ if (typeof conf.get === 'function') return readRef(conf);
121
159
  try {
122
- const res = Config(conf);
123
- if (res && typeof res.get === 'function') {
124
- const val = res.get();
125
- return (val && typeof val === 'object') ? val : {};
126
- }
127
- return (res && typeof res === 'object') ? res : {};
128
- } catch (err) {
129
- /* Config validation fallback to object */
130
- return (conf && typeof conf === 'object') ? conf : {};
160
+ // Config() returns volatile boxes now, so unwrap them; a plain input stays plain.
161
+ return plainConfig(Config(conf));
162
+ } catch {
163
+ return plainConfig(conf) && typeof plainConfig(conf) === 'object' ? plainConfig(conf) : {};
131
164
  }
132
165
  }
133
166
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-dsml-artifact-guard",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Sanitizes leaked DeepSeek DSML closing tags from model text streams",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -33,12 +33,15 @@
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@deepseek-ai/cordis": "^4.0.1",
36
- "@deepseek-ai/schemastery": "^3.18.1"
36
+ "@deepseek-ai/schemastery": "^3.18.4"
37
37
  },
38
38
  "exports": {
39
39
  ".": "./lib/index.js",
40
40
  "./client": "./lib/client.js",
41
41
  "./cordis.patch.yml": "./cordis.patch.yml",
42
42
  "./package.json": "./package.json"
43
+ },
44
+ "devDependencies": {
45
+ "@deepseek-ai/schemastery": "^3.18.4"
43
46
  }
44
47
  }