agentvibes 4.2.0 → 4.4.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 (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +2 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -433
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -269
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  79. package/.claude/hooks/play-tts-macos.sh +368 -368
  80. package/.claude/hooks/play-tts-piper.sh +679 -679
  81. package/.claude/hooks/play-tts-soprano.sh +356 -356
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -301
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -81
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -84
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -145
  99. package/.claude/hooks/tts-queue.sh +165 -165
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -548
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -170
  149. package/README.md +2029 -2007
  150. package/RELEASE_NOTES.md +1310 -1203
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1840
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1453
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -110
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -824
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -44
  182. package/src/console/footer-config.js +50 -50
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -62
  185. package/src/console/tabs/agents-tab.js +1684 -1516
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -53
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -1212
  192. package/src/console/tabs/settings-tab.js +152 -79
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -25
  195. package/src/console/widgets/format-utils.js +89 -89
  196. package/src/console/widgets/notice.js +55 -55
  197. package/src/console/widgets/personality-picker.js +185 -185
  198. package/src/console/widgets/reverb-picker.js +94 -94
  199. package/src/console/widgets/track-picker.js +285 -285
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5829
  202. package/src/services/agent-voice-store.js +423 -423
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -285
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -482
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. package/.claude/config/background-music-position.txt +0 -1
@@ -1,358 +1,358 @@
1
- /**
2
- * File Ownership Verifier - Cross-Platform Security Validation
3
- * Story 4.3: File Ownership Verification
4
- *
5
- * Verifies that files are owned by the current user before processing.
6
- * This prevents malicious files planted by other users from being used.
7
- *
8
- * Handles platform-specific differences:
9
- * - Unix/Linux/macOS: UID-based ownership checking
10
- * - Windows: Uses fs.statSync().uid if available, graceful fallback
11
- * - Network mounts: Documented limitation, best-effort checking
12
- *
13
- * @module file-ownership-verifier
14
- * @requires fs
15
- * @requires os
16
- * @requires path
17
- */
18
-
19
- import fs from 'node:fs';
20
- import os from 'node:os';
21
- import path from 'node:path';
22
-
23
- /**
24
- * Story 4.3: Verify file is owned by current user
25
- *
26
- * On Unix-like systems (Linux, macOS):
27
- * - Compares fs.statSync().uid with process.getuid()
28
- *
29
- * On Windows:
30
- * - Attempts UID comparison if available
31
- * - Gracefully falls back to path/permission checks if UID unavailable
32
- * - Network mount files may not have reliable ownership info
33
- *
34
- * Performance: Typically < 5ms (stat operation only, no I/O)
35
- *
36
- * @param {string} filePath - Path to file to check
37
- * @param {Object} options - Verification options
38
- * @param {boolean} options.allowNetworkMounts - Allow verification to succeed on network mounts (default: true)
39
- * @param {Function} options.logger - Optional logger function for sanitized logs
40
- * @returns {Object} {
41
- * isOwned: boolean,
42
- * error: string|null,
43
- * ownerUid: number|null,
44
- * currentUid: number|null,
45
- * isNetworkMount: boolean,
46
- * platform: string
47
- * }
48
- */
49
- export function verifyFileOwnership(filePath, options = {}) {
50
- const { allowNetworkMounts = true, logger = null } = options;
51
-
52
- try {
53
- if (!filePath || typeof filePath !== 'string') {
54
- const error = 'File path must be a non-empty string';
55
- logger?.(`ownership-check: invalid-path - ${error}`);
56
- return {
57
- isOwned: false,
58
- error,
59
- ownerUid: null,
60
- currentUid: null,
61
- isNetworkMount: false,
62
- platform: process.platform
63
- };
64
- }
65
-
66
- // Check file exists
67
- if (!fs.existsSync(filePath)) {
68
- const error = 'File does not exist';
69
- logger?.(`ownership-check: missing-file - ${filePath}`);
70
- return {
71
- isOwned: false,
72
- error,
73
- ownerUid: null,
74
- currentUid: null,
75
- isNetworkMount: false,
76
- platform: process.platform
77
- };
78
- }
79
-
80
- // Get file stats
81
- const stats = fs.statSync(filePath);
82
-
83
- // Check if it's a regular file
84
- if (!stats.isFile()) {
85
- const error = 'Path must be a regular file';
86
- logger?.(`ownership-check: not-file - ${filePath}`);
87
- return {
88
- isOwned: false,
89
- error,
90
- ownerUid: null,
91
- currentUid: null,
92
- isNetworkMount: false,
93
- platform: process.platform
94
- };
95
- }
96
-
97
- const currentUid = process.getuid ? process.getuid() : null;
98
- const fileUid = stats.uid;
99
-
100
- // Determine platform type for logging
101
- const platformType = getPlatformType();
102
- const isNetworkMount = checkIsNetworkMount(filePath);
103
-
104
- // Log verification attempt (sanitized - no sensitive paths)
105
- logger?.(`ownership-check: started - platform=${platformType}, network=${isNetworkMount}`);
106
-
107
- // Platform-specific ownership verification
108
- if (process.platform === 'win32') {
109
- return verifyWindowsOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger);
110
- } else {
111
- // Unix-like systems (Linux, macOS)
112
- return verifyUnixOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger);
113
- }
114
-
115
- } catch (err) {
116
- const error = `Error verifying file ownership: ${err.message}`;
117
- logger?.(`ownership-check: error - ${error}`);
118
- return {
119
- isOwned: false,
120
- error,
121
- ownerUid: null,
122
- currentUid: null,
123
- isNetworkMount: false,
124
- platform: process.platform
125
- };
126
- }
127
- }
128
-
129
- /**
130
- * Unix/Linux/macOS ownership verification
131
- * Uses UID comparison for reliable security
132
- *
133
- * @private
134
- */
135
- function verifyUnixOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger) {
136
- if (currentUid === null) {
137
- const error = 'Unable to determine current user UID (not available on this platform)';
138
- logger?.(`ownership-check: no-uid-support`);
139
- return {
140
- isOwned: false,
141
- error,
142
- ownerUid: stats.uid,
143
- currentUid: null,
144
- isNetworkMount,
145
- platform: process.platform
146
- };
147
- }
148
-
149
- // Check if UIDs match
150
- if (stats.uid === currentUid) {
151
- logger?.(`ownership-check: success - uid=${currentUid}`);
152
- return {
153
- isOwned: true,
154
- error: null,
155
- ownerUid: stats.uid,
156
- currentUid,
157
- isNetworkMount,
158
- platform: process.platform
159
- };
160
- }
161
-
162
- // UIDs don't match - file owned by different user
163
- const error = 'File not owned by current user (security check failed)';
164
- logger?.(`ownership-check: failed - file-uid=${stats.uid}, user-uid=${currentUid}`);
165
- return {
166
- isOwned: false,
167
- error,
168
- ownerUid: stats.uid,
169
- currentUid,
170
- isNetworkMount,
171
- platform: process.platform
172
- };
173
- }
174
-
175
- /**
176
- * Windows ownership verification
177
- * Windows doesn't have traditional UID system, use file permissions
178
- *
179
- * @private
180
- */
181
- function verifyWindowsOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger) {
182
- // On Windows, process.getuid() is typically undefined
183
- // Try to verify through accessible permissions instead
184
-
185
- // If running in WSL (Windows Subsystem for Linux), we have UID available
186
- if (currentUid !== null && stats.uid !== undefined) {
187
- if (stats.uid === currentUid) {
188
- logger?.(`ownership-check: windows-wsl-success - uid=${currentUid}`);
189
- return {
190
- isOwned: true,
191
- error: null,
192
- ownerUid: stats.uid,
193
- currentUid,
194
- isNetworkMount,
195
- platform: process.platform
196
- };
197
- }
198
-
199
- const error = 'File not owned by current user (security check failed)';
200
- logger?.(`ownership-check: windows-wsl-failed - file-uid=${stats.uid}, user-uid=${currentUid}`);
201
- return {
202
- isOwned: false,
203
- error,
204
- ownerUid: stats.uid,
205
- currentUid,
206
- isNetworkMount,
207
- platform: process.platform
208
- };
209
- }
210
-
211
- // Native Windows without UID support
212
- // Check if file is readable and writable by current user (best effort)
213
- try {
214
- // Try to read and write to verify we own it
215
- fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK);
216
-
217
- // If we can read and write, assume we own it
218
- logger?.(`ownership-check: windows-native-success - readable-writable`);
219
- return {
220
- isOwned: true,
221
- error: null,
222
- ownerUid: null,
223
- currentUid: null,
224
- isNetworkMount,
225
- platform: process.platform
226
- };
227
- } catch (err) {
228
- // Can't read/write - likely not owned by us or permission issue
229
- const error = `File not owned by current user or not accessible (Windows): ${err.message}`;
230
- logger?.(`ownership-check: windows-native-failed - ${err.code}`);
231
- return {
232
- isOwned: false,
233
- error,
234
- ownerUid: null,
235
- currentUid: null,
236
- isNetworkMount,
237
- platform: process.platform
238
- };
239
- }
240
- }
241
-
242
- /**
243
- * Determine if path is likely a network mount
244
- * Used to set expectations for ownership checking reliability
245
- *
246
- * @private
247
- */
248
- function checkIsNetworkMount(filePath) {
249
- const resolvedPath = path.resolve(filePath);
250
-
251
- if (process.platform === 'win32') {
252
- // Windows: check for UNC paths (\\server\share) or mapped drives
253
- // Also check for paths under %APPDATA% or similar which might be network-synced
254
- return resolvedPath.startsWith('\\\\') ||
255
- resolvedPath.match(/^[A-Z]:\\[^\\]*\\netshare/i);
256
- } else {
257
- // Unix: check for common network mount prefixes
258
- // /mnt, /media, NFS mount points
259
- return resolvedPath.startsWith('/mnt/') ||
260
- resolvedPath.startsWith('/media/') ||
261
- resolvedPath.includes(':/'); // NFS mount indicators
262
- }
263
- }
264
-
265
- /**
266
- * Get human-readable platform type
267
- *
268
- * @private
269
- */
270
- function getPlatformType() {
271
- switch (process.platform) {
272
- case 'linux':
273
- return 'linux';
274
- case 'darwin':
275
- return 'macos';
276
- case 'win32':
277
- return 'windows';
278
- default:
279
- return process.platform;
280
- }
281
- }
282
-
283
- /**
284
- * Story 4.3: Get current user information for logging/debugging
285
- *
286
- * Returns user info in a sanitized format safe for logging.
287
- * Does NOT include sensitive paths or full usernames.
288
- *
289
- * @returns {Object} {
290
- * uid: number|null,
291
- * gid: number|null,
292
- * username: string,
293
- * platform: string
294
- * }
295
- */
296
- export function getCurrentUserInfo() {
297
- try {
298
- const uid = process.getuid ? process.getuid() : null;
299
- const gid = process.getgid ? process.getgid() : null;
300
- const username = os.userInfo().username || 'unknown';
301
-
302
- return {
303
- uid,
304
- gid,
305
- username,
306
- platform: getPlatformType()
307
- };
308
- } catch (err) {
309
- return {
310
- uid: null,
311
- gid: null,
312
- username: 'unknown',
313
- platform: getPlatformType()
314
- };
315
- }
316
- }
317
-
318
- /**
319
- * Story 4.3: Batch verify ownership of multiple files
320
- *
321
- * Efficiently checks ownership of multiple files.
322
- * Returns detailed results for each file.
323
- *
324
- * @param {string[]} filePaths - Array of file paths to check
325
- * @param {Object} options - Verification options (passed to verifyFileOwnership)
326
- * @returns {Object} {
327
- * allOwned: boolean,
328
- * results: Array<{path: string, isOwned: boolean, error: string|null}>
329
- * }
330
- */
331
- export function verifyMultipleFiles(filePaths, options = {}) {
332
- const results = [];
333
- let allOwned = true;
334
-
335
- for (const filePath of filePaths) {
336
- const result = verifyFileOwnership(filePath, options);
337
- results.push({
338
- path: filePath,
339
- isOwned: result.isOwned,
340
- error: result.error
341
- });
342
-
343
- if (!result.isOwned) {
344
- allOwned = false;
345
- }
346
- }
347
-
348
- return {
349
- allOwned,
350
- results
351
- };
352
- }
353
-
354
- export default {
355
- verifyFileOwnership,
356
- getCurrentUserInfo,
357
- verifyMultipleFiles
358
- };
1
+ /**
2
+ * File Ownership Verifier - Cross-Platform Security Validation
3
+ * Story 4.3: File Ownership Verification
4
+ *
5
+ * Verifies that files are owned by the current user before processing.
6
+ * This prevents malicious files planted by other users from being used.
7
+ *
8
+ * Handles platform-specific differences:
9
+ * - Unix/Linux/macOS: UID-based ownership checking
10
+ * - Windows: Uses fs.statSync().uid if available, graceful fallback
11
+ * - Network mounts: Documented limitation, best-effort checking
12
+ *
13
+ * @module file-ownership-verifier
14
+ * @requires fs
15
+ * @requires os
16
+ * @requires path
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+
23
+ /**
24
+ * Story 4.3: Verify file is owned by current user
25
+ *
26
+ * On Unix-like systems (Linux, macOS):
27
+ * - Compares fs.statSync().uid with process.getuid()
28
+ *
29
+ * On Windows:
30
+ * - Attempts UID comparison if available
31
+ * - Gracefully falls back to path/permission checks if UID unavailable
32
+ * - Network mount files may not have reliable ownership info
33
+ *
34
+ * Performance: Typically < 5ms (stat operation only, no I/O)
35
+ *
36
+ * @param {string} filePath - Path to file to check
37
+ * @param {Object} options - Verification options
38
+ * @param {boolean} options.allowNetworkMounts - Allow verification to succeed on network mounts (default: true)
39
+ * @param {Function} options.logger - Optional logger function for sanitized logs
40
+ * @returns {Object} {
41
+ * isOwned: boolean,
42
+ * error: string|null,
43
+ * ownerUid: number|null,
44
+ * currentUid: number|null,
45
+ * isNetworkMount: boolean,
46
+ * platform: string
47
+ * }
48
+ */
49
+ export function verifyFileOwnership(filePath, options = {}) {
50
+ const { allowNetworkMounts = true, logger = null } = options;
51
+
52
+ try {
53
+ if (!filePath || typeof filePath !== 'string') {
54
+ const error = 'File path must be a non-empty string';
55
+ logger?.(`ownership-check: invalid-path - ${error}`);
56
+ return {
57
+ isOwned: false,
58
+ error,
59
+ ownerUid: null,
60
+ currentUid: null,
61
+ isNetworkMount: false,
62
+ platform: process.platform
63
+ };
64
+ }
65
+
66
+ // Check file exists
67
+ if (!fs.existsSync(filePath)) {
68
+ const error = 'File does not exist';
69
+ logger?.(`ownership-check: missing-file - ${filePath}`);
70
+ return {
71
+ isOwned: false,
72
+ error,
73
+ ownerUid: null,
74
+ currentUid: null,
75
+ isNetworkMount: false,
76
+ platform: process.platform
77
+ };
78
+ }
79
+
80
+ // Get file stats
81
+ const stats = fs.statSync(filePath);
82
+
83
+ // Check if it's a regular file
84
+ if (!stats.isFile()) {
85
+ const error = 'Path must be a regular file';
86
+ logger?.(`ownership-check: not-file - ${filePath}`);
87
+ return {
88
+ isOwned: false,
89
+ error,
90
+ ownerUid: null,
91
+ currentUid: null,
92
+ isNetworkMount: false,
93
+ platform: process.platform
94
+ };
95
+ }
96
+
97
+ const currentUid = process.getuid ? process.getuid() : null;
98
+ const fileUid = stats.uid;
99
+
100
+ // Determine platform type for logging
101
+ const platformType = getPlatformType();
102
+ const isNetworkMount = checkIsNetworkMount(filePath);
103
+
104
+ // Log verification attempt (sanitized - no sensitive paths)
105
+ logger?.(`ownership-check: started - platform=${platformType}, network=${isNetworkMount}`);
106
+
107
+ // Platform-specific ownership verification
108
+ if (process.platform === 'win32') {
109
+ return verifyWindowsOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger);
110
+ } else {
111
+ // Unix-like systems (Linux, macOS)
112
+ return verifyUnixOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger);
113
+ }
114
+
115
+ } catch (err) {
116
+ const error = `Error verifying file ownership: ${err.message}`;
117
+ logger?.(`ownership-check: error - ${error}`);
118
+ return {
119
+ isOwned: false,
120
+ error,
121
+ ownerUid: null,
122
+ currentUid: null,
123
+ isNetworkMount: false,
124
+ platform: process.platform
125
+ };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Unix/Linux/macOS ownership verification
131
+ * Uses UID comparison for reliable security
132
+ *
133
+ * @private
134
+ */
135
+ function verifyUnixOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger) {
136
+ if (currentUid === null) {
137
+ const error = 'Unable to determine current user UID (not available on this platform)';
138
+ logger?.(`ownership-check: no-uid-support`);
139
+ return {
140
+ isOwned: false,
141
+ error,
142
+ ownerUid: stats.uid,
143
+ currentUid: null,
144
+ isNetworkMount,
145
+ platform: process.platform
146
+ };
147
+ }
148
+
149
+ // Check if UIDs match
150
+ if (stats.uid === currentUid) {
151
+ logger?.(`ownership-check: success - uid=${currentUid}`);
152
+ return {
153
+ isOwned: true,
154
+ error: null,
155
+ ownerUid: stats.uid,
156
+ currentUid,
157
+ isNetworkMount,
158
+ platform: process.platform
159
+ };
160
+ }
161
+
162
+ // UIDs don't match - file owned by different user
163
+ const error = 'File not owned by current user (security check failed)';
164
+ logger?.(`ownership-check: failed - file-uid=${stats.uid}, user-uid=${currentUid}`);
165
+ return {
166
+ isOwned: false,
167
+ error,
168
+ ownerUid: stats.uid,
169
+ currentUid,
170
+ isNetworkMount,
171
+ platform: process.platform
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Windows ownership verification
177
+ * Windows doesn't have traditional UID system, use file permissions
178
+ *
179
+ * @private
180
+ */
181
+ function verifyWindowsOwnership(filePath, stats, currentUid, isNetworkMount, allowNetworkMounts, logger) {
182
+ // On Windows, process.getuid() is typically undefined
183
+ // Try to verify through accessible permissions instead
184
+
185
+ // If running in WSL (Windows Subsystem for Linux), we have UID available
186
+ if (currentUid !== null && stats.uid !== undefined) {
187
+ if (stats.uid === currentUid) {
188
+ logger?.(`ownership-check: windows-wsl-success - uid=${currentUid}`);
189
+ return {
190
+ isOwned: true,
191
+ error: null,
192
+ ownerUid: stats.uid,
193
+ currentUid,
194
+ isNetworkMount,
195
+ platform: process.platform
196
+ };
197
+ }
198
+
199
+ const error = 'File not owned by current user (security check failed)';
200
+ logger?.(`ownership-check: windows-wsl-failed - file-uid=${stats.uid}, user-uid=${currentUid}`);
201
+ return {
202
+ isOwned: false,
203
+ error,
204
+ ownerUid: stats.uid,
205
+ currentUid,
206
+ isNetworkMount,
207
+ platform: process.platform
208
+ };
209
+ }
210
+
211
+ // Native Windows without UID support
212
+ // Check if file is readable and writable by current user (best effort)
213
+ try {
214
+ // Try to read and write to verify we own it
215
+ fs.accessSync(filePath, fs.constants.R_OK | fs.constants.W_OK);
216
+
217
+ // If we can read and write, assume we own it
218
+ logger?.(`ownership-check: windows-native-success - readable-writable`);
219
+ return {
220
+ isOwned: true,
221
+ error: null,
222
+ ownerUid: null,
223
+ currentUid: null,
224
+ isNetworkMount,
225
+ platform: process.platform
226
+ };
227
+ } catch (err) {
228
+ // Can't read/write - likely not owned by us or permission issue
229
+ const error = `File not owned by current user or not accessible (Windows): ${err.message}`;
230
+ logger?.(`ownership-check: windows-native-failed - ${err.code}`);
231
+ return {
232
+ isOwned: false,
233
+ error,
234
+ ownerUid: null,
235
+ currentUid: null,
236
+ isNetworkMount,
237
+ platform: process.platform
238
+ };
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Determine if path is likely a network mount
244
+ * Used to set expectations for ownership checking reliability
245
+ *
246
+ * @private
247
+ */
248
+ function checkIsNetworkMount(filePath) {
249
+ const resolvedPath = path.resolve(filePath);
250
+
251
+ if (process.platform === 'win32') {
252
+ // Windows: check for UNC paths (\\server\share) or mapped drives
253
+ // Also check for paths under %APPDATA% or similar which might be network-synced
254
+ return resolvedPath.startsWith('\\\\') ||
255
+ resolvedPath.match(/^[A-Z]:\\[^\\]*\\netshare/i);
256
+ } else {
257
+ // Unix: check for common network mount prefixes
258
+ // /mnt, /media, NFS mount points
259
+ return resolvedPath.startsWith('/mnt/') ||
260
+ resolvedPath.startsWith('/media/') ||
261
+ resolvedPath.includes(':/'); // NFS mount indicators
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Get human-readable platform type
267
+ *
268
+ * @private
269
+ */
270
+ function getPlatformType() {
271
+ switch (process.platform) {
272
+ case 'linux':
273
+ return 'linux';
274
+ case 'darwin':
275
+ return 'macos';
276
+ case 'win32':
277
+ return 'windows';
278
+ default:
279
+ return process.platform;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Story 4.3: Get current user information for logging/debugging
285
+ *
286
+ * Returns user info in a sanitized format safe for logging.
287
+ * Does NOT include sensitive paths or full usernames.
288
+ *
289
+ * @returns {Object} {
290
+ * uid: number|null,
291
+ * gid: number|null,
292
+ * username: string,
293
+ * platform: string
294
+ * }
295
+ */
296
+ export function getCurrentUserInfo() {
297
+ try {
298
+ const uid = process.getuid ? process.getuid() : null;
299
+ const gid = process.getgid ? process.getgid() : null;
300
+ const username = os.userInfo().username || 'unknown';
301
+
302
+ return {
303
+ uid,
304
+ gid,
305
+ username,
306
+ platform: getPlatformType()
307
+ };
308
+ } catch (err) {
309
+ return {
310
+ uid: null,
311
+ gid: null,
312
+ username: 'unknown',
313
+ platform: getPlatformType()
314
+ };
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Story 4.3: Batch verify ownership of multiple files
320
+ *
321
+ * Efficiently checks ownership of multiple files.
322
+ * Returns detailed results for each file.
323
+ *
324
+ * @param {string[]} filePaths - Array of file paths to check
325
+ * @param {Object} options - Verification options (passed to verifyFileOwnership)
326
+ * @returns {Object} {
327
+ * allOwned: boolean,
328
+ * results: Array<{path: string, isOwned: boolean, error: string|null}>
329
+ * }
330
+ */
331
+ export function verifyMultipleFiles(filePaths, options = {}) {
332
+ const results = [];
333
+ let allOwned = true;
334
+
335
+ for (const filePath of filePaths) {
336
+ const result = verifyFileOwnership(filePath, options);
337
+ results.push({
338
+ path: filePath,
339
+ isOwned: result.isOwned,
340
+ error: result.error
341
+ });
342
+
343
+ if (!result.isOwned) {
344
+ allOwned = false;
345
+ }
346
+ }
347
+
348
+ return {
349
+ allOwned,
350
+ results
351
+ };
352
+ }
353
+
354
+ export default {
355
+ verifyFileOwnership,
356
+ getCurrentUserInfo,
357
+ verifyMultipleFiles
358
+ };