dsh-mask 0.2.0 → 0.2.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/ARCHITECTURE.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## Roles
8
8
 
9
- - **Consumes public services only**: `commands`, `storageDomain` (hard `inject`); `tools` (registered via `ctx.inject(['tools'], …)` when present). The masking seam is the `agent/pre-step` waterfall.
9
+ - **Consumes public services only**: `commands` (hard `inject`); `storageDomain` (optional `ctx.get` — missing means a memory-only restore table with a one-time warning); `tools` (registered via `ctx.inject(['tools'], …)` when present). The masking seam is the `agent/pre-step` waterfall.
10
10
  - **`lib/` is zero-DSH-dependency**: services are wired only at the boundary in `index.mjs`; `lib/` depends only on `node:` built-ins (the single sanctioned exception is `lib/domain.mjs`, which imports `zod` and `@deepseek-ai/dsh-storage-domain` because the domain record schema is a persistence-boundary validator).
11
11
 
12
12
  ## Module map
@@ -58,7 +58,7 @@ The detector is a pluggable Provider: `Stripper` and `createStripper` accept an
58
58
 
59
59
  ## Storage domain
60
60
 
61
- The `dsh_mask` domain has one `restore` table keyed by session id. Its record holds `entries: { placeholder: original }` plus `updatedAt`. This is the only place plaintext PII is stored, and only when `persistRestoreTable: true`; the `Stripper` keeps a bounded in-memory copy (`maxRestoreEntriesPerSession`, `maxSessions` with LRU eviction).
61
+ The `dsh_mask` domain has one `restore` table keyed by session id. Its record holds `entries: { placeholder: original }` plus `updatedAt`. This is the only place plaintext PII is stored, and only when `persistRestoreTable: true`; the `Stripper` keeps a bounded in-memory copy (`maxRestoreEntriesPerSession`, `maxSessions` with LRU eviction). `storageDomain` is an optional service: the bundle patch inserts only the `mask` row, so a profile that already composes the storage stack (`web`, via `@deepseek-ai/dsh-web-app`) provides it, while a bare profile without it gets a memory-only restore table (`persistRestoreTable` is a no-op with a one-time warning).
62
62
 
63
63
  ## Safety boundaries
64
64
 
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project are 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
+ ## [0.2.1] - 2026-08-27
9
+
10
+ ### Fixed
11
+
12
+ - The bundle patch no longer inserts the storage stack (`@deepseek-ai/dsh-storage` / `dsh-storage-json` / `dsh-storage-domain`), which crashed `dsh web` with `duplicate loader entry id: storage` because `@deepseek-ai/dsh-web-app` already composes the same ids (issue #2). `storageDomain` is now an optional service: a profile that composes the storage stack provides it, while a bare profile without it degrades to a memory-only restore table with a one-time warning instead of hanging on `pending (waiting for service: storageDomain)`.
13
+
8
14
  ## [0.2.0] - 2026-08-26
9
15
 
10
16
  ### Added
package/README.es.md CHANGED
@@ -74,6 +74,8 @@ dsh --profile web --dump-config | grep -A2 'id: mask'
74
74
  - **Canal tarball**: `pnpm pack` y luego `dsh plugin --profile web add ./dsh-mask-<version>.tgz`.
75
75
  - **Desinstalar**: `dsh plugin --profile web remove dsh-mask`.
76
76
 
77
+ `dsh-mask` ya no incluye la pila de almacenamiento. Los perfiles que ya la componen (el perfil `web` lo hace, vía `@deepseek-ai/dsh-web-app`) aportan `storageDomain`, así que la persistencia funciona de inmediato. En un perfil bare sin almacenamiento el plugin se monta y enmascara igualmente, pero la tabla es solo en memoria (se pierde al reiniciar): compón la pila de almacenamiento en tu parche de perfil, o pon `persistRestoreTable: false`.
78
+
77
79
  ## Configuration
78
80
 
79
81
  Todas las opciones son campos Schemastery `Config` (modificables desde cordis.yml). `cordis.patch.yml` documenta cada clave.
package/README.hi.md CHANGED
@@ -74,6 +74,8 @@ dsh --profile web --dump-config | grep -A2 'id: mask'
74
74
  - **tarball चैनल**: `pnpm pack` फिर `dsh plugin --profile web add ./dsh-mask-<version>.tgz`।
75
75
  - **अनइंस्टॉल**: `dsh plugin --profile web remove dsh-mask`।
76
76
 
77
+ `dsh-mask` अब storage स्टैक बंडल नहीं करता। जो प्रोफ़ाइल इसे पहले से रचते हैं (`web` प्रोफ़ाइल `@deepseek-ai/dsh-web-app` के ज़रिए करती है) वे `storageDomain` देते हैं, इसलिए persistence तुरंत काम करता है। बिना storage वाले bare प्रोफ़ाइल में प्लगइन फिर भी माउंट होता है और मास्क करता है, पर restore तालिका केवल मेमोरी में रहती है (रीस्टार्ट पर खो जाती है): अपने प्रोफ़ाइल पैच में storage स्टैक रचें, या `persistRestoreTable: false` करें।
78
+
77
79
  ## Configuration
78
80
 
79
81
  सभी विकल्प Schemastery `Config` फ़ील्ड हैं (cordis.yml से बदले जा सकते हैं)। `cordis.patch.yml` हर कुंजी का दस्तावेज़ देता है।
package/README.md CHANGED
@@ -81,6 +81,8 @@ Then tailor the entity list in your profile patch:
81
81
  - **tarball channel**: `pnpm pack` in this repo, then `dsh plugin --profile web add ./dsh-mask-<version>.tgz`.
82
82
  - **uninstall**: `dsh plugin --profile web remove dsh-mask` (or remove the row from the profile patch).
83
83
 
84
+ `dsh-mask` no longer bundles the storage stack. Profiles that already compose it (the `web` profile does, via `@deepseek-ai/dsh-web-app`) provide `storageDomain`, so persistence works out of the box. On a bare profile without storage the plugin still mounts and masks, but the restore table is memory-only (lost on restart) — compose the storage stack in your profile patch, or set `persistRestoreTable: false`.
85
+
84
86
  ## Configuration
85
87
 
86
88
  All tunables are Schemastery `Config` fields (changeable from cordis.yml). An id-targeted override replaces the whole row — restate every key you need. `cordis.patch.yml` documents each key inline.
package/README.pt.md CHANGED
@@ -74,6 +74,8 @@ dsh --profile web --dump-config | grep -A2 'id: mask'
74
74
  - **Canal tarball**: `pnpm pack` e depois `dsh plugin --profile web add ./dsh-mask-<version>.tgz`.
75
75
  - **Desinstalar**: `dsh plugin --profile web remove dsh-mask`.
76
76
 
77
+ `dsh-mask` não inclui mais a pilha de armazenamento. Perfis que já a compõem (o perfil `web` o faz, via `@deepseek-ai/dsh-web-app`) fornecem `storageDomain`, então a persistência funciona imediatamente. Em um perfil bare sem armazenamento o plugin monta e mascara mesmo assim, mas a tabela é só em memória (perdida ao reiniciar): componha a pilha de armazenamento no seu patch de perfil, ou defina `persistRestoreTable: false`.
78
+
77
79
  ## Configuration
78
80
 
79
81
  Todas as opções são campos Schemastery `Config` (alteráveis via cordis.yml). O `cordis.patch.yml` documenta cada chave.
package/README.zh.md CHANGED
@@ -80,6 +80,8 @@ dsh --profile web --dump-config | grep -A2 'id: mask'
80
80
  - **tarball 通道**:在本仓库 `pnpm pack`,再 `dsh plugin --profile web add ./dsh-mask-<version>.tgz`。
81
81
  - **卸载**:`dsh plugin --profile web remove dsh-mask`(或从 profile patch 删掉该行)。
82
82
 
83
+ `dsh-mask` 不再自带 storage 栈。已组合该栈的 profile(`web` profile 通过 `@deepseek-ai/dsh-web-app` 提供)自带 `storageDomain`,持久化开箱即用。未组合 storage 的 bare profile 仍可正常挂载与遮罩,但恢复表仅存内存(重启丢失)——请在 profile patch 中组合 storage 栈,或将 `persistRestoreTable` 设为 `false`。
84
+
83
85
  ## Configuration
84
86
 
85
87
  所有可调项都是 Schemastery `Config` 字段(可从 cordis.yml 覆盖)。按 id 覆盖会替换整行——请重述所有需要的键。`cordis.patch.yml` 逐键内联注释。
package/cordis.patch.yml CHANGED
@@ -2,20 +2,16 @@
2
2
  #
3
3
  # Every key below is a Config field (Schemastery schema); invalid values fail
4
4
  # the profile load loudly. See README.md "Configuration" for the full table.
5
+ #
6
+ # This patch inserts only the `mask` row. It deliberately does NOT insert the
7
+ # storage stack (`storage` / `storage-json` / `storage-domain`): a profile that
8
+ # already composes it — the `web` profile does, via @deepseek-ai/dsh-web-app —
9
+ # would collide on the duplicate loader entry id `storage` and refuse to boot
10
+ # (issue #2). The plugin treats `storageDomain` as an optional service: when
11
+ # the profile provides it, the restore table persists to the `dsh_mask` domain;
12
+ # on a profile without storage, the restore table is memory-only and
13
+ # `persistRestoreTable: true` is a no-op with a one-time warning.
5
14
  - insert:
6
- - id: storage
7
- name: '@deepseek-ai/dsh-storage'
8
-
9
- - id: storage-json
10
- name: '@deepseek-ai/dsh-storage-json'
11
- config:
12
- root: !!js dshHomePath('storages')
13
-
14
- - id: storage-domain
15
- name: '@deepseek-ai/dsh-storage-domain'
16
- config:
17
- backend: json
18
-
19
15
  - id: mask
20
16
  name: dsh-mask
21
17
  config:
@@ -47,7 +43,9 @@
47
43
  registerCommand: true
48
44
  registerTools: true
49
45
  # Persist the placeholder->original restore table to the controlled
50
- # dsh_mask storage domain (false = memory only, lost on restart).
46
+ # dsh_mask storage domain (false = memory only, lost on restart). A
47
+ # no-op with a one-time warning when the profile composes no storage
48
+ # stack (storageDomain service absent).
51
49
  persistRestoreTable: true
52
50
  # Per-session cap on restore entries; the oldest placeholders are
53
51
  # evicted first (their responses can no longer be restored).
package/index.mjs CHANGED
@@ -12,8 +12,9 @@
12
12
  // - 表面:/mask 命令(status|on|off|restore|help)与 mask_test 工具(试跑一段
13
13
  // 文本看替换效果,绝不回显原文)。
14
14
  //
15
- // 只消费公开服务:commands/storageDomain(inject 声明),tools 经 ctx.inject
16
- // 可选注册;lib/ 零 DSH 依赖,服务只在边界接线。
15
+ // 只消费公开服务:commands(inject 声明)、storageDomain(ctx.get 可选,缺失时
16
+ // 恢复表降级为纯内存并一次性告警),tools 经 ctx.inject 可选注册;lib/ 零 DSH
17
+ // 依赖,服务只在边界接线。
17
18
 
18
19
  import { Context } from '@deepseek-ai/cordis'
19
20
  import Schema from '@deepseek-ai/schemastery'
@@ -41,8 +42,12 @@ import { dshMaskDomainSpec } from './lib/domain.mjs'
41
42
 
42
43
  export const name = PLUGIN_NAME
43
44
 
44
- /** 必需服务:缺失即加载失败(响亮)。 */
45
- export const inject = ['commands', 'storageDomain']
45
+ /**
46
+ * 必需服务:缺失即加载失败(响亮)。storageDomain 是可选服务(apply 内
47
+ * ctx.get 读取,缺失时恢复表降级为纯内存),不再作为 inject 硬依赖——否则
48
+ * bare profile 会卡在 `pending (waiting for service: storageDomain)`。
49
+ */
50
+ export const inject = ['commands']
46
51
 
47
52
  /**
48
53
  * 宿主 append 是否盖章 ignorable 信封(运行时能力探测)。
@@ -273,13 +278,21 @@ export function apply(ctx, config = {}) {
273
278
  const warn = (message) => logger.warn(message)
274
279
  const eventGate = makeEventGate(KNOWN_SESSION_EVENT_TYPES, probeIgnorableAppend())
275
280
 
276
- // --- 恢复表:ctx.storageDomain 领域 'dsh_mask'(异步打开,操作路径 await)。
277
- /** @type {Promise<any>} 打开的领域(含 table/close),RestoreStore 按需消费。 */
278
- const domainPromise = ctx.storageDomain.open(dshMaskDomainSpec).then((domain) => {
279
- ctx.effect(() => () => { void domain.close() }, `${PLUGIN_NAME}.domain.close`)
280
- return domain
281
- })
282
- domainPromise.catch(() => {}) // 消费方各自处理拒绝;此处仅避免未处理拒绝告警。
281
+ // --- 恢复表:可选 storageDomain 的 'dsh_mask' 领域(异步打开,操作路径 await)。
282
+ // storageDomain 缺失(bare profile 未组合存储栈)时降级为纯内存恢复表,
283
+ // persistRestoreTable 视为 no-op 并一次性告警(可选 seam 失败关闭,绝不卡 pending)。
284
+ const storageDomain = ctx.get('storageDomain')
285
+ /** @type {Promise<any>|null} 打开的领域(含 table/close),RestoreStore 按需消费。 */
286
+ let domainPromise = null
287
+ if (storageDomain !== undefined) {
288
+ domainPromise = storageDomain.open(dshMaskDomainSpec).then((domain) => {
289
+ ctx.effect(() => () => { void domain.close() }, `${PLUGIN_NAME}.domain.close`)
290
+ return domain
291
+ })
292
+ domainPromise.catch(() => {}) // 消费方各自处理拒绝;此处仅避免未处理拒绝告警。
293
+ } else if (resolved.persistRestoreTable) {
294
+ warn('storageDomain not composed: the restore table is memory-only (lost on restart). Compose the storage stack (storage / storage-json / storage-domain) in the profile, or set persistRestoreTable: false.')
295
+ }
283
296
 
284
297
  const store = new RestoreStore({
285
298
  entities: resolved.entities,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-mask",
3
3
  "description": "PII masking middleware for DeepSeek Harness: anonymize names, phones, emails, ID cards, bank cards, keys, and addresses to placeholders before they reach the model, restore them at the display layer, keep the restore table only in memory and a controlled storage domain, never log plaintext, and expose /mask and the mask_test tool",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/PerryLink/dsh-mask.git"
@@ -88,6 +88,7 @@
88
88
  },
89
89
  "peerDependencies": {
90
90
  "@deepseek-ai/cordis": "^4.0.1",
91
+ "@deepseek-ai/dsh-commands": "0.1.1-rc.2",
91
92
  "@deepseek-ai/dsh-session": ">=0.1.0-rc.8 <0.2.0",
92
93
  "@deepseek-ai/dsh-storage": ">=0.1.0-rc.8 <0.2.0",
93
94
  "@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.8 <0.2.0",
@@ -96,7 +97,6 @@
96
97
  "@deepseek-ai/schemastery": "^3.18.0"
97
98
  },
98
99
  "dependencies": {
99
- "typescript": "^5.9.0",
100
100
  "zod": "^4.4.3"
101
101
  },
102
102
  "devDependencies": {
@@ -113,7 +113,8 @@
113
113
  "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
114
114
  "@deepseek-ai/schemastery": "^3.18.0",
115
115
  "@types/node": "^22.19.0",
116
- "oxlint": "0.18.1"
116
+ "oxlint": "0.18.1",
117
+ "typescript": "^5.9.0"
117
118
  },
118
119
  "scripts": {
119
120
  "typecheck": "tsc -p tsconfig.check.json",