mcpField
T
TACIT: Tracked Agent Capabilities In Types

TACIT: Tracked Agent Capabilities In Types

本地服务
github.com/lampepflAI 与智能体1372 周前更新

关于 TACIT: Tracked Agent Capabilities In Types

TACIT 是 AI Agent 安全框架,通过 Scala 3 捕获检查和能力库运行 Agent 编写的代码。它面向需要限制文件、进程、网络及子 Agent 访问范围的 MCP 兼容 Agent。

主要功能

  1. 01使用 Scala 3 捕获检查类型校验代码
  2. 02在本地 Scala REPL 中执行代码
  3. 03通过类型化能力控制外部操作
  4. 04限制命令、网络和文件系统访问
  5. 05支持无状态执行和有状态会话
  6. 06记录执行日志

适用场景

  1. 01限制 Agent 的文件系统访问范围
  2. 02限定 Agent 可执行的命令
  3. 03将网络访问限制在允许的主机
  4. 04通过 MCP 接口运行 Agent 编写的代码

MCP 相关

TACIT 提供基于 stdio 上 JSON-RPC 的标准 MCP 服务,公开沙箱化 Scala 代码执行工具。MCP 兼容 Agent 可连接该服务,能力库负责管理外部操作访问。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

暂未收录安装方式

我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。

代码仓库

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

TACIT:类型中的跟踪式 Agent 能力

论文: 使用跟踪式能力保障 Agent 安全(ACM) · arXiv:2603.00991 · 🏆 CAIS 26 最佳论文奖

想开箱即用地使用 TACIT? ATC(具备跟踪式能力的 Agent) 是基于 TACIT 构建、可直接使用的终端编码 Agent,无需单独设置 Agent 或 MCP。它还提供更精细的权限控制(默认拒绝,可临时授权或按会话授权)和更丰富的用户交互。

TACIT(类型中的跟踪式 Agent 能力)是用于 AI Agent 的安全保障框架。 Agent 不会直接调用工具,而是使用 Scala 3 编写代码,并借助 捕获检查:这是一种能够静态跟踪能力的类型系统,可强制确保 Agent 代码无法伪造访问权限、无法执行超出其预算的操作,也无法从纯子计算中泄漏信息。 它提供 MCP 接口,因此可轻松用于所有兼容 MCP 的 Agent。

TACIT 框架概览

该框架包含三个主要组件:

  • Scala 3 编译器。 启用捕获检查后,Agent 提交的代码会在安全模式下进行验证和类型检查,从而强制执行具备能力安全性的语言子集。
  • Scala REPL。 本地 REPL 实例执行编译后的代码,并管理多次交互中的状态。支持无状态的单次执行和有状态的会话。
  • 能力安全库。 提供类型化 API,作为 Agent 代码与现实世界交互的唯一通道,包括文件系统、进程执行、网络和子 Agent。该库可扩展:只需修改库代码即可添加新能力,无需更改 MCP 服务本身。

快速开始

TACIT 提供标准 MCP 服务,通过 stdio 使用 JSON-RPC 进行通信。它适用于任何兼容 MCP 的 Agent,包括 Claude Code、OpenCode、GitHub Copilot 等。

需要 JDK 17+。

安装 TACIT

选择以下安装方式之一。推荐使用 tacit CLI 包装器。

选项 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 服务~/.cache/tacit/TACIT.jar
库~/.cache/tacit/TACIT-library.jar
选项 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 服务./TACIT.jar
库./TACIT-library.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.jar(或 ./dist/TACIT.jar)
库./TACIT-library.jar(或 ./dist/TACIT-library.jar)

通过上述任一选项安装 TACIT 后,请配置你的 Agent 来启动 MCP 服务。

配置 Agent

在 Agent 的配置中将 TACIT 添加为 MCP 服务。如果安装了 tacit CLI,只需使用 tacit serve。如果手动安装了 TACIT,则改用明确的 java -jar ... --library-jar ... 形式。

Claude Code

添加到项目的 .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

添加到 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"
      ]
    }
  }
}
GitHub Copilot(VS Code)

添加到 .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"
      ]
    }
  }
}

现在,你的 Agent 可以使用 TACIT 的工具来执行沙盒化的 Scala 代码。

推荐:禁用内置工具

要充分发挥 TACIT 基于能力的安全机制,请禁用 Agent 内置的文件、Shell 和网络工具,让所有操作都通过沙盒化的 REPL 执行。

Claude Code

使用 --disallowedTools 启动,以阻止内置工具:

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

或者添加到项目的 .claude/settings.json:

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

在 opencode.json 中将内置工具的权限设为 "deny":

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

在 VS Code 的 settings.json 中限制 Copilot 可用的工具:

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

配置

可通过 CLI 标志或 JSON 配置文件配置服务。将标志直接传入 Agent 的 MCP 参数,或使用 --config 指定 JSON 文件。

配置分为服务配置(传输、录制、会话)和库配置(沙盒行为、能力)。在 JSON 配置文件中,库设置位于 libraryConfig 键下,并会直接传递给库进行处理。

CLI 标志

服务标志:

标志描述
--library-jar <path>必填。 库 JAR 文件(TACIT-library.jar)的路径
-r/--record <dir>将每次执行记录到磁盘
-q/--quiet禁止显示启动横幅以及请求/响应日志
--no-session禁用与会话相关的工具
--safe-mode / --no-safe-mode为每次执行启用/禁用 REPL 中 Scala 3 的 language.experimental.safe(默认:启用;参见 安全模式)
--exec-timeout-ms <ms>单次 REPL 求值的墙钟超时时间(默认:无;参见 执行超时)
-c/--config <path>JSON 配置文件(--config 之后的标志会覆盖文件中的值)

库标志(部分 libraryConfig 字段的简写):

标志描述
-s/--strict通过 exec 阻止内置拒绝列表中的不安全命令(文件操作、Shell、解释器、网络工具、命令运行器等);匹配不区分大小写。适合快速试验;用于实际部署时,建议使用 --command-permissions。
--command-permissions <patterns>以逗号分隔的可通过 exec 执行的命令 glob 模式(例如 echo,py*,ls)。只有 * 会被视为通配符。设置后,--strict 将被忽略。
--network-permissions <patterns>以逗号分隔的可访问主机 glob 模式(例如 *.example.com,api.github.com)。只有 * 会被视为通配符。
--allowed-roots <paths>以逗号分隔的 requestFileSystem 根目录范围限制(例如 /home/me/project,/tmp)。请求的根目录必须解析为其中某个路径的子路径。未设置时,默认使用服务的工作目录。
--classified-paths <patterns>以逗号分隔的已分类路径模式(采用 gitignore 风格,见下文)
--llm-base-url <url>大模型 API 基础 URL
--llm-api-key <key>大模型 API 密钥
--llm-model <name>大模型名称
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 等)、shell、解释器(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[_] 值会被解包。Agent 的主要输出仍显示屏蔽后的形式(Classified(***)),因此只有能读取此文件的人才能看到真实内容。系统会自动创建父目录;对于尚不存在的接收文件,在 POSIX 系统上会以仅所有者可访问的权限(rw-------)创建(以原子方式创建,因此文件不会以更宽松的权限存在)。如果文件已存在,则会在保持其权限不变的情况下追加内容。未设置时,打印行为保持正常,且不会写入磁盘。

classifiedWrite(可选,默认值为 true;仅适用于 JSON 配置)。设为 false 时,将拒绝所有对分类路径的写入:writeClassified(path, content)、access(path).writeClassified(content),以及对分类路径调用 mkdir()。 请注意,由于 classify 可以包装任意值,启用此项意味着 Agent 可以用任意内容覆盖分类文件(例如 .ssh/authorized_keys):Classified 机制保护的是机密性,而非完整性。在分类文件必须对 Agent 只读的部署中,请将此项设为 false。

分类路径模式

分类路径模式遵循 gitignore 风格的语法。路径与某个模式匹配,或是匹配路径的后代时,即会被分类。

模式匹配项示例
.ssh名为 .ssh 的任意路径组件/home/user/.ssh/id_rsa
.env.*与 glob 匹配的任意组件/project/.env.local
config/*/keys相对于文件系统根目录,包含通配符<root>/config/prod/keys/secret.pem
**/secrets任意深度的 secrets<root>/a/b/secrets/key.txt
/home/user/.ssh绝对路径(已解析符号链接)/home/user/.ssh/id_rsa

规则:

  • 模式中没有 /: 与任意路径组件匹配(按基本名称匹配)
  • 包含 / 的相对模式: 以文件系统根目录为锚点;支持 *、**、?、[…]
  • 绝对模式: 与完整路径匹配;非 glob 前缀会通过符号链接解析
  • 末尾的 / 会被去除(不区分目录)

未配置 classifiedPaths 时的默认分类模式:.ssh、.gnupg、.env、.env.*、.netrc、.npmrc、.pypirc、.docker、.kube、.aws、.azure、.gcloud。

工具

工具参数描述
execute_scalacode在新的 REPL 中执行 Scala 代码片段(无状态)
create_repl_session-创建持久化 REPL 会话,返回 session_id
execute_in_sessionsession_id、code在现有会话中执行代码(有状态)
list_sessions-列出活动会话 ID
delete_repl_sessionsession_id删除会话
show_interface-显示完整的能力 API 参考

活动会话数上限为 100;达到上限时,create_repl_session 会返回错误。执行输出上限为 10 MiB(结果中会标明截断情况),且每次调用中,exec 最多捕获 8 MiB 标准输出和 8 MiB 标准错误。

示例:有状态会话
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")

安全特性

无论 Agent 是否存在目标偏差、产生幻觉或遭受提示词注入攻击,TACIT 的类型系统都能提供三项安全保障:

属性含义
能力安全性能力无法伪造或遗忘。Agent 只能通过明确授予它的能力访问资源。
能力完备性能力会约束所有与安全相关的效应。Agent 只能通过已授予的能力与外部世界交互。
局部纯性可强制特定计算不产生副作用。这样可以防止 Agent 处理分类数据时发生信息泄露。
能力 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 值(例如通过 readClassified 读取的 Authorization 令牌)会发送到允许列表中的主机,但 Agent 代码永远无法观察到该值。因此,Agent 可以使用其无法以其他方式读取的机密信息,通过身份验证访问允许的 API。httpPostClassified 补全了这一能力:它会 POST 一个 Classified[String] 请求体,并返回一个 Classified[String] 响应,因此敏感数据可以通过外部服务往返传输,同时仍受信息流控制(见下文)。

通过 Classified 控制信息流

设想一个典型的编码 Agent 正在处理项目目录。其中一些文件是普通文件(源代码、构建配置、README),另一些则包含敏感信息:.env 中的 API 密钥、secrets/ 中的凭据、内部文档。Agent 由云端托管的大模型(第三方服务)驱动。我们希望 Agent 能使用或处理敏感数据(总结内部文档、轮换密钥、处理报告),但绝不将其泄露给云服务提供商。

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
}

那么,Agent 如何利用分类数据完成有用的工作?答案是采用双大模型设计:由单独的可信本地大模型处理分类内容。框架提供了一个接受 Classified[String] 并返回 Classified[String] 的 chat 重载。可信大模型可以看到内容,但结果仍会保持封装状态,绝不会流回不可信的云端模型。

分类数据流

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)
}

除了可信大模型和分类文件,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]
  }
}
安全模式

Agent 生成的代码会在 Scala 3 的安全模式(import language.experimental.safe)下编译,该模式会强制使用具备能力安全性的语言子集:

  1. 不允许未经检查的类型转换或模式匹配
  2. 不允许使用 caps.unsafe 模块中的功能
  3. 不允许使用 @unchecked 注解
  4. 不允许使用运行时反射
  5. 编译时启用捕获检查和显式空值,并追踪所有变更副作用
  6. 全局对象和函数只有在安全实现后才可访问

这些限制可防止 Agent 通过不安全的类型转换、反射或类型系统漏洞“遗忘”能力。未通过编译的代码绝不会执行。

安全模式是一项仍在积极开发中的实验性功能。默认情况下,TACIT 使用静态代码验证器检查禁止的模式,以强制执行安全模式子集。--safe-mode 标志(或 JSON 配置中的 "safeMode": true)还会在每次 REPL 执行时导入 language.experimental.safe,启用 Scala 3 编译器内的强制检查。

执行超时

--exec-timeout-ms <ms>(或 JSON 配置中的 "executionTimeoutMs")用于限制单次 REPL 求值的墙上时间。该值必须为正数;启动时会拒绝零或负值。超时后,客户端会收到提示错误,而不是一直等待;对于有状态会话,会话会保留之前的状态,因此被放弃的语句不会产生可观察到的影响。

监控程序会在工作线程上运行每次求值,且仅能尽力而为:能够响应中断的工作(阻塞 I/O、休眠、大多数库调用)会受到可靠的时间限制;但如果纯 CPU 循环从不检查中断,就会在后台继续运行,并持续占用 REPL 的输出锁。若要强制抢占,需要在进程级别进行隔离;此选项是稳健性保护措施,而非沙箱边界。未设置时(默认情况),求值不会超时。

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)进行配置。支持任何兼容 OpenAI 的 API。

实验结果

我们从安全性和表达能力两方面评估 TACIT(完整详情请参阅论文第 4 节)。

安全性(RQ1)。 在 classified 模式下(将机密数据封装在 Classified[String] 中),Claude Sonnet 4.6 和 MiniMax M2.5 在全部 131 次试验中均达到 100% 安全性。类型系统会拦截所有注入和恶意任务。实用性仍然很高(Sonnet 为 99.2%,MiniMax 为 90.0%)。

表达能力(RQ2)。 在 τ2-bench 和 SWE-bench Lite 上,使用 TACIT 能力安全型框架的 Agent,在所有受测模型(gpt-oss-120b、MiniMax M2.5、DeepSeek V3.2)上,表现均持平或略优于标准工具调用基线,表明编写类型安全的 Scala 不会降低 Agent 的性能。

扩展库:添加自己的 API

库(library/)定义了用户代码可在 REPL 中调用的能力 API。 若要实现自定义权限和细粒度访问控制,可以修改库并仅重新构建库 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 需要外部库,请将其添加到 build.sbt 中的 lib 项目:

lazy val lib = project
  .in(file("library"))
  .settings(
    // ... existing settings ...
    libraryDependencies ++= Seq(
      "com.openai" % "openai-java" % "4.81.0",
      "org.postgresql" % "postgresql" % "42.7.3",  // ← add your dep
    ),
  )
6. 重新构建库 JAR
sbt "lib/assembly"

除非你修改了 CodeValidator(第 4 步)或其他服务端代码,否则无需重新构建服务端 JAR。只需让服务端指向新的库 JAR:

java -jar server.jar --library-jar new-library.jar
7. 在开发 REPL 中试用新 API

若要快速迭代而无需启动 Agent,可启动开发 REPL。这是一个交互式 Scala 提示符,预加载了能力 API 和 MCP 服务所用的同一 CodeValidator:

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],因此 Agent 代码既不能实例化它们,也不能扩展它们:能力只能来自 request* 作用域。添加任何能力时,请保持这一约束:为类提供 private[library] 构造函数,并将具体实现类也设为 private[library]。

  • 接口本身也已封闭。 InterfaceImpl 的构造函数为 private[library],因此库之外的代码无法选择策略 JSON。服务器会为每个沙箱注册一次库配置(在任何代码运行之前,通过 REPL 的类加载器调用 InterfaceImpl.configure),而前导代码会实例化无参数的 SandboxInterface,其策略就是已注册的配置。扩展 SandboxInterface 本身的 Agent 代码只能获得策略相同且受其约束的接口,不可能获得权限更广的接口。

  • 捕获检查目前是实验性的。 项目使用 -language:experimental.captureChecking。编译器行为可能会随 Scala 3 nightly 版本而变化。如果遇到意料之外的错误,可以暂时移除该标志,检查问题是否与捕获检查有关。

  • 该库使用 Scala 3 nightly 版本。 构建会自动获取最新的 Scala 3 nightly 版本。这意味着你的代码必须兼容最新的 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 是一个 fat JAR。 sbt "lib/assembly" 会生成一个包含该库所有依赖项(例如 openai-java)的 JAR。如果你添加依赖,它会自动打包进去。

  • 服务器在编译时依赖库类型。 服务器依赖接口类型来运行 REPL。请确保你的更改与服务器所需的接口兼容。

  • 先在库层级测试你的 API。 library/test/ 目录包含使用 MUnit 编写的库级测试,通过 scala-cli test library --server=false 运行(这些测试不属于 sbt test;--server=false 用于避开当前 Scala 3 nightly 版本与 scala-cli 捆绑的 Bloop 服务器之间的 ASM 冲突)。在通过 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 运行。

直接运行服务器(不使用 Agent)
# 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

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。