jishushell 0.4.24 → 0.4.30
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/browserless-chromium-container.yaml +78 -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 +30 -1
- package/apps/openclaw-container.yaml +37 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +22 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +54 -4
- 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/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.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 +3 -0
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/instances.js +787 -16
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +1 -1
- package/dist/routes/setup.js.map +1 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.js +410 -17
- 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-manager.d.ts +24 -1
- package/dist/services/app/app-manager.js +664 -116
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +6 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- 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/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 +89 -0
- package/dist/services/connection-apply.js +421 -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 +37 -0
- package/dist/services/connection-transactor.js +341 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +137 -23
- 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 +18 -7
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.js +375 -16
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/process-manager.js +1 -1
- 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 +218 -5
- 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.d.ts +87 -0
- package/dist/services/runtime/adapters/openclaw.js +250 -2
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- 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 +15 -0
- package/dist/services/setup-manager.js +6 -6
- 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/types.d.ts +171 -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/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/install/jishu-install.sh +323 -27
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +3 -1
- package/public/assets/Dashboard-rkWp-CXd.js +1 -0
- package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-_GHoklgo.js} +1 -1
- package/public/assets/HermesConfigForm-anDnwUp_.js +4 -0
- package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-ZU9_-hDr.js} +1 -1
- package/public/assets/InstanceDetail-CN0FH1aw.js +92 -0
- package/public/assets/{Login-BWsZH2mu.js → Login-BItXqYAJ.js} +1 -1
- package/public/assets/NewInstance-BousE6kY.js +1 -0
- package/public/assets/ProviderRecommendations-DFYj7Fb6.js +1 -0
- package/public/assets/Settings-Bttc6QmM.js +1 -0
- package/public/assets/Setup-Bsxx1zgj.js +1 -0
- package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-DPZpAKgO.js} +2 -2
- package/public/assets/index-8xZy1z5k.css +1 -0
- package/public/assets/index-Dw3HhUYE.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-5s2UB6is.js +2 -0
- package/public/index.html +2 -2
- package/scripts/check-app-spec.mjs +443 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/run.sh +4 -4
- 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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getAdvertisedHostForPort, getGatewayPort, getInstanceRuntime, } from "../instance-manager.js";
|
|
2
|
+
function isServiceTask(task) {
|
|
3
|
+
return (task.role ?? "service") === "service";
|
|
4
|
+
}
|
|
5
|
+
function pickTask(spec, provide) {
|
|
6
|
+
if (typeof provide.task === "string" && provide.task) {
|
|
7
|
+
return spec.tasks?.find((t) => t.name === provide.task);
|
|
8
|
+
}
|
|
9
|
+
return spec.tasks?.find(isServiceTask);
|
|
10
|
+
}
|
|
11
|
+
function hostPortOf(p) {
|
|
12
|
+
// host_port wins when defined (container yaml with host:container mapping);
|
|
13
|
+
// otherwise plain `port` is the host port too (process-mode tasks).
|
|
14
|
+
if (typeof p.host_port === "number" && p.host_port > 0)
|
|
15
|
+
return p.host_port;
|
|
16
|
+
if (typeof p.port === "number" && p.port > 0)
|
|
17
|
+
return p.port;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
function pickPort(task, provide) {
|
|
21
|
+
const ports = Array.isArray(task.ports) ? task.ports : [];
|
|
22
|
+
if (ports.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
// 1. Explicit portName disambiguation (multi-port-same-number rare case).
|
|
25
|
+
const portName = provide.portName;
|
|
26
|
+
if (typeof portName === "string" && portName) {
|
|
27
|
+
const named = ports.find((p) => p.name === portName);
|
|
28
|
+
return named ? hostPortOf(named) : undefined;
|
|
29
|
+
}
|
|
30
|
+
// 2. Match by provide.port number against task port's host-side number.
|
|
31
|
+
if (typeof provide.port === "number") {
|
|
32
|
+
const matched = ports.find((p) => hostPortOf(p) === provide.port);
|
|
33
|
+
if (matched)
|
|
34
|
+
return hostPortOf(matched);
|
|
35
|
+
// provide.port specified but no matching task port — caller declared
|
|
36
|
+
// a port that does not exist on this task. Honor declared value as a
|
|
37
|
+
// best-effort fallback so registry stays populated; the entry will
|
|
38
|
+
// fail health probing later if the port isn't actually listening.
|
|
39
|
+
return provide.port;
|
|
40
|
+
}
|
|
41
|
+
// 3. No provide.port — first port on the chosen task.
|
|
42
|
+
return hostPortOf(ports[0]);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Try to read the actual allocated host port from the instance's runtime
|
|
46
|
+
* spec. Currently only the gateway port is reliably tracked per-instance
|
|
47
|
+
* (see `extractGatewayPort` in instance-manager); other ports fall back
|
|
48
|
+
* to the spec value.
|
|
49
|
+
*
|
|
50
|
+
* For multi-port instances, runtime.ports[] is consulted by name match.
|
|
51
|
+
*/
|
|
52
|
+
function readRuntimePort(instanceId, taskName, portName, declaredHostPort) {
|
|
53
|
+
const runtime = getInstanceRuntime(instanceId);
|
|
54
|
+
if (!runtime)
|
|
55
|
+
return declaredHostPort;
|
|
56
|
+
// First: gateway port via the well-defined helper. Only useful when the
|
|
57
|
+
// requested port is the gateway port (named "gateway" or matching the
|
|
58
|
+
// declared spec port that the gateway helper resolves).
|
|
59
|
+
if (portName === "gateway" || taskName === "gateway") {
|
|
60
|
+
const gw = getGatewayPort(instanceId);
|
|
61
|
+
if (gw > 0)
|
|
62
|
+
return gw;
|
|
63
|
+
}
|
|
64
|
+
// Second: scan runtime.ports[] by name when present (RuntimeSpec shape).
|
|
65
|
+
const runtimePorts = Array.isArray(runtime.ports) ? runtime.ports : [];
|
|
66
|
+
if (portName) {
|
|
67
|
+
const named = runtimePorts.find((p) => p?.name === portName);
|
|
68
|
+
if (named && Number.isInteger(named.hostPort) && named.hostPort > 0) {
|
|
69
|
+
return named.hostPort;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Match by declared host port — runtime allocation may have reassigned
|
|
73
|
+
// it; the runtime entry name should still match the port spec name.
|
|
74
|
+
if (typeof declaredHostPort === "number") {
|
|
75
|
+
const matched = runtimePorts.find((p) => Number.isInteger(p?.hostPort) && p.hostPort > 0);
|
|
76
|
+
if (matched && runtimePorts.length === 1)
|
|
77
|
+
return matched.hostPort;
|
|
78
|
+
}
|
|
79
|
+
return declaredHostPort;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the provide's actual endpoint for a given instance.
|
|
83
|
+
*
|
|
84
|
+
* - Returns null for url-only provides (cannot enter capability registry,
|
|
85
|
+
* §5.1 boundary).
|
|
86
|
+
* - Returns null when no port can be determined from spec or runtime.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveProvideEndpoint(instanceId, spec, provide) {
|
|
89
|
+
// url-only provide — out of capability registry scope (see §5.1).
|
|
90
|
+
if (typeof provide.url === "string" && provide.url.trim())
|
|
91
|
+
return null;
|
|
92
|
+
const task = pickTask(spec, provide);
|
|
93
|
+
if (!task)
|
|
94
|
+
return null;
|
|
95
|
+
const declared = pickPort(task, provide);
|
|
96
|
+
if (typeof declared !== "number" || declared <= 0)
|
|
97
|
+
return null;
|
|
98
|
+
const portName = provide.portName ?? findPortName(task, declared);
|
|
99
|
+
const actual = readRuntimePort(instanceId, task.name, portName, declared) ?? declared;
|
|
100
|
+
const host = getAdvertisedHostForPort(actual);
|
|
101
|
+
return {
|
|
102
|
+
host,
|
|
103
|
+
hostPort: actual,
|
|
104
|
+
...(provide.path ? { path: provide.path } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function findPortName(task, hostPort) {
|
|
108
|
+
const ports = Array.isArray(task.ports) ? task.ports : [];
|
|
109
|
+
const matched = ports.find((p) => hostPortOf(p) === hostPort);
|
|
110
|
+
return matched?.name;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=provide-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provide-resolver.js","sourceRoot":"","sources":["../../../src/services/app/provide-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §12 of the app-interconnect design — `validateCapabilityEndpoint`.
|
|
3
|
+
*
|
|
4
|
+
* Runs in the transactor's Validate step (§10.3 step 1d), once per resolved
|
|
5
|
+
* binding, before any persist hook touches durable state. Catches three
|
|
6
|
+
* classes of registry tampering / drift that the LLM-proxy SSRF guard
|
|
7
|
+
* (`validateUpstreamUrl`) does NOT cover for non-`proxy-upstream` paths:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Fresh re-resolve mismatch**: re-resolve the provider's endpoint
|
|
10
|
+
* from its spec, compare host/port/path against the registry entry.
|
|
11
|
+
* If they diverge, the registry was tampered after registration.
|
|
12
|
+
* 2. **Out-of-LAN host**: provider must be on loopback or a private
|
|
13
|
+
* LAN range (reuses `isPrivateHost` from llm-proxy/ssrf).
|
|
14
|
+
* 3. **Protocol whitelist by category**: each category locks the set
|
|
15
|
+
* of allowed protocols (see PROTOCOL_WHITELIST below).
|
|
16
|
+
*
|
|
17
|
+
* Failure throws `ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400)`.
|
|
18
|
+
*
|
|
19
|
+
* **All five inputs are provider-side**, never consumer-side. Callers
|
|
20
|
+
* looking up the provider spec/provide from the registry entry are
|
|
21
|
+
* responsible for the lookup; this module only validates.
|
|
22
|
+
*
|
|
23
|
+
* Capabilities matching `*-terminal` are skipped entirely — terminals
|
|
24
|
+
* have no network endpoint to validate.
|
|
25
|
+
*/
|
|
26
|
+
import type { AppSpec, AppProvide } from "../types.js";
|
|
27
|
+
import type { CapabilityEntry } from "./capability-registry.js";
|
|
28
|
+
export type EndpointCategory = "llm" | "search" | "browser" | "mcp" | "default";
|
|
29
|
+
/**
|
|
30
|
+
* Validate that `registryEntry` is safe to apply for the given category.
|
|
31
|
+
* Throws `INVALID_CAPABILITY_ENDPOINT (400)` on any failure; returns void
|
|
32
|
+
* on success.
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateCapabilityEndpoint(providerSpec: AppSpec, providerInstanceId: string, providerProvide: AppProvide, registryEntry: CapabilityEntry, category: EndpointCategory): void;
|
|
35
|
+
/**
|
|
36
|
+
* Look up the `providerProvide` block from a registry entry, by matching
|
|
37
|
+
* `entry.capability` against `spec.provides[].capability`. Returns
|
|
38
|
+
* `undefined` if no match — caller decides whether that's a hard error or
|
|
39
|
+
* a soft "skip validation" path (e.g. legacy instances with synthesized specs).
|
|
40
|
+
*/
|
|
41
|
+
export declare function findProviderProvide(spec: AppSpec, capability: string): AppProvide | undefined;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { resolveProvideEndpoint } from "./app/provide-resolver.js";
|
|
2
|
+
import { isPrivateHost } from "./llm-proxy/ssrf.js";
|
|
3
|
+
import { ConnectionError } from "./connection-resolver.js";
|
|
4
|
+
const PROTOCOL_WHITELIST = {
|
|
5
|
+
llm: new Set(["http", "https"]),
|
|
6
|
+
search: new Set(["http", "https"]),
|
|
7
|
+
browser: new Set(["ws", "wss", "http", "https"]),
|
|
8
|
+
mcp: new Set(["http", "https", "ws", "sse"]),
|
|
9
|
+
default: new Set(["http", "https", "ws", "wss", "tcp"]),
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Validate that `registryEntry` is safe to apply for the given category.
|
|
13
|
+
* Throws `INVALID_CAPABILITY_ENDPOINT (400)` on any failure; returns void
|
|
14
|
+
* on success.
|
|
15
|
+
*/
|
|
16
|
+
export function validateCapabilityEndpoint(providerSpec, providerInstanceId, providerProvide, registryEntry, category) {
|
|
17
|
+
const cap = providerProvide.capability ?? registryEntry.capability;
|
|
18
|
+
// *-terminal capabilities have no reachable endpoint; skip entirely.
|
|
19
|
+
if (/-terminal$/.test(cap))
|
|
20
|
+
return;
|
|
21
|
+
// 1. Fresh re-resolve — registryEntry must match what the provider's
|
|
22
|
+
// spec currently resolves to. If anything has been tampered (host,
|
|
23
|
+
// port, path) the resolved address in apply hooks would already be
|
|
24
|
+
// pointing somewhere unexpected; reject before write.
|
|
25
|
+
//
|
|
26
|
+
// Legacy carve-out: `loadCapabilitySpecForLegacyInstance` returns a
|
|
27
|
+
// synthetic spec with `tasks: []` (it strips runtime to prevent
|
|
28
|
+
// nomad-job reconstruction from drifting from the live instance, see
|
|
29
|
+
// runtime/migrations.ts). For those specs `resolveProvideEndpoint`
|
|
30
|
+
// cannot produce an endpoint at all; we degrade gracefully — drop
|
|
31
|
+
// the fresh-resolve check, keep the host / protocol / visibility
|
|
32
|
+
// checks below. Registry tampering on legacy instances is still
|
|
33
|
+
// bounded by host-must-be-LAN + protocol whitelist.
|
|
34
|
+
const hasTasks = Array.isArray(providerSpec.tasks) && providerSpec.tasks.length > 0;
|
|
35
|
+
if (hasTasks) {
|
|
36
|
+
const fresh = resolveProvideEndpoint(providerInstanceId, providerSpec, providerProvide);
|
|
37
|
+
if (!fresh) {
|
|
38
|
+
throw new ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400, `Cannot re-resolve endpoint for provider '${providerInstanceId}' capability '${cap}'`, { providerInstanceId, capability: cap, reason: "fresh-resolve-failed" });
|
|
39
|
+
}
|
|
40
|
+
const freshPath = fresh.path ?? "";
|
|
41
|
+
const registryPath = registryEntry.path ?? "";
|
|
42
|
+
if (fresh.host !== registryEntry.host ||
|
|
43
|
+
fresh.hostPort !== registryEntry.hostPort ||
|
|
44
|
+
freshPath !== registryPath) {
|
|
45
|
+
throw new ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400, `Registry endpoint for provider '${providerInstanceId}' capability '${cap}' does not match a fresh resolve from spec`, {
|
|
46
|
+
providerInstanceId,
|
|
47
|
+
capability: cap,
|
|
48
|
+
reason: "fresh-resolve-mismatch",
|
|
49
|
+
registry: { host: registryEntry.host, hostPort: registryEntry.hostPort, path: registryPath },
|
|
50
|
+
fresh: { host: fresh.host, hostPort: fresh.hostPort, path: freshPath },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 1b. Spec vs registry protocol 一致性检查。
|
|
55
|
+
// spec 侧 protocol 来自 providerProvide.protocol(两种 spec 都带该字段),
|
|
56
|
+
// registry 侧来自 registryEntry.protocol。若两者不一致说明 registry 在注册
|
|
57
|
+
// 后被篡改(或写入时出错),consumer 端将被导向错误协议,需要拒绝。
|
|
58
|
+
// legacy synthetic spec(hasTasks=false)同样执行该检查,因为 provide.protocol
|
|
59
|
+
// 字段存在于 provide 块上,而非 task 上。
|
|
60
|
+
//
|
|
61
|
+
// trim()+toLowerCase() 必须与 `app-manager.normalizeProvideProtocol` 完全
|
|
62
|
+
// 对齐,否则 yaml 里 `protocol: " http "` 之类的写法会让 spec 侧拿到带空格
|
|
63
|
+
// 的值,而 registerCapabilities 已经把 registry 侧 normalize 成 "http",
|
|
64
|
+
// 两者比对会误判为篡改。
|
|
65
|
+
const specProtocol = (providerProvide.protocol ?? "http").trim().toLowerCase();
|
|
66
|
+
const registryProtocol = String(registryEntry.protocol ?? "").trim().toLowerCase();
|
|
67
|
+
if (specProtocol !== registryProtocol) {
|
|
68
|
+
throw new ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400, `Registry protocol '${registryProtocol}' for provider '${providerInstanceId}' capability '${cap}' does not match spec protocol '${specProtocol}'`, {
|
|
69
|
+
providerInstanceId,
|
|
70
|
+
capability: cap,
|
|
71
|
+
reason: "protocol-spec-mismatch",
|
|
72
|
+
details: { specProtocol, registryProtocol },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// 2. Host must be loopback or LAN. We're only ever supposed to bind
|
|
76
|
+
// Connections-tab providers to local apps; a public hostname here is
|
|
77
|
+
// either a misconfiguration or active SSRF.
|
|
78
|
+
if (!isPrivateHost(registryEntry.host)) {
|
|
79
|
+
throw new ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400, `Provider host '${registryEntry.host}' is not in the loopback/LAN range`, { providerInstanceId, capability: cap, reason: "host-not-private", host: registryEntry.host });
|
|
80
|
+
}
|
|
81
|
+
// 3. Protocol must be in the category-specific whitelist.
|
|
82
|
+
const allowed = PROTOCOL_WHITELIST[category] ?? PROTOCOL_WHITELIST.default;
|
|
83
|
+
const protocol = String(registryEntry.protocol ?? "").toLowerCase();
|
|
84
|
+
if (!protocol || !allowed.has(protocol)) {
|
|
85
|
+
throw new ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400, `Protocol '${protocol || "(empty)"}' is not allowed for category '${category}'`, {
|
|
86
|
+
providerInstanceId,
|
|
87
|
+
capability: cap,
|
|
88
|
+
reason: "protocol-not-whitelisted",
|
|
89
|
+
protocol,
|
|
90
|
+
category,
|
|
91
|
+
allowed: Array.from(allowed),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Look up the `providerProvide` block from a registry entry, by matching
|
|
97
|
+
* `entry.capability` against `spec.provides[].capability`. Returns
|
|
98
|
+
* `undefined` if no match — caller decides whether that's a hard error or
|
|
99
|
+
* a soft "skip validation" path (e.g. legacy instances with synthesized specs).
|
|
100
|
+
*/
|
|
101
|
+
export function findProviderProvide(spec, capability) {
|
|
102
|
+
return spec.provides?.find((p) => p.capability === capability);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=capability-endpoint-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-endpoint-validator.js","sourceRoot":"","sources":["../../src/services/capability-endpoint-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CapabilityEntry, CapabilityStatus } from "./capability-registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Probe an entry. Cached for 30s per (instanceId, capability) pair.
|
|
4
|
+
*/
|
|
5
|
+
export declare function probe(entry: CapabilityEntry): Promise<CapabilityStatus>;
|
|
6
|
+
/**
|
|
7
|
+
* Force-refresh the cached probe for a specific entry. Called after
|
|
8
|
+
* provider start/stop transitions so the UI sees the new status without
|
|
9
|
+
* waiting for the 30s TTL.
|
|
10
|
+
*/
|
|
11
|
+
export declare function invalidate(entry: CapabilityEntry): void;
|
|
12
|
+
/**
|
|
13
|
+
* Drop the entire probe cache — used during tests and when registry is
|
|
14
|
+
* rebuilt at startup.
|
|
15
|
+
*/
|
|
16
|
+
export declare function clearCache(): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability health prober (§5.6).
|
|
3
|
+
*
|
|
4
|
+
* Probes a registered capability endpoint to confirm the provider is
|
|
5
|
+
* actually listening, before the resolver hands its address to consumers
|
|
6
|
+
* via Connections binding. Prevents stale registry entries from breaking
|
|
7
|
+
* downstream apps (e.g. an Ollama instance that was registered then died
|
|
8
|
+
* — without health probing the consumer would dial a closed port).
|
|
9
|
+
*
|
|
10
|
+
* Probe strategy is derived from the entry's protocol when no explicit
|
|
11
|
+
* `health` spec is provided on the provide:
|
|
12
|
+
* - http/https → HEAD on `path || "/"` with 5s timeout
|
|
13
|
+
* - ws/wss → TCP connect on hostPort
|
|
14
|
+
* - tcp → TCP connect on hostPort
|
|
15
|
+
* - terminal/ → no-op probe (always "unknown" → status not changed)
|
|
16
|
+
* other → no-op probe
|
|
17
|
+
*
|
|
18
|
+
* Results are cached for 30s per capability+instance to avoid hammering
|
|
19
|
+
* providers on every Connections UI refresh.
|
|
20
|
+
*/
|
|
21
|
+
import { request as httpRequest } from "http";
|
|
22
|
+
import { request as httpsRequest } from "https";
|
|
23
|
+
import { connect as tcpConnect } from "net";
|
|
24
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
25
|
+
const CACHE_TTL_MS = 30_000;
|
|
26
|
+
const _cache = new Map();
|
|
27
|
+
function cacheKey(entry) {
|
|
28
|
+
return `${entry.instanceId}::${entry.capability}`;
|
|
29
|
+
}
|
|
30
|
+
async function probeHttp(entry) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const isHttps = entry.protocol === "https";
|
|
33
|
+
const lib = isHttps ? httpsRequest : httpRequest;
|
|
34
|
+
const path = entry.path && entry.path !== "/" ? entry.path : "/";
|
|
35
|
+
const req = lib({
|
|
36
|
+
host: entry.host,
|
|
37
|
+
port: entry.hostPort,
|
|
38
|
+
method: "HEAD",
|
|
39
|
+
path,
|
|
40
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
41
|
+
}, (res) => {
|
|
42
|
+
// Any HTTP response (even 404) means the provider is listening.
|
|
43
|
+
res.resume();
|
|
44
|
+
// 5xx responses suggest the provider is up but unhealthy. Treat
|
|
45
|
+
// them as "running" anyway — the failure mode of "consumer can't
|
|
46
|
+
// talk to provider" surfaces through the consumer's own error
|
|
47
|
+
// path; the registry only certifies the port is open.
|
|
48
|
+
resolve("running");
|
|
49
|
+
});
|
|
50
|
+
req.on("timeout", () => {
|
|
51
|
+
req.destroy();
|
|
52
|
+
resolve("stopped");
|
|
53
|
+
});
|
|
54
|
+
req.on("error", () => resolve("stopped"));
|
|
55
|
+
req.end();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function probeTcp(entry) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const sock = tcpConnect({ host: entry.host, port: entry.hostPort });
|
|
61
|
+
let resolved = false;
|
|
62
|
+
const finish = (status) => {
|
|
63
|
+
if (resolved)
|
|
64
|
+
return;
|
|
65
|
+
resolved = true;
|
|
66
|
+
try {
|
|
67
|
+
sock.destroy();
|
|
68
|
+
}
|
|
69
|
+
catch { /* noop */ }
|
|
70
|
+
resolve(status);
|
|
71
|
+
};
|
|
72
|
+
sock.setTimeout(PROBE_TIMEOUT_MS);
|
|
73
|
+
sock.once("connect", () => finish("running"));
|
|
74
|
+
sock.once("timeout", () => finish("stopped"));
|
|
75
|
+
sock.once("error", () => finish("stopped"));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Probe an entry. Cached for 30s per (instanceId, capability) pair.
|
|
80
|
+
*/
|
|
81
|
+
export async function probe(entry) {
|
|
82
|
+
const key = cacheKey(entry);
|
|
83
|
+
const hit = _cache.get(key);
|
|
84
|
+
if (hit && Date.now() - hit.ts < CACHE_TTL_MS)
|
|
85
|
+
return hit.status;
|
|
86
|
+
let status = "unknown";
|
|
87
|
+
const protocol = (entry.protocol || "").toLowerCase();
|
|
88
|
+
switch (protocol) {
|
|
89
|
+
case "http":
|
|
90
|
+
case "https":
|
|
91
|
+
status = await probeHttp(entry);
|
|
92
|
+
break;
|
|
93
|
+
case "tcp":
|
|
94
|
+
case "ws":
|
|
95
|
+
case "wss":
|
|
96
|
+
status = await probeTcp(entry);
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
// terminal / mcp / sse / unknown — no portable probe; trust registry.
|
|
100
|
+
status = entry.status;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
_cache.set(key, { status, ts: Date.now() });
|
|
104
|
+
return status;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Force-refresh the cached probe for a specific entry. Called after
|
|
108
|
+
* provider start/stop transitions so the UI sees the new status without
|
|
109
|
+
* waiting for the 30s TTL.
|
|
110
|
+
*/
|
|
111
|
+
export function invalidate(entry) {
|
|
112
|
+
_cache.delete(cacheKey(entry));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Drop the entire probe cache — used during tests and when registry is
|
|
116
|
+
* rebuilt at startup.
|
|
117
|
+
*/
|
|
118
|
+
export function clearCache() {
|
|
119
|
+
_cache.clear();
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=capability-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-health.js","sourceRoot":"","sources":["../../src/services/capability-health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type CapabilityStatus = "running" | "stopped" | "unknown";
|
|
2
|
+
/**
|
|
3
|
+
* Single provider entry in the registry. v3.4.x extends the legacy 5-field
|
|
4
|
+
* shape (instanceId / hostPort / address / path / registered_at) to ~10
|
|
5
|
+
* fields so the Connections UI can render rich metadata without re-querying
|
|
6
|
+
* each provider's spec.
|
|
7
|
+
*/
|
|
8
|
+
export interface CapabilityEntry {
|
|
9
|
+
instanceId: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
capability: string;
|
|
12
|
+
host: string;
|
|
13
|
+
hostPort: number;
|
|
14
|
+
path?: string;
|
|
15
|
+
address: string;
|
|
16
|
+
protocol: string;
|
|
17
|
+
visibility?: "external" | "internal" | string;
|
|
18
|
+
status: CapabilityStatus;
|
|
19
|
+
lastSeenRunningAt?: string;
|
|
20
|
+
registeredAt: string;
|
|
21
|
+
/**
|
|
22
|
+
* §17 (PR 8) — when present, MCP-binding adapters expose this
|
|
23
|
+
* capability through the jishushell MCP firewall using the schema
|
|
24
|
+
* here, instead of letting the upstream MCP package's `tools/list`
|
|
25
|
+
* descriptions reach the LLM. Copied verbatim from the producing
|
|
26
|
+
* spec's `provides[].tool_schema` at registerProvider time.
|
|
27
|
+
*/
|
|
28
|
+
toolSchema?: import("../types.js").ToolSchema;
|
|
29
|
+
/**
|
|
30
|
+
* §6 (PR B) — auth config for consumers of this capability. Copied
|
|
31
|
+
* verbatim from the producing spec's `provides[].auth` at registerProvider
|
|
32
|
+
* time. Absent = no auth required.
|
|
33
|
+
*/
|
|
34
|
+
auth?: import("../types.js").AppProvideAuth;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Legacy shape kept side-by-side during PR 1-3 transition.
|
|
38
|
+
*/
|
|
39
|
+
interface LegacyCapabilityEntry {
|
|
40
|
+
instanceId: string;
|
|
41
|
+
hostPort: number;
|
|
42
|
+
address: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
registered_at: string;
|
|
45
|
+
}
|
|
46
|
+
interface CapabilityRegistryFile {
|
|
47
|
+
/**
|
|
48
|
+
* v3.4.x new shape: a capability may have multiple providers (e.g. two
|
|
49
|
+
* Ollama instances). UI lets users pick among candidates.
|
|
50
|
+
*/
|
|
51
|
+
providersByCapability?: Record<string, CapabilityEntry[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Legacy shape: single entry per capability, last-writer-wins. PR 1 keeps
|
|
54
|
+
* it dual-written; PR 3 removes it.
|
|
55
|
+
*/
|
|
56
|
+
capabilities?: Record<string, LegacyCapabilityEntry>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read the registry, migrating any legacy-only entries on the fly. Always
|
|
60
|
+
* returns both shapes populated — the compat layer relies on this.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readRegistry(): CapabilityRegistryFile;
|
|
63
|
+
/**
|
|
64
|
+
* Compat-view accessor: returns the **first** provider for a capability in
|
|
65
|
+
* legacy 5-field shape. PR 3 sub-step 3f deletes this function once all
|
|
66
|
+
* call sites are switched to `getProviderEntry` / `listProviders`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getCapabilityEntry(capability: string): LegacyCapabilityEntry | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* List all providers for a capability. Used by Connections UI candidate
|
|
71
|
+
* dropdown and PR 3 multi-candidate resolution.
|
|
72
|
+
*/
|
|
73
|
+
export declare function listProviders(capability: string): CapabilityEntry[];
|
|
74
|
+
/**
|
|
75
|
+
* Locate a specific provider by composite key. The `(instanceId, capability)`
|
|
76
|
+
* pair is required (v3.1 §5.3) because one instance may publish multiple
|
|
77
|
+
* capabilities (`ollama-binary` provides both `ollama-api` and
|
|
78
|
+
* `ollama-terminal`); `instanceId` alone is not unique.
|
|
79
|
+
*/
|
|
80
|
+
export declare function getProviderEntry(instanceId: string, capability: string): CapabilityEntry | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Register or update a single provider entry. Writes to both the new
|
|
83
|
+
* `providersByCapability` list and the legacy `capabilities` map (compat
|
|
84
|
+
* layer; PR 3 sub-step 3f drops the legacy write).
|
|
85
|
+
*/
|
|
86
|
+
export declare function registerProvider(entry: CapabilityEntry): void;
|
|
87
|
+
/**
|
|
88
|
+
* Mark all providers from a given instance as `stopped` instead of deleting
|
|
89
|
+
* them, so the Connections UI can still show them as candidates (greyed out).
|
|
90
|
+
* Replaces the previous `unregisterCapabilities` semantics on stop (§5.4).
|
|
91
|
+
*/
|
|
92
|
+
export declare function setProviderStatus(instanceId: string, status: CapabilityStatus): void;
|
|
93
|
+
/**
|
|
94
|
+
* Remove all providers for an instance permanently — used for uninstall /
|
|
95
|
+
* delete, NOT for stop (§5.4). After PR 3 sub-step 3c this is the only
|
|
96
|
+
* path that strips entries from the registry.
|
|
97
|
+
*/
|
|
98
|
+
export declare function unregisterProviders(instanceId: string): void;
|
|
99
|
+
/**
|
|
100
|
+
* Snapshot of the full registry (read-only). UI / suggestion engine consumes.
|
|
101
|
+
* Same dedup semantics as `listProviders` so callers iterating the snapshot
|
|
102
|
+
* (e.g. category-prefix candidate matching in `routes/instances.ts`) don't
|
|
103
|
+
* leak duplicates.
|
|
104
|
+
*/
|
|
105
|
+
export declare function snapshot(): CapabilityRegistryFile;
|
|
106
|
+
export {};
|