@dsh-electron/dsh-plugin-multi-root-workspace 0.1.5 → 0.1.7

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 (101) hide show
  1. package/CHANGELOG.en.md +56 -1
  2. package/CHANGELOG.md +56 -1
  3. package/README.en.md +157 -92
  4. package/README.md +158 -93
  5. package/cordis.patch.yml +61 -0
  6. package/lib/client.js +2819 -115
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-82Y27Iom.js → compat-DF_yL1bB.js} +180 -4
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-Cn1Jj5Xi.js → instructions-D19jRxN0.js} +109 -28
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +43 -0
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +240 -25
  28. package/lib/session-directory-BLOstut1.js +9 -0
  29. package/lib/tool-context-CpgIFcCB.js +6 -0
  30. package/lib/types/client/RootSwitcher.d.ts +63 -0
  31. package/lib/types/client/WorkspaceFoldersAction.d.ts +41 -0
  32. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  33. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  34. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  35. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  36. package/lib/types/client/files-tree/face.d.ts +107 -0
  37. package/lib/types/client/files-tree/locales.d.ts +10 -0
  38. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  39. package/lib/types/client/files-tree/store.d.ts +81 -0
  40. package/lib/types/client/index.d.ts +13 -2
  41. package/lib/types/client/locales.d.ts +60 -0
  42. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  43. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  44. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  45. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  46. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  47. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  48. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  49. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  50. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  51. package/lib/types/client/panel-client.d.ts +2 -1
  52. package/lib/types/client/styles.d.ts +1 -1
  53. package/lib/types/command.d.ts +16 -3
  54. package/lib/types/compat/client-icons.d.ts +18 -0
  55. package/lib/types/compat/client-session.d.ts +2 -0
  56. package/lib/types/compat/dsh-version.d.ts +5 -3
  57. package/lib/types/compat/instruction-overlap.d.ts +7 -0
  58. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  59. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  60. package/lib/types/compat/process-result.d.ts +14 -0
  61. package/lib/types/compat/pty-marker.d.ts +83 -0
  62. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  63. package/lib/types/compat/session-directory.d.ts +4 -0
  64. package/lib/types/compat/storage-zod.d.ts +65 -0
  65. package/lib/types/compat/tool-context.d.ts +9 -0
  66. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  67. package/lib/types/compat/windows-native.d.ts +68 -0
  68. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  69. package/lib/types/compat/workspace-files.d.ts +7 -0
  70. package/lib/types/contract.d.ts +35 -4
  71. package/lib/types/dialects.d.ts +99 -3
  72. package/lib/types/fs.d.ts +32 -10
  73. package/lib/types/index.d.ts +5 -0
  74. package/lib/types/lsp.d.ts +4 -0
  75. package/lib/types/method-wrapper.d.ts +2 -0
  76. package/lib/types/process-fence.d.ts +54 -0
  77. package/lib/types/profile-isolation.d.ts +178 -0
  78. package/lib/types/providers.d.ts +70 -0
  79. package/lib/types/registry.d.ts +132 -31
  80. package/lib/types/roots.d.ts +100 -13
  81. package/lib/types/sandbox.d.ts +59 -12
  82. package/lib/types/scope.d.ts +203 -14
  83. package/lib/types/windows-broker.d.ts +17 -0
  84. package/lib/types/windows-deny.d.ts +1 -0
  85. package/lib/types/windows-profile.d.ts +10 -0
  86. package/lib/types/windows-runner-contract.d.ts +97 -0
  87. package/lib/types/windows-runner.d.ts +1 -0
  88. package/lib/types/workspace-files.d.ts +4 -0
  89. package/lib/types/workspace-routing.d.ts +11 -0
  90. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  91. package/lib/windows-runner.js +1311 -0
  92. package/lib/workspace-files.js +203 -0
  93. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  94. package/package.json +116 -51
  95. package/scripts/windows-broker/install.ps1 +89 -0
  96. package/scripts/windows-broker/rights.cs +58 -0
  97. package/scripts/windows-broker/service.cs +323 -0
  98. package/scripts/windows-broker/verify-install.ps1 +80 -0
  99. package/lib/command-DyOWujPo.js +0 -690
  100. package/lib/sandbox-D8Fw95FM.js +0 -290
  101. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
@@ -0,0 +1,203 @@
1
+ import { l as isCanonicallyUnder } from "./roots-B-0l8mNG.js";
2
+ import { t as routeWorkspacePath } from "./workspace-routing-YPxaQ4V3.js";
3
+ import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
4
+ import { posix, resolve, win32 } from "node:path";
5
+ //#region src/compat/workspace-files.ts
6
+ function adaptPathWatch(service, wrap) {
7
+ return wrapMethod(service, "changes", (original) => original.length < 3 ? original : wrap(original));
8
+ }
9
+ /** Match both legacy `readRelated` and modern `readBytes({ baseFile })` tail resolution. */
10
+ function resolveRelatedWorkspacePath(absoluteBase, tail) {
11
+ const paths = absoluteBase.startsWith("/") ? posix : win32;
12
+ return paths.resolve(paths.dirname(absoluteBase), tail.replace(/\\/g, "/"));
13
+ }
14
+ //#endregion
15
+ //#region src/workspace-files.ts
16
+ const inject = [
17
+ "multiRootCompat",
18
+ "multiRootProviders",
19
+ "multiRootScope"
20
+ ];
21
+ function apply(ctx) {
22
+ ctx.inject(["workspaceFiles", "fs"], (scope) => {
23
+ const service = scope.get("workspaceFiles");
24
+ scope.effect(() => {
25
+ const rowLifetime = new AbortController();
26
+ const restoreList = wrapMethod(service, "list", (original) => async function(request, path, signal) {
27
+ if (scope.multiRootScope.scopeOf(request.workspaceRoot).length === 0) return original.call(this, request, path, signal);
28
+ const route = await routeWorkspacePath(scope, request.workspaceRoot, path, signal);
29
+ if (route === void 0 || route.workspaceRoot === route.primaryRoot) return original.call(this, request, path, signal);
30
+ const result = await original.call(this, {
31
+ ...request,
32
+ workspaceRoot: route.workspaceRoot
33
+ }, route.absolutePath, signal);
34
+ if (!scope.multiRootScope.scopeOf(route.primaryRoot).includes(route.workspaceRoot)) throw await outside(path);
35
+ return result;
36
+ });
37
+ const restoreStat = wrapMethod(service, "stat", (original) => async function(request, path, signal) {
38
+ const routed = await routeRead(scope, request, path, signal);
39
+ if (routed === "deny") throw await outside(path);
40
+ return original.call(this, routed.request, routed.path, signal);
41
+ });
42
+ const restoreRead = wrapMethod(service, "read", (original) => async function(request, path, range, signal) {
43
+ const routed = await routeRead(scope, request, path, signal);
44
+ if (routed === "deny") throw await outside(path);
45
+ return original.call(this, routed.request, routed.path, range, signal);
46
+ });
47
+ const restoreReadBytes = wrapMethod(service, "readBytes", (original) => {
48
+ const call = original;
49
+ return (async function(request, path, options, signal) {
50
+ const baseFile = options?.baseFile;
51
+ if (baseFile !== void 0) {
52
+ const routed = await routeRead(scope, request, baseFile, signal);
53
+ if (routed === "deny") throw await outside(baseFile);
54
+ const escaped = await relatedTailWithheld(scope, request.workspaceRoot, routed, path, signal);
55
+ if (escaped !== void 0) throw await outside(escaped);
56
+ const routedOptions = baseFile === routed.path ? options : {
57
+ ...options,
58
+ baseFile: routed.path
59
+ };
60
+ return call.call(this, routed.request, path, routedOptions, signal);
61
+ }
62
+ const routed = await routeRead(scope, request, path, signal);
63
+ if (routed === "deny") throw await outside(path);
64
+ return call.call(this, routed.request, routed.path, options, signal);
65
+ });
66
+ });
67
+ const legacy = service;
68
+ const restoreReadAll = typeof legacy.readAll === "function" ? wrapMethod(legacy, "readAll", (original) => (async function(request, path, signal) {
69
+ const routed = await routeRead(scope, request, path, signal);
70
+ if (routed === "deny") throw await outside(path);
71
+ return original.call(this, routed.request, routed.path, signal);
72
+ })) : void 0;
73
+ const restoreReadRelated = typeof legacy.readRelated === "function" ? wrapMethod(legacy, "readRelated", (original) => (async function(request, path, relativePath, signal) {
74
+ const routed = await routeRead(scope, request, path, signal);
75
+ if (routed === "deny") throw await outside(path);
76
+ const escaped = await relatedTailWithheld(scope, request.workspaceRoot, routed, relativePath, signal);
77
+ if (escaped !== void 0) throw await outside(escaped);
78
+ return original.call(this, routed.request, routed.path, relativePath, signal);
79
+ })) : void 0;
80
+ const restoreWatch = adaptPathWatch(service, (originalChanges) => async function* (request, path, signal) {
81
+ if (scope.multiRootScope.scopeOf(request.workspaceRoot).length === 0 && registeredRoots(scope, request.workspaceRoot).length === 0) {
82
+ yield* originalChanges.call(this, request, path, signal);
83
+ return;
84
+ }
85
+ const route = await routeWorkspacePath(scope, request.workspaceRoot, path, signal);
86
+ if (route === void 0 || route.workspaceRoot === route.primaryRoot) {
87
+ if (underWithheldRoot(scope, request.workspaceRoot, path)) throw await outside(path);
88
+ yield* originalChanges.call(this, request, path, signal);
89
+ return;
90
+ }
91
+ const lifetime = new AbortController();
92
+ const unsubscribe = scope.multiRootScope.subscribe(() => {
93
+ if (!scope.multiRootScope.scopeOf(route.primaryRoot).includes(route.workspaceRoot)) lifetime.abort(/* @__PURE__ */ new Error("the additional root was revoked"));
94
+ });
95
+ try {
96
+ for await (const frame of originalChanges.call(this, {
97
+ ...request,
98
+ workspaceRoot: route.workspaceRoot
99
+ }, route.absolutePath, AbortSignal.any([
100
+ signal,
101
+ lifetime.signal,
102
+ rowLifetime.signal
103
+ ]))) {
104
+ if (!scope.multiRootScope.scopeOf(route.primaryRoot).includes(route.workspaceRoot)) throw await outside(path);
105
+ yield frame;
106
+ }
107
+ } finally {
108
+ unsubscribe();
109
+ }
110
+ });
111
+ return () => {
112
+ rowLifetime.abort(/* @__PURE__ */ new Error("multi-root workspace-files routing unloaded"));
113
+ restoreList();
114
+ restoreStat();
115
+ restoreRead();
116
+ restoreReadBytes();
117
+ restoreReadAll?.();
118
+ restoreReadRelated?.();
119
+ restoreWatch();
120
+ };
121
+ }, "multi-root:workspace-files routing");
122
+ });
123
+ }
124
+ /**
125
+ * Route one read-style `workspaceFiles` call (`stat`/`read`/`readBytes`,
126
+ * plus the legacy `readAll`/`readRelated` pair where a release has them).
127
+ *
128
+ * Upstream deliberately gives reads no workspace confinement, so the
129
+ * multi-root boundary lives here: a path canonically inside a *live*
130
+ * additional root is answered under that root's scope; a path lexically
131
+ * inside a *registered but withheld* root is denied, because re-resolving it
132
+ * would follow whatever replaced the registered directory — the grant must
133
+ * never travel to a directory nobody registered. Anything else (primary
134
+ * root, genuinely unrelated paths) keeps upstream's default read access.
135
+ */
136
+ async function routeRead(scope, request, path, signal) {
137
+ if (scope.multiRootScope.scopeOf(request.workspaceRoot).length === 0 && registeredRoots(scope, request.workspaceRoot).length === 0) return {
138
+ request,
139
+ path
140
+ };
141
+ const route = await routeWorkspacePath(scope, request.workspaceRoot, path, signal);
142
+ if (route !== void 0 && route.workspaceRoot !== route.primaryRoot) {
143
+ if (!scope.multiRootScope.scopeOf(route.primaryRoot).includes(route.workspaceRoot)) return "deny";
144
+ return {
145
+ request: {
146
+ ...request,
147
+ workspaceRoot: route.workspaceRoot
148
+ },
149
+ path: route.absolutePath
150
+ };
151
+ }
152
+ return underWithheldRoot(scope, request.workspaceRoot, path) ? "deny" : {
153
+ request,
154
+ path
155
+ };
156
+ }
157
+ /**
158
+ * Every registration of one primary root, in any state — the scope only
159
+ * publishes the effective grant, so the withheld set has to come from the
160
+ * registry. Absent the registry service (degraded compositions, bare test
161
+ * contexts) there is no withheld set to enforce.
162
+ */
163
+ function registeredRoots(scope, primaryRoot) {
164
+ return scope.get("multiRootRegistry")?.list(primaryRoot) ?? [];
165
+ }
166
+ /**
167
+ * Whether `path` lexically sits under a registered root that is not granted
168
+ * right now. The comparison is lexical on purpose: the canonical resolution
169
+ * of a withheld root escapes by definition (that is what `redirected` means),
170
+ * so only the recorded spelling recognizes the stale row. `scopeOf` is the
171
+ * grant authority — it re-validates `canonicalPath` on every call, so a
172
+ * redirected root drops out even before the next registry `refresh()` run.
173
+ */
174
+ function underWithheldRoot(scope, primaryRoot, path) {
175
+ const statuses = registeredRoots(scope, primaryRoot);
176
+ if (statuses.length === 0) return false;
177
+ const lexical = resolve(primaryRoot, path);
178
+ const candidates = statuses.filter((status) => isCanonicallyUnder(lexical, status.path));
179
+ if (candidates.length === 0) return false;
180
+ const granted = new Set(scope.multiRootScope.scopeOf(primaryRoot));
181
+ return candidates.some((status) => !granted.has(status.path));
182
+ }
183
+ /**
184
+ * Resolve `tail` from the canonical base file's directory and report the joined path
185
+ * when it lands under a withheld root. `readRelated`'s tail and `readBytes`'s
186
+ * `baseFile`-relative target both read a file located relative to another
187
+ * file, so the granted base must not reach back into a dead grant.
188
+ */
189
+ async function relatedTailWithheld(scope, primaryRoot, base, tail, signal) {
190
+ if (registeredRoots(scope, primaryRoot).length === 0) return void 0;
191
+ const targetBase = await scope.fs.resolve(base.path, {
192
+ cwd: base.request.workspaceRoot,
193
+ signal
194
+ });
195
+ const target = resolveRelatedWorkspacePath(scope.fs.processPath(targetBase), tail);
196
+ return underWithheldRoot(scope, primaryRoot, target) ? target : void 0;
197
+ }
198
+ async function outside(path) {
199
+ const { RemoteError } = await import("@deepseek-ai/dsh-typert-protocol");
200
+ return new RemoteError("workspace-file/outside-workspace", `"${path}" is no longer in this workspace`, { path });
201
+ }
202
+ //#endregion
203
+ export { apply, inject };
@@ -0,0 +1,26 @@
1
+ import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
2
+ //#region src/workspace-routing.ts
3
+ /** Resolve relative paths against the primary cwd; never search other roots by basename. */
4
+ async function routeWorkspacePath(ctx, primaryRoot, path, signal, cwd = primaryRoot) {
5
+ signal?.throwIfAborted();
6
+ const primary = canonicalPath(primaryRoot);
7
+ const roots = [primary, ...ctx.multiRootScope.scopeOf(primary)];
8
+ const target = await ctx.fs.resolve(path, {
9
+ cwd,
10
+ ...signal === void 0 ? {} : { signal }
11
+ });
12
+ for (const root of roots) {
13
+ const rootTarget = await ctx.fs.resolve(root, signal === void 0 ? {} : { signal });
14
+ if (!ctx.fs.contains(rootTarget, target)) continue;
15
+ if (root !== primary && !ctx.multiRootScope.scopeOf(primary).includes(root)) return void 0;
16
+ signal?.throwIfAborted();
17
+ return {
18
+ primaryRoot: primary,
19
+ workspaceRoot: root,
20
+ target,
21
+ absolutePath: ctx.fs.processPath(target)
22
+ };
23
+ }
24
+ }
25
+ //#endregion
26
+ export { routeWorkspacePath as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-electron/dsh-plugin-multi-root-workspace",
3
3
  "description": "Multi-root Workspace for DeepSeek Harness: adds N additional roots to the workspace sandbox scope as an out-of-tree bundle",
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -30,6 +30,10 @@
30
30
  "types": "./lib/types/registry.d.ts",
31
31
  "default": "./lib/registry.js"
32
32
  },
33
+ "./providers": {
34
+ "types": "./lib/types/providers.d.ts",
35
+ "default": "./lib/providers.js"
36
+ },
33
37
  "./instructions": {
34
38
  "types": "./lib/types/instructions.d.ts",
35
39
  "default": "./lib/instructions.js"
@@ -42,7 +46,23 @@
42
46
  "types": "./lib/types/client/index.d.ts",
43
47
  "default": "./lib/client.js"
44
48
  },
45
- "./package.json": "./package.json"
49
+ "./package.json": "./package.json",
50
+ "./lsp": {
51
+ "types": "./lib/types/lsp.d.ts",
52
+ "default": "./lib/lsp.js"
53
+ },
54
+ "./workspace-files": {
55
+ "types": "./lib/types/workspace-files.d.ts",
56
+ "default": "./lib/workspace-files.js"
57
+ },
58
+ "./process-fence": {
59
+ "types": "./lib/types/process-fence.d.ts",
60
+ "default": "./lib/process-fence.js"
61
+ },
62
+ "./windows-runner": {
63
+ "types": "./lib/types/windows-runner.d.ts",
64
+ "default": "./lib/windows-runner.js"
65
+ }
46
66
  },
47
67
  "files": [
48
68
  "lib/*.js",
@@ -53,7 +73,9 @@
53
73
  "README.en.md",
54
74
  "CHANGELOG.md",
55
75
  "CHANGELOG.en.md",
56
- "LICENSE"
76
+ "LICENSE",
77
+ "scripts/windows-broker/*.ps1",
78
+ "scripts/windows-broker/*.cs"
57
79
  ],
58
80
  "publishConfig": {
59
81
  "access": "public"
@@ -67,21 +89,27 @@
67
89
  },
68
90
  "license": "MIT",
69
91
  "peerDependencies": {
70
- "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
71
- "@deepseek-ai/dsh-agent": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
72
- "@deepseek-ai/dsh-agent-instructions": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
73
- "@deepseek-ai/dsh-client-connection": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
74
- "@deepseek-ai/dsh-commands": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
75
- "@deepseek-ai/dsh-fs": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
76
- "@deepseek-ai/dsh-fs-local": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
77
- "@deepseek-ai/dsh-llm": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
78
- "@deepseek-ai/dsh-sandbox": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
79
- "@deepseek-ai/dsh-sandbox-local": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
80
- "@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
81
- "@deepseek-ai/dsh-session": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
82
- "@deepseek-ai/dsh-storage-domain": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
83
- "@deepseek-ai/dsh-subprocess": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2",
84
- "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
92
+ "@deepseek-ai/cordis": ">=4.0.1 <5.0.0 || 4.0.5-alpha.1 || 4.0.5-alpha.2",
93
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
94
+ "@deepseek-ai/dsh-agent-instructions": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
95
+ "@deepseek-ai/dsh-api-workspace-files": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
96
+ "@deepseek-ai/dsh-client-connection": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
97
+ "@deepseek-ai/dsh-commands": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
98
+ "@deepseek-ai/dsh-fs": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
99
+ "@deepseek-ai/dsh-fs-local": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
100
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
101
+ "@deepseek-ai/dsh-lsp": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
102
+ "@deepseek-ai/dsh-lsp-stdio": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
103
+ "@deepseek-ai/dsh-sandbox": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
104
+ "@deepseek-ai/dsh-sandbox-local": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
105
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
106
+ "@deepseek-ai/dsh-sandbox-windows-acl": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
107
+ "@deepseek-ai/dsh-session": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
108
+ "@deepseek-ai/dsh-storage-domain": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
109
+ "@deepseek-ai/dsh-subprocess": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
110
+ "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
111
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2",
112
+ "@deepseek-ai/dsh-typert-protocol": "0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4 || 0.2.1-alpha.5 || 0.2.1-rc.1 || 0.2.1-rc.2"
85
113
  },
86
114
  "peerDependenciesMeta": {
87
115
  "@deepseek-ai/dsh-agent": {
@@ -107,42 +135,75 @@
107
135
  },
108
136
  "@deepseek-ai/dsh-system-prompt": {
109
137
  "optional": true
138
+ },
139
+ "@deepseek-ai/dsh-lsp": {
140
+ "optional": true
141
+ },
142
+ "@deepseek-ai/dsh-lsp-stdio": {
143
+ "optional": true
144
+ },
145
+ "@deepseek-ai/dsh-api-workspace-files": {
146
+ "optional": true
147
+ },
148
+ "@deepseek-ai/dsh-typert-protocol": {
149
+ "optional": true
150
+ },
151
+ "@deepseek-ai/dsh-sandbox-windows-acl": {
152
+ "optional": true
153
+ },
154
+ "@deepseek-ai/dsh-tools": {
155
+ "optional": true
110
156
  }
111
157
  },
112
158
  "devDependencies": {
113
- "@deepseek-ai/cordis": "4.0.4",
114
- "@deepseek-ai/dsh": "0.2.0-rc.2",
115
- "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
116
- "@deepseek-ai/dsh-agent-instructions": "0.2.0-rc.2",
117
- "@deepseek-ai/dsh-app-boot": "0.2.0-rc.2",
118
- "@deepseek-ai/dsh-base": "0.2.0-rc.2",
119
- "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
120
- "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
121
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
122
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
123
- "@deepseek-ai/dsh-client-ui-sidebar": "0.2.0-rc.2",
124
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
125
- "@deepseek-ai/dsh-client-ui-workspace": "0.2.0-rc.2",
126
- "@deepseek-ai/dsh-commands": "0.2.0-rc.2",
127
- "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
128
- "@deepseek-ai/dsh-fs-local": "0.2.0-rc.2",
129
- "@deepseek-ai/dsh-fs-sandbox": "0.2.0-rc.2",
130
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
131
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
132
- "@deepseek-ai/dsh-sandbox-local": "0.2.0-rc.2",
133
- "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
134
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
135
- "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
136
- "@deepseek-ai/dsh-storage": "0.2.0-rc.2",
137
- "@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2",
138
- "@deepseek-ai/dsh-storage-json": "0.2.0-rc.2",
139
- "@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
140
- "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
141
- "@deepseek-ai/dsh-workspace": "0.2.0-rc.2",
159
+ "@deepseek-ai/cordis": "4.0.5",
160
+ "@deepseek-ai/dsh": "0.2.1-rc.2",
161
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
162
+ "@deepseek-ai/dsh-agent-instructions": "0.2.1-rc.2",
163
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.2",
164
+ "@deepseek-ai/dsh-api-workspace-files": "0.2.1-rc.2",
165
+ "@deepseek-ai/dsh-app-boot": "0.2.1-rc.2",
166
+ "@deepseek-ai/dsh-base": "0.2.1-rc.2",
167
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.2",
168
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
169
+ "@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
170
+ "@deepseek-ai/dsh-client-ui-dockkit": "0.2.1-rc.2",
171
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.2",
172
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.2",
173
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.2",
174
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.2",
175
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
176
+ "@deepseek-ai/dsh-client-ui-workspace": "0.2.1-rc.2",
177
+ "@deepseek-ai/dsh-commands": "0.2.1-rc.2",
178
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
179
+ "@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
180
+ "@deepseek-ai/dsh-fs-sandbox": "0.2.1-rc.2",
181
+ "@deepseek-ai/dsh-host-open-in-app": "0.2.1-rc.2",
182
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
183
+ "@deepseek-ai/dsh-lsp": "0.2.1-rc.2",
184
+ "@deepseek-ai/dsh-lsp-stdio": "0.2.1-rc.2",
185
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
186
+ "@deepseek-ai/dsh-sandbox-local": "0.2.1-rc.2",
187
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.2",
188
+ "@deepseek-ai/dsh-sandbox-windows-acl": "0.2.1-rc.2",
189
+ "@deepseek-ai/dsh-session": "0.2.1-rc.2",
190
+ "@deepseek-ai/dsh-session-projection": "0.2.1-rc.2",
191
+ "@deepseek-ai/dsh-storage": "0.2.1-rc.2",
192
+ "@deepseek-ai/dsh-storage-domain": "0.2.1-rc.2",
193
+ "@deepseek-ai/dsh-storage-json": "0.2.1-rc.2",
194
+ "@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
195
+ "@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.2",
196
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.2",
197
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
198
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
199
+ "@deepseek-ai/dsh-util-workspace-path": "0.2.1-rc.2",
200
+ "@deepseek-ai/dsh-workspace": "0.2.1-rc.2",
142
201
  "@testing-library/react": "^16.3.3",
143
202
  "@types/node": "^22.0.0",
144
203
  "@types/react": "~18.3.31",
204
+ "@types/react-dom": "~18.3.7",
145
205
  "clsx": "^2.0.0",
206
+ "immer": "^10.2.0",
146
207
  "jsdom": "^29.1.1",
147
208
  "oxlint": "^1.76.0",
148
209
  "react": "^18.3.1",
@@ -150,7 +211,8 @@
150
211
  "tsdown": "^0.22.0",
151
212
  "typescript": "^6.0.3",
152
213
  "vitest": "^4.1.8",
153
- "yaml": "^2.9.0"
214
+ "yaml": "^2.9.0",
215
+ "zustand": "~4.5.7"
154
216
  },
155
217
  "dsh": {
156
218
  "bundle": {
@@ -167,11 +229,12 @@
167
229
  }
168
230
  },
169
231
  "dependencies": {
170
- "zod": "^4.6.2",
171
232
  "@deepseek-ai/node-addon-system": "^0.1.2",
172
- "koffi": "^3.1.0"
233
+ "koffi": "^3.1.0",
234
+ "zod": "^4.6.2"
173
235
  },
174
236
  "scripts": {
237
+ "bench:roots": "node --experimental-transform-types scripts/bench-root-scale.mjs",
175
238
  "build": "pnpm run build:types && pnpm run bundle",
176
239
  "build:types": "tsc -p tsconfig.host.json && tsc -p tsconfig.client.json",
177
240
  "bundle": "node scripts/clean-lib.mjs && tsdown",
@@ -180,13 +243,15 @@
180
243
  "kernel:probe": "node scripts/check-kernel-runner.mjs",
181
244
  "lint": "oxlint",
182
245
  "release": "node scripts/bump-version.mjs",
246
+ "pack:check": "node scripts/check-pack.mjs",
183
247
  "smoke": "pnpm smoke:compose && pnpm smoke:behavior && pnpm smoke:journey",
184
248
  "smoke:behavior": "node scripts/smoke-behavior.mjs",
185
249
  "smoke:compose": "node scripts/smoke-compose.mjs",
186
250
  "smoke:journey": "node scripts/smoke-journey.mjs",
251
+ "smoke:packed": "node scripts/smoke-packed.mjs",
187
252
  "test": "vitest run",
188
253
  "test:watch": "vitest",
189
254
  "typecheck": "tsc -p tsconfig.host.json --noEmit && tsc -p tsconfig.client.json --noEmit",
190
- "verify:all": "pnpm lint && pnpm typecheck && pnpm build && pnpm kernel:probe && pnpm test && pnpm smoke"
255
+ "verify:all": "pnpm lint && pnpm typecheck && pnpm build && pnpm kernel:probe && pnpm test && pnpm smoke && pnpm pack:check && pnpm smoke:packed"
191
256
  }
192
257
  }
@@ -0,0 +1,89 @@
1
+ # Run from an elevated PowerShell. A dedicated virtual service account
2
+ # authenticates one installation SID; its executable is administrator-owned.
3
+ [CmdletBinding()]
4
+ param([string]$UserSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value, [switch]$Uninstall)
5
+ $ErrorActionPreference = 'Stop'
6
+ $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
7
+ if (-not ([Security.Principal.WindowsPrincipal]::new($identity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
8
+ throw 'Run this installer in an elevated PowerShell.'
9
+ }
10
+ $sid = [Security.Principal.SecurityIdentifier]::new($UserSid).Value
11
+ $sha = [Security.Cryptography.SHA256]::Create()
12
+ try { $hash = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($sid)))).Replace('-', '').ToLower().Substring(0,16) }
13
+ finally { $sha.Dispose() }
14
+ $name = "DshMultiRootBroker-$hash"
15
+ $directory = Join-Path $env:ProgramFiles $name
16
+ $sc = Join-Path $env:SystemRoot 'System32/sc.exe'
17
+ # Virtual service SIDs are deterministic and do not require a user password.
18
+ $serviceSidText = (& $sc showsid $name | Out-String)
19
+ if ($LASTEXITCODE -ne 0) { throw 'Cannot resolve broker service SID.' }
20
+ $serviceSid = [regex]::Match($serviceSidText, 'S-1-5-80-(?:[0-9]+-){4}[0-9]+').Value
21
+ if (-not $serviceSid) { throw 'Missing broker service SID.' }
22
+ if (-not ('DshBrokerRights' -as [type])) { Add-Type -Path (Join-Path $PSScriptRoot 'rights.cs') }
23
+ $rightsRecord = Join-Path $directory 'added-rights.json'
24
+ $service = Get-Service -Name $name -ErrorAction SilentlyContinue
25
+ if ($service) { Stop-Service $name -Force; & $sc delete $name | Out-Null; if ($LASTEXITCODE -ne 0) { throw 'Cannot remove previous broker service.' }; Start-Sleep -Milliseconds 500 }
26
+ # Revoke only rights this installer added; preserve pre-existing account policy.
27
+ if (Test-Path $rightsRecord) {
28
+ # Windows PowerShell 5.1 emits the JSON array as a single object; cast the
29
+ # result value directly (not inside @(...)) so it yields one element per
30
+ # recorded right.
31
+ [DshBrokerRights]::Remove($serviceSid, [string[]](Get-Content -Raw $rightsRecord | ConvertFrom-Json))
32
+ Remove-Item $rightsRecord -Force
33
+ }
34
+ if ($Uninstall) { if (Test-Path $directory) { Remove-Item $directory -Recurse -Force }; Write-Output "Removed $name"; return }
35
+ New-Item -ItemType Directory -Path $directory -Force | Out-Null
36
+ # The installed code and directory must never be writable by the client user.
37
+ $acl = [Security.AccessControl.DirectorySecurity]::new()
38
+ $acl.SetAccessRuleProtection($true, $false)
39
+ foreach ($trustee in @('S-1-5-18', 'S-1-5-32-544')) {
40
+ $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new($trustee), 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
41
+ }
42
+ $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new($serviceSid), 'ReadAndExecute', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
43
+ $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new($sid), 'ReadAndExecute', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
44
+ $acl.SetOwner([Security.Principal.SecurityIdentifier]::new('S-1-5-32-544'))
45
+ Set-Acl -LiteralPath $directory -AclObject $acl
46
+ $exe = Join-Path $directory 'broker.exe'
47
+ if (Test-Path $exe) { Remove-Item $exe -Force }
48
+ $framework = Join-Path $env:SystemRoot 'Microsoft.NET/Framework64/v4.0.30319'
49
+ $compiler = Join-Path $framework 'csc.exe'
50
+ & $compiler /nologo /target:exe /platform:anycpu "/out:$exe" "/reference:$(Join-Path $framework 'System.ServiceProcess.dll')" (Join-Path $PSScriptRoot 'service.cs')
51
+ if ($LASTEXITCODE -ne 0) { throw 'Broker compilation failed.' }
52
+ $pendingRecord = "$rightsRecord.pending"
53
+ $addedRights = [string[]]@()
54
+ $serviceCreated = $false
55
+ try {
56
+ & $sc create $name binPath= "`"$exe`" $sid $name" start= auto obj= "NT SERVICE\$name" | Out-Null
57
+ if ($LASTEXITCODE -ne 0) { throw 'Broker service registration failed.' }
58
+ $serviceCreated = $true
59
+ $addedRights = [DshBrokerRights]::Install($serviceSid)
60
+ # Publish a complete record; a failed write must not leave malformed JSON
61
+ # on the upgrade/uninstall path.
62
+ ConvertTo-Json -InputObject @($addedRights) | Set-Content -LiteralPath $pendingRecord
63
+ Move-Item -LiteralPath $pendingRecord -Destination $rightsRecord -Force
64
+ Start-Service $name
65
+ (Get-Service $name).WaitForStatus('Running', [TimeSpan]::FromSeconds(15))
66
+ } catch {
67
+ $installationError = $_
68
+ try {
69
+ if ($serviceCreated) {
70
+ $failedService = Get-Service -Name $name -ErrorAction SilentlyContinue
71
+ if ($failedService -and $failedService.Status -ne 'Stopped') { Stop-Service $name -Force }
72
+ }
73
+ } finally {
74
+ try {
75
+ # Revoke exactly this attempt's additions even if stopping fails.
76
+ # Remove the record only AFTER successful revocation.
77
+ [DshBrokerRights]::Remove($serviceSid, $addedRights)
78
+ Remove-Item -LiteralPath $pendingRecord -Force -ErrorAction SilentlyContinue
79
+ Remove-Item -LiteralPath $rightsRecord -Force -ErrorAction SilentlyContinue
80
+ } finally {
81
+ if ($serviceCreated) {
82
+ & $sc delete $name | Out-Null
83
+ if ($LASTEXITCODE -ne 0) { throw 'Cannot roll back failed broker service registration.' }
84
+ }
85
+ }
86
+ }
87
+ throw $installationError
88
+ }
89
+ Write-Output "Installed $name for $sid"
@@ -0,0 +1,58 @@
1
+ // Installer-only LSA policy adapter. This code never runs in the broker or
2
+ // accepts rights chosen by a client. Only the dedicated service SID is valid.
3
+ using System;
4
+ using System.ComponentModel;
5
+ using System.Collections.Generic;
6
+ using System.Runtime.InteropServices;
7
+ using System.Security.Principal;
8
+ using System.Text.RegularExpressions;
9
+
10
+ public static class DshBrokerRights {
11
+ static readonly string[] Required = { "SeServiceLogonRight", "SeCreateTokenPrivilege", "SeAssignPrimaryTokenPrivilege", "SeTcbPrivilege", "SeDebugPrivilege", "SeImpersonatePrivilege" };
12
+ public static string[] Install(string serviceSid) { return Change(serviceSid, null); }
13
+ public static void Remove(string serviceSid, string[] added) { Change(serviceSid, added); }
14
+ static string[] Change(string serviceSid, string[] remove) {
15
+ if (!Regex.IsMatch(serviceSid, @"^S-1-5-80-(?:[0-9]+-){4}[0-9]+$")) throw new ArgumentException("Expected dedicated service SID");
16
+ var identity = new SecurityIdentifier(serviceSid); var sid = new byte[identity.BinaryLength]; identity.GetBinaryForm(sid, 0);
17
+ var attributes = new ObjectAttributes { Length = Marshal.SizeOf(typeof(ObjectAttributes)) };
18
+ IntPtr policy; Check(LsaOpenPolicy(IntPtr.Zero, ref attributes, 0x810, out policy), "LsaOpenPolicy");
19
+ try {
20
+ var current = new HashSet<string>(StringComparer.Ordinal);
21
+ IntPtr list; uint count;
22
+ uint status = LsaEnumerateAccountRights(policy, sid, out list, out count);
23
+ if (status != 0xC0000034) {
24
+ Check(status, "LsaEnumerateAccountRights");
25
+ try {
26
+ if (count > 128) throw new ArgumentException("Invalid rights count");
27
+ for (int i = 0; i < count; i++) {
28
+ var item = (UnicodeString)Marshal.PtrToStructure(IntPtr.Add(list, i * Marshal.SizeOf(typeof(UnicodeString))), typeof(UnicodeString));
29
+ current.Add(Marshal.PtrToStringUni(item.Buffer, item.Length / 2));
30
+ }
31
+ } finally { LsaFreeMemory(list); }
32
+ }
33
+ var changes = new List<string>();
34
+ foreach (var right in remove ?? Required) {
35
+ if (Array.IndexOf(Required, right) < 0) throw new ArgumentException("Unexpected broker right");
36
+ if (remove == null ? !current.Contains(right) : current.Contains(right)) changes.Add(right);
37
+ }
38
+ if (changes.Count == 0) return changes.ToArray();
39
+ var values = new UnicodeString[changes.Count];
40
+ try {
41
+ for (int i = 0; i < values.Length; i++) values[i] = new UnicodeString { Length = checked((ushort)(changes[i].Length * 2)), MaximumLength = checked((ushort)(changes[i].Length * 2 + 2)), Buffer = Marshal.StringToHGlobalUni(changes[i]) };
42
+ Check(remove == null ? LsaAddAccountRights(policy, sid, values, (uint)values.Length)
43
+ : LsaRemoveAccountRights(policy, sid, false, values, (uint)values.Length), "Broker account rights");
44
+ } finally { foreach (var value in values) if (value.Buffer != IntPtr.Zero) Marshal.FreeHGlobal(value.Buffer); }
45
+ return changes.ToArray();
46
+ } finally { LsaClose(policy); }
47
+ }
48
+ static void Check(uint code, string name) { if (code != 0) throw new Win32Exception((int)LsaNtStatusToWinError(code), name); }
49
+ [StructLayout(LayoutKind.Sequential)] struct UnicodeString { public ushort Length, MaximumLength; public IntPtr Buffer; }
50
+ [StructLayout(LayoutKind.Sequential)] struct ObjectAttributes { public int Length; public IntPtr RootDirectory, ObjectName; public uint Attributes; public IntPtr SecurityDescriptor, SecurityQualityOfService; }
51
+ [DllImport("advapi32.dll")] static extern uint LsaOpenPolicy(IntPtr system, ref ObjectAttributes attributes, uint access, out IntPtr policy);
52
+ [DllImport("advapi32.dll")] static extern uint LsaEnumerateAccountRights(IntPtr policy, byte[] sid, out IntPtr rights, out uint count);
53
+ [DllImport("advapi32.dll")] static extern uint LsaAddAccountRights(IntPtr policy, byte[] sid, UnicodeString[] rights, uint count);
54
+ [DllImport("advapi32.dll")] static extern uint LsaRemoveAccountRights(IntPtr policy, byte[] sid, [MarshalAs(UnmanagedType.U1)] bool all, UnicodeString[] rights, uint count);
55
+ [DllImport("advapi32.dll")] static extern uint LsaNtStatusToWinError(uint status);
56
+ [DllImport("advapi32.dll")] static extern uint LsaFreeMemory(IntPtr memory);
57
+ [DllImport("advapi32.dll")] static extern uint LsaClose(IntPtr policy);
58
+ }