mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-24 16:13:44 +00:00
153 lines
4.2 KiB
TypeScript
153 lines
4.2 KiB
TypeScript
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import fsp from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const EXECUTABLE_FILE_EXTENSIONS = new Set([
|
|
'.app',
|
|
'.applescript',
|
|
'.bat',
|
|
'.cmd',
|
|
'.com',
|
|
'.command',
|
|
'.desktop',
|
|
'.dmg',
|
|
'.exe',
|
|
'.hta',
|
|
'.jar',
|
|
'.js',
|
|
'.jse',
|
|
'.lnk',
|
|
'.msi',
|
|
'.msp',
|
|
'.pkg',
|
|
'.ps1',
|
|
'.reg',
|
|
'.scpt',
|
|
'.sh',
|
|
'.url',
|
|
'.vbs',
|
|
'.vbe',
|
|
'.wsf',
|
|
]);
|
|
|
|
export type LocalFileAuthorization =
|
|
| { allowed: true; filePath: string }
|
|
| { allowed: false; reason: 'invalid' | 'missing' | 'outside-roots' };
|
|
|
|
export function isMainRendererSender(
|
|
senderId: number,
|
|
mainRendererId: number | null | undefined
|
|
): boolean {
|
|
return typeof mainRendererId === 'number' && senderId === mainRendererId;
|
|
}
|
|
|
|
function isPathInsideRoot(candidate: string, root: string): boolean {
|
|
const relative = path.relative(root, candidate);
|
|
return (
|
|
relative === '' ||
|
|
(!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Authorize an existing path against real filesystem roots.
|
|
*
|
|
* Both the candidate and roots are resolved through realpath so a symlink
|
|
* stored inside a workspace cannot escape to ~/.ssh, ~/.aws, or another
|
|
* location outside the active Space.
|
|
*/
|
|
export async function authorizeLocalFilePath(
|
|
candidatePath: string,
|
|
allowedRoots: Iterable<string>
|
|
): Promise<LocalFileAuthorization> {
|
|
if (!candidatePath || !path.isAbsolute(candidatePath)) {
|
|
return { allowed: false, reason: 'invalid' };
|
|
}
|
|
|
|
let realCandidate: string;
|
|
try {
|
|
realCandidate = await fsp.realpath(candidatePath);
|
|
} catch {
|
|
return { allowed: false, reason: 'missing' };
|
|
}
|
|
|
|
for (const root of allowedRoots) {
|
|
if (!root || !path.isAbsolute(root)) continue;
|
|
try {
|
|
const realRoot = await fsp.realpath(root);
|
|
if (isPathInsideRoot(realCandidate, realRoot)) {
|
|
return { allowed: true, filePath: realCandidate };
|
|
}
|
|
} catch {
|
|
// A stale or missing workspace binding grants no access.
|
|
}
|
|
}
|
|
|
|
return { allowed: false, reason: 'outside-roots' };
|
|
}
|
|
|
|
/**
|
|
* Authorize a preview path from the renderer.
|
|
*
|
|
* Agent/file events may carry a workspace-relative path. Resolve those paths
|
|
* only against roots that the main renderer has explicitly registered for the
|
|
* active Space; never against process.cwd(), HOME, or application asset roots.
|
|
* Absolute paths retain the stricter realpath containment check above.
|
|
*/
|
|
export async function authorizeLocalPreviewPath(
|
|
candidatePath: string,
|
|
activeWorkspaceRoots: Iterable<string>,
|
|
additionalAbsoluteRoots: Iterable<string> = []
|
|
): Promise<LocalFileAuthorization> {
|
|
if (!candidatePath) return { allowed: false, reason: 'invalid' };
|
|
|
|
if (path.isAbsolute(candidatePath)) {
|
|
return authorizeLocalFilePath(candidatePath, [
|
|
...activeWorkspaceRoots,
|
|
...additionalAbsoluteRoots,
|
|
]);
|
|
}
|
|
|
|
const roots = [...activeWorkspaceRoots].filter(
|
|
(root) => root && path.isAbsolute(root)
|
|
);
|
|
if (roots.length === 0) return { allowed: false, reason: 'invalid' };
|
|
|
|
let sawMissing = false;
|
|
for (const root of roots) {
|
|
const authorization = await authorizeLocalFilePath(
|
|
path.resolve(root, candidatePath),
|
|
[root]
|
|
);
|
|
if (authorization.allowed) return authorization;
|
|
if (authorization.reason === 'missing') sawMissing = true;
|
|
}
|
|
|
|
return {
|
|
allowed: false,
|
|
reason: sawMissing ? 'missing' : 'outside-roots',
|
|
};
|
|
}
|
|
|
|
export function isExecutableExternalOpenPath(
|
|
filePath: string,
|
|
mode = 0
|
|
): boolean {
|
|
return (
|
|
EXECUTABLE_FILE_EXTENSIONS.has(path.extname(filePath).toLowerCase()) ||
|
|
(mode & 0o111) !== 0
|
|
);
|
|
}
|