Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app/agent/factory/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def browser_agent(options: Chat):
options.project_id,
Agents.browser_agent,
working_directory=working_directory,
user_id=options.skill_config_user_id(),
)
skill_toolkit = message_integration.register_toolkits(skill_toolkit)

Expand Down
1 change: 1 addition & 0 deletions backend/app/agent/factory/developer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ async def developer_agent(options: Chat):
options.project_id,
Agents.developer_agent,
working_directory=working_directory,
user_id=options.skill_config_user_id(),
)
skill_toolkit = message_integration.register_toolkits(skill_toolkit)

Expand Down
1 change: 1 addition & 0 deletions backend/app/agent/factory/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ async def document_agent(options: Chat):
options.project_id,
Agents.document_agent,
working_directory=working_directory,
user_id=options.skill_config_user_id(),
)
skill_toolkit = message_integration.register_toolkits(skill_toolkit)

Expand Down
1 change: 1 addition & 0 deletions backend/app/agent/factory/multi_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def multi_modal_agent(options: Chat):
options.project_id,
Agents.multi_modal_agent,
working_directory=working_directory,
user_id=options.skill_config_user_id(),
)
skill_toolkit = message_integration.register_toolkits(skill_toolkit)
tools = [
Expand Down
1 change: 1 addition & 0 deletions backend/app/agent/factory/social_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ async def social_media_agent(options: Chat):
options.project_id,
Agents.social_media_agent,
working_directory=working_directory,
user_id=options.skill_config_user_id(),
).get_tools(),
# *DiscordToolkit(options.project_id).get_tools(),
# *GoogleSuiteToolkit(options.project_id).get_tools(),
Expand Down
11 changes: 11 additions & 0 deletions backend/app/model/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ def check_model_type(cls, model_type: str):
logger.debug("model_type is invalid")
return model_type

def skill_config_user_id(self) -> str | None:
"""Return the filesystem user_id used by skills-config.

This must stay aligned with frontend `emailToUserId` so
`~/.eigent/<user_id>/skills-config.json` is shared consistently.
"""
user_id = re.sub(
r'[\\/*?:"<>|\s]', "_", self.email.split("@")[0]
).strip(".")
return user_id or None

def get_bun_env(self) -> dict[str, str]:
return (
{"NPM_CONFIG_REGISTRY": self.bun_mirror} if self.bun_mirror else {}
Expand Down
4 changes: 2 additions & 2 deletions backend/app/service/chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2195,7 +2195,7 @@ def _create_coordinator_and_task_agents() -> list[ListenChatAgent]:
options.project_id,
key,
working_directory=working_directory,
user_id=options.user_id,
user_id=options.skill_config_user_id(),
).get_tools(),
],
)
Expand Down Expand Up @@ -2267,7 +2267,7 @@ def _create_new_worker_agent() -> ListenChatAgent:
options.project_id,
Agents.new_worker_agent,
working_directory=working_directory,
user_id=options.user_id,
user_id=options.skill_config_user_id(),
).get_tools(),
],
)
Expand Down
58 changes: 52 additions & 6 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,31 @@ function registerIpcHandlers() {
return null;
}

const normalizePathForCompare = (value: string) =>
process.platform === 'win32' ? value.toLowerCase() : value;

function assertPathUnderSkillsRoot(targetPath: string): string {
const resolvedRoot = path.resolve(SKILLS_ROOT);
const resolvedTarget = path.resolve(targetPath);
const rootCmp = normalizePathForCompare(resolvedRoot);
const targetCmp = normalizePathForCompare(resolvedTarget);
const rootWithSep = rootCmp.endsWith(path.sep)
? rootCmp
: `${rootCmp}${path.sep}`;
if (targetCmp !== rootCmp && !targetCmp.startsWith(rootWithSep)) {
throw new Error('Path is outside skills directory');
}
return resolvedTarget;
}

function resolveSkillDirPath(skillDirName: string): string {
const name = String(skillDirName || '').trim();
if (!name) {
throw new Error('Skill folder name is required');
}
return assertPathUnderSkillsRoot(path.join(SKILLS_ROOT, name));
}

ipcMain.handle('get-skills-dir', async () => {
try {
if (!existsSync(SKILLS_ROOT)) {
Expand Down Expand Up @@ -935,7 +960,7 @@ function registerIpcHandlers() {
'skill-write',
async (_event, skillDirName: string, content: string) => {
try {
const dir = path.join(SKILLS_ROOT, skillDirName);
const dir = resolveSkillDirPath(skillDirName);
await fsp.mkdir(dir, { recursive: true });
await fsp.writeFile(path.join(dir, SKILL_FILE), content, 'utf-8');
return { success: true };
Expand All @@ -948,7 +973,7 @@ function registerIpcHandlers() {

ipcMain.handle('skill-delete', async (_event, skillDirName: string) => {
try {
const dir = path.join(SKILLS_ROOT, skillDirName);
const dir = resolveSkillDirPath(skillDirName);
if (!existsSync(dir)) return { success: true };
await fsp.rm(dir, { recursive: true, force: true });
return { success: true };
Expand All @@ -961,8 +986,10 @@ function registerIpcHandlers() {
ipcMain.handle('skill-read', async (_event, filePath: string) => {
try {
const fullPath = path.isAbsolute(filePath)
? filePath
: path.join(SKILLS_ROOT, filePath, SKILL_FILE);
? assertPathUnderSkillsRoot(filePath)
: assertPathUnderSkillsRoot(
path.join(SKILLS_ROOT, filePath, SKILL_FILE)
);
const content = await fsp.readFile(fullPath, 'utf-8');
return { success: true, content };
} catch (error: any) {
Expand All @@ -973,7 +1000,7 @@ function registerIpcHandlers() {

ipcMain.handle('skill-list-files', async (_event, skillDirName: string) => {
try {
const dir = path.join(SKILLS_ROOT, skillDirName);
const dir = resolveSkillDirPath(skillDirName);
if (!existsSync(dir))
return { success: false, error: 'Skill folder not found', files: [] };
const entries = await fsp.readdir(dir, { withFileTypes: true });
Expand Down Expand Up @@ -1940,9 +1967,28 @@ async function importSkillsFromZip(
// Step 1: Extract zip into temp directory
await fsp.mkdir(tempDir, { recursive: true });
const directory = await unzipper.Open.file(zipPath);
const resolvedTempDir = path.resolve(tempDir);
const comparePath = (value: string) =>
process.platform === 'win32' ? value.toLowerCase() : value;
const resolvedTempDirCmp = comparePath(resolvedTempDir);
const resolvedTempDirWithSep = resolvedTempDirCmp.endsWith(path.sep)
? resolvedTempDirCmp
: `${resolvedTempDirCmp}${path.sep}`;
for (const file of directory.files as any[]) {
if (file.type === 'Directory') continue;
const destPath = path.join(tempDir, file.path);
const normalizedArchivePath = path
.normalize(String(file.path))
.replace(/^([/\\])+/, '');
const destPath = path.join(tempDir, normalizedArchivePath);
const resolvedDestPathCmp = comparePath(path.resolve(destPath));
// Protect against zip-slip (e.g. entries containing ../)
if (
!normalizedArchivePath ||
(resolvedDestPathCmp !== resolvedTempDirCmp &&
!resolvedDestPathCmp.startsWith(resolvedTempDirWithSep))
) {
return { success: false, error: 'Zip archive contains unsafe paths' };
}
const destDir = path.dirname(destPath);
await fsp.mkdir(destDir, { recursive: true });
const content = await file.buffer();
Expand Down