mcp-tacit
TACIT: Tracked Agent Capabilities In Types
Статья: Securing Agents With Tracked Capabilities (ACM) · arXiv:2603.00991 · 🏆 Награда за лучшую статью на CAIS 26
TACIT (Tracked Agent Capabilities In Types) — это механизм безопасности для ИИ-агентов. Вместо прямого вызова инструментов агенты пишут код на Scala 3 с проверкой захватов: система типов статически отслеживает возможности и гарантирует, что код агента не может подделать права доступа, не может выполнять эффекты сверх своего бюджета и не может допускать утечки информации из чистых подвычислений. Он предоставляет интерфейс MCP, так что его легко могут использовать все MCP-совместимые агенты.

Фреймворк состоит из трёх основных компонентов:
Компилятор Scala 3. Код, отправленный агентом, проверяется и типизируется с включённой проверкой захватов в безопасном режиме, который обеспечивает подмножество языка, безопасное с точки зрения возможностей.
Scala REPL. Локальный экземпляр REPL выполняет скомпилированный код и управляет состоянием между взаимодействиями. Поддерживает как одноразовое выполнение без состояния, так и сеансы с состоянием.
Библиотека безопасности возможностей. Типизированный API, служащий единственным шлюзом, через который код агента взаимодействует с реальным миром: файловая система, запуск процессов, сеть и субагенты. Библиотека расширяема: новые возможности добавляются изменением только кода библиотеки, без изменения самого MCP-сервера.
Быстрый старт
TACIT предоставляет стандартный MCP-сервер, который общается по JSON-RPC через stdio. Он работает с любым MCP-совместимым агентом, включая Claude Code, OpenCode, GitHub Copilot и другие.
Требуется JDK 17+.
Установка TACIT
Выберите один из способов установки ниже. Рекомендуется использовать CLI-обёртку tacit.
Вариант 1: установка tacit (рекомендуется)
tacit — небольшая команда-обёртка для локального управления TACIT. Выполните tacit setup один раз, чтобы установить команду и загрузить последний релиз, tacit update — чтобы обновить JAR-файлы, tacit self update — чтобы обновить саму обёртку, и tacit serve — чтобы запустить 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Эта команда устанавливает tacit в ~/.local/bin, проверяет, что ~/.local/bin есть в PATH, и загружает последний релиз в ~/.cache/tacit/.
Основные команды:
# 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По умолчанию tacit использует:
Компонент | Путь по умолчанию |
MCP-сервер |
|
Библиотека |
|
Вариант 2: прямая загрузка готовых релизных JAR-файлов
Если вы не хотите использовать обёртку, воспользуйтесь скриптом загрузки релизов.
# 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Опционально:
# 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По умолчанию загружаются:
JAR | Путь по умолчанию |
MCP-сервер |
|
Библиотека |
|
И обёртка, и скрипт проверяют загруженные JAR-файлы на соответствие SHA-256-дайджестам, опубликованным в метаданных релиза, и отказываются устанавливать JAR, чей дайджест отсутствует или не совпадает. Загрузки попадают во временную папку и перемещаются на место только после проверки, так что неудачная загрузка никогда не заменяет ранее установленный JAR-файл.
Чтобы собрать из текущего дерева исходников, см. Вариант 3 ниже.
Вариант 3: сборка из исходников
Требуются JDK 17+ и sbt 1.12+.
git clone https://github.com/lampepfl/tacit.git
cd tacit
./build.shОпционально:
# Build and copy JARs into a custom directory
./build.sh ./dist
# Show full sbt output while building
./build.sh --verboseПри этом собираются и копируются два JAR-файла:
JAR | Путь |
MCP-сервер |
|
Библиотека |
|
После установки TACIT любым из перечисленных выше способов настройте своего агента на запуск MCP-сервера.
Настройка агента
Добавьте TACIT как MCP-сервер в конфигурацию агента. Если вы установили CLI tacit, просто используйте tacit serve. Если вы установили TACIT вручную, используйте явную форму java -jar ... --library-jar ....
Добавьте в .mcp.json вашего проекта (или в ~/.claude.json для глобальной настройки).
С tacit:
{
"mcpServers": {
"tacit": {
"command": "tacit",
"args": ["serve"]
}
}
}С указанием путей к JAR вручную:
{
"mcpServers": {
"tacit": {
"command": "java",
"args": [
"-jar", "/path/to/TACIT.jar",
"--library-jar", "/path/to/TACIT-library.jar"
]
}
}
}Добавьте в ваш opencode.json.
С tacit:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"tacit": {
"type": "local",
"enabled": true,
"command": ["tacit", "serve"]
}
}
}С указанием путей к JAR вручную:
{
"$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"
]
}
}
}Добавьте в ваш .vscode/mcp.json.
С tacit:
{
"servers": {
"tacit": {
"command": "tacit",
"args": ["serve"]
}
}
}С указанием путей к JAR вручную:
{
"servers": {
"tacit": {
"command": "java",
"args": [
"-jar", "/path/to/TACIT.jar",
"--library-jar", "/path/to/TACIT-library.jar"
]
}
}
}Теперь ваш агент может использовать инструменты TACIT для выполнения Scala-кода в песочнице.
Рекомендуется: отключение встроенных инструментов
Чтобы в полной мере воспользоваться преимуществами безопасности TACIT, основанной на возможностях, отключите встроенные инструменты агента для работы с файлами, оболочкой и сетью, чтобы все операции проходили через REPL в песочнице.
Запустите с --disallowedTools, чтобы заблокировать встроенные инструменты:
claude --disallowedTools "Bash,Read,Write,Edit,WebFetch"Или добавьте в .claude/settings.json вашего проекта:
{
"permissions": {
"disallowedTools": ["Bash", "Read", "Write", "Edit", "WebFetch"]
}
}Установите для встроенных инструментов значение "deny" в 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": { "..." : "..." }
}
}В вашем settings.json в VS Code ограничьте инструменты, доступные Copilot:
{
"github.copilot.chat.agent.tools": {
"terminal": false,
"fs_read": false,
"fs_write": false
}
}Related MCP server: edict-lang
Конфигурация
Сервер можно настраивать через CLI-флаги или JSON-файл конфигурации. Передавайте флаги напрямую в MCP-аргументы агента или используйте --config, чтобы указать JSON-файл.
Конфигурация разделена на конфигурацию сервера (транспорт, запись, сеансы) и конфигурацию библиотеки (поведение песочницы, возможности). В JSON-файле конфигурации настройки библиотеки находятся в ключе libraryConfig и передаются напрямую библиотеке для обработки.
CLI-флаги
Флаги сервера:
Флаг | Описание |
| Обязательный. Путь к библиотечному JAR-файлу ( |
| Записывать каждое выполнение на диск |
| Подавить стартовый баннер и журналирование запросов/ответов |
| Отключить инструменты, связанные с сеансами |
| Включить/отключить |
| Тайм-аут по реальному времени для одного вычисления в REPL (по умолчанию: нет; см. Тайм-аут выполнения) |
| JSON-файл конфигурации (флаги после |
Флаги библиотеки (краткая форма для некоторых полей libraryConfig):
Флаг | Описание |
| Блокирует через exec встроенный список запрещённых небезопасных команд (файловые операции, оболочки, интерпретаторы, сетевые инструменты, запускатели команд, ...); сопоставление не зависит от регистра. Удобно для быстрых экспериментов; для реальных развёртываний предпочитайте |
| Разделённые запятыми glob-шаблоны выполняемых команд (например, |
| Разделённые запятыми glob-шаблоны доступных хостов (например, |
| Разделённые запятыми внешние границы для корней |
| Разделённые запятыми шаблоны классифицированных путей (в стиле gitignore, см. ниже) |
| Базовый URL API LLM |
| Ключ API LLM |
| Имя модели LLM |
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 (необязательно). Разрешённый список для exec: список glob-шаблонов (в качестве подстановочного знака используется только *), которым должна соответствовать каждая команда, передаваемая в exec. Он действует поверх набора, объявленного для конкретной области через requestExecPermission(...); команда должна быть в обоих, чтобы реально выполниться. Если задано, strictMode игнорируется. В реальных развёртываниях всегда следует задавать этот список явно.
strictMode (необязательно, по умолчанию true). Быстрый вариант для экспериментов, который блокирует через exec встроенный список запрещённых небезопасных команд: команды файловых операций (cat, ls, rm, tar, chmod, ...), оболочки, интерпретаторы (python, node, perl, ...), сетевые инструменты (curl, wget, ssh, ...), запускатели команд (xargs, nohup, env, ...) и тому подобное. Сопоставление не зависит от регистра и применяется к базовому имени команды. Удобно, когда хочется просто попробовать, но слишком грубо для реального использования; предпочитайте commandPermissions.
networkPermissions (необязательно). Разрешённый список для сети: список glob-шаблонов (в качестве подстановочного знака используется только *), которым должен соответствовать каждый хост, доступ к которому осуществляется через httpGet/httpPost/httpRequest. Как и commandPermissions, это действует поверх набора, объявленного для конкретной области через requestNetwork(...); хост должен быть в обоих. Если не задано, применяется только разрешённый список requestNetwork для конкретной области.
allowedRoots (необязательно). Внешняя граница файловой системы: список путей, ограничивающих область, где может действовать requestFileSystem(root). Запрошенный корень должен разрешаться (включая символические ссылки) в путь, равный одному из этих путей или вложенный в него, иначе доступ запрещён. Если параметр не задан, по умолчанию используется текущий рабочий каталог сервера, поэтому песочница ограничена этим поддеревом (отказ по умолчанию). Задайте его явно, чтобы расширить или переместить границу. Маскирование классифицированных путей по-прежнему применяется в пределах любого предоставленного корня.
Символические ссылки всегда разрешаются до проверки ограничений, включая висячие (запись через висячую ссылку создаёт её цель, поэтому проверяется именно цель). Записи, найденные через children/walk (и, следовательно, через find/grepRecursive), которые разрешаются за пределы предоставленного корня, например .venv/bin/python или node_modules/.bin ссылка, опускаются, а не отслеживаются и не сообщаются как ошибки.
secureOutput (необязательно). Путь к файлу, доступному только для добавления, который зеркалирует каждый вызов println/print/printf из изоляции, но со значениями Classified[_] развёрнутыми. Основной вывод агента по-прежнему показывает замаскированную форму (Classified(***)), поэтому только тот, кто может прочитать этот файл, видит реальное содержимое. Родительские каталоги создаются автоматически, а файл-приёмник, который ещё не существует, создаётся с правами только для владельца (rw-------) на POSIX-системах (атомарно, поэтому он никогда не существует с более широкими правами). Существующий файл дополняется без изменения его прав. Если параметр не задан, печать ведёт себя нормально, и ничего не записывается на диск.
classifiedWrite (необязательно, по умолчанию true; только в JSON-конфигурации). Если установлено значение false, все записи в классифицированные пути запрещены: writeClassified(path, content), access(path).writeClassified(content) и mkdir() для классифицированного пути.
Обратите внимание, что поскольку classify может обернуть любое значение, оставление этой опции включённой означает, что агент может перезаписать классифицированные файлы (например, .ssh/authorized_keys) произвольным содержимым: механизм Classified защищает конфиденциальность, а не целостность. Установите false в развёртываниях, где классифицированные файлы должны быть доступны агенту только для чтения.
Шаблоны классифицированных путей
Шаблоны классифицированных путей используют синтаксис в стиле gitignore. Путь классифицируется, если он соответствует шаблону или является потомком совпадения.
Шаблон | Совпадения | Пример |
| Любой компонент пути с именем |
|
| Любой компонент, соответствующий glob |
|
| Относительно корня файловой системы, с wildcard |
|
|
|
|
| Абсолютный путь (символические ссылки разрешены) |
|
Правила:
Нет
/в шаблоне: сопоставляется с любым компонентом пути (сопоставление по базовому имени)Относительный шаблон с
/: привязан к корню файловой системы; поддерживает*,**,?,[…]Абсолютный шаблон: сопоставляется с полным путём; не-glob префикс разрешается через символические ссылки
Завершающий
/удаляется (без различия только для каталогов)
Стандартные классифицированные шаблоны (когда classifiedPaths не настроен): .ssh, .gnupg, .env, .env.*, .netrc, .npmrc, .pypirc, .docker, .kube, .aws, .azure, .gcloud.
Инструменты
Инструмент | Параметры | Описание |
|
| Выполнить Scala-фрагмент в новом REPL (без состояния) |
| - | Создать постоянную REPL-сессию, возвращает |
|
| Выполнить код в существующей сессии (с состоянием) |
| - | Список активных ID сессий |
|
| Удалить сессию |
| - | Показать полный справочник API возможностей |
Сессии ограничены 100 активными сессиями; create_repl_session возвращает ошибку при достижении лимита. Вывод выполнения ограничен 10 МиБ (усечение помечается в результате), а exec захватывает не более 8 МиБ stdout и 8 МиБ stderr за вызов.
Пример: Сессия с состоянием
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")Функции безопасности
Система типов TACIT обеспечивает три гарантии безопасности, которые действуют независимо от того, является ли агент несоответствующим, галлюцинирует или находится под атакой с внедрением промптов:
Свойство | Что это означает |
Безопасность возможностей | Возможности нельзя подделать или забыть. Агент может получать доступ к ресурсам только через явно предоставленные ему возможности. |
Полнота возможностей | Возможности регулируют все эффекты, влияющие на безопасность. Агент взаимодействует с миром только через свои предоставленные возможности. |
Локальная чистота | Конкретные вычисления могут быть принудительно выполнены без побочных эффектов. Это предотвращает утечку информации при обработке классифицированных данных агентами. |
API возможностей
Библиотека предоставляет три метода запроса возможностей, каждый из которых ограничивает доступ блоком. Возможности не могут выйти за пределы своего блока. Это обеспечивается на этапе компиляции проверкой захвата.
// 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
}Сетевые методы также принимают обычные headers: Map[String, String] и
secretHeaders: Map[String, Classified[String]]. Значение secretHeaders (например,
токен Authorization, прочитанный через readClassified) отправляется на разрешённый
хост, но никогда не доступен коду агента, что позволяет агенту аутентифицироваться
в разрешённом API с секретом, который он не может прочитать иначе. httpPostClassified
завершает картину: он отправляет тело Classified[String] и возвращает
ответ Classified[String], поэтому чувствительные данные могут проходить через
внешний сервис, оставаясь под контролем информационных потоков (см. ниже).
Контроль информационных потоков через Classified
Рассмотрим типичного агента кода, работающего с каталогом проекта. Некоторые файлы обычные (исходный код, конфигурации сборки, README). Другие чувствительные: ключи API в .env, учётные данные в secrets/, внутренние документы. Агент работает на облачной LLM (сторонний сервис). Мы хотим, чтобы агент использовал или обрабатывал чувствительные данные (резюмировал внутренние документы, ротировал ключи, обрабатывал отчёты), но никогда не передавал их облачному провайдеру.
TACIT решает эту проблему с помощью типа Classified[T]. Файлы в назначенных классифицированных путях (настраивается через --classified-paths с шаблонами в стиле gitignore, например .ssh, .env.*, secrets, **/keys) возвращают своё содержимое, обёрнутое в Classified[String], вместо обычного String. Если не настроено иное, общие секретные пути (.ssh, .gnupg, .env, .env.* и т.д.) классифицируются по умолчанию. Система типов обеспечивает доступ только для чистых функций: Classified.map принимает только чистые функции (T -> U), то есть без эффектов и без захваченных возможностей. Вы можете преобразовывать данные, но не можете отправить их куда-либо. Любая попытка вывода классифицированных данных отклоняется на этапе компиляции:
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
}Так как же агент может выполнять полезную работу с классифицированными данными? Через двойную LLM конструкцию: отдельная доверенная локальная LLM обрабатывает классифицированное содержимое. Фреймворк предоставляет перегрузку chat, которая принимает Classified[String] и возвращает Classified[String]. Доверенная LLM видит содержимое, но результат остаётся обёрнутым и никогда не может попасть обратно в недоверенную облачную модель.

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)
}Помимо доверенной LLM и классифицированных файлов, значение Classified может также
выходить на разрешённый сетевой хост без рассекречивания, либо как секретный
заголовок запроса (например, аутентификация в разрешённом API), либо как классифицированное
тело POST, ответ которого остаётся обёрнутым:
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]
}
}Безопасный режим
Код, сгенерированный агентом, компилируется в безопасном режиме Scala 3 (import language.experimental.safe), который обеспечивает подмножество языка, безопасное для возможностей:
Нет непроверенных приведений типов или сопоставлений с образцом
Нет функций из модуля
caps.unsafeНет аннотаций
@uncheckedНет рефлексии во время выполнения
Компиляция с проверкой захвата и явными null, отслеживание всех эффектов мутации
Глобальные объекты и функции доступны только в том случае, если они реализованы безопасно
Эти ограничения предотвращают "забывание" возможностей агентами через непроверенные приведения, рефлексию или дыры в системе типов. Код, не прошедший компиляцию, никогда не выполняется.
Безопасный режим — это экспериментальная функция, находящаяся в активной разработке. По умолчанию TACIT использует статический валидатор кода, который проверяет запрещённые шаблоны для обеспечения подмножества безопасного режима. Флаг --safe-mode (или "safeMode": true в JSON-конфигурации) дополнительно импортирует language.experimental.safe в каждое выполнение REPL, включая встроенную проверку компилятора Scala 3.
Тайм-аут выполнения
--exec-timeout-ms <ms> (или "executionTimeoutMs" в JSON-конфигурации) ограничивает
время выполнения одной оценки REPL по настенным часам. Значение должно быть положительным;
нулевые или отрицательные значения отклоняются при запуске. По истечении тайм-аута клиент получает
понятную ошибку вместо зависания, а для сессий с состоянием сессия сохраняет
своё предыдущее состояние, поэтому заброшенный оператор не имеет наблюдаемого эффекта.
Сторожевой таймер запускает каждую оценку в рабочем потоке и является best-effort: работа, реагирующая на прерывание (блокирующий ввод-вывод, ожидания, большинство вызовов библиотек), надёжно ограничена, но чистый цикл CPU, который никогда не проверяет прерывание, продолжает работать в фоновом режиме и продолжает удерживать блокировку вывода REPL. Жёсткое вытеснение потребовало бы изоляции на уровне процессов; этот параметр — защита от сбоев, а не граница песочницы. Если не задано (по умолчанию), оценки выполняются без тайм-аута.
Интеграция LLM
Вторичная LLM доступна через метод chat, не требуется область возможностей. Безопасность обеспечивается системой типов Classified: chat(String): String для обычных данных, chat(Classified[String]): Classified[String] для чувствительных данных.
// 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
}Настройка через флаги CLI (--llm-base-url, --llm-api-key, --llm-model) или файл JSON-конфигурации (--config). Поддерживается любой API, совместимый с OpenAI.
Экспериментальные результаты
Мы оцениваем TACIT на безопасность и выразительность (см. статью Раздел 4 для полных деталей).
Безопасность (RQ1). В классифицированном режиме (секреты обёрнуты в Classified[String]) и Claude Sonnet 4.6, и MiniMax M2.5 достигают 100% безопасности во всех 131 испытаниях. Каждая инъекция и вредоносная задача блокируются системой типов. Полезность остаётся высокой (99.2% для Sonnet, 90.0% для MiniMax).
Выразительность (RQ2). На τ2-bench и SWE-bench Lite агенты, использующие безопасный для возможностей каркас TACIT, соответствуют или немного превосходят стандартные базовые показатели вызова инструментов на всех протестированных моделях (gpt-oss-120b, MiniMax M2.5, DeepSeek V3.2), что демонстрирует, что написание типобезопасного Scala не ухудшает производительность агента.
Расширение библиотеки: добавление собственного API
Библиотека (library/) определяет API возможностей, который пользовательский код может вызывать внутри REPL.
Для реализации пользовательских разрешений и детального контроля доступа вы можете добавить новые возможности (например, доступ к базе данных, очереди сообщений, управление серверами), изменив библиотеку и пересобрав только JAR-файл библиотеки.
Структура библиотеки
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Пошаговое руководство: добавление нового API
Вот пример добавления гипотетической возможности requestDatabase.
1. Определите типы и возможность в 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Ключевые моменты:
Класс возможности должен расширять
caps.SharedCapability. Именно это позволяет проверщику захватов Scala 3 предотвращать выход возможности за пределы её блока.Метод
request*принимает блокop, который получает возможность как контекстный параметр (?=>). Знак^означает, что возможность отслеживается проверщиком захватов.Методы операций (например,
query) принимают возможность как параметрusing, поэтому их можно вызывать только внутри соответствующего блокаrequest*.
2. Реализуйте операции в impl/
Создайте 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. Подключите его в InterfaceImpl
В library/impl/InterfaceImpl.scala экспортируйте новые операции и реализуйте метод 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. Заблокируйте прямой доступ в валидаторе (на стороне сервера)
Если ваш новый API оборачивает Java/Scala библиотеку, которую пользователи не должны вызывать напрямую, добавьте запрещённые шаблоны в 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"),Это гарантирует, что пользовательский код будет проходить через API возможностей, а не в обход его.
5. Добавьте зависимости (при необходимости)
Если вашему новому API требуются внешние библиотеки, добавьте их в проект lib в 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. Пересоберите JAR-файл библиотеки
sbt "lib/assembly"Вам не нужно пересобирать серверный JAR, если вы не меняли CodeValidator (шаг 4) или другой серверный код. Просто укажите серверу на новый JAR-файл библиотеки:
java -jar server.jar --library-jar new-library.jar7. Попробуйте новый API в dev REPL
Для быстрой итерации без запуска агента запустите dev REPL — интерактивную Scala-оболочку с предзагруженным API возможностей и тем же CodeValidator, который использует MCP-сервер:
sbt devRepl # default config
sbt "devRepl --strict --config my.json" # with flagsНа что обратить внимание
Возможности должны расширять
caps.SharedCapability. Именно это обеспечивает работу проверки захватов. Без этого компилятор не сможет отслеживать область действия возможности, и пользователи смогут вынести её за пределы блокаrequest*.Классы возможностей и их реализации запечатаны. Все типы возможностей (
FileSystem,Network,ProcessPermission,IOCapability,Classified,FileEntry) и все классы реализаций (RealFileSystem,NetworkImpl,LlmOps, ...) имеют конструкторыprivate[library], поэтому код агента не может ни создать их экземпляры, ни расширить их: возможности могут исходить только из областейrequest*. Соблюдайте этот инвариант для любой добавляемой возможности: дайте классу конструкторprivate[library]и сделайте конкретные реализации такжеprivate[library].Сам интерфейс тоже запечатан. Конструктор
InterfaceImplимеет модификаторprivate[library], поэтому ничто за пределами библиотеки не может выбрать JSON-политику. Сервер регистрирует конфигурацию библиотеки один раз на песочницу (InterfaceImpl.configure, вызывается через загрузчик классов REPL до выполнения любого кода), а преамбула создаётSandboxInterfaceбез параметров, политика которого — эта зарегистрированная конфигурация. Код агента, расширяющийSandboxInterface, получает только идентичный интерфейс, привязанный к политике, но никогда не более широкий.Проверка захватов экспериментальна. Проект использует
-language:experimental.captureChecking. Поведение компилятора может меняться между ночными сборками Scala 3. Если вы столкнулись с неожиданными ошибками, проверьте, связана ли проблема с проверкой захватов, временно удалив этот флаг.Библиотека использует ночные сборки Scala 3. Сборка автоматически загружает последнюю ночную сборку Scala 3. Это означает, что ваш код должен быть совместим с новейшей версией Scala. Если вам нужна стабильность, укажите конкретную версию в
build.sbt(val scala3Version = "3.x.y").Interface.scalaпоставляется как ресурс. Сервер копируетInterface.scalaв свои ресурсы во время сборки, чтобы инструментshow_interfaceмог его отобразить. Если вы добавите новые API, пользователи увидят их черезshow_interfaceавтоматически, без дополнительных действий.Запрещённые шаблоны применяются к пользовательскому коду, а не к коду библиотеки. Валидатор в
CodeValidator.scalaпроверяет только пользовательский код. Сама библиотека может свободно использоватьjava.io,java.net,ProcessBuilderи т.д. в своей реализации. Но если ваш новый API оборачивает Java API, вам следует добавить соответствующий запрещённый шаблон, чтобы пользователи не могли обойти вашу обёртку возможностей.JAR-файл библиотеки — это толстый JAR.
sbt "lib/assembly"создаёт JAR, включающий все зависимости библиотеки (например,openai-java). Если вы добавите зависимость, она будет включена автоматически.Сервер зависит от типов библиотеки во время компиляции. Сервер зависит от типа интерфейса для запуска REPL. Убедитесь, что ваше изменение совместимо с ожидаемым сервером интерфейсом.
Сначала тестируйте свой API на уровне библиотеки. Каталог
library/test/содержит тесты уровня библиотеки на MUnit, запускаемые командойscala-cli test library --server=false(они не являются частьюsbt test;--server=falseобходит конфликт ASM между текущими ночными сборками Scala 3 и сервером Bloop, входящим в состав scala-cli). Тестируйте новые операции там перед интеграционным тестированием через MCP-сервер. Примеры см. вLibrarySuite.test.scala.
Разработка
Требования:
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запускает только серверные наборы тестов. Наборыlibrary/test/запускаются отдельно командой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Цитирование
@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}
}Лицензия
Apache-2.0
This server cannot be installed
Maintenance
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
- FlicenseNot gradedqualityDmaintenanceA 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
- AlicenseCqualityBmaintenanceAgent-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.2212311MIT
- AlicenseNot gradedqualityCmaintenanceRuntime safety guardrails for AI coding agents. Checks file access, validates shell commands, and scores your repo's AI safety — all via MCP.58MIT
- FlicenseBqualityAmaintenanceA disciplined engineering harness for AI coding agents that provides ground-truth MCP tools and adversarial enforcement skills to transform generic LLMs into high-precision engineers.1001
Related MCP Connectors
An effect gate for AI agents: at-most-once side effects, spend limits, and signed receipts.
Security firewall for AI agents — scans MCP calls for injection, secrets, and risks.
MCP-native Trust Infrastructure for AI Agents. Persistent encrypted memory with Trust Quotient.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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