jishushell 0.4.24 → 0.5.15

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 (281) hide show
  1. package/INSTALL-NOTICE +11 -0
  2. package/apps/anythingllm-container.yaml +287 -0
  3. package/apps/browserless-chromium-container.yaml +90 -0
  4. package/apps/filebrowser-container.yaml +163 -0
  5. package/apps/hermes-container.yaml +36 -2
  6. package/apps/ollama-binary.yaml +91 -90
  7. package/apps/ollama-cpu-container.yaml +8 -1
  8. package/apps/ollama-with-hollama-binary.yaml +91 -90
  9. package/apps/openclaw-binary.yaml +38 -1
  10. package/apps/openclaw-container.yaml +45 -2
  11. package/apps/openclaw-with-ollama-container.yaml +11 -2
  12. package/apps/openclaw-with-searxng-container.yaml +26 -2
  13. package/apps/openwebui-container.yaml +45 -1
  14. package/apps/playwright-container.yaml +7 -1
  15. package/apps/searxng-container.yaml +58 -7
  16. package/apps/weknora-container.yaml +471 -0
  17. package/dist/cli/app.js +79 -9
  18. package/dist/cli/app.js.map +1 -1
  19. package/dist/cli/doctor.d.ts +12 -12
  20. package/dist/cli/doctor.js +242 -55
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/llm.d.ts +4 -3
  23. package/dist/cli/llm.js +4 -3
  24. package/dist/cli/llm.js.map +1 -1
  25. package/dist/cli/panel.d.ts +6 -5
  26. package/dist/cli/panel.js +10 -9
  27. package/dist/cli/panel.js.map +1 -1
  28. package/dist/config.d.ts +19 -0
  29. package/dist/config.js +99 -1
  30. package/dist/config.js.map +1 -1
  31. package/dist/control.d.ts +7 -6
  32. package/dist/control.js +7 -6
  33. package/dist/control.js.map +1 -1
  34. package/dist/install.js +3 -3
  35. package/dist/install.js.map +1 -1
  36. package/dist/routes/agent-apps.d.ts +1 -1
  37. package/dist/routes/agent-apps.js +1 -1
  38. package/dist/routes/apps.js +44 -11
  39. package/dist/routes/apps.js.map +1 -1
  40. package/dist/routes/auth.js +5 -2
  41. package/dist/routes/auth.js.map +1 -1
  42. package/dist/routes/backup.js +64 -11
  43. package/dist/routes/backup.js.map +1 -1
  44. package/dist/routes/external-mounts.d.ts +17 -0
  45. package/dist/routes/external-mounts.js +73 -0
  46. package/dist/routes/external-mounts.js.map +1 -0
  47. package/dist/routes/file-mounts.d.ts +13 -0
  48. package/dist/routes/file-mounts.js +90 -0
  49. package/dist/routes/file-mounts.js.map +1 -0
  50. package/dist/routes/files-organize.d.ts +28 -0
  51. package/dist/routes/files-organize.js +167 -0
  52. package/dist/routes/files-organize.js.map +1 -0
  53. package/dist/routes/files.d.ts +31 -0
  54. package/dist/routes/files.js +321 -0
  55. package/dist/routes/files.js.map +1 -0
  56. package/dist/routes/instances.js +826 -17
  57. package/dist/routes/instances.js.map +1 -1
  58. package/dist/routes/internal.d.ts +2 -0
  59. package/dist/routes/internal.js +59 -0
  60. package/dist/routes/internal.js.map +1 -0
  61. package/dist/routes/llm.js +24 -35
  62. package/dist/routes/llm.js.map +1 -1
  63. package/dist/routes/setup.js +10 -10
  64. package/dist/routes/setup.js.map +1 -1
  65. package/dist/routes/system.js +1 -1
  66. package/dist/routes/system.js.map +1 -1
  67. package/dist/routes/webdav.d.ts +17 -0
  68. package/dist/routes/webdav.js +114 -0
  69. package/dist/routes/webdav.js.map +1 -0
  70. package/dist/server.d.ts +9 -0
  71. package/dist/server.js +751 -20
  72. package/dist/server.js.map +1 -1
  73. package/dist/services/agent-apps/catalog.js +4 -3
  74. package/dist/services/agent-apps/catalog.js.map +1 -1
  75. package/dist/services/agent-apps/index.d.ts +1 -1
  76. package/dist/services/agent-apps/index.js +1 -1
  77. package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
  78. package/dist/services/agent-apps/installers/adapter.js +1 -1
  79. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  80. package/dist/services/agent-apps/installers/shell-script.js +3 -3
  81. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  82. package/dist/services/agent-apps/types.d.ts +2 -2
  83. package/dist/services/agent-apps/types.js +1 -1
  84. package/dist/services/app/app-compiler.d.ts +1 -1
  85. package/dist/services/app/app-compiler.js +5 -5
  86. package/dist/services/app/app-compiler.js.map +1 -1
  87. package/dist/services/app/app-manager.d.ts +25 -1
  88. package/dist/services/app/app-manager.js +829 -150
  89. package/dist/services/app/app-manager.js.map +1 -1
  90. package/dist/services/app/custom-manager.js.map +1 -1
  91. package/dist/services/app/hermes-agent-manager.js +7 -4
  92. package/dist/services/app/hermes-agent-manager.js.map +1 -1
  93. package/dist/services/app/ollama-manager.js +1 -1
  94. package/dist/services/app/ollama-manager.js.map +1 -1
  95. package/dist/services/app/openclaw-manager.js +20 -3
  96. package/dist/services/app/openclaw-manager.js.map +1 -1
  97. package/dist/services/app/platform-transform.d.ts +32 -0
  98. package/dist/services/app/platform-transform.js +65 -0
  99. package/dist/services/app/platform-transform.js.map +1 -0
  100. package/dist/services/app/provide-resolver.d.ts +29 -0
  101. package/dist/services/app/provide-resolver.js +112 -0
  102. package/dist/services/app/provide-resolver.js.map +1 -0
  103. package/dist/services/app-passwords.d.ts +61 -0
  104. package/dist/services/app-passwords.js +173 -0
  105. package/dist/services/app-passwords.js.map +1 -0
  106. package/dist/services/backup-manager.d.ts +11 -0
  107. package/dist/services/backup-manager.js +177 -4
  108. package/dist/services/backup-manager.js.map +1 -1
  109. package/dist/services/capability-endpoint-validator.d.ts +41 -0
  110. package/dist/services/capability-endpoint-validator.js +104 -0
  111. package/dist/services/capability-endpoint-validator.js.map +1 -0
  112. package/dist/services/capability-health.d.ts +16 -0
  113. package/dist/services/capability-health.js +121 -0
  114. package/dist/services/capability-health.js.map +1 -0
  115. package/dist/services/capability-registry.d.ts +106 -0
  116. package/dist/services/capability-registry.js +313 -0
  117. package/dist/services/capability-registry.js.map +1 -0
  118. package/dist/services/connection-apply.d.ts +91 -0
  119. package/dist/services/connection-apply.js +475 -0
  120. package/dist/services/connection-apply.js.map +1 -0
  121. package/dist/services/connection-resolver.d.ts +65 -0
  122. package/dist/services/connection-resolver.js +281 -0
  123. package/dist/services/connection-resolver.js.map +1 -0
  124. package/dist/services/connection-transactor.d.ts +39 -0
  125. package/dist/services/connection-transactor.js +351 -0
  126. package/dist/services/connection-transactor.js.map +1 -0
  127. package/dist/services/external-mounts.d.ts +40 -0
  128. package/dist/services/external-mounts.js +187 -0
  129. package/dist/services/external-mounts.js.map +1 -0
  130. package/dist/services/files-manager.d.ts +252 -0
  131. package/dist/services/files-manager.js +1075 -0
  132. package/dist/services/files-manager.js.map +1 -0
  133. package/dist/services/files-mounts.d.ts +42 -0
  134. package/dist/services/files-mounts.js +207 -0
  135. package/dist/services/files-mounts.js.map +1 -0
  136. package/dist/services/instance-manager.d.ts +13 -0
  137. package/dist/services/instance-manager.js +138 -46
  138. package/dist/services/instance-manager.js.map +1 -1
  139. package/dist/services/llm-proxy/index.d.ts +16 -2
  140. package/dist/services/llm-proxy/index.js +48 -44
  141. package/dist/services/llm-proxy/index.js.map +1 -1
  142. package/dist/services/llm-proxy/probe.d.ts +6 -0
  143. package/dist/services/llm-proxy/probe.js +85 -0
  144. package/dist/services/llm-proxy/probe.js.map +1 -0
  145. package/dist/services/llm-proxy/ssrf.d.ts +1 -0
  146. package/dist/services/llm-proxy/ssrf.js +24 -9
  147. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  148. package/dist/services/nomad-manager.d.ts +4 -0
  149. package/dist/services/nomad-manager.js +428 -35
  150. package/dist/services/nomad-manager.js.map +1 -1
  151. package/dist/services/organize/applier.d.ts +46 -0
  152. package/dist/services/organize/applier.js +218 -0
  153. package/dist/services/organize/applier.js.map +1 -0
  154. package/dist/services/organize/rules.d.ts +57 -0
  155. package/dist/services/organize/rules.js +286 -0
  156. package/dist/services/organize/rules.js.map +1 -0
  157. package/dist/services/organize/scanner.d.ts +50 -0
  158. package/dist/services/organize/scanner.js +366 -0
  159. package/dist/services/organize/scanner.js.map +1 -0
  160. package/dist/services/organize/store.d.ts +14 -0
  161. package/dist/services/organize/store.js +82 -0
  162. package/dist/services/organize/store.js.map +1 -0
  163. package/dist/services/panel-manager.js +20 -1
  164. package/dist/services/panel-manager.js.map +1 -1
  165. package/dist/services/process-manager.js +4 -3
  166. package/dist/services/process-manager.js.map +1 -1
  167. package/dist/services/runtime/adapters/hermes.d.ts +30 -1
  168. package/dist/services/runtime/adapters/hermes.js +219 -6
  169. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  170. package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
  171. package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
  172. package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
  173. package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
  174. package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
  175. package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
  176. package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
  177. package/dist/services/runtime/adapters/openclaw.js +1171 -11
  178. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  179. package/dist/services/runtime/instance.d.ts +1 -1
  180. package/dist/services/runtime/instance.js +1 -1
  181. package/dist/services/runtime/instance.js.map +1 -1
  182. package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
  183. package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
  184. package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
  185. package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
  186. package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
  187. package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
  188. package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
  189. package/dist/services/runtime/mcp-shims/firewall.js +129 -0
  190. package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
  191. package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
  192. package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
  193. package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
  194. package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
  195. package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
  196. package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
  197. package/dist/services/runtime/migrations.d.ts +8 -0
  198. package/dist/services/runtime/migrations.js +100 -0
  199. package/dist/services/runtime/migrations.js.map +1 -1
  200. package/dist/services/runtime/types.d.ts +46 -0
  201. package/dist/services/setup-manager.js +99 -24
  202. package/dist/services/setup-manager.js.map +1 -1
  203. package/dist/services/suggestions.d.ts +27 -0
  204. package/dist/services/suggestions.js +133 -0
  205. package/dist/services/suggestions.js.map +1 -0
  206. package/dist/services/task-registry.js +4 -2
  207. package/dist/services/task-registry.js.map +1 -1
  208. package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
  209. package/dist/services/telemetry/device-fingerprint.js +1 -1
  210. package/dist/services/types-shim.d.ts +16 -0
  211. package/dist/services/types-shim.js +2 -0
  212. package/dist/services/types-shim.js.map +1 -0
  213. package/dist/services/webdav/server.d.ts +24 -0
  214. package/dist/services/webdav/server.js +420 -0
  215. package/dist/services/webdav/server.js.map +1 -0
  216. package/dist/services/webdav/xml-builder.d.ts +73 -0
  217. package/dist/services/webdav/xml-builder.js +156 -0
  218. package/dist/services/webdav/xml-builder.js.map +1 -0
  219. package/dist/services/workspace-builder.d.ts +29 -0
  220. package/dist/services/workspace-builder.js +188 -0
  221. package/dist/services/workspace-builder.js.map +1 -0
  222. package/dist/types.d.ts +231 -1
  223. package/dist/utils/instance-lock.d.ts +22 -0
  224. package/dist/utils/instance-lock.js +48 -0
  225. package/dist/utils/instance-lock.js.map +1 -0
  226. package/dist/utils/path-locks.d.ts +30 -0
  227. package/dist/utils/path-locks.js +63 -0
  228. package/dist/utils/path-locks.js.map +1 -0
  229. package/dist/utils/path-safety.d.ts +41 -0
  230. package/dist/utils/path-safety.js +119 -0
  231. package/dist/utils/path-safety.js.map +1 -0
  232. package/dist/utils/safe-json.js +55 -22
  233. package/dist/utils/safe-json.js.map +1 -1
  234. package/dist/utils/safe-write.d.ts +24 -0
  235. package/dist/utils/safe-write.js +82 -0
  236. package/dist/utils/safe-write.js.map +1 -0
  237. package/install/jishu-install.sh +323 -27
  238. package/install/jishu-uninstall.sh +353 -20
  239. package/package.json +18 -1
  240. package/public/assets/Dashboard-BdWPtroF.js +1 -0
  241. package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
  242. package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
  243. package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
  244. package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
  245. package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
  246. package/public/assets/NewInstance-BIYDmJis.js +1 -0
  247. package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
  248. package/public/assets/Settings-Cc-tYBil.js +1 -0
  249. package/public/assets/Setup-lGZEk5jq.js +1 -0
  250. package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
  251. package/public/assets/index-87IJXG-w.css +1 -0
  252. package/public/assets/index-BZc5zH7u.js +19 -0
  253. package/public/assets/providers-DtNXh9JD.js +1 -0
  254. package/public/assets/registry-BWnkJgZ1.js +2 -0
  255. package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
  256. package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
  257. package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
  258. package/public/index.html +4 -4
  259. package/scripts/check-app-spec.mjs +457 -0
  260. package/scripts/check-i18n.mjs +154 -0
  261. package/scripts/check-new-file-tests.mjs +230 -0
  262. package/scripts/check-quarantine-expiry.mjs +105 -0
  263. package/scripts/perf/README.md +49 -0
  264. package/scripts/perf/auth.js +99 -0
  265. package/scripts/perf/config.js +63 -0
  266. package/scripts/perf/instances.js +143 -0
  267. package/scripts/perf/proxy.js +96 -0
  268. package/scripts/run.sh +4 -4
  269. package/scripts/smoke/files-w1.sh +142 -0
  270. package/scripts/smoke-backend.mjs +122 -0
  271. package/scripts/smoke-post-publish.mjs +346 -0
  272. package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
  273. package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
  274. package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
  275. package/public/assets/NewInstance-BCIrAd86.js +0 -1
  276. package/public/assets/Settings-xkDcduFz.js +0 -1
  277. package/public/assets/Setup-Cfuwj4gV.js +0 -1
  278. package/public/assets/index-CPhVFEsx.css +0 -1
  279. package/public/assets/index-DQsM6Joa.js +0 -19
  280. package/public/assets/providers-V-vwrExZ.js +0 -1
  281. package/public/assets/registry-B4UFJdpA.js +0 -2
package/dist/types.d.ts CHANGED
@@ -26,6 +26,12 @@ export interface ServiceResult {
26
26
  eval_id?: string;
27
27
  /** Port allocation metadata from adapters (process-manager / nomad-manager). */
28
28
  port_allocation?: unknown;
29
+ /**
30
+ * Stable error code for callers that prefer machine-readable tags over
31
+ * regex-matching localized error messages. Populated by PR 3
32
+ * resolveConnections (`MISSING_REQUIRED_CONNECTION` / `AMBIGUOUS_CONNECTION` / etc).
33
+ */
34
+ code?: string;
29
35
  }
30
36
  export interface ExecResult {
31
37
  stdout: string;
@@ -105,6 +111,25 @@ export interface PanelConfig {
105
111
  };
106
112
  telemetry?: false;
107
113
  activation_reported?: boolean;
114
+ /**
115
+ * External mount points — let any host directory appear under
116
+ * ~/.jishushell/files/ as a virtual subtree. The user picks the
117
+ * alias; host_path can be any directory the panel user can read
118
+ * (sensitive system directories are flagged but allowed if the
119
+ * user insists). Mode "ro" forbids any write through the panel
120
+ * or WebDAV; "rw" allows it.
121
+ *
122
+ * Implementation: the panel does NOT create a real symlink in
123
+ * files/ — it virtualizes the path at the FilesManager layer.
124
+ * That avoids the symlink-defense conflict and works identically
125
+ * across docker / raw_exec / process modes.
126
+ */
127
+ external_mounts?: Array<{
128
+ alias: string;
129
+ host_path: string;
130
+ mode: "ro" | "rw";
131
+ description?: string;
132
+ }>;
108
133
  [key: string]: any;
109
134
  }
110
135
  export type AppTaskRuntime = "container" | "process" | "vm";
@@ -120,6 +145,16 @@ export interface AppTaskPort {
120
145
  host_port?: number;
121
146
  container_port?: number;
122
147
  visibility?: AppTaskPortVisibility;
148
+ /**
149
+ * Name of a Nomad `host_network` block declared in nomad.hcl. Default is
150
+ * "external" (eth0 / primary LAN interface). Use "docker_bridge" to
151
+ * publish on the docker0 IP (172.17.0.1) — required when peer containers
152
+ * in the same group dial this port via `host.docker.internal`. Without
153
+ * this, Nomad picks the wrong interface and cross-task
154
+ * host.docker.internal calls get "connection refused" (e.g. weknora-app
155
+ * → paradedb on port 18093).
156
+ */
157
+ host_network?: string;
123
158
  }
124
159
  export interface AppTaskHealth {
125
160
  http?: {
@@ -157,6 +192,29 @@ export interface AppTask {
157
192
  after?: string[];
158
193
  /** Volumes: string format "src:dest[:ro]" or object { source, target, readonly? } */
159
194
  volumes?: Array<string | AppTaskVolume>;
195
+ /**
196
+ * Container task user spec. Format: "uid:gid" (numeric) or "uid". Special
197
+ * value "host" resolves to the panel process's `process.getuid()`:`getgid()`
198
+ * at job-build time — keeps bind-mounted data dirs writable by the panel
199
+ * user (typically `pi`) without forcing the container to run as root and
200
+ * without needing `chown`/`CAP_DAC_OVERRIDE` gymnastics. Mirrors how the
201
+ * hermes / openclaw adapters set `User`. When omitted on container tasks
202
+ * the unified builder falls back to "host" by default.
203
+ */
204
+ user?: string;
205
+ /**
206
+ * Linux capabilities to add back on top of the runtime baseline
207
+ * (`cap_drop: ["ALL"]`). Validated against a tight allowlist
208
+ * (CHOWN / DAC_OVERRIDE / FOWNER / SETUID / SETGID / SETPCAP /
209
+ * NET_BIND_SERVICE) before passing to the docker driver. Required by
210
+ * canonical postgres/redis/-style images whose entrypoint runs as root
211
+ * and uses `gosu`+`chown` to drop privileges to a non-root user before
212
+ * exec'ing the main process. The container still drops to a non-root
213
+ * user before serving traffic — this only re-arms the kernel facilities
214
+ * the drop step needs. Caps outside the allowlist are silently dropped
215
+ * (fail-closed).
216
+ */
217
+ cap_add?: string[];
160
218
  }
161
219
  export interface AppTerminalCommandPreset {
162
220
  cmd: string;
@@ -173,25 +231,183 @@ export interface AppTerminalConfig {
173
231
  timeout_ms?: number;
174
232
  commands?: Record<string, Array<string | AppTerminalCommandPreset>>;
175
233
  }
234
+ /**
235
+ * Health-probe spec attached to a provide. When omitted, the prober derives
236
+ * a default strategy from `protocol` (§5.6: http→HEAD, ws/tcp→connect,
237
+ * mcp→initialize, terminal→no-op).
238
+ */
239
+ export interface ProvideHealthSpec {
240
+ kind?: "http" | "tcp" | "mcp" | "none";
241
+ /** HTTP path to probe (defaults to "/"). Ignored for non-http kinds. */
242
+ path?: string;
243
+ /** Custom port to probe; defaults to the provide's resolved hostPort. */
244
+ port?: number;
245
+ }
176
246
  export interface AppProvide {
177
247
  capability: string;
248
+ /** Task that exposes this capability — required for resolveProvideEndpoint
249
+ * (§5.5) to pick the right port when an app has multiple service tasks. */
250
+ task?: string;
251
+ /** Optional port-name disambiguator when multiple ports share a number. */
252
+ portName?: string;
178
253
  port?: number;
179
254
  path?: string;
180
255
  url?: string;
181
- protocol?: "http" | "https" | "tcp" | "udp" | (string & {});
256
+ protocol?: "http" | "https" | "tcp" | "udp" | "ws" | "wss" | "sse" | "mcp" | "terminal" | (string & {});
182
257
  visibility?: string;
183
258
  description?: string;
184
259
  terminal?: AppTerminalConfig;
260
+ /** Optional explicit health probe; defaults derived from `protocol` (§5.6). */
261
+ health?: ProvideHealthSpec;
262
+ /**
263
+ * §17 (PR 8) — canonical MCP tool surface. Filled when this capability
264
+ * is meant to be re-exposed to an LLM agent through an MCP server. The
265
+ * jishushell MCP firewall uses these fields verbatim — upstream
266
+ * `tools/list` descriptions are discarded — to prevent third-party
267
+ * packages from prompt-injecting the LLM via their own tool wording.
268
+ * When absent, adapters fall back to the legacy per-capability path
269
+ * (PR 7 in-tree shim or direct `npx <pkg>` wiring).
270
+ */
271
+ tool_schema?: ToolSchema;
272
+ /**
273
+ * §6 — describes how a consumer that binds this `provides[]` capability
274
+ * should authenticate when calling the endpoint. Optional. Absent = no auth.
275
+ */
276
+ auth?: AppProvideAuth;
277
+ /**
278
+ * Controls how the instance detail page generates the iframe `src` for
279
+ * this capability:
280
+ *
281
+ * - `"auto"` (default): direct LAN URL when the published port listens
282
+ * on a non-loopback interface, fall back to panel proxy when only
283
+ * loopback. Best for apps users typically reach by IP+port.
284
+ * - `"proxy"`: always use the panel's same-origin reverse proxy path
285
+ * (`/api/instances/:id/provides/:capability/`). Needed when the
286
+ * upstream is on a network the browser can't dial directly (corp
287
+ * firewall blocks high ports, VPN, mixed-content boundary) or when
288
+ * the upstream emits `X-Frame-Options: SAMEORIGIN`/CSP that block
289
+ * cross-origin iframe embedding — the proxy strips those headers.
290
+ * - `"direct"`: always emit the direct LAN URL, never the proxy path.
291
+ * Useful for upstreams that can't safely run behind a sub-path
292
+ * reverse proxy (some SPAs hardcode absolute asset URLs).
293
+ */
294
+ embedded?: "auto" | "proxy" | "direct";
295
+ }
296
+ /**
297
+ * §6 — describes how a consumer that binds this `provides[]` capability
298
+ * should authenticate when calling the endpoint. Optional. Absent = no auth.
299
+ *
300
+ * `tokenSource` is a small DSL the apply hook resolves at apply time:
301
+ * "instance.env.<NAME>" — read from the provider instance's runtime env
302
+ * "instance.config.<path>" — read from the provider's openclaw.json (json-pointer)
303
+ * "proxy.token" — read from the embedded llm-proxy's per-instance token
304
+ *
305
+ * Validation rules (PR A): `tokenSource` is required when kind !== "none".
306
+ * At the type level we keep it optional because the validator catches violations.
307
+ */
308
+ export interface AppProvideAuth {
309
+ kind: "none" | "bearer" | "header" | "query";
310
+ tokenSource?: string;
311
+ headerName?: string;
312
+ tokenPrefix?: string;
313
+ }
314
+ /**
315
+ * §17.3.1 — what the LLM sees for an MCP-class capability.
316
+ *
317
+ * `name` / `description` / `parameters` are forwarded verbatim by the
318
+ * MCP firewall's `tools/list` response, replacing whatever the upstream
319
+ * package would have advertised. `upstream` tells the firewall how to
320
+ * spawn the actual implementation.
321
+ */
322
+ export interface ToolSchema {
323
+ /** MCP tool name. Must match `^[a-z][a-z0-9_]*$`. */
324
+ name: string;
325
+ /**
326
+ * Human-language description shown to the LLM. Keep minimal —
327
+ * "news / recent events / today" style suggestions invite query
328
+ * rewriting in some models (see §17.1).
329
+ */
330
+ description: string;
331
+ /**
332
+ * MCP `inputSchema`. Stored under the `parameters` key in yaml for
333
+ * brevity; the firewall publishes it as `inputSchema` on the wire.
334
+ */
335
+ parameters: {
336
+ type: "object";
337
+ properties: Record<string, Record<string, any>>;
338
+ required?: string[];
339
+ };
340
+ /**
341
+ * How the firewall spawns the actual upstream MCP server. Args are
342
+ * passed through unchanged. `env_template` values may reference
343
+ * `${SLOT}` placeholders that the writeMcpEntry helper resolves
344
+ * against the connection-resolver's env output before serializing
345
+ * into firewall config.
346
+ */
347
+ upstream: {
348
+ command: string;
349
+ args?: string[];
350
+ env_template?: Record<string, string>;
351
+ };
352
+ /**
353
+ * When true (default), wrap each `tools/call` result in
354
+ * `{untrusted: true, source: <capability_id>, content: ...}` so the
355
+ * LLM treats output as untrusted data, not instructions. Mirrors
356
+ * OpenClaw's `wrapWebContent` defense.
357
+ */
358
+ wrap_outputs?: boolean;
185
359
  }
186
360
  export interface AppRequire {
187
361
  capability: string;
188
362
  inject_as: string;
189
363
  required?: boolean;
364
+ /**
365
+ * Multi-candidate semantics. `"one"` (default) lets the user pick exactly
366
+ * one provider; `"many"` allows multiple providers (e.g. several MCP
367
+ * servers merged together).
368
+ */
369
+ cardinality?: "one" | "many";
370
+ /**
371
+ * LLM apply mode (§7.1.2). Only meaningful when `capability` resolves to
372
+ * the `llm` category. `"proxy-upstream"` writes the consumer instance's
373
+ * `x-jishushell.proxy.upstream` (OpenClaw / Hermes default); `"openai-env"`
374
+ * injects `OPENAI_API_BASE_URL` / `OPENAI_API_KEY` directly into runtime
375
+ * env (Open WebUI / generic OpenAI clients). Omit to fall back to the
376
+ * adapter-type default.
377
+ */
378
+ apply?: "proxy-upstream" | "openai-env";
379
+ }
380
+ /**
381
+ * Persisted binding state. Stored under `instance.json.connections[slot]`.
382
+ * `null` denotes user-explicit disconnect (do not auto-fall-back).
383
+ * Missing slot = "never set" (legacy single-candidate fallback may apply).
384
+ */
385
+ export interface InstanceConnectionSingle {
386
+ kind: "single";
387
+ providerId: string;
388
+ capability: string;
389
+ /** LLM-only: which model the user picked. */
390
+ selectedModelId?: string;
391
+ }
392
+ export interface InstanceConnectionMany {
393
+ kind: "many";
394
+ providers: Array<{
395
+ providerId: string;
396
+ capability: string;
397
+ }>;
398
+ }
399
+ export type InstanceConnection = InstanceConnectionSingle | InstanceConnectionMany | null;
400
+ export type InstanceConnections = Record<string, InstanceConnection>;
401
+ export interface DismissedSuggestion {
402
+ slot: string;
403
+ until: string;
190
404
  }
191
405
  export type AppLifecycleStep = {
192
406
  run: string;
193
407
  timeout_ms?: number;
194
408
  successIfCommandExists?: string;
409
+ sudo?: boolean;
410
+ ifFileExists?: string;
195
411
  } | {
196
412
  downloadImage: string;
197
413
  timeout_ms?: number;
@@ -207,6 +423,12 @@ export type AppLifecycleStep = {
207
423
  deleteBinary: string;
208
424
  } | {
209
425
  mkdir: string;
426
+ } | {
427
+ chown: {
428
+ path: string;
429
+ owner: string;
430
+ recursive?: boolean;
431
+ };
210
432
  } | {
211
433
  deleteDir: string;
212
434
  } | {
@@ -217,6 +439,14 @@ export interface AppLifecycle {
217
439
  pre_install?: AppLifecycleStep[];
218
440
  /** Steps run during `app install`, before the job is created. */
219
441
  install?: AppLifecycleStep[];
442
+ /**
443
+ * Steps run on every app start, immediately before the Nomad job is
444
+ * submitted. Use for idempotent invariants the runtime depends on but
445
+ * that may drift between starts (e.g. chowning a bind-mount to match
446
+ * the container's runtime user when `cap_drop: ALL` strips
447
+ * CAP_DAC_OVERRIDE).
448
+ */
449
+ pre_start?: AppLifecycleStep[];
220
450
  /** Steps run during `app uninstall`, before files are removed. */
221
451
  uninstall?: AppLifecycleStep[];
222
452
  /** Steps run after the Nomad job transitions to `running`. */
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Per-instance promise-chain mutex for serializing operations that mutate
3
+ * a single instance's durable state across multiple files (instance.json,
4
+ * provider.env, openclaw.json, mcporter.json, …).
5
+ *
6
+ * Mirrors the existing pattern in `llm-proxy/index.ts:_configSaveLocks`:
7
+ * each `withInstanceLock(id, fn)` call chains onto whatever promise was
8
+ * last queued for that id, so concurrent callers see strict FIFO ordering.
9
+ * The map self-cleans entries when the chain settles to keep memory flat.
10
+ *
11
+ * Use this for any code path that performs more than one file write on a
12
+ * given instance id (PUT /connections transactor, startApp, stopApp,
13
+ * restartApp). Operations on different instance ids never block each other.
14
+ */
15
+ /**
16
+ * Run `fn` exclusively against `instanceId`. Returns `fn`'s value (or
17
+ * propagates its rejection). Pending callers form a FIFO chain — each
18
+ * waits for prior ones to settle, even if they reject.
19
+ */
20
+ export declare function withInstanceLock<T>(instanceId: string, fn: () => Promise<T>): Promise<T>;
21
+ /** Test-only helper for verifying the chain is empty between cases. */
22
+ export declare function __instanceLockSize(): number;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Per-instance promise-chain mutex for serializing operations that mutate
3
+ * a single instance's durable state across multiple files (instance.json,
4
+ * provider.env, openclaw.json, mcporter.json, …).
5
+ *
6
+ * Mirrors the existing pattern in `llm-proxy/index.ts:_configSaveLocks`:
7
+ * each `withInstanceLock(id, fn)` call chains onto whatever promise was
8
+ * last queued for that id, so concurrent callers see strict FIFO ordering.
9
+ * The map self-cleans entries when the chain settles to keep memory flat.
10
+ *
11
+ * Use this for any code path that performs more than one file write on a
12
+ * given instance id (PUT /connections transactor, startApp, stopApp,
13
+ * restartApp). Operations on different instance ids never block each other.
14
+ */
15
+ const _instanceLocks = new Map();
16
+ /**
17
+ * Run `fn` exclusively against `instanceId`. Returns `fn`'s value (or
18
+ * propagates its rejection). Pending callers form a FIFO chain — each
19
+ * waits for prior ones to settle, even if they reject.
20
+ */
21
+ export async function withInstanceLock(instanceId, fn) {
22
+ const prev = _instanceLocks.get(instanceId) ?? Promise.resolve();
23
+ // Swallow prior rejections so the chain doesn't break — each caller still
24
+ // gets its own promise's result. The map only tracks ordering, not health.
25
+ const current = prev.catch(() => undefined).then(() => fn());
26
+ // Identity check uses the same reference we store; .finally() returns a
27
+ // new promise, so we must store `current` and attach the cleanup as a
28
+ // side-effect, not store the .finally() result. The trailing .catch()
29
+ // swallows the cleanup-promise's rejection — without it Node logs an
30
+ // "unhandled rejection" each time `fn()` throws even though the caller
31
+ // already handled it via the returned `current`.
32
+ current
33
+ .finally(() => {
34
+ if (_instanceLocks.get(instanceId) === current) {
35
+ _instanceLocks.delete(instanceId);
36
+ }
37
+ })
38
+ .catch(() => {
39
+ // intentional no-op — caller handles the original rejection
40
+ });
41
+ _instanceLocks.set(instanceId, current);
42
+ return current;
43
+ }
44
+ /** Test-only helper for verifying the chain is empty between cases. */
45
+ export function __instanceLockSize() {
46
+ return _instanceLocks.size;
47
+ }
48
+ //# sourceMappingURL=instance-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-lock.js","sourceRoot":"","sources":["../../src/utils/instance-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,cAAc,GAAG,IAAI,GAAG,EAA4B,CAAC;AAE3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAAkB,EAClB,EAAoB;IAEpB,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IACjE,0EAA0E;IAC1E,2EAA2E;IAC3E,MAAM,OAAO,GAAe,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACzE,wEAAwE;IACxE,sEAAsE;IACtE,sEAAsE;IACtE,qEAAqE;IACrE,uEAAuE;IACvE,iDAAiD;IACjD,OAAO;SACJ,OAAO,CAAC,GAAG,EAAE;QACZ,IAAI,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,OAAO,EAAE,CAAC;YAC/C,cAAc,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC;SACD,KAAK,CAAC,GAAG,EAAE;QACV,4DAA4D;IAC9D,CAAC,CAAC,CAAC;IACL,cAAc,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACxC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,kBAAkB;IAChC,OAAO,cAAc,CAAC,IAAI,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Path-scoped advisory locks for the Files module (W1 PR-2).
3
+ *
4
+ * Single-process serialization: all callers using the same `path` string run
5
+ * in arrival order; different paths run independently in parallel.
6
+ *
7
+ * This is *advisory* — it only protects callers that opt in via withPathLock.
8
+ * It does not protect against external writers (Agent, WebDAV, ssh) that
9
+ * touch the file directly. Those paths have their own atomicity (POSIX
10
+ * rename) and the application's tolerance is "POSIX is source of truth".
11
+ *
12
+ * Stale locks (> 60s) are logged but not broken — correctness over recovery.
13
+ */
14
+ /**
15
+ * Run `fn` while holding an advisory lock on `path`.
16
+ *
17
+ * If another caller holds the lock, this call queues behind it; the queue
18
+ * preserves arrival order.
19
+ *
20
+ * Throws (or resolves) propagate from `fn`. The lock is always released.
21
+ */
22
+ export declare function withPathLock<T>(path: string, fn: () => Promise<T>): Promise<T>;
23
+ /**
24
+ * Test-only: clear all locks. Do not use in production.
25
+ */
26
+ export declare function _clearAllPathLocks(): void;
27
+ /**
28
+ * Test-only: number of active locks. Do not use in production.
29
+ */
30
+ export declare function _activePathLockCount(): number;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Path-scoped advisory locks for the Files module (W1 PR-2).
3
+ *
4
+ * Single-process serialization: all callers using the same `path` string run
5
+ * in arrival order; different paths run independently in parallel.
6
+ *
7
+ * This is *advisory* — it only protects callers that opt in via withPathLock.
8
+ * It does not protect against external writers (Agent, WebDAV, ssh) that
9
+ * touch the file directly. Those paths have their own atomicity (POSIX
10
+ * rename) and the application's tolerance is "POSIX is source of truth".
11
+ *
12
+ * Stale locks (> 60s) are logged but not broken — correctness over recovery.
13
+ */
14
+ const locks = new Map();
15
+ const STALE_LOCK_MS = 60_000;
16
+ /**
17
+ * Run `fn` while holding an advisory lock on `path`.
18
+ *
19
+ * If another caller holds the lock, this call queues behind it; the queue
20
+ * preserves arrival order.
21
+ *
22
+ * Throws (or resolves) propagate from `fn`. The lock is always released.
23
+ */
24
+ export async function withPathLock(path, fn) {
25
+ const existing = locks.get(path);
26
+ if (existing && Date.now() - existing.startedAt > STALE_LOCK_MS) {
27
+ console.warn(`[path-lock] stale lock detected for ${path} (held ${Date.now() - existing.startedAt}ms)`);
28
+ }
29
+ const previous = existing?.promise ?? Promise.resolve();
30
+ let release;
31
+ const next = new Promise((resolve) => {
32
+ release = resolve;
33
+ });
34
+ // Our slot in the chain: wait for previous holder, then release `next`
35
+ const ourPromise = previous.then(() => next);
36
+ const entry = { promise: ourPromise, startedAt: Date.now() };
37
+ locks.set(path, entry);
38
+ try {
39
+ await previous;
40
+ // Re-stamp startedAt to "actual hold start" for stale detection
41
+ entry.startedAt = Date.now();
42
+ return await fn();
43
+ }
44
+ finally {
45
+ release();
46
+ if (locks.get(path) === entry) {
47
+ locks.delete(path);
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * Test-only: clear all locks. Do not use in production.
53
+ */
54
+ export function _clearAllPathLocks() {
55
+ locks.clear();
56
+ }
57
+ /**
58
+ * Test-only: number of active locks. Do not use in production.
59
+ */
60
+ export function _activePathLockCount() {
61
+ return locks.size;
62
+ }
63
+ //# sourceMappingURL=path-locks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-locks.js","sourceRoot":"","sources":["../../src/utils/path-locks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;AAC3C,MAAM,aAAa,GAAG,MAAM,CAAC;AAE7B;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAY,EACZ,EAAoB;IAEpB,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,SAAS,GAAG,aAAa,EAAE,CAAC;QAChE,OAAO,CAAC,IAAI,CACV,uCAAuC,IAAI,UACzC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,SACxB,KAAK,CACN,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,EAAE,OAAO,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAExD,IAAI,OAAoB,CAAC;IACzB,MAAM,IAAI,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACzC,OAAO,GAAG,OAAO,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,uEAAuE;IACvE,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAc,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IACxE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAEvB,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC;QACf,gEAAgE;QAChE,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,OAAO,MAAM,EAAE,EAAE,CAAC;IACpB,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAC;QACV,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC;YAC9B,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB;IAChC,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC"}
@@ -0,0 +1,41 @@
1
+ export declare class PathSafetyError extends Error {
2
+ reason: string;
3
+ constructor(message: string, reason: string);
4
+ }
5
+ /**
6
+ * Validate a user-provided relative path string against common attack
7
+ * vectors. Performs no IO.
8
+ *
9
+ * Accepted:
10
+ * - "" (root)
11
+ * - "notes/x.md", "深/文件.txt", "with-dashes_and.dots"
12
+ *
13
+ * Rejected (PathSafetyError):
14
+ * - leading "/" (absolute)
15
+ * - drive letters ("C:\\foo", "Z:foo")
16
+ * - any "..", ".", or all-dot ("...", "....") segment
17
+ * - empty segments ("a//b")
18
+ * - leading or trailing "/"
19
+ * - backslash anywhere
20
+ * - null bytes or control chars (< 0x20)
21
+ * - segments ending in "." or " " (Windows quirk; some sync tools misbehave)
22
+ * - segment > 255 bytes (ext4 NAME_MAX)
23
+ * - total > 4096 bytes (Linux PATH_MAX)
24
+ * - non-string input
25
+ */
26
+ export declare function validateRelativePath(input: unknown): void;
27
+ /**
28
+ * Resolve a validated relative path inside a fixed root, then verify the
29
+ * resolved path is still inside that root using a separator-explicit prefix
30
+ * check (so `/foo` does not match `/foobar`).
31
+ *
32
+ * Performs no IO. Symlink resolution is the caller's responsibility.
33
+ *
34
+ * @throws PathSafetyError on validation failure or escape attempt
35
+ */
36
+ export declare function resolveSafe(root: string, rel: string): string;
37
+ /**
38
+ * Returns true if any segment of the path begins with ".".
39
+ * Used to optionally hide dotfiles in directory listings.
40
+ */
41
+ export declare function isHidden(input: string): boolean;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Path safety utilities for the Files module (W1).
3
+ *
4
+ * Three layers of defense (per .omc/plans/files-w1.md § 7.1):
5
+ * 1. validateRelativePath — string-level checks (this module, no IO)
6
+ * 2. resolveSafe — path.resolve + prefix check (this module, no IO)
7
+ * 3. realpath/symlink — performed by FilesManager (defense 3, IO)
8
+ *
9
+ * Empty string is the conventional root path and is accepted by both functions.
10
+ */
11
+ import { resolve, sep } from "path";
12
+ const PATH_MAX_BYTES = 4096;
13
+ const NAME_MAX_BYTES = 255;
14
+ export class PathSafetyError extends Error {
15
+ reason;
16
+ constructor(message, reason) {
17
+ super(message);
18
+ this.reason = reason;
19
+ this.name = "PathSafetyError";
20
+ }
21
+ }
22
+ /**
23
+ * Validate a user-provided relative path string against common attack
24
+ * vectors. Performs no IO.
25
+ *
26
+ * Accepted:
27
+ * - "" (root)
28
+ * - "notes/x.md", "深/文件.txt", "with-dashes_and.dots"
29
+ *
30
+ * Rejected (PathSafetyError):
31
+ * - leading "/" (absolute)
32
+ * - drive letters ("C:\\foo", "Z:foo")
33
+ * - any "..", ".", or all-dot ("...", "....") segment
34
+ * - empty segments ("a//b")
35
+ * - leading or trailing "/"
36
+ * - backslash anywhere
37
+ * - null bytes or control chars (< 0x20)
38
+ * - segments ending in "." or " " (Windows quirk; some sync tools misbehave)
39
+ * - segment > 255 bytes (ext4 NAME_MAX)
40
+ * - total > 4096 bytes (Linux PATH_MAX)
41
+ * - non-string input
42
+ */
43
+ export function validateRelativePath(input) {
44
+ if (typeof input !== "string") {
45
+ throw new PathSafetyError("path must be a string", "type");
46
+ }
47
+ if (input === "")
48
+ return; // root, ok
49
+ if (Buffer.byteLength(input, "utf8") > PATH_MAX_BYTES) {
50
+ throw new PathSafetyError("path too long", "path-length");
51
+ }
52
+ if (input.startsWith("/")) {
53
+ throw new PathSafetyError("path must be relative (no leading /)", "absolute");
54
+ }
55
+ if (input.includes("\\")) {
56
+ throw new PathSafetyError("backslash not allowed", "backslash");
57
+ }
58
+ // Windows drive letter: e.g. "C:\\foo", "Z:foo", "Z:/foo"
59
+ if (/^[A-Za-z]:/.test(input)) {
60
+ throw new PathSafetyError("drive letter not allowed", "drive-letter");
61
+ }
62
+ for (let i = 0; i < input.length; i++) {
63
+ const code = input.charCodeAt(i);
64
+ if (code === 0) {
65
+ throw new PathSafetyError("null byte not allowed", "null-byte");
66
+ }
67
+ if (code < 0x20) {
68
+ throw new PathSafetyError("control character not allowed", "control-char");
69
+ }
70
+ }
71
+ if (input.endsWith("/")) {
72
+ throw new PathSafetyError("path must not end with /", "trailing-slash");
73
+ }
74
+ const segments = input.split("/");
75
+ for (const seg of segments) {
76
+ if (seg === "") {
77
+ throw new PathSafetyError("empty path segment", "empty-segment");
78
+ }
79
+ // Reject "." ".." and any all-dots variant ("..." "...." etc.)
80
+ if (/^\.+$/.test(seg)) {
81
+ throw new PathSafetyError(`all-dots segment "${seg}" not allowed`, "dot-segment");
82
+ }
83
+ if (Buffer.byteLength(seg, "utf8") > NAME_MAX_BYTES) {
84
+ throw new PathSafetyError("path segment too long", "segment-length");
85
+ }
86
+ if (seg.endsWith(".") || seg.endsWith(" ")) {
87
+ throw new PathSafetyError(`segment ends with "." or space`, "windows-quirk");
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Resolve a validated relative path inside a fixed root, then verify the
93
+ * resolved path is still inside that root using a separator-explicit prefix
94
+ * check (so `/foo` does not match `/foobar`).
95
+ *
96
+ * Performs no IO. Symlink resolution is the caller's responsibility.
97
+ *
98
+ * @throws PathSafetyError on validation failure or escape attempt
99
+ */
100
+ export function resolveSafe(root, rel) {
101
+ validateRelativePath(rel);
102
+ const resolved = resolve(root, rel);
103
+ const rootWithSep = root.endsWith(sep) ? root : root + sep;
104
+ const rootNoSep = root.endsWith(sep) ? root.slice(0, -1) : root;
105
+ if (resolved !== rootNoSep && !resolved.startsWith(rootWithSep)) {
106
+ throw new PathSafetyError("path escapes root", "escape");
107
+ }
108
+ return resolved;
109
+ }
110
+ /**
111
+ * Returns true if any segment of the path begins with ".".
112
+ * Used to optionally hide dotfiles in directory listings.
113
+ */
114
+ export function isHidden(input) {
115
+ if (input === "")
116
+ return false;
117
+ return input.split("/").some(s => s.startsWith("."));
118
+ }
119
+ //# sourceMappingURL=path-safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-safety.js","sourceRoot":"","sources":["../../src/utils/path-safety.ts"],"names":[],"mappings":"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"}