关于 TACIT: Tracked Agent Capabilities In Types
TACIT 是 AI Agent 安全框架,通过 Scala 3 捕获检查和能力库运行 Agent 编写的代码。它面向需要限制文件、进程、网络及子 Agent 访问范围的 MCP 兼容 Agent。
主要功能
- 01使用 Scala 3 捕获检查类型校验代码
- 02在本地 Scala REPL 中执行代码
- 03通过类型化能力控制外部操作
- 04限制命令、网络和文件系统访问
- 05支持无状态执行和有状态会话
- 06记录执行日志
适用场景
- 01限制 Agent 的文件系统访问范围
- 02限定 Agent 可执行的命令
- 03将网络访问限制在允许的主机
- 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。

该框架包含三个主要组件:
- 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_scala | code | 在新的 REPL 中执行 Scala 代码片段(无状态) |
create_repl_session | - | 创建持久化 REPL 会话,返回 session_id |
execute_in_session | session_id、code | 在现有会话中执行代码(有状态) |
list_sessions | - | 列出活动会话 ID |
delete_repl_session | session_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)下编译,该模式会强制使用具备能力安全性的语言子集:
- 不允许未经检查的类型转换或模式匹配
- 不允许使用
caps.unsafe模块中的功能 - 不允许使用
@unchecked注解 - 不允许使用运行时反射
- 编译时启用捕获检查和显式空值,并追踪所有变更副作用
- 全局对象和函数只有在安全实现后才可访问
这些限制可防止 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
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。