@yancyyu/openhermit 1.6.28 → 1.6.30

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 (177) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-DsQt4FHy.js +52 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CrWocIjq.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-B6d8ysWi.js} +1 -1
  5. package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-DAIYCFP8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
  9. package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DZou1667.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
  20. package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
  30. package/dist-renderer/assets/{graph-Enirf-f8.js → graph-DY3qbzqj.js} +1 -1
  31. package/dist-renderer/assets/{index-DY1zqsb6.js → index-BlOrAXp3.js} +551 -537
  32. package/dist-renderer/assets/{index-AjxP_rE_.js → index-Bs27J5gB.js} +1 -1
  33. package/dist-renderer/assets/{index-CtlzGepK.js → index-C8B_nKOF.js} +1 -1
  34. package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
  35. package/dist-renderer/assets/{index-COZPUWJW.js → index-DLKyDr4T.js} +1 -1
  36. package/dist-renderer/assets/{index-DdhqolqE.js → index-Dhsk3_DD.js} +1 -1
  37. package/dist-renderer/assets/{index-ChR1D6ZF.js → index-GpUvV2xs.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
  41. package/dist-renderer/assets/{layout-CPFgj98r.js → layout-BZLlNmbr.js} +1 -1
  42. package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-qz6v45xy.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-CVd5GNDw.js → treemap-GDKQZRPO-CGKpOUF2.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +4 -1
  56. package/src/main/ipc/extensions.ts +353 -0
  57. package/src/main/server.ts +907 -184
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
  59. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
  60. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
  61. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
  62. package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
  63. package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
  64. package/src/main/services/extensions/install/McpInstallService.ts +407 -0
  65. package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
  66. package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
  67. package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
  68. package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
  69. package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
  70. package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
  71. package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
  72. package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
  73. package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
  74. package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
  75. package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
  76. package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
  77. package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
  78. package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
  79. package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
  80. package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
  81. package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
  82. package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
  83. package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
  84. package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
  85. package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
  86. package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
  87. package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
  88. package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
  89. package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
  90. package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
  91. package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
  92. package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
  93. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  94. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  95. package/src/main/services/team/cliFlavor.ts +54 -0
  96. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  97. package/src/main/services/teams-mvp/TaskDispatchService.ts +883 -95
  98. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  99. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  100. package/src/main/services/teams-mvp/index.ts +3 -0
  101. package/src/main/utils/atomicWrite.ts +72 -0
  102. package/src/main/utils/childProcess.ts +554 -0
  103. package/src/main/utils/cliEnv.ts +54 -0
  104. package/src/main/utils/cliPathMerge.ts +97 -0
  105. package/src/main/utils/pathDecoder.ts +664 -0
  106. package/src/main/utils/pathValidation.ts +432 -0
  107. package/src/main/utils/shellEnv.ts +331 -0
  108. package/src/renderer/App.tsx +5 -0
  109. package/src/renderer/api/httpClient.ts +128 -0
  110. package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
  111. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  112. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  113. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  114. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  115. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  116. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  117. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
  118. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  119. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  120. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  121. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  122. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  123. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  124. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  125. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  126. package/src/renderer/components/settings/sections/TaskBusSection.tsx +144 -84
  127. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  128. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  129. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  130. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  131. package/src/renderer/components/team/TeamDetailView.tsx +55 -98
  132. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  133. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
  134. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  135. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  136. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  137. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  138. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  139. package/src/renderer/components/team/kanban/KanbanBoard.tsx +31 -65
  140. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  141. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  142. package/src/renderer/components/team/messages/MessagesPanel.tsx +100 -26
  143. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  144. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  145. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  146. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  147. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  148. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  149. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  150. package/src/renderer/store/slices/teamSlice.ts +67 -25
  151. package/src/renderer/types/tabs.ts +1 -0
  152. package/src/shared/types/api.ts +58 -0
  153. package/src/shared/types/extensions/index.ts +1 -0
  154. package/src/shared/types/extensions/mcp.ts +2 -0
  155. package/src/shared/types/extensions/plugin.ts +2 -1
  156. package/src/shared/types/extensions/skill.ts +7 -0
  157. package/src/shared/types/team.ts +104 -1
  158. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  159. package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
  160. package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
  161. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
  162. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
  163. package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
  164. package/dist-renderer/assets/index-BIOJremZ.css +0 -1
  165. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  166. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  167. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  168. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  169. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  170. package/src/features/recent-projects/main/index.ts +0 -3
  171. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  172. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  173. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  174. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  175. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  176. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  177. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Path Validation Utilities.
3
+ *
4
+ * Provides security sandboxing for file path access to prevent
5
+ * unauthorized access to sensitive system files.
6
+ *
7
+ * Cross-platform: uses path.resolve() for consistent drive-letter
8
+ * handling on Windows (normalizeForCompare, isPathWithinRoot).
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+
14
+ import { getAppDataPath, getClaudeBasePath, getHomeDir } from './pathDecoder';
15
+
16
+ /**
17
+ * Sensitive file patterns that should never be accessible.
18
+ * These are checked against the normalized absolute path.
19
+ */
20
+ const SENSITIVE_PATTERNS: RegExp[] = [
21
+ // SSH keys and config
22
+ /[/\\]\.ssh[/\\]/i,
23
+ // AWS credentials
24
+ /[/\\]\.aws[/\\]/i,
25
+ // GCP credentials
26
+ /[/\\]\.config[/\\]gcloud[/\\]/i,
27
+ // Azure credentials
28
+ /[/\\]\.azure[/\\]/i,
29
+ // Environment files (anywhere in path)
30
+ /[/\\]\.env($|\.)/i,
31
+ // Git credentials
32
+ /[/\\]\.git-credentials$/i,
33
+ /[/\\]\.gitconfig$/i,
34
+ // NPM tokens
35
+ /[/\\]\.npmrc$/i,
36
+ // Docker credentials
37
+ /[/\\]\.docker[/\\]config\.json$/i,
38
+ // Kubernetes config
39
+ /[/\\]\.kube[/\\]config$/i,
40
+ // Password files
41
+ /[/\\]\.password/i,
42
+ /[/\\]\.secret/i,
43
+ // Private keys
44
+ /[/\\]id_rsa$/i,
45
+ /[/\\]id_ed25519$/i,
46
+ /[/\\]id_ecdsa$/i,
47
+ /[/\\][^/\\]*\.pem$/i,
48
+ /[/\\][^/\\]*\.key$/i,
49
+ // System files
50
+ /^\/etc\/passwd$/,
51
+ /^\/etc\/shadow$/,
52
+ // Credentials in filename
53
+ /credentials\.json$/i,
54
+ /secrets\.json$/i,
55
+ /tokens\.json$/i,
56
+ ];
57
+
58
+ /**
59
+ * Result of path validation.
60
+ */
61
+ export interface PathValidationResult {
62
+ valid: boolean;
63
+ error?: string;
64
+ normalizedPath?: string;
65
+ }
66
+
67
+ function normalizeForCompare(input: string, isWindows: boolean): string {
68
+ const normalized = path.resolve(path.normalize(input));
69
+ return isWindows ? normalized.toLowerCase() : normalized;
70
+ }
71
+
72
+ export function isPathWithinRoot(targetPath: string, rootPath: string): boolean {
73
+ const isWindows = process.platform === 'win32';
74
+ const target = normalizeForCompare(targetPath, isWindows);
75
+ const root = normalizeForCompare(rootPath, isWindows);
76
+ const relative = path.relative(root, target);
77
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
78
+ }
79
+
80
+ function resolveRealPathIfExists(inputPath: string): string | null {
81
+ try {
82
+ return fs.realpathSync.native(inputPath);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Checks if a path matches any sensitive file patterns.
90
+ *
91
+ * @param normalizedPath - The normalized absolute path to check
92
+ * @returns true if path matches a sensitive pattern
93
+ */
94
+ export function matchesSensitivePattern(normalizedPath: string): boolean {
95
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(normalizedPath));
96
+ }
97
+
98
+ /**
99
+ * Checks if a path is within allowed directories.
100
+ *
101
+ * Allowed directories:
102
+ * - The project path itself
103
+ * - The ~/.claude directory (for session data)
104
+ * - The app-owned data directory (attachments, task attachments)
105
+ *
106
+ * @param normalizedPath - The normalized absolute path to check
107
+ * @param projectPath - The project root path (can be null for global access)
108
+ * @returns true if path is within allowed directories
109
+ */
110
+ export function isPathWithinAllowedDirectories(
111
+ normalizedPath: string,
112
+ projectPath: string | null
113
+ ): boolean {
114
+ const isWindows = process.platform === 'win32';
115
+ const normalizedTarget = normalizeForCompare(normalizedPath, isWindows);
116
+ const claudeDir = getClaudeBasePath();
117
+ const normalizedClaudeDir = normalizeForCompare(claudeDir, isWindows);
118
+ const appDataDir = getAppDataPath();
119
+ const normalizedAppDataDir = normalizeForCompare(appDataDir, isWindows);
120
+
121
+ // Always allow access to ~/.claude for session data
122
+ if (isPathWithinRoot(normalizedTarget, normalizedClaudeDir)) {
123
+ return true;
124
+ }
125
+
126
+ // Allow app-owned persisted data such as message attachment files.
127
+ if (isPathWithinRoot(normalizedTarget, normalizedAppDataDir)) {
128
+ return true;
129
+ }
130
+
131
+ // If project path provided, allow access within project
132
+ if (projectPath) {
133
+ const normalizedProjectPath = normalizeForCompare(projectPath, isWindows);
134
+ if (isPathWithinRoot(normalizedTarget, normalizedProjectPath)) {
135
+ return true;
136
+ }
137
+ }
138
+
139
+ return false;
140
+ }
141
+
142
+ /**
143
+ * Validates a file path for safe reading.
144
+ *
145
+ * Security checks performed:
146
+ * 1. Path must be absolute
147
+ * 2. Path traversal prevention (no ..)
148
+ * 3. Must be within allowed directories (project, ~/.claude, or app data)
149
+ * 4. Must not match sensitive file patterns
150
+ *
151
+ * @param filePath - The file path to validate
152
+ * @param projectPath - The project root path (can be null for global access)
153
+ * @returns Validation result with normalized path if valid
154
+ */
155
+ export function validateFilePath(
156
+ filePath: string,
157
+ projectPath: string | null
158
+ ): PathValidationResult {
159
+ // Must be a non-empty string
160
+ if (!filePath || typeof filePath !== 'string') {
161
+ return { valid: false, error: 'Invalid file path' };
162
+ }
163
+
164
+ // Expand ~ to home directory
165
+ const expandedPath = filePath.startsWith('~')
166
+ ? path.join(getHomeDir(), filePath.slice(1))
167
+ : filePath;
168
+
169
+ // Must be absolute path
170
+ const normalizedInput = path.normalize(expandedPath);
171
+ if (!path.isAbsolute(normalizedInput)) {
172
+ return { valid: false, error: 'Path must be absolute' };
173
+ }
174
+
175
+ // Normalize and resolve the path to remove traversal segments safely
176
+ const normalizedPath = path.resolve(normalizedInput);
177
+
178
+ // Check against sensitive patterns
179
+ if (matchesSensitivePattern(normalizedPath)) {
180
+ return { valid: false, error: 'Access to sensitive files is not allowed' };
181
+ }
182
+
183
+ // Check if within allowed directories
184
+ if (!isPathWithinAllowedDirectories(normalizedPath, projectPath)) {
185
+ return {
186
+ valid: false,
187
+ error: 'Path is outside allowed directories (project or Claude root)',
188
+ };
189
+ }
190
+
191
+ // If target exists, validate real path containment to prevent symlink escapes.
192
+ const realTargetPath = resolveRealPathIfExists(normalizedPath);
193
+ if (realTargetPath) {
194
+ const isWindows = process.platform === 'win32';
195
+ const normalizedRealTarget = normalizeForCompare(realTargetPath, isWindows);
196
+ if (matchesSensitivePattern(normalizedRealTarget)) {
197
+ return { valid: false, error: 'Access to sensitive files is not allowed' };
198
+ }
199
+
200
+ const realProjectPath = projectPath
201
+ ? (resolveRealPathIfExists(projectPath) ?? path.resolve(path.normalize(projectPath)))
202
+ : null;
203
+
204
+ if (!isPathWithinAllowedDirectories(normalizedRealTarget, realProjectPath)) {
205
+ return {
206
+ valid: false,
207
+ error: 'Path is outside allowed directories (project or Claude root)',
208
+ };
209
+ }
210
+ }
211
+
212
+ return { valid: true, normalizedPath };
213
+ }
214
+
215
+ /**
216
+ * Validates a path for opening when it was explicitly chosen by the user
217
+ * via the system folder picker. Only checks sensitive patterns, not
218
+ * allowed-directories (project / ~/.claude).
219
+ *
220
+ * @param targetPath - The path to open
221
+ * @returns Validation result
222
+ */
223
+ export function validateOpenPathUserSelected(targetPath: string): PathValidationResult {
224
+ if (!targetPath || typeof targetPath !== 'string') {
225
+ return { valid: false, error: 'Invalid path' };
226
+ }
227
+
228
+ const expandedPath = targetPath.startsWith('~')
229
+ ? path.join(getHomeDir(), targetPath.slice(1))
230
+ : targetPath;
231
+
232
+ const normalizedPath = path.resolve(path.normalize(expandedPath));
233
+
234
+ if (!path.isAbsolute(normalizedPath)) {
235
+ return { valid: false, error: 'Path must be absolute' };
236
+ }
237
+
238
+ if (matchesSensitivePattern(normalizedPath)) {
239
+ return { valid: false, error: 'Cannot open sensitive files' };
240
+ }
241
+
242
+ const realTargetPath = resolveRealPathIfExists(normalizedPath);
243
+ if (realTargetPath) {
244
+ const isWindows = process.platform === 'win32';
245
+ const normalizedRealTarget = normalizeForCompare(realTargetPath, isWindows);
246
+ if (matchesSensitivePattern(normalizedRealTarget)) {
247
+ return { valid: false, error: 'Cannot open sensitive files' };
248
+ }
249
+ }
250
+
251
+ return { valid: true, normalizedPath };
252
+ }
253
+
254
+ /**
255
+ * Validates a path for shell:openPath operation.
256
+ * More permissive than file reading - allows opening project directories
257
+ * and Claude data directories.
258
+ *
259
+ * @param targetPath - The path to open
260
+ * @param projectPath - The project root path (can be null)
261
+ * @returns Validation result
262
+ */
263
+ export function validateOpenPath(
264
+ targetPath: string,
265
+ projectPath: string | null
266
+ ): PathValidationResult {
267
+ if (!targetPath || typeof targetPath !== 'string') {
268
+ return { valid: false, error: 'Invalid path' };
269
+ }
270
+
271
+ // Expand ~ to home directory
272
+ const expandedPath = targetPath.startsWith('~')
273
+ ? path.join(getHomeDir(), targetPath.slice(1))
274
+ : targetPath;
275
+
276
+ const normalizedPath = path.resolve(path.normalize(expandedPath));
277
+
278
+ // Must be absolute after expansion
279
+ if (!path.isAbsolute(normalizedPath)) {
280
+ return { valid: false, error: 'Path must be absolute' };
281
+ }
282
+
283
+ // Check against sensitive patterns (still block sensitive files)
284
+ if (matchesSensitivePattern(normalizedPath)) {
285
+ return { valid: false, error: 'Cannot open sensitive files' };
286
+ }
287
+
288
+ // For shell:openPath, we're more permissive but still require
289
+ // the path to be within project or claude directories
290
+ if (!isPathWithinAllowedDirectories(normalizedPath, projectPath)) {
291
+ return {
292
+ valid: false,
293
+ error: 'Path is outside allowed directories',
294
+ };
295
+ }
296
+
297
+ // If target exists, validate real path containment to prevent symlink escapes.
298
+ const realTargetPath = resolveRealPathIfExists(normalizedPath);
299
+ if (realTargetPath) {
300
+ const isWindows = process.platform === 'win32';
301
+ const normalizedRealTarget = normalizeForCompare(realTargetPath, isWindows);
302
+ if (matchesSensitivePattern(normalizedRealTarget)) {
303
+ return { valid: false, error: 'Cannot open sensitive files' };
304
+ }
305
+
306
+ const realProjectPath = projectPath
307
+ ? (resolveRealPathIfExists(projectPath) ?? path.resolve(path.normalize(projectPath)))
308
+ : null;
309
+
310
+ if (!isPathWithinAllowedDirectories(normalizedRealTarget, realProjectPath)) {
311
+ return {
312
+ valid: false,
313
+ error: 'Path is outside allowed directories',
314
+ };
315
+ }
316
+ }
317
+
318
+ return { valid: true, normalizedPath };
319
+ }
320
+
321
+ // =============================================================================
322
+ // Editor-specific validation utilities
323
+ // =============================================================================
324
+
325
+ const MAX_FILENAME_LENGTH = 255;
326
+
327
+ /** Characters forbidden in file/directory names. */
328
+ // eslint-disable-next-line no-control-regex, sonarjs/no-control-regex -- Intentional: validating filenames against control characters
329
+ const INVALID_FILENAME_CHARS = /[\x00-\x1f/\\:*?"<>|]/;
330
+ const WINDOWS_RESERVED_BASENAMES = new Set([
331
+ 'con',
332
+ 'prn',
333
+ 'aux',
334
+ 'nul',
335
+ 'com1',
336
+ 'com2',
337
+ 'com3',
338
+ 'com4',
339
+ 'com5',
340
+ 'com6',
341
+ 'com7',
342
+ 'com8',
343
+ 'com9',
344
+ 'lpt1',
345
+ 'lpt2',
346
+ 'lpt3',
347
+ 'lpt4',
348
+ 'lpt5',
349
+ 'lpt6',
350
+ 'lpt7',
351
+ 'lpt8',
352
+ 'lpt9',
353
+ ]);
354
+
355
+ export function isWindowsReservedFileName(name: string): boolean {
356
+ if (typeof name !== 'string') {
357
+ return false;
358
+ }
359
+
360
+ const normalized = name
361
+ .trim()
362
+ .replace(/[. ]+$/g, '')
363
+ .toLowerCase();
364
+ if (!normalized) {
365
+ return false;
366
+ }
367
+
368
+ const stem = normalized.split('.')[0] ?? normalized;
369
+ return WINDOWS_RESERVED_BASENAMES.has(stem);
370
+ }
371
+
372
+ /**
373
+ * Validates a file or directory name for creation.
374
+ * Prevents path traversal, control chars, and OS-invalid characters.
375
+ */
376
+ export function validateFileName(name: string): PathValidationResult {
377
+ if (!name || typeof name !== 'string') {
378
+ return { valid: false, error: 'Name is required' };
379
+ }
380
+
381
+ const trimmed = name.trim();
382
+ if (trimmed.length === 0) {
383
+ return { valid: false, error: 'Name cannot be empty' };
384
+ }
385
+
386
+ if (trimmed.length > MAX_FILENAME_LENGTH) {
387
+ return { valid: false, error: `Name exceeds ${MAX_FILENAME_LENGTH} characters` };
388
+ }
389
+
390
+ if (trimmed === '.' || trimmed === '..') {
391
+ return { valid: false, error: 'Invalid name' };
392
+ }
393
+
394
+ if (INVALID_FILENAME_CHARS.test(trimmed)) {
395
+ return { valid: false, error: 'Name contains invalid characters' };
396
+ }
397
+
398
+ if (/[. ]$/.test(name)) {
399
+ return { valid: false, error: 'Name cannot end with a space or period' };
400
+ }
401
+
402
+ if (isWindowsReservedFileName(trimmed)) {
403
+ return { valid: false, error: 'Name is reserved on Windows' };
404
+ }
405
+
406
+ return { valid: true };
407
+ }
408
+
409
+ /** Blocked device/pseudo-filesystem path prefixes. */
410
+ const DEVICE_PATH_PREFIXES = ['/dev/', '/proc/', '/sys/'];
411
+ const WINDOWS_DEVICE_PREFIX = '\\\\.\\';
412
+
413
+ /**
414
+ * Returns true if the path points to a device or pseudo-filesystem
415
+ * (/dev/, /proc/, /sys/, \\\\.\\).
416
+ */
417
+ export function isDevicePath(filePath: string): boolean {
418
+ const lower = filePath.toLowerCase();
419
+ if (DEVICE_PATH_PREFIXES.some((prefix) => lower.startsWith(prefix))) {
420
+ return true;
421
+ }
422
+ return filePath.startsWith(WINDOWS_DEVICE_PREFIX);
423
+ }
424
+
425
+ /**
426
+ * Returns true if the path contains a `.git/` segment.
427
+ * Used to block writes to git internals.
428
+ */
429
+ export function isGitInternalPath(filePath: string): boolean {
430
+ const normalized = filePath.replace(/\\/g, '/');
431
+ return normalized.includes('/.git/') || normalized.endsWith('/.git');
432
+ }