@highway1/cli 0.1.49 → 0.1.52

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.
@@ -1,27 +1,65 @@
1
1
  import { createServer, Socket } from 'net';
2
2
  import type { Server } from 'net';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
3
5
  import {
4
- createNode,
6
+ createRelayClient,
7
+ createRelayIndexOperations,
5
8
  importKeyPair,
6
9
  createMessageRouter,
7
- createDHTOperations,
8
10
  sign,
9
11
  verify,
10
12
  extractPublicKey,
11
13
  createEnvelope,
12
14
  signEnvelope,
13
- type ClawiverseNode,
15
+ createTrustSystem,
16
+ MessageQueue,
17
+ DefenseMiddleware,
18
+ type RelayClient,
14
19
  type MessageRouter,
15
- type DHTOperations,
20
+ type RelayIndexOperations,
21
+ type TrustSystem,
22
+ type MessageEnvelope,
16
23
  } from '@highway1/core';
17
24
  import { createLogger } from '@highway1/core';
18
25
  import { getIdentity, getBootstrapPeers } from '../config.js';
19
26
 
20
27
  const logger = createLogger('daemon');
21
28
 
29
+ // Default relay URLs (CVP-0011)
30
+ const DEFAULT_RELAY_URLS = [
31
+ 'ws://relay.highway1.net:8080',
32
+ ];
33
+
34
+ function getRelayUrls(): string[] {
35
+ const envRelays = process.env.HW1_RELAY_URLS;
36
+ if (envRelays) return envRelays.split(',').map((u) => u.trim());
37
+ return DEFAULT_RELAY_URLS;
38
+ }
39
+
40
+ type DaemonCommand =
41
+ | 'send'
42
+ | 'discover'
43
+ | 'status'
44
+ | 'messages'
45
+ | 'shutdown'
46
+ // Queue commands
47
+ | 'inbox'
48
+ | 'get_message'
49
+ | 'mark_read'
50
+ | 'delete_message'
51
+ | 'outbox'
52
+ | 'retry_message'
53
+ // Defense commands
54
+ | 'block'
55
+ | 'unblock'
56
+ | 'allowlist'
57
+ // Stats
58
+ | 'queue_stats';
59
+
22
60
  interface DaemonRequest {
23
61
  id: string;
24
- command: 'send' | 'discover' | 'status' | 'shutdown';
62
+ command: DaemonCommand;
25
63
  params: any;
26
64
  }
27
65
 
@@ -33,18 +71,21 @@ interface DaemonResponse {
33
71
  }
34
72
 
35
73
  export class ClawDaemon {
36
- private node: ClawiverseNode | null = null;
74
+ private relayClient: RelayClient | null = null;
75
+ private relayIndex: RelayIndexOperations | null = null;
37
76
  private router: MessageRouter | null = null;
38
- private dht: DHTOperations | null = null;
39
77
  private server: Server | null = null;
40
78
  private socketPath: string;
41
79
  private identity: any;
42
- private bootstrapPeers: string[];
80
+
81
+ // Persistent queue + defense
82
+ private queue: MessageQueue | null = null;
83
+ private defense: DefenseMiddleware | null = null;
84
+ private trustSystem: TrustSystem | null = null;
43
85
 
44
86
  constructor(socketPath: string = '/tmp/clawiverse.sock') {
45
87
  this.socketPath = socketPath;
46
88
  this.identity = getIdentity();
47
- this.bootstrapPeers = getBootstrapPeers();
48
89
 
49
90
  if (!this.identity) {
50
91
  throw new Error('No identity found. Run "clawiverse init" first.');
@@ -55,24 +96,38 @@ export class ClawDaemon {
55
96
  try {
56
97
  logger.info('Starting Clawiverse daemon', { socketPath: this.socketPath });
57
98
 
58
- // Initialize node once (eliminates 4s overhead per command)
59
99
  const keyPair = importKeyPair({
60
100
  publicKey: this.identity.publicKey,
61
101
  privateKey: this.identity.privateKey,
62
102
  });
63
103
 
64
- this.node = await createNode({
104
+ // Load agent card for HELLO message
105
+ const { getAgentCard, createAgentCard, signAgentCard } = await import('@highway1/core');
106
+ const cardConfig = getAgentCard();
107
+ const capabilities = (cardConfig?.capabilities ?? []).map((c: string) => ({
108
+ id: c, name: c, description: `Capability: ${c}`,
109
+ }));
110
+ const agentCard = createAgentCard(
111
+ this.identity.did,
112
+ cardConfig?.name ?? 'Clawiverse Agent',
113
+ cardConfig?.description ?? '',
114
+ capabilities,
115
+ [],
116
+ );
117
+ const signedCard = await signAgentCard(agentCard, (data) => sign(data, keyPair.privateKey));
118
+
119
+ const relayUrls = getRelayUrls();
120
+ this.relayClient = createRelayClient({
121
+ relayUrls,
122
+ did: this.identity.did,
65
123
  keyPair,
66
- bootstrapPeers: this.bootstrapPeers,
67
- enableDHT: true,
68
- reserveRelaySlot: this.bootstrapPeers.length > 0,
124
+ card: signedCard,
69
125
  });
70
126
 
71
- await this.node.start();
72
- logger.info('Node started', { peerId: this.node.getPeerId() });
127
+ await this.relayClient.start();
128
+ logger.info('Relay client started', { relays: this.relayClient.getConnectedRelays() });
73
129
 
74
- // Initialize DHT and router
75
- this.dht = createDHTOperations(this.node.libp2p);
130
+ this.relayIndex = createRelayIndexOperations(this.relayClient);
76
131
 
77
132
  const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
78
133
  try {
@@ -85,16 +140,39 @@ export class ClawDaemon {
85
140
  }
86
141
  };
87
142
 
88
- this.router = createMessageRouter(
89
- this.node.libp2p,
90
- verifyFn,
91
- this.dht,
92
- this.bootstrapPeers
93
- );
94
-
143
+ this.router = createMessageRouter(this.relayClient, verifyFn);
95
144
  await this.router.start();
96
145
  logger.info('Router started');
97
146
 
147
+ // Initialize trust system
148
+ const dataDir = join(homedir(), '.clawiverse');
149
+ this.trustSystem = createTrustSystem({
150
+ dbPath: join(dataDir, 'trust'),
151
+ getPublicKey: async (did: string) => extractPublicKey(did),
152
+ });
153
+ await this.trustSystem.start();
154
+ logger.info('Trust system started');
155
+
156
+ // Initialize message queue (LevelDB persistence per CVP-0010 §2.3)
157
+ this.queue = new MessageQueue({
158
+ dbPath: join(dataDir, 'inbox'),
159
+ });
160
+ await this.queue.start();
161
+ logger.info('Message queue started');
162
+
163
+ // Initialize defense middleware
164
+ this.defense = new DefenseMiddleware({
165
+ trustSystem: this.trustSystem,
166
+ storage: this.queue.store,
167
+ minTrustScore: 0,
168
+ });
169
+ logger.info('Defense middleware initialized');
170
+
171
+ // Register catch-all handler: defense check + queue persistence
172
+ this.router.registerCatchAllHandler(async (envelope) => {
173
+ return await this.handleIncomingMessage(envelope);
174
+ });
175
+
98
176
  // Create IPC server
99
177
  this.server = createServer((socket) => {
100
178
  this.handleConnection(socket);
@@ -105,7 +183,7 @@ export class ClawDaemon {
105
183
 
106
184
  console.log(`✓ Clawiverse daemon started`);
107
185
  console.log(` Socket: ${this.socketPath}`);
108
- console.log(` Peer ID: ${this.node.getPeerId()}`);
186
+ console.log(` Relays: ${this.relayClient.getConnectedRelays().join(', ')}`);
109
187
  console.log(` DID: ${this.identity.did}`);
110
188
  } catch (error) {
111
189
  logger.error('Failed to start daemon', error);
@@ -113,21 +191,63 @@ export class ClawDaemon {
113
191
  }
114
192
  }
115
193
 
194
+ private async handleIncomingMessage(envelope: MessageEnvelope): Promise<MessageEnvelope | void> {
195
+ if (!this.defense || !this.queue || !this.trustSystem) return;
196
+
197
+ const result = await this.defense.checkMessage(envelope);
198
+ if (!result.allowed) {
199
+ logger.warn('Message rejected by defense', { id: envelope.id, reason: result.reason });
200
+ return;
201
+ }
202
+
203
+ await this.queue.enqueueInbound(envelope, result.trustScore);
204
+
205
+ await this.trustSystem.recordInteraction({
206
+ agentDid: envelope.from,
207
+ timestamp: Date.now(),
208
+ type: 'message',
209
+ success: true,
210
+ responseTime: 0,
211
+ });
212
+
213
+ logger.info('Message queued', { id: envelope.id, from: envelope.from });
214
+ }
215
+
116
216
  private handleConnection(socket: Socket): void {
217
+ let buffer = '';
218
+
117
219
  socket.on('data', async (data) => {
118
- try {
119
- const request: DaemonRequest = JSON.parse(data.toString());
120
- logger.debug('Received request', { command: request.command, id: request.id });
121
-
122
- const response = await this.handleRequest(request);
123
- socket.write(JSON.stringify(response) + '\n');
124
- } catch (error) {
125
- const errorResponse: DaemonResponse = {
126
- id: 'unknown',
127
- success: false,
128
- error: (error as Error).message,
129
- };
130
- socket.write(JSON.stringify(errorResponse) + '\n');
220
+ buffer += data.toString();
221
+ const lines = buffer.split('\n');
222
+ buffer = lines.pop() ?? '';
223
+
224
+ for (const line of lines) {
225
+ if (!line.trim()) continue;
226
+ try {
227
+ const request: DaemonRequest = JSON.parse(line);
228
+ logger.debug('Received request', { command: request.command, id: request.id });
229
+ const response = await this.handleRequest(request);
230
+ socket.write(JSON.stringify(response) + '\n');
231
+ } catch (error) {
232
+ const errorResponse: DaemonResponse = {
233
+ id: 'unknown',
234
+ success: false,
235
+ error: (error as Error).message,
236
+ };
237
+ socket.write(JSON.stringify(errorResponse) + '\n');
238
+ }
239
+ }
240
+
241
+ if (buffer.trim()) {
242
+ try {
243
+ const request: DaemonRequest = JSON.parse(buffer);
244
+ buffer = '';
245
+ logger.debug('Received request', { command: request.command, id: request.id });
246
+ const response = await this.handleRequest(request);
247
+ socket.write(JSON.stringify(response) + '\n');
248
+ } catch {
249
+ // Not complete JSON yet, keep buffering
250
+ }
131
251
  }
132
252
  });
133
253
 
@@ -139,19 +259,27 @@ export class ClawDaemon {
139
259
  private async handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
140
260
  try {
141
261
  switch (req.command) {
142
- case 'send':
143
- return await this.handleSend(req);
144
-
145
- case 'discover':
146
- return await this.handleDiscover(req);
147
-
148
- case 'status':
149
- return this.handleStatus(req);
150
-
262
+ case 'send': return await this.handleSend(req);
263
+ case 'discover': return await this.handleDiscover(req);
264
+ case 'status': return this.handleStatus(req);
265
+ case 'messages': return await this.handleMessages(req);
151
266
  case 'shutdown':
152
267
  await this.shutdown();
153
268
  return { id: req.id, success: true };
154
269
 
270
+ case 'inbox': return await this.handleInbox(req);
271
+ case 'get_message': return await this.handleGetMessage(req);
272
+ case 'mark_read': return await this.handleMarkRead(req);
273
+ case 'delete_message': return await this.handleDeleteMessage(req);
274
+ case 'outbox': return await this.handleOutbox(req);
275
+ case 'retry_message': return await this.handleRetryMessage(req);
276
+
277
+ case 'block': return await this.handleBlock(req);
278
+ case 'unblock': return await this.handleUnblock(req);
279
+ case 'allowlist': return await this.handleAllowlist(req);
280
+
281
+ case 'queue_stats': return await this.handleQueueStats(req);
282
+
155
283
  default:
156
284
  return { id: req.id, success: false, error: 'Unknown command' };
157
285
  }
@@ -162,13 +290,12 @@ export class ClawDaemon {
162
290
  }
163
291
 
164
292
  private async handleSend(req: DaemonRequest): Promise<DaemonResponse> {
165
- const { to, protocol, payload, type, peer } = req.params;
293
+ const { to, protocol, payload, type } = req.params;
166
294
 
167
295
  if (!this.router) {
168
296
  return { id: req.id, success: false, error: 'Router not initialized' };
169
297
  }
170
298
 
171
- // Create and sign envelope
172
299
  const envelope = createEnvelope(
173
300
  this.identity.did,
174
301
  to,
@@ -186,65 +313,141 @@ export class ClawDaemon {
186
313
  sign(data, keyPair.privateKey)
187
314
  );
188
315
 
189
- // Build peer hint if provided
190
- let peerHint = undefined;
191
- if (peer) {
192
- const parts = peer.split('/p2p/');
193
- if (parts.length >= 2) {
194
- peerHint = {
195
- peerId: parts[parts.length - 1],
196
- multiaddrs: [peer],
197
- };
198
- }
316
+ if (this.queue) {
317
+ await this.queue.enqueueOutbound(signedEnvelope);
199
318
  }
200
319
 
201
- // Send message
202
- const response = await this.router.sendMessage(signedEnvelope, peerHint);
320
+ const response = await this.router.sendMessage(signedEnvelope);
321
+
322
+ if (this.queue) {
323
+ await this.queue.markOutboundDelivered(signedEnvelope.id);
324
+ }
203
325
 
204
326
  return {
205
327
  id: req.id,
206
328
  success: true,
207
- data: {
208
- id: signedEnvelope.id,
209
- response: response || null,
210
- },
329
+ data: { id: signedEnvelope.id, response: response || null },
211
330
  };
212
331
  }
213
332
 
214
333
  private async handleDiscover(req: DaemonRequest): Promise<DaemonResponse> {
215
334
  const { query } = req.params;
216
-
217
- if (!this.dht) {
218
- return { id: req.id, success: false, error: 'DHT not initialized' };
219
- }
220
-
221
- const results = await this.dht.searchSemantic(query);
222
-
223
- return {
224
- id: req.id,
225
- success: true,
226
- data: results,
227
- };
335
+ if (!this.relayIndex) return { id: req.id, success: false, error: 'Relay index not initialized' };
336
+ const results = await this.relayIndex.searchSemantic(query);
337
+ return { id: req.id, success: true, data: results };
228
338
  }
229
339
 
230
340
  private handleStatus(req: DaemonRequest): DaemonResponse {
231
- if (!this.node) {
232
- return { id: req.id, success: false, error: 'Node not initialized' };
233
- }
234
-
341
+ if (!this.relayClient) return { id: req.id, success: false, error: 'Relay client not initialized' };
235
342
  return {
236
343
  id: req.id,
237
344
  success: true,
238
345
  data: {
239
346
  running: true,
240
- peerId: this.node.getPeerId(),
347
+ connectedRelays: this.relayClient.getConnectedRelays(),
348
+ peerCount: this.relayClient.getPeerCount(),
241
349
  did: this.identity.did,
242
- multiaddrs: this.node.getMultiaddrs(),
243
- bootstrapPeers: this.bootstrapPeers,
244
350
  },
245
351
  };
246
352
  }
247
353
 
354
+ private async handleMessages(req: DaemonRequest): Promise<DaemonResponse> {
355
+ const { limit = 10 } = req.params || {};
356
+ if (this.queue) {
357
+ const page = await this.queue.getInbox({}, { limit });
358
+ return {
359
+ id: req.id,
360
+ success: true,
361
+ data: {
362
+ messages: page.messages.map((m) => ({ ...m.envelope, receivedAt: m.receivedAt })),
363
+ total: page.total,
364
+ },
365
+ };
366
+ }
367
+ return { id: req.id, success: true, data: { messages: [], total: 0 } };
368
+ }
369
+
370
+ // ─── Queue Handlers ───────────────────────────────────────────────────────
371
+
372
+ private async handleInbox(req: DaemonRequest): Promise<DaemonResponse> {
373
+ if (!this.queue) return { id: req.id, success: false, error: 'Queue not initialized' };
374
+ const { filter, pagination } = req.params || {};
375
+ const page = await this.queue.getInbox(filter, pagination);
376
+ return { id: req.id, success: true, data: page };
377
+ }
378
+
379
+ private async handleGetMessage(req: DaemonRequest): Promise<DaemonResponse> {
380
+ if (!this.queue) return { id: req.id, success: false, error: 'Queue not initialized' };
381
+ const { id } = req.params;
382
+ const msg = await this.queue.getMessage(id);
383
+ if (!msg) return { id: req.id, success: false, error: 'Message not found' };
384
+ return { id: req.id, success: true, data: msg };
385
+ }
386
+
387
+ private async handleMarkRead(req: DaemonRequest): Promise<DaemonResponse> {
388
+ if (!this.queue) return { id: req.id, success: false, error: 'Queue not initialized' };
389
+ await this.queue.markAsRead(req.params.id);
390
+ return { id: req.id, success: true };
391
+ }
392
+
393
+ private async handleDeleteMessage(req: DaemonRequest): Promise<DaemonResponse> {
394
+ if (!this.queue) return { id: req.id, success: false, error: 'Queue not initialized' };
395
+ await this.queue.deleteMessage(req.params.id);
396
+ return { id: req.id, success: true };
397
+ }
398
+
399
+ private async handleOutbox(req: DaemonRequest): Promise<DaemonResponse> {
400
+ if (!this.queue) return { id: req.id, success: false, error: 'Queue not initialized' };
401
+ const page = await this.queue.getOutbox(req.params?.pagination);
402
+ return { id: req.id, success: true, data: page };
403
+ }
404
+
405
+ private async handleRetryMessage(req: DaemonRequest): Promise<DaemonResponse> {
406
+ if (!this.queue) return { id: req.id, success: false, error: 'Queue not initialized' };
407
+ await this.queue.retryMessage(req.params.id);
408
+ return { id: req.id, success: true };
409
+ }
410
+
411
+ // ─── Defense Handlers ─────────────────────────────────────────────────────
412
+
413
+ private async handleBlock(req: DaemonRequest): Promise<DaemonResponse> {
414
+ if (!this.defense) return { id: req.id, success: false, error: 'Defense not initialized' };
415
+ const { did, reason = 'Blocked by user' } = req.params;
416
+ await this.defense.blockAgent(did, reason, this.identity.did);
417
+ return { id: req.id, success: true };
418
+ }
419
+
420
+ private async handleUnblock(req: DaemonRequest): Promise<DaemonResponse> {
421
+ if (!this.defense) return { id: req.id, success: false, error: 'Defense not initialized' };
422
+ await this.defense.unblockAgent(req.params.did);
423
+ return { id: req.id, success: true };
424
+ }
425
+
426
+ private async handleAllowlist(req: DaemonRequest): Promise<DaemonResponse> {
427
+ if (!this.defense || !this.queue) return { id: req.id, success: false, error: 'Defense not initialized' };
428
+ const { action, did, note } = req.params;
429
+ switch (action) {
430
+ case 'add':
431
+ await this.defense.allowAgent(did, note);
432
+ return { id: req.id, success: true };
433
+ case 'remove':
434
+ await this.defense.removeFromAllowlist(did);
435
+ return { id: req.id, success: true };
436
+ case 'list': {
437
+ const entries = await this.queue.store.listAllowed();
438
+ return { id: req.id, success: true, data: entries };
439
+ }
440
+ default:
441
+ return { id: req.id, success: false, error: `Unknown allowlist action: ${action}` };
442
+ }
443
+ }
444
+
445
+ private async handleQueueStats(req: DaemonRequest): Promise<DaemonResponse> {
446
+ if (!this.queue) return { id: req.id, success: false, error: 'Queue not initialized' };
447
+ const stats = await this.queue.getStats();
448
+ return { id: req.id, success: true, data: stats };
449
+ }
450
+
248
451
  async shutdown(): Promise<void> {
249
452
  logger.info('Shutting down daemon');
250
453
 
@@ -253,9 +456,19 @@ export class ClawDaemon {
253
456
  this.router = null;
254
457
  }
255
458
 
256
- if (this.node) {
257
- await this.node.stop();
258
- this.node = null;
459
+ if (this.queue) {
460
+ await this.queue.stop();
461
+ this.queue = null;
462
+ }
463
+
464
+ if (this.trustSystem) {
465
+ await this.trustSystem.stop();
466
+ this.trustSystem = null;
467
+ }
468
+
469
+ if (this.relayClient) {
470
+ await this.relayClient.stop();
471
+ this.relayClient = null;
259
472
  }
260
473
 
261
474
  if (this.server) {
package/src/index.ts CHANGED
@@ -24,6 +24,11 @@ import { registerIdentityCommand } from './commands/identity.js';
24
24
  import { registerCardCommand } from './commands/card.js';
25
25
  import { createTrustCommand } from './commands/trust.js';
26
26
  import { registerDaemonCommand } from './commands/daemon.js';
27
+ import { createInboxCommand } from './commands/inbox.js';
28
+ import { registerStopCommand } from './commands/stop.js';
29
+ import { registerAskCommand } from './commands/ask.js';
30
+ import { registerServeCommand } from './commands/serve.js';
31
+ import { registerPeersCommand } from './commands/peers.js';
27
32
 
28
33
  const require = createRequire(import.meta.url);
29
34
  const { version } = require('../package.json');
@@ -42,10 +47,15 @@ registerInitCommand(program);
42
47
  registerJoinCommand(program);
43
48
  registerDiscoverCommand(program);
44
49
  registerSendCommand(program);
50
+ registerAskCommand(program);
51
+ registerServeCommand(program);
52
+ registerPeersCommand(program);
45
53
  registerStatusCommand(program);
46
54
  registerIdentityCommand(program);
47
55
  registerCardCommand(program);
48
56
  registerDaemonCommand(program);
57
+ registerStopCommand(program);
49
58
  program.addCommand(createTrustCommand());
59
+ program.addCommand(createInboxCommand());
50
60
 
51
61
  program.parse();
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Clawiverse Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.