c118996746
- SwiftUI macOS app with C++17 code analysis engine (ObjC++ bridge) - Agentic AI loop: LLM plans → tool calls → execution → feedback loop - 15 agent tools: file ops, terminal, git, xcode build, code intel - 7 persistent terminal tools with background session management - Chat sidebar with agent step rendering and auto-apply - NVIDIA NIM API integration (Llama 3.3 70B default) - OpenAI tool_calls format with prompt-based fallback - Code editor with syntax highlighting and multi-tab support - File tree, console view, terminal view - Git integration and workspace management
184 lines
7.3 KiB
Swift
184 lines
7.3 KiB
Swift
// AgentConfig.swift
|
|
// CxSwiftAgent — Immutable configuration for the MCP coding agent.
|
|
//
|
|
// Mirrors CxCodingAgent::Config (Perl) with Swift patterns:
|
|
// - Environment variable loading
|
|
// - Config file parsing (cx-agent.conf)
|
|
// - Path traversal protection
|
|
// - Profile validation
|
|
|
|
import Foundation
|
|
|
|
struct AgentConfig: Sendable {
|
|
let host: String
|
|
let port: Int
|
|
let workspaceRoot: String
|
|
let logLevel: String
|
|
let maxFileSize: Int
|
|
let llmEndpoint: String
|
|
let llmModel: String
|
|
let nvidiaApiKey: String
|
|
let xcodePath: String
|
|
let sandboxMode: Bool
|
|
let profile: String
|
|
let authToken: String
|
|
let transport: String
|
|
let allowedOrigins: [String]
|
|
let excludePatterns: [String]
|
|
let rateLimit: Int
|
|
let maxRequestSize: Int
|
|
let toolTimeout: Int
|
|
let giteaUrl: String
|
|
let giteaToken: String
|
|
let giteaOrg: String
|
|
|
|
static let defaultExcludePatterns = [
|
|
".git", "node_modules", "vendor", "__pycache__",
|
|
".DS_Store", ".build", "DerivedData", ".next",
|
|
"dist", "build", "coverage", ".nyc_output",
|
|
]
|
|
|
|
static func fromEnvironment() -> AgentConfig {
|
|
let env = ProcessInfo.processInfo.environment
|
|
let ws = env["CX_WORKSPACE"] ?? env["PWD"] ?? FileManager.default.currentDirectoryPath
|
|
|
|
// Load config file first
|
|
let fileCfg = loadConfigFile(workspaceRoot: ws)
|
|
|
|
// Environment overrides
|
|
let transport: String = {
|
|
if CommandLine.arguments.contains("--stdio") { return "stdio" }
|
|
return env["CX_TRANSPORT"] ?? fileCfg["transport"] ?? "http"
|
|
}()
|
|
|
|
let profile = (env["CX_PROFILE"] ?? fileCfg["profile"] ?? "development").lowercased()
|
|
let validProfiles: Set<String> = ["development", "staging", "production"]
|
|
|
|
return AgentConfig(
|
|
host: env["MCP_HOST"] ?? fileCfg["host"] ?? "0.0.0.0",
|
|
port: Int(env["MCP_PORT"] ?? fileCfg["port"] ?? "") ?? 5100,
|
|
workspaceRoot: ws,
|
|
logLevel: {
|
|
let level = (env["MCP_LOG_LEVEL"] ?? fileCfg["log_level"] ?? "info").lowercased()
|
|
if profile == "production" && level == "debug" { return "warn" }
|
|
return level
|
|
}(),
|
|
maxFileSize: Int(env["CX_MAX_FILE_SIZE"] ?? fileCfg["max_file_size"] ?? "") ?? 10_485_760,
|
|
llmEndpoint: env["CX_LLM_ENDPOINT"] ?? fileCfg["llm_endpoint"] ?? "http://localhost:8080",
|
|
llmModel: env["CX_LLM_MODEL"] ?? fileCfg["llm_model"] ?? "cx-model-2.5",
|
|
nvidiaApiKey: env["NVIDIA_API_KEY"] ?? fileCfg["nvidia_api_key"] ?? "",
|
|
xcodePath: env["CX_XCODE_PATH"] ?? fileCfg["xcode_path"] ?? "/usr/bin/xcodebuild",
|
|
sandboxMode: (env["CX_SANDBOX"] ?? fileCfg["sandbox_mode"] ?? "0") == "1",
|
|
profile: validProfiles.contains(profile) ? profile : "development",
|
|
authToken: env["CX_AUTH_TOKEN"] ?? fileCfg["auth_token"] ?? "",
|
|
transport: (transport == "http" || transport == "stdio") ? transport : "http",
|
|
allowedOrigins: {
|
|
let raw = env["CX_ALLOWED_ORIGINS"] ?? fileCfg["allowed_origins"] ?? "*"
|
|
return raw.isEmpty ? ["*"] : raw.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
|
}(),
|
|
excludePatterns: {
|
|
if let raw = env["CX_EXCLUDE_PATTERNS"] ?? fileCfg["exclude_patterns"] {
|
|
return raw.isEmpty ? [] : raw.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
|
}
|
|
return defaultExcludePatterns
|
|
}(),
|
|
rateLimit: Int(env["CX_RATE_LIMIT"] ?? fileCfg["rate_limit"] ?? "") ?? 0,
|
|
maxRequestSize: Int(env["CX_MAX_REQUEST_SIZE"] ?? "") ?? 16_777_216,
|
|
toolTimeout: Int(env["CX_TOOL_TIMEOUT"] ?? fileCfg["tool_timeout"] ?? "") ?? 300,
|
|
giteaUrl: env["CXGIT_API_URL"] ?? fileCfg["gitea_url"] ?? "https://git.cxllm-studio.com",
|
|
giteaToken: env["CXGIT_API_TOKEN"] ?? fileCfg["gitea_token"] ?? "",
|
|
giteaOrg: env["CXGIT_ORG"] ?? fileCfg["gitea_org"] ?? "CxAI-LLM"
|
|
)
|
|
}
|
|
|
|
// MARK: - Path Resolution
|
|
|
|
func resolvePath(_ path: String) -> String? {
|
|
guard !path.isEmpty else { return nil }
|
|
|
|
let abs: String
|
|
if path.hasPrefix("/") {
|
|
abs = path
|
|
} else {
|
|
abs = (workspaceRoot as NSString).appendingPathComponent(path)
|
|
}
|
|
|
|
let resolved = (abs as NSString).standardizingPath
|
|
|
|
// Path traversal protection
|
|
guard resolved == workspaceRoot || resolved.hasPrefix(workspaceRoot + "/") else {
|
|
return nil
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
func isExcluded(_ path: String) -> Bool {
|
|
for pattern in excludePatterns {
|
|
if path.contains(pattern) { return true }
|
|
}
|
|
return false
|
|
}
|
|
|
|
var hasLLM: Bool { !nvidiaApiKey.isEmpty || !llmEndpoint.isEmpty }
|
|
var hasAuth: Bool { !authToken.isEmpty }
|
|
var hasGitea: Bool { !giteaUrl.isEmpty && !giteaToken.isEmpty }
|
|
var isProduction: Bool { profile == "production" }
|
|
|
|
func isOriginAllowed(_ origin: String?) -> Bool {
|
|
guard let origin else { return true }
|
|
return allowedOrigins.contains("*") || allowedOrigins.contains(origin)
|
|
}
|
|
|
|
func toDict() -> [String: Any] {
|
|
[
|
|
"host": host,
|
|
"port": port,
|
|
"workspace_root": workspaceRoot,
|
|
"log_level": logLevel,
|
|
"max_file_size": maxFileSize,
|
|
"llm_endpoint": llmEndpoint,
|
|
"llm_model": llmModel,
|
|
"has_nvidia_key": !nvidiaApiKey.isEmpty,
|
|
"xcode_path": xcodePath,
|
|
"sandbox_mode": sandboxMode,
|
|
"profile": profile,
|
|
"has_auth": hasAuth,
|
|
"has_gitea": hasGitea,
|
|
"gitea_url": giteaUrl,
|
|
"gitea_org": giteaOrg,
|
|
"transport": transport,
|
|
"allowed_origins": allowedOrigins,
|
|
"exclude_patterns": excludePatterns,
|
|
"rate_limit": rateLimit,
|
|
"tool_timeout": toolTimeout,
|
|
]
|
|
}
|
|
|
|
// MARK: - Config File Loader
|
|
|
|
private static func loadConfigFile(workspaceRoot: String) -> [String: String] {
|
|
var cfg: [String: String] = [:]
|
|
for name in ["cx-agent.conf", ".cx-agent.conf"] {
|
|
let path = (workspaceRoot as NSString).appendingPathComponent(name)
|
|
guard let contents = try? String(contentsOfFile: path, encoding: .utf8) else { continue }
|
|
for line in contents.components(separatedBy: .newlines) {
|
|
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
|
if trimmed.isEmpty || trimmed.hasPrefix("#") { continue }
|
|
let parts = trimmed.split(separator: "=", maxSplits: 1)
|
|
if parts.count == 2 {
|
|
let key = String(parts[0]).trimmingCharacters(in: .whitespaces)
|
|
var val = String(parts[1]).trimmingCharacters(in: .whitespaces)
|
|
// Strip quotes
|
|
if (val.hasPrefix("\"") && val.hasSuffix("\"")) ||
|
|
(val.hasPrefix("'") && val.hasSuffix("'")) {
|
|
val = String(val.dropFirst().dropLast())
|
|
}
|
|
cfg[key] = val
|
|
}
|
|
}
|
|
break // only load first found
|
|
}
|
|
return cfg
|
|
}
|
|
}
|