agentvibes 4.2.0 → 4.4.1

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 +208 -84
  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 +5895 -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 +143 -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,285 +1,285 @@
1
- /**
2
- * Music File Validator - Security-Critical Path and File Validation
3
- * Story 4.1: Path Validation and Security Hardening
4
- *
5
- * CRITICAL: All functions enforce CLAUDE.md security requirements:
6
- * - Use path.resolve() for all path operations
7
- * - Verify file ownership before processing external files
8
- * - Prevent path traversal attacks via traversal patterns, symlinks, etc.
9
- * - Validate file exists, is readable, and owned by current user
10
- *
11
- * @module music-file-validator
12
- * @requires fs
13
- * @requires path
14
- * @requires os
15
- */
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
- import os from 'node:os';
20
-
21
- /**
22
- * Story 4.1: Validate that a file path is safe and within user's home directory
23
- *
24
- * Security checks:
25
- * 1. Resolves path to absolute form (prevents ../ tricks)
26
- * 2. Validates path is within home directory
27
- * 3. Rejects symlinks pointing outside home
28
- * 4. Verifies file ownership matches current user
29
- * 5. Checks file is readable
30
- *
31
- * @param {string} userPath - Path provided by user (absolute or relative)
32
- * @param {string} userHomeDir - User's home directory (default: process.env.HOME)
33
- * @returns {Object} { isValid: boolean, error: string|null, resolvedPath: string|null }
34
- * @throws {Error} If path validation encounters unexpected errors
35
- */
36
- export function isPathSafe(userPath, userHomeDir = null) {
37
- try {
38
- // Parameter validation
39
- if (!userPath || typeof userPath !== 'string') {
40
- return {
41
- isValid: false,
42
- error: 'Path must be a non-empty string',
43
- resolvedPath: null
44
- };
45
- }
46
-
47
- // Use provided home dir or default to environment
48
- const homeDir = userHomeDir || (process.env.HOME || process.env.USERPROFILE);
49
- if (!homeDir) {
50
- return {
51
- isValid: false,
52
- error: 'Unable to determine home directory',
53
- resolvedPath: null
54
- };
55
- }
56
-
57
- // CRITICAL: Use path.resolve() to get absolute path (CLAUDE.md requirement)
58
- // This prevents directory traversal attacks like "../../etc/passwd"
59
- const resolvedPath = path.resolve(userPath);
60
- const resolvedHome = path.resolve(homeDir);
61
-
62
- // Check if path is within home directory
63
- // Must either match exactly OR start with home + separator (prevents /home/user2 bypass)
64
- const isWithinHome = resolvedPath === resolvedHome ||
65
- resolvedPath.startsWith(resolvedHome + path.sep);
66
-
67
- if (!isWithinHome) {
68
- return {
69
- isValid: false,
70
- error: `Security validation failed: path must be within home directory (${resolvedHome})`,
71
- resolvedPath: null
72
- };
73
- }
74
-
75
- // SECURITY: Use lstatSync first to detect symlinks before following them (#131)
76
- let lstats;
77
- try {
78
- lstats = fs.lstatSync(resolvedPath);
79
- } catch (err) {
80
- if (err.code === 'ENOENT') {
81
- return { isValid: false, error: `File not found: ${userPath}`, resolvedPath: null };
82
- }
83
- return { isValid: false, error: `Cannot access file: ${err.message}`, resolvedPath: null };
84
- }
85
-
86
- // Check if symlink - if so, verify target is also within home directory
87
- if (lstats.isSymbolicLink()) {
88
- try {
89
- const targetPath = fs.realpathSync(resolvedPath);
90
- const isTargetWithinHome = targetPath === resolvedHome ||
91
- targetPath.startsWith(resolvedHome + path.sep);
92
-
93
- if (!isTargetWithinHome) {
94
- return {
95
- isValid: false,
96
- error: 'Security validation failed: symlink target must be within home directory',
97
- resolvedPath: null
98
- };
99
- }
100
- } catch (err) {
101
- return {
102
- isValid: false,
103
- error: `Failed to resolve symlink: ${err.message}`,
104
- resolvedPath: null
105
- };
106
- }
107
- }
108
-
109
- // Get file stats (follows symlinks for regular file check)
110
- const stats = fs.statSync(resolvedPath);
111
-
112
- // Check if it's a regular file (not directory, special file, etc)
113
- if (!stats.isFile()) {
114
- return {
115
- isValid: false,
116
- error: `Path must be a regular file, not a ${stats.isDirectory() ? 'directory' : 'special file'}`,
117
- resolvedPath: null
118
- };
119
- }
120
-
121
- // CRITICAL: Verify file ownership (CLAUDE.md requirement)
122
- // Prevent other users from planting malicious files
123
- // SECURITY: Fail-secure on platforms where getuid is unavailable (#131)
124
- const currentUserId = process.getuid ? process.getuid() : null;
125
- if (currentUserId === null) {
126
- return {
127
- isValid: false,
128
- error: 'Security validation failed: unable to verify file ownership on this platform',
129
- resolvedPath: null
130
- };
131
- }
132
- if (stats.uid !== currentUserId) {
133
- return {
134
- isValid: false,
135
- error: 'Security validation failed: file not owned by current user',
136
- resolvedPath: null
137
- };
138
- }
139
-
140
- // Check if file is readable
141
- try {
142
- fs.accessSync(resolvedPath, fs.constants.R_OK);
143
- } catch (err) {
144
- return {
145
- isValid: false,
146
- error: `File is not readable: ${err.message}`,
147
- resolvedPath: null
148
- };
149
- }
150
-
151
- // All checks passed
152
- return {
153
- isValid: true,
154
- error: null,
155
- resolvedPath
156
- };
157
-
158
- } catch (err) {
159
- // Unexpected error during validation
160
- return {
161
- isValid: false,
162
- error: `Unexpected validation error: ${err.message}`,
163
- resolvedPath: null
164
- };
165
- }
166
- }
167
-
168
- /**
169
- * Verify file size is within acceptable limits for music files
170
- *
171
- * @param {string} filePath - Path to file (must already be validated with isPathSafe)
172
- * @param {number} maxSizeBytes - Maximum file size in bytes (default: 50MB)
173
- * @returns {Object} { isValid: boolean, error: string|null, sizeBytes: number }
174
- */
175
- export function validateFileSize(filePath, maxSizeBytes = 50 * 1024 * 1024) {
176
- try {
177
- if (!fs.existsSync(filePath)) {
178
- return {
179
- isValid: false,
180
- error: 'File does not exist',
181
- sizeBytes: 0
182
- };
183
- }
184
-
185
- const stats = fs.statSync(filePath);
186
- const sizeBytes = stats.size;
187
-
188
- if (sizeBytes > maxSizeBytes) {
189
- const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
190
- const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(2);
191
- return {
192
- isValid: false,
193
- error: `File size (${sizeMB}MB) exceeds maximum (${maxMB}MB)`,
194
- sizeBytes
195
- };
196
- }
197
-
198
- if (sizeBytes === 0) {
199
- return {
200
- isValid: false,
201
- error: 'File is empty',
202
- sizeBytes
203
- };
204
- }
205
-
206
- return {
207
- isValid: true,
208
- error: null,
209
- sizeBytes
210
- };
211
-
212
- } catch (err) {
213
- return {
214
- isValid: false,
215
- error: `Error checking file size: ${err.message}`,
216
- sizeBytes: 0
217
- };
218
- }
219
- }
220
-
221
- /**
222
- * Get secure temp directory for audio file operations
223
- * Uses XDG_RUNTIME_DIR if available (follows CLAUDE.md requirement)
224
- * Falls back to user-specific /tmp directory
225
- *
226
- * @param {string} prefix - Directory name prefix (default: 'agentvibes-music')
227
- * @returns {string} Secure temp directory path
228
- */
229
- export function getSecureTempDir(prefix = 'agentvibes-music') {
230
- const xdgRuntime = process.env.XDG_RUNTIME_DIR;
231
-
232
- if (xdgRuntime && fs.existsSync(xdgRuntime)) {
233
- return path.join(xdgRuntime, `${prefix}-${process.pid}`);
234
- }
235
-
236
- // Fallback to user-specific /tmp
237
- const userTmp = path.join(os.tmpdir(), `${prefix}-${process.env.USER || 'user'}`);
238
- return userTmp;
239
- }
240
-
241
- /**
242
- * Create secure temp directory with restrictive permissions
243
- *
244
- * @param {string} dirPath - Directory path to create
245
- * @returns {Object} { success: boolean, error: string|null, dirPath: string|null }
246
- */
247
- export function createSecureTempDir(dirPath) {
248
- try {
249
- // Create directory if it doesn't exist
250
- if (!fs.existsSync(dirPath)) {
251
- fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
252
- } else {
253
- // If exists, verify permissions are restrictive
254
- const stats = fs.statSync(dirPath);
255
- // Check if world-readable (mode & 0o077 should be 0 for secure)
256
- if ((stats.mode & 0o077) !== 0) {
257
- return {
258
- success: false,
259
- error: 'Temp directory has insecure permissions',
260
- dirPath: null
261
- };
262
- }
263
- }
264
-
265
- return {
266
- success: true,
267
- error: null,
268
- dirPath
269
- };
270
-
271
- } catch (err) {
272
- return {
273
- success: false,
274
- error: `Failed to create secure temp directory: ${err.message}`,
275
- dirPath: null
276
- };
277
- }
278
- }
279
-
280
- export default {
281
- isPathSafe,
282
- validateFileSize,
283
- getSecureTempDir,
284
- createSecureTempDir
285
- };
1
+ /**
2
+ * Music File Validator - Security-Critical Path and File Validation
3
+ * Story 4.1: Path Validation and Security Hardening
4
+ *
5
+ * CRITICAL: All functions enforce CLAUDE.md security requirements:
6
+ * - Use path.resolve() for all path operations
7
+ * - Verify file ownership before processing external files
8
+ * - Prevent path traversal attacks via traversal patterns, symlinks, etc.
9
+ * - Validate file exists, is readable, and owned by current user
10
+ *
11
+ * @module music-file-validator
12
+ * @requires fs
13
+ * @requires path
14
+ * @requires os
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import os from 'node:os';
20
+
21
+ /**
22
+ * Story 4.1: Validate that a file path is safe and within user's home directory
23
+ *
24
+ * Security checks:
25
+ * 1. Resolves path to absolute form (prevents ../ tricks)
26
+ * 2. Validates path is within home directory
27
+ * 3. Rejects symlinks pointing outside home
28
+ * 4. Verifies file ownership matches current user
29
+ * 5. Checks file is readable
30
+ *
31
+ * @param {string} userPath - Path provided by user (absolute or relative)
32
+ * @param {string} userHomeDir - User's home directory (default: process.env.HOME)
33
+ * @returns {Object} { isValid: boolean, error: string|null, resolvedPath: string|null }
34
+ * @throws {Error} If path validation encounters unexpected errors
35
+ */
36
+ export function isPathSafe(userPath, userHomeDir = null) {
37
+ try {
38
+ // Parameter validation
39
+ if (!userPath || typeof userPath !== 'string') {
40
+ return {
41
+ isValid: false,
42
+ error: 'Path must be a non-empty string',
43
+ resolvedPath: null
44
+ };
45
+ }
46
+
47
+ // Use provided home dir or default to environment
48
+ const homeDir = userHomeDir || (process.env.HOME || process.env.USERPROFILE);
49
+ if (!homeDir) {
50
+ return {
51
+ isValid: false,
52
+ error: 'Unable to determine home directory',
53
+ resolvedPath: null
54
+ };
55
+ }
56
+
57
+ // CRITICAL: Use path.resolve() to get absolute path (CLAUDE.md requirement)
58
+ // This prevents directory traversal attacks like "../../etc/passwd"
59
+ const resolvedPath = path.resolve(userPath);
60
+ const resolvedHome = path.resolve(homeDir);
61
+
62
+ // Check if path is within home directory
63
+ // Must either match exactly OR start with home + separator (prevents /home/user2 bypass)
64
+ const isWithinHome = resolvedPath === resolvedHome ||
65
+ resolvedPath.startsWith(resolvedHome + path.sep);
66
+
67
+ if (!isWithinHome) {
68
+ return {
69
+ isValid: false,
70
+ error: `Security validation failed: path must be within home directory (${resolvedHome})`,
71
+ resolvedPath: null
72
+ };
73
+ }
74
+
75
+ // SECURITY: Use lstatSync first to detect symlinks before following them (#131)
76
+ let lstats;
77
+ try {
78
+ lstats = fs.lstatSync(resolvedPath);
79
+ } catch (err) {
80
+ if (err.code === 'ENOENT') {
81
+ return { isValid: false, error: `File not found: ${userPath}`, resolvedPath: null };
82
+ }
83
+ return { isValid: false, error: `Cannot access file: ${err.message}`, resolvedPath: null };
84
+ }
85
+
86
+ // Check if symlink - if so, verify target is also within home directory
87
+ if (lstats.isSymbolicLink()) {
88
+ try {
89
+ const targetPath = fs.realpathSync(resolvedPath);
90
+ const isTargetWithinHome = targetPath === resolvedHome ||
91
+ targetPath.startsWith(resolvedHome + path.sep);
92
+
93
+ if (!isTargetWithinHome) {
94
+ return {
95
+ isValid: false,
96
+ error: 'Security validation failed: symlink target must be within home directory',
97
+ resolvedPath: null
98
+ };
99
+ }
100
+ } catch (err) {
101
+ return {
102
+ isValid: false,
103
+ error: `Failed to resolve symlink: ${err.message}`,
104
+ resolvedPath: null
105
+ };
106
+ }
107
+ }
108
+
109
+ // Get file stats (follows symlinks for regular file check)
110
+ const stats = fs.statSync(resolvedPath);
111
+
112
+ // Check if it's a regular file (not directory, special file, etc)
113
+ if (!stats.isFile()) {
114
+ return {
115
+ isValid: false,
116
+ error: `Path must be a regular file, not a ${stats.isDirectory() ? 'directory' : 'special file'}`,
117
+ resolvedPath: null
118
+ };
119
+ }
120
+
121
+ // CRITICAL: Verify file ownership (CLAUDE.md requirement)
122
+ // Prevent other users from planting malicious files
123
+ // SECURITY: Fail-secure on platforms where getuid is unavailable (#131)
124
+ const currentUserId = process.getuid ? process.getuid() : null;
125
+ if (currentUserId === null) {
126
+ return {
127
+ isValid: false,
128
+ error: 'Security validation failed: unable to verify file ownership on this platform',
129
+ resolvedPath: null
130
+ };
131
+ }
132
+ if (stats.uid !== currentUserId) {
133
+ return {
134
+ isValid: false,
135
+ error: 'Security validation failed: file not owned by current user',
136
+ resolvedPath: null
137
+ };
138
+ }
139
+
140
+ // Check if file is readable
141
+ try {
142
+ fs.accessSync(resolvedPath, fs.constants.R_OK);
143
+ } catch (err) {
144
+ return {
145
+ isValid: false,
146
+ error: `File is not readable: ${err.message}`,
147
+ resolvedPath: null
148
+ };
149
+ }
150
+
151
+ // All checks passed
152
+ return {
153
+ isValid: true,
154
+ error: null,
155
+ resolvedPath
156
+ };
157
+
158
+ } catch (err) {
159
+ // Unexpected error during validation
160
+ return {
161
+ isValid: false,
162
+ error: `Unexpected validation error: ${err.message}`,
163
+ resolvedPath: null
164
+ };
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Verify file size is within acceptable limits for music files
170
+ *
171
+ * @param {string} filePath - Path to file (must already be validated with isPathSafe)
172
+ * @param {number} maxSizeBytes - Maximum file size in bytes (default: 50MB)
173
+ * @returns {Object} { isValid: boolean, error: string|null, sizeBytes: number }
174
+ */
175
+ export function validateFileSize(filePath, maxSizeBytes = 50 * 1024 * 1024) {
176
+ try {
177
+ if (!fs.existsSync(filePath)) {
178
+ return {
179
+ isValid: false,
180
+ error: 'File does not exist',
181
+ sizeBytes: 0
182
+ };
183
+ }
184
+
185
+ const stats = fs.statSync(filePath);
186
+ const sizeBytes = stats.size;
187
+
188
+ if (sizeBytes > maxSizeBytes) {
189
+ const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
190
+ const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(2);
191
+ return {
192
+ isValid: false,
193
+ error: `File size (${sizeMB}MB) exceeds maximum (${maxMB}MB)`,
194
+ sizeBytes
195
+ };
196
+ }
197
+
198
+ if (sizeBytes === 0) {
199
+ return {
200
+ isValid: false,
201
+ error: 'File is empty',
202
+ sizeBytes
203
+ };
204
+ }
205
+
206
+ return {
207
+ isValid: true,
208
+ error: null,
209
+ sizeBytes
210
+ };
211
+
212
+ } catch (err) {
213
+ return {
214
+ isValid: false,
215
+ error: `Error checking file size: ${err.message}`,
216
+ sizeBytes: 0
217
+ };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Get secure temp directory for audio file operations
223
+ * Uses XDG_RUNTIME_DIR if available (follows CLAUDE.md requirement)
224
+ * Falls back to user-specific /tmp directory
225
+ *
226
+ * @param {string} prefix - Directory name prefix (default: 'agentvibes-music')
227
+ * @returns {string} Secure temp directory path
228
+ */
229
+ export function getSecureTempDir(prefix = 'agentvibes-music') {
230
+ const xdgRuntime = process.env.XDG_RUNTIME_DIR;
231
+
232
+ if (xdgRuntime && fs.existsSync(xdgRuntime)) {
233
+ return path.join(xdgRuntime, `${prefix}-${process.pid}`);
234
+ }
235
+
236
+ // Fallback to user-specific /tmp
237
+ const userTmp = path.join(os.tmpdir(), `${prefix}-${process.env.USER || 'user'}`);
238
+ return userTmp;
239
+ }
240
+
241
+ /**
242
+ * Create secure temp directory with restrictive permissions
243
+ *
244
+ * @param {string} dirPath - Directory path to create
245
+ * @returns {Object} { success: boolean, error: string|null, dirPath: string|null }
246
+ */
247
+ export function createSecureTempDir(dirPath) {
248
+ try {
249
+ // Create directory if it doesn't exist
250
+ if (!fs.existsSync(dirPath)) {
251
+ fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
252
+ } else {
253
+ // If exists, verify permissions are restrictive
254
+ const stats = fs.statSync(dirPath);
255
+ // Check if world-readable (mode & 0o077 should be 0 for secure)
256
+ if ((stats.mode & 0o077) !== 0) {
257
+ return {
258
+ success: false,
259
+ error: 'Temp directory has insecure permissions',
260
+ dirPath: null
261
+ };
262
+ }
263
+ }
264
+
265
+ return {
266
+ success: true,
267
+ error: null,
268
+ dirPath
269
+ };
270
+
271
+ } catch (err) {
272
+ return {
273
+ success: false,
274
+ error: `Failed to create secure temp directory: ${err.message}`,
275
+ dirPath: null
276
+ };
277
+ }
278
+ }
279
+
280
+ export default {
281
+ isPathSafe,
282
+ validateFileSize,
283
+ getSecureTempDir,
284
+ createSecureTempDir
285
+ };