package ar.com.companeros.runtime; import ar.com.companeros.decision.DecisionGateway; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.time.Duration; import java.util.Set; /** Configuración del operador; nunca contiene una clave de API o datos enviados al modelo. */ public final class AgentConfig { // Defaults de ensayo: dos jugadores y ninguna llamada paga automática. private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create(); private static final Set FIELDS = Set.of("schemaVersion", "populationLimit", "modelsEnabled", "foundationVerified", "solEnabled", "maxSessionUsd", "minAgentIntervalSeconds", "maxRequestsHour", "autoConnect", "webEnabled"); private final Path file; private int populationLimit = 2; private boolean modelsEnabled, foundationVerified, solEnabled; private BigDecimal maxSessionUsd = BigDecimal.ZERO; private int minAgentIntervalSeconds = 60, maxRequestsHour = 30; private boolean autoConnect = true; private boolean webEnabled; private ResearchConfig research = ResearchConfig.defaults(); private AgentConfig(Path file) { this.file = file; } public static AgentConfig load(Path configDir) { Path root = configDir.toAbsolutePath().normalize(); Path legacy = root.resolve("companeros"), current = root.resolve("raps"); try { if (Files.isDirectory(legacy) && !Files.exists(current)) Files.move(legacy, current); } catch (IOException failure) { throw new IllegalStateException("No se pudo migrar la configuración anterior", failure); } Path file = current.resolve("agents.json"); AgentConfig config = new AgentConfig(file); try { config.research = ResearchConfig.load(current); config.webEnabled = config.research.enabled(); if (!Files.exists(file)) { config.write(); return config; } if (Files.size(file) > 16_384) throw new IOException("Configuración fuera de límite"); JsonObject data = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject(); if (!FIELDS.containsAll(data.keySet()) || integer(data, "schemaVersion", 1) != 1) throw new IllegalArgumentException("Formato de configuración no reconocido"); config.populationLimit = integer(data, "populationLimit", 2); validPopulation(config.populationLimit); config.modelsEnabled = bool(data, "modelsEnabled", false); config.foundationVerified = bool(data, "foundationVerified", false); config.solEnabled = bool(data, "solEnabled", false); config.autoConnect = bool(data, "autoConnect", true); config.maxSessionUsd = data.has("maxSessionUsd") ? data.get("maxSessionUsd").getAsBigDecimal() : BigDecimal.ZERO; config.minAgentIntervalSeconds = integer(data, "minAgentIntervalSeconds", 60); config.maxRequestsHour = integer(data, "maxRequestsHour", 30); config.decisionConfig(); // Valida cupos antes de habilitar cualquier proveedor. return config; } catch (IOException | RuntimeException error) { throw new IllegalStateException("No se pudo cargar agents.json; se conserva el archivo original", error); } } // Lectura y modificación limitada de población; flags del proveedor se administran en archivo. public synchronized int populationLimit() { return populationLimit; } public synchronized boolean autoConnect() { return autoConnect; } public synchronized boolean webEnabled() { return webEnabled; } public synchronized void setPopulationLimit(int value) { validPopulation(value); int previous = populationLimit; populationLimit = value; try { write(); } catch (IOException error) { populationLimit = previous; throw new IllegalStateException("No se pudo guardar el límite de población", error); } } public synchronized DecisionGateway.Config decisionConfig() { return new DecisionGateway.Config(modelsEnabled, foundationVerified, solEnabled, maxSessionUsd, maxRequestsHour, 16, 2, 2000, minAgentIntervalSeconds, Duration.ofSeconds(20), webEnabled, research.maxRequestsHour(), research.minAgentIntervalSeconds(), research.allowedDomains()); } // Guardado atómico: rutas fijas del servicio confiable, sin parámetros de filesystem para bots. private void write() throws IOException { Files.createDirectories(file.getParent()); JsonObject data = new JsonObject(); data.addProperty("schemaVersion", 1); data.addProperty("populationLimit", populationLimit); data.addProperty("modelsEnabled", modelsEnabled); data.addProperty("foundationVerified", foundationVerified); data.addProperty("solEnabled", solEnabled); data.addProperty("maxSessionUsd", maxSessionUsd); data.addProperty("minAgentIntervalSeconds", minAgentIntervalSeconds); data.addProperty("maxRequestsHour", maxRequestsHour); data.addProperty("autoConnect", autoConnect); Path temporary = file.resolveSibling("agents.json.tmp"); Files.writeString(temporary, JSON.toJson(data) + System.lineSeparator(), StandardCharsets.UTF_8); try { Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } catch (AtomicMoveNotSupportedException unavailable) { Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING); } finally { Files.deleteIfExists(temporary); } } private static void validPopulation(int value) { if (value != 2 && value != 4 && value != 6 && value != 10) throw new IllegalArgumentException("La población admite 2, 4, 6 o 10 jugadores"); } private static int integer(JsonObject data, String key, int fallback) { if (!data.has(key)) return fallback; if (!data.get(key).isJsonPrimitive() || !data.get(key).getAsJsonPrimitive().isNumber()) throw new IllegalArgumentException("Valor numérico inválido"); return data.get(key).getAsBigDecimal().intValueExact(); } private static boolean bool(JsonObject data, String key, boolean fallback) { if (!data.has(key)) return fallback; if (!data.get(key).isJsonPrimitive() || !data.get(key).getAsJsonPrimitive().isBoolean()) throw new IllegalArgumentException("Valor booleano inválido"); return data.get(key).getAsBoolean(); } }