dsh-perm-gate 2.0.0 → 2.1.2

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/INSTALL.ja.md +57 -1
  3. package/INSTALL.ko.md +54 -1
  4. package/INSTALL.md +53 -1
  5. package/INSTALL.zh.md +48 -1
  6. package/README.ja.md +86 -12
  7. package/README.ko.md +83 -12
  8. package/README.md +120 -24
  9. package/README.zh.md +88 -10
  10. package/cordis.patch.yml +34 -2
  11. package/dsh.plugin.json +22 -0
  12. package/lib/agent-identity.d.ts +43 -0
  13. package/lib/agent-identity.js +95 -0
  14. package/lib/client.js +114 -0
  15. package/lib/client.js.map +1 -1
  16. package/lib/compiler.d.ts +39 -0
  17. package/lib/compiler.js +106 -0
  18. package/lib/config.d.ts +95 -2
  19. package/lib/config.js +28 -3
  20. package/lib/engine.js +9 -2
  21. package/lib/evaluate.d.ts +9 -0
  22. package/lib/evaluate.js +189 -0
  23. package/lib/events.d.ts +13 -0
  24. package/lib/events.js +31 -0
  25. package/lib/index.d.ts +1 -1
  26. package/lib/index.js +235 -6
  27. package/lib/network-lifecycle.d.ts +107 -0
  28. package/lib/network-lifecycle.js +180 -0
  29. package/lib/network.d.ts +109 -0
  30. package/lib/network.js +196 -0
  31. package/lib/proxy.d.ts +160 -0
  32. package/lib/proxy.js +529 -0
  33. package/lib/risk.js +25 -3
  34. package/lib/rule-chain.d.ts +39 -0
  35. package/lib/rule-chain.js +222 -0
  36. package/lib/rule-dims.d.ts +139 -0
  37. package/lib/rule-dims.js +194 -0
  38. package/lib/rule.d.ts +25 -0
  39. package/lib/rule.js +15 -1
  40. package/lib/runtime.d.ts +87 -0
  41. package/lib/runtime.js +327 -2
  42. package/lib/session-sweep.d.ts +45 -0
  43. package/lib/session-sweep.js +153 -0
  44. package/lib/shadow.d.ts +34 -0
  45. package/lib/shadow.js +143 -0
  46. package/lib/watch.d.ts +42 -0
  47. package/lib/watch.js +170 -0
  48. package/package.json +8 -2
  49. package/scripts/patch-permission-glyph.mjs +209 -0
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Add `permissive-full` to the DSH composer's permission-glyph map.
4
+ *
5
+ * WHY THIS EXISTS
6
+ * ---------------
7
+ * The composer's permission picker renders an icon per preset, looked up in a
8
+ * CLOSED map inside `@deepseek-ai/dsh-client-ui-conversation`:
9
+ *
10
+ * const permissionGlyphs = new Map([
11
+ * ["read-only", <svg …/>],
12
+ * ["permissive", <svg …/>], // the 自动审查 tier's shield+eye
13
+ * ["workspace-write", <svg …/>],
14
+ * ["danger-full-access", <svg …/>],
15
+ * ]);
16
+ * function permissionGlyph(value) { return permissionGlyphs.get(value); }
17
+ *
18
+ * The source comment states the rule: "host-configured names outside the design
19
+ * set get none." There is no plugin-facing seam — the option objects the host
20
+ * supplies carry only `{value, name, description}`, and the map itself is a
21
+ * closure variable. So a plugin-contributed tier such as `permissive-full`
22
+ * renders with no icon in the dropdown and falls back to plain text in the
23
+ * trigger, while the built-in `permissive` tier shows the shield+eye.
24
+ *
25
+ * The only way to give our tier the same glyph is to add an entry to that map.
26
+ * This script does exactly that: it duplicates the `permissive` entry under the
27
+ * `permissive-full` key, so the two tiers of one plugin look alike.
28
+ *
29
+ * VOLUNTARY, NOT AUTOMATIC
30
+ * ------------------------
31
+ * This edits a **host** package. It is deliberately NOT wired to `postinstall`:
32
+ * a plugin must not rewrite the harness it is installed into without being
33
+ * asked. Run it yourself, and re-run it after every DSH upgrade — a DSH
34
+ * upgrade replaces the file and the patch is gone. Reinstalling this plugin
35
+ * does NOT bring it back: `dsh plugin --profile web add …` writes only the
36
+ * profile's own `node_modules`, and on a stock install the DSH package, the
37
+ * profile's `node_modules` entry and this bundle are three paths to ONE file
38
+ * (a symlink and a junction into the same inode).
39
+ *
40
+ * USAGE
41
+ * npx dsh-perm-gate-patch-glyph # patch the bundle it finds
42
+ * npx dsh-perm-gate-patch-glyph --check # report only; exit 1 if missing
43
+ * node scripts/patch-permission-glyph.mjs <path/to/client.js>
44
+ *
45
+ * With no argument it probes the usual global-install locations. The script is
46
+ * idempotent (a second run is a no-op), backs the file up once, and refuses to
47
+ * write a bundle it cannot slice correctly, so it can never half-apply.
48
+ */
49
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
50
+ import { execFileSync } from 'node:child_process'
51
+ import { homedir } from 'node:os'
52
+ import { dirname, join, resolve } from 'node:path'
53
+ import { pathToFileURL } from 'node:url'
54
+
55
+ /** The map key whose entry is cloned, and the key it is cloned onto. */
56
+ export const SOURCE_KEY = 'permissive'
57
+ export const TARGET_KEY = 'permissive-full'
58
+
59
+ /** Relative path from a package root to the bundle holding the map. */
60
+ const BUNDLE = join('@deepseek-ai', 'dsh-client-ui-conversation', 'lib', 'client.js')
61
+
62
+ /** Thrown for every refusal; the CLI turns it into an exit code 1. */
63
+ export class GlyphPatchError extends Error {}
64
+
65
+ /**
66
+ * Candidate bundle paths, most specific first. Pure: no filesystem access, so
67
+ * the probe order is unit-testable.
68
+ *
69
+ * `execDir` is `dirname(process.execPath)`. Deriving from the running node
70
+ * binary — never from a hardcoded nvm version or install path — is what keeps
71
+ * the script working across node upgrades and re-pointed install symlinks.
72
+ *
73
+ * @param {{ execDir: string, cwd: string, dshHome: string }} where
74
+ * @returns {string[]} absolute candidate paths
75
+ */
76
+ export function candidateBundles({ execDir, cwd, dshHome }) {
77
+ return [
78
+ // 1. `@deepseek-ai` scope hoisted beside the running node binary.
79
+ join(execDir, 'node_modules', BUNDLE),
80
+ // 2. The same scope nested inside the `dsh` package itself.
81
+ join(execDir, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', BUNDLE),
82
+ // 3. Whatever profile the caller is standing in.
83
+ join(cwd, 'node_modules', BUNDLE),
84
+ // 4. The hoisted scope shared by every profile under DSH_HOME.
85
+ join(dshHome, 'profiles', 'node_modules', BUNDLE),
86
+ ]
87
+ }
88
+
89
+ /**
90
+ * Extract the full `["<key>", …]` map entry, by bracket depth.
91
+ *
92
+ * The scan starts ON the entry's opening `[` (the one immediately before the
93
+ * key), so depth returns to zero exactly at that entry's closing `]`. Starting
94
+ * anywhere else — e.g. at the first `(` of the value expression — ends the
95
+ * slice in the wrong place and yields a broken bundle.
96
+ *
97
+ * Safe here because the entry's only string literals (svg path `d` attributes)
98
+ * contain no brackets.
99
+ *
100
+ * @param {string} source
101
+ * @param {string} key
102
+ * @returns {string} the entry, brackets included
103
+ */
104
+ export function sliceEntry(source, key) {
105
+ const start = source.indexOf(`["${key}",`)
106
+ if (start === -1) throw new GlyphPatchError(`anchor not found: ["${key}",`)
107
+ let depth = 0
108
+ for (let i = start; i < source.length; i += 1) {
109
+ const ch = source[i]
110
+ if (ch === '[' || ch === '(' || ch === '{') depth += 1
111
+ else if (ch === ']' || ch === ')' || ch === '}') {
112
+ depth -= 1
113
+ if (depth === 0) return source.slice(start, i + 1)
114
+ }
115
+ }
116
+ throw new GlyphPatchError(`unterminated entry for ["${key}",`)
117
+ }
118
+
119
+ /**
120
+ * Clone the `permissive` glyph entry onto the `permissive-full` key.
121
+ *
122
+ * @param {string} source the bundle's text
123
+ * @returns {{ changed: boolean, source: string, entry: string | null }}
124
+ * `changed: false` means the target key was already present (idempotent no-op).
125
+ */
126
+ export function applyGlyphPatch(source) {
127
+ if (source.includes(`["${TARGET_KEY}",`)) return { changed: false, source, entry: null }
128
+ const sourceEntry = sliceEntry(source, SOURCE_KEY)
129
+ if (!sourceEntry.includes('svg')) {
130
+ throw new GlyphPatchError(`the ["${SOURCE_KEY}",] entry does not look like a glyph`)
131
+ }
132
+ const clonedEntry = sourceEntry.replace(`["${SOURCE_KEY}",`, `["${TARGET_KEY}",`)
133
+ return { changed: true, source: source.replace(sourceEntry, `${sourceEntry},\n\t\t\t${clonedEntry}`), entry: clonedEntry }
134
+ }
135
+
136
+ /** Resolve the bundle to patch, or throw with an explanation. */
137
+ export function findBundle(argv, where) {
138
+ const explicit = argv.slice(2).find((arg) => !arg.startsWith('-'))
139
+ if (explicit !== undefined && explicit !== '') {
140
+ const p = resolve(explicit)
141
+ if (!existsSync(p)) throw new GlyphPatchError(`no such file: ${p}`)
142
+ return p
143
+ }
144
+ for (const candidate of candidateBundles(where)) {
145
+ if (existsSync(candidate)) return candidate
146
+ }
147
+ throw new GlyphPatchError(
148
+ 'could not locate @deepseek-ai/dsh-client-ui-conversation.\n' +
149
+ 'Pass the bundle path explicitly:\n' +
150
+ ` ${argv[1] ?? 'dsh-perm-gate-patch-glyph'} <…>/dsh-client-ui-conversation/lib/client.js`,
151
+ )
152
+ }
153
+
154
+ function main(argv) {
155
+ const check = argv.includes('--check')
156
+ const where = {
157
+ execDir: dirname(process.execPath),
158
+ cwd: process.cwd(),
159
+ dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
160
+ }
161
+
162
+ const bundle = findBundle(argv, where)
163
+ const original = readFileSync(bundle, 'utf8')
164
+ const { changed, source: patched } = applyGlyphPatch(original)
165
+
166
+ if (!changed) {
167
+ process.stdout.write(`patch-permission-glyph: ["${TARGET_KEY}",] already present in\n ${bundle}\n`)
168
+ return 0
169
+ }
170
+ if (check) {
171
+ process.stdout.write(
172
+ `patch-permission-glyph: ["${TARGET_KEY}",] is MISSING from\n ${bundle}\n` +
173
+ ' (--check: nothing written; re-run without --check to apply)\n',
174
+ )
175
+ return 1
176
+ }
177
+
178
+ // Back up once, then write.
179
+ const backup = `${bundle}.bak-permgate-glyph`
180
+ if (!existsSync(backup)) copyFileSync(bundle, backup)
181
+ writeFileSync(bundle, patched, 'utf8')
182
+
183
+ // Fail loudly rather than leaving a broken bundle behind.
184
+ try {
185
+ execFileSync(process.execPath, ['--check', bundle], { stdio: 'pipe' })
186
+ } catch (error) {
187
+ copyFileSync(backup, bundle)
188
+ throw new GlyphPatchError(`syntax check failed; restored the backup.\n${String(error?.stderr ?? error)}`)
189
+ }
190
+
191
+ process.stdout.write(
192
+ `patch-permission-glyph: added ["${TARGET_KEY}",] to\n ${bundle}\n` +
193
+ ` backup: ${backup}\n` +
194
+ ' NOTE: this edits a DSH package — re-run after any DSH upgrade or reinstall.\n',
195
+ )
196
+ return 0
197
+ }
198
+
199
+ const invokedDirectly = process.argv[1] !== undefined
200
+ && import.meta.url === pathToFileURL(resolve(process.argv[1])).href
201
+
202
+ if (invokedDirectly) {
203
+ try {
204
+ process.exitCode = main(process.argv)
205
+ } catch (error) {
206
+ process.stderr.write(`patch-permission-glyph: ${error instanceof Error ? error.message : String(error)}\n`)
207
+ process.exitCode = 1
208
+ }
209
+ }