@yancyyu/openhermit 1.6.29 → 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.
- package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DsQt4FHy.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-CrWocIjq.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-B6d8ysWi.js} +1 -1
- package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-DAIYCFP8.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
- package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DZou1667.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
- package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-DY3qbzqj.js} +1 -1
- package/dist-renderer/assets/{index-BowUl0Jb.js → index-BlOrAXp3.js} +542 -532
- package/dist-renderer/assets/{index-6m1ZAymG.js → index-Bs27J5gB.js} +1 -1
- package/dist-renderer/assets/{index-Dp3kJTEe.js → index-C8B_nKOF.js} +1 -1
- package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
- package/dist-renderer/assets/{index-TOpt_T7A.js → index-DLKyDr4T.js} +1 -1
- package/dist-renderer/assets/{index-qNBNjW4K.js → index-Dhsk3_DD.js} +1 -1
- package/dist-renderer/assets/{index-vAykq1H1.js → index-GpUvV2xs.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
- package/dist-renderer/assets/{layout-DNANbrI4.js → layout-BZLlNmbr.js} +1 -1
- package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-qz6v45xy.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-CGKpOUF2.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +4 -1
- package/src/main/ipc/extensions.ts +353 -0
- package/src/main/server.ts +209 -6
- package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
- package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
- package/src/main/services/extensions/install/McpInstallService.ts +407 -0
- package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
- package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
- package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
- package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
- package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
- package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
- package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
- package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
- package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
- package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
- package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
- package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
- package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
- package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
- package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
- package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
- package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
- package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
- package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
- package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
- package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
- package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
- package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
- package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
- package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
- package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
- package/src/main/services/team/cliFlavor.ts +54 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +3 -0
- package/src/main/utils/atomicWrite.ts +72 -0
- package/src/main/utils/childProcess.ts +554 -0
- package/src/main/utils/cliEnv.ts +54 -0
- package/src/main/utils/cliPathMerge.ts +97 -0
- package/src/main/utils/pathDecoder.ts +664 -0
- package/src/main/utils/pathValidation.ts +432 -0
- package/src/main/utils/shellEnv.ts +331 -0
- package/src/renderer/api/httpClient.ts +61 -0
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
- package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
- package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
- package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
- package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
- package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
- package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
- package/src/renderer/components/team/HarnessSelect.tsx +71 -0
- package/src/renderer/components/team/TeamDetailView.tsx +35 -0
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
- package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
- package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
- package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
- package/src/renderer/store/slices/extensionsSlice.ts +42 -107
- package/src/renderer/store/slices/teamSlice.ts +8 -2
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/extensions/index.ts +1 -0
- package/src/shared/types/extensions/mcp.ts +2 -0
- package/src/shared/types/extensions/plugin.ts +2 -1
- package/src/shared/types/extensions/skill.ts +7 -0
- package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
- package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
- package/dist-renderer/assets/index-BhellmRb.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
- package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
- package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
- package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
- package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
- package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
- package/src/features/recent-projects/main/index.ts +0 -3
- package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
- package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
- package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
- package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
- package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
- package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
- 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
|
+
}
|