dsh-mask 0.1.3 → 0.2.0
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 +8 -4
- package/CHANGELOG.md +14 -0
- package/LICENSE +72 -66
- package/README.es.md +5 -1
- package/README.hi.md +5 -1
- package/README.md +8 -2
- package/README.pt.md +5 -1
- package/README.zh.md +5 -1
- package/cordis.patch.yml +12 -4
- package/index.mjs +79 -29
- package/lib/constants.mjs +6 -3
- package/lib/errors.mjs +2 -2
- package/lib/mask.mjs +25 -0
- package/lib/strip.mjs +34 -19
- package/package.json +1 -1
- package/types.d.ts +7 -2
package/ARCHITECTURE.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| `index.mjs` | The single host face: `Config` schema + `resolveConfig`, `probeIgnorableAppend`, the `agent/pre-step` masking listener, the `/mask` command handler, the `mask_test` tool factory, `apply()` |
|
|
17
17
|
| `lib/constants.mjs` | Vocabulary + protocol constants (entity names/labels, modes, scopes, error codes, defaults, bounds) — zero dependency |
|
|
18
18
|
| `lib/errors.mjs` | Structured domain errors (`MaskError` with stable `code` + `details`) |
|
|
19
|
-
| `lib/strip.mjs` | The regex PII detector
|
|
19
|
+
| `lib/strip.mjs` | The regex PII detector (`regexDetect`), the pluggable-detector `Stripper` (strip/stripInto/restore/mapping/stats/loadMapping), and `createStripper` — zero dependency |
|
|
20
20
|
| `lib/mask.mjs` | Message masking: rewrite `UserMessage` text blocks through a `Stripper` — zero dependency |
|
|
21
21
|
| `lib/sanitize.mjs` | Pure display/log redaction (PII entities, secrets, URL credentials, mapping summarization) |
|
|
22
22
|
| `lib/gate.mjs` | Session-event adaptive gate (append only when the host records the type or supports `ignorable`) |
|
|
@@ -48,6 +48,10 @@ Ported from Pii-Stripper-Middleware's `core.py`:
|
|
|
48
48
|
- Overlap resolution sorts by start ascending, then score descending, and keeps only non-overlapping spans (first-come, higher score wins at the same position) — an 18-digit ID card beats a 16–19-digit bank-card match.
|
|
49
49
|
- Same original value reuses the same placeholder; the counter is monotonic per session so placeholders never collide across turns.
|
|
50
50
|
|
|
51
|
+
## Detector Provider seam
|
|
52
|
+
|
|
53
|
+
The detector is a pluggable Provider: `Stripper` and `createStripper` accept an optional `detector: (text) => PIIEntity[]`. When omitted, the built-in `regexDetect` (the ported regex set) runs unchanged, so the zero-dependency default path is untouched. An external recognizer (e.g. NER for `person`/`address`) plugs in by supplying that one function; overlap resolution, placeholder reuse, counting, and restore all stay shared.
|
|
54
|
+
|
|
51
55
|
## Session events (adaptive gate)
|
|
52
56
|
|
|
53
57
|
`mask/applied` is declared through `SessionEventMap` declaration merging in `types.d.ts`. At runtime the plugin appends it only when either (a) the host's `KNOWN_SESSION_EVENT_TYPES` already includes the type, or (b) the host `Session.append` supports the `ignorable` envelope (`probeIgnorableAppend`). On `0.1.1-rc.2` neither is true (verified: rc.2 append reads only `surfaceOp`/`sourceEventSeqs` and never stamps `ignorable`), so the gate stays closed and appends are skipped — sessions keep loading. The audit payload is counts + type distribution only, never plaintext or the mapping.
|
|
@@ -65,6 +69,6 @@ The `dsh_mask` domain has one `restore` table keyed by session id. Its record ho
|
|
|
65
69
|
|
|
66
70
|
## Reserved seams
|
|
67
71
|
|
|
68
|
-
- `mode: regex+ner` — external name/address recognition; fails loudly until a recognizer is wired in.
|
|
69
|
-
- `scope: tools` — masking
|
|
70
|
-
- A browser half would consume the restore table to transparently un-mask assistant bubbles;
|
|
72
|
+
- `mode: regex+ner` — external name/address recognition; fails loudly until a recognizer is wired in (the detector Provider seam above is the plug point).
|
|
73
|
+
- `scope: tools` — implemented as `tools/post-execute` result-content masking. Tool-argument rewriting is deliberately NOT offered upstream: `tools/pre-execute`'s `PreToolDecision` has no input rewrite because logged/rendered arguments must match what ran, so `tools` scope masks the other model-visible tool surface (the result content) instead of the arguments.
|
|
74
|
+
- A browser half would consume the restore table to transparently un-mask assistant bubbles; the host-side restore surface (`/mask restore` + `RestoreStore.restore`) ships, and the browser slot is feature-flagged behind `maskClientEnabled` (default false) pending live slot-catalog verification.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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.0] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`tools` masking scope.** `scope` now accepts `'messages'` and/or `'tools'` (string or array, default `['messages']`). The `tools` surface masks tool-result text blocks on `tools/post-execute` before they are logged and fed back to the model. Tool-argument rewriting stays out of scope because the upstream `tools/pre-execute` `PreToolDecision` deliberately offers no input rewrite (logged/rendered arguments must match what ran).
|
|
13
|
+
- **Detector Provider seam.** `Stripper`/`createStripper` accept an optional `detector: (text) => PIIEntity[]`; the built-in `regexDetect` remains the zero-dependency default, unchanged, so an external NER recognizer can plug in later without touching the masking pipeline.
|
|
14
|
+
- **`maskClientEnabled` feature flag** (default `false`) for the future browser-half reveal bubble; the host restore surface (`/mask restore` + `RestoreStore.restore`) already backs it.
|
|
15
|
+
|
|
16
|
+
## [0.1.4] - 2026-08-23
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Add the `[Unreleased]` Keep-a-Changelog section and refresh the repo-local development notes (`AGENTS.md`) to match the current `0.1.3` release.
|
|
21
|
+
|
|
8
22
|
## [0.1.3] - 2026-08-22
|
|
9
23
|
|
|
10
24
|
### Changed
|
package/LICENSE
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
Copyright 2026 PerryLink
|
|
2
|
-
|
|
3
1
|
Apache License
|
|
4
2
|
Version 2.0, January 2004
|
|
5
3
|
http://www.apache.org/licenses/
|
|
@@ -34,9 +32,10 @@ Copyright 2026 PerryLink
|
|
|
34
32
|
not limited to compiled object code, generated documentation,
|
|
35
33
|
and conversions to other media types.
|
|
36
34
|
|
|
37
|
-
"Work" shall mean the work of authorship
|
|
38
|
-
the License, as indicated by a
|
|
39
|
-
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
40
39
|
|
|
41
40
|
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
41
|
form, that is based on (or derived from) the Work and for which the
|
|
@@ -46,21 +45,23 @@ Copyright 2026 PerryLink
|
|
|
46
45
|
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
46
|
the Work and Derivative Works thereof.
|
|
48
47
|
|
|
49
|
-
"Contribution" shall mean
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
or
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
64
65
|
|
|
65
66
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
66
67
|
this License, each Contributor hereby grants to You a perpetual,
|
|
@@ -78,47 +79,53 @@ Copyright 2026 PerryLink
|
|
|
78
79
|
by such Contributor that are necessarily infringed by their
|
|
79
80
|
Contribution(s) alone or by combination of their Contribution(s)
|
|
80
81
|
with the Work to which such Contribution(s) was submitted. If You
|
|
81
|
-
institute patent litigation against any entity (including a
|
|
82
|
-
or counterclaim in a lawsuit) alleging that the Work
|
|
83
|
-
incorporated within the Work constitutes direct
|
|
84
|
-
infringement, then any patent licenses
|
|
85
|
-
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
86
88
|
|
|
87
89
|
4. Redistribution. You may reproduce and distribute copies of the
|
|
88
90
|
Work or Derivative Works thereof in any medium, with or without
|
|
89
91
|
modifications, and in Source or Object form, provided that You
|
|
90
92
|
meet the following conditions:
|
|
91
93
|
|
|
92
|
-
(a) You must give any other recipients of the Work or
|
|
93
|
-
a copy of this License; and
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
94
96
|
|
|
95
97
|
(b) You must cause any modified files to carry prominent notices
|
|
96
98
|
stating that You changed the files; and
|
|
97
99
|
|
|
98
100
|
(c) You must retain, in the Source form of any Derivative Works
|
|
99
101
|
that You distribute, all copyright, patent, trademark, and
|
|
100
|
-
attribution notices from the Source form of the Work,
|
|
101
|
-
those notices that do not pertain to any part of
|
|
102
|
-
Works; and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
103
105
|
|
|
104
106
|
(d) If the Work includes a "NOTICE" text file as part of its
|
|
105
|
-
distribution,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
122
129
|
|
|
123
130
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
124
131
|
any Contribution intentionally submitted for inclusion in the Work
|
|
@@ -138,9 +145,8 @@ Copyright 2026 PerryLink
|
|
|
138
145
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
139
146
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
140
147
|
implied, including, without limitation, any warranties or conditions
|
|
141
|
-
of TITLE, MERCHANTABILITY,
|
|
142
|
-
|
|
143
|
-
conditions. You are solely responsible for determining the
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
144
150
|
appropriateness of using or redistributing the Work and assume any
|
|
145
151
|
risks associated with Your exercise of permissions under this License.
|
|
146
152
|
|
|
@@ -149,23 +155,23 @@ Copyright 2026 PerryLink
|
|
|
149
155
|
unless required by applicable law (such as deliberate and grossly
|
|
150
156
|
negligent acts) or agreed to in writing, shall any Contributor be
|
|
151
157
|
liable to You for damages, including any direct, indirect, special,
|
|
152
|
-
incidental, or
|
|
153
|
-
of this License or out of the use or inability to use the
|
|
154
|
-
(including but not limited to damages for loss of goodwill,
|
|
155
|
-
stoppage, computer failure or malfunction, or
|
|
156
|
-
damages or losses), even if such Contributor
|
|
157
|
-
possibility of such damages.
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
158
164
|
|
|
159
165
|
9. Accepting Warranty or Additional Liability. While redistributing
|
|
160
|
-
the Work or Derivative Works thereof, You may choose to offer,
|
|
161
|
-
charge a fee for, acceptance of support, warranty, indemnity,
|
|
162
|
-
other liability obligations and/or rights consistent with this
|
|
163
|
-
However, in accepting such obligations, You may
|
|
164
|
-
|
|
165
|
-
of any other Contributor, and only if You agree to indemnify,
|
|
166
|
-
and hold each Contributor harmless for any liability
|
|
167
|
-
claims asserted against, such Contributor by reason
|
|
168
|
-
any such warranty or additional liability.
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
169
175
|
|
|
170
176
|
END OF TERMS AND CONDITIONS
|
|
171
177
|
|
|
@@ -180,7 +186,7 @@ Copyright 2026 PerryLink
|
|
|
180
186
|
same "printed page" as the copyright notice for easier
|
|
181
187
|
identification within third-party archives.
|
|
182
188
|
|
|
183
|
-
Copyright
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
184
190
|
|
|
185
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
186
192
|
you may not use this file except in compliance with the License.
|
package/README.es.md
CHANGED
|
@@ -83,7 +83,7 @@ Todas las opciones son campos Schemastery `Config` (modificables desde cordis.ym
|
|
|
83
83
|
| `enabled` | `true` | Interruptor maestro |
|
|
84
84
|
| `mode` | `regex` | Solo `regex` implementado (`regex+ner` reservado) |
|
|
85
85
|
| `entities` | `[phone, email, id-card, bank-card, key]` | Tipos de PII a enmascarar; `ip` opt-in, `person`/`address` requieren NER |
|
|
86
|
-
| `scope` | `messages` |
|
|
86
|
+
| `scope` | `[messages]` | Superficies: `messages` (mensajes agent/pre-step) y `tools` (texto de resultados de herramientas). Acepta una cadena o un array, p. ej. `[messages, tools]` |
|
|
87
87
|
| `registerCommand` | `true` | Registra el comando `/mask` |
|
|
88
88
|
| `registerTools` | `true` | Registra la herramienta `mask_test` |
|
|
89
89
|
| `persistRestoreTable` | `true` | Persiste la tabla en el dominio `dsh_mask` |
|
|
@@ -134,6 +134,10 @@ pnpm pack
|
|
|
134
134
|
|
|
135
135
|
Sin paso de build: ESM puro, `index.mjs` y `lib/` son los artefactos enviados.
|
|
136
136
|
|
|
137
|
+
### Benchmark
|
|
138
|
+
|
|
139
|
+
El benchmark de PII (P/R/F1 por tipo sobre 108 muestras sintéticas) está en [`benchmark/RESULTS.md`](benchmark/RESULTS.md); regenéralo con `node benchmark/run.mjs` (sin build, cero dependencias nuevas).
|
|
140
|
+
|
|
137
141
|
## Topics
|
|
138
142
|
|
|
139
143
|
`dsh`, `dsh-plugin`, `deepseek-harness`, `deepseek`, `cordis`, `pii`, `mask`, `privacy`, `anonymization`, `security`
|
package/README.hi.md
CHANGED
|
@@ -83,7 +83,7 @@ dsh --profile web --dump-config | grep -A2 'id: mask'
|
|
|
83
83
|
| `enabled` | `true` | मुख्य स्विच |
|
|
84
84
|
| `mode` | `regex` | केवल `regex` लागू (`regex+ner` आरक्षित) |
|
|
85
85
|
| `entities` | `[phone, email, id-card, bank-card, key]` | PII प्रकार; `ip` opt-in, `person`/`address` को NER चाहिए |
|
|
86
|
-
| `scope` | `messages` |
|
|
86
|
+
| `scope` | `[messages]` | सतहें: `messages` (agent/pre-step संदेश) और `tools` (टूल परिणाम पाठ)। स्ट्रिंग या array स्वीकार करता है, जैसे `[messages, tools]` |
|
|
87
87
|
| `registerCommand` | `true` | `/mask` कमांड पंजीकृत करें |
|
|
88
88
|
| `registerTools` | `true` | `mask_test` टूल पंजीकृत करें |
|
|
89
89
|
| `persistRestoreTable` | `true` | तालिका को `dsh_mask` डोमेन में सहेजें |
|
|
@@ -134,6 +134,10 @@ pnpm pack
|
|
|
134
134
|
|
|
135
135
|
कोई build चरण नहीं: शुद्ध ESM, `index.mjs` और `lib/` ही भेजे गए आर्टिफ़ैक्ट हैं।
|
|
136
136
|
|
|
137
|
+
### Benchmark
|
|
138
|
+
|
|
139
|
+
PII बेंचमार्क (108 सिंथेटिक नमूनों पर प्रति-प्रकार P/R/F1) [`benchmark/RESULTS.md`](benchmark/RESULTS.md) में है; `node benchmark/run.mjs` से दोबारा बनाएँ (कोई build नहीं, कोई नई निर्भरता नहीं)।
|
|
140
|
+
|
|
137
141
|
## Topics
|
|
138
142
|
|
|
139
143
|
`dsh`, `dsh-plugin`, `deepseek-harness`, `deepseek`, `cordis`, `pii`, `mask`, `privacy`, `anonymization`, `security`
|
package/README.md
CHANGED
|
@@ -90,12 +90,13 @@ All tunables are Schemastery `Config` fields (changeable from cordis.yml). An id
|
|
|
90
90
|
| `enabled` | `true` | Master switch; `false` unregisters the listener, the `/mask` command, and the `mask_test` tool |
|
|
91
91
|
| `mode` | `regex` | Detection mode; only `regex` is implemented (`regex+ner` for name/address recognition is reserved and fails loud) |
|
|
92
92
|
| `entities` | `[phone, email, id-card, bank-card, key]` | Which PII types to mask; `ip` is also regex-capable (opt-in), `person`/`address` require NER |
|
|
93
|
-
| `scope` | `messages` | Masking surface;
|
|
93
|
+
| `scope` | `[messages]` | Masking surface(s); `messages` masks agent/pre-step messages, `tools` masks tool-result text on tools/post-execute. Accepts a string or an array, e.g. `[messages, tools]` |
|
|
94
94
|
| `registerCommand` | `true` | Register the `/mask` command |
|
|
95
95
|
| `registerTools` | `true` | Register the `mask_test` tool when the tools service is present |
|
|
96
96
|
| `persistRestoreTable` | `true` | Persist the restore table to the controlled `dsh_mask` storage domain (`false` = memory only) |
|
|
97
97
|
| `maxRestoreEntriesPerSession` | `500` | Per-session restore entry cap (oldest evicted first) |
|
|
98
98
|
| `maxSessions` | `1000` | In-memory session cap (least-recently-used evicted, mapping reloaded on demand) |
|
|
99
|
+
| `maskClientEnabled` | `false` | Feature flag for the browser half "reveal" bubble (defensive; off by default until the live slot catalog verifies the target slot) |
|
|
99
100
|
|
|
100
101
|
Example override in your profile patch:
|
|
101
102
|
|
|
@@ -114,6 +115,7 @@ Example override in your profile patch:
|
|
|
114
115
|
| Surface | Reveals plaintext | Notes |
|
|
115
116
|
|---|---|---|
|
|
116
117
|
| `agent/pre-step` masking | never | Rewrites messages to placeholders before they are logged or sent to the model |
|
|
118
|
+
| `tools/post-execute` masking | never | Rewrites tool-result text blocks to placeholders before they are logged or fed back to the model (scope: `tools`) |
|
|
117
119
|
| `/mask status` | never | Enabled state, total replaced, type distribution |
|
|
118
120
|
| `/mask on` / `/mask off` | never | Runtime toggle (resets to `config.enabled` on restart) |
|
|
119
121
|
| `/mask restore <text>` | yes (explicit) | Unmaps placeholders back to the values stored for this session |
|
|
@@ -130,7 +132,7 @@ Example override in your profile patch:
|
|
|
130
132
|
- **Plaintext never enters the session log.** The masked (placeholder) form is what gets logged and sent to the model, so model-visible content is reconstructable from the log in placeholder form; the originals stay in the restore table.
|
|
131
133
|
- **Sanitize before display/log.** `lib/sanitize.mjs` redacts PII, secrets, and URL credentials before any text reaches the model or the log; `mask_test` and `/mask status` never echo originals.
|
|
132
134
|
- **Controlled restore.** `/mask restore` is the single explicit reveal surface, and it only reads the mapping for the active session.
|
|
133
|
-
- **Fail closed.** Unimplemented `mode` (`regex+ner`), `scope`
|
|
135
|
+
- **Fail closed.** Unimplemented `mode` (`regex+ner`), unknown `scope` values, NER-only entities, and out-of-bounds numbers all fail loudly at load.
|
|
134
136
|
- **Registrations are effects.** The listener, command, tool, and storage-domain close are all Cordis effects — stop/hot-reload removes them.
|
|
135
137
|
|
|
136
138
|
## Known limitations
|
|
@@ -153,6 +155,10 @@ pnpm pack # the published tarball
|
|
|
153
155
|
|
|
154
156
|
There is no build step: pure ESM, `index.mjs` and `lib/` are the shipped artifacts.
|
|
155
157
|
|
|
158
|
+
### Benchmark
|
|
159
|
+
|
|
160
|
+
The PII benchmark (per-type P/R/F1 over 108 synthetic samples) is published in [`benchmark/RESULTS.md`](benchmark/RESULTS.md); regenerate it with `node benchmark/run.mjs` (no build step, zero new dependencies).
|
|
161
|
+
|
|
156
162
|
## Topics
|
|
157
163
|
|
|
158
164
|
`dsh`, `dsh-plugin`, `deepseek-harness`, `deepseek`, `cordis`, `pii`, `mask`, `privacy`, `anonymization`, `security`
|
package/README.pt.md
CHANGED
|
@@ -83,7 +83,7 @@ Todas as opções são campos Schemastery `Config` (alteráveis via cordis.yml).
|
|
|
83
83
|
| `enabled` | `true` | Interruptor mestre |
|
|
84
84
|
| `mode` | `regex` | Só `regex` implementado (`regex+ner` reservado) |
|
|
85
85
|
| `entities` | `[phone, email, id-card, bank-card, key]` | Tipos de PII; `ip` opt-in, `person`/`address` exigem NER |
|
|
86
|
-
| `scope` | `messages` |
|
|
86
|
+
| `scope` | `[messages]` | Superfícies: `messages` (mensagens agent/pre-step) e `tools` (texto de resultados de ferramentas). Aceita string ou array, ex. `[messages, tools]` |
|
|
87
87
|
| `registerCommand` | `true` | Registra o comando `/mask` |
|
|
88
88
|
| `registerTools` | `true` | Registra a ferramenta `mask_test` |
|
|
89
89
|
| `persistRestoreTable` | `true` | Persiste a tabela no domínio `dsh_mask` |
|
|
@@ -134,6 +134,10 @@ pnpm pack
|
|
|
134
134
|
|
|
135
135
|
Sem etapa de build: ESM puro, `index.mjs` e `lib/` são os artefatos enviados.
|
|
136
136
|
|
|
137
|
+
### Benchmark
|
|
138
|
+
|
|
139
|
+
O benchmark de PII (P/R/F1 por tipo em 108 amostras sintéticas) está em [`benchmark/RESULTS.md`](benchmark/RESULTS.md); regenere-o com `node benchmark/run.mjs` (sem build, zero dependências novas).
|
|
140
|
+
|
|
137
141
|
## Topics
|
|
138
142
|
|
|
139
143
|
`dsh`, `dsh-plugin`, `deepseek-harness`, `deepseek`, `cordis`, `pii`, `mask`, `privacy`, `anonymization`, `security`
|
package/README.zh.md
CHANGED
|
@@ -89,7 +89,7 @@ dsh --profile web --dump-config | grep -A2 'id: mask'
|
|
|
89
89
|
| `enabled` | `true` | 总开关;`false` 卸载监听器、`/mask` 命令与 `mask_test` 工具 |
|
|
90
90
|
| `mode` | `regex` | 检测模式;只有 `regex` 实现(`regex+ner` 姓名/地址识别预留并响亮失败) |
|
|
91
91
|
| `entities` | `[phone, email, id-card, bank-card, key]` | 要遮罩的 PII 类型;`ip` 也支持正则(可选),`person`/`address` 需要 NER |
|
|
92
|
-
| `scope` | `messages` |
|
|
92
|
+
| `scope` | `[messages]` | 遮罩作用域;`messages` 遮罩 agent/pre-step 消息,`tools` 遮罩 tools/post-execute 工具结果。可为字符串或数组,如 `[messages, tools]` |
|
|
93
93
|
| `registerCommand` | `true` | 注册 `/mask` 命令 |
|
|
94
94
|
| `registerTools` | `true` | tools 服务存在时注册 `mask_test` 工具 |
|
|
95
95
|
| `persistRestoreTable` | `true` | 把恢复表持久化到受控 `dsh_mask` 领域(`false` = 仅内存) |
|
|
@@ -152,6 +152,10 @@ pnpm pack # 发布 tarball
|
|
|
152
152
|
|
|
153
153
|
无构建步骤:纯 ESM,`index.mjs` 与 `lib/` 即发布产物。
|
|
154
154
|
|
|
155
|
+
### Benchmark
|
|
156
|
+
|
|
157
|
+
PII 基准(108 个合成样本的逐类 P/R/F1)见 [`benchmark/RESULTS.md`](benchmark/RESULTS.md);用 `node benchmark/run.mjs` 复现(无需构建、零新依赖)。
|
|
158
|
+
|
|
155
159
|
## Topics
|
|
156
160
|
|
|
157
161
|
`dsh`, `dsh-plugin`, `deepseek-harness`, `deepseek`, `cordis`, `pii`, `mask`, `privacy`, `anonymization`, `security`
|
package/cordis.patch.yml
CHANGED
|
@@ -36,10 +36,13 @@
|
|
|
36
36
|
- id-card
|
|
37
37
|
- bank-card
|
|
38
38
|
- key
|
|
39
|
-
# Masking surface.
|
|
40
|
-
# they enter the model
|
|
41
|
-
#
|
|
42
|
-
|
|
39
|
+
# Masking surface(s). 'messages' masks agent/pre-step messages before
|
|
40
|
+
# they enter the model; 'tools' masks tool-result text on
|
|
41
|
+
# tools/post-execute before it is logged and fed back to the model
|
|
42
|
+
# (tool-argument rewriting is blocked upstream — see ARCHITECTURE.md).
|
|
43
|
+
# Accepts a single string or an array: [messages, tools].
|
|
44
|
+
scope:
|
|
45
|
+
- messages
|
|
43
46
|
# Surface switches: register the /mask command and the mask_test tool.
|
|
44
47
|
registerCommand: true
|
|
45
48
|
registerTools: true
|
|
@@ -52,3 +55,8 @@
|
|
|
52
55
|
# Cap on in-memory session strippers; least-recently-used sessions are
|
|
53
56
|
# evicted (mapping reloads from the storage domain on demand).
|
|
54
57
|
maxSessions: 1000
|
|
58
|
+
# Browser half "reveal" bubble (defensive, feature-flagged). The host
|
|
59
|
+
# restore surface (/mask restore + the in-memory/domain restore table)
|
|
60
|
+
# already backs it; the client slot ships separately and is off by
|
|
61
|
+
# default until the live slot catalog verifies the target slot.
|
|
62
|
+
maskClientEnabled: false
|
package/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from './lib/constants.mjs'
|
|
35
35
|
import { badConfig, messageOf, nerModeUnsupported, nerUnsupported, scopeUnsupported } from './lib/errors.mjs'
|
|
36
36
|
import { createStripper } from './lib/strip.mjs'
|
|
37
|
-
import { maskMessages } from './lib/mask.mjs'
|
|
37
|
+
import { maskMessages, maskContentBlocks } from './lib/mask.mjs'
|
|
38
38
|
import { makeEventGate, maybeAppendSessionEvent } from './lib/gate.mjs'
|
|
39
39
|
import { RestoreStore } from './lib/store.mjs'
|
|
40
40
|
import { dshMaskDomainSpec } from './lib/domain.mjs'
|
|
@@ -78,12 +78,16 @@ export const Config = Schema.object({
|
|
|
78
78
|
enabled: Schema.boolean().default(DEFAULTS.ENABLED),
|
|
79
79
|
mode: Schema.union([MODES.REGEX, MODES.REGEX_NER]).default(DEFAULTS.MODE),
|
|
80
80
|
entities: Schema.array(Schema.string()).default(DEFAULTS.ENTITIES),
|
|
81
|
-
scope: Schema.union([
|
|
81
|
+
scope: Schema.union([
|
|
82
|
+
Schema.array(Schema.union([SCOPES.MESSAGES, SCOPES.TOOLS])),
|
|
83
|
+
Schema.union([SCOPES.MESSAGES, SCOPES.TOOLS]),
|
|
84
|
+
]).default(DEFAULTS.SCOPE),
|
|
82
85
|
registerCommand: Schema.boolean().default(DEFAULTS.REGISTER_COMMAND),
|
|
83
86
|
registerTools: Schema.boolean().default(DEFAULTS.REGISTER_TOOLS),
|
|
84
87
|
persistRestoreTable: Schema.boolean().default(DEFAULTS.PERSIST_RESTORE_TABLE),
|
|
85
88
|
maxRestoreEntriesPerSession: Schema.number().default(DEFAULTS.MAX_RESTORE_ENTRIES_PER_SESSION),
|
|
86
89
|
maxSessions: Schema.number().default(DEFAULTS.MAX_SESSIONS),
|
|
90
|
+
maskClientEnabled: Schema.boolean().default(DEFAULTS.MASK_CLIENT_ENABLED),
|
|
87
91
|
})
|
|
88
92
|
|
|
89
93
|
/**
|
|
@@ -92,16 +96,18 @@ export const Config = Schema.object({
|
|
|
92
96
|
* @returns {Required<Config> & {entities: string[]}} 校验后的配置。
|
|
93
97
|
*/
|
|
94
98
|
export function resolveConfig(config = {}) {
|
|
99
|
+
const rawScope = config.scope ?? DEFAULTS.SCOPE
|
|
95
100
|
const resolved = {
|
|
96
101
|
enabled: config.enabled ?? DEFAULTS.ENABLED,
|
|
97
102
|
mode: config.mode ?? DEFAULTS.MODE,
|
|
98
103
|
entities: [...(config.entities ?? DEFAULTS.ENTITIES)],
|
|
99
|
-
scope:
|
|
104
|
+
scope: typeof rawScope === 'string' ? [rawScope] : [...rawScope],
|
|
100
105
|
registerCommand: config.registerCommand ?? DEFAULTS.REGISTER_COMMAND,
|
|
101
106
|
registerTools: config.registerTools ?? DEFAULTS.REGISTER_TOOLS,
|
|
102
107
|
persistRestoreTable: config.persistRestoreTable ?? DEFAULTS.PERSIST_RESTORE_TABLE,
|
|
103
108
|
maxRestoreEntriesPerSession: config.maxRestoreEntriesPerSession ?? DEFAULTS.MAX_RESTORE_ENTRIES_PER_SESSION,
|
|
104
109
|
maxSessions: config.maxSessions ?? DEFAULTS.MAX_SESSIONS,
|
|
110
|
+
maskClientEnabled: config.maskClientEnabled ?? DEFAULTS.MASK_CLIENT_ENABLED,
|
|
105
111
|
}
|
|
106
112
|
if (resolved.enabled === false) return resolved
|
|
107
113
|
|
|
@@ -111,8 +117,19 @@ export function resolveConfig(config = {}) {
|
|
|
111
117
|
if (resolved.mode !== MODES.REGEX) {
|
|
112
118
|
throw badConfig(`mode ${JSON.stringify(resolved.mode)} must be one of regex|regex+ner`)
|
|
113
119
|
}
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
const scopeSeen = new Set()
|
|
121
|
+
resolved.scope = resolved.scope.filter((surface) => {
|
|
122
|
+
if (scopeSeen.has(surface)) return false
|
|
123
|
+
scopeSeen.add(surface)
|
|
124
|
+
return true
|
|
125
|
+
})
|
|
126
|
+
if (resolved.scope.length === 0) {
|
|
127
|
+
throw badConfig('scope must list at least one surface (messages and/or tools)')
|
|
128
|
+
}
|
|
129
|
+
for (const surface of resolved.scope) {
|
|
130
|
+
if (surface !== SCOPES.MESSAGES && surface !== SCOPES.TOOLS) {
|
|
131
|
+
throw scopeUnsupported(surface)
|
|
132
|
+
}
|
|
116
133
|
}
|
|
117
134
|
const seen = new Set()
|
|
118
135
|
resolved.entities = resolved.entities.filter((entity) => {
|
|
@@ -277,30 +294,62 @@ export function apply(ctx, config = {}) {
|
|
|
277
294
|
let runtimeEnabled = true
|
|
278
295
|
|
|
279
296
|
// --- agent/pre-step 遮罩(waterfall:先 next() 取下游决策,再遮罩其消息)。
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
297
|
+
if (resolved.scope.includes(SCOPES.MESSAGES)) {
|
|
298
|
+
ctx.on('agent/pre-step', async ({ agent, messages }, next) => {
|
|
299
|
+
if (!runtimeEnabled) return next()
|
|
300
|
+
const decision = await next()
|
|
301
|
+
if (decision.kind !== 'enter') return decision
|
|
302
|
+
const sessionId = agent?.session?.id
|
|
303
|
+
if (sessionId === undefined || sessionId === null || sessionId === '') return decision
|
|
304
|
+
const stripper = store.stripperFor(sessionId)
|
|
305
|
+
const before = stripper.stats()
|
|
306
|
+
const { messages: maskedMessages, replaced } = maskMessages(decision.messages, stripper)
|
|
307
|
+
if (replaced === 0) return decision
|
|
308
|
+
const after = stripper.stats()
|
|
309
|
+
const distribution = {}
|
|
310
|
+
for (const [label, count] of Object.entries(after.distribution)) {
|
|
311
|
+
const delta = count - (before.distribution[label] ?? 0)
|
|
312
|
+
if (delta > 0) distribution[label] = delta
|
|
313
|
+
}
|
|
314
|
+
void store.persist(sessionId)
|
|
315
|
+
maybeAppendSessionEvent(agent.session, SESSION_EVENTS.APPLIED, {
|
|
316
|
+
sessionId,
|
|
317
|
+
replaced,
|
|
318
|
+
distribution,
|
|
319
|
+
}, eventGate, warn)
|
|
320
|
+
return { kind: 'enter', messages: maskedMessages }
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- tools/post-execute 遮罩(scope: tools):工具结果 text 块里的 PII 在回喂模型
|
|
325
|
+
// 与落盘前脱敏为占位符。工具入参本身无法在 pre-execute 改写(上游契约),故 tools
|
|
326
|
+
// 作用域落在结果这一可改写的模型可见面(见 ARCHITECTURE.md)。
|
|
327
|
+
if (resolved.scope.includes(SCOPES.TOOLS)) {
|
|
328
|
+
ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
329
|
+
if (!runtimeEnabled) return next()
|
|
330
|
+
const decision = await next()
|
|
331
|
+
if (decision.kind !== 'accept') return decision
|
|
332
|
+
const sessionId = exec?.agent?.session?.id
|
|
333
|
+
if (sessionId === undefined || sessionId === null || sessionId === '') return decision
|
|
334
|
+
const stripper = store.stripperFor(sessionId)
|
|
335
|
+
const before = stripper.stats()
|
|
336
|
+
const { blocks: maskedBlocks, replaced } = maskContentBlocks(result.content, stripper)
|
|
337
|
+
if (replaced === 0) return decision
|
|
338
|
+
const after = stripper.stats()
|
|
339
|
+
const distribution = {}
|
|
340
|
+
for (const [label, count] of Object.entries(after.distribution)) {
|
|
341
|
+
const delta = count - (before.distribution[label] ?? 0)
|
|
342
|
+
if (delta > 0) distribution[label] = delta
|
|
343
|
+
}
|
|
344
|
+
void store.persist(sessionId)
|
|
345
|
+
maybeAppendSessionEvent(exec.agent?.session, SESSION_EVENTS.APPLIED, {
|
|
346
|
+
sessionId,
|
|
347
|
+
replaced,
|
|
348
|
+
distribution,
|
|
349
|
+
}, eventGate, warn)
|
|
350
|
+
return { kind: 'accept', content: maskedBlocks }
|
|
351
|
+
})
|
|
352
|
+
}
|
|
304
353
|
|
|
305
354
|
// --- /mask 命令(Consumer)。
|
|
306
355
|
if (resolved.registerCommand) {
|
|
@@ -373,6 +422,7 @@ export {
|
|
|
373
422
|
RestoreStore,
|
|
374
423
|
createStripper,
|
|
375
424
|
maskMessages,
|
|
425
|
+
maskContentBlocks,
|
|
376
426
|
makeEventGate,
|
|
377
427
|
maybeAppendSessionEvent,
|
|
378
428
|
dshMaskDomainSpec,
|
package/lib/constants.mjs
CHANGED
|
@@ -46,10 +46,12 @@ export const MODES = Object.freeze({
|
|
|
46
46
|
REGEX_NER: 'regex+ner',
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
// 作用域词汇:messages
|
|
50
|
-
// tools
|
|
49
|
+
// 作用域词汇:messages = agent/pre-step 入站消息遮罩;
|
|
50
|
+
// tools = tools/post-execute 工具结果遮罩(工具入参改写被上游
|
|
51
|
+
// tools/pre-execute 契约禁止,见 ARCHITECTURE.md)。
|
|
51
52
|
export const SCOPES = Object.freeze({
|
|
52
53
|
MESSAGES: 'messages',
|
|
54
|
+
TOOLS: 'tools',
|
|
53
55
|
})
|
|
54
56
|
|
|
55
57
|
// 会话事件类型(插件自有;运行时是否 append 取决于宿主是否收录该类型,
|
|
@@ -71,12 +73,13 @@ export const DEFAULTS = Object.freeze({
|
|
|
71
73
|
ENABLED: true,
|
|
72
74
|
MODE: MODES.REGEX,
|
|
73
75
|
ENTITIES: ['phone', 'email', 'id-card', 'bank-card', 'key'],
|
|
74
|
-
SCOPE: SCOPES.MESSAGES,
|
|
76
|
+
SCOPE: [SCOPES.MESSAGES],
|
|
75
77
|
REGISTER_COMMAND: true,
|
|
76
78
|
REGISTER_TOOLS: true,
|
|
77
79
|
PERSIST_RESTORE_TABLE: true,
|
|
78
80
|
MAX_RESTORE_ENTRIES_PER_SESSION: 500,
|
|
79
81
|
MAX_SESSIONS: 1000,
|
|
82
|
+
MASK_CLIENT_ENABLED: false,
|
|
80
83
|
})
|
|
81
84
|
|
|
82
85
|
// 配置字段合法性边界(加载期校验,越界响亮失败)。
|
package/lib/errors.mjs
CHANGED
|
@@ -55,14 +55,14 @@ export function nerModeUnsupported(mode) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* 配置请求了未知作用域。
|
|
59
59
|
* @param {unknown} scope - 配置值。
|
|
60
60
|
* @returns {MaskError} code=SCOPE_UNSUPPORTED。
|
|
61
61
|
*/
|
|
62
62
|
export function scopeUnsupported(scope) {
|
|
63
63
|
return new MaskError(
|
|
64
64
|
ERROR_CODES.SCOPE_UNSUPPORTED,
|
|
65
|
-
`scope ${JSON.stringify(scope)} is not
|
|
65
|
+
`scope ${JSON.stringify(scope)} is not a valid surface; expected "messages" and/or "tools"`,
|
|
66
66
|
{ scope },
|
|
67
67
|
)
|
|
68
68
|
}
|
package/lib/mask.mjs
CHANGED
|
@@ -43,3 +43,28 @@ export function maskMessages(messages, stripper) {
|
|
|
43
43
|
})
|
|
44
44
|
return { messages: masked, replaced }
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 遮罩任意内容块数组(工具结果的 text 块等),非文本块原样保留。
|
|
49
|
+
* @param {object[]} blocks - 内容块数组(含 {type:'text', text})。
|
|
50
|
+
* @param {import('./strip.mjs').Stripper} stripper - 累计脱敏器。
|
|
51
|
+
* @returns {{blocks: object[], replaced: number}} 遮罩后数组与本次替换数。
|
|
52
|
+
*/
|
|
53
|
+
export function maskContentBlocks(blocks, stripper) {
|
|
54
|
+
if (!Array.isArray(blocks)) return { blocks, replaced: 0 }
|
|
55
|
+
let replaced = 0
|
|
56
|
+
let changed = false
|
|
57
|
+
const masked = blocks.map((block) => {
|
|
58
|
+
if (block === null || typeof block !== 'object' || block.type !== 'text' || typeof block.text !== 'string') {
|
|
59
|
+
return block
|
|
60
|
+
}
|
|
61
|
+
const result = stripper.stripInto(block.text)
|
|
62
|
+
if (result.replaced > 0) {
|
|
63
|
+
replaced += result.replaced
|
|
64
|
+
changed = true
|
|
65
|
+
}
|
|
66
|
+
return { ...block, text: result.text }
|
|
67
|
+
})
|
|
68
|
+
if (!changed) return { blocks, replaced: 0 }
|
|
69
|
+
return { blocks: masked, replaced }
|
|
70
|
+
}
|
package/lib/strip.mjs
CHANGED
|
@@ -31,19 +31,47 @@ export const BUILTIN_PATTERNS = Object.freeze([
|
|
|
31
31
|
* @typedef {{text: string, entity: string, label: string, start: number, end: number, score: number}} PIIEntity
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 内置正则检测器(Provider 默认实现):在文本上跑全部 pattern,返回实体列表。
|
|
36
|
+
* 纯函数、零依赖;作为可插拔 detector 的缺省,不改变既有检测语义。
|
|
37
|
+
* @param {string} text - 待检测文本。
|
|
38
|
+
* @param {Array<{entity: string, source: RegExp, score: number}>} patterns - 正则 pattern 列表。
|
|
39
|
+
* @returns {PIIEntity[]} 实体列表。
|
|
40
|
+
*/
|
|
41
|
+
export function regexDetect(text, patterns) {
|
|
42
|
+
/** @type {PIIEntity[]} */
|
|
43
|
+
const entities = []
|
|
44
|
+
for (const pattern of patterns) {
|
|
45
|
+
for (const match of text.matchAll(pattern.source)) {
|
|
46
|
+
entities.push({
|
|
47
|
+
text: match[0],
|
|
48
|
+
entity: pattern.entity,
|
|
49
|
+
label: ENTITY_LABELS[pattern.entity] ?? pattern.entity,
|
|
50
|
+
start: match.index,
|
|
51
|
+
end: match.index + match[0].length,
|
|
52
|
+
score: pattern.score,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return entities
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
/**
|
|
35
60
|
* PII 脱敏器:把 PII 替换为 `<LABEL_N>` 占位符,并能按映射还原。
|
|
36
61
|
*
|
|
62
|
+
* 检测器是可插拔 Provider:`detector` 为 `(text) => PIIEntity[]` 时用它(如
|
|
63
|
+
* 外部 NER 识别器),否则回退到内置正则 {@link regexDetect}(零依赖默认,行为不变)。
|
|
37
64
|
* 同一会话内跨请求累计(stripInto 不重置),保证同一原文始终复用同一占位符,
|
|
38
65
|
* 使模型在整段历史里看到的占位符语义一致;strip() 是单次模式(每次重置),
|
|
39
66
|
* 供 mask_test 这类"试跑一段文本"的独立场景使用。
|
|
40
67
|
*/
|
|
41
68
|
export class Stripper {
|
|
42
69
|
/**
|
|
43
|
-
* @param {object} options - {patterns: Array<{entity: string, source: RegExp, score: number}>, maxEntries?: number}。
|
|
70
|
+
* @param {object} options - {patterns: Array<{entity: string, source: RegExp, score: number}>, detector?: (text: string) => PIIEntity[], maxEntries?: number}。
|
|
44
71
|
*/
|
|
45
72
|
constructor(options) {
|
|
46
73
|
this.patterns = options.patterns
|
|
74
|
+
this.detector = options.detector ?? null
|
|
47
75
|
this.maxEntries = options.maxEntries ?? LIMITS.MAX_RESTORE_ENTRIES
|
|
48
76
|
this.reset()
|
|
49
77
|
}
|
|
@@ -151,26 +179,12 @@ export class Stripper {
|
|
|
151
179
|
}
|
|
152
180
|
|
|
153
181
|
/**
|
|
154
|
-
*
|
|
182
|
+
* 检测全部实体:优先走注入的 detector Provider,否则回退内置正则。
|
|
155
183
|
* @param {string} text - 文本。
|
|
156
184
|
* @returns {PIIEntity[]} 实体列表。
|
|
157
185
|
*/
|
|
158
186
|
_detect(text) {
|
|
159
|
-
|
|
160
|
-
const entities = []
|
|
161
|
-
for (const pattern of this.patterns) {
|
|
162
|
-
for (const match of text.matchAll(pattern.source)) {
|
|
163
|
-
entities.push({
|
|
164
|
-
text: match[0],
|
|
165
|
-
entity: pattern.entity,
|
|
166
|
-
label: ENTITY_LABELS[pattern.entity] ?? pattern.entity,
|
|
167
|
-
start: match.index,
|
|
168
|
-
end: match.index + match[0].length,
|
|
169
|
-
score: pattern.score,
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return entities
|
|
187
|
+
return this.detector !== null ? this.detector(text) : regexDetect(text, this.patterns)
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
/**
|
|
@@ -245,11 +259,12 @@ export class Stripper {
|
|
|
245
259
|
|
|
246
260
|
/**
|
|
247
261
|
* 按启用的实体集创建脱敏器。
|
|
248
|
-
* @param {object} [options] - {entities?: string[], maxEntries?: number}。
|
|
262
|
+
* @param {object} [options] - {entities?: string[], maxEntries?: number, detector?: (text: string) => PIIEntity[]}。
|
|
263
|
+
* detector 为可插拔检测器 Provider;缺省时使用内置正则(零依赖默认)。
|
|
249
264
|
* @returns {Stripper} 配置好的脱敏器。
|
|
250
265
|
*/
|
|
251
266
|
export function createStripper(options = {}) {
|
|
252
267
|
const enabled = new Set(options.entities ?? Object.keys(ENTITY_LABELS))
|
|
253
268
|
const patterns = BUILTIN_PATTERNS.filter(pattern => enabled.has(pattern.entity))
|
|
254
|
-
return new Stripper({ patterns, maxEntries: options.maxEntries })
|
|
269
|
+
return new Stripper({ patterns, maxEntries: options.maxEntries, detector: options.detector })
|
|
255
270
|
}
|
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.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/PerryLink/dsh-mask.git"
|
package/types.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export interface Config {
|
|
|
23
23
|
mode?: 'regex' | 'regex+ner'
|
|
24
24
|
/** 启用的实体类型;regex 集为 phone/email/id-card/bank-card/key/ip,person/address 需 NER。 */
|
|
25
25
|
entities?: string[]
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* 遮罩作用域;'messages' = agent/pre-step 入站消息,'tools' = tools/post-execute
|
|
28
|
+
* 工具结果。可传单个字符串或数组(如 `['messages', 'tools']`)。默认 `['messages']`。
|
|
29
|
+
*/
|
|
30
|
+
scope?: 'messages' | 'tools' | Array<'messages' | 'tools'>
|
|
28
31
|
/** 注册 /mask 命令(默认 true)。 */
|
|
29
32
|
registerCommand?: boolean
|
|
30
33
|
/** tools 服务存在时注册 mask_test 工具(默认 true)。 */
|
|
@@ -35,6 +38,8 @@ export interface Config {
|
|
|
35
38
|
maxRestoreEntriesPerSession?: number
|
|
36
39
|
/** 内存会话脱敏器上限(LRU 逐出,映射按需从领域回载)。 */
|
|
37
40
|
maxSessions?: number
|
|
41
|
+
/** 浏览器半自动还原气泡开关(防御性 client slot;默认 false,未启用前对 UI 无副作用)。 */
|
|
42
|
+
maskClientEnabled?: boolean
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/** mask_test 工具规范结果。 */
|