@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.8

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 (95) hide show
  1. package/CHANGELOG.en.md +51 -1
  2. package/CHANGELOG.md +51 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +69 -4
  6. package/lib/client.js +2678 -276
  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-DR2LvmYZ.js +600 -0
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-Nlp2AiRX.js} +56 -24
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  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-CVAcT_gy.js +743 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +57 -9
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/compat.d.ts +15 -5
  64. package/lib/types/contract.d.ts +31 -19
  65. package/lib/types/dialects.d.ts +99 -3
  66. package/lib/types/fs.d.ts +32 -10
  67. package/lib/types/index.d.ts +8 -3
  68. package/lib/types/process-fence.d.ts +54 -0
  69. package/lib/types/profile-isolation.d.ts +178 -0
  70. package/lib/types/providers.d.ts +70 -0
  71. package/lib/types/registry.d.ts +132 -31
  72. package/lib/types/roots.d.ts +100 -13
  73. package/lib/types/sandbox.d.ts +59 -3
  74. package/lib/types/scope.d.ts +200 -14
  75. package/lib/types/windows-broker.d.ts +17 -0
  76. package/lib/types/windows-deny.d.ts +1 -0
  77. package/lib/types/windows-profile.d.ts +10 -13
  78. package/lib/types/windows-runner-contract.d.ts +97 -0
  79. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  80. package/lib/windows-runner.js +1256 -13
  81. package/lib/workspace-files.js +138 -3
  82. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  83. package/package.json +84 -62
  84. package/scripts/windows-broker/install.ps1 +89 -0
  85. package/scripts/windows-broker/rights.cs +58 -0
  86. package/scripts/windows-broker/service.cs +323 -0
  87. package/scripts/windows-broker/verify-install.ps1 +80 -0
  88. package/lib/command-CTdNvwt1.js +0 -791
  89. package/lib/compat-B6ZqbQBy.js +0 -295
  90. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  91. package/lib/sandbox-BRXl_FpF.js +0 -132
  92. package/lib/types/client/RootFileTree.d.ts +0 -12
  93. package/lib/types/panel-files.d.ts +0 -3
  94. package/lib/windows-profile-CiFsFYIV.js +0 -216
  95. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
@@ -1,11 +1,23 @@
1
- import { n as routeWorkspacePath, t as wrapMethod } from "./method-wrapper-Zc-JlDP3.js";
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";
2
5
  //#region src/compat/workspace-files.ts
3
6
  function adaptPathWatch(service, wrap) {
4
7
  return wrapMethod(service, "changes", (original) => original.length < 3 ? original : wrap(original));
5
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
+ }
6
14
  //#endregion
7
15
  //#region src/workspace-files.ts
8
- const inject = ["multiRootCompat", "multiRootScope"];
16
+ const inject = [
17
+ "multiRootCompat",
18
+ "multiRootProviders",
19
+ "multiRootScope"
20
+ ];
9
21
  function apply(ctx) {
10
22
  ctx.inject(["workspaceFiles", "fs"], (scope) => {
11
23
  const service = scope.get("workspaceFiles");
@@ -22,13 +34,57 @@ function apply(ctx) {
22
34
  if (!scope.multiRootScope.scopeOf(route.primaryRoot).includes(route.workspaceRoot)) throw await outside(path);
23
35
  return result;
24
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;
25
80
  const restoreWatch = adaptPathWatch(service, (originalChanges) => async function* (request, path, signal) {
26
- if (scope.multiRootScope.scopeOf(request.workspaceRoot).length === 0) {
81
+ if (scope.multiRootScope.scopeOf(request.workspaceRoot).length === 0 && registeredRoots(scope, request.workspaceRoot).length === 0) {
27
82
  yield* originalChanges.call(this, request, path, signal);
28
83
  return;
29
84
  }
30
85
  const route = await routeWorkspacePath(scope, request.workspaceRoot, path, signal);
31
86
  if (route === void 0 || route.workspaceRoot === route.primaryRoot) {
87
+ if (underWithheldRoot(scope, request.workspaceRoot, path)) throw await outside(path);
32
88
  yield* originalChanges.call(this, request, path, signal);
33
89
  return;
34
90
  }
@@ -55,11 +111,90 @@ function apply(ctx) {
55
111
  return () => {
56
112
  rowLifetime.abort(/* @__PURE__ */ new Error("multi-root workspace-files routing unloaded"));
57
113
  restoreList();
114
+ restoreStat();
115
+ restoreRead();
116
+ restoreReadBytes();
117
+ restoreReadAll?.();
118
+ restoreReadRelated?.();
58
119
  restoreWatch();
59
120
  };
60
121
  }, "multi-root:workspace-files routing");
61
122
  });
62
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
+ }
63
198
  async function outside(path) {
64
199
  const { RemoteError } = await import("@deepseek-ai/dsh-typert-protocol");
65
200
  return new RemoteError("workspace-file/outside-workspace", `"${path}" is no longer in this workspace`, { path });
@@ -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.6",
4
+ "version": "0.1.8",
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"
@@ -51,6 +55,10 @@
51
55
  "types": "./lib/types/workspace-files.d.ts",
52
56
  "default": "./lib/workspace-files.js"
53
57
  },
58
+ "./process-fence": {
59
+ "types": "./lib/types/process-fence.d.ts",
60
+ "default": "./lib/process-fence.js"
61
+ },
54
62
  "./windows-runner": {
55
63
  "types": "./lib/types/windows-runner.d.ts",
56
64
  "default": "./lib/windows-runner.js"
@@ -65,7 +73,9 @@
65
73
  "README.en.md",
66
74
  "CHANGELOG.md",
67
75
  "CHANGELOG.en.md",
68
- "LICENSE"
76
+ "LICENSE",
77
+ "scripts/windows-broker/*.ps1",
78
+ "scripts/windows-broker/*.cs"
69
79
  ],
70
80
  "publishConfig": {
71
81
  "access": "public"
@@ -79,27 +89,27 @@
79
89
  },
80
90
  "license": "MIT",
81
91
  "peerDependencies": {
82
- "@deepseek-ai/cordis": ">=4.0.1 <5.0.0 || 4.0.5-alpha.1",
83
- "@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",
84
- "@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",
85
- "@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",
86
- "@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",
87
- "@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",
88
- "@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",
89
- "@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",
90
- "@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",
91
- "@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",
92
- "@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",
93
- "@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",
94
- "@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",
95
- "@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",
96
- "@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",
97
- "@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",
98
- "@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",
99
- "@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",
100
- "@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",
101
- "@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",
102
- "@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"
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"
103
113
  },
104
114
  "peerDependenciesMeta": {
105
115
  "@deepseek-ai/dsh-agent": {
@@ -146,39 +156,54 @@
146
156
  }
147
157
  },
148
158
  "devDependencies": {
149
- "@deepseek-ai/cordis": "4.0.5-alpha.1",
150
- "@deepseek-ai/dsh": "0.2.1-alpha.2",
151
- "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
152
- "@deepseek-ai/dsh-agent-instructions": "0.2.1-alpha.2",
153
- "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.2",
154
- "@deepseek-ai/dsh-base": "0.2.1-alpha.2",
155
- "@deepseek-ai/dsh-client-connection": "0.2.1-alpha.2",
156
- "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.2",
157
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.2",
158
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.2",
159
- "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-alpha.2",
160
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.2",
161
- "@deepseek-ai/dsh-client-ui-workspace": "0.2.1-alpha.2",
162
- "@deepseek-ai/dsh-commands": "0.2.1-alpha.2",
163
- "@deepseek-ai/dsh-fs": "0.2.1-alpha.2",
164
- "@deepseek-ai/dsh-fs-local": "0.2.1-alpha.2",
165
- "@deepseek-ai/dsh-fs-sandbox": "0.2.1-alpha.2",
166
- "@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
167
- "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
168
- "@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.2",
169
- "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
170
- "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
171
- "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
172
- "@deepseek-ai/dsh-storage": "0.2.1-alpha.2",
173
- "@deepseek-ai/dsh-storage-domain": "0.2.1-alpha.2",
174
- "@deepseek-ai/dsh-storage-json": "0.2.1-alpha.2",
175
- "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.2",
176
- "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.2",
177
- "@deepseek-ai/dsh-workspace": "0.2.1-alpha.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",
178
201
  "@testing-library/react": "^16.3.3",
179
202
  "@types/node": "^22.0.0",
180
203
  "@types/react": "~18.3.31",
204
+ "@types/react-dom": "~18.3.7",
181
205
  "clsx": "^2.0.0",
206
+ "immer": "^10.2.0",
182
207
  "jsdom": "^29.1.1",
183
208
  "oxlint": "^1.76.0",
184
209
  "react": "^18.3.1",
@@ -187,13 +212,7 @@
187
212
  "typescript": "^6.0.3",
188
213
  "vitest": "^4.1.8",
189
214
  "yaml": "^2.9.0",
190
- "@deepseek-ai/dsh-lsp": "0.2.1-alpha.2",
191
- "@deepseek-ai/dsh-lsp-stdio": "0.2.1-alpha.2",
192
- "@deepseek-ai/dsh-api-workspace-files": "0.2.1-alpha.2",
193
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
194
- "@deepseek-ai/dsh-sandbox-windows-acl": "0.2.1-alpha.2",
195
- "@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.2",
196
- "@deepseek-ai/dsh-tools": "0.2.1-alpha.2"
215
+ "zustand": "~4.5.7"
197
216
  },
198
217
  "dsh": {
199
218
  "bundle": {
@@ -210,11 +229,12 @@
210
229
  }
211
230
  },
212
231
  "dependencies": {
213
- "zod": "^4.6.2",
214
232
  "@deepseek-ai/node-addon-system": "^0.1.2",
215
- "koffi": "^3.1.0"
233
+ "koffi": "^3.1.0",
234
+ "zod": "^4.6.2"
216
235
  },
217
236
  "scripts": {
237
+ "bench:roots": "node --experimental-transform-types scripts/bench-root-scale.mjs",
218
238
  "build": "pnpm run build:types && pnpm run bundle",
219
239
  "build:types": "tsc -p tsconfig.host.json && tsc -p tsconfig.client.json",
220
240
  "bundle": "node scripts/clean-lib.mjs && tsdown",
@@ -223,13 +243,15 @@
223
243
  "kernel:probe": "node scripts/check-kernel-runner.mjs",
224
244
  "lint": "oxlint",
225
245
  "release": "node scripts/bump-version.mjs",
246
+ "pack:check": "node scripts/check-pack.mjs",
226
247
  "smoke": "pnpm smoke:compose && pnpm smoke:behavior && pnpm smoke:journey",
227
248
  "smoke:behavior": "node scripts/smoke-behavior.mjs",
228
249
  "smoke:compose": "node scripts/smoke-compose.mjs",
229
250
  "smoke:journey": "node scripts/smoke-journey.mjs",
251
+ "smoke:packed": "node scripts/smoke-packed.mjs",
230
252
  "test": "vitest run",
231
253
  "test:watch": "vitest",
232
254
  "typecheck": "tsc -p tsconfig.host.json --noEmit && tsc -p tsconfig.client.json --noEmit",
233
- "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"
234
256
  }
235
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
+ }