Skip to main content
Glama

TACIT: Tracked Agent Capabilities In Types

Paper: Securing Agents With Tracked Capabilities (ACM) · arXiv:2603.00991 · 🏆 Premio al Mejor Artículo en CAIS 26

TACIT (Tracked Agent Capabilities In Types) es un arnés de seguridad para agentes de IA. En lugar de llamar a herramientas directamente, los agentes escriben código en Scala 3 con capture checking: un sistema de tipos que rastrea estáticamente las capacidades y garantiza que el código del agente no pueda falsificar derechos de acceso, no pueda realizar efectos más allá de su presupuesto y no pueda filtrar información de subcomputaciones puras. Proporciona una interfaz MCP, de modo que puede ser utilizado fácilmente por todos los agentes compatibles con MCP.

TACIT Framework Overview

El framework tiene tres componentes principales:

  • Compilador de Scala 3. El código enviado por el agente se valida y se verifica con el capture checking habilitado en modo seguro, que impone un subconjunto de lenguaje seguro para capacidades.

  • REPL de Scala. Una instancia REPL local ejecuta el código compilado y gestiona el estado entre interacciones. Admite tanto la ejecución de una sola vez sin estado como sesiones con estado.

  • Biblioteca de seguridad de capacidades. Una API tipada que sirve como la única puerta de entrada a través de la cual el código del agente interactúa con el mundo real: sistema de archivos, ejecución de procesos, red y subagentes. La biblioteca es extensible: añade nuevas capacidades modificando solo el código de la biblioteca, sin cambiar el servidor MCP en sí.

Inicio Rápido

TACIT proporciona un servidor MCP estándar que se comunica mediante JSON-RPC sobre stdio. Funciona con cualquier agente compatible con MCP, incluidos Claude Code, OpenCode, GitHub Copilot y otros.

Requiere JDK 17+.

Instalación de TACIT

Elige una de las opciones de instalación a continuación. El envoltorio CLI tacit es la opción recomendada.

Opción 1: Instalar tacit (Recomendado)

tacit es un pequeño comando envoltorio para gestionar TACIT localmente. Usa tacit setup una vez para instalar el comando y obtener la última versión, tacit update para actualizar los JARs, tacit self update para actualizar el propio envoltorio, y tacit serve para lanzar el servidor MCP.

# Download the wrapper directly (no git clone required)
curl -fsSL https://raw.githubusercontent.com/lampepfl/tacit/refs/heads/main/tacit -o tacit
chmod +x tacit

# Install it and download the latest TACIT release
./tacit setup

Esto instala el comando tacit en ~/.local/bin, asegura que ~/.local/bin esté en PATH, y descarga la última versión en ~/.cache/tacit/.

Comandos comunes:

# Refresh the cached release if a new version exists
tacit update

# Refresh the tacit wrapper itself
tacit self update

# Start the MCP server
tacit serve

# Remove the wrapper and cached release
tacit self uninstall

Por defecto, tacit usa:

Recurso

Ruta por defecto

MCP Server

~/.cache/tacit/TACIT.jar

Library

~/.cache/tacit/TACIT-library.jar

Opción 2: Descargar los JARs de la versión precompilada directamente

Si no quieres el envoltorio, usa el script de descarga de la versión.

# Download the script directly (no git clone required)
curl -fsSL https://raw.githubusercontent.com/lampepfl/tacit/refs/heads/main/download_release.sh -o download_release.sh
chmod +x download_release.sh

./download_release.sh

Opcional:

# Or use wget instead of curl
wget -q https://raw.githubusercontent.com/lampepfl/tacit/refs/heads/main/download_release.sh -O download_release.sh
chmod +x download_release.sh

# Download into a custom directory
./download_release.sh ./dist
./download_release.sh --pre-release ./dist

Por defecto, esto descarga:

JAR

Ruta por defecto

MCP Server

./TACIT.jar

Library

./TACIT-library.jar

Tanto el envoltorio como el script verifican los JARs descargados contra los resúmenes SHA-256 publicados en los metadatos de la versión y se niegan a instalar un JAR cuyo resumen falte o no coincida. Las descargas se guardan en un directorio temporal y se mueven a su lugar solo después de la verificación, por lo que una descarga fallida nunca reemplaza un JAR previamente instalado.

Para compilar desde el árbol de fuentes actual, consulta la Opción 3 a continuación.

Opción 3: Compilar desde el código fuente

Requiere JDK 17+ y sbt 1.12+.

git clone https://github.com/lampepfl/tacit.git
cd tacit

./build.sh

Opcional:

# Build and copy JARs into a custom directory
./build.sh ./dist

# Show full sbt output while building
./build.sh --verbose

Esto compila y copia dos JARs:

JAR

Ruta

MCP Server

./TACIT.jar (o ./dist/TACIT.jar)

Library

./TACIT-library.jar (o ./dist/TACIT-library.jar)

Una vez que TACIT esté instalado mediante cualquiera de las opciones anteriores, configura tu agente para lanzar el servidor MCP.

Configura tu Agente

Añade TACIT como servidor MCP en la configuración de tu agente. Si instalaste el CLI tacit, simplemente usa tacit serve. Si instalaste TACIT manualmente, usa la forma explícita java -jar ... --library-jar ... en su lugar.

Añade a .mcp.json de tu proyecto (o ~/.claude.json para global).

Con tacit:

{
  "mcpServers": {
    "tacit": {
      "command": "tacit",
      "args": ["serve"]
    }
  }
}

Con rutas JAR manuales:

{
  "mcpServers": {
    "tacit": {
      "command": "java",
      "args": [
        "-jar", "/path/to/TACIT.jar",
        "--library-jar", "/path/to/TACIT-library.jar"
      ]
    }
  }
}

Añade a tu opencode.json.

Con tacit:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "tacit": {
      "type": "local",
      "enabled": true,
      "command": ["tacit", "serve"]
    }
  }
}

Con rutas JAR manuales:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "tacit": {
      "type": "local",
      "enabled": true,
      "command": [
        "java",
        "-jar", "/path/to/TACIT.jar",
        "--library-jar", "/path/to/TACIT-library.jar"
      ]
    }
  }
}

Añade a tu .vscode/mcp.json.

Con tacit:

{
  "servers": {
    "tacit": {
      "command": "tacit",
      "args": ["serve"]
    }
  }
}

Con rutas JAR manuales:

{
  "servers": {
    "tacit": {
      "command": "java",
      "args": [
        "-jar", "/path/to/TACIT.jar",
        "--library-jar", "/path/to/TACIT-library.jar"
      ]
    }
  }
}

Tu agente ahora puede usar las herramientas de TACIT para ejecutar código Scala en un entorno aislado.

Recomendado: Deshabilitar las Herramientas Integradas

Para beneficiarse completamente de la seguridad basada en capacidades de TACIT, deshabilita las herramientas integradas de archivo, shell y red del agente para que todas las operaciones pasen por el REPL aislado.

Lanza con --disallowedTools para bloquear las herramientas integradas:

claude --disallowedTools "Bash,Read,Write,Edit,WebFetch"

O añade a .claude/settings.json de tu proyecto:

{
  "permissions": {
    "disallowedTools": ["Bash", "Read", "Write", "Edit", "WebFetch"]
  }
}

Establece los permisos de las herramientas integradas a "deny" en opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "*": "ask",
    "bash": "deny",
    "read": "deny",
    "edit": "deny",
    "glob": "deny",
    "grep": "deny",
    "list": "deny",
    "tacit*": "allow"
  },
  "mcp": {
    "tacit": { "..." : "..." }
  }
}

En tu settings.json de VS Code, restringe las herramientas disponibles para Copilot:

{
  "github.copilot.chat.agent.tools": {
    "terminal": false,
    "fs_read": false,
    "fs_write": false
  }
}

Related MCP server: edict-lang

Configuración

El servidor se puede configurar mediante banderas CLI o un archivo de configuración JSON. Pasa las banderas directamente en los argumentos MCP de tu agente, o usa --config para apuntar a un archivo JSON.

La configuración se divide en configuración del servidor (transporte, grabación, sesiones) y configuración de la biblioteca (comportamiento del sandbox, capacidades). En el archivo de configuración JSON, los ajustes de la biblioteca se encuentran bajo la clave libraryConfig y se pasan directamente a la biblioteca para su procesamiento.

Banderas CLI

Banderas del servidor:

Bandera

Descripción

--library-jar <path>

Requerido. Ruta al JAR de la biblioteca (TACIT-library.jar)

-r/--record <dir>

Registra cada ejecución en disco

-q/--quiet

Suprime el banner de inicio y el registro de solicitudes/respuestas

--no-session

Deshabilita las herramientas relacionadas con sesiones

--safe-mode / --no-safe-mode

Habilita/deshabilita language.experimental.safe de Scala 3 en el REPL para cada ejecución (por defecto: activado; ver Modo Seguro)

--exec-timeout-ms <ms>

Tiempo de espera de reloj de pared para una sola evaluación REPL (por defecto: ninguno; ver Tiempo de Espera de Ejecución)

-c/--config <path>

Archivo de configuración JSON (las banderas después de --config anulan los valores del archivo)

Banderas de la biblioteca (abreviatura de algunos campos de libraryConfig):

Bandera

Descripción

-s/--strict

Bloquea una lista negra integrada de comandos inseguros (operaciones de archivo, shells, intérpretes, herramientas de red, ejecutores de comandos, ...) a través de exec; la coincidencia no distingue mayúsculas. Conveniente para experimentos rápidos; para despliegues reales prefiere --command-permissions.

--command-permissions <patterns>

Patrones glob separados por comas de comandos ejecutables (p. ej. echo,py*,ls). Solo * se interpreta como comodín. Cuando se establece, --strict se ignora.

--network-permissions <patterns>

Patrones glob separados por comas de hosts alcanzables (p. ej. *.example.com,api.github.com). Solo * se interpreta como comodín.

--allowed-roots <paths>

Límite exterior separado por comas en las raíces de requestFileSystem (p. ej. /home/me/project,/tmp). Una raíz solicitada debe resolverse a una ruta dentro de una de estas. Por defecto, el directorio de trabajo del servidor cuando no se establece.

--classified-paths <patterns>

Patrones de rutas clasificadas separados por comas (estilo gitignore, ver más abajo)

--llm-base-url <url>

URL base de la API LLM

--llm-api-key <key>

Clave de API LLM

--llm-model <name>

Nombre del modelo LLM

Archivo de Configuración JSON

{
  "recordPath": "/tmp/recordings",
  "quiet": true,
  "sessionEnabled": true,
  "safeMode": true,
  "executionTimeoutMs": 60000,
  "libraryJarPath": "/path/to/TACIT-library.jar",
  "libraryConfig": {
    "commandPermissions": ["sbt", "scala", "javac", "java", "make"],
    "networkPermissions": ["*.scala-lang.org", "github.com", "docs.oracle.com"],
    "allowedRoots": ["/home/user/project", "/tmp"],
    "classifiedPaths": [".ssh", ".env", ".env.*", "secrets"],
    "secureOutput": "/tmp/secure.log",
    "classifiedWrite": false,
    "llm": {
      "baseUrl": "https://api.example.com",
      "apiKey": "sk-...",
      "model": "gpt-..."
    }
  }
}

commandPermissions (opcional). La lista blanca de exec: una lista de patrones glob (solo * es un comodín) que todo comando pasado a exec debe coincidir. Esto se superpone al conjunto por ámbito declarado por requestExecPermission(...); un comando debe estar en ambos para ejecutarse realmente. Cuando se establece, strictMode se ignora. En despliegues reales siempre debes configurar esta lista explícitamente.

strictMode (opcional, por defecto true). Un valor por defecto para experimentos rápidos que bloquea una lista negra integrada de comandos inseguros a través de exec: comandos de operaciones de archivo (cat, ls, rm, tar, chmod, ...), shells, intérpretes (python, node, perl, ...), herramientas de red (curl, wget, ssh, ...), ejecutores de comandos (xargs, nohup, env, ...), y similares. La coincidencia no distingue mayúsculas en el nombre base del comando. Conveniente cuando solo quieres probar cosas, pero demasiado grueso para uso real; prefiere commandPermissions.

networkPermissions (opcional). La lista blanca de red: una lista de patrones glob (solo * es un comodín) que todo host alcanzado a través de httpGet/httpPost/httpRequest debe coincidir. Al igual que commandPermissions, esto se superpone al conjunto por ámbito declarado por requestNetwork(...); un host debe estar en ambos. Cuando no se establece, solo se aplica la lista blanca por ámbito de requestNetwork.

allowedRoots (opcional). El límite exterior del sistema de archivos: una lista de rutas que confinan dónde puede operar requestFileSystem(root). Una raíz solicitada debe resolverse (símbolos incluidos) a una ruta igual o anidada bajo una de estas, o se deniega el acceso. Cuando no se establece, se usa por defecto el directorio de trabajo actual del servidor, por lo que el sandbox queda confinado a ese subárbol (cierre por fallo). Establézcalo explícitamente para ampliar o reubicar el límite. El enmascaramiento de rutas clasificadas sigue aplicándose dentro de cualquier raíz concedida.

Los enlaces simbólicos siempre se resuelven antes de la comprobación de contención, incluidos los colgantes (una escritura a través de un enlace colgante crea su destino, por lo que el destino es lo que se comprueba). Las entradas encontradas por children/walk (y por tanto por find/grepRecursive) que se resuelven fuera de la raíz concedida, como un enlace .venv/bin/python o node_modules/.bin, se omiten en lugar de seguirse o notificarse como errores.

secureOutput (opcional). Ruta a un archivo de solo añadidura que refleja cada llamada println/print/printf del aislamiento, pero con los valores Classified[_] desenvueltos. La salida principal del agente sigue mostrando la forma enmascarada (Classified(***)), por lo que solo quien pueda leer este archivo ve el contenido real. Los directorios padre se crean automáticamente, y un archivo de sumidero que aún no existe se crea con permisos solo para el propietario (rw-------) en sistemas POSIX (atómicamente, de modo que nunca existe con permisos más amplios). Un archivo existente se añade con sus permisos sin cambios. Cuando no se configura, la impresión se comporta con normalidad y no se escribe nada en el disco.

classifiedWrite (opcional, por defecto true; solo en configuración JSON). Cuando se establece en false, todas las escrituras a rutas clasificadas se deniegan: writeClassified(path, content), access(path).writeClassified(content) y mkdir() en una ruta clasificada. Tenga en cuenta que, como classify puede envolver cualquier valor, dejar esto habilitado significa que un agente puede sobrescribir archivos clasificados (p. ej. .ssh/authorized_keys) con contenido arbitrario: el mecanismo Classified protege la confidencialidad, no la integridad. Establezca esto en false en despliegues donde los archivos clasificados deban ser de solo lectura para el agente.

Patrones de Rutas Clasificadas

Los patrones de rutas clasificadas siguen la sintaxis de estilo gitignore. Una ruta se clasifica si coincide con un patrón o es descendiente de una coincidencia.

Patrón

Coincide con

Ejemplo

.ssh

Cualquier componente de ruta llamado .ssh

/home/user/.ssh/id_rsa

.env.*

Cualquier componente que coincida con el glob

/project/.env.local

config/*/keys

Relativo a la raíz del sistema de archivos, con comodín

<root>/config/prod/keys/secret.pem

**/secrets

secrets a cualquier profundidad

<root>/a/b/secrets/key.txt

/home/user/.ssh

Ruta absoluta (símbolos resueltos)

/home/user/.ssh/id_rsa

Reglas:

  • Sin / en el patrón: coincide con cualquier componente de ruta (coincidencia de nombre base)

  • Patrón relativo con /: anclado a la raíz del sistema de archivos; admite *, **, ?, […]

  • Patrón absoluto: se compara con la ruta completa; el prefijo no glob se resuelve a través de enlaces simbólicos

  • / final se elimina (sin distinción de solo directorio)

Patrones clasificados por defecto (cuando classifiedPaths no está configurado): .ssh, .gnupg, .env, .env.*, .netrc, .npmrc, .pypirc, .docker, .kube, .aws, .azure, .gcloud.

Herramientas

Herramienta

Parámetros

Descripción

execute_scala

code

Ejecuta un fragmento de Scala en un REPL nuevo (sin estado)

create_repl_session

-

Crea una sesión REPL persistente, devuelve session_id

execute_in_session

session_id, code

Ejecuta código en una sesión existente (con estado)

list_sessions

-

Lista los IDs de sesión activos

delete_repl_session

session_id

Elimina una sesión

show_interface

-

Muestra la referencia completa de la API de capacidades

Las sesiones están limitadas a 100 sesiones activas; create_repl_session devuelve un error al alcanzar el límite. La salida de ejecución está limitada a 10 MiB (el truncamiento se marca en el resultado), y exec captura como máximo 8 MiB de stdout y 8 MiB de stderr por invocación.

Ejemplo: Sesión con Estado

1. create_repl_session          → session_id: "abc-123"
2. execute_in_session(code: "val x = 42")   → x: Int = 42
3. execute_in_session(code: "x * 2")        → val res0: Int = 84
4. delete_repl_session(session_id: "abc-123")

Características de Seguridad

El sistema de tipos de TACIT proporciona tres garantías de seguridad que se mantienen independientemente de si el agente está desalineado, alucinando o bajo un ataque de inyección de prompts:

Propiedad

Qué significa

Seguridad de capacidades

Las capacidades no pueden falsificarse ni olvidarse. El agente solo puede acceder a recursos a través de capacidades explícitamente concedidas.

Completitud de capacidades

Las capacidades regulan todos los efectos relevantes para la seguridad. El agente interactúa con el mundo solo a través de sus capacidades concedidas.

Pureza local

Cálculos específicos pueden imponerse como libres de efectos secundarios. Esto evita la fuga de información cuando los agentes procesan datos clasificados.

API de Capacidades

La biblioteca expone tres métodos de solicitud de capacidades, cada uno limitando el acceso a un bloque. Las capacidades no pueden escapar de su bloque de alcance. Esto se impone en tiempo de compilación por el verificador de captura.

// File system: scoped to a root directory
requestFileSystem("/tmp/work") {
  val f = access("data.txt")
  f.write("hello")
  val lines = f.readLines()
  grep("data.txt", "hello")
  find(".", "*.txt")
}

// Process execution: scoped to an allowlist of commands
requestExecPermission(Set("ls", "cat")) {
  val result = exec("ls", List("-la"))
  println(result.stdout)
}

// Network: scoped to an allowlist of hosts
requestNetwork(Set("api.example.com")) {
  val body = httpGet("https://api.example.com/data")
  httpPost("https://api.example.com/submit", """{"key":"value"}""")
  // Arbitrary verbs with a status code:
  val resp = httpRequest("DELETE", "https://api.example.com/item/42")  // resp.status, resp.body
}

Los métodos de red también toman headers: Map[String, String] y secretHeaders: Map[String, Classified[String]] simples. Un valor de secretHeaders (p. ej. un token Authorization leído mediante readClassified) se envía al host de la lista de permitidos pero nunca es observable para el código del agente, lo que permite que un agente se autentique en una API permitida con un secreto que no puede leer de otro modo. httpPostClassified completa el cuadro: envía un cuerpo Classified[String] y devuelve una respuesta Classified[String], de modo que los datos sensibles pueden viajar de ida y vuelta a través de un servicio externo mientras permanecen bajo el control de flujo de información (ver más abajo).

Control de Flujo de Información mediante Classified

Considere un agente de código típico trabajando en un directorio de proyecto. Algunos archivos son ordinarios (código fuente, configuraciones de compilación, README). Otros son sensibles: claves API en .env, credenciales en secrets/, documentos internos. El agente está impulsado por un LLM alojado en la nube (un servicio de terceros). Queremos que el agente use o procese los datos sensibles (resumir documentos internos, rotar claves, procesar informes) pero nunca los filtre al proveedor de la nube.

TACIT resuelve esto mediante el tipo Classified[T]. Los archivos bajo rutas clasificadas designadas (configuradas mediante --classified-paths con patrones de estilo gitignore, p. ej. .ssh, .env.*, secrets, **/keys) devuelven su contenido envuelto en Classified[String] en lugar de String simple. Si no se configura de otro modo, las rutas de secretos comunes (.ssh, .gnupg, .env, .env.*, etc.) se clasifican por defecto. El sistema de tipos impone acceso solo puro: Classified.map acepta solo funciones puras (T -> U), lo que significa sin efectos y sin capacidades capturadas. Puede transformar los datos, pero no puede enviarlos a ningún lugar. Cualquier intento de exfiltrar datos clasificados se rechaza en tiempo de compilación:

requestFileSystem("/project") {
  val secret = readClassified("secrets/api-key.txt")

  // Compile error: map captures the file capability, not a pure function
  secret.map: s =>
    access("exfil.txt").write(s) // error: capturing f is not allowed
    s

  // Compile error: print out the classified content to the cloud LLM
  secret.map: s =>
    println(s) // error: capturing IOCapability is not allowed
    s
}

Entonces, ¿cómo puede el agente hacer un trabajo útil con datos clasificados? A través de un diseño LLM dual: un LLM local confiable separado procesa el contenido clasificado. El framework proporciona una sobrecarga de chat que acepta Classified[String] y devuelve Classified[String]. El LLM confiable ve el contenido, pero el resultado permanece envuelto y nunca puede fluir de vuelta al modelo de nube no confiable.

Flujo de Datos Clasificados

requestFileSystem("/project") {
  // OK: read classified content
  val doc = readClassified("secrets/contract-v2.txt")

  // OK: pure transformation
  val upper = doc.map(_.trim)

  // OK: send to trusted local LLM, result stays Classified
  val summary = chat(doc.map(s => s"Summarize the following document:\n$s"))
  // summary: Classified[String], content is still protected

  // OK: write back to a classified file
  writeClassified("secrets/summary.txt", summary)
}

Además del LLM confiable y los archivos clasificados, un valor Classified también puede fluir hacia un host de red de la lista de permitida sin ser desclasificado, ya sea como un encabezado de solicitud secreto (p. ej. autenticación a una API permitida) o como un cuerpo POST clasificado cuya respuesta permanece envuelta:

requestNetwork(Set("api.example.com")) {
  requestFileSystem("/project") {
    val key = readClassified("secrets/api.key")

    // OK: the token reaches the allowlisted host as a header, but is never
    // observable to agent code (the value cannot be printed or inspected).
    val me = httpGet("https://api.example.com/me",
                     secretHeaders = Map("Authorization" -> key.map("Bearer " + _)))

    // OK: secret body in, Classified response out.
    val payload = readClassified("secrets/report.json")
    val reply = httpPostClassified("https://api.example.com/process", payload)
    // reply: Classified[String]
  }
}

Modo Seguro

El código generado por el agente se compila bajo el modo seguro de Scala 3 (import language.experimental.safe), que impone un subconjunto de lenguaje seguro de capacidades:

  1. Sin conversiones de tipo no verificadas ni coincidencias de patrones

  2. Sin características del módulo caps.unsafe

  3. Sin anotaciones @unchecked

  4. Sin reflexión en tiempo de ejecución

  5. Compilar con verificación de captura y nulos explícitos habilitados, rastreando todos los efectos de mutación

  6. Los objetos y funciones globales son accesibles solo si se implementan de forma segura

Estas restricciones evitan que los agentes "olviden" capacidades a través de castings inseguros, reflexión o agujeros del sistema de tipos. El código que no pasa la compilación nunca se ejecuta.

El modo seguro es una característica experimental aún en desarrollo activo. Por defecto, TACIT usa un validador de código estático que verifica patrones prohibidos para imponer el subconjunto de modo seguro. La bandera --safe-mode (o "safeMode": true en la configuración JSON) además importa language.experimental.safe en cada ejecución REPL, optando por la aplicación en el compilador de Scala 3.

Tiempo de Espera de Ejecución

--exec-timeout-ms <ms> (o "executionTimeoutMs" en la configuración JSON) limita el tiempo de reloj de pared de una sola evaluación REPL. El valor debe ser positivo; los valores cero o negativos se rechazan al inicio. En caso de tiempo de espera, el cliente recibe un error de prompt en lugar de colgarse, y para sesiones con estado la sesión conserva su estado anterior, por lo que la declaración abandonada no tiene efecto observable.

El vigilante ejecuta cada evaluación en un hilo de trabajo y es de mejor esfuerzo: el trabajo que responde a interrupciones (I/O bloqueante, sueños, la mayoría de las llamadas de biblioteca) se limita de manera confiable, pero un bucle de CPU puro que nunca verifica la interrupción sigue ejecutándose en segundo plano y continúa manteniendo el bloqueo de salida del REPL. La preempción dura requeriría aislamiento a nivel de proceso; este control es una protección de robustez, no un límite de sandbox. Cuando no se configura (el valor por defecto), las evaluaciones se ejecutan sin tiempo de espera.

Integración con LLM

Un LLM secundario está disponible a través del método chat, sin necesidad de alcance de capacidad. La seguridad proviene del sistema de tipos Classified: chat(String): String para datos regulares, chat(Classified[String]): Classified[String] para datos sensibles.

// Regular chat
val answer = chat("What is 2 + 2?")

// Classified chat: input and output stay wrapped
requestFileSystem("/secrets") {
  val secret = readClassified("/secrets/key.txt")
  val result = chat(secret.map(s => s"Summarize: $s"))
  // result is Classified[String], cannot be printed or leaked
}

Configúrelo mediante banderas CLI (--llm-base-url, --llm-api-key, --llm-model) o un archivo de configuración JSON (--config). Se admite cualquier API compatible con OpenAI.

Resultados Experimentales

Evaluamos TACIT en seguridad y expresividad (ver la sección 4 del documento para detalles completos).

Seguridad (RQ1). En modo clasificado (secretos envueltos en Classified[String]), tanto Claude Sonnet 4.6 como MiniMax M2.5 logran 100% de seguridad en los 131 ensayos. Cada inyección y tarea maliciosa es bloqueada por el sistema de tipos. La utilidad sigue siendo alta (99.2% para Sonnet, 90.0% para MiniMax).

Expresividad (RQ2). En τ2-bench y SWE-bench Lite, los agentes que usan el arnés de capacidades seguras de TACIT igualan o superan ligeramente los puntos de referencia estándar de llamada a herramientas en todos los modelos probados (gpt-oss-120b, MiniMax M2.5, DeepSeek V3.2), demostrando que escribir Scala con seguridad de tipos no degrada el rendimiento agéntico.

Extender la Biblioteca: Añadir su Propia API

La biblioteca (library/) define la API de capacidades que el código de usuario puede llamar dentro del REPL. Para implementar permisos personalizados y control de acceso de grano fino, puede añadir nuevas capacidades (p. ej., acceso a bases de datos, colas de mensajes, gestión de servidores) modificando la biblioteca y reconstruyendo solo el JAR de la biblioteca.

Estructura de la Biblioteca

library/
├── Interface.scala          # Public API trait (what user code sees)
├── impl/
│   ├── InterfaceImpl.scala  # Wires everything together (exports Ops objects)
│   ├── BaseFileSystem.scala    # Shared path validation and gitignore-style classified-path matching
│   ├── FileOps.scala           # grep, grepRecursive, find
│   ├── ProcessOps.scala        # exec, execOutput
│   ├── WebOps.scala            # httpGet, httpPost, httpRequest, httpPostClassified
│   ├── LlmOps.scala            # chat
│   ├── RealFileSystem.scala    # FileSystem on real disk
│   ├── VirtualFileSystem.scala # In-memory FileSystem (for testing)
│   ├── ClassifiedImpl.scala    # Classified[T] wrapper implementation
│   ├── ProcessPermissionImpl.scala # Concrete ProcessPermission
│   ├── NetworkImpl.scala       # Concrete Network
│   ├── GlobMatcher.scala       # Shared `*`-glob to regex utility
│   ├── LibraryConfig.scala     # Library configuration with JSON parsing
│   └── LlmConfig.scala        # LLM configuration case class
└── test/                    # Library-level tests

Paso a paso: Añadir una nueva API

Aquí hay un ejemplo de cómo añadir una capacidad hipotética requestDatabase.

1. Define los tipos y la capacidad en Interface.scala

// Add a result type
case class QueryResult(columns: List[String], rows: List[List[String]])

// Add a capability class. Note the `private[library]` constructor: capability
// classes must not be constructible or extendable by agent code.
class DatabasePermission private[library] (val connectionString: String) extends caps.SharedCapability

// Add methods to the Interface trait
trait Interface:
  // ... existing methods ...

  def requestDatabase[T](connectionString: String)(op: DatabasePermission^ ?=> T)(using IOCapability): T

  def query(sql: String)(using DatabasePermission): QueryResult

Puntos clave:

  • La clase de capacidad debe extender caps.SharedCapability. Esto es lo que permite al comprobador de captura de Scala 3 evitar que la capacidad escape de su bloque con ámbito.

  • El método request* toma un bloque op que recibe la capacidad como parámetro de contexto (?=>). La marca ^ significa que la capacidad es rastreada por el comprobador de captura.

  • Los métodos de operación (como query) toman la capacidad como parámetro using, por lo que solo se pueden llamar dentro del bloque request* correspondiente.

2. Implementa las operaciones en impl/

Crea library/impl/DatabaseOps.scala:

package tacit.library

import language.experimental.captureChecking

object DatabaseOps:
  def query(sql: String)(using perm: DatabasePermission): QueryResult =
    // Your implementation here
    // perm.connectionString has the connection info
    ???

3. Conéctalo en InterfaceImpl

En library/impl/InterfaceImpl.scala, exporta tus nuevas operaciones e implementa el método request*:

abstract class InterfaceImpl private[library] (...) extends Interface:
  export FileOps.*
  export ProcessOps.*
  export WebOps.*
  export DatabaseOps.*   // ← add this

  // ... existing methods ...

  def requestDatabase[T](connectionString: String)(op: DatabasePermission^ ?=> T)(using IOCapability): T =
    val perm = new DatabasePermission(connectionString)
    op(using perm)

4. Bloquea el acceso directo en el validador (lado del servidor)

Si tu nueva API envuelve una biblioteca Java/Scala a la que los usuarios no deberían llamar directamente, añade patrones prohibidos a src/main/scala/executor/CodeValidator.scala:

ForbiddenPattern("db-jdbc", raw"java\.sql\b".r, "Direct JDBC access is forbidden; use requestDatabase"),
ForbiddenPattern("db-driver", raw"DriverManager".r, "DriverManager is forbidden; use requestDatabase"),

Esto garantiza que el código del usuario pase por la API de capacidades en lugar de omitirla.

5. Añade dependencias (si es necesario)

Si tu nueva API requiere bibliotecas externas, añádelas al proyecto lib en build.sbt:

lazy val lib = project
  .in(file("library"))
  .settings(
    // ... existing settings ...
    libraryDependencies ++= Seq(
      "com.openai" % "openai-java" % "4.38.0",
      "org.postgresql" % "postgresql" % "42.7.3",  // ← add your dep
    ),
  )

6. Reconstruye el JAR de la biblioteca

sbt "lib/assembly"

No necesitas reconstruir el JAR del servidor a menos que hayas cambiado CodeValidator (paso 4) u otro código del lado del servidor. Solo apunta el servidor al nuevo JAR de la biblioteca:

java -jar server.jar --library-jar new-library.jar

7. Prueba tu nueva API en el REPL de desarrollo

Para iterar rápidamente sin iniciar un agente, lanza el REPL de desarrollo, un prompt interactivo de Scala precargado con la API de capacidades y el mismo CodeValidator que usa el servidor MCP:

sbt devRepl                                  # default config
sbt "devRepl --strict --config my.json"      # with flags

Cosas a tener en cuenta

  • Las capacidades deben extender caps.SharedCapability. Esto es lo que hace que funcione la comprobación de captura. Sin ello, el compilador no puede rastrear el ámbito de la capacidad y los usuarios podrían filtrarla fuera del bloque request*.

  • Las clases de capacidades y sus implementaciones están selladas. Todos los tipos de capacidades (FileSystem, Network, ProcessPermission, IOCapability, Classified, FileEntry) y todas las clases de implementación (RealFileSystem, NetworkImpl, LlmOps, ...) tienen constructores private[library], por lo que el código del agente no puede ni instanciarlas ni extenderlas: las capacidades solo pueden provenir de los ámbitos request*. Mantén esta invariante para cualquier capacidad que añadas: dale a la clase un constructor private[library] y haz que las implementaciones concretas también sean private[library].

  • La interfaz en sí también está sellada. El constructor de InterfaceImpl es private[library], por lo que nada fuera de la biblioteca puede elegir el JSON de políticas. El servidor registra la configuración de la biblioteca una vez por sandbox (InterfaceImpl.configure, llamado a través del cargador de clases del REPL antes de que se ejecute cualquier código), y el preámbulo instancia el SandboxInterface sin parámetros, cuya política es esa configuración registrada. El código del agente que extiende SandboxInterface solo obtiene una interfaz idéntica y vinculada a la política, nunca una más amplia.

  • La comprobación de captura es experimental. El proyecto usa -language:experimental.captureChecking. El comportamiento del compilador puede cambiar entre versiones nightly de Scala 3. Si encuentras errores inesperados, comprueba si el problema es la comprobación de captura eliminando temporalmente la bandera.

  • La biblioteca usa Scala 3 nightly. La compilación descarga automáticamente la última versión nightly de Scala 3. Esto significa que tu código debe ser compatible con Scala de última generación. Fija una versión específica en build.sbt (val scala3Version = "3.x.y") si necesitas estabilidad.

  • Interface.scala se incluye como recurso. El servidor copia Interface.scala en sus recursos en tiempo de compilación para que la herramienta show_interface pueda mostrarlo. Si añades nuevas APIs, los usuarios las verán automáticamente a través de show_interface, sin trabajo adicional.

  • Los patrones prohibidos se aplican al código del usuario, no al código de la biblioteca. El validador en CodeValidator.scala solo comprueba el código enviado por el usuario. La biblioteca en sí puede usar libremente java.io, java.net, ProcessBuilder, etc. en su implementación. Pero si tu nueva API envuelve una API de Java, deberías añadir un patrón prohibido correspondiente para que los usuarios no puedan omitir tu envoltorio de capacidades.

  • El JAR de la biblioteca es un JAR gordo. sbt "lib/assembly" produce un JAR que incluye todas las dependencias de la biblioteca (p. ej., openai-java). Si añades una dependencia, se incluirá automáticamente.

  • El servidor depende de los tipos de la biblioteca en tiempo de compilación. El servidor depende del tipo de interfaz para ejecutar el REPL. Asegúrate de que tu cambio sea compatible con la interfaz esperada por el servidor.

  • Prueba tu API a nivel de biblioteca primero. El directorio library/test/ contiene pruebas a nivel de biblioteca que usan MUnit, ejecutadas con scala-cli test library --server=false (no forman parte de sbt test; --server=false evita un conflicto de ASM entre las versiones nightly actuales de Scala 3 y el servidor Bloop incluido con scala-cli). Prueba tus nuevas operaciones allí antes de hacer pruebas de integración a través del servidor MCP. Consulta LibrarySuite.test.scala para ver ejemplos.

Desarrollo

Requisitos:

  • JDK 17+

  • sbt 1.12+

sbt clean                      # Clean build artifacts
sbt compile                    # Compile
sbt test                       # Run the server test suites (src/test/scala)
sbt "testOnly *McpServerSuite" # Run a single server suite
scala-cli test library --server=false   # Run the library test suites (library/test)
sbt assembly                   # Build both JARs (server + library)
sbt "lib/assembly"             # Build library JAR only
sbt devRepl                    # Interactive REPL for testing the library

sbt test ejecuta solo las suites del servidor. Las suites de library/test/ se ejecutan por separado con scala-cli test library --server=false.

# Basic
java -jar target/scala-*/TACIT-assembly-*.jar \
  --library-jar library/target/scala-*/TACIT-library.jar

# With logging
java -jar server.jar --library-jar library.jar --record ./log

# With JSON config
java -jar server.jar --library-jar library.jar --config config.json

Citación

@inbook{10.1145/3786335.3813127,
author = {Odersky, Martin and Zhao, Yaoyu and Xu, Yichen and Bra\v{c}evac, Oliver and Pham, Cao Nguyen},
title = {Securing Agents With Tracked Capabilities},
year = {2026},
isbn = {9798400724152},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
url = {https://doi.org/10.1145/3786335.3813127},
booktitle = {Proceedings of the ACM Conference on AI and Agentic Systems},
pages = {812–838},
numpages = {27}
}

Licencia

Apache-2.0

A
license - permissive license
Not graded
quality - not tested
A
maintenance

Maintenance

UpdatingMaintainers
UpdatingResponse time
2wRelease cycle
10Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A unified MCP server providing observability, safety control, and behavior evolution for high-agency AI agents through tracing, replaying, and auditing. It features real-time firewall guardrails and ML-driven anomaly detection to monitor, block, or fork agent actions based on risk.
    7
  • A
    license
    C
    quality
    B
    maintenance
    Agent-first programming language: agents produce JSON AST, the compiler validates, type-checks, effect-checks, verifies contracts via Z3/SMT, and compiles to WASM. 19 MCP tools for the full compile-and-execute loop.
    22
    123
    11
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lampepfl/TACIT'

If you have feedback or need assistance with the MCP directory API, please join our Discord server