Volver al índice
src/main/java/ar/com/companeros/tools/ToolRegistry.java
package ar.com.companeros.tools;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/** Biblioteca certificada y propuestas de herramientas dentro de config/companeros/Tools. */
public final class ToolRegistry {
private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create();
private static final int MAX_PROPOSALS = 256;
private static final int MAX_DEPENDENCY_DEPTH = 4;
private final Path directory;
private final Map<Key, ToolManifest> certified = new LinkedHashMap<>();
private final Map<String, Proposal> pending = new LinkedHashMap<>();
private final Map<String, PrimitiveDefinition> primitives = new LinkedHashMap<>();
private final Map<Key, Statistics> statistics = new HashMap<>();
private boolean statisticsDirty;
public ToolRegistry(Path toolsDirectory) throws IOException {
directory = toolsDirectory.toAbsolutePath().normalize();
if (Files.isSymbolicLink(directory)) throw new IOException("Tools no puede ser un enlace simbólico");
Files.createDirectories(directory);
installMinecraftPrimitives();
loadStatistics();
}
// El registro de capacidades pertenece al código del mod, nunca a los archivos propuestos.
public synchronized void registerPrimitive(String name, ToolPermission permission, Set<String> allowedArguments) {
ToolManifest.identifier(name);
for (String argument : allowedArguments) ToolManifest.argumentName(argument);
if (primitives.containsKey(name)) throw ToolManifest.invalid("Primitiva ya registrada");
primitives.put(name, new PrimitiveDefinition(java.util.Objects.requireNonNull(permission), Set.copyOf(allowedArguments)));
}
public synchronized void registerCertified(ToolManifest manifest) {
Key key = new Key(manifest.id(), manifest.version());
ToolManifest existing = certified.get(key);
if (existing != null) {
if (!existing.sha256().equals(manifest.sha256())) throw ToolManifest.invalid("Una versión certificada es inmutable");
return;
}
validateDefinition(manifest, new HashSet<>(), 0, new int[] { 0 });
certified.put(key, manifest);
}
/** Solo un controlador confiable llama esta promoción después de probar el hash exacto. */
public synchronized ToolManifest certify(String proposalHash, UUID trustedReviewer, TestEvidence evidence) {
Proposal proposal = pending.get(proposalHash);
if (proposal == null) throw ToolManifest.invalid("Propuesta desconocida");
if (trustedReviewer == null || evidence == null || !evidence.passed()
|| !proposalHash.equals(evidence.manifestHash()) || evidence.suite().isBlank()
|| evidence.suite().length() > 160 || evidence.completedAt() == null
|| evidence.completedAt().isAfter(Instant.now().plusSeconds(60)))
throw ToolManifest.invalid("La certificación requiere evidencia aprobada del hash exacto");
registerCertified(proposal.manifest());
pending.remove(proposalHash);
return proposal.manifest();
}
// Los nombres de archivo se construyen con identificadores validados; el JSON no elige rutas.
public synchronized Proposal propose(UUID trustedAuthor, String source) throws IOException {
if (pending.size() >= MAX_PROPOSALS) throw ToolManifest.invalid("Límite de propuestas pendientes alcanzado");
ToolManifest manifest = ToolManifest.parse(source, trustedAuthor);
validateDefinition(manifest, new HashSet<>(), 0, new int[] { 0 });
Proposal old = pending.get(manifest.sha256());
if (old != null) return old;
Proposal proposal = new Proposal(manifest, Instant.now());
JsonObject envelope = new JsonObject();
envelope.addProperty("status", "pending");
envelope.addProperty("author", trustedAuthor.toString());
envelope.addProperty("createdAt", proposal.createdAt().toString());
envelope.addProperty("sha256", manifest.sha256());
envelope.add("manifest", manifest.toJson());
String name = "pending_" + manifest.id() + "_v" + manifest.version() + "_" + manifest.sha256().substring(0, 16) + ".json";
writeAtomically(directory.resolve(name), JSON.toJson(envelope));
pending.put(manifest.sha256(), proposal);
return proposal;
}
/** Recupera candidatos. Ningún archivo se convierte automáticamente en herramienta ejecutable. */
public synchronized LoadReport loadProposals() throws IOException {
List<String> rejected = new ArrayList<>();
int loaded = 0;
List<Path> files;
try (var paths = Files.list(directory)) {
files = paths.filter(path -> path.getFileName().toString().startsWith("pending_")
&& path.getFileName().toString().endsWith(".json")).sorted().limit(MAX_PROPOSALS + 1L).toList();
}
if (files.size() > MAX_PROPOSALS) rejected.add("Cantidad de propuestas superior al límite");
for (Path file : files.stream().limit(MAX_PROPOSALS).toList()) {
try {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.size(file) > ToolManifest.MAX_SOURCE_BYTES + 4096L)
throw ToolManifest.invalid("Archivo inválido o demasiado grande");
JsonElement parsed = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8));
if (!parsed.isJsonObject()) throw ToolManifest.invalid("Sobre inválido");
JsonObject envelope = parsed.getAsJsonObject();
ToolManifest.rejectUnknown(envelope, Set.of("status", "author", "createdAt", "sha256", "manifest"));
if (!"pending".equals(envelope.get("status").getAsString())) throw ToolManifest.invalid("El archivo no puede conceder aprobación");
UUID author = UUID.fromString(envelope.get("author").getAsString());
ToolManifest manifest = ToolManifest.parse(envelope.get("manifest").toString(), author);
if (!manifest.sha256().equals(envelope.get("sha256").getAsString())) throw ToolManifest.invalid("El contenido no coincide con su hash");
ToolManifest approved = certified.get(new Key(manifest.id(), manifest.version()));
if (approved != null) {
if (!approved.sha256().equals(manifest.sha256())) throw ToolManifest.invalid("Versión certificada adulterada");
continue; // Un sobre pendiente antiguo no revoca una certificación comprobada.
}
validateDefinition(manifest, new HashSet<>(), 0, new int[] { 0 });
if (pending.size() >= MAX_PROPOSALS && !pending.containsKey(manifest.sha256()))
throw ToolManifest.invalid("Límite de propuestas alcanzado");
pending.put(manifest.sha256(), new Proposal(manifest, Instant.parse(envelope.get("createdAt").getAsString())));
loaded++;
} catch (RuntimeException exception) { rejected.add(file.getFileName() + ": " + safeMessage(exception)); }
catch (StackOverflowError error) { rejected.add(file.getFileName() + ": JSON demasiado profundo"); }
}
return new LoadReport(loaded, List.copyOf(rejected));
}
// Preparación: permisos, dependencias y argumentos se comprueban antes de iniciar acciones.
public synchronized ToolExecution start(String id, int version, ExecutionContext context, PrimitiveExecutor executor) {
ToolManifest manifest = certified.get(new Key(ToolManifest.identifier(id), version));
if (manifest == null) throw ToolManifest.invalid("Herramienta no certificada");
return prepare(manifest, context, executor, status -> recordResult(new Key(manifest.id(), manifest.version()), status));
}
// Sólo el servicio local de aprendizaje abre pruebas pendientes; el JSON no concede este permiso.
synchronized ToolExecution startTrial(ToolManifest manifest, ExecutionContext context, PrimitiveExecutor executor,
java.util.function.Consumer<ToolExecution.Status> completed) {
Proposal proposal = pending.get(manifest.sha256());
if (proposal == null || !proposal.manifest().author().equals(context.actorId()))
throw ToolManifest.invalid("La prueba pertenece al autor de una propuesta pendiente");
return prepare(manifest, context, executor, status -> {
recordResult(new Key(manifest.id(), manifest.version()), status);
completed.accept(status);
});
}
synchronized void validateInputs(ToolManifest manifest, ExecutionContext context) {
compile(manifest, context.inputs(), context.permissions(), new ArrayList<>(), new ArrayList<>(), 0, manifest.maxTicks());
}
Path directory() { return directory; }
private ToolExecution prepare(ToolManifest manifest, ExecutionContext context, PrimitiveExecutor executor,
java.util.function.Consumer<ToolExecution.Status> completed) {
if (context == null || executor == null) throw ToolManifest.invalid("Falta el contexto de ejecución");
if (!context.permissions().containsAll(manifest.permissions())) throw ToolManifest.invalid("Permisos insuficientes");
List<CompiledStep> steps = new ArrayList<>();
List<ExecutionBudget> budgets = new ArrayList<>();
compile(manifest, context.inputs(), context.permissions(), steps, budgets, 0, manifest.maxTicks());
if (steps.size() > manifest.maxSteps()) throw ToolManifest.invalid("La composición excede su límite de pasos");
return new ToolExecution(manifest, context, List.copyOf(steps), List.copyOf(budgets), executor,
completed);
}
public synchronized List<ToolManifest> certifiedTools() { return List.copyOf(certified.values()); }
public synchronized List<Proposal> pendingProposals() { return List.copyOf(pending.values()); }
public synchronized Statistics statistics(String id, int version) {
return statistics.getOrDefault(new Key(id, version), new Statistics(0, 0, 0, ""));
}
public synchronized void saveStatistics() throws IOException {
if (!statisticsDirty) return;
JsonObject document = new JsonObject();
document.addProperty("schema", 1);
JsonObject entries = new JsonObject();
for (var entry : statistics.entrySet()) {
JsonObject value = new JsonObject();
value.addProperty("successes", entry.getValue().successes());
value.addProperty("failures", entry.getValue().failures());
value.addProperty("cancellations", entry.getValue().cancellations());
value.addProperty("lastResult", entry.getValue().lastResult());
entries.add(entry.getKey().id() + ":" + entry.getKey().version(), value);
}
document.add("tools", entries);
writeAtomically(directory.resolve("statistics.json"), JSON.toJson(document));
statisticsDirty = false;
}
private synchronized void recordResult(Key key, ToolExecution.Status status) {
Statistics old = statistics.getOrDefault(key, new Statistics(0, 0, 0, ""));
statistics.put(key, new Statistics(increment(old.successes(), status == ToolExecution.Status.SUCCEEDED),
increment(old.failures(), status == ToolExecution.Status.FAILED),
increment(old.cancellations(), status == ToolExecution.Status.CANCELLED), Instant.now().toString()));
statisticsDirty = true;
}
private void validateDefinition(ToolManifest manifest, Set<Key> ancestors, int depth, int[] visits) {
if (depth > MAX_DEPENDENCY_DEPTH) throw ToolManifest.invalid("Dependencias demasiado profundas");
if (++visits[0] > 512) throw ToolManifest.invalid("Grafo de dependencias demasiado grande");
Key key = new Key(manifest.id(), manifest.version());
if (!ancestors.add(key)) throw ToolManifest.invalid("Dependencia circular");
try {
for (ToolManifest.Dependency dependency : manifest.dependencies()) {
ToolManifest child = exactDependency(dependency);
if (!manifest.permissions().containsAll(child.permissions())) throw ToolManifest.invalid("Faltan permisos de una dependencia");
validateDefinition(child, ancestors, depth + 1, visits);
}
for (ToolManifest.Step step : manifest.steps()) {
if (step.primitive() == null) continue;
PrimitiveDefinition definition = primitives.get(step.primitive());
if (definition == null) throw ToolManifest.invalid("Primitiva no permitida: " + step.primitive());
if (!manifest.permissions().contains(definition.permission())) throw ToolManifest.invalid("Falta un permiso declarado");
validatePrimitiveArguments(step.primitive(), step.arguments(), definition, true);
}
} finally { ancestors.remove(key); }
}
private ToolManifest exactDependency(ToolManifest.Dependency dependency) {
ToolManifest child = certified.get(new Key(dependency.id(), dependency.version()));
if (child == null || !child.sha256().equals(dependency.sha256())) throw ToolManifest.invalid("Dependencia no certificada o hash diferente");
return child;
}
private void compile(ToolManifest manifest, JsonObject inputs, Set<ToolPermission> permissions,
List<CompiledStep> result, List<ExecutionBudget> budgets, int depth, int maximumTicks) {
if (depth > MAX_DEPENDENCY_DEPTH) throw ToolManifest.invalid("Composición demasiado profunda");
if (!permissions.containsAll(manifest.permissions())) throw ToolManifest.invalid("Permisos insuficientes para una dependencia");
int start = result.size();
for (ToolManifest.Step step : manifest.steps()) {
JsonObject args = ToolManifest.resolve(step.arguments(), inputs);
if (step.primitive() != null) {
PrimitiveDefinition definition = primitives.get(step.primitive());
validatePrimitiveArguments(step.primitive(), args, definition, false);
result.add(new CompiledStep(step.primitive(), args, step.timeoutTicks()));
} else {
ToolManifest child = exactDependency(step.tool());
compile(child, args, permissions, result, budgets, depth + 1, Math.min(step.timeoutTicks(), child.maxTicks()));
}
if (result.size() > ToolManifest.MAX_STEPS) throw ToolManifest.invalid("Demasiados pasos expandidos");
}
if (result.size() - start > manifest.maxSteps()) throw ToolManifest.invalid("La dependencia excede su límite de pasos");
budgets.add(new ExecutionBudget(start, result.size(), Math.min(maximumTicks, manifest.maxTicks())));
}
private void installMinecraftPrimitives() {
registerPrimitive("move_to", ToolPermission.MOVE, Set.of("x", "y", "z"));
registerPrimitive("mine_block", ToolPermission.MINE, Set.of("x", "y", "z"));
registerPrimitive("place_block", ToolPermission.PLACE, Set.of("x", "y", "z", "item"));
registerPrimitive("craft", ToolPermission.CRAFT, Set.of("item", "count"));
registerPrimitive("eat", ToolPermission.EAT, Set.of());
registerPrimitive("attack", ToolPermission.COMBAT, Set.of("target"));
registerPrimitive("collect", ToolPermission.INVENTORY, Set.of("radius"));
registerPrimitive("store", ToolPermission.INVENTORY, Set.of("x", "y", "z", "item", "count"));
registerPrimitive("inspect", ToolPermission.WORLD_READ, Set.of());
registerPrimitive("research_recipe", ToolPermission.WORLD_READ, Set.of("item"));
registerPrimitive("research_web", ToolPermission.WORLD_READ, Set.of("query"));
registerPrimitive("choose_skin", ToolPermission.WORLD_READ, Set.of("profile"));
registerPrimitive("parasite_upgrade_propose", ToolPermission.WORLD_READ, Set.of("capacity", "goal"));
registerPrimitive("parasite_upgrade_status", ToolPermission.WORLD_READ, Set.of());
registerPrimitive("parasite_upgrade_catalog", ToolPermission.WORLD_READ, Set.of("query"));
registerPrimitive("parasite_upgrade_apply", ToolPermission.PARASITE_UPGRADE, Set.of("quote"));
registerPrimitive("parasite_appearance", ToolPermission.PARASITE_UPGRADE, Set.of("prefab", "slot"));
registerPrimitive("parasite_appearance_catalog", ToolPermission.WORLD_READ, Set.of());
registerPrimitive("propose_tool", ToolPermission.WORLD_READ, Set.of("manifest"));
registerPrimitive("suggest_change", ToolPermission.SUGGESTION_WRITE, Set.of("manifest"));
registerPrimitive("sneak", ToolPermission.MOVE, Set.of("enabled"));
registerPrimitive("create_banner", ToolPermission.CRAFT, Set.of("baseColor", "circleColor", "symbol", "symbolColor"));
registerPrimitive("place_banner", ToolPermission.PLACE, Set.of("x", "y", "z"));
registerPrimitive("message_internal", ToolPermission.INTERNAL_CHAT, Set.of("recipient", "text"));
registerPrimitive("message_global", ToolPermission.GLOBAL_CHAT, Set.of("text"));
}
private static void validatePrimitiveArguments(String primitive, JsonObject args, PrimitiveDefinition definition, boolean referencesAllowed) {
if (definition == null) throw ToolManifest.invalid("Primitiva desconocida");
ToolManifest.rejectUnknown(args, definition.arguments());
ToolManifest.validateArguments(args, referencesAllowed);
Set<String> required = switch (primitive) {
case "move_to", "mine_block", "place_banner" -> Set.of("x", "y", "z");
case "place_block", "store" -> Set.of("x", "y", "z", "item");
case "craft", "research_recipe" -> Set.of("item");
case "research_web" -> Set.of("query");
case "choose_skin" -> Set.of("profile");
case "parasite_upgrade_propose" -> Set.of("capacity", "goal");
case "parasite_upgrade_catalog" -> Set.of("query");
case "parasite_upgrade_apply" -> Set.of("quote");
case "parasite_appearance" -> Set.of("prefab", "slot");
case "propose_tool", "suggest_change" -> Set.of("manifest");
case "sneak" -> Set.of("enabled");
case "create_banner" -> Set.of("baseColor", "circleColor", "symbol", "symbolColor");
case "attack" -> Set.of("target");
case "message_internal" -> Set.of("recipient", "text");
case "message_global" -> Set.of("text");
default -> Set.of();
};
if (!args.keySet().containsAll(required)) throw ToolManifest.invalid("Faltan argumentos de la primitiva " + primitive);
for (var entry : args.entrySet()) {
JsonElement value = entry.getValue();
if (referencesAllowed && value.isJsonObject()) continue;
switch (entry.getKey()) {
case "x", "y", "z", "count", "radius" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) throw ToolManifest.invalid("Se esperaba un número");
double number = value.getAsDouble();
if (!Double.isFinite(number) || Math.abs(number) > 30_000_000) throw ToolManifest.invalid("Coordenada fuera del límite");
if (entry.getKey().equals("count") && (number != Math.floor(number) || number < 1 || number > 64))
throw ToolManifest.invalid("Cantidad fuera del límite");
if (entry.getKey().equals("radius") && (number <= 0 || number > 4)) throw ToolManifest.invalid("Alcance de recogida fuera del límite");
if (!primitive.equals("move_to") && Set.of("x", "y", "z").contains(entry.getKey()) && number != Math.floor(number))
throw ToolManifest.invalid("La posición del bloque debe usar enteros");
}
case "item" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()
|| !value.getAsString().matches("[a-z0-9_.-]+:[a-z0-9_./-]+")) throw ToolManifest.invalid("Identificador de item inválido");
}
case "profile" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) throw ToolManifest.invalid("Cuenta de skin inválida");
ar.com.companeros.identity.SkinSources.profile(value.getAsString());
}
case "capacity" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) throw ToolManifest.invalid("Capacidad inválida");
try { ar.com.companeros.horror.ParasiteUpgrades.capacity(value.getAsString()); }
catch (IllegalArgumentException failure) { throw ToolManifest.invalid("Capacidad inválida"); }
}
case "goal" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString() || value.getAsString().isBlank()
|| value.getAsString().length() > 220 || value.getAsString().contains("\n")) throw ToolManifest.invalid("Objetivo inválido");
}
case "quote" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString() || !canonicalUuid(value.getAsString()))
throw ToolManifest.invalid("Cotización inválida");
}
case "target" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) throw ToolManifest.invalid("Identidad inválida");
try { UUID.fromString(value.getAsString()); }
catch (IllegalArgumentException exception) { throw ToolManifest.invalid("Identidad inválida"); }
}
case "recipient" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) throw ToolManifest.invalid("Destinatario inválido");
String recipient = value.getAsString();
if (!recipient.equalsIgnoreCase("all") && !recipient.matches("[A-Za-z0-9_]{1,16}") && !canonicalUuid(recipient))
throw ToolManifest.invalid("El destinatario debe ser all, un nombre o UUID");
}
case "text", "query" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString() || value.getAsString().isBlank())
throw ToolManifest.invalid("Mensaje vacío o inválido");
if (entry.getKey().equals("query") && (value.getAsString().length() > 220 || value.getAsString().contains("\n")))
throw ToolManifest.invalid("Consulta web fuera de límite");
}
case "manifest" -> {
if (!Set.of("propose_tool", "suggest_change").contains(primitive) || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()
|| value.getAsString().isBlank() || value.getAsString().getBytes(StandardCharsets.UTF_8).length > 4096)
throw ToolManifest.invalid("Propuesta de herramienta fuera del límite");
}
case "enabled" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) throw ToolManifest.invalid("Se requiere true o false");
}
case "baseColor", "circleColor", "symbolColor" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()
|| java.util.Arrays.stream(net.minecraft.world.item.DyeColor.values())
.noneMatch(color -> color.getName().equals(value.getAsString())))
throw ToolManifest.invalid("Color de bandera desconocido");
}
case "symbol" -> {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
throw ToolManifest.invalid("Símbolo de bandera inválido");
ToolManifest.identifier(value.getAsString());
}
default -> { } // Una extensión confiable puede definir otros argumentos escalares.
}
}
}
private static boolean canonicalUuid(String value) {
try { return UUID.fromString(value).toString().equalsIgnoreCase(value); }
catch (IllegalArgumentException exception) { return false; }
}
// Estadísticas persistentes: escritura explícita en checkpoint, nunca en cada tick.
private void loadStatistics() throws IOException {
Path file = directory.resolve("statistics.json");
if (!Files.exists(file, LinkOption.NOFOLLOW_LINKS)) return;
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.size(file) > 1_048_576)
throw new IOException("Archivo de estadísticas inválido");
try {
JsonObject root = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject();
if (root.get("schema").getAsInt() != 1) throw ToolManifest.invalid("Estadísticas de versión desconocida");
JsonObject entries = root.getAsJsonObject("tools");
if (entries.size() > 4096) throw ToolManifest.invalid("Demasiadas estadísticas");
for (var entry : entries.entrySet()) {
String[] parts = entry.getKey().split(":", -1);
if (parts.length != 2) throw ToolManifest.invalid("Clave de estadística inválida");
int version = Integer.parseInt(parts[1]);
if (version < 1 || version > 1_000_000) throw ToolManifest.invalid("Versión inválida");
JsonObject value = entry.getValue().getAsJsonObject();
statistics.put(new Key(ToolManifest.identifier(parts[0]), version), new Statistics(
nonnegative(value, "successes"), nonnegative(value, "failures"), nonnegative(value, "cancellations"),
value.get("lastResult").getAsString()));
}
} catch (RuntimeException exception) { throw new IOException("Estadísticas dañadas", exception); }
}
private static long nonnegative(JsonObject value, String key) {
long number = value.get(key).getAsBigDecimal().longValueExact();
if (number < 0) throw ToolManifest.invalid("Estadística negativa");
return number;
}
private static long increment(long value, boolean increment) {
return increment && value < Long.MAX_VALUE ? value + 1 : value;
}
private void writeAtomically(Path target, String source) throws IOException {
Path checked = target.toAbsolutePath().normalize();
if (!checked.getParent().equals(directory) || Files.isSymbolicLink(checked)) throw new IOException("Ruta fuera de Tools");
Path temporary = directory.resolve("tool_write_" + UUID.randomUUID() + ".tmp");
try {
Files.writeString(temporary, source, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
try { Files.move(temporary, checked, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); }
catch (java.nio.file.AtomicMoveNotSupportedException exception) { Files.move(temporary, checked, StandardCopyOption.REPLACE_EXISTING); }
} finally { Files.deleteIfExists(temporary); }
}
private static String safeMessage(RuntimeException exception) {
String message = exception.getMessage();
return message == null ? "Formato inválido" : message.substring(0, Math.min(message.length(), 256));
}
private record Key(String id, int version) { }
private record PrimitiveDefinition(ToolPermission permission, Set<String> arguments) { }
public record Proposal(ToolManifest manifest, Instant createdAt) { }
public record LoadReport(int loaded, List<String> rejected) { }
public record TestEvidence(String manifestHash, String suite, boolean passed, Instant completedAt) { }
public record Statistics(long successes, long failures, long cancellations, String lastResult) {
public double successRate() { double tested = (double) successes + failures; return tested == 0 ? 0 : successes / tested; }
}
public record ExecutionContext(UUID actorId, long startTick, Set<ToolPermission> permissions, JsonObject inputs) {
public ExecutionContext {
if (actorId == null || startTick < 0 || permissions == null || inputs == null) throw ToolManifest.invalid("Contexto inválido");
permissions = Set.copyOf(permissions);
inputs = inputs.deepCopy();
ToolManifest.validateArguments(inputs, false);
}
@Override public JsonObject inputs() { return inputs.deepCopy(); }
}
static record CompiledStep(String primitive, JsonObject arguments, int timeoutTicks) { }
static record ExecutionBudget(int startStep, int endStep, int maxTicks) { }
}