@highway1/cli 0.1.49 → 0.1.50

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,5 +1,7 @@
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
6
  createNode,
5
7
  importKeyPair,
@@ -10,18 +12,43 @@ import {
10
12
  extractPublicKey,
11
13
  createEnvelope,
12
14
  signEnvelope,
15
+ createTrustSystem,
16
+ MessageQueue,
17
+ DefenseMiddleware,
13
18
  type ClawiverseNode,
14
19
  type MessageRouter,
15
20
  type DHTOperations,
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
+ type DaemonCommand =
30
+ | 'send'
31
+ | 'discover'
32
+ | 'status'
33
+ | 'messages'
34
+ | 'shutdown'
35
+ // Queue commands
36
+ | 'inbox'
37
+ | 'get_message'
38
+ | 'mark_read'
39
+ | 'delete_message'
40
+ | 'outbox'
41
+ | 'retry_message'
42
+ // Defense commands
43
+ | 'block'
44
+ | 'unblock'
45
+ | 'allowlist'
46
+ // Stats
47
+ | 'queue_stats';
48
+
22
49
  interface DaemonRequest {
23
50
  id: string;
24
- command: 'send' | 'discover' | 'status' | 'shutdown';
51
+ command: DaemonCommand;
25
52
  params: any;
26
53
  }
27
54
 
@@ -41,6 +68,11 @@ export class ClawDaemon {
41
68
  private identity: any;
42
69
  private bootstrapPeers: string[];
43
70
 
71
+ // New: persistent queue + defense
72
+ private queue: MessageQueue | null = null;
73
+ private defense: DefenseMiddleware | null = null;
74
+ private trustSystem: TrustSystem | null = null;
75
+
44
76
  constructor(socketPath: string = '/tmp/clawiverse.sock') {
45
77
  this.socketPath = socketPath;
46
78
  this.identity = getIdentity();
@@ -55,7 +87,6 @@ export class ClawDaemon {
55
87
  try {
56
88
  logger.info('Starting Clawiverse daemon', { socketPath: this.socketPath });
57
89
 
58
- // Initialize node once (eliminates 4s overhead per command)
59
90
  const keyPair = importKeyPair({
60
91
  publicKey: this.identity.publicKey,
61
92
  privateKey: this.identity.privateKey,
@@ -71,7 +102,6 @@ export class ClawDaemon {
71
102
  await this.node.start();
72
103
  logger.info('Node started', { peerId: this.node.getPeerId() });
73
104
 
74
- // Initialize DHT and router
75
105
  this.dht = createDHTOperations(this.node.libp2p);
76
106
 
77
107
  const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
@@ -95,6 +125,35 @@ export class ClawDaemon {
95
125
  await this.router.start();
96
126
  logger.info('Router started');
97
127
 
128
+ // Initialize trust system
129
+ const dataDir = join(homedir(), '.clawiverse');
130
+ this.trustSystem = createTrustSystem({
131
+ dbPath: join(dataDir, 'trust'),
132
+ getPublicKey: async (did: string) => extractPublicKey(did),
133
+ });
134
+ await this.trustSystem.start();
135
+ logger.info('Trust system started');
136
+
137
+ // Initialize message queue (LevelDB persistence per CVP-0010 §2.3)
138
+ this.queue = new MessageQueue({
139
+ dbPath: join(dataDir, 'inbox'),
140
+ });
141
+ await this.queue.start();
142
+ logger.info('Message queue started');
143
+
144
+ // Initialize defense middleware
145
+ this.defense = new DefenseMiddleware({
146
+ trustSystem: this.trustSystem,
147
+ storage: this.queue.store,
148
+ minTrustScore: 0, // Accept all by default
149
+ });
150
+ logger.info('Defense middleware initialized');
151
+
152
+ // Register catch-all handler: defense check + queue persistence
153
+ this.router.registerCatchAllHandler(async (envelope) => {
154
+ return await this.handleIncomingMessage(envelope);
155
+ });
156
+
98
157
  // Create IPC server
99
158
  this.server = createServer((socket) => {
100
159
  this.handleConnection(socket);
@@ -113,21 +172,68 @@ export class ClawDaemon {
113
172
  }
114
173
  }
115
174
 
175
+ private async handleIncomingMessage(envelope: MessageEnvelope): Promise<MessageEnvelope | void> {
176
+ if (!this.defense || !this.queue || !this.trustSystem) return;
177
+
178
+ // Run defense checks
179
+ const result = await this.defense.checkMessage(envelope);
180
+ if (!result.allowed) {
181
+ logger.warn('Message rejected by defense', { id: envelope.id, reason: result.reason });
182
+ return;
183
+ }
184
+
185
+ // Persist to inbox
186
+ await this.queue.enqueueInbound(envelope, result.trustScore);
187
+
188
+ // Record interaction for trust scoring
189
+ await this.trustSystem.recordInteraction({
190
+ agentDid: envelope.from,
191
+ timestamp: Date.now(),
192
+ type: 'message',
193
+ success: true,
194
+ responseTime: 0,
195
+ });
196
+
197
+ logger.info('Message queued', { id: envelope.id, from: envelope.from });
198
+ }
199
+
116
200
  private handleConnection(socket: Socket): void {
201
+ let buffer = '';
202
+
117
203
  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');
204
+ buffer += data.toString();
205
+ // Handle newline-delimited JSON
206
+ const lines = buffer.split('\n');
207
+ buffer = lines.pop() ?? '';
208
+
209
+ for (const line of lines) {
210
+ if (!line.trim()) continue;
211
+ try {
212
+ const request: DaemonRequest = JSON.parse(line);
213
+ logger.debug('Received request', { command: request.command, id: request.id });
214
+ const response = await this.handleRequest(request);
215
+ socket.write(JSON.stringify(response) + '\n');
216
+ } catch (error) {
217
+ const errorResponse: DaemonResponse = {
218
+ id: 'unknown',
219
+ success: false,
220
+ error: (error as Error).message,
221
+ };
222
+ socket.write(JSON.stringify(errorResponse) + '\n');
223
+ }
224
+ }
225
+
226
+ // Also handle single-message (no newline) for backward compat
227
+ if (buffer.trim()) {
228
+ try {
229
+ const request: DaemonRequest = JSON.parse(buffer);
230
+ buffer = '';
231
+ logger.debug('Received request', { command: request.command, id: request.id });
232
+ const response = await this.handleRequest(request);
233
+ socket.write(JSON.stringify(response) + '\n');
234
+ } catch {
235
+ // Not complete JSON yet, keep buffering
236
+ }
131
237
  }
132
238
  });
133
239
 
@@ -139,19 +245,30 @@ export class ClawDaemon {
139
245
  private async handleRequest(req: DaemonRequest): Promise<DaemonResponse> {
140
246
  try {
141
247
  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
-
248
+ case 'send': return await this.handleSend(req);
249
+ case 'discover': return await this.handleDiscover(req);
250
+ case 'status': return this.handleStatus(req);
251
+ case 'messages': return await this.handleMessages(req);
151
252
  case 'shutdown':
152
253
  await this.shutdown();
153
254
  return { id: req.id, success: true };
154
255
 
256
+ // Queue commands
257
+ case 'inbox': return await this.handleInbox(req);
258
+ case 'get_message': return await this.handleGetMessage(req);
259
+ case 'mark_read': return await this.handleMarkRead(req);
260
+ case 'delete_message': return await this.handleDeleteMessage(req);
261
+ case 'outbox': return await this.handleOutbox(req);
262
+ case 'retry_message': return await this.handleRetryMessage(req);
263
+
264
+ // Defense commands
265
+ case 'block': return await this.handleBlock(req);
266
+ case 'unblock': return await this.handleUnblock(req);
267
+ case 'allowlist': return await this.handleAllowlist(req);
268
+
269
+ // Stats
270
+ case 'queue_stats': return await this.handleQueueStats(req);
271
+
155
272
  default:
156
273
  return { id: req.id, success: false, error: 'Unknown command' };
157
274
  }
@@ -168,7 +285,6 @@ export class ClawDaemon {
168
285
  return { id: req.id, success: false, error: 'Router not initialized' };
169
286
  }
170
287
 
171
- // Create and sign envelope
172
288
  const envelope = createEnvelope(
173
289
  this.identity.did,
174
290
  to,
@@ -186,52 +302,41 @@ export class ClawDaemon {
186
302
  sign(data, keyPair.privateKey)
187
303
  );
188
304
 
189
- // Build peer hint if provided
305
+ // Track outbound in queue
306
+ if (this.queue) {
307
+ await this.queue.enqueueOutbound(signedEnvelope);
308
+ }
309
+
190
310
  let peerHint = undefined;
191
311
  if (peer) {
192
312
  const parts = peer.split('/p2p/');
193
313
  if (parts.length >= 2) {
194
- peerHint = {
195
- peerId: parts[parts.length - 1],
196
- multiaddrs: [peer],
197
- };
314
+ peerHint = { peerId: parts[parts.length - 1], multiaddrs: [peer] };
198
315
  }
199
316
  }
200
317
 
201
- // Send message
202
318
  const response = await this.router.sendMessage(signedEnvelope, peerHint);
203
319
 
320
+ if (this.queue) {
321
+ await this.queue.markOutboundDelivered(signedEnvelope.id);
322
+ }
323
+
204
324
  return {
205
325
  id: req.id,
206
326
  success: true,
207
- data: {
208
- id: signedEnvelope.id,
209
- response: response || null,
210
- },
327
+ data: { id: signedEnvelope.id, response: response || null },
211
328
  };
212
329
  }
213
330
 
214
331
  private async handleDiscover(req: DaemonRequest): Promise<DaemonResponse> {
215
332
  const { query } = req.params;
216
-
217
- if (!this.dht) {
218
- return { id: req.id, success: false, error: 'DHT not initialized' };
219
- }
220
-
333
+ if (!this.dht) return { id: req.id, success: false, error: 'DHT not initialized' };
221
334
  const results = await this.dht.searchSemantic(query);
222
-
223
- return {
224
- id: req.id,
225
- success: true,
226
- data: results,
227
- };
335
+ return { id: req.id, success: true, data: results };
228
336
  }
229
337
 
230
338
  private handleStatus(req: DaemonRequest): DaemonResponse {
231
- if (!this.node) {
232
- return { id: req.id, success: false, error: 'Node not initialized' };
233
- }
234
-
339
+ if (!this.node) return { id: req.id, success: false, error: 'Node not initialized' };
235
340
  return {
236
341
  id: req.id,
237
342
  success: true,
@@ -245,6 +350,104 @@ export class ClawDaemon {
245
350
  };
246
351
  }
247
352
 
353
+ /** Legacy messages command — delegates to inbox for backward compat */
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,6 +456,16 @@ export class ClawDaemon {
253
456
  this.router = null;
254
457
  }
255
458
 
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
+
256
469
  if (this.node) {
257
470
  await this.node.stop();
258
471
  this.node = null;
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.