Skip to main content
Glama

TACIT:类型中追踪的代理能力

论文: 使用追踪能力保护代理安全(ACM)· arXiv:2603.00991 · 🏆 CAIS 26 最佳论文奖

TACIT(Tracked Agent Capabilities In Types,类型中追踪的代理能力)是一个面向 AI 代理的安全防护装置。 代理不直接调用工具,而是使用 Scala 3 编写代码,并启用捕获检查:这是一种类型系统,能够静态追踪能力,并强制保证代理代码无法伪造访问权限、无法超出其预算执行效果、无法从纯子计算中泄露信息。 它提供了 MCP 接口,因此所有兼容 MCP 的代理都可以轻松使用。

TACIT 框架概览

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

  • Scala 3 编译器。 代理提交的代码会经过验证和类型检查,并在安全模式下启用捕获检查,从而强制实施一个能力安全的语言子集。

  • Scala REPL。 本地 REPL 实例执行编译后的代码,并在交互之间管理状态。支持无状态的一次性执行和有状态的会话。

  • 能力安全库。 一个类型化 API,是代理代码与现实世界交互的唯一通道:文件系统、进程执行、网络和子代理。该库是可扩展的:只需修改库代码即可添加新能力,无需更改 MCP 服务器本身。

快速开始

TACIT 提供一个标准的 MCP 服务器,通过 stdio 上的 JSON-RPC 进行通信。它适用于任何兼容 MCP 的代理,包括 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。下载文件会先放入临时目录,只有在验证通过后才会移动到最终位置,因此下载失败永远不会替换已安装的 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 后,请配置您的代理以启动 MCP 服务器。

配置您的代理

在代理的配置中将 TACIT 添加为 MCP 服务器。如果您安装了 tacit CLI,只需使用 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 基于能力的安全性,请禁用代理内置的文件、Shell 和网络工具,以便所有操作都通过沙箱化的 REPL 进行。

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

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

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

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

在 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": { "..." : "..." }
  }
}

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

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

Related MCP server: repo-seatbelt

配置

服务器可以通过 CLI 标志或 JSON 配置文件进行配置。直接在代理的 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>

可执行命令的逗号分隔 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>

LLM API 基础 URL

--llm-api-key <key>

LLM API 密钥

--llm-model <name>

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 等)、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[_] 值会被解包。代理的主输出仍然显示掩码形式(Classified(***)),因此只有能读取该文件的人才能看到真实内容。父目录会自动创建,尚不存在的接收文件在 POSIX 系统上以仅所有者权限(rw-------)创建(原子操作,因此绝不会以更宽的权限存在)。已有文件则以不变的权限追加写入。未设置时,打印行为正常,不会向磁盘写入任何内容。

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

分类路径模式

分类路径模式遵循 gitignore 风格的语法。如果路径匹配某个模式或是某个匹配项的后代,则该路径被分类。

模式

匹配内容

示例

.ssh

任何名为 .ssh 的路径组件

/home/user/.ssh/id_rsa

.env.*

匹配该通配符的任何组件

/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

规则:

  • 模式中无 /: 匹配任意路径组件(基名匹配)

  • 带 / 的相对模式: 锚定到文件系统根目录;支持 *、**、?、[…]

  • 绝对模式: 针对完整路径匹配;非通配符前缀通过符号链接解析

  • 尾部 / 会被去除(不区分仅目录)

默认分类模式(未配置 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 的 stdout 和 8 MiB 的 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 值(例如通过 readClassified 读取的 Authorization 令牌)会被发送到白名单主机,但代理代码永远无法观察到它,这使代理能够使用其无法读取的机密向允许的 API 进行身份验证。httpPostClassified 完善了这一设计:它 POST 一个 Classified[String] 请求体并返回一个 Classified[String] 响应,因此敏感数据可以在保持信息流控制的同时通过外部服务往返传输(见下文)。

通过 Classified 实现信息流控制

考虑一个在项目目录上工作的典型代码代理。有些文件是普通的(源代码、构建配置、README)。其他文件是敏感的:.env 中的 API 密钥、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 处理分类内容。框架提供了一个接受 Classified[String] 并返回 Classified[String] 的 chat 重载。可信 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)下编译,该模式强制一个能力安全的语言子集:

  1. 不允许未经检查的类型转换或模式匹配

  2. 不允许使用 caps.unsafe 模块中的特性

  3. 不允许 @unchecked 注解

  4. 不允许运行时反射

  5. 启用捕获检查和显式空值编译,跟踪所有变更效应

  6. 全局对象和函数只有在安全实现时才可访问

这些限制防止代理通过不安全的转换、反射或类型系统漏洞"遗忘"能力。未通过编译的代码永远不会被执行。

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

执行超时

--exec-timeout-ms <ms>(或 JSON 配置中的 "executionTimeoutMs")限制单次 REPL 求值的墙钟时间。该值必须为正数;零或负值在启动时被拒绝。超时时客户端会收到明确的错误提示而不是挂起,对于有状态会话,会话保持其先前状态,因此被放弃的语句没有可观察的效应。

看门狗在工作者线程上运行每次求值,并且是尽力而为的:可中断响应的工作(阻塞 I/O、睡眠、大多数库调用)会被可靠地限制,但从不检查中断的纯 CPU 循环会在后台继续运行并持续持有 REPL 的输出锁。硬抢占需要进程级隔离;此旋钮是健壮性保护,而非沙箱边界。未设置时(默认),求值没有超时。

LLM 集成

通过 chat 方法可以使用辅助 LLM,无需能力作用域。安全性来自 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[String] 中)下,Claude Sonnet 4.6 和 MiniMax M2.5 在所有 131 次试验中均实现100% 安全性。每次注入和恶意任务都被类型系统阻止。实用性保持较高(Sonnet 为 99.2%,MiniMax 为 90.0%)。

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

扩展库:添加你自己的 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.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.jar

7. 在开发 REPL 中尝试您的新 API

为了快速迭代而无需启动代理,请启动开发 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] 构造函数,因此代理代码既不能实例化也不能扩展它们:能力只能来自 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" 生成一个包含库所有依赖项(例如 openai-java)的 JAR。如果您添加依赖项,它将自动被打包。

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

  • 先在库级别测试您的 API。 library/test/ 目录包含使用 MUnit 的库级测试,通过 scala-cli test library --server=false 运行(它们不属于 sbt test;--server=false 可避开当前 Scala 3 夜间版与 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 单独运行。

# 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

Related MCP Connectors

Related MCP Servers

  • 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
    84 npm
    13
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to operate through a local, signed boundary that blocks prompt injection and secret leakage, verifies outputs, preserves cross-session memory, and provides offline-verifiable receipts. It also exposes 900+ MCP tools for discoverable agent actions.
    7
    MIT