@highway1/cli 0.1.48 → 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.
- package/dist/index.js +5292 -1172
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/commands/ask.ts +158 -0
- package/src/commands/card.ts +8 -3
- package/src/commands/identity.ts +12 -3
- package/src/commands/inbox.ts +222 -0
- package/src/commands/peers.ts +85 -0
- package/src/commands/send.ts +17 -9
- package/src/commands/serve.ts +271 -0
- package/src/commands/status.ts +18 -3
- package/src/commands/stop.ts +49 -0
- package/src/commands/trust.ts +144 -5
- package/src/daemon/client.ts +31 -9
- package/src/daemon/server.ts +265 -52
- package/src/index.ts +10 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve Command - CVP-0010 §2.2
|
|
3
|
+
*
|
|
4
|
+
* Registers custom handlers that execute when matching requests arrive.
|
|
5
|
+
*
|
|
6
|
+
* hw1 serve --on "translate" --exec "./translate.sh"
|
|
7
|
+
* hw1 serve --on "code_review" --exec "python review.py"
|
|
8
|
+
* hw1 serve --handlers ./my-handlers/
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { DaemonClient } from '../daemon/client.js';
|
|
13
|
+
import { createLogger } from '@highway1/core';
|
|
14
|
+
import { spawn } from 'child_process';
|
|
15
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
16
|
+
import { join, resolve, basename } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('cli:serve');
|
|
19
|
+
|
|
20
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024; // 256 KiB
|
|
21
|
+
|
|
22
|
+
interface HandlerEntry {
|
|
23
|
+
capability: string;
|
|
24
|
+
exec: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerServeCommand(program: Command): void {
|
|
28
|
+
program
|
|
29
|
+
.command('serve')
|
|
30
|
+
.description('Register handlers for incoming requests (CVP-0010 §2.2)')
|
|
31
|
+
.option('--on <capability>', 'Capability name to handle')
|
|
32
|
+
.option('--exec <script>', 'Script to execute for matching requests')
|
|
33
|
+
.option('--handlers <dir>', 'Directory of handler scripts (filename = capability name)')
|
|
34
|
+
.option('--allow-from <dids...>', 'Only accept requests from these DIDs (default: deny-all except allowlist)')
|
|
35
|
+
.option('--public', 'Accept requests from any agent (overrides --allow-from)')
|
|
36
|
+
.option('--max-concurrency <n>', 'Max concurrent handler executions', '4')
|
|
37
|
+
.option('--timeout <seconds>', 'Handler execution timeout in seconds', '60')
|
|
38
|
+
.option('--format <fmt>', 'Output format: text|json', 'text')
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
const client = new DaemonClient();
|
|
41
|
+
if (!(await client.isDaemonRunning())) {
|
|
42
|
+
console.error('Daemon not running. Start with: hw1 join');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build handler list
|
|
47
|
+
const handlers: HandlerEntry[] = [];
|
|
48
|
+
|
|
49
|
+
if (options.on && options.exec) {
|
|
50
|
+
handlers.push({ capability: options.on, exec: resolve(options.exec) });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options.handlers) {
|
|
54
|
+
const dir = resolve(options.handlers);
|
|
55
|
+
const entries = await readdir(dir);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const fullPath = join(dir, entry);
|
|
58
|
+
const s = await stat(fullPath);
|
|
59
|
+
if (!s.isFile()) continue;
|
|
60
|
+
// Strip extension to get capability name; skip _default for now
|
|
61
|
+
const cap = basename(entry).replace(/\.[^.]+$/, '');
|
|
62
|
+
handlers.push({ capability: cap, exec: fullPath });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (handlers.length === 0) {
|
|
67
|
+
console.error('No handlers specified. Use --on/--exec or --handlers <dir>');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const maxConcurrency = parseInt(options.maxConcurrency, 10);
|
|
72
|
+
const timeoutMs = parseInt(options.timeout, 10) * 1000;
|
|
73
|
+
let activeCount = 0;
|
|
74
|
+
|
|
75
|
+
// Apply allowlist if --allow-from specified
|
|
76
|
+
if (options.allowFrom && !options.public) {
|
|
77
|
+
for (const did of options.allowFrom) {
|
|
78
|
+
await client.send('allowlist', { action: 'add', did, note: 'hw1 serve --allow-from' });
|
|
79
|
+
}
|
|
80
|
+
if (options.format !== 'json') {
|
|
81
|
+
console.log(`Allowlisted ${options.allowFrom.length} DID(s)`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.format !== 'json') {
|
|
86
|
+
console.log(`\nServing ${handlers.length} handler(s):`);
|
|
87
|
+
for (const h of handlers) {
|
|
88
|
+
console.log(` ${h.capability} → ${h.exec}`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`\nMax concurrency: ${maxConcurrency}, timeout: ${options.timeout}s`);
|
|
91
|
+
console.log('Waiting for requests... (Ctrl+C to stop)\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Subscribe to incoming messages via queue
|
|
95
|
+
const subscriptionFilter = {
|
|
96
|
+
protocol: handlers.map((h) => `clawiverse/${h.capability}`),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Poll inbox for new messages matching our capabilities
|
|
100
|
+
const poll = async () => {
|
|
101
|
+
try {
|
|
102
|
+
const page = await client.send('inbox', {
|
|
103
|
+
filter: { unreadOnly: true, status: 'pending' },
|
|
104
|
+
pagination: { limit: 10 },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
for (const msg of page.messages ?? []) {
|
|
108
|
+
const envelope = msg.envelope;
|
|
109
|
+
const protocol: string = envelope.protocol ?? '';
|
|
110
|
+
|
|
111
|
+
// Find matching handler
|
|
112
|
+
const handler = handlers.find((h) =>
|
|
113
|
+
protocol.includes(h.capability) ||
|
|
114
|
+
(envelope.payload as any)?.capability === h.capability
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (!handler) continue;
|
|
118
|
+
|
|
119
|
+
// Mark as read immediately to avoid double-processing
|
|
120
|
+
await client.send('mark_read', { id: envelope.id });
|
|
121
|
+
|
|
122
|
+
// Concurrency control
|
|
123
|
+
if (activeCount >= maxConcurrency) {
|
|
124
|
+
if (options.format !== 'json') {
|
|
125
|
+
console.log(`[BUSY] Rejected ${envelope.id.slice(-8)} from ${envelope.from.slice(0, 20)}…`);
|
|
126
|
+
}
|
|
127
|
+
// Send BUSY response
|
|
128
|
+
await client.send('send', {
|
|
129
|
+
to: envelope.from,
|
|
130
|
+
protocol: envelope.protocol,
|
|
131
|
+
payload: { error: 'BUSY', message: 'Server at capacity, try again later' },
|
|
132
|
+
type: 'response',
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Payload size check
|
|
138
|
+
const payloadStr = JSON.stringify(envelope.payload ?? '');
|
|
139
|
+
if (payloadStr.length > MAX_PAYLOAD_BYTES) {
|
|
140
|
+
await client.send('send', {
|
|
141
|
+
to: envelope.from,
|
|
142
|
+
protocol: envelope.protocol,
|
|
143
|
+
payload: { error: 'PAYLOAD_TOO_LARGE', message: `Max payload is ${MAX_PAYLOAD_BYTES} bytes` },
|
|
144
|
+
type: 'response',
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Execute handler
|
|
150
|
+
activeCount++;
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
|
|
153
|
+
if (options.format !== 'json') {
|
|
154
|
+
console.log(`[${new Date().toLocaleTimeString()}] ${handler.capability} ← ${envelope.from.slice(0, 30)}…`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
executeHandler(handler.exec, envelope.payload, timeoutMs)
|
|
158
|
+
.then(async (result) => {
|
|
159
|
+
const latencyMs = Date.now() - startTime;
|
|
160
|
+
await client.send('send', {
|
|
161
|
+
to: envelope.from,
|
|
162
|
+
protocol: envelope.protocol,
|
|
163
|
+
payload: result,
|
|
164
|
+
type: 'response',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (options.format === 'json') {
|
|
168
|
+
console.log(JSON.stringify({
|
|
169
|
+
event: 'handled',
|
|
170
|
+
capability: handler.capability,
|
|
171
|
+
from: envelope.from,
|
|
172
|
+
latencyMs,
|
|
173
|
+
success: true,
|
|
174
|
+
}));
|
|
175
|
+
} else {
|
|
176
|
+
console.log(` → responded in ${latencyMs}ms`);
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
.catch(async (err) => {
|
|
180
|
+
const latencyMs = Date.now() - startTime;
|
|
181
|
+
const isTimeout = err.message?.includes('timeout');
|
|
182
|
+
await client.send('send', {
|
|
183
|
+
to: envelope.from,
|
|
184
|
+
protocol: envelope.protocol,
|
|
185
|
+
payload: {
|
|
186
|
+
error: isTimeout ? 'TIMEOUT' : 'HANDLER_ERROR',
|
|
187
|
+
message: err.message,
|
|
188
|
+
},
|
|
189
|
+
type: 'response',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (options.format === 'json') {
|
|
193
|
+
console.log(JSON.stringify({
|
|
194
|
+
event: 'error',
|
|
195
|
+
capability: handler.capability,
|
|
196
|
+
from: envelope.from,
|
|
197
|
+
latencyMs,
|
|
198
|
+
error: err.message,
|
|
199
|
+
}));
|
|
200
|
+
} else {
|
|
201
|
+
console.error(` → error after ${latencyMs}ms: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.finally(() => {
|
|
205
|
+
activeCount--;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
logger.warn('Poll error', err);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Poll every 500ms
|
|
214
|
+
const interval = setInterval(poll, 500);
|
|
215
|
+
|
|
216
|
+
// Graceful shutdown
|
|
217
|
+
const shutdown = () => {
|
|
218
|
+
clearInterval(interval);
|
|
219
|
+
if (options.format !== 'json') console.log('\nStopped serving.');
|
|
220
|
+
process.exit(0);
|
|
221
|
+
};
|
|
222
|
+
process.on('SIGINT', shutdown);
|
|
223
|
+
process.on('SIGTERM', shutdown);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function executeHandler(
|
|
228
|
+
scriptPath: string,
|
|
229
|
+
payload: unknown,
|
|
230
|
+
timeoutMs: number
|
|
231
|
+
): Promise<unknown> {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const child = spawn(scriptPath, [], {
|
|
234
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
235
|
+
env: { ...process.env },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
let stdout = '';
|
|
239
|
+
let stderr = '';
|
|
240
|
+
|
|
241
|
+
child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
|
|
242
|
+
child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
|
|
243
|
+
|
|
244
|
+
const timer = setTimeout(() => {
|
|
245
|
+
child.kill('SIGTERM');
|
|
246
|
+
reject(new Error(`Handler timeout after ${timeoutMs}ms`));
|
|
247
|
+
}, timeoutMs);
|
|
248
|
+
|
|
249
|
+
child.on('close', (code) => {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
if (code !== 0) {
|
|
252
|
+
reject(new Error(`Handler exited with code ${code}: ${stderr.trim()}`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
resolve(JSON.parse(stdout.trim()));
|
|
257
|
+
} catch {
|
|
258
|
+
resolve({ result: stdout.trim() });
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
child.on('error', (err) => {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
reject(err);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Write payload to stdin
|
|
268
|
+
child.stdin.write(JSON.stringify(payload));
|
|
269
|
+
child.stdin.end();
|
|
270
|
+
});
|
|
271
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -6,10 +6,9 @@ export function registerStatusCommand(program: Command): void {
|
|
|
6
6
|
program
|
|
7
7
|
.command('status')
|
|
8
8
|
.description('Show current status')
|
|
9
|
-
.
|
|
9
|
+
.option('--format <fmt>', 'Output format: text|json', 'text')
|
|
10
|
+
.action(async (options) => {
|
|
10
11
|
try {
|
|
11
|
-
printHeader('Clawiverse Status');
|
|
12
|
-
|
|
13
12
|
const identity = getIdentity();
|
|
14
13
|
const card = getAgentCard();
|
|
15
14
|
const bootstrapPeers = getBootstrapPeers();
|
|
@@ -19,6 +18,22 @@ export function registerStatusCommand(program: Command): void {
|
|
|
19
18
|
process.exit(1);
|
|
20
19
|
}
|
|
21
20
|
|
|
21
|
+
if (options.format === 'json') {
|
|
22
|
+
console.log(JSON.stringify({
|
|
23
|
+
identity: {
|
|
24
|
+
did: identity.did,
|
|
25
|
+
publicKey: identity.publicKey,
|
|
26
|
+
},
|
|
27
|
+
agentCard: card || null,
|
|
28
|
+
network: {
|
|
29
|
+
bootstrapPeers,
|
|
30
|
+
},
|
|
31
|
+
}, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
printHeader('Clawiverse Status');
|
|
36
|
+
|
|
22
37
|
printSection('Identity');
|
|
23
38
|
printKeyValue('DID', identity.did);
|
|
24
39
|
printKeyValue('Public Key', identity.publicKey.substring(0, 16) + '...');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop Command - CVP-0010 §2.5
|
|
3
|
+
*
|
|
4
|
+
* Cleaner alternative to `hw1 daemon stop`
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { DaemonClient } from '../daemon/client.js';
|
|
9
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
10
|
+
import { success, info } from '../ui.js';
|
|
11
|
+
|
|
12
|
+
const PID_FILE = '/tmp/clawiverse.pid';
|
|
13
|
+
const SOCKET_PATH = '/tmp/clawiverse.sock';
|
|
14
|
+
|
|
15
|
+
export function registerStopCommand(program: Command): void {
|
|
16
|
+
program
|
|
17
|
+
.command('stop')
|
|
18
|
+
.description('Stop the Highway 1 daemon')
|
|
19
|
+
.action(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const client = new DaemonClient(SOCKET_PATH);
|
|
22
|
+
|
|
23
|
+
if (!(await client.isDaemonRunning())) {
|
|
24
|
+
info('Daemon not running');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await client.send('shutdown', {});
|
|
29
|
+
success('Daemon stopped');
|
|
30
|
+
|
|
31
|
+
// Clean up PID file
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(PID_FILE)) {
|
|
34
|
+
unlinkSync(PID_FILE);
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
|
|
38
|
+
// Clean up socket file
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync(SOCKET_PATH)) {
|
|
41
|
+
unlinkSync(SOCKET_PATH);
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`Failed to stop daemon: ${(err as Error).message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
package/src/commands/trust.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { getConfig, getIdentity } from '../config.js';
|
|
7
|
-
import { createLogger, createTrustSystem } from '@highway1/core';
|
|
7
|
+
import { createLogger, createTrustSystem, extractPublicKey } from '@highway1/core';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
|
|
@@ -24,7 +24,7 @@ export function createTrustCommand(): Command {
|
|
|
24
24
|
const dataDir = join(homedir(), '.clawiverse');
|
|
25
25
|
const trustSystem = createTrustSystem({
|
|
26
26
|
dbPath: `${dataDir}/trust`,
|
|
27
|
-
getPublicKey: async () =>
|
|
27
|
+
getPublicKey: async (did: string) => extractPublicKey(did), // CVP-0010 §4.1 fix
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
await trustSystem.start();
|
|
@@ -71,7 +71,7 @@ export function createTrustCommand(): Command {
|
|
|
71
71
|
const dataDir = join(homedir(), '.clawiverse');
|
|
72
72
|
const trustSystem = createTrustSystem({
|
|
73
73
|
dbPath: `${dataDir}/trust`,
|
|
74
|
-
getPublicKey: async () =>
|
|
74
|
+
getPublicKey: async (did: string) => extractPublicKey(did), // CVP-0010 §4.1 fix
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
await trustSystem.start();
|
|
@@ -126,7 +126,7 @@ export function createTrustCommand(): Command {
|
|
|
126
126
|
const dataDir = join(homedir(), '.clawiverse');
|
|
127
127
|
const trustSystem = createTrustSystem({
|
|
128
128
|
dbPath: `${dataDir}/trust`,
|
|
129
|
-
getPublicKey: async () =>
|
|
129
|
+
getPublicKey: async (did: string) => extractPublicKey(did), // CVP-0010 §4.1 fix
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
await trustSystem.start();
|
|
@@ -170,7 +170,7 @@ export function createTrustCommand(): Command {
|
|
|
170
170
|
const dataDir = join(homedir(), '.clawiverse');
|
|
171
171
|
const trustSystem = createTrustSystem({
|
|
172
172
|
dbPath: `${dataDir}/trust`,
|
|
173
|
-
getPublicKey: async () =>
|
|
173
|
+
getPublicKey: async (did: string) => extractPublicKey(did), // CVP-0010 §4.1 fix
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
await trustSystem.start();
|
|
@@ -211,5 +211,144 @@ export function createTrustCommand(): Command {
|
|
|
211
211
|
}
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
// Block an agent (via daemon defense layer)
|
|
215
|
+
trust
|
|
216
|
+
.command('block <did>')
|
|
217
|
+
.description('Block an agent from sending you messages')
|
|
218
|
+
.option('-r, --reason <reason>', 'Reason for blocking', 'Blocked by user')
|
|
219
|
+
.action(async (did: string, options: { reason: string }) => {
|
|
220
|
+
try {
|
|
221
|
+
const { DaemonClient } = await import('../daemon/client.js');
|
|
222
|
+
const client = new DaemonClient();
|
|
223
|
+
if (await client.isDaemonRunning()) {
|
|
224
|
+
await client.send('block', { did, reason: options.reason });
|
|
225
|
+
console.log(`Blocked ${did}`);
|
|
226
|
+
} else {
|
|
227
|
+
// Daemon not running — write directly to storage
|
|
228
|
+
const { MessageStorage } = await import('@highway1/core');
|
|
229
|
+
const storage = new MessageStorage(join(homedir(), '.clawiverse', 'inbox'));
|
|
230
|
+
await storage.open();
|
|
231
|
+
await storage.putBlock({ did, reason: options.reason, blockedAt: Date.now(), blockedBy: 'local' });
|
|
232
|
+
await storage.close();
|
|
233
|
+
console.log(`Blocked ${did}`);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.error('Failed to block agent', error);
|
|
237
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Unblock an agent
|
|
243
|
+
trust
|
|
244
|
+
.command('unblock <did>')
|
|
245
|
+
.description('Unblock a previously blocked agent')
|
|
246
|
+
.action(async (did: string) => {
|
|
247
|
+
try {
|
|
248
|
+
const { DaemonClient } = await import('../daemon/client.js');
|
|
249
|
+
const client = new DaemonClient();
|
|
250
|
+
if (await client.isDaemonRunning()) {
|
|
251
|
+
await client.send('unblock', { did });
|
|
252
|
+
} else {
|
|
253
|
+
const { MessageStorage } = await import('@highway1/core');
|
|
254
|
+
const storage = new MessageStorage(join(homedir(), '.clawiverse', 'inbox'));
|
|
255
|
+
await storage.open();
|
|
256
|
+
await storage.deleteBlock(did);
|
|
257
|
+
await storage.close();
|
|
258
|
+
}
|
|
259
|
+
console.log(`Unblocked ${did}`);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logger.error('Failed to unblock agent', error);
|
|
262
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// List blocked agents
|
|
268
|
+
trust
|
|
269
|
+
.command('list-blocked')
|
|
270
|
+
.description('List all blocked agents')
|
|
271
|
+
.action(async () => {
|
|
272
|
+
try {
|
|
273
|
+
const { MessageStorage } = await import('@highway1/core');
|
|
274
|
+
const storage = new MessageStorage(join(homedir(), '.clawiverse', 'inbox'));
|
|
275
|
+
await storage.open();
|
|
276
|
+
const blocked = await storage.listBlocked();
|
|
277
|
+
await storage.close();
|
|
278
|
+
|
|
279
|
+
if (blocked.length === 0) {
|
|
280
|
+
console.log('No blocked agents.');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
console.log(`\nBlocked agents (${blocked.length}):\n`);
|
|
284
|
+
for (const entry of blocked) {
|
|
285
|
+
console.log(` ${entry.did}`);
|
|
286
|
+
console.log(` Reason: ${entry.reason}`);
|
|
287
|
+
console.log(` Blocked: ${new Date(entry.blockedAt).toLocaleString()}`);
|
|
288
|
+
}
|
|
289
|
+
console.log();
|
|
290
|
+
} catch (error) {
|
|
291
|
+
logger.error('Failed to list blocked agents', error);
|
|
292
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Add to allowlist
|
|
298
|
+
trust
|
|
299
|
+
.command('allow <did>')
|
|
300
|
+
.description('Add an agent to your allowlist (bypasses all defense checks)')
|
|
301
|
+
.option('-n, --note <note>', 'Note about this agent')
|
|
302
|
+
.action(async (did: string, options: { note?: string }) => {
|
|
303
|
+
try {
|
|
304
|
+
const { DaemonClient } = await import('../daemon/client.js');
|
|
305
|
+
const client = new DaemonClient();
|
|
306
|
+
if (await client.isDaemonRunning()) {
|
|
307
|
+
await client.send('allowlist', { action: 'add', did, note: options.note });
|
|
308
|
+
} else {
|
|
309
|
+
const { MessageStorage } = await import('@highway1/core');
|
|
310
|
+
const storage = new MessageStorage(join(homedir(), '.clawiverse', 'inbox'));
|
|
311
|
+
await storage.open();
|
|
312
|
+
await storage.putAllow({ did, addedAt: Date.now(), note: options.note });
|
|
313
|
+
await storage.close();
|
|
314
|
+
}
|
|
315
|
+
console.log(`Added ${did} to allowlist`);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger.error('Failed to allowlist agent', error);
|
|
318
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// List allowlisted agents
|
|
324
|
+
trust
|
|
325
|
+
.command('list-allowed')
|
|
326
|
+
.description('List all allowlisted agents')
|
|
327
|
+
.action(async () => {
|
|
328
|
+
try {
|
|
329
|
+
const { MessageStorage } = await import('@highway1/core');
|
|
330
|
+
const storage = new MessageStorage(join(homedir(), '.clawiverse', 'inbox'));
|
|
331
|
+
await storage.open();
|
|
332
|
+
const allowed = await storage.listAllowed();
|
|
333
|
+
await storage.close();
|
|
334
|
+
|
|
335
|
+
if (allowed.length === 0) {
|
|
336
|
+
console.log('No allowlisted agents.');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
console.log(`\nAllowlisted agents (${allowed.length}):\n`);
|
|
340
|
+
for (const entry of allowed) {
|
|
341
|
+
console.log(` ${entry.did}`);
|
|
342
|
+
if (entry.note) console.log(` Note: ${entry.note}`);
|
|
343
|
+
console.log(` Added: ${new Date(entry.addedAt).toLocaleString()}`);
|
|
344
|
+
}
|
|
345
|
+
console.log();
|
|
346
|
+
} catch (error) {
|
|
347
|
+
logger.error('Failed to list allowed agents', error);
|
|
348
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
214
353
|
return trust;
|
|
215
354
|
}
|
package/src/daemon/client.ts
CHANGED
|
@@ -16,25 +16,47 @@ export class DaemonClient {
|
|
|
16
16
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
17
17
|
|
|
18
18
|
let responseReceived = false;
|
|
19
|
+
let buffer = ''; // CVP-0010 §1.2: Buffer for NDJSON framing
|
|
19
20
|
|
|
20
21
|
socket.on('connect', () => {
|
|
21
22
|
const request = { id: requestId, command, params };
|
|
22
23
|
logger.debug('Sending request', { command, id: requestId });
|
|
23
|
-
|
|
24
|
+
// CVP-0010 §1.2: Send with newline delimiter
|
|
25
|
+
socket.write(JSON.stringify(request) + '\n');
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
socket.on('data', (data) => {
|
|
27
29
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// CVP-0010 §1.2: Accumulate data and split by newlines
|
|
31
|
+
buffer += data.toString();
|
|
32
|
+
const lines = buffer.split('\n');
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
// Keep incomplete line in buffer
|
|
35
|
+
buffer = lines.pop() || '';
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// Process complete lines
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (!line.trim()) continue;
|
|
40
|
+
|
|
41
|
+
const response = JSON.parse(line);
|
|
42
|
+
|
|
43
|
+
// Match response by ID
|
|
44
|
+
if (response.id !== requestId) {
|
|
45
|
+
logger.debug('Ignoring unmatched response', { responseId: response.id, requestId });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
responseReceived = true;
|
|
50
|
+
socket.end();
|
|
51
|
+
|
|
52
|
+
logger.debug('Received response', { success: response.success, id: response.id });
|
|
53
|
+
|
|
54
|
+
if (response.success) {
|
|
55
|
+
resolve(response.data);
|
|
56
|
+
} else {
|
|
57
|
+
reject(new Error(response.error));
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
38
60
|
}
|
|
39
61
|
} catch (error) {
|
|
40
62
|
reject(new Error(`Failed to parse response: ${(error as Error).message}`));
|