-
-
Notifications
You must be signed in to change notification settings - Fork 494
fix: support symlinked global agent instructions #301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -72,6 +72,8 @@ export interface WorkspaceReadPath { | |||||||||||||||||||||||
| skillRead?: SkillReadResolution; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| type InitialAgentsFileSource = "global" | "workspace"; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export interface OpenWorkspaceInput { | ||||||||||||||||||||||||
| path: string; | ||||||||||||||||||||||||
| mode?: WorkspaceMode; | ||||||||||||||||||||||||
|
|
@@ -412,17 +414,17 @@ export class WorkspaceRegistry { | |||||||||||||||||||||||
| private async loadInitialAgentsFiles(root: string): Promise<LoadedAgentsFile[]> { | ||||||||||||||||||||||||
| const agentDir = resolve(this.config.agentDir); | ||||||||||||||||||||||||
| const resolvedRoot = (await tryRealpath(root)) ?? root; | ||||||||||||||||||||||||
| const resolvedAgentDir = (await tryRealpath(agentDir)) ?? agentDir; | ||||||||||||||||||||||||
| const loadedFiles: LoadedAgentsFile[] = []; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for (const file of loadProjectContextFiles({ cwd: root, agentDir })) { | ||||||||||||||||||||||||
| const path = resolve(file.path); | ||||||||||||||||||||||||
| if (!isInitialAgentsFilePath(path, root, agentDir)) continue; | ||||||||||||||||||||||||
| const source = initialAgentsFileSource(path, root, agentDir); | ||||||||||||||||||||||||
| if (!source) continue; | ||||||||||||||||||||||||
| const content = await readResolvedContextFile( | ||||||||||||||||||||||||
| path, | ||||||||||||||||||||||||
| file.content, | ||||||||||||||||||||||||
| source, | ||||||||||||||||||||||||
| resolvedRoot, | ||||||||||||||||||||||||
| resolvedAgentDir, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| if (content === undefined) continue; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -527,20 +529,30 @@ export function formatAgentsPath(path: string, workspaceRoot: string | undefined | |||||||||||||||||||||||
| return relationship.split(sep).join("/"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function isInitialAgentsFilePath(path: string, root: string, agentDir: string): boolean { | ||||||||||||||||||||||||
| if (isPathInsideRoot(path, agentDir)) return true; | ||||||||||||||||||||||||
| return isPathInsideRoot(path, root) && dirname(path) === root; | ||||||||||||||||||||||||
| function initialAgentsFileSource( | ||||||||||||||||||||||||
| path: string, | ||||||||||||||||||||||||
| root: string, | ||||||||||||||||||||||||
| agentDir: string, | ||||||||||||||||||||||||
| ): InitialAgentsFileSource | undefined { | ||||||||||||||||||||||||
| if (isPathInsideRoot(path, agentDir)) return "global"; | ||||||||||||||||||||||||
| if (isPathInsideRoot(path, root) && dirname(path) === root) return "workspace"; | ||||||||||||||||||||||||
|
Comment on lines
+537
to
+538
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- src/workspaces.ts: relevant definitions and callers ---'
sed -n '390,565p' src/workspaces.ts
printf '%s\n' '--- agentDir definitions and configuration ---'
rg -n -C 3 --glob '!node_modules' 'agentDir|AGENTS\.md|loadInitialAgentsFiles|initialAgentsFileSource|readResolvedContextFile' src
printf '%s\n' '--- relevant tests and configuration defaults ---'
rg -n -C 4 --glob '*.{ts,json,md}' 'agentDir|allowedRoots|initial agents|AGENTS\.md' . | head -n 240Repository: Waishnav/devspace Length of output: 47495 🏁 Script executed: #!/bin/bash
set -e
sed -n '390,565p' src/workspaces.ts
printf '\n--- references ---\n'
rg -n -C 3 --glob '!node_modules' 'agentDir|AGENTS\.md|loadInitialAgentsFiles|initialAgentsFileSource|readResolvedContextFile' srcRepository: Waishnav/devspace Length of output: 33070 Sensitive Data Exposure (CWE-59) Exploitability: Difficult Preserve workspace-root containment when The configuration accepts any non-empty Classify direct workspace-root instruction files as function initialAgentsFileSource(
path: string,
root: string,
agentDir: string,
): InitialAgentsFileSource | undefined {
- if (isPathInsideRoot(path, agentDir)) return "global";
if (isPathInsideRoot(path, root) && dirname(path) === root) return "workspace";
+ if (isPathInsideRoot(path, agentDir)) return "global";
return undefined;
}Add a regression test where 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async function readResolvedContextFile( | ||||||||||||||||||||||||
| path: string, | ||||||||||||||||||||||||
| fallbackContent: string, | ||||||||||||||||||||||||
| source: InitialAgentsFileSource, | ||||||||||||||||||||||||
| root: string, | ||||||||||||||||||||||||
| agentDir: string, | ||||||||||||||||||||||||
| ): Promise<string | undefined> { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| const resolvedPath = await realpath(path); | ||||||||||||||||||||||||
| if (!isInitialAgentsFilePath(resolvedPath, root, agentDir)) return undefined; | ||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||
| source === "workspace" && | ||||||||||||||||||||||||
| (!isPathInsideRoot(resolvedPath, root) || dirname(resolvedPath) !== root) | ||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return await readFile(resolvedPath, "utf8"); | ||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||
| return fallbackContent; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the configured
agentDiris the workspace root or one of its ancestors, this branch classifies the workspace-root instruction asglobalbefore checking whether it is a workspace instruction. A workspaceAGENTS.mdsymlink can then resolve outside the workspace without the intended containment check, exposing an arbitrary readable file as agent instructions. Classify only the actual global instruction slot as global, or prioritize workspace-root classification.How this was verified: Configuration permits any absolute agent directory, and a workspace instruction beneath that directory reaches the unrestricted global branch before its resolved target is checked.