clawnexus 0.2.7 → 0.3.0
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/dist/a2a/card.d.ts +29 -0
- package/dist/a2a/card.js +30 -0
- package/dist/adapter/index.d.ts +4 -0
- package/dist/adapter/index.js +28 -0
- package/dist/adapter/nanobot.d.ts +12 -0
- package/dist/adapter/nanobot.js +97 -0
- package/dist/adapter/nanoclaw.d.ts +27 -0
- package/dist/adapter/nanoclaw.js +260 -0
- package/dist/adapter/openclaw.d.ts +9 -0
- package/dist/adapter/openclaw.js +62 -0
- package/dist/adapter/openfang.d.ts +11 -0
- package/dist/adapter/openfang.js +88 -0
- package/dist/adapter/types.d.ts +16 -0
- package/dist/adapter/types.js +4 -0
- package/dist/api/server.d.ts +1 -0
- package/dist/api/server.js +31 -0
- package/dist/cli/index.js +7 -2
- package/dist/discovery/broadcast.js +5 -4
- package/dist/health/checker.d.ts +4 -0
- package/dist/health/checker.js +84 -20
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -1
- package/dist/local/probe.d.ts +4 -0
- package/dist/local/probe.js +127 -12
- package/dist/registry/store.js +3 -0
- package/dist/scanner/active.d.ts +1 -0
- package/dist/scanner/active.js +73 -19
- package/dist/scanner/fingerprint.d.ts +22 -0
- package/dist/scanner/fingerprint.js +135 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/scanner/active.js
CHANGED
|
@@ -38,11 +38,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
38
38
|
exports.ActiveScanner = void 0;
|
|
39
39
|
const node_events_1 = require("node:events");
|
|
40
40
|
const os = __importStar(require("node:os"));
|
|
41
|
+
const fingerprint_js_1 = require("./fingerprint.js");
|
|
41
42
|
const wireguard_js_1 = require("./wireguard.js");
|
|
43
|
+
const index_js_1 = require("../adapter/index.js");
|
|
42
44
|
const CONCURRENCY = 50;
|
|
43
45
|
const TIMEOUT_PER_HOST = 2_000;
|
|
44
46
|
const DEFAULT_PORT = 18789;
|
|
45
47
|
const CONFIG_PATH = "/__openclaw/control-ui-config.json";
|
|
48
|
+
// Default ports for all known implementations
|
|
49
|
+
const BUILTIN_PORTS = [
|
|
50
|
+
18789, // OpenClaw, GoClaw
|
|
51
|
+
42617, // ZeroClaw
|
|
52
|
+
18790, // PicoClaw
|
|
53
|
+
];
|
|
54
|
+
const DEFAULT_SCAN_PORTS = [...new Set([...BUILTIN_PORTS, ...(0, index_js_1.getAllAdapterPorts)()])];
|
|
46
55
|
class ActiveScanner extends node_events_1.EventEmitter {
|
|
47
56
|
store;
|
|
48
57
|
scanning = false;
|
|
@@ -71,8 +80,8 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
71
80
|
}
|
|
72
81
|
// 2. Scan subnets (skip if only explicit targets were given)
|
|
73
82
|
if (!hasExplicitTargets) {
|
|
74
|
-
const ports = options?.ports ??
|
|
75
|
-
const allPorts = [...new Set([
|
|
83
|
+
const ports = options?.ports ?? DEFAULT_SCAN_PORTS;
|
|
84
|
+
const allPorts = [...new Set([...DEFAULT_SCAN_PORTS, ...ports])];
|
|
76
85
|
const wgInfo = await (0, wireguard_js_1.detectWireGuard)();
|
|
77
86
|
const subnets = this.detectSubnets(wgInfo);
|
|
78
87
|
for (const si of subnets) {
|
|
@@ -160,25 +169,18 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
160
169
|
return discovered;
|
|
161
170
|
}
|
|
162
171
|
async probeHost(host, port = DEFAULT_PORT, networkScope = "local") {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return null;
|
|
170
|
-
const config = (await res.json());
|
|
171
|
-
if (!config.assistantAgentId)
|
|
172
|
-
return null;
|
|
173
|
-
// Resolve lan_host: prefer known hostname from existing registry entries
|
|
174
|
-
// over raw IP to enable multi-NIC deduplication
|
|
175
|
-
const lan_host = this.resolveHostForDedup(host, port, config.assistantAgentId);
|
|
172
|
+
// Try OpenClaw-compatible config endpoint first
|
|
173
|
+
const configResult = await this.probeConfigEndpoint(host, port);
|
|
174
|
+
if (configResult) {
|
|
175
|
+
// Got config — identify variant (openclaw vs goclaw)
|
|
176
|
+
const fp = await (0, fingerprint_js_1.identifyImplementation)(host, port, configResult);
|
|
177
|
+
const lan_host = this.resolveHostForDedup(host, port, configResult.assistantAgentId);
|
|
176
178
|
const now = new Date().toISOString();
|
|
177
179
|
return {
|
|
178
|
-
agent_id:
|
|
179
|
-
auto_name: "",
|
|
180
|
-
assistant_name:
|
|
181
|
-
display_name:
|
|
180
|
+
agent_id: configResult.assistantAgentId,
|
|
181
|
+
auto_name: "",
|
|
182
|
+
assistant_name: configResult.assistantName ?? "",
|
|
183
|
+
display_name: configResult.displayName ?? configResult.assistantName ?? "",
|
|
182
184
|
lan_host,
|
|
183
185
|
address: host,
|
|
184
186
|
gateway_port: port,
|
|
@@ -188,8 +190,60 @@ class ActiveScanner extends node_events_1.EventEmitter {
|
|
|
188
190
|
status: "online",
|
|
189
191
|
last_seen: now,
|
|
190
192
|
discovered_at: now,
|
|
193
|
+
implementation: fp.implementation,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// No config endpoint — try fingerprint-only identification (zeroclaw, picoclaw)
|
|
197
|
+
const fp = await (0, fingerprint_js_1.identifyImplementation)(host, port, null);
|
|
198
|
+
if (fp.implementation !== "unknown") {
|
|
199
|
+
const now = new Date().toISOString();
|
|
200
|
+
return {
|
|
201
|
+
agent_id: `${fp.implementation}@${host}`,
|
|
202
|
+
auto_name: "",
|
|
203
|
+
assistant_name: "",
|
|
204
|
+
display_name: fp.implementation,
|
|
205
|
+
lan_host: host,
|
|
206
|
+
address: host,
|
|
207
|
+
gateway_port: port,
|
|
208
|
+
tls: false,
|
|
209
|
+
discovery_source: "scan",
|
|
210
|
+
network_scope: networkScope,
|
|
211
|
+
status: "online",
|
|
212
|
+
last_seen: now,
|
|
213
|
+
discovered_at: now,
|
|
214
|
+
implementation: fp.implementation,
|
|
191
215
|
};
|
|
192
216
|
}
|
|
217
|
+
// Fingerprint unknown — try framework adapters as final fallback
|
|
218
|
+
for (const adapter of index_js_1.ADAPTERS) {
|
|
219
|
+
const probe = await adapter.probe(host, port);
|
|
220
|
+
if (probe) {
|
|
221
|
+
const now = new Date().toISOString();
|
|
222
|
+
const partial = adapter.toClawInstance(host, port, probe);
|
|
223
|
+
return {
|
|
224
|
+
...partial,
|
|
225
|
+
auto_name: "",
|
|
226
|
+
network_scope: networkScope,
|
|
227
|
+
last_seen: now,
|
|
228
|
+
discovered_at: now,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
async probeConfigEndpoint(host, port) {
|
|
235
|
+
const url = `http://${host}:${port}${CONFIG_PATH}`;
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(url, {
|
|
238
|
+
signal: AbortSignal.timeout(TIMEOUT_PER_HOST),
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok)
|
|
241
|
+
return null;
|
|
242
|
+
const config = (await res.json());
|
|
243
|
+
if (!config.assistantAgentId)
|
|
244
|
+
return null;
|
|
245
|
+
return config;
|
|
246
|
+
}
|
|
193
247
|
catch {
|
|
194
248
|
return null;
|
|
195
249
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ClawImplementation, ControlUiConfig } from "../types.js";
|
|
2
|
+
export interface FingerprintResult {
|
|
3
|
+
implementation: ClawImplementation;
|
|
4
|
+
confidence: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Identify the implementation variant of a host.
|
|
8
|
+
*
|
|
9
|
+
* Probe order (highest to lowest confidence):
|
|
10
|
+
* 1. /.well-known/claw-identity.json — ClawLink Protocol (self-declared)
|
|
11
|
+
* 2. /__openclaw/control-ui-config.json analysis — field count heuristic
|
|
12
|
+
* 3. /health endpoint — ZeroClaw vs PicoClaw differentiation
|
|
13
|
+
*/
|
|
14
|
+
export declare function identifyImplementation(host: string, port: number, config?: ControlUiConfig | null): Promise<FingerprintResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Detect implementation from a 404 error response body.
|
|
17
|
+
* Auxiliary signal — low confidence, used as tiebreaker.
|
|
18
|
+
*
|
|
19
|
+
* Go net/http: "404 page not found\n" (plain text)
|
|
20
|
+
* Fastify (Node.js): JSON {"message":"Route ... not found","error":"Not Found","statusCode":404}
|
|
21
|
+
*/
|
|
22
|
+
export declare function detect404Format(body: string): "go" | "fastify" | "unknown";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Fingerprint identification — multi-signal cascade to identify OpenClaw variants
|
|
3
|
+
//
|
|
4
|
+
// Supported implementations:
|
|
5
|
+
// openclaw — Node.js, port 18789, serves /__openclaw/control-ui-config.json
|
|
6
|
+
// goclaw — Go (chi), port 18789, serves /__openclaw/control-ui-config.json (compat mode)
|
|
7
|
+
// zeroclaw — Rust (Axum), port 42617, /health with "paired" field
|
|
8
|
+
// picoclaw — Go (net/http), port 18790, /health + /ready (K8s-style probes)
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.identifyImplementation = identifyImplementation;
|
|
11
|
+
exports.detect404Format = detect404Format;
|
|
12
|
+
const FINGERPRINT_TIMEOUT = 2_000;
|
|
13
|
+
/**
|
|
14
|
+
* Identify the implementation variant of a host.
|
|
15
|
+
*
|
|
16
|
+
* Probe order (highest to lowest confidence):
|
|
17
|
+
* 1. /.well-known/claw-identity.json — ClawLink Protocol (self-declared)
|
|
18
|
+
* 2. /__openclaw/control-ui-config.json analysis — field count heuristic
|
|
19
|
+
* 3. /health endpoint — ZeroClaw vs PicoClaw differentiation
|
|
20
|
+
*/
|
|
21
|
+
async function identifyImplementation(host, port, config) {
|
|
22
|
+
// Signal 1: ClawLink identity endpoint (highest priority)
|
|
23
|
+
const clawlink = await probeClawIdentity(host, port);
|
|
24
|
+
if (clawlink)
|
|
25
|
+
return clawlink;
|
|
26
|
+
// Signal 2: If we got a control-ui-config, analyze it
|
|
27
|
+
if (config) {
|
|
28
|
+
return analyzeConfig(config);
|
|
29
|
+
}
|
|
30
|
+
// Signal 3: No config → check /health for non-OpenClaw variants
|
|
31
|
+
const healthResult = await probeHealth(host, port);
|
|
32
|
+
if (healthResult)
|
|
33
|
+
return healthResult;
|
|
34
|
+
return { implementation: "unknown", confidence: 0.1 };
|
|
35
|
+
}
|
|
36
|
+
async function probeClawIdentity(host, port) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`http://${host}:${port}/.well-known/claw-identity.json`, { signal: AbortSignal.timeout(FINGERPRINT_TIMEOUT) });
|
|
39
|
+
if (!res.ok)
|
|
40
|
+
return null;
|
|
41
|
+
const data = (await res.json());
|
|
42
|
+
if (data.implementation && typeof data.implementation === "string") {
|
|
43
|
+
const impl = data.implementation.toLowerCase();
|
|
44
|
+
const known = [
|
|
45
|
+
"openclaw", "goclaw", "zeroclaw", "picoclaw", "nanoclaw", "nanobot",
|
|
46
|
+
];
|
|
47
|
+
return {
|
|
48
|
+
implementation: known.includes(impl) ? impl : "unknown",
|
|
49
|
+
confidence: 1.0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Analyze control-ui-config.json to distinguish OpenClaw from GoClaw.
|
|
60
|
+
*
|
|
61
|
+
* OpenClaw's config is feature-rich (>8 fields), while GoClaw's compat mode
|
|
62
|
+
* returns a minimal subset (<6 fields, missing UI-specific keys).
|
|
63
|
+
*/
|
|
64
|
+
function analyzeConfig(config) {
|
|
65
|
+
const knownFields = Object.keys(config);
|
|
66
|
+
const fieldCount = knownFields.length;
|
|
67
|
+
// GoClaw compat mode: minimal config, typically <6 fields
|
|
68
|
+
// and missing OpenClaw-specific UI fields
|
|
69
|
+
const uiFields = [
|
|
70
|
+
"controlUi", "assistantUrl", "webSearchEnabled",
|
|
71
|
+
"customInstructions", "tools",
|
|
72
|
+
];
|
|
73
|
+
const hasUiFields = uiFields.some((f) => f in config);
|
|
74
|
+
if (fieldCount < 6 && !hasUiFields) {
|
|
75
|
+
return { implementation: "goclaw", confidence: 0.7 };
|
|
76
|
+
}
|
|
77
|
+
return { implementation: "openclaw", confidence: 0.8 };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Probe /health to identify ZeroClaw or PicoClaw.
|
|
81
|
+
*
|
|
82
|
+
* ZeroClaw: /health returns JSON with "paired" + "runtime" fields
|
|
83
|
+
* PicoClaw: /health + /ready both return 200 (K8s-style)
|
|
84
|
+
*/
|
|
85
|
+
async function probeHealth(host, port) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`http://${host}:${port}/health`, {
|
|
88
|
+
signal: AbortSignal.timeout(FINGERPRINT_TIMEOUT),
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
return null;
|
|
92
|
+
const text = await res.text();
|
|
93
|
+
// ZeroClaw: JSON with "paired" field
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(text);
|
|
96
|
+
if ("paired" in data) {
|
|
97
|
+
return { implementation: "zeroclaw", confidence: 0.9 };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Not JSON, continue checking
|
|
102
|
+
}
|
|
103
|
+
// PicoClaw: /health OK + /ready also OK
|
|
104
|
+
const readyRes = await fetch(`http://${host}:${port}/ready`, {
|
|
105
|
+
signal: AbortSignal.timeout(FINGERPRINT_TIMEOUT),
|
|
106
|
+
});
|
|
107
|
+
if (readyRes.ok) {
|
|
108
|
+
return { implementation: "picoclaw", confidence: 0.8 };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Detect implementation from a 404 error response body.
|
|
118
|
+
* Auxiliary signal — low confidence, used as tiebreaker.
|
|
119
|
+
*
|
|
120
|
+
* Go net/http: "404 page not found\n" (plain text)
|
|
121
|
+
* Fastify (Node.js): JSON {"message":"Route ... not found","error":"Not Found","statusCode":404}
|
|
122
|
+
*/
|
|
123
|
+
function detect404Format(body) {
|
|
124
|
+
if (body.trim() === "404 page not found")
|
|
125
|
+
return "go";
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse(body);
|
|
128
|
+
if (data.statusCode === 404 && typeof data.error === "string")
|
|
129
|
+
return "fastify";
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Not JSON
|
|
133
|
+
}
|
|
134
|
+
return "unknown";
|
|
135
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type ClawImplementation = "openclaw" | "goclaw" | "zeroclaw" | "picoclaw" | "nanoclaw" | "nanobot" | "openfang" | "unknown";
|
|
1
2
|
export interface ClawInstance {
|
|
2
3
|
agent_id: string;
|
|
3
4
|
auto_name: string;
|
|
@@ -18,6 +19,7 @@ export interface ClawInstance {
|
|
|
18
19
|
is_self?: boolean;
|
|
19
20
|
claw_name?: string;
|
|
20
21
|
owner_pubkey?: string;
|
|
22
|
+
implementation?: ClawImplementation;
|
|
21
23
|
labels?: Record<string, string>;
|
|
22
24
|
}
|
|
23
25
|
export interface Connectivity {
|