clawnexus 0.0.1 → 0.2.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/README.md +108 -8
- package/dist/agent/engine.d.ts +17 -0
- package/dist/agent/engine.js +231 -0
- package/dist/agent/protocol.d.ts +12 -0
- package/dist/agent/protocol.js +159 -0
- package/dist/agent/router.d.ts +41 -0
- package/dist/agent/router.js +179 -0
- package/dist/agent/tasks.d.ts +28 -0
- package/dist/agent/tasks.js +294 -0
- package/dist/agent/types.d.ts +141 -0
- package/dist/agent/types.js +3 -0
- package/dist/api/server.d.ts +64 -0
- package/dist/api/server.js +564 -0
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +1153 -0
- package/dist/crypto/keys.d.ts +21 -0
- package/dist/crypto/keys.js +97 -0
- package/dist/discovery/broadcast.d.ts +27 -0
- package/dist/discovery/broadcast.js +289 -0
- package/dist/health/checker.d.ts +14 -0
- package/dist/health/checker.js +108 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +51 -0
- package/dist/local/probe.d.ts +15 -0
- package/dist/local/probe.js +137 -0
- package/dist/mdns/listener.d.ts +22 -0
- package/dist/mdns/listener.js +159 -0
- package/dist/registry/auto-name.d.ts +15 -0
- package/dist/registry/auto-name.js +57 -0
- package/dist/registry/auto-register.d.ts +20 -0
- package/dist/registry/auto-register.js +102 -0
- package/dist/registry/client.d.ts +53 -0
- package/dist/registry/client.js +96 -0
- package/dist/registry/discovery.d.ts +14 -0
- package/dist/registry/discovery.js +57 -0
- package/dist/registry/store.d.ts +40 -0
- package/dist/registry/store.js +289 -0
- package/dist/relay/connector.d.ts +38 -0
- package/dist/relay/connector.js +232 -0
- package/dist/relay/crypto.d.ts +23 -0
- package/dist/relay/crypto.js +102 -0
- package/dist/relay/types.d.ts +77 -0
- package/dist/relay/types.js +3 -0
- package/dist/scanner/active.d.ts +21 -0
- package/dist/scanner/active.js +206 -0
- package/dist/scanner/wireguard.d.ts +14 -0
- package/dist/scanner/wireguard.js +180 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +3 -0
- package/package.json +54 -23
- package/index.js +0 -2
package/README.md
CHANGED
|
@@ -1,8 +1,108 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
# clawnexus
|
|
2
|
+
|
|
3
|
+
ClawNexus daemon and CLI — AI instance registry for OpenClaw.
|
|
4
|
+
|
|
5
|
+
Discovers OpenClaw instances on your local network, assigns human-readable aliases, and exposes an HTTP API for querying and managing them.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g clawnexus
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js >= 22.
|
|
14
|
+
|
|
15
|
+
## CLI Usage
|
|
16
|
+
|
|
17
|
+
### Daemon Management
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
clawnexus start # Start the daemon (background process)
|
|
21
|
+
clawnexus stop # Stop the daemon
|
|
22
|
+
clawnexus restart # Restart the daemon
|
|
23
|
+
clawnexus status # Show daemon status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Instance Discovery
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
clawnexus scan # Scan local network for OpenClaw instances
|
|
30
|
+
clawnexus list # List all known instances
|
|
31
|
+
clawnexus list --json # Machine-readable JSON output
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Instance Management
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
clawnexus alias <id|address> <name> # Set a friendly alias
|
|
38
|
+
clawnexus info <name|address> # Show instance details
|
|
39
|
+
clawnexus forget <name|address> # Remove from registry
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Connection
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
clawnexus connect <name> # Output ws:// address for an instance
|
|
46
|
+
clawnexus open <name> # Open WebChat UI in browser
|
|
47
|
+
clawnexus relay status # Show relay connection status
|
|
48
|
+
clawnexus connect <name.claw> # Connect via relay (v0.4)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Global Flags
|
|
52
|
+
|
|
53
|
+
| Flag | Description | Default |
|
|
54
|
+
|------|-------------|---------|
|
|
55
|
+
| `--json` | Machine-readable JSON output | `false` |
|
|
56
|
+
| `--timeout <ms>` | Request timeout | `5000` |
|
|
57
|
+
| `--api <url>` | Daemon API URL | `http://localhost:17890` |
|
|
58
|
+
|
|
59
|
+
## Daemon HTTP API
|
|
60
|
+
|
|
61
|
+
The daemon listens on `http://localhost:17890` by default.
|
|
62
|
+
|
|
63
|
+
| Method | Path | Description |
|
|
64
|
+
|--------|------|-------------|
|
|
65
|
+
| `GET` | `/health` | Daemon health status |
|
|
66
|
+
| `GET` | `/instances` | List all instances |
|
|
67
|
+
| `GET` | `/instances/:id` | Get a single instance |
|
|
68
|
+
| `PUT` | `/instances/:id/alias` | Set/update alias |
|
|
69
|
+
| `DELETE` | `/instances/:id` | Remove instance |
|
|
70
|
+
| `POST` | `/scan` | Trigger network scan |
|
|
71
|
+
| `POST` | `/relay/connect` | Connect via relay (v0.4) |
|
|
72
|
+
| `GET` | `/relay/status` | Relay connection status |
|
|
73
|
+
| `DELETE` | `/relay/disconnect/:room_id` | Disconnect relay room |
|
|
74
|
+
|
|
75
|
+
See [docs/api.md](../../docs/api.md) for full request/response examples.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
| Environment Variable | Description | Default |
|
|
80
|
+
|---------------------|-------------|---------|
|
|
81
|
+
| `CLAWNEXUS_PORT` | Daemon API port | `17890` |
|
|
82
|
+
| `CLAWNEXUS_HOST` | Daemon bind address | `127.0.0.1` |
|
|
83
|
+
| `CLAWNEXUS_API` | CLI target API URL | `http://localhost:17890` |
|
|
84
|
+
|
|
85
|
+
Data is stored in `~/.clawnexus/`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
~/.clawnexus/
|
|
89
|
+
├── registry.json # Instance registry
|
|
90
|
+
├── daemon.pid # PID file
|
|
91
|
+
└── policy.json # Agent policy (v1.0)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Programmatic Usage
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { startDaemon } from "clawnexus";
|
|
98
|
+
|
|
99
|
+
const handle = await startDaemon({ port: 17890, host: "127.0.0.1" });
|
|
100
|
+
|
|
101
|
+
// Access components
|
|
102
|
+
console.log(handle.store.getAll()); // List instances
|
|
103
|
+
await handle.app.close(); // Graceful shutdown
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PolicyConfig, PolicyDecision, LayerBEnvelope } from "./types.js";
|
|
2
|
+
export declare class PolicyEngine {
|
|
3
|
+
private config;
|
|
4
|
+
private readonly configDir;
|
|
5
|
+
private readonly configPath;
|
|
6
|
+
private rateCounters;
|
|
7
|
+
constructor(configDir?: string);
|
|
8
|
+
init(): Promise<void>;
|
|
9
|
+
evaluate(envelope: LayerBEnvelope, peerTrustScore?: number): PolicyDecision;
|
|
10
|
+
getConfig(): PolicyConfig;
|
|
11
|
+
updateConfig(full: PolicyConfig): Promise<void>;
|
|
12
|
+
patchConfig(partial: Partial<PolicyConfig>): Promise<void>;
|
|
13
|
+
resetConfig(): Promise<void>;
|
|
14
|
+
private isRateLimited;
|
|
15
|
+
private incrementRate;
|
|
16
|
+
private saveConfig;
|
|
17
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Layer B — Policy Decision Engine
|
|
3
|
+
// Evaluates inbound proposals against local policy config
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.PolicyEngine = void 0;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
const CLAWNEXUS_DIR = path.join(os.homedir(), ".clawnexus");
|
|
43
|
+
const POLICY_PATH = path.join(CLAWNEXUS_DIR, "policy.json");
|
|
44
|
+
const DEFAULT_POLICY = {
|
|
45
|
+
mode: "queue",
|
|
46
|
+
trust_threshold: 50,
|
|
47
|
+
rate_limit: {
|
|
48
|
+
max_per_minute: 10,
|
|
49
|
+
max_per_peer_minute: 3,
|
|
50
|
+
},
|
|
51
|
+
delegation: {
|
|
52
|
+
allow: false,
|
|
53
|
+
max_depth: 3,
|
|
54
|
+
},
|
|
55
|
+
capability_filter: [],
|
|
56
|
+
access_control: {
|
|
57
|
+
whitelist: [],
|
|
58
|
+
blacklist: [],
|
|
59
|
+
},
|
|
60
|
+
auto_approve_types: [],
|
|
61
|
+
max_concurrent_tasks: 5,
|
|
62
|
+
};
|
|
63
|
+
class PolicyEngine {
|
|
64
|
+
config = { ...DEFAULT_POLICY };
|
|
65
|
+
configDir;
|
|
66
|
+
configPath;
|
|
67
|
+
rateCounters = new Map();
|
|
68
|
+
constructor(configDir) {
|
|
69
|
+
this.configDir = configDir ?? CLAWNEXUS_DIR;
|
|
70
|
+
this.configPath = path.join(this.configDir, "policy.json");
|
|
71
|
+
}
|
|
72
|
+
async init() {
|
|
73
|
+
await fs.promises.mkdir(this.configDir, { recursive: true });
|
|
74
|
+
if (fs.existsSync(this.configPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = await fs.promises.readFile(this.configPath, "utf-8");
|
|
77
|
+
const data = JSON.parse(raw);
|
|
78
|
+
this.config = { ...DEFAULT_POLICY, ...data };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Corrupted — use defaults
|
|
82
|
+
this.config = { ...DEFAULT_POLICY };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
await this.saveConfig();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
evaluate(envelope, peerTrustScore = 0) {
|
|
90
|
+
const peer = envelope.from;
|
|
91
|
+
// 1. Blacklist check
|
|
92
|
+
if (this.config.access_control.blacklist.includes(peer)) {
|
|
93
|
+
return { result: "reject", reason: "policy_denied", details: "Peer is blacklisted" };
|
|
94
|
+
}
|
|
95
|
+
// 2. Rate limit check
|
|
96
|
+
if (this.isRateLimited(peer)) {
|
|
97
|
+
return { result: "reject", reason: "rate_limited", details: "Rate limit exceeded" };
|
|
98
|
+
}
|
|
99
|
+
this.incrementRate(peer);
|
|
100
|
+
// 3. Whitelist check — whitelisted peers bypass trust/capability checks
|
|
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) {
|
|
104
|
+
return { result: "reject", reason: "trust_insufficient", details: `Score ${peerTrustScore} < threshold ${this.config.trust_threshold}` };
|
|
105
|
+
}
|
|
106
|
+
// 5. Delegation depth check
|
|
107
|
+
if (envelope.type === "delegate") {
|
|
108
|
+
const dp = envelope.payload;
|
|
109
|
+
if (!this.config.delegation.allow) {
|
|
110
|
+
return { result: "reject", reason: "policy_denied", details: "Delegation not allowed" };
|
|
111
|
+
}
|
|
112
|
+
if ((dp.task?.delegation_depth ?? 0) > this.config.delegation.max_depth) {
|
|
113
|
+
return { result: "reject", reason: "policy_denied", details: "Delegation depth exceeded" };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 6. Capability filter (if non-empty, task_type must match)
|
|
117
|
+
if (envelope.type === "propose" || envelope.type === "delegate") {
|
|
118
|
+
const task = envelope.type === "propose"
|
|
119
|
+
? envelope.payload.task
|
|
120
|
+
: envelope.payload.task;
|
|
121
|
+
if (this.config.capability_filter.length > 0) {
|
|
122
|
+
const matches = this.config.capability_filter.some((pattern) => task.task_type === pattern || matchGlob(pattern, task.task_type));
|
|
123
|
+
if (!matches) {
|
|
124
|
+
return { result: "reject", reason: "capability_mismatch", details: `task_type "${task.task_type}" not in capability filter` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 7. Approval mode
|
|
129
|
+
switch (this.config.mode) {
|
|
130
|
+
case "auto":
|
|
131
|
+
return { result: "accept", reason: "auto_approved" };
|
|
132
|
+
case "queue":
|
|
133
|
+
return { result: "queue", reason: "queued_for_review" };
|
|
134
|
+
case "hybrid": {
|
|
135
|
+
if (isWhitelisted) {
|
|
136
|
+
return { result: "accept", reason: "auto_approved", details: "Whitelisted peer" };
|
|
137
|
+
}
|
|
138
|
+
// Check auto_approve_types
|
|
139
|
+
if (envelope.type === "propose") {
|
|
140
|
+
const taskType = envelope.payload.task.task_type;
|
|
141
|
+
if (this.config.auto_approve_types.includes(taskType)) {
|
|
142
|
+
return { result: "accept", reason: "auto_approved", details: `task_type "${taskType}" auto-approved` };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { result: "queue", reason: "queued_for_review" };
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
return { result: "queue", reason: "queued_for_review" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
getConfig() {
|
|
152
|
+
return { ...this.config };
|
|
153
|
+
}
|
|
154
|
+
async updateConfig(full) {
|
|
155
|
+
this.config = { ...full };
|
|
156
|
+
await this.saveConfig();
|
|
157
|
+
}
|
|
158
|
+
async patchConfig(partial) {
|
|
159
|
+
this.config = deepMerge(this.config, partial);
|
|
160
|
+
await this.saveConfig();
|
|
161
|
+
}
|
|
162
|
+
async resetConfig() {
|
|
163
|
+
this.config = { ...DEFAULT_POLICY };
|
|
164
|
+
await this.saveConfig();
|
|
165
|
+
}
|
|
166
|
+
isRateLimited(peer) {
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
// Global rate
|
|
169
|
+
const global = this.rateCounters.get("__global__");
|
|
170
|
+
if (global && global.resetAt > now && global.count >= this.config.rate_limit.max_per_minute) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
// Per-peer rate
|
|
174
|
+
const peerRate = this.rateCounters.get(peer);
|
|
175
|
+
if (peerRate && peerRate.resetAt > now && peerRate.count >= this.config.rate_limit.max_per_peer_minute) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
incrementRate(peer) {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
const windowEnd = now + 60_000;
|
|
183
|
+
for (const key of ["__global__", peer]) {
|
|
184
|
+
const existing = this.rateCounters.get(key);
|
|
185
|
+
if (!existing || existing.resetAt <= now) {
|
|
186
|
+
this.rateCounters.set(key, { count: 1, resetAt: windowEnd });
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
existing.count++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async saveConfig() {
|
|
194
|
+
const json = JSON.stringify(this.config, null, 2);
|
|
195
|
+
const tmpPath = this.configPath + ".tmp";
|
|
196
|
+
await fs.promises.writeFile(tmpPath, json, "utf-8");
|
|
197
|
+
await fs.promises.rename(tmpPath, this.configPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
exports.PolicyEngine = PolicyEngine;
|
|
201
|
+
function matchGlob(pattern, value) {
|
|
202
|
+
// Simple glob: only supports trailing *
|
|
203
|
+
if (pattern.endsWith("*")) {
|
|
204
|
+
return value.startsWith(pattern.slice(0, -1));
|
|
205
|
+
}
|
|
206
|
+
return pattern === value;
|
|
207
|
+
}
|
|
208
|
+
function deepMerge(target, source) {
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
const result = { ...target };
|
|
211
|
+
const src = source;
|
|
212
|
+
const tgt = target;
|
|
213
|
+
for (const key of Object.keys(src)) {
|
|
214
|
+
const sv = src[key];
|
|
215
|
+
const tv = tgt[key];
|
|
216
|
+
if (sv !== null &&
|
|
217
|
+
sv !== undefined &&
|
|
218
|
+
typeof sv === "object" &&
|
|
219
|
+
!Array.isArray(sv) &&
|
|
220
|
+
tv !== null &&
|
|
221
|
+
tv !== undefined &&
|
|
222
|
+
typeof tv === "object" &&
|
|
223
|
+
!Array.isArray(tv)) {
|
|
224
|
+
result[key] = { ...tv, ...sv };
|
|
225
|
+
}
|
|
226
|
+
else if (sv !== undefined) {
|
|
227
|
+
result[key] = sv;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LayerBEnvelope, LayerBMessageType, LayerBPayload } from "./types.js";
|
|
2
|
+
export interface EnvelopeOptions {
|
|
3
|
+
in_reply_to?: string;
|
|
4
|
+
ttl?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function createEnvelope(from: string, to: string, type: LayerBMessageType, payload: LayerBPayload, opts?: EnvelopeOptions): LayerBEnvelope;
|
|
7
|
+
export declare class ProtocolError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare function parseEnvelope(raw: string): LayerBEnvelope;
|
|
11
|
+
export declare function validatePayload(type: LayerBMessageType, payload: LayerBPayload): void;
|
|
12
|
+
export declare function isExpired(envelope: LayerBEnvelope): boolean;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Layer B — Message Protocol
|
|
3
|
+
// Pure functions: envelope construction, parsing, validation
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.ProtocolError = void 0;
|
|
6
|
+
exports.createEnvelope = createEnvelope;
|
|
7
|
+
exports.parseEnvelope = parseEnvelope;
|
|
8
|
+
exports.validatePayload = validatePayload;
|
|
9
|
+
exports.isExpired = isExpired;
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const PROTOCOL = "clawnexus-agent";
|
|
12
|
+
const VERSION = "1.0";
|
|
13
|
+
const DEFAULT_TTL = 300; // 5 minutes
|
|
14
|
+
const VALID_TYPES = new Set([
|
|
15
|
+
"query", "propose", "accept", "reject", "delegate",
|
|
16
|
+
"report", "cancel", "capability", "heartbeat",
|
|
17
|
+
]);
|
|
18
|
+
function createEnvelope(from, to, type, payload, opts) {
|
|
19
|
+
return {
|
|
20
|
+
protocol: PROTOCOL,
|
|
21
|
+
version: VERSION,
|
|
22
|
+
message_id: (0, node_crypto_1.randomUUID)(),
|
|
23
|
+
in_reply_to: opts?.in_reply_to,
|
|
24
|
+
from,
|
|
25
|
+
to,
|
|
26
|
+
type,
|
|
27
|
+
payload,
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
ttl: opts?.ttl ?? DEFAULT_TTL,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
class ProtocolError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "ProtocolError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ProtocolError = ProtocolError;
|
|
39
|
+
function parseEnvelope(raw) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new ProtocolError("Invalid JSON");
|
|
46
|
+
}
|
|
47
|
+
const obj = parsed;
|
|
48
|
+
if (obj.protocol !== PROTOCOL) {
|
|
49
|
+
throw new ProtocolError(`Unknown protocol: ${obj.protocol}`);
|
|
50
|
+
}
|
|
51
|
+
if (obj.version !== VERSION) {
|
|
52
|
+
throw new ProtocolError(`Unsupported version: ${obj.version}`);
|
|
53
|
+
}
|
|
54
|
+
if (!obj.message_id || typeof obj.message_id !== "string") {
|
|
55
|
+
throw new ProtocolError("Missing message_id");
|
|
56
|
+
}
|
|
57
|
+
if (!obj.from || typeof obj.from !== "string") {
|
|
58
|
+
throw new ProtocolError("Missing from");
|
|
59
|
+
}
|
|
60
|
+
if (!obj.to || typeof obj.to !== "string") {
|
|
61
|
+
throw new ProtocolError("Missing to");
|
|
62
|
+
}
|
|
63
|
+
if (!VALID_TYPES.has(obj.type)) {
|
|
64
|
+
throw new ProtocolError(`Invalid type: ${obj.type}`);
|
|
65
|
+
}
|
|
66
|
+
if (!obj.payload || typeof obj.payload !== "object") {
|
|
67
|
+
throw new ProtocolError("Missing payload");
|
|
68
|
+
}
|
|
69
|
+
if (!obj.timestamp || typeof obj.timestamp !== "string") {
|
|
70
|
+
throw new ProtocolError("Missing timestamp");
|
|
71
|
+
}
|
|
72
|
+
validatePayload(obj.type, obj.payload);
|
|
73
|
+
return obj;
|
|
74
|
+
}
|
|
75
|
+
function validatePayload(type, payload) {
|
|
76
|
+
switch (type) {
|
|
77
|
+
case "query": {
|
|
78
|
+
const p = payload;
|
|
79
|
+
if (!p.query_type)
|
|
80
|
+
throw new ProtocolError("query: missing query_type");
|
|
81
|
+
if (!["capabilities", "status", "availability"].includes(p.query_type)) {
|
|
82
|
+
throw new ProtocolError(`query: invalid query_type: ${p.query_type}`);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "propose": {
|
|
87
|
+
const p = payload;
|
|
88
|
+
if (!p.task)
|
|
89
|
+
throw new ProtocolError("propose: missing task");
|
|
90
|
+
if (!p.task.task_type)
|
|
91
|
+
throw new ProtocolError("propose: missing task.task_type");
|
|
92
|
+
if (!p.task.description)
|
|
93
|
+
throw new ProtocolError("propose: missing task.description");
|
|
94
|
+
if (p.task.delegation_depth !== undefined && p.task.delegation_depth > 5) {
|
|
95
|
+
throw new ProtocolError("propose: delegation_depth exceeds hard cap (5)");
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "accept": {
|
|
100
|
+
const p = payload;
|
|
101
|
+
if (!p.task_id)
|
|
102
|
+
throw new ProtocolError("accept: missing task_id");
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "reject": {
|
|
106
|
+
const p = payload;
|
|
107
|
+
if (!p.task_id)
|
|
108
|
+
throw new ProtocolError("reject: missing task_id");
|
|
109
|
+
if (!p.reason)
|
|
110
|
+
throw new ProtocolError("reject: missing reason");
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "delegate": {
|
|
114
|
+
const p = payload;
|
|
115
|
+
if (!p.task_id)
|
|
116
|
+
throw new ProtocolError("delegate: missing task_id");
|
|
117
|
+
if (!p.original_from)
|
|
118
|
+
throw new ProtocolError("delegate: missing original_from");
|
|
119
|
+
if (!p.task)
|
|
120
|
+
throw new ProtocolError("delegate: missing task");
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case "report": {
|
|
124
|
+
const p = payload;
|
|
125
|
+
if (!p.task_id)
|
|
126
|
+
throw new ProtocolError("report: missing task_id");
|
|
127
|
+
if (!p.status)
|
|
128
|
+
throw new ProtocolError("report: missing status");
|
|
129
|
+
if (!["completed", "failed", "progress"].includes(p.status)) {
|
|
130
|
+
throw new ProtocolError(`report: invalid status: ${p.status}`);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case "cancel": {
|
|
135
|
+
const p = payload;
|
|
136
|
+
if (!p.task_id)
|
|
137
|
+
throw new ProtocolError("cancel: missing task_id");
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "capability": {
|
|
141
|
+
const p = payload;
|
|
142
|
+
if (!Array.isArray(p.capabilities)) {
|
|
143
|
+
throw new ProtocolError("capability: missing capabilities array");
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "heartbeat": {
|
|
148
|
+
const p = payload;
|
|
149
|
+
if (!p.task_id)
|
|
150
|
+
throw new ProtocolError("heartbeat: missing task_id");
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function isExpired(envelope) {
|
|
156
|
+
const ttl = envelope.ttl ?? DEFAULT_TTL;
|
|
157
|
+
const created = new Date(envelope.timestamp).getTime();
|
|
158
|
+
return Date.now() - created > ttl * 1000;
|
|
159
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { RelayConnector } from "../relay/connector.js";
|
|
3
|
+
import type { PolicyEngine } from "./engine.js";
|
|
4
|
+
import type { TaskManager } from "./tasks.js";
|
|
5
|
+
import type { LayerBEnvelope, ProposePayload, TaskRecord } from "./types.js";
|
|
6
|
+
export interface AgentRouterOptions {
|
|
7
|
+
connector: RelayConnector;
|
|
8
|
+
engine: PolicyEngine;
|
|
9
|
+
tasks: TaskManager;
|
|
10
|
+
localClawId: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class AgentRouter extends EventEmitter {
|
|
13
|
+
private readonly connector;
|
|
14
|
+
private readonly engine;
|
|
15
|
+
private readonly tasks;
|
|
16
|
+
private readonly localClawId;
|
|
17
|
+
private dataHandler;
|
|
18
|
+
private inbox;
|
|
19
|
+
constructor(opts: AgentRouterOptions);
|
|
20
|
+
start(): void;
|
|
21
|
+
stop(): void;
|
|
22
|
+
sendMessage(roomId: string, envelope: LayerBEnvelope): boolean;
|
|
23
|
+
/** Initiate a propose to a peer (outbound task) */
|
|
24
|
+
propose(roomId: string, targetClawId: string, task: ProposePayload["task"]): TaskRecord;
|
|
25
|
+
/** Send a query to a peer */
|
|
26
|
+
query(roomId: string, targetClawId: string, queryType: "capabilities" | "status" | "availability"): LayerBEnvelope;
|
|
27
|
+
/** Approve a queued inbound proposal */
|
|
28
|
+
approveInbox(messageId: string): TaskRecord | null;
|
|
29
|
+
/** Deny a queued inbound proposal */
|
|
30
|
+
denyInbox(messageId: string, reason?: string): void;
|
|
31
|
+
/** Get pending inbox items */
|
|
32
|
+
getInbox(): Array<{
|
|
33
|
+
message_id: string;
|
|
34
|
+
envelope: LayerBEnvelope;
|
|
35
|
+
roomId: string;
|
|
36
|
+
}>;
|
|
37
|
+
private handleData;
|
|
38
|
+
private handleProposal;
|
|
39
|
+
private acceptProposal;
|
|
40
|
+
private handleQuery;
|
|
41
|
+
}
|