clawnexus 0.2.8 → 0.3.1

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.
@@ -0,0 +1,29 @@
1
+ import type { ClawInstance } from "../types.js";
2
+ export interface AgentSkill {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ tags: string[];
7
+ examples?: string[];
8
+ inputModes?: string[];
9
+ outputModes?: string[];
10
+ }
11
+ export interface AgentCard {
12
+ name: string;
13
+ description: string;
14
+ url: string;
15
+ version: string;
16
+ capabilities: {
17
+ streaming: boolean;
18
+ pushNotifications: boolean;
19
+ stateTransitionHistory: boolean;
20
+ };
21
+ skills: AgentSkill[];
22
+ defaultInputModes: string[];
23
+ defaultOutputModes: string[];
24
+ provider: {
25
+ name: string;
26
+ url?: string;
27
+ };
28
+ }
29
+ export declare function buildAgentCard(instance: ClawInstance, daemonVersion: string, skills?: AgentSkill[]): AgentCard;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // A2A Agent Card generation — maps ClawInstance to A2A v0.3.0 Agent Card
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.buildAgentCard = buildAgentCard;
5
+ const DEFAULT_SKILL = {
6
+ id: "general-assistant",
7
+ name: "General Assistant",
8
+ description: "General-purpose AI assistant",
9
+ tags: ["general"],
10
+ };
11
+ function buildAgentCard(instance, daemonVersion, skills) {
12
+ return {
13
+ name: instance.alias ?? instance.auto_name,
14
+ description: instance.display_name || instance.assistant_name,
15
+ url: `http://${instance.address}:17890`,
16
+ version: daemonVersion,
17
+ capabilities: {
18
+ streaming: false,
19
+ pushNotifications: false,
20
+ stateTransitionHistory: false,
21
+ },
22
+ skills: skills && skills.length > 0 ? skills : [DEFAULT_SKILL],
23
+ defaultInputModes: ["text/plain"],
24
+ defaultOutputModes: ["text/plain"],
25
+ provider: {
26
+ name: "ClawNexus",
27
+ url: "https://github.com/SilverstreamsAI/ClawNexus",
28
+ },
29
+ };
30
+ }
@@ -4,11 +4,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.ADAPTERS = void 0;
5
5
  exports.getAdapter = getAdapter;
6
6
  exports.getAllAdapterPorts = getAllAdapterPorts;
7
+ const openclaw_js_1 = require("./openclaw.js");
7
8
  const nanoclaw_js_1 = require("./nanoclaw.js");
8
9
  const nanobot_js_1 = require("./nanobot.js");
10
+ const openfang_js_1 = require("./openfang.js");
9
11
  exports.ADAPTERS = [
12
+ new openclaw_js_1.OpenClawAdapter(),
10
13
  new nanoclaw_js_1.NanoClawAdapter(),
11
14
  new nanobot_js_1.NanoBotAdapter(),
15
+ new openfang_js_1.OpenFangAdapter(),
12
16
  ];
13
17
  function getAdapter(name) {
14
18
  return exports.ADAPTERS.find((a) => a.name === name);
@@ -5,6 +5,7 @@ export declare class NanoBotAdapter implements FrameworkAdapter {
5
5
  readonly defaultPorts: number[];
6
6
  probe(host: string, port: number): Promise<ProbeResult | null>;
7
7
  toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
+ healthCheck(host: string, port: number): Promise<boolean>;
8
9
  private probeHealth;
9
10
  private probeApiHealth;
10
11
  private extractProbeResult;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- // NanoBot adapter — Python variant, default ports 8000/8080
2
+ // NanoBot adapter — Python variant, default ports 18790/8000/8080
3
3
  // Probe: /health → check for framework/app: "nanobot", fallback /api/health
4
4
  // Heuristic: python_version field on expected port → infer nanobot
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -7,7 +7,7 @@ exports.NanoBotAdapter = void 0;
7
7
  const PROBE_TIMEOUT = 2_000;
8
8
  class NanoBotAdapter {
9
9
  name = "nanobot";
10
- defaultPorts = [8000, 8080];
10
+ defaultPorts = [18790, 8000, 8080];
11
11
  async probe(host, port) {
12
12
  // Try /health first
13
13
  const healthResult = await this.probeHealth(host, port);
@@ -30,6 +30,21 @@ class NanoBotAdapter {
30
30
  implementation: "nanobot",
31
31
  };
32
32
  }
33
+ async healthCheck(host, port) {
34
+ try {
35
+ const res = await fetch(`http://${host}:${port}/health`, {
36
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
37
+ });
38
+ if (!res.ok)
39
+ return false;
40
+ const data = (await res.json());
41
+ return data.framework === "nanobot" || data.app === "nanobot" ||
42
+ (typeof data.python_version === "string" && this.defaultPorts.includes(port));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
33
48
  async probeHealth(host, port) {
34
49
  try {
35
50
  const res = await fetch(`http://${host}:${port}/health`, {
@@ -3,8 +3,25 @@ import type { FrameworkAdapter, ProbeResult } from "./types.js";
3
3
  export declare class NanoClawAdapter implements FrameworkAdapter {
4
4
  readonly name = "nanoclaw";
5
5
  readonly defaultPorts: number[];
6
- probe(host: string, port: number): Promise<ProbeResult | null>;
7
- toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
- private probeHealth;
9
- private probeApiInfo;
6
+ private _lastProjectDir;
7
+ probe(_host: string, _port: number): Promise<ProbeResult | null>;
8
+ probeLocal(): Promise<ProbeResult | null>;
9
+ healthCheckLocal(): Promise<boolean>;
10
+ toClawInstance(host: string, _port: number, probe: ProbeResult): Partial<ClawInstance>;
11
+ healthCheck(_host: string, _port: number): Promise<boolean>;
12
+ /** Scan /proc for Node.js processes whose cwd contains a nanoclaw package.json */
13
+ private _findFromProc;
14
+ /** Check common paths under home directory */
15
+ private _findFromCandidateDirs;
16
+ /** Check if a directory is a nanoclaw project */
17
+ private _isNanoClawDir;
18
+ /** Read package.json and verify it's nanoclaw */
19
+ private _readPackageJson;
20
+ /** Extract ASSISTANT_NAME from .env file */
21
+ private _readAssistantName;
22
+ /** Find a running process with cwd matching the project dir */
23
+ private _findRunningPid;
24
+ /** Count JSON files in data/ipc/{channel}/messages/ as a proxy for active tasks */
25
+ private _countIpcTasks;
26
+ private _fileExists;
10
27
  }
@@ -1,74 +1,259 @@
1
1
  "use strict";
2
- // NanoClaw adapter — TypeScript variant, default ports 3100/3101
3
- // Probe: /health check for framework: "nanoclaw", fallback /api/info
2
+ // NanoClaw adapter — ProjectProbe implementation
3
+ // NanoClaw has no HTTP server, so discovery uses local filesystem probing:
4
+ // /proc scan for running processes, package.json, .env, data/ipc/ directory.
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
4
38
  Object.defineProperty(exports, "__esModule", { value: true });
5
39
  exports.NanoClawAdapter = void 0;
6
- const PROBE_TIMEOUT = 2_000;
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const os = __importStar(require("node:os"));
43
+ const NANOCLAW_PACKAGE_NAME = "nanoclaw";
44
+ // Common paths where nanoclaw might be installed
45
+ const CANDIDATE_DIRS = [
46
+ "nanoclaw",
47
+ "NanoClaw",
48
+ "projects/nanoclaw",
49
+ "projects/NanoClaw",
50
+ ];
7
51
  class NanoClawAdapter {
8
52
  name = "nanoclaw";
9
- defaultPorts = [3100, 3101];
10
- async probe(host, port) {
11
- // Try /health first
12
- const healthResult = await this.probeHealth(host, port);
13
- if (healthResult)
14
- return healthResult;
15
- // Fallback: /api/info
16
- return this.probeApiInfo(host, port);
53
+ defaultPorts = [];
54
+ // Cached project dir from last successful probe
55
+ _lastProjectDir = null;
56
+ async probe(_host, _port) {
57
+ return null;
58
+ }
59
+ async probeLocal() {
60
+ // 1. Try to find via running Node.js processes (/proc scan)
61
+ let projectDir = await this._findFromProc();
62
+ // 2. Fallback: scan common home directory paths
63
+ if (!projectDir) {
64
+ projectDir = await this._findFromCandidateDirs();
65
+ }
66
+ if (!projectDir)
67
+ return null;
68
+ this._lastProjectDir = projectDir;
69
+ // Read package.json for version
70
+ const pkgInfo = await this._readPackageJson(projectDir);
71
+ if (!pkgInfo)
72
+ return null; // not actually nanoclaw
73
+ // Read .env for ASSISTANT_NAME (optional)
74
+ const assistantName = await this._readAssistantName(projectDir);
75
+ // Check if process is running
76
+ const procInfo = await this._findRunningPid(projectDir);
77
+ // Count active IPC tasks (data/ipc/*/messages/*.json)
78
+ const activeTasks = await this._countIpcTasks(projectDir);
79
+ // Check if store/messages.db exists (has been run before)
80
+ const hasMessageDb = await this._fileExists(path.join(projectDir, "store", "messages.db"));
81
+ return {
82
+ name: NANOCLAW_PACKAGE_NAME,
83
+ version: pkgInfo.version,
84
+ display_name: assistantName ?? undefined,
85
+ metadata: {
86
+ project_dir: projectDir,
87
+ is_running: procInfo !== null,
88
+ pid: procInfo,
89
+ active_tasks: activeTasks,
90
+ has_message_db: hasMessageDb,
91
+ },
92
+ };
93
+ }
94
+ async healthCheckLocal() {
95
+ const projectDir = this._lastProjectDir;
96
+ if (!projectDir)
97
+ return false;
98
+ // Check if a nanoclaw process is running with this project dir
99
+ const pid = await this._findRunningPid(projectDir);
100
+ if (pid !== null)
101
+ return true;
102
+ // Fallback: check store/messages.db mtime (recent activity = likely alive)
103
+ try {
104
+ const dbPath = path.join(projectDir, "store", "messages.db");
105
+ const stat = await fs.promises.stat(dbPath);
106
+ const ageMs = Date.now() - stat.mtimeMs;
107
+ // Consider "healthy" if db was modified in last 5 minutes
108
+ return ageMs < 5 * 60 * 1000;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
17
113
  }
18
- toClawInstance(host, port, probe) {
114
+ toClawInstance(host, _port, probe) {
115
+ const meta = probe.metadata;
19
116
  return {
20
117
  agent_id: `nanoclaw@${host}`,
21
118
  assistant_name: probe.display_name ?? "",
22
119
  display_name: probe.display_name ?? "nanoclaw",
23
120
  lan_host: host,
24
121
  address: host,
25
- gateway_port: port,
122
+ gateway_port: 0,
26
123
  tls: false,
27
- discovery_source: "scan",
28
- status: "online",
124
+ discovery_source: "local",
29
125
  implementation: "nanoclaw",
126
+ labels: meta?.project_dir ? { project_dir: meta.project_dir } : undefined,
30
127
  };
31
128
  }
32
- async probeHealth(host, port) {
129
+ async healthCheck(_host, _port) {
130
+ return false;
131
+ }
132
+ /** Scan /proc for Node.js processes whose cwd contains a nanoclaw package.json */
133
+ async _findFromProc() {
134
+ if (process.platform !== "linux")
135
+ return null;
33
136
  try {
34
- const res = await fetch(`http://${host}:${port}/health`, {
35
- signal: AbortSignal.timeout(PROBE_TIMEOUT),
36
- });
37
- if (!res.ok)
38
- return null;
39
- const data = (await res.json());
40
- if (data.framework === "nanoclaw") {
41
- return {
42
- name: "nanoclaw",
43
- version: typeof data.version === "string" ? data.version : undefined,
44
- display_name: typeof data.name === "string" ? data.name : undefined,
45
- };
137
+ const entries = await fs.promises.readdir("/proc");
138
+ for (const entry of entries) {
139
+ // Only numeric dirs (PIDs)
140
+ if (!/^\d+$/.test(entry))
141
+ continue;
142
+ try {
143
+ const cwd = await fs.promises.readlink(`/proc/${entry}/cwd`);
144
+ if (await this._isNanoClawDir(cwd)) {
145
+ return cwd;
146
+ }
147
+ }
148
+ catch {
149
+ // Permission denied or process gone — skip
150
+ }
46
151
  }
47
- return null;
48
152
  }
49
153
  catch {
50
- return null;
154
+ // /proc not available
51
155
  }
156
+ return null;
52
157
  }
53
- async probeApiInfo(host, port) {
158
+ /** Check common paths under home directory */
159
+ async _findFromCandidateDirs() {
160
+ const home = os.homedir();
161
+ for (const rel of CANDIDATE_DIRS) {
162
+ const dir = path.join(home, rel);
163
+ if (await this._isNanoClawDir(dir)) {
164
+ return dir;
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ /** Check if a directory is a nanoclaw project */
170
+ async _isNanoClawDir(dir) {
171
+ const info = await this._readPackageJson(dir);
172
+ return info !== null;
173
+ }
174
+ /** Read package.json and verify it's nanoclaw */
175
+ async _readPackageJson(dir) {
54
176
  try {
55
- const res = await fetch(`http://${host}:${port}/api/info`, {
56
- signal: AbortSignal.timeout(PROBE_TIMEOUT),
57
- });
58
- if (!res.ok)
59
- return null;
60
- const data = (await res.json());
61
- if (data.framework === "nanoclaw") {
62
- return {
63
- name: "nanoclaw",
64
- version: typeof data.version === "string" ? data.version : undefined,
65
- display_name: typeof data.name === "string" ? data.name : undefined,
66
- };
177
+ const raw = await fs.promises.readFile(path.join(dir, "package.json"), "utf-8");
178
+ const pkg = JSON.parse(raw);
179
+ if (pkg.name === NANOCLAW_PACKAGE_NAME) {
180
+ return { version: pkg.version };
181
+ }
182
+ }
183
+ catch {
184
+ // Not found or invalid
185
+ }
186
+ return null;
187
+ }
188
+ /** Extract ASSISTANT_NAME from .env file */
189
+ async _readAssistantName(dir) {
190
+ try {
191
+ const raw = await fs.promises.readFile(path.join(dir, ".env"), "utf-8");
192
+ const match = raw.match(/^ASSISTANT_NAME=(.*)$/m);
193
+ if (match?.[1]) {
194
+ return match[1].trim().replace(/^["']|["']$/g, "");
67
195
  }
68
- return null;
69
196
  }
70
197
  catch {
198
+ // .env not found
199
+ }
200
+ return null;
201
+ }
202
+ /** Find a running process with cwd matching the project dir */
203
+ async _findRunningPid(dir) {
204
+ if (process.platform !== "linux")
71
205
  return null;
206
+ try {
207
+ const entries = await fs.promises.readdir("/proc");
208
+ for (const entry of entries) {
209
+ if (!/^\d+$/.test(entry))
210
+ continue;
211
+ try {
212
+ const cwd = await fs.promises.readlink(`/proc/${entry}/cwd`);
213
+ if (cwd === dir) {
214
+ return parseInt(entry, 10);
215
+ }
216
+ }
217
+ catch {
218
+ // skip
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ // /proc not available
224
+ }
225
+ return null;
226
+ }
227
+ /** Count JSON files in data/ipc/{channel}/messages/ as a proxy for active tasks */
228
+ async _countIpcTasks(dir) {
229
+ const ipcDir = path.join(dir, "data", "ipc");
230
+ try {
231
+ const channels = await fs.promises.readdir(ipcDir);
232
+ let count = 0;
233
+ for (const ch of channels) {
234
+ const messagesDir = path.join(ipcDir, ch, "messages");
235
+ try {
236
+ const files = await fs.promises.readdir(messagesDir);
237
+ count += files.filter((f) => f.endsWith(".json")).length;
238
+ }
239
+ catch {
240
+ // messages dir doesn't exist for this channel
241
+ }
242
+ }
243
+ return count;
244
+ }
245
+ catch {
246
+ // data/ipc doesn't exist
247
+ return 0;
248
+ }
249
+ }
250
+ async _fileExists(filePath) {
251
+ try {
252
+ await fs.promises.access(filePath);
253
+ return true;
254
+ }
255
+ catch {
256
+ return false;
72
257
  }
73
258
  }
74
259
  }
@@ -0,0 +1,9 @@
1
+ import type { ClawInstance } from "../types.js";
2
+ import type { FrameworkAdapter, ProbeResult } from "./types.js";
3
+ export declare class OpenClawAdapter implements FrameworkAdapter {
4
+ readonly name = "openclaw";
5
+ readonly defaultPorts: number[];
6
+ probe(host: string, port: number): Promise<ProbeResult | null>;
7
+ toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
+ healthCheck(host: string, port: number): Promise<boolean>;
9
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ // OpenClaw adapter — canonical OpenClaw (and compatible variants like GoClaw)
3
+ // Probe: /__openclaw/control-ui-config.json → extract agentId, assistantName, displayName
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.OpenClawAdapter = void 0;
6
+ const PROBE_TIMEOUT = 2_000;
7
+ const CONFIG_PATH = "/__openclaw/control-ui-config.json";
8
+ class OpenClawAdapter {
9
+ name = "openclaw";
10
+ defaultPorts = [18789];
11
+ async probe(host, port) {
12
+ try {
13
+ const res = await fetch(`http://${host}:${port}${CONFIG_PATH}`, {
14
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
15
+ });
16
+ if (!res.ok)
17
+ return null;
18
+ const config = (await res.json());
19
+ if (!config.assistantAgentId)
20
+ return null;
21
+ return {
22
+ name: "openclaw",
23
+ display_name: config.displayName ?? config.assistantName ?? undefined,
24
+ metadata: {
25
+ assistantAgentId: config.assistantAgentId,
26
+ assistantName: config.assistantName,
27
+ displayName: config.displayName,
28
+ },
29
+ };
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ toClawInstance(host, port, probe) {
36
+ const meta = probe.metadata;
37
+ return {
38
+ agent_id: meta?.assistantAgentId ?? `openclaw@${host}`,
39
+ assistant_name: meta?.assistantName ?? "",
40
+ display_name: meta?.displayName ?? meta?.assistantName ?? "",
41
+ lan_host: host,
42
+ address: host,
43
+ gateway_port: port,
44
+ tls: false,
45
+ discovery_source: "scan",
46
+ status: "online",
47
+ implementation: "openclaw",
48
+ };
49
+ }
50
+ async healthCheck(host, port) {
51
+ try {
52
+ const res = await fetch(`http://${host}:${port}${CONFIG_PATH}`, {
53
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
54
+ });
55
+ return res.ok;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ }
62
+ exports.OpenClawAdapter = OpenClawAdapter;
@@ -0,0 +1,11 @@
1
+ import type { ClawInstance } from "../types.js";
2
+ import type { FrameworkAdapter, ProbeResult } from "./types.js";
3
+ export declare class OpenFangAdapter implements FrameworkAdapter {
4
+ readonly name = "openfang";
5
+ readonly defaultPorts: number[];
6
+ probe(host: string, port: number): Promise<ProbeResult | null>;
7
+ toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
8
+ healthCheck(host: string, port: number): Promise<boolean>;
9
+ private isOpenFang;
10
+ private fetchAgentJson;
11
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ // OpenFang adapter — port 4200, REST API
3
+ // Probe: /api/health → confirm OpenFang, identity from /.well-known/agent.json
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.OpenFangAdapter = void 0;
6
+ const PROBE_TIMEOUT = 2_000;
7
+ class OpenFangAdapter {
8
+ name = "openfang";
9
+ defaultPorts = [4200];
10
+ async probe(host, port) {
11
+ try {
12
+ const res = await fetch(`http://${host}:${port}/api/health`, {
13
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
14
+ });
15
+ if (!res.ok)
16
+ return null;
17
+ const data = (await res.json());
18
+ if (!this.isOpenFang(data))
19
+ return null;
20
+ // Try to get identity from /.well-known/agent.json
21
+ const agent = await this.fetchAgentJson(host, port);
22
+ return {
23
+ name: "openfang",
24
+ version: data.version ?? agent?.version,
25
+ display_name: agent?.display_name ?? agent?.name,
26
+ metadata: {
27
+ agent_id: agent?.agent_id,
28
+ agent_name: agent?.name,
29
+ },
30
+ };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ toClawInstance(host, port, probe) {
37
+ const meta = probe.metadata;
38
+ return {
39
+ agent_id: meta?.agent_id ?? `openfang@${host}`,
40
+ assistant_name: meta?.agent_name ?? "",
41
+ display_name: probe.display_name ?? "openfang",
42
+ lan_host: host,
43
+ address: host,
44
+ gateway_port: port,
45
+ tls: false,
46
+ discovery_source: "scan",
47
+ status: "online",
48
+ implementation: "openfang",
49
+ };
50
+ }
51
+ async healthCheck(host, port) {
52
+ try {
53
+ const res = await fetch(`http://${host}:${port}/api/health`, {
54
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
55
+ });
56
+ if (!res.ok)
57
+ return false;
58
+ const data = (await res.json());
59
+ return data.status === "ok";
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ isOpenFang(data) {
66
+ // Confirm OpenFang by framework field or status shape
67
+ if (data.framework === "openfang")
68
+ return true;
69
+ // Heuristic: /api/health with status: "ok" on default port
70
+ if (data.status === "ok")
71
+ return true;
72
+ return false;
73
+ }
74
+ async fetchAgentJson(host, port) {
75
+ try {
76
+ const res = await fetch(`http://${host}:${port}/.well-known/agent.json`, {
77
+ signal: AbortSignal.timeout(PROBE_TIMEOUT),
78
+ });
79
+ if (!res.ok)
80
+ return null;
81
+ return (await res.json());
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ }
88
+ exports.OpenFangAdapter = OpenFangAdapter;
@@ -10,4 +10,7 @@ export interface FrameworkAdapter {
10
10
  readonly defaultPorts: number[];
11
11
  probe(host: string, port: number): Promise<ProbeResult | null>;
12
12
  toClawInstance(host: string, port: number, probe: ProbeResult): Partial<ClawInstance>;
13
+ healthCheck(host: string, port: number): Promise<boolean>;
14
+ probeLocal?(): Promise<ProbeResult | null>;
15
+ healthCheckLocal?(): Promise<boolean>;
13
16
  }
@@ -99,8 +99,8 @@ class PolicyEngine {
99
99
  this.incrementRate(peer);
100
100
  // 3. Whitelist check — whitelisted peers bypass trust/capability checks
101
101
  const isWhitelisted = this.config.access_control.whitelist.includes(peer);
102
- // 4. Trust score check (skip for whitelisted)
103
- if (!isWhitelisted && peerTrustScore < this.config.trust_threshold) {
102
+ // 4. Trust score check (skip for whitelisted and auto mode)
103
+ if (this.config.mode !== "auto" && !isWhitelisted && peerTrustScore < this.config.trust_threshold) {
104
104
  return { result: "reject", reason: "trust_insufficient", details: `Score ${peerTrustScore} < threshold ${this.config.trust_threshold}` };
105
105
  }
106
106
  // 5. Delegation depth check
@@ -189,6 +189,13 @@ class PolicyEngine {
189
189
  existing.count++;
190
190
  }
191
191
  }
192
+ // Periodic cleanup: remove expired entries when map grows large
193
+ if (this.rateCounters.size > 100) {
194
+ for (const [key, val] of this.rateCounters) {
195
+ if (val.resetAt <= now)
196
+ this.rateCounters.delete(key);
197
+ }
198
+ }
192
199
  }
193
200
  async saveConfig() {
194
201
  const json = JSON.stringify(this.config, null, 2);