refactor: extend TOML parser for nested tables, inline tables, and JSON arrays
- Add support for nested table headers ([tools.shell], [tools.file_read], etc.)
- Add support for inline tables (capabilities = { General = 1.0, Coding = 0.7 })
- Add support for JSON-style arrays (shell_allowed_executables = ["bash", "sh"])
- Maintain backward compatibility with old CSV formats (deprecated, logged as warnings)
- Refactor buildConfig() to handle new value types (Map, List, Boolean, Number)
- Update docs/sample-config.toml to use clean TOML shapes
- Add comprehensive tests for inline tables, nested sections, JSON arrays, and fallback parsing
- Remove ugly workarounds: capabilities no longer a CSV string, tool flags now in proper nested sections
This commit is contained in:
@@ -33,34 +33,158 @@ object ConfigLoader {
|
|||||||
private fun parseToml(content: String): CorrexConfig {
|
private fun parseToml(content: String): CorrexConfig {
|
||||||
val lines = content.trim().split("\n")
|
val lines = content.trim().split("\n")
|
||||||
var currentSection = ""
|
var currentSection = ""
|
||||||
val sections = mutableMapOf<String, MutableMap<String, String>>()
|
val sections = mutableMapOf<String, MutableMap<String, Any>>()
|
||||||
|
val providers = mutableListOf<MutableMap<String, Any>>()
|
||||||
|
var currentProvider: MutableMap<String, Any>? = null
|
||||||
|
|
||||||
for (line in lines) {
|
for ((lineNum, line) in lines.withIndex()) {
|
||||||
val trimmed = line.trim()
|
val trimmed = line.trim()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
trimmed.isEmpty() || trimmed.startsWith("#") -> {
|
trimmed.isEmpty() || trimmed.startsWith("#") -> {
|
||||||
// Skip empty lines and comments
|
// Skip empty lines and comments
|
||||||
}
|
}
|
||||||
trimmed.startsWith("[") && trimmed.endsWith("]") -> {
|
trimmed == "[[providers]]" -> {
|
||||||
// Parse section headers like [server]
|
// Start a new provider entry
|
||||||
|
if (currentProvider != null) {
|
||||||
|
providers.add(currentProvider)
|
||||||
|
}
|
||||||
|
currentProvider = mutableMapOf()
|
||||||
|
currentSection = ""
|
||||||
|
}
|
||||||
|
trimmed.startsWith("[") && !trimmed.startsWith("[[") && trimmed.endsWith("]") -> {
|
||||||
|
// Parse section headers like [server] or [tools.shell]
|
||||||
|
if (currentProvider != null) {
|
||||||
|
providers.add(currentProvider)
|
||||||
|
currentProvider = null
|
||||||
|
}
|
||||||
currentSection = trimmed.substring(1, trimmed.length - 1).trim()
|
currentSection = trimmed.substring(1, trimmed.length - 1).trim()
|
||||||
sections.putIfAbsent(currentSection, mutableMapOf())
|
sections.putIfAbsent(currentSection, mutableMapOf())
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Parse key=value pairs
|
// Parse key=value pairs
|
||||||
val eqIndex = trimmed.indexOf("=")
|
val eqIndex = trimmed.indexOf("=")
|
||||||
if (eqIndex > 0 && currentSection.isNotEmpty()) {
|
if (eqIndex > 0) {
|
||||||
val key = trimmed.substring(0, eqIndex).trim()
|
val key = trimmed.substring(0, eqIndex).trim()
|
||||||
val value = trimmed.substring(eqIndex + 1).trim()
|
val valueStr = trimmed.substring(eqIndex + 1).trim()
|
||||||
val cleanedValue = stripQuotes(value)
|
val parsedValue = parseValue(valueStr, lineNum + 1)
|
||||||
sections[currentSection]?.put(key, cleanedValue)
|
|
||||||
|
if (currentProvider != null) {
|
||||||
|
currentProvider[key] = parsedValue
|
||||||
|
} else if (currentSection.isNotEmpty()) {
|
||||||
|
sections[currentSection]?.put(key, parsedValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildConfig(sections)
|
// Don't forget the last provider if file ends with [[providers]] block
|
||||||
|
if (currentProvider != null) {
|
||||||
|
providers.add(currentProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildConfig(sections, providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseValue(valueStr: String, lineNum: Int): Any {
|
||||||
|
return when {
|
||||||
|
// Inline table: { key = value, key2 = value2 }
|
||||||
|
valueStr.startsWith("{") && valueStr.endsWith("}") -> {
|
||||||
|
parseInlineTable(valueStr, lineNum)
|
||||||
|
}
|
||||||
|
// JSON-style array: ["item1", "item2"]
|
||||||
|
valueStr.startsWith("[") && valueStr.endsWith("]") -> {
|
||||||
|
parseArray(valueStr, lineNum)
|
||||||
|
}
|
||||||
|
// CSV fallback for backward compat (detect by "," and "=" pattern without brackets)
|
||||||
|
valueStr.contains(",") && valueStr.contains("=") && !valueStr.startsWith("\"") -> {
|
||||||
|
parseCSVCapabilities(valueStr)
|
||||||
|
}
|
||||||
|
// Boolean
|
||||||
|
valueStr == "true" || valueStr == "false" -> valueStr.toBoolean()
|
||||||
|
// Number
|
||||||
|
valueStr.toDoubleOrNull() != null -> {
|
||||||
|
if (valueStr.contains(".")) valueStr.toDouble() else valueStr.toLong()
|
||||||
|
}
|
||||||
|
// String (with or without quotes)
|
||||||
|
else -> stripQuotes(valueStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseInlineTable(tableStr: String, lineNum: Int): Map<String, Any> {
|
||||||
|
val result = mutableMapOf<String, Any>()
|
||||||
|
val content = tableStr.substring(1, tableStr.length - 1).trim()
|
||||||
|
if (content.isEmpty()) return result
|
||||||
|
|
||||||
|
val pairs = splitTopLevel(content, ',')
|
||||||
|
for (pair in pairs) {
|
||||||
|
val eqIdx = pair.indexOf("=")
|
||||||
|
if (eqIdx > 0) {
|
||||||
|
val key = pair.substring(0, eqIdx).trim()
|
||||||
|
val valStr = pair.substring(eqIdx + 1).trim()
|
||||||
|
result[key] = parseValue(valStr, lineNum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseArray(arrayStr: String, lineNum: Int): List<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
val content = arrayStr.substring(1, arrayStr.length - 1).trim()
|
||||||
|
if (content.isEmpty()) return result
|
||||||
|
|
||||||
|
val items = splitTopLevel(content, ',')
|
||||||
|
for (item in items) {
|
||||||
|
val trimmed = item.trim()
|
||||||
|
result.add(stripQuotes(trimmed))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun splitTopLevel(text: String, delimiter: Char): List<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
var current = StringBuilder()
|
||||||
|
var inQuotes = false
|
||||||
|
var quoteChar = ' '
|
||||||
|
|
||||||
|
for (ch in text) {
|
||||||
|
when {
|
||||||
|
(ch == '"' || ch == '\'') && !inQuotes -> {
|
||||||
|
inQuotes = true
|
||||||
|
quoteChar = ch
|
||||||
|
current.append(ch)
|
||||||
|
}
|
||||||
|
ch == quoteChar && inQuotes -> {
|
||||||
|
inQuotes = false
|
||||||
|
current.append(ch)
|
||||||
|
}
|
||||||
|
ch == delimiter && !inQuotes -> {
|
||||||
|
result.add(current.toString().trim())
|
||||||
|
current = StringBuilder()
|
||||||
|
}
|
||||||
|
else -> current.append(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
result.add(current.toString().trim())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCSVCapabilities(capsStr: String): Map<String, Double> {
|
||||||
|
val result = mutableMapOf<String, Double>()
|
||||||
|
capsStr.split(",").forEach { pair ->
|
||||||
|
val parts = pair.trim().split("=")
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val cap = parts[0].trim()
|
||||||
|
val score = parts[1].trim().toDoubleOrNull()
|
||||||
|
if (score != null && score >= 0.0 && score <= 1.0) {
|
||||||
|
result[cap] = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stripQuotes(value: String): String {
|
private fun stripQuotes(value: String): String {
|
||||||
@@ -73,47 +197,211 @@ object ConfigLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildConfig(sections: Map<String, Map<String, String>>): CorrexConfig {
|
private fun buildConfig(
|
||||||
|
sections: Map<String, Map<String, Any>>,
|
||||||
|
providersList: List<Map<String, Any>> = emptyList(),
|
||||||
|
): CorrexConfig {
|
||||||
val serverSection = sections["server"] ?: emptyMap()
|
val serverSection = sections["server"] ?: emptyMap()
|
||||||
val tuiSection = sections["tui"] ?: emptyMap()
|
val tuiSection = sections["tui"] ?: emptyMap()
|
||||||
val cliSection = sections["cli"] ?: emptyMap()
|
val cliSection = sections["cli"] ?: emptyMap()
|
||||||
val approvalSection = sections["approval"] ?: emptyMap()
|
val approvalSection = sections["approval"] ?: emptyMap()
|
||||||
val toolsSection = sections["tools"] ?: emptyMap()
|
val toolsSection = sections["tools"] ?: emptyMap()
|
||||||
|
val toolsShellSection = sections["tools.shell"] ?: emptyMap()
|
||||||
|
val toolsFileReadSection = sections["tools.file_read"] ?: emptyMap()
|
||||||
|
val toolsFileWriteSection = sections["tools.file_write"] ?: emptyMap()
|
||||||
|
val toolsFileEditSection = sections["tools.file_edit"] ?: emptyMap()
|
||||||
|
|
||||||
val server = ServerConfig(
|
val server = ServerConfig(
|
||||||
host = serverSection["host"] ?: "localhost",
|
host = asString(serverSection["host"], "localhost"),
|
||||||
port = serverSection["port"]?.toIntOrNull() ?: 8080,
|
port = asInt(serverSection["port"], 8080),
|
||||||
)
|
)
|
||||||
|
|
||||||
val tui = TuiConfig(
|
val tui = TuiConfig(
|
||||||
theme = tuiSection["theme"] ?: "dark",
|
theme = asString(tuiSection["theme"], "dark"),
|
||||||
sessionListLimit = tuiSection["session_list_limit"]?.toIntOrNull() ?: 5,
|
sessionListLimit = asInt(tuiSection["session_list_limit"], 5),
|
||||||
)
|
)
|
||||||
|
|
||||||
val cli = CliConfig(
|
val cli = CliConfig(
|
||||||
defaultOutput = cliSection["default_output"] ?: "human",
|
defaultOutput = asString(cliSection["default_output"], "human"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val approval = ApprovalConfig(
|
val approval = ApprovalConfig(
|
||||||
timeoutMs = approvalSection["timeout_ms"]?.toLongOrNull() ?: 300_000L,
|
timeoutMs = asLong(approvalSection["timeout_ms"], 300_000L),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Resolve tool enable flags: prefer nested [tools.shell], [tools.file_read], etc.
|
||||||
|
// Fall back to flat [tools] section for backward compat
|
||||||
|
val shellEnabled = when {
|
||||||
|
toolsShellSection.containsKey("enabled") -> asBoolean(toolsShellSection["enabled"], true)
|
||||||
|
toolsSection.containsKey("shell_enabled") -> {
|
||||||
|
System.err.println("Warning: 'shell_enabled' in [tools] is deprecated, use [tools.shell] enabled instead")
|
||||||
|
asBoolean(toolsSection["shell_enabled"], true)
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileReadEnabled = when {
|
||||||
|
toolsFileReadSection.containsKey("enabled") -> asBoolean(toolsFileReadSection["enabled"], true)
|
||||||
|
toolsSection.containsKey("file_read_enabled") -> {
|
||||||
|
System.err.println("Warning: 'file_read_enabled' in [tools] is deprecated, use [tools.file_read] enabled instead")
|
||||||
|
asBoolean(toolsSection["file_read_enabled"], true)
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileWriteEnabled = when {
|
||||||
|
toolsFileWriteSection.containsKey("enabled") -> asBoolean(toolsFileWriteSection["enabled"], true)
|
||||||
|
toolsSection.containsKey("file_write_enabled") -> {
|
||||||
|
System.err.println("Warning: 'file_write_enabled' in [tools] is deprecated, use [tools.file_write] enabled instead")
|
||||||
|
asBoolean(toolsSection["file_write_enabled"], true)
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileEditEnabled = when {
|
||||||
|
toolsFileEditSection.containsKey("enabled") -> asBoolean(toolsFileEditSection["enabled"], true)
|
||||||
|
toolsSection.containsKey("file_edit_enabled") -> {
|
||||||
|
System.err.println("Warning: 'file_edit_enabled' in [tools] is deprecated, use [tools.file_edit] enabled instead")
|
||||||
|
asBoolean(toolsSection["file_edit_enabled"], true)
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
val shellAllowedExecutables = when {
|
||||||
|
toolsShellSection.containsKey("allowed_executables") -> {
|
||||||
|
asStringList(toolsShellSection["allowed_executables"])
|
||||||
|
}
|
||||||
|
toolsSection.containsKey("shell_allowed_executables") -> {
|
||||||
|
val val1 = toolsSection["shell_allowed_executables"]
|
||||||
|
when {
|
||||||
|
val1 is List<*> -> val1.filterIsInstance<String>()
|
||||||
|
val1 is String -> {
|
||||||
|
// Backward compat: check if it's CSV or already a single item
|
||||||
|
if (val1.contains(",")) {
|
||||||
|
System.err.println("Warning: CSV format 'shell_allowed_executables' is deprecated, use JSON array instead")
|
||||||
|
val1.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
} else {
|
||||||
|
listOf(val1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
val tools = ToolsConfig(
|
val tools = ToolsConfig(
|
||||||
sandboxRoot = toolsSection["sandbox_root"] ?: "",
|
sandboxRoot = asString(toolsSection["sandbox_root"], ""),
|
||||||
workingDir = toolsSection["working_dir"] ?: "",
|
workingDir = asString(toolsSection["working_dir"], ""),
|
||||||
shellAllowedExecutables = toolsSection["shell_allowed_executables"]
|
shellAllowedExecutables = shellAllowedExecutables,
|
||||||
?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
defaultSystemPromptPath = asString(
|
||||||
?: emptyList(),
|
toolsSection["default_system_prompt_path"],
|
||||||
defaultSystemPromptPath = toolsSection["default_system_prompt_path"]
|
"~/.config/correx/prompts/default_system.md"
|
||||||
?: "~/.config/correx/prompts/default_system.md",
|
),
|
||||||
|
shellEnabled = shellEnabled,
|
||||||
|
fileReadEnabled = fileReadEnabled,
|
||||||
|
fileWriteEnabled = fileWriteEnabled,
|
||||||
|
fileEditEnabled = fileEditEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val providers = providersList.mapNotNull { providerMap ->
|
||||||
|
val id = asString(providerMap["id"]) ?: return@mapNotNull null
|
||||||
|
val type = asString(providerMap["type"]) ?: return@mapNotNull null
|
||||||
|
val modelId = asString(providerMap["model_id"]) ?: return@mapNotNull null
|
||||||
|
val modelPath = asString(providerMap["model_path"], "")
|
||||||
|
val url = asString(providerMap["url"], "http://127.0.0.1:10000")
|
||||||
|
val capabilities = parseCapabilities(providerMap["capabilities"])
|
||||||
|
|
||||||
|
ProviderConfig(
|
||||||
|
id = id,
|
||||||
|
type = type,
|
||||||
|
modelId = modelId,
|
||||||
|
modelPath = modelPath,
|
||||||
|
url = url,
|
||||||
|
capabilities = capabilities,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return CorrexConfig(
|
return CorrexConfig(
|
||||||
server = server,
|
server = server,
|
||||||
tui = tui,
|
tui = tui,
|
||||||
cli = cli,
|
cli = cli,
|
||||||
approval = approval,
|
approval = approval,
|
||||||
tools = tools,
|
tools = tools,
|
||||||
|
providers = providers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun asString(value: Any?, default: String = ""): String {
|
||||||
|
return when (value) {
|
||||||
|
is String -> value
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun asStringOrNull(value: Any?): String? {
|
||||||
|
return when (value) {
|
||||||
|
is String -> value
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun asStringList(value: Any?): List<String> {
|
||||||
|
return when (value) {
|
||||||
|
is List<*> -> value.filterIsInstance<String>()
|
||||||
|
is String -> if (value.contains(",")) {
|
||||||
|
value.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
} else {
|
||||||
|
listOf(value)
|
||||||
|
}
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun asInt(value: Any?, default: Int = 0): Int {
|
||||||
|
return when (value) {
|
||||||
|
is Int -> value
|
||||||
|
is Long -> value.toInt()
|
||||||
|
is String -> value.toIntOrNull() ?: default
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun asLong(value: Any?, default: Long = 0L): Long {
|
||||||
|
return when (value) {
|
||||||
|
is Long -> value
|
||||||
|
is Int -> value.toLong()
|
||||||
|
is String -> value.toLongOrNull() ?: default
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun asBoolean(value: Any?, default: Boolean = false): Boolean {
|
||||||
|
return when (value) {
|
||||||
|
is Boolean -> value
|
||||||
|
is String -> value.lowercase() == "true"
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCapabilities(value: Any?): Map<String, Double> {
|
||||||
|
return when (value) {
|
||||||
|
is Map<*, *> -> {
|
||||||
|
// Inline table parsed earlier
|
||||||
|
value.filterKeys { it is String }
|
||||||
|
.mapKeys { it.key as String }
|
||||||
|
.mapValues { (_, v) ->
|
||||||
|
when (v) {
|
||||||
|
is Double -> v
|
||||||
|
is Number -> v.toDouble()
|
||||||
|
is String -> v.toDoubleOrNull() ?: 0.0
|
||||||
|
else -> 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter { it.value in 0.0..1.0 }
|
||||||
|
}
|
||||||
|
is String -> parseCSVCapabilities(value)
|
||||||
|
else -> emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,4 +73,148 @@ class ConfigLoaderTest {
|
|||||||
// Verify it returns a non-null Path object
|
// Verify it returns a non-null Path object
|
||||||
assertEquals("config.toml", configPath.fileName.toString())
|
assertEquals("config.toml", configPath.fileName.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml parses providers with inline table capabilities`() {
|
||||||
|
val toml = """
|
||||||
|
[server]
|
||||||
|
host = "localhost"
|
||||||
|
|
||||||
|
[[providers]]
|
||||||
|
id = "local-llama"
|
||||||
|
type = "llamacpp"
|
||||||
|
model_id = "mistral-7b"
|
||||||
|
model_path = "/path/to/model.gguf"
|
||||||
|
url = "http://127.0.0.1:10000"
|
||||||
|
capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6 }
|
||||||
|
|
||||||
|
[[providers]]
|
||||||
|
id = "alt-llama"
|
||||||
|
type = "llamacpp"
|
||||||
|
model_id = "neural-chat-7b"
|
||||||
|
model_path = "/path/to/model2.gguf"
|
||||||
|
url = "http://127.0.0.1:10001"
|
||||||
|
capabilities = { General = 0.9, Coding = 0.8 }
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val loader = ConfigLoader::class.java
|
||||||
|
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals(2, result.providers.size)
|
||||||
|
assertEquals("local-llama", result.providers[0].id)
|
||||||
|
assertEquals("llamacpp", result.providers[0].type)
|
||||||
|
assertEquals("mistral-7b", result.providers[0].modelId)
|
||||||
|
assertEquals("/path/to/model.gguf", result.providers[0].modelPath)
|
||||||
|
assertEquals("http://127.0.0.1:10000", result.providers[0].url)
|
||||||
|
assertEquals(3, result.providers[0].capabilities.size)
|
||||||
|
assertEquals(1.0, result.providers[0].capabilities["General"])
|
||||||
|
assertEquals(0.7, result.providers[0].capabilities["Coding"])
|
||||||
|
assertEquals(0.6, result.providers[0].capabilities["Reasoning"])
|
||||||
|
|
||||||
|
assertEquals("alt-llama", result.providers[1].id)
|
||||||
|
assertEquals("neural-chat-7b", result.providers[1].modelId)
|
||||||
|
assertEquals(2, result.providers[1].capabilities.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml parses nested tool sections with enable flags`() {
|
||||||
|
val toml = """
|
||||||
|
[tools]
|
||||||
|
sandbox_root = "/tmp/correx"
|
||||||
|
|
||||||
|
[tools.shell]
|
||||||
|
enabled = false
|
||||||
|
allowed_executables = ["bash", "sh", "python3"]
|
||||||
|
|
||||||
|
[tools.file_read]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[tools.file_write]
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
[tools.file_edit]
|
||||||
|
enabled = true
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val loader = ConfigLoader::class.java
|
||||||
|
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals(false, result.tools.shellEnabled)
|
||||||
|
assertEquals(true, result.tools.fileReadEnabled)
|
||||||
|
assertEquals(false, result.tools.fileWriteEnabled)
|
||||||
|
assertEquals(true, result.tools.fileEditEnabled)
|
||||||
|
assertEquals(3, result.tools.shellAllowedExecutables.size)
|
||||||
|
assertEquals("bash", result.tools.shellAllowedExecutables[0])
|
||||||
|
assertEquals("sh", result.tools.shellAllowedExecutables[1])
|
||||||
|
assertEquals("python3", result.tools.shellAllowedExecutables[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml parses JSON array shell_allowed_executables`() {
|
||||||
|
val toml = """
|
||||||
|
[tools]
|
||||||
|
shell_allowed_executables = ["bash", "sh", "node", "python3"]
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val loader = ConfigLoader::class.java
|
||||||
|
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals(4, result.tools.shellAllowedExecutables.size)
|
||||||
|
assertEquals("bash", result.tools.shellAllowedExecutables[0])
|
||||||
|
assertEquals("sh", result.tools.shellAllowedExecutables[1])
|
||||||
|
assertEquals("node", result.tools.shellAllowedExecutables[2])
|
||||||
|
assertEquals("python3", result.tools.shellAllowedExecutables[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml parses old flat tool enable flags for backward compat`() {
|
||||||
|
val toml = """
|
||||||
|
[tools]
|
||||||
|
shell_enabled = false
|
||||||
|
file_read_enabled = true
|
||||||
|
file_write_enabled = false
|
||||||
|
file_edit_enabled = true
|
||||||
|
shell_allowed_executables = "bash,sh,python3"
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val loader = ConfigLoader::class.java
|
||||||
|
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals(false, result.tools.shellEnabled)
|
||||||
|
assertEquals(true, result.tools.fileReadEnabled)
|
||||||
|
assertEquals(false, result.tools.fileWriteEnabled)
|
||||||
|
assertEquals(true, result.tools.fileEditEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `parseToml parses capabilities as CSV string for backward compat`() {
|
||||||
|
val toml = """
|
||||||
|
[server]
|
||||||
|
host = "localhost"
|
||||||
|
|
||||||
|
[[providers]]
|
||||||
|
id = "legacy-llama"
|
||||||
|
type = "llamacpp"
|
||||||
|
model_id = "mistral-7b"
|
||||||
|
capabilities = "General=1.0,Coding=0.7,Reasoning=0.6"
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val loader = ConfigLoader::class.java
|
||||||
|
val parseTomlMethod = loader.getDeclaredMethod("parseToml", String::class.java)
|
||||||
|
parseTomlMethod.isAccessible = true
|
||||||
|
val result = parseTomlMethod.invoke(ConfigLoader, toml) as CorrexConfig
|
||||||
|
|
||||||
|
assertEquals(1, result.providers.size)
|
||||||
|
assertEquals(3, result.providers[0].capabilities.size)
|
||||||
|
assertEquals(1.0, result.providers[0].capabilities["General"])
|
||||||
|
assertEquals(0.7, result.providers[0].capabilities["Coding"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# CORREX Configuration Sample
|
||||||
|
# Place at ~/.config/correx/config.toml
|
||||||
|
|
||||||
|
[server]
|
||||||
|
host = "localhost"
|
||||||
|
port = 8080
|
||||||
|
|
||||||
|
[tui]
|
||||||
|
theme = "dark"
|
||||||
|
session_list_limit = 5
|
||||||
|
|
||||||
|
[cli]
|
||||||
|
default_output = "human"
|
||||||
|
|
||||||
|
[approval]
|
||||||
|
timeout_ms = 300000
|
||||||
|
|
||||||
|
[tools]
|
||||||
|
sandbox_root = "~/.config/correx/sandbox"
|
||||||
|
working_dir = "/tmp"
|
||||||
|
default_system_prompt_path = "~/.config/correx/prompts/default_system.md"
|
||||||
|
|
||||||
|
[tools.shell]
|
||||||
|
enabled = true
|
||||||
|
allowed_executables = ["bash", "sh", "python3", "node"]
|
||||||
|
|
||||||
|
[tools.file_read]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[tools.file_write]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[tools.file_edit]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
# Provider configuration (array of tables)
|
||||||
|
[[providers]]
|
||||||
|
id = "local-llama"
|
||||||
|
type = "llamacpp"
|
||||||
|
model_id = "mistral-7b"
|
||||||
|
model_path = "~/models/mistral-7b-gguf/model.gguf"
|
||||||
|
url = "http://127.0.0.1:10000"
|
||||||
|
capabilities = { General = 1.0, Coding = 0.7, Reasoning = 0.6, Summarization = 0.8, ToolCalling = 0.5 }
|
||||||
|
|
||||||
|
# Example: second provider (if you have multiple)
|
||||||
|
# [[providers]]
|
||||||
|
# id = "alternative-llama"
|
||||||
|
# type = "llamacpp"
|
||||||
|
# model_id = "neural-chat-7b"
|
||||||
|
# model_path = "~/models/neural-chat-7b-gguf/model.gguf"
|
||||||
|
# url = "http://127.0.0.1:10001"
|
||||||
|
# capabilities = { General = 0.9, Coding = 0.8, Reasoning = 0.7, Summarization = 0.75, ToolCalling = 0.6 }
|
||||||
Reference in New Issue
Block a user