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
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import com.itangcent.easyapi.core.logging.IdeaLog
* directories. This is the preferred form: the agent does not know the
* user's home directory, so addressing files by name avoids guessing
* absolute paths.
* 2. An absolute path inside a tracked rule directory — validated by
* [ToolContext.ruleFileResolver.resolve]. Accepted as a fallback.
* 2. A path inside a tracked rule directory — validated by
* [ToolContext.ruleFileResolver.resolve]. Relative paths resolve against
* the project base directory, never the process working directory.
* Accepted as a fallback.
*
* Paths that fall outside the tracked directories are **not** silently read.
* For clearly-foreign paths (the `/etc` directory, `.java`/`.kt` files,
Expand All @@ -35,7 +37,8 @@ class ReadRuleFileTool : AiTool, IdeaLog {
"(e.g. \"security.properties\") or a scope-prefixed name " +
"(\"global:jwt.rules\" / \"project:custom.rules\"); the tool " +
"resolves it against the tracked.easyapi/ rule folders. An " +
"absolute path inside a tracked folder is also accepted. You do " +
"absolute path inside a tracked folder is also accepted; a " +
"relative path resolves against the project root. You do " +
"NOT know the user's home directory — never hard-code " +
"\"/Users/<name>\" or a literal \"~\"; address files by name. " +
"An out-of-scope path asks the user for one-time consent. NOT " +
Expand Down Expand Up @@ -83,7 +86,7 @@ class ReadRuleFileTool : AiTool, IdeaLog {
// One-time consent granted — read the requested path directly.
LOG.info("read_rule_file: user granted consent for $path")
val target = runCatching {
java.nio.file.Paths.get(path).toAbsolutePath().normalize()
ctx.ruleFileResolver.absolutize(java.nio.file.Paths.get(path))
}.getOrNull() ?: return buildOutsideAllowedError(path)
return readPath(target, path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,28 @@ class RuleFileResolver(private val project: Project) {
/**
* Resolve [requestedPath] against the allowed directories.
*
* Relative paths resolve against the project base directory (see
* [absolutize]) before the allow-list check.
*
* @return The resolved, normalised [Path] if it is inside an allowed dir,
* or `null` if access is refused.
*/
fun resolve(requestedPath: String): Path? {
val candidate = Paths.get(requestedPath).toAbsolutePath().normalize()
val candidate = absolutize(Paths.get(requestedPath))
return allowedDirs().firstOrNull { candidate.startsWith(it) }?.let { candidate }
}

/**
* Absolutises [path]. Relative paths resolve against the project base
* directory — never the process working directory — so an agent-supplied
* path like `.easy.api.properties` addresses a file inside the project.
*/
fun absolutize(path: Path): Path {
val base = project.basePath
val resolved = if (!path.isAbsolute && base != null) Paths.get(base).resolve(path) else path
return resolved.toAbsolutePath().normalize()
}

/**
* Resolve a rule file by **name** (optionally scope-prefixed), searching
* the tracked rule directories.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,27 @@ class PerceptionToolsTest : EasyApiLightCodeInsightFixtureTestCase() {
}
}

fun testReadRuleFileRelativePathResolvesAgainstProjectDir() {
// Issue #754: a consented relative path must resolve against the
// project base directory, not the process working directory. The
// project root is not a tracked rule dir here, so the read goes
// through the consent gate; once granted, the project-root file
// is read.
val basePath = project.basePath ?: throw IllegalStateException("project base path required")
val rootRuleFile = java.io.File(basePath, "root-rule-754.properties")
try {
rootRuleFile.writeText("api.name=ProjectRoot")
val gate = com.itangcent.easyapi.core.ai.agent.FakeFileReadConsentGate(grant = true)
val result = runBlocking {
ReadRuleFileTool().execute(mapOf("path" to "root-rule-754.properties"), ctx(readConsents = gate))
}
Assert.assertTrue("result: $result", result is ToolResult.Text)
Assert.assertEquals("api.name=ProjectRoot", (result as ToolResult.Text).value)
} finally {
rootRuleFile.delete()
}
}

// --- GetExistingRulesForKeyTool ---

fun testGetExistingRulesFallsBackToGetAll() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ class RuleFileResolverTest : EasyApiLightCodeInsightFixtureTestCase() {
assertEquals(expected, resolver.resolve(target.toString()))
}

fun testResolveRelativePathAgainstProjectBase() {
// Issue #754: a relative path must resolve against the project base
// directory, never the process working directory.
val base = project.basePath ?: return
val expected = Paths.get(base, ".easyapi", "rule.config")
.toAbsolutePath().normalize()
assertEquals(expected, resolver().resolve(".easyapi/rule.config"))
}

fun testAbsolutizeRelativePathUsesProjectBase() {
val base = project.basePath ?: return
val expected = Paths.get(base, ".easy.api.properties")
.toAbsolutePath().normalize()
assertEquals(expected, resolver().absolutize(Paths.get(".easy.api.properties")))
}

fun testResolveRefusesRelativePathOutsideAllowedDirs() {
// Even project-relative, a path outside the tracked rule dirs is refused.
assertNull(resolver().resolve("src/main/resources/rule.config"))
}

// --- resolveByName ---

fun testResolvesByNameInGlobalDir() {
Expand Down
Loading