@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,214 @@
1
+ import type { Pack } from '@sonde/packs';
2
+ import { packRegistry } from '@sonde/packs';
3
+ import type { PackManifest } from '@sonde/shared';
4
+ import {
5
+ type PermissionCheck,
6
+ type ScanResult,
7
+ type SystemChecker,
8
+ checkPackPermissions,
9
+ createSystemChecker,
10
+ scanForSoftware,
11
+ } from '../system/scanner.js';
12
+
13
+ export interface PackState {
14
+ /** Packs currently loaded/active on this agent */
15
+ installed: Map<string, Pack>;
16
+ /** All available packs from the registry */
17
+ available: ReadonlyMap<string, Pack>;
18
+ }
19
+
20
+ export interface PackCommandDeps {
21
+ state: PackState;
22
+ checker: SystemChecker;
23
+ getUserGroups: () => string[];
24
+ log: (msg: string) => void;
25
+ }
26
+
27
+ function createDefaultDeps(): PackCommandDeps {
28
+ return {
29
+ state: {
30
+ installed: new Map(packRegistry),
31
+ available: packRegistry,
32
+ },
33
+ checker: createSystemChecker(),
34
+ getUserGroups: getProcessUserGroups,
35
+ log: console.log,
36
+ };
37
+ }
38
+
39
+ function getProcessUserGroups(): string[] {
40
+ // On Unix, process.getgroups() returns numeric GIDs
41
+ // For MVP, we return an empty array — real implementation would
42
+ // map GIDs to group names via /etc/group
43
+ try {
44
+ if (typeof process.getgroups === 'function') {
45
+ return process.getgroups().map(String);
46
+ }
47
+ } catch {
48
+ // Not available on all platforms
49
+ }
50
+ return [];
51
+ }
52
+
53
+ export function cmdPacksList(deps?: PackCommandDeps): void {
54
+ const { state, log } = deps ?? createDefaultDeps();
55
+
56
+ if (state.installed.size === 0) {
57
+ log('No packs installed.');
58
+ return;
59
+ }
60
+
61
+ log('Installed packs:');
62
+ log('');
63
+ for (const [name, pack] of state.installed) {
64
+ const probeCount = pack.manifest.probes.length;
65
+ log(` ${name} v${pack.manifest.version} (${probeCount} probes)`);
66
+ log(` ${pack.manifest.description}`);
67
+ for (const probe of pack.manifest.probes) {
68
+ log(` - ${name}.${probe.name}: ${probe.description}`);
69
+ }
70
+ log('');
71
+ }
72
+ }
73
+
74
+ export function cmdPacksScan(deps?: PackCommandDeps): ScanResult[] {
75
+ const { state, checker, log } = deps ?? createDefaultDeps();
76
+
77
+ const manifests = [...state.available.values()].map((p) => p.manifest);
78
+ const results = scanForSoftware(manifests, checker);
79
+
80
+ log('Software scan results:');
81
+ log('');
82
+
83
+ const detected = results.filter((r) => r.detected);
84
+ const notDetected = results.filter((r) => !r.detected);
85
+
86
+ if (detected.length > 0) {
87
+ log(' Detected:');
88
+ for (const r of detected) {
89
+ const installed = state.installed.has(r.packName);
90
+ const status = installed ? '(installed)' : '(available)';
91
+ const matches: string[] = [];
92
+ if (r.matchedCommands.length > 0) matches.push(`commands: ${r.matchedCommands.join(', ')}`);
93
+ if (r.matchedFiles.length > 0) matches.push(`files: ${r.matchedFiles.join(', ')}`);
94
+ if (r.matchedServices.length > 0) matches.push(`services: ${r.matchedServices.join(', ')}`);
95
+ log(` ${r.packName} ${status} [${matches.join('; ')}]`);
96
+ }
97
+ log('');
98
+ }
99
+
100
+ if (notDetected.length > 0) {
101
+ log(' Not detected:');
102
+ for (const r of notDetected) {
103
+ log(` ${r.packName}`);
104
+ }
105
+ log('');
106
+ }
107
+
108
+ return results;
109
+ }
110
+
111
+ export function cmdPacksInstall(
112
+ name: string,
113
+ deps?: PackCommandDeps,
114
+ ): { success: boolean; permissions?: PermissionCheck } {
115
+ const { state, checker, getUserGroups, log } = deps ?? createDefaultDeps();
116
+
117
+ const pack = state.available.get(name);
118
+ if (!pack) {
119
+ log(`Error: Pack "${name}" not found.`);
120
+ log(`Available packs: ${[...state.available.keys()].join(', ')}`);
121
+ return { success: false };
122
+ }
123
+
124
+ if (state.installed.has(name)) {
125
+ log(`Pack "${name}" is already installed.`);
126
+ return { success: true };
127
+ }
128
+
129
+ // Check permissions
130
+ const userGroups = getUserGroups();
131
+ const permissions = checkPackPermissions(pack.manifest, checker, userGroups);
132
+
133
+ if (!permissions.satisfied) {
134
+ log(`Pack "${name}" requires additional permissions:`);
135
+ if (permissions.missingGroups.length > 0) {
136
+ log(` Missing groups: ${permissions.missingGroups.join(', ')}`);
137
+ log(' To grant access:');
138
+ for (const group of permissions.missingGroups) {
139
+ log(` sudo usermod -aG ${group} $(whoami)`);
140
+ }
141
+ }
142
+ if (permissions.missingCommands.length > 0) {
143
+ log(` Missing commands: ${permissions.missingCommands.join(', ')}`);
144
+ log(' Install the required software before enabling this pack.');
145
+ }
146
+ if (permissions.missingFiles.length > 0) {
147
+ log(` Missing files: ${permissions.missingFiles.join(', ')}`);
148
+ }
149
+ return { success: false, permissions };
150
+ }
151
+
152
+ state.installed.set(name, pack);
153
+ log(`Pack "${name}" installed successfully.`);
154
+ log(` ${pack.manifest.probes.length} probes now available.`);
155
+ return { success: true, permissions };
156
+ }
157
+
158
+ export function cmdPacksUninstall(name: string, deps?: PackCommandDeps): boolean {
159
+ const { state, log } = deps ?? createDefaultDeps();
160
+
161
+ if (!state.installed.has(name)) {
162
+ log(`Error: Pack "${name}" is not installed.`);
163
+ return false;
164
+ }
165
+
166
+ state.installed.delete(name);
167
+ log(`Pack "${name}" uninstalled.`);
168
+ return true;
169
+ }
170
+
171
+ export function handlePacksCommand(subArgs: string[]): void {
172
+ const subcommand = subArgs[0];
173
+
174
+ switch (subcommand) {
175
+ case 'list':
176
+ cmdPacksList();
177
+ break;
178
+ case 'scan':
179
+ cmdPacksScan();
180
+ break;
181
+ case 'install': {
182
+ const name = subArgs[1];
183
+ if (!name) {
184
+ console.error('Usage: sonde packs install <name>');
185
+ process.exit(1);
186
+ }
187
+ const result = cmdPacksInstall(name);
188
+ if (!result.success) process.exit(1);
189
+ break;
190
+ }
191
+ case 'uninstall': {
192
+ const name = subArgs[1];
193
+ if (!name) {
194
+ console.error('Usage: sonde packs uninstall <name>');
195
+ process.exit(1);
196
+ }
197
+ if (!cmdPacksUninstall(name)) process.exit(1);
198
+ break;
199
+ }
200
+ default:
201
+ console.log('Usage: sonde packs <command>');
202
+ console.log('');
203
+ console.log('Commands:');
204
+ console.log(' list Show installed packs and their probes');
205
+ console.log(' scan Detect available software, suggest packs');
206
+ console.log(' install Load and activate a pack');
207
+ console.log(' uninstall Remove a pack');
208
+ if (subcommand) {
209
+ console.error(`\nUnknown packs command: ${subcommand}`);
210
+ process.exit(1);
211
+ }
212
+ break;
213
+ }
214
+ }
package/src/config.ts ADDED
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export interface AgentConfig {
6
+ hubUrl: string;
7
+ apiKey: string;
8
+ agentName: string;
9
+ agentId?: string;
10
+ enrollmentToken?: string;
11
+ certPath?: string;
12
+ keyPath?: string;
13
+ caCertPath?: string;
14
+ scrubPatterns?: string[];
15
+ }
16
+
17
+ const CONFIG_DIR = path.join(os.homedir(), '.sonde');
18
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
19
+
20
+ export function getConfigPath(): string {
21
+ return CONFIG_FILE;
22
+ }
23
+
24
+ export function loadConfig(): AgentConfig | undefined {
25
+ try {
26
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
27
+ return JSON.parse(raw) as AgentConfig;
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ export function saveConfig(config: AgentConfig): void {
34
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
35
+ fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
36
+ }
37
+
38
+ export function getConfigDir(): string {
39
+ return CONFIG_DIR;
40
+ }
41
+
42
+ /** Save cert/key/ca PEM files to ~/.sonde/ and update config paths. */
43
+ export function saveCerts(
44
+ config: AgentConfig,
45
+ certPem: string,
46
+ keyPem: string,
47
+ caCertPem: string,
48
+ ): void {
49
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
50
+
51
+ const certPath = path.join(CONFIG_DIR, 'cert.pem');
52
+ const keyPath = path.join(CONFIG_DIR, 'key.pem');
53
+ const caCertPath = path.join(CONFIG_DIR, 'ca.pem');
54
+
55
+ fs.writeFileSync(certPath, certPem, 'utf-8');
56
+ fs.writeFileSync(keyPath, keyPem, 'utf-8');
57
+ fs.writeFileSync(caCertPath, caCertPem, 'utf-8');
58
+
59
+ config.certPath = certPath;
60
+ config.keyPath = keyPath;
61
+ config.caCertPath = caCertPath;
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ import os from 'node:os';
4
+ import { handlePacksCommand } from './cli/packs.js';
5
+ import { type AgentConfig, getConfigPath, loadConfig, saveConfig } from './config.js';
6
+ import { AgentConnection, type ConnectionEvents, enrollWithHub } from './runtime/connection.js';
7
+ import { ProbeExecutor } from './runtime/executor.js';
8
+ import { checkNotRoot } from './runtime/privilege.js';
9
+ import { buildPatterns } from './runtime/scrubber.js';
10
+
11
+ const args = process.argv.slice(2);
12
+ const command = args[0];
13
+
14
+ function hasFlag(flag: string): boolean {
15
+ return args.includes(flag);
16
+ }
17
+
18
+ function printUsage(): void {
19
+ console.log('Usage: sonde [command]');
20
+ console.log('');
21
+ console.log('Commands:');
22
+ console.log(' (none) Launch management TUI (if enrolled)');
23
+ console.log(' install Interactive guided setup (enroll + scan + packs)');
24
+ console.log(' enroll Enroll this agent with a hub');
25
+ console.log(' start Start the agent (TUI by default, --headless for daemon)');
26
+ console.log(' status Show agent status');
27
+ console.log(' packs Manage packs (list, scan, install, uninstall)');
28
+ console.log('');
29
+ console.log('Enroll options:');
30
+ console.log(' --hub <url> Hub URL (e.g. http://localhost:3000)');
31
+ console.log(' --key <key> API key for authentication');
32
+ console.log(' --name <name> Agent name (default: hostname)');
33
+ console.log(' --token <token> Enrollment token for mTLS cert issuance');
34
+ console.log('');
35
+ console.log('Start options:');
36
+ console.log(' --headless Run without TUI (for systemd / background)');
37
+ }
38
+
39
+ function getArg(flag: string): string | undefined {
40
+ const idx = args.indexOf(flag);
41
+ if (idx === -1 || idx + 1 >= args.length) return undefined;
42
+ return args[idx + 1];
43
+ }
44
+
45
+ interface Runtime {
46
+ config: AgentConfig;
47
+ executor: ProbeExecutor;
48
+ connection: AgentConnection;
49
+ }
50
+
51
+ function createRuntime(events: ConnectionEvents): Runtime {
52
+ checkNotRoot();
53
+
54
+ const config = loadConfig();
55
+ if (!config) {
56
+ console.error('Error: Agent not enrolled. Run "sonde enroll" first.');
57
+ process.exit(1);
58
+ }
59
+
60
+ const executor = new ProbeExecutor(undefined, undefined, buildPatterns(config.scrubPatterns));
61
+ const connection = new AgentConnection(config, executor, events);
62
+
63
+ return { config, executor, connection };
64
+ }
65
+
66
+ async function cmdEnroll(): Promise<void> {
67
+ const hubUrl = getArg('--hub');
68
+ const apiKey = getArg('--key');
69
+ const agentName = getArg('--name') ?? os.hostname();
70
+ const enrollmentToken = getArg('--token');
71
+
72
+ if (!hubUrl || !apiKey) {
73
+ console.error('Error: --hub and --key are required');
74
+ console.error(' sonde enroll --hub http://localhost:3000 --key your-api-key');
75
+ process.exit(1);
76
+ }
77
+
78
+ const config: AgentConfig = { hubUrl, apiKey, agentName };
79
+ if (enrollmentToken) {
80
+ config.enrollmentToken = enrollmentToken;
81
+ }
82
+ saveConfig(config);
83
+
84
+ const executor = new ProbeExecutor();
85
+ console.log(`Enrolling with hub at ${hubUrl}...`);
86
+
87
+ const { agentId, certIssued } = await enrollWithHub(config, executor);
88
+ config.agentId = agentId;
89
+ // Clear the one-time token after use
90
+ config.enrollmentToken = undefined;
91
+ saveConfig(config);
92
+
93
+ console.log('Agent enrolled successfully.');
94
+ console.log(` Hub: ${hubUrl}`);
95
+ console.log(` Name: ${agentName}`);
96
+ console.log(` Agent ID: ${agentId}`);
97
+ console.log(` Config: ${getConfigPath()}`);
98
+ if (certIssued) {
99
+ console.log(' mTLS: Client certificate issued and saved');
100
+ }
101
+ console.log('');
102
+ console.log('Run "sonde start" to connect.');
103
+ }
104
+
105
+ function cmdStart(): void {
106
+ const { config, connection } = createRuntime({
107
+ onConnected: (agentId) => {
108
+ console.log(`Connected to hub (agentId: ${agentId})`);
109
+ },
110
+ onDisconnected: () => {
111
+ console.log('Disconnected from hub, reconnecting...');
112
+ },
113
+ onError: (err) => {
114
+ console.error(`Connection error: ${err.message}`);
115
+ },
116
+ onRegistered: (agentId) => {
117
+ config.agentId = agentId;
118
+ saveConfig(config);
119
+ },
120
+ });
121
+
122
+ console.log('Sonde Agent v0.1.0');
123
+ console.log(` Name: ${config.agentName}`);
124
+ console.log(` Hub: ${config.hubUrl}`);
125
+ console.log('');
126
+
127
+ connection.start();
128
+
129
+ process.on('SIGINT', () => {
130
+ console.log('\nShutting down...');
131
+ connection.stop();
132
+ process.exit(0);
133
+ });
134
+ }
135
+
136
+ async function cmdManager(): Promise<void> {
137
+ const { render } = await import('ink');
138
+ const { createElement } = await import('react');
139
+ const { ManagerApp } = await import('./tui/manager/ManagerApp.js');
140
+ const { waitUntilExit } = render(createElement(ManagerApp, { createRuntime }));
141
+ await waitUntilExit();
142
+ }
143
+
144
+ function cmdStatus(): void {
145
+ const config = loadConfig();
146
+ if (!config) {
147
+ console.log('Status: Not enrolled');
148
+ console.log(`Run "sonde enroll" to get started.`);
149
+ return;
150
+ }
151
+
152
+ console.log('Sonde Agent Status');
153
+ console.log(` Name: ${config.agentName}`);
154
+ console.log(` Hub: ${config.hubUrl}`);
155
+ console.log(` Agent ID: ${config.agentId ?? '(not yet assigned)'}`);
156
+ console.log(` Config: ${getConfigPath()}`);
157
+ }
158
+
159
+ async function cmdInstall(): Promise<void> {
160
+ const initialHubUrl = getArg('--hub');
161
+ const { render } = await import('ink');
162
+ const { createElement } = await import('react');
163
+ const { InstallerApp } = await import('./tui/installer/InstallerApp.js');
164
+ const { waitUntilExit } = render(createElement(InstallerApp, { initialHubUrl }));
165
+ await waitUntilExit();
166
+ }
167
+
168
+ switch (command) {
169
+ case 'install':
170
+ cmdInstall().catch((err: Error) => {
171
+ console.error(err.message);
172
+ process.exit(1);
173
+ });
174
+ break;
175
+ case 'enroll':
176
+ cmdEnroll().catch((err: Error) => {
177
+ console.error(`Enrollment failed: ${err.message}`);
178
+ process.exit(1);
179
+ });
180
+ break;
181
+ case 'start':
182
+ if (hasFlag('--headless')) {
183
+ cmdStart();
184
+ } else {
185
+ cmdManager().catch((err: Error) => {
186
+ console.error(err.message);
187
+ process.exit(1);
188
+ });
189
+ }
190
+ break;
191
+ case 'status':
192
+ cmdStatus();
193
+ break;
194
+ case 'packs':
195
+ handlePacksCommand(args.slice(1));
196
+ break;
197
+ default:
198
+ if (command) {
199
+ printUsage();
200
+ console.error(`\nUnknown command: ${command}`);
201
+ process.exit(1);
202
+ } else {
203
+ // No command: launch TUI if enrolled, otherwise show usage
204
+ const config = loadConfig();
205
+ if (config) {
206
+ cmdManager().catch((err: Error) => {
207
+ console.error(err.message);
208
+ process.exit(1);
209
+ });
210
+ } else {
211
+ printUsage();
212
+ }
213
+ }
214
+ break;
215
+ }
216
+
217
+ export { createRuntime };
218
+ export type { Runtime };
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { AgentConfig } from '../config.js';
3
+ import { generateAttestation, hashConfig, hashFile } from './attestation.js';
4
+ import { ProbeExecutor } from './executor.js';
5
+
6
+ function makeConfig(overrides?: Partial<AgentConfig>): AgentConfig {
7
+ return {
8
+ hubUrl: 'http://localhost:3000',
9
+ apiKey: 'test-key',
10
+ agentName: 'test-agent',
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ function makeExecutor(): ProbeExecutor {
16
+ return new ProbeExecutor(new Map());
17
+ }
18
+
19
+ describe('generateAttestation', () => {
20
+ it('returns all required fields', () => {
21
+ const att = generateAttestation(makeConfig(), makeExecutor());
22
+ expect(att).toHaveProperty('osVersion');
23
+ expect(att).toHaveProperty('binaryHash');
24
+ expect(att).toHaveProperty('installedPacks');
25
+ expect(att).toHaveProperty('configHash');
26
+ expect(att).toHaveProperty('nodeVersion');
27
+ });
28
+
29
+ it('osVersion contains platform info', () => {
30
+ const att = generateAttestation(makeConfig(), makeExecutor());
31
+ expect(att.osVersion).toContain(process.platform);
32
+ });
33
+
34
+ it('nodeVersion matches process.version', () => {
35
+ const att = generateAttestation(makeConfig(), makeExecutor());
36
+ expect(att.nodeVersion).toBe(process.version);
37
+ });
38
+
39
+ it('installedPacks reflects loaded packs', () => {
40
+ const att = generateAttestation(makeConfig(), makeExecutor());
41
+ // Empty map → no packs
42
+ expect(att.installedPacks).toEqual([]);
43
+ });
44
+ });
45
+
46
+ describe('hashConfig', () => {
47
+ it('is deterministic for same config', () => {
48
+ const cfg = makeConfig();
49
+ expect(hashConfig(cfg)).toBe(hashConfig(cfg));
50
+ });
51
+
52
+ it('ignores apiKey changes', () => {
53
+ const a = hashConfig(makeConfig({ apiKey: 'key-a' }));
54
+ const b = hashConfig(makeConfig({ apiKey: 'key-b' }));
55
+ expect(a).toBe(b);
56
+ });
57
+
58
+ it('ignores enrollmentToken changes', () => {
59
+ const a = hashConfig(makeConfig({ enrollmentToken: 'tok-a' }));
60
+ const b = hashConfig(makeConfig({ enrollmentToken: 'tok-b' }));
61
+ expect(a).toBe(b);
62
+ });
63
+ });
64
+
65
+ describe('hashFile', () => {
66
+ it('returns empty string for missing file', () => {
67
+ expect(hashFile('/nonexistent/path/to/file')).toBe('');
68
+ });
69
+ });
@@ -0,0 +1,36 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import type { AttestationData } from '@sonde/shared';
5
+ import type { AgentConfig } from '../config.js';
6
+ import type { ProbeExecutor } from './executor.js';
7
+
8
+ /** SHA-256 hex of a file. Returns empty string on any error. */
9
+ export function hashFile(filePath: string): string {
10
+ try {
11
+ const data = fs.readFileSync(filePath);
12
+ return crypto.createHash('sha256').update(data).digest('hex');
13
+ } catch {
14
+ return '';
15
+ }
16
+ }
17
+
18
+ /** SHA-256 hex of config with sensitive fields stripped, keys sorted for determinism. */
19
+ export function hashConfig(config: AgentConfig): string {
20
+ const { apiKey: _, enrollmentToken: _t, ...rest } = config;
21
+ const sorted = JSON.stringify(rest, Object.keys(rest).sort());
22
+ return crypto.createHash('sha256').update(sorted).digest('hex');
23
+ }
24
+
25
+ export function generateAttestation(config: AgentConfig, executor: ProbeExecutor): AttestationData {
26
+ return {
27
+ osVersion: `${os.platform()} ${os.release()} ${os.arch()}`,
28
+ binaryHash: hashFile(process.argv[1] ?? ''),
29
+ installedPacks: executor.getLoadedPacks().map((p) => ({
30
+ name: p.name,
31
+ version: p.version,
32
+ })),
33
+ configHash: hashConfig(config),
34
+ nodeVersion: process.version,
35
+ };
36
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AgentAuditLog } from './audit.js';
3
+
4
+ describe('AgentAuditLog', () => {
5
+ it('logs entries and verifies chain', () => {
6
+ const log = new AgentAuditLog();
7
+
8
+ log.log('system.disk.usage', 'success', 10);
9
+ log.log('system.memory.usage', 'success', 20);
10
+ log.log('system.cpu.usage', 'error', 30);
11
+
12
+ const entries = log.getRecent();
13
+ expect(entries).toHaveLength(3);
14
+ expect(entries[0]?.prevHash).toBe('');
15
+ expect(entries[1]?.prevHash).not.toBe('');
16
+ expect(log.verifyChain()).toEqual({ valid: true });
17
+ });
18
+
19
+ it('ring buffer cap works', () => {
20
+ const log = new AgentAuditLog(3);
21
+
22
+ log.log('p1', 'success', 1);
23
+ log.log('p2', 'success', 2);
24
+ log.log('p3', 'success', 3);
25
+ log.log('p4', 'success', 4);
26
+
27
+ const entries = log.getRecent();
28
+ expect(entries).toHaveLength(3);
29
+ expect(entries[0]?.probe).toBe('p2');
30
+ expect(entries[2]?.probe).toBe('p4');
31
+ });
32
+
33
+ it('empty log is valid', () => {
34
+ const log = new AgentAuditLog();
35
+ expect(log.verifyChain()).toEqual({ valid: true });
36
+ });
37
+
38
+ it('getRecent(n) returns last n entries', () => {
39
+ const log = new AgentAuditLog();
40
+ log.log('p1', 'success', 1);
41
+ log.log('p2', 'success', 2);
42
+ log.log('p3', 'success', 3);
43
+
44
+ const last2 = log.getRecent(2);
45
+ expect(last2).toHaveLength(2);
46
+ expect(last2[0]?.probe).toBe('p2');
47
+ expect(last2[1]?.probe).toBe('p3');
48
+ });
49
+
50
+ it('chain after ring buffer eviction still verifies within remaining entries', () => {
51
+ const log = new AgentAuditLog(2);
52
+
53
+ log.log('p1', 'success', 1);
54
+ log.log('p2', 'success', 2);
55
+ log.log('p3', 'success', 3);
56
+
57
+ // After eviction, the first remaining entry's prevHash won't match empty string
58
+ // verifyChain checks what's in the buffer, genesis may have non-empty prevHash
59
+ const result = log.verifyChain();
60
+ // First entry in buffer (p2) has a prevHash from p1, which is no longer genesis
61
+ // This is expected: a truncated chain won't verify from genesis
62
+ expect(result.valid).toBe(false);
63
+ });
64
+ });