@sonde/agent 0.0.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.
Files changed (149) hide show
  1. package/dist/cli/packs.d.ts +23 -0
  2. package/dist/cli/packs.d.ts.map +1 -0
  3. package/dist/cli/packs.js +172 -0
  4. package/dist/cli/packs.js.map +1 -0
  5. package/dist/cli/packs.test.d.ts +2 -0
  6. package/dist/cli/packs.test.d.ts.map +1 -0
  7. package/dist/cli/packs.test.js +171 -0
  8. package/dist/cli/packs.test.js.map +1 -0
  9. package/dist/config.d.ts +18 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +38 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +191 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/runtime/attestation.d.ts +9 -0
  18. package/dist/runtime/attestation.d.ts.map +1 -0
  19. package/dist/runtime/attestation.js +32 -0
  20. package/dist/runtime/attestation.js.map +1 -0
  21. package/dist/runtime/attestation.test.d.ts +2 -0
  22. package/dist/runtime/attestation.test.d.ts.map +1 -0
  23. package/dist/runtime/attestation.test.js +59 -0
  24. package/dist/runtime/attestation.test.js.map +1 -0
  25. package/dist/runtime/audit.d.ts +19 -0
  26. package/dist/runtime/audit.d.ts.map +1 -0
  27. package/dist/runtime/audit.js +52 -0
  28. package/dist/runtime/audit.js.map +1 -0
  29. package/dist/runtime/audit.test.d.ts +2 -0
  30. package/dist/runtime/audit.test.d.ts.map +1 -0
  31. package/dist/runtime/audit.test.js +53 -0
  32. package/dist/runtime/audit.test.js.map +1 -0
  33. package/dist/runtime/connection.d.ts +55 -0
  34. package/dist/runtime/connection.d.ts.map +1 -0
  35. package/dist/runtime/connection.js +325 -0
  36. package/dist/runtime/connection.js.map +1 -0
  37. package/dist/runtime/connection.test.d.ts +2 -0
  38. package/dist/runtime/connection.test.d.ts.map +1 -0
  39. package/dist/runtime/connection.test.js +221 -0
  40. package/dist/runtime/connection.test.js.map +1 -0
  41. package/dist/runtime/executor.d.ts +21 -0
  42. package/dist/runtime/executor.d.ts.map +1 -0
  43. package/dist/runtime/executor.js +89 -0
  44. package/dist/runtime/executor.js.map +1 -0
  45. package/dist/runtime/executor.test.d.ts +2 -0
  46. package/dist/runtime/executor.test.d.ts.map +1 -0
  47. package/dist/runtime/executor.test.js +88 -0
  48. package/dist/runtime/executor.test.js.map +1 -0
  49. package/dist/runtime/privilege.d.ts +9 -0
  50. package/dist/runtime/privilege.d.ts.map +1 -0
  51. package/dist/runtime/privilege.js +35 -0
  52. package/dist/runtime/privilege.js.map +1 -0
  53. package/dist/runtime/privilege.test.d.ts +2 -0
  54. package/dist/runtime/privilege.test.d.ts.map +1 -0
  55. package/dist/runtime/privilege.test.js +22 -0
  56. package/dist/runtime/privilege.test.js.map +1 -0
  57. package/dist/runtime/scrubber.d.ts +17 -0
  58. package/dist/runtime/scrubber.d.ts.map +1 -0
  59. package/dist/runtime/scrubber.js +84 -0
  60. package/dist/runtime/scrubber.js.map +1 -0
  61. package/dist/runtime/scrubber.test.d.ts +2 -0
  62. package/dist/runtime/scrubber.test.d.ts.map +1 -0
  63. package/dist/runtime/scrubber.test.js +72 -0
  64. package/dist/runtime/scrubber.test.js.map +1 -0
  65. package/dist/system/scanner.d.ts +32 -0
  66. package/dist/system/scanner.d.ts.map +1 -0
  67. package/dist/system/scanner.js +90 -0
  68. package/dist/system/scanner.js.map +1 -0
  69. package/dist/system/scanner.test.d.ts +2 -0
  70. package/dist/system/scanner.test.d.ts.map +1 -0
  71. package/dist/system/scanner.test.js +121 -0
  72. package/dist/system/scanner.test.js.map +1 -0
  73. package/dist/tui/installer/InstallerApp.d.ts +11 -0
  74. package/dist/tui/installer/InstallerApp.d.ts.map +1 -0
  75. package/dist/tui/installer/InstallerApp.js +32 -0
  76. package/dist/tui/installer/InstallerApp.js.map +1 -0
  77. package/dist/tui/installer/StepComplete.d.ts +9 -0
  78. package/dist/tui/installer/StepComplete.d.ts.map +1 -0
  79. package/dist/tui/installer/StepComplete.js +46 -0
  80. package/dist/tui/installer/StepComplete.js.map +1 -0
  81. package/dist/tui/installer/StepHub.d.ts +8 -0
  82. package/dist/tui/installer/StepHub.d.ts.map +1 -0
  83. package/dist/tui/installer/StepHub.js +65 -0
  84. package/dist/tui/installer/StepHub.js.map +1 -0
  85. package/dist/tui/installer/StepPacks.d.ts +9 -0
  86. package/dist/tui/installer/StepPacks.d.ts.map +1 -0
  87. package/dist/tui/installer/StepPacks.js +35 -0
  88. package/dist/tui/installer/StepPacks.js.map +1 -0
  89. package/dist/tui/installer/StepPermissions.d.ts +9 -0
  90. package/dist/tui/installer/StepPermissions.d.ts.map +1 -0
  91. package/dist/tui/installer/StepPermissions.js +39 -0
  92. package/dist/tui/installer/StepPermissions.js.map +1 -0
  93. package/dist/tui/installer/StepScan.d.ts +7 -0
  94. package/dist/tui/installer/StepScan.d.ts.map +1 -0
  95. package/dist/tui/installer/StepScan.js +38 -0
  96. package/dist/tui/installer/StepScan.js.map +1 -0
  97. package/dist/tui/manager/ActivityLog.d.ts +7 -0
  98. package/dist/tui/manager/ActivityLog.d.ts.map +1 -0
  99. package/dist/tui/manager/ActivityLog.js +25 -0
  100. package/dist/tui/manager/ActivityLog.js.map +1 -0
  101. package/dist/tui/manager/AuditView.d.ts +7 -0
  102. package/dist/tui/manager/AuditView.d.ts.map +1 -0
  103. package/dist/tui/manager/AuditView.js +32 -0
  104. package/dist/tui/manager/AuditView.js.map +1 -0
  105. package/dist/tui/manager/ManagerApp.d.ts +20 -0
  106. package/dist/tui/manager/ManagerApp.d.ts.map +1 -0
  107. package/dist/tui/manager/ManagerApp.js +79 -0
  108. package/dist/tui/manager/ManagerApp.js.map +1 -0
  109. package/dist/tui/manager/PackManager.d.ts +7 -0
  110. package/dist/tui/manager/PackManager.d.ts.map +1 -0
  111. package/dist/tui/manager/PackManager.js +22 -0
  112. package/dist/tui/manager/PackManager.js.map +1 -0
  113. package/dist/tui/manager/StatusView.d.ts +15 -0
  114. package/dist/tui/manager/StatusView.d.ts.map +1 -0
  115. package/dist/tui/manager/StatusView.js +10 -0
  116. package/dist/tui/manager/StatusView.js.map +1 -0
  117. package/package.json +45 -0
  118. package/scripts/install.sh +11 -0
  119. package/src/cli/packs.test.ts +213 -0
  120. package/src/cli/packs.ts +214 -0
  121. package/src/config.ts +62 -0
  122. package/src/index.ts +218 -0
  123. package/src/runtime/attestation.test.ts +69 -0
  124. package/src/runtime/attestation.ts +36 -0
  125. package/src/runtime/audit.test.ts +64 -0
  126. package/src/runtime/audit.ts +70 -0
  127. package/src/runtime/connection.test.ts +303 -0
  128. package/src/runtime/connection.ts +389 -0
  129. package/src/runtime/executor.test.ts +112 -0
  130. package/src/runtime/executor.ts +107 -0
  131. package/src/runtime/privilege.test.ts +25 -0
  132. package/src/runtime/privilege.ts +36 -0
  133. package/src/runtime/scrubber.test.ts +84 -0
  134. package/src/runtime/scrubber.ts +96 -0
  135. package/src/system/scanner.test.ts +154 -0
  136. package/src/system/scanner.ts +133 -0
  137. package/src/tui/installer/InstallerApp.tsx +86 -0
  138. package/src/tui/installer/StepComplete.tsx +94 -0
  139. package/src/tui/installer/StepHub.tsx +111 -0
  140. package/src/tui/installer/StepPacks.tsx +73 -0
  141. package/src/tui/installer/StepPermissions.tsx +104 -0
  142. package/src/tui/installer/StepScan.tsx +82 -0
  143. package/src/tui/manager/ActivityLog.tsx +57 -0
  144. package/src/tui/manager/AuditView.tsx +73 -0
  145. package/src/tui/manager/ManagerApp.tsx +157 -0
  146. package/src/tui/manager/PackManager.tsx +71 -0
  147. package/src/tui/manager/StatusView.tsx +103 -0
  148. package/tsconfig.json +13 -0
  149. package/vitest.config.ts +8 -0
@@ -0,0 +1,70 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export interface AgentAuditEntry {
4
+ timestamp: string;
5
+ probe: string;
6
+ status: string;
7
+ durationMs: number;
8
+ prevHash: string;
9
+ }
10
+
11
+ const DEFAULT_CAPACITY = 1000;
12
+
13
+ export class AgentAuditLog {
14
+ private entries: AgentAuditEntry[] = [];
15
+ private capacity: number;
16
+
17
+ constructor(capacity = DEFAULT_CAPACITY) {
18
+ this.capacity = capacity;
19
+ }
20
+
21
+ log(probe: string, status: string, durationMs: number): void {
22
+ const prevHash =
23
+ this.entries.length > 0
24
+ ? crypto
25
+ .createHash('sha256')
26
+ .update(JSON.stringify(this.entries[this.entries.length - 1]))
27
+ .digest('hex')
28
+ : '';
29
+
30
+ const entry: AgentAuditEntry = {
31
+ timestamp: new Date().toISOString(),
32
+ probe,
33
+ status,
34
+ durationMs,
35
+ prevHash,
36
+ };
37
+
38
+ this.entries.push(entry);
39
+
40
+ // Ring buffer: drop oldest when over capacity
41
+ if (this.entries.length > this.capacity) {
42
+ this.entries.shift();
43
+ }
44
+ }
45
+
46
+ getRecent(n?: number): AgentAuditEntry[] {
47
+ if (n === undefined) return [...this.entries];
48
+ return this.entries.slice(-n);
49
+ }
50
+
51
+ verifyChain(): { valid: boolean; brokenAt?: number } {
52
+ if (this.entries.length === 0) return { valid: true };
53
+
54
+ const first = this.entries[0];
55
+ if (first && first.prevHash !== '') {
56
+ return { valid: false, brokenAt: 0 };
57
+ }
58
+
59
+ for (let i = 1; i < this.entries.length; i++) {
60
+ const prev = this.entries[i - 1] as AgentAuditEntry;
61
+ const curr = this.entries[i] as AgentAuditEntry;
62
+ const expectedHash = crypto.createHash('sha256').update(JSON.stringify(prev)).digest('hex');
63
+ if (curr.prevHash !== expectedHash) {
64
+ return { valid: false, brokenAt: i };
65
+ }
66
+ }
67
+
68
+ return { valid: true };
69
+ }
70
+ }
@@ -0,0 +1,303 @@
1
+ import type { ProbeRequest } from '@sonde/shared';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import type { AgentConfig } from '../config.js';
4
+ import { AgentConnection, enrollWithHub } from './connection.js';
5
+ import { ProbeExecutor } from './executor.js';
6
+
7
+ // Mock WebSocket
8
+ const mockWsInstances: Array<{
9
+ url: string;
10
+ handlers: Record<string, (...args: unknown[]) => void>;
11
+ send: ReturnType<typeof vi.fn>;
12
+ close: ReturnType<typeof vi.fn>;
13
+ readyState: number;
14
+ }> = [];
15
+
16
+ vi.mock('ws', () => {
17
+ const OPEN = 1;
18
+ const CLOSED = 3;
19
+
20
+ class MockWebSocket {
21
+ url: string;
22
+ readyState: number;
23
+ send = vi.fn();
24
+ close = vi.fn();
25
+ private handlers: Record<string, (...args: unknown[]) => void> = {};
26
+
27
+ constructor(url: string, _opts?: unknown) {
28
+ this.url = url;
29
+ this.readyState = OPEN;
30
+ mockWsInstances.push({
31
+ url,
32
+ handlers: this.handlers,
33
+ send: this.send,
34
+ close: this.close,
35
+ readyState: this.readyState,
36
+ });
37
+ }
38
+
39
+ on(event: string, handler: (...args: unknown[]) => void) {
40
+ this.handlers[event] = handler;
41
+ }
42
+
43
+ // Helper: simulate receiving events from tests
44
+ _emit(event: string, ...args: unknown[]) {
45
+ this.handlers[event]?.(...args);
46
+ }
47
+ }
48
+
49
+ // Attach static constants
50
+ (MockWebSocket as unknown as Record<string, number>).OPEN = OPEN;
51
+ (MockWebSocket as unknown as Record<string, number>).CLOSED = CLOSED;
52
+
53
+ return { default: MockWebSocket, WebSocket: MockWebSocket };
54
+ });
55
+
56
+ function createConfig(): AgentConfig {
57
+ return {
58
+ hubUrl: 'http://localhost:3000',
59
+ apiKey: 'test-key',
60
+ agentName: 'test-agent',
61
+ };
62
+ }
63
+
64
+ function createExecutor(): ProbeExecutor {
65
+ return new ProbeExecutor(new Map());
66
+ }
67
+
68
+ describe('AgentConnection', () => {
69
+ beforeEach(() => {
70
+ mockWsInstances.length = 0;
71
+ vi.useFakeTimers();
72
+ });
73
+
74
+ afterEach(() => {
75
+ vi.useRealTimers();
76
+ });
77
+
78
+ it('connects to the correct WebSocket URL', () => {
79
+ const conn = new AgentConnection(createConfig(), createExecutor());
80
+ conn.start();
81
+
82
+ expect(mockWsInstances).toHaveLength(1);
83
+ expect(mockWsInstances[0]?.url).toBe('ws://localhost:3000/ws/agent');
84
+
85
+ conn.stop();
86
+ });
87
+
88
+ it('converts https hub URL to wss', () => {
89
+ const config = createConfig();
90
+ config.hubUrl = 'https://hub.example.com';
91
+
92
+ const conn = new AgentConnection(config, createExecutor());
93
+ conn.start();
94
+
95
+ expect(mockWsInstances[0]?.url).toBe('wss://hub.example.com/ws/agent');
96
+
97
+ conn.stop();
98
+ });
99
+
100
+ it('sends register message on open', () => {
101
+ const conn = new AgentConnection(createConfig(), createExecutor());
102
+ conn.start();
103
+
104
+ // Simulate WebSocket open
105
+ const ws = mockWsInstances[0];
106
+ ws?.handlers.open?.();
107
+
108
+ expect(ws?.send).toHaveBeenCalledOnce();
109
+ const sent = JSON.parse(ws?.send.mock.calls[0]?.[0] as string);
110
+ expect(sent.type).toBe('agent.register');
111
+ expect(sent.payload.name).toBe('test-agent');
112
+
113
+ conn.stop();
114
+ });
115
+
116
+ it('stores agentId from hub.ack', () => {
117
+ const onRegistered = vi.fn();
118
+ const conn = new AgentConnection(createConfig(), createExecutor(), { onRegistered });
119
+ conn.start();
120
+
121
+ const ws = mockWsInstances[0];
122
+ ws?.handlers.open?.();
123
+
124
+ // Simulate hub.ack
125
+ ws?.handlers.message?.(
126
+ JSON.stringify({
127
+ id: '00000000-0000-0000-0000-000000000001',
128
+ type: 'hub.ack',
129
+ timestamp: new Date().toISOString(),
130
+ agentId: 'assigned-id',
131
+ signature: '',
132
+ payload: { agentId: 'assigned-id' },
133
+ }),
134
+ );
135
+
136
+ expect(onRegistered).toHaveBeenCalledWith('assigned-id');
137
+ expect(conn.getAgentId()).toBe('assigned-id');
138
+
139
+ conn.stop();
140
+ });
141
+
142
+ it('sends heartbeats on interval', () => {
143
+ const conn = new AgentConnection(createConfig(), createExecutor());
144
+ conn.start();
145
+
146
+ const ws = mockWsInstances[0];
147
+ ws?.handlers.open?.();
148
+
149
+ // Simulate hub.ack to set agentId
150
+ ws?.handlers.message?.(
151
+ JSON.stringify({
152
+ id: '00000000-0000-0000-0000-000000000001',
153
+ type: 'hub.ack',
154
+ timestamp: new Date().toISOString(),
155
+ agentId: 'agent-1',
156
+ signature: '',
157
+ payload: { agentId: 'agent-1' },
158
+ }),
159
+ );
160
+
161
+ // Register sends one message, ack handler sends another (onConnected)
162
+ const callsBeforeHeartbeat = ws?.send.mock.calls.length ?? 0;
163
+
164
+ // Advance time by 30 seconds (HEARTBEAT_INTERVAL_MS)
165
+ vi.advanceTimersByTime(30_000);
166
+
167
+ const heartbeatCalls = (ws?.send.mock.calls.length ?? 0) - callsBeforeHeartbeat;
168
+ expect(heartbeatCalls).toBe(1);
169
+
170
+ const lastCall = ws?.send.mock.calls.at(-1)?.[0] as string;
171
+ const msg = JSON.parse(lastCall);
172
+ expect(msg.type).toBe('agent.heartbeat');
173
+ expect(msg.agentId).toBe('agent-1');
174
+
175
+ conn.stop();
176
+ });
177
+
178
+ it('schedules reconnect with exponential backoff on close', () => {
179
+ const onDisconnected = vi.fn();
180
+ const conn = new AgentConnection(createConfig(), createExecutor(), { onDisconnected });
181
+ conn.start();
182
+
183
+ const ws = mockWsInstances[0];
184
+
185
+ // First disconnect
186
+ ws?.handlers.close?.();
187
+ expect(onDisconnected).toHaveBeenCalledOnce();
188
+ expect(mockWsInstances).toHaveLength(1); // Not reconnected yet
189
+
190
+ // Advance past first reconnect delay (1s)
191
+ vi.advanceTimersByTime(1_000);
192
+ expect(mockWsInstances).toHaveLength(2); // Reconnected
193
+
194
+ // Second disconnect
195
+ mockWsInstances[1]?.handlers.close?.();
196
+
197
+ // Advance 1s — should NOT reconnect yet (backoff is 2s)
198
+ vi.advanceTimersByTime(1_000);
199
+ expect(mockWsInstances).toHaveLength(2);
200
+
201
+ // Advance another 1s (total 2s) — should reconnect
202
+ vi.advanceTimersByTime(1_000);
203
+ expect(mockWsInstances).toHaveLength(3);
204
+
205
+ conn.stop();
206
+ });
207
+
208
+ it('does not reconnect after stop()', () => {
209
+ const conn = new AgentConnection(createConfig(), createExecutor());
210
+ conn.start();
211
+
212
+ conn.stop();
213
+
214
+ const ws = mockWsInstances[0];
215
+ ws?.handlers.close?.();
216
+
217
+ vi.advanceTimersByTime(60_000);
218
+ expect(mockWsInstances).toHaveLength(1); // No reconnect
219
+ });
220
+ });
221
+
222
+ describe('enrollWithHub', () => {
223
+ beforeEach(() => {
224
+ mockWsInstances.length = 0;
225
+ vi.useFakeTimers();
226
+ });
227
+
228
+ afterEach(() => {
229
+ vi.useRealTimers();
230
+ });
231
+
232
+ it('connects to correct URL and sends register', () => {
233
+ const promise = enrollWithHub(createConfig(), createExecutor());
234
+
235
+ expect(mockWsInstances).toHaveLength(1);
236
+ expect(mockWsInstances[0]?.url).toBe('ws://localhost:3000/ws/agent');
237
+
238
+ const ws = mockWsInstances[0];
239
+ ws?.handlers.open?.();
240
+
241
+ expect(ws?.send).toHaveBeenCalledOnce();
242
+ const sent = JSON.parse(ws?.send.mock.calls[0]?.[0] as string);
243
+ expect(sent.type).toBe('agent.register');
244
+ expect(sent.payload.name).toBe('test-agent');
245
+
246
+ // Send ack to resolve and avoid dangling promise
247
+ ws?.handlers.message?.(
248
+ JSON.stringify({
249
+ id: '00000000-0000-0000-0000-000000000001',
250
+ type: 'hub.ack',
251
+ timestamp: new Date().toISOString(),
252
+ agentId: 'enrolled-id',
253
+ signature: '',
254
+ payload: { agentId: 'enrolled-id' },
255
+ }),
256
+ );
257
+
258
+ return promise;
259
+ });
260
+
261
+ it('returns agentId from hub.ack', async () => {
262
+ const promise = enrollWithHub(createConfig(), createExecutor());
263
+
264
+ const ws = mockWsInstances[0];
265
+ ws?.handlers.open?.();
266
+
267
+ ws?.handlers.message?.(
268
+ JSON.stringify({
269
+ id: '00000000-0000-0000-0000-000000000001',
270
+ type: 'hub.ack',
271
+ timestamp: new Date().toISOString(),
272
+ agentId: 'enrolled-id',
273
+ signature: '',
274
+ payload: { agentId: 'enrolled-id' },
275
+ }),
276
+ );
277
+
278
+ const result = await promise;
279
+ expect(result.agentId).toBe('enrolled-id');
280
+ expect(result.certIssued).toBe(false);
281
+ });
282
+
283
+ it('rejects on timeout when no ack received', async () => {
284
+ const promise = enrollWithHub(createConfig(), createExecutor());
285
+
286
+ const ws = mockWsInstances[0];
287
+ ws?.handlers.open?.();
288
+
289
+ // Advance past the 10s timeout
290
+ vi.advanceTimersByTime(10_000);
291
+
292
+ await expect(promise).rejects.toThrow('Enrollment timed out');
293
+ });
294
+
295
+ it('rejects on WebSocket error', async () => {
296
+ const promise = enrollWithHub(createConfig(), createExecutor());
297
+
298
+ const ws = mockWsInstances[0];
299
+ ws?.handlers.error?.(new Error('Connection refused'));
300
+
301
+ await expect(promise).rejects.toThrow('Connection refused');
302
+ });
303
+ });