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.
- package/INSTALL-NOTICE +11 -0
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +90 -0
- package/apps/filebrowser-container.yaml +163 -0
- package/apps/hermes-container.yaml +36 -2
- package/apps/ollama-binary.yaml +91 -90
- package/apps/ollama-cpu-container.yaml +8 -1
- package/apps/ollama-with-hollama-binary.yaml +91 -90
- package/apps/openclaw-binary.yaml +38 -1
- package/apps/openclaw-container.yaml +45 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +26 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +58 -7
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/app.js +79 -9
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/doctor.d.ts +12 -12
- package/dist/cli/doctor.js +242 -55
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/llm.d.ts +4 -3
- package/dist/cli/llm.js +4 -3
- package/dist/cli/llm.js.map +1 -1
- package/dist/cli/panel.d.ts +6 -5
- package/dist/cli/panel.js +10 -9
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.js.map +1 -1
- package/dist/install.js +3 -3
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +1 -1
- package/dist/routes/agent-apps.js +1 -1
- package/dist/routes/apps.js +44 -11
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/auth.js +5 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +826 -17
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +10 -10
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +751 -20
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.js +4 -3
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/index.d.ts +1 -1
- package/dist/services/agent-apps/index.js +1 -1
- package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
- package/dist/services/agent-apps/installers/adapter.js +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +3 -3
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +2 -2
- package/dist/services/agent-apps/types.js +1 -1
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +25 -1
- package/dist/services/app/app-manager.js +829 -150
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +7 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +20 -3
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app/provide-resolver.d.ts +29 -0
- package/dist/services/app/provide-resolver.js +112 -0
- package/dist/services/app/provide-resolver.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +177 -4
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.d.ts +41 -0
- package/dist/services/capability-endpoint-validator.js +104 -0
- package/dist/services/capability-endpoint-validator.js.map +1 -0
- package/dist/services/capability-health.d.ts +16 -0
- package/dist/services/capability-health.js +121 -0
- package/dist/services/capability-health.js.map +1 -0
- package/dist/services/capability-registry.d.ts +106 -0
- package/dist/services/capability-registry.js +313 -0
- package/dist/services/capability-registry.js.map +1 -0
- package/dist/services/connection-apply.d.ts +91 -0
- package/dist/services/connection-apply.js +475 -0
- package/dist/services/connection-apply.js.map +1 -0
- package/dist/services/connection-resolver.d.ts +65 -0
- package/dist/services/connection-resolver.js +281 -0
- package/dist/services/connection-resolver.js.map +1 -0
- package/dist/services/connection-transactor.d.ts +39 -0
- package/dist/services/connection-transactor.js +351 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +138 -46
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +16 -2
- package/dist/services/llm-proxy/index.js +48 -44
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/probe.d.ts +6 -0
- package/dist/services/llm-proxy/probe.js +85 -0
- package/dist/services/llm-proxy/probe.js.map +1 -0
- package/dist/services/llm-proxy/ssrf.d.ts +1 -0
- package/dist/services/llm-proxy/ssrf.js +24 -9
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +4 -0
- package/dist/services/nomad-manager.js +428 -35
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +20 -1
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +4 -3
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +30 -1
- package/dist/services/runtime/adapters/hermes.js +219 -6
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
- package/dist/services/runtime/adapters/openclaw.js +1171 -11
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
- package/dist/services/runtime/mcp-shims/firewall.js +129 -0
- package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +8 -0
- package/dist/services/runtime/migrations.js +100 -0
- package/dist/services/runtime/migrations.js.map +1 -1
- package/dist/services/runtime/types.d.ts +46 -0
- package/dist/services/setup-manager.js +99 -24
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.d.ts +27 -0
- package/dist/services/suggestions.js +133 -0
- package/dist/services/suggestions.js.map +1 -0
- package/dist/services/task-registry.js +4 -2
- package/dist/services/task-registry.js.map +1 -1
- package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
- package/dist/services/telemetry/device-fingerprint.js +1 -1
- package/dist/services/types-shim.d.ts +16 -0
- package/dist/services/types-shim.js +2 -0
- package/dist/services/types-shim.js.map +1 -0
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +231 -1
- package/dist/utils/instance-lock.d.ts +22 -0
- package/dist/utils/instance-lock.js +48 -0
- package/dist/utils/instance-lock.js.map +1 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +323 -27
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +18 -1
- package/public/assets/Dashboard-BdWPtroF.js +1 -0
- package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
- package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
- package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
- package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
- package/public/assets/NewInstance-BIYDmJis.js +1 -0
- package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
- package/public/assets/Settings-Cc-tYBil.js +1 -0
- package/public/assets/Setup-lGZEk5jq.js +1 -0
- package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
- package/public/assets/index-87IJXG-w.css +1 -0
- package/public/assets/index-BZc5zH7u.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-BWnkJgZ1.js +2 -0
- package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +457 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/run.sh +4 -4
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
- package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
- package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
- package/public/assets/NewInstance-BCIrAd86.js +0 -1
- package/public/assets/Settings-xkDcduFz.js +0 -1
- package/public/assets/Setup-Cfuwj4gV.js +0 -1
- package/public/assets/index-CPhVFEsx.css +0 -1
- package/public/assets/index-DQsM6Joa.js +0 -19
- package/public/assets/providers-V-vwrExZ.js +0 -1
- 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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"}
|