Skip to content

Commit b3644b6

Browse files
authored
fix(skills): inject active skill roots (#1411)
1 parent 7625da7 commit b3644b6

4 files changed

Lines changed: 337 additions & 56 deletions

File tree

src/main/presenter/skillPresenter/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,12 @@ export class SkillPresenter implements ISkillPresenter {
382382
const scripts = (await this.listSkillScripts(metadata.name)).filter((script) => script.enabled)
383383
const lines = [
384384
'## DeepChat Runtime Context',
385-
'- Skill root: available via `SKILL_ROOT` and `DEEPCHAT_SKILL_ROOT`.',
386-
'- `skill_run` executes from the current session workdir when available.',
387-
'- Recommended base_directory: `<skill_root>`'
385+
`- Skill root: \`${metadata.skillRoot}\`.`,
386+
'- Relative paths mentioned by this skill are relative to the skill root unless stated otherwise.',
387+
'- When this skill needs script execution, prefer `skill_run` over `exec`.'
388388
]
389389

390390
if (scripts.length > 0) {
391-
lines.push('- Preferred execution tool: `skill_run`')
392391
lines.push('- Bundled runnable scripts:')
393392
lines.push(
394393
...scripts.map((script) => {
@@ -401,7 +400,6 @@ export class SkillPresenter implements ISkillPresenter {
401400
}
402401

403402
lines.push('- Do not guess script paths or change directories to locate skill files.')
404-
lines.push('- Prefer `skill_run` over inline `python -c`, `node -e`, or ad-hoc shell snippets.')
405403

406404
return lines.join('\n')
407405
}

src/main/presenter/toolPresenter/agentTools/agentToolManager.ts

Lines changed: 144 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -664,10 +664,6 @@ export class AgentToolManager {
664664
return this.callProcessTool(toolName, args, conversationId)
665665
}
666666

667-
if (!this.fileSystemHandler) {
668-
throw new Error('FileSystem handler not initialized')
669-
}
670-
671667
const schema = this.fileSystemSchemas[toolName as keyof typeof this.fileSystemSchemas]
672668
if (!schema) {
673669
throw new Error(`No schema found for FileSystem tool: ${toolName}`)
@@ -680,6 +676,50 @@ export class AgentToolManager {
680676

681677
const parsedArgs = validationResult.data
682678

679+
if (toolName === 'exec') {
680+
if (!this.bashHandler) {
681+
throw new Error('Bash handler not initialized for exec tool')
682+
}
683+
const execArgs = parsedArgs as {
684+
command: string
685+
timeoutMs?: number
686+
description?: string
687+
cwd?: string
688+
background?: boolean
689+
yieldMs?: number
690+
}
691+
const commandResult = await this.bashHandler.executeCommand(
692+
{
693+
command: execArgs.command,
694+
timeout: execArgs.timeoutMs,
695+
description: execArgs.description ?? 'Execute command',
696+
cwd: execArgs.cwd,
697+
background: execArgs.background,
698+
yieldMs: execArgs.yieldMs
699+
},
700+
{
701+
conversationId
702+
}
703+
)
704+
const content =
705+
typeof commandResult.output === 'string'
706+
? commandResult.output
707+
: JSON.stringify(commandResult.output)
708+
return {
709+
content,
710+
rawData: {
711+
content,
712+
rtkApplied: commandResult.rtkApplied,
713+
rtkMode: commandResult.rtkMode,
714+
rtkFallbackReason: commandResult.rtkFallbackReason
715+
}
716+
}
717+
}
718+
719+
if (!this.fileSystemHandler) {
720+
throw new Error('FileSystem handler not initialized')
721+
}
722+
683723
// Get dynamic workdir from conversation settings
684724
let dynamicWorkdir: string | null = null
685725
if (conversationId) {
@@ -698,7 +738,7 @@ export class AgentToolManager {
698738
const baseDirectory = explicitBaseDirectory ?? dynamicWorkdir ?? undefined
699739
const workspaceRoot =
700740
dynamicWorkdir ?? this.agentWorkspacePath ?? this.getDefaultAgentWorkspacePath()
701-
const allowedDirectories = this.buildAllowedDirectories(workspaceRoot, conversationId)
741+
const allowedDirectories = await this.buildAllowedDirectories(workspaceRoot, conversationId)
702742
const fileSystemHandler = new AgentFileSystemHandler(allowedDirectories, { conversationId })
703743

704744
try {
@@ -868,45 +908,6 @@ export class AgentToolManager {
868908
)
869909
}
870910
}
871-
case 'exec': {
872-
if (!this.bashHandler) {
873-
throw new Error('Bash handler not initialized for exec tool')
874-
}
875-
const execArgs = parsedArgs as {
876-
command: string
877-
timeoutMs?: number
878-
description?: string
879-
cwd?: string
880-
background?: boolean
881-
yieldMs?: number
882-
}
883-
const commandResult = await this.bashHandler.executeCommand(
884-
{
885-
command: execArgs.command,
886-
timeout: execArgs.timeoutMs,
887-
description: execArgs.description ?? 'Execute command',
888-
cwd: execArgs.cwd,
889-
background: execArgs.background,
890-
yieldMs: execArgs.yieldMs
891-
},
892-
{
893-
conversationId
894-
}
895-
)
896-
const content =
897-
typeof commandResult.output === 'string'
898-
? commandResult.output
899-
: JSON.stringify(commandResult.output)
900-
return {
901-
content,
902-
rawData: {
903-
content,
904-
rtkApplied: commandResult.rtkApplied,
905-
rtkMode: commandResult.rtkMode,
906-
rtkFallbackReason: commandResult.rtkFallbackReason
907-
}
908-
}
909-
}
910911
default:
911912
throw new Error(`Unknown FileSystem tool: ${toolName}`)
912913
}
@@ -937,7 +938,10 @@ export class AgentToolManager {
937938
}
938939
}
939940

940-
private buildAllowedDirectories(workspacePath: string, conversationId?: string): string[] {
941+
private async buildAllowedDirectories(
942+
workspacePath: string,
943+
conversationId?: string
944+
): Promise<string[]> {
941945
const ordered: string[] = []
942946
const seen = new Set<string>()
943947
const addPath = (value?: string | null) => {
@@ -951,7 +955,14 @@ export class AgentToolManager {
951955

952956
addPath(workspacePath)
953957
addPath(this.agentWorkspacePath)
954-
addPath(this.configPresenter.getSkillsPath())
958+
959+
if (conversationId) {
960+
const activeSkillRoots = await this.resolveActiveSkillRoots(conversationId)
961+
for (const skillRoot of activeSkillRoots) {
962+
addPath(skillRoot)
963+
}
964+
}
965+
955966
addPath(path.join(app.getPath('home'), '.deepchat'))
956967
addPath(app.getPath('temp'))
957968

@@ -965,6 +976,86 @@ export class AgentToolManager {
965976
return ordered
966977
}
967978

979+
private async resolveActiveSkillRoots(conversationId: string): Promise<string[]> {
980+
const skillPresenter = this.getSkillPresenter()
981+
if (!skillPresenter?.getActiveSkills || !skillPresenter?.getMetadataList) {
982+
return []
983+
}
984+
985+
let activeSkillNames: string[]
986+
let metadataList: Awaited<ReturnType<typeof skillPresenter.getMetadataList>>
987+
988+
try {
989+
;[activeSkillNames, metadataList] = await Promise.all([
990+
skillPresenter.getActiveSkills(conversationId),
991+
skillPresenter.getMetadataList()
992+
])
993+
} catch (error) {
994+
logger.warn('[AgentToolManager] Failed to resolve active skill roots', {
995+
conversationId,
996+
error
997+
})
998+
return []
999+
}
1000+
1001+
const metadataByName = new Map(
1002+
metadataList
1003+
.filter((metadata) => metadata?.name?.trim())
1004+
.map((metadata) => [metadata.name.trim(), metadata])
1005+
)
1006+
const roots: string[] = []
1007+
1008+
for (const skillName of activeSkillNames) {
1009+
const normalizedSkillName = skillName?.trim()
1010+
if (!normalizedSkillName) {
1011+
continue
1012+
}
1013+
1014+
const metadata = metadataByName.get(normalizedSkillName)
1015+
if (!metadata) {
1016+
logger.warn(
1017+
'[AgentToolManager] Active skill metadata missing during file allowlist build',
1018+
{
1019+
conversationId,
1020+
skillName: normalizedSkillName
1021+
}
1022+
)
1023+
continue
1024+
}
1025+
1026+
const skillRoot = metadata.skillRoot?.trim()
1027+
if (!skillRoot) {
1028+
logger.warn('[AgentToolManager] Active skill root missing during file allowlist build', {
1029+
conversationId,
1030+
skillName: normalizedSkillName
1031+
})
1032+
continue
1033+
}
1034+
1035+
try {
1036+
const resolvedRoot = path.resolve(skillRoot)
1037+
if (!fs.existsSync(resolvedRoot) || !fs.statSync(resolvedRoot).isDirectory()) {
1038+
logger.warn('[AgentToolManager] Active skill root is not a directory', {
1039+
conversationId,
1040+
skillName: normalizedSkillName,
1041+
skillRoot: resolvedRoot
1042+
})
1043+
continue
1044+
}
1045+
roots.push(resolvedRoot)
1046+
} catch (error) {
1047+
logger.warn('[AgentToolManager] Failed to normalize active skill root', {
1048+
conversationId,
1049+
skillName: normalizedSkillName,
1050+
skillRoot,
1051+
error
1052+
})
1053+
}
1054+
}
1055+
1056+
return roots
1057+
}
1058+
9681059
private async resolveValidatedReadPath(
9691060
fileSystemHandler: AgentFileSystemHandler,
9701061
requestedPath: string,
@@ -1470,8 +1561,13 @@ export class AgentToolManager {
14701561

14711562
const workspaceRoot =
14721563
dynamicWorkdir ?? this.agentWorkspacePath ?? this.getDefaultAgentWorkspacePath()
1473-
const allowedDirectories = this.buildAllowedDirectories(workspaceRoot, conversationId)
1564+
const allowedDirectories = await this.buildAllowedDirectories(workspaceRoot, conversationId)
14741565
const fileSystemHandler = new AgentFileSystemHandler(allowedDirectories, { conversationId })
1566+
const explicitBaseDirectory =
1567+
typeof args.base_directory === 'string' && args.base_directory.trim().length > 0
1568+
? args.base_directory
1569+
: undefined
1570+
const baseDirectory = explicitBaseDirectory ?? dynamicWorkdir ?? undefined
14751571

14761572
// Collect target paths
14771573
const targets = this.collectWriteTargets(toolName, args)
@@ -1485,7 +1581,7 @@ export class AgentToolManager {
14851581
// Check each path
14861582
const denied: string[] = []
14871583
for (const target of targets) {
1488-
const resolved = fileSystemHandler.resolvePath(target, undefined)
1584+
const resolved = fileSystemHandler.resolvePath(target, baseDirectory)
14891585
if (!fileSystemHandler.isPathAllowedAbsolute(resolved)) {
14901586
denied.push(target)
14911587
}

test/main/presenter/skillPresenter/skillPresenter.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,14 @@ describe('SkillPresenter', () => {
358358
expect(content).toBeTruthy()
359359
expect(content?.name).toBe('test-skill')
360360
expect(content?.content).toContain('Skill content')
361-
expect(content?.content).toContain('Skill root: resolved server-side by `skill_run`.')
362-
expect(content?.content).toContain('Recommended base_directory: `<skill_root>`')
363-
expect(content?.content).not.toContain('/mock/home/.deepchat/skills/test-skill')
361+
expect(content?.content).toContain('Skill root: `')
362+
expect(content?.content).toContain('/.deepchat/skills/test-skill`.')
363+
expect(content?.content).toContain(
364+
'Relative paths mentioned by this skill are relative to the skill root unless stated otherwise.'
365+
)
366+
expect(content?.content).toContain(
367+
'When this skill needs script execution, prefer `skill_run` over `exec`.'
368+
)
364369
})
365370

366371
it('should return null for non-existent skill', async () => {

0 commit comments

Comments
 (0)