TACIT: Tracked Agent Capabilities In Types について
TACITは、Scala 3のキャプチャチェックと能力ライブラリを通じてエージェント作成コードを実行する、AIエージェント向けの安全機構です。ファイル、プロセス、ネットワーク、サブエージェントへのアクセスを制限したいMCP対応エージェントを対象とします。
主な機能
- 01Scala 3のキャプチャチェックによる型検査
- 02ローカルScala REPLでのコード実行
- 03型付き能力を介した外部操作の制御
- 04コマンド、ネットワーク、ファイルシステムの制限
- 05ステートレス実行とステートフルセッション
- 06実行ログの記録
活用シーン
- 01エージェントのファイルシステムアクセス範囲の制限
- 02エージェントが実行できるコマンドの限定
- 03許可されたホストへのネットワークアクセス制限
- 04MCPインターフェース経由でエージェント作成コードを実行
MCP 対応
TACITはstdio上のJSON-RPCを使う標準MCPサーバーを提供し、サンドボックス化されたScalaコード実行ツールを公開します。MCP対応エージェントが接続でき、能力ライブラリが外部操作へのアクセスを仲介します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
TACIT:型による追跡可能なエージェント機能
論文: 追跡可能な機能によるエージェントのセキュリティ確保(ACM) · arXiv:2603.00991 · 🏆 CAIS 26 最優秀論文賞
すぐに TACIT を使いたいですか? ATC(追跡可能な機能を備えたエージェント) は、TACIT を基盤とするすぐに使えるターミナル型コーディングエージェントです。別途エージェントや MCP のセットアップは必要ありません。また、よりきめ細かな権限制御(デフォルトで拒否し、一時的およびセッションごとの許可を付与)と、より充実したユーザーインタラクションも提供します。
TACIT(型による追跡可能なエージェント機能)は、AI エージェント向けのセーフティハーネスです。 エージェントはツールを直接呼び出す代わりに、キャプチャチェックを使用する Scala 3 でコードを記述します。キャプチャチェックは、機能を静的に追跡する型システムであり、エージェントコードがアクセス権を偽造できないこと、許可された予算を超える副作用を実行できないこと、純粋なサブ計算から情報を漏えいできないことを保証します。 MCP インターフェースを備えているため、MCP 互換のあらゆるエージェントで簡単に使用できます。

このフレームワークは、主に次の 3 つのコンポーネントで構成されています。
- Scala 3 コンパイラー。 エージェントから送信されたコードは、セーフモードでキャプチャチェックを有効にして検証および型チェックされます。これにより、機能の安全性が保たれる言語サブセットが適用されます。
- Scala REPL。 ローカルの REPL インスタンスがコンパイル済みコードを実行し、インタラクション間の状態を管理します。ステートレスな単発実行とステートフルなセッションの両方をサポートします。
- 機能安全性ライブラリ。 エージェントコードが実世界(ファイルシステム、プロセス実行、ネットワーク、サブエージェント)とやり取りするための唯一のゲートウェイとなる、型付き API です。このライブラリは拡張可能です。MCP サーバー自体を変更せずに、ライブラリコードのみを変更して新しい機能を追加できます。
クイックスタート
TACIT は、stdio 経由で JSON-RPC を使用して通信する標準 MCP サーバーを提供します。Claude Code、OpenCode、GitHub Copilot など、MCP 互換のあらゆるエージェントで動作します。
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 |
ラッパーとスクリプトはどちらも、ダウンロードした 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
これにより、2 つの 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 ... の形式を明示的に指定してください。
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"
]
}
}
}
エージェントが TACIT のツールを使用して、サンドボックス化された Scala コードを実行できるようになりました。
推奨:組み込みツールを無効にする
TACIT の機能ベースの安全性を十分に活用するには、エージェントの組み込みファイル、シェル、ネットワークツールを無効にし、すべての操作がサンドボックス化された 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 設定ファイルで設定できます。フラグをエージェントの 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> | 1 回の REPL 評価に対する実時間のタイムアウト(デフォルト:なし。詳しくは実行タイムアウトを参照) |
-c/--config <path> | JSON 設定ファイル(--config の後に指定したフラグはファイル内の値を上書きします) |
ライブラリフラグ(一部の libraryConfig フィールドの省略表記):
| フラグ | 説明 |
|---|---|
-s/--strict | exec を介した安全でないコマンド(ファイル操作、シェル、インタープリター、ネットワークツール、コマンドランナーなど)を、組み込みの拒否リストでブロックします。一致判定では大文字と小文字を区別しません。手軽な実験に便利です。実運用では --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> | 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 の許可リストです。すべての exec に渡すコマンドが一致する必要がある、グロブパターンのリストです(ワイルドカードは * のみ)。requestExecPermission(...) で宣言されたスコープごとの設定に加えて適用され、実行するには両方に含まれている必要があります。設定されている場合、strictMode は無視されます。実運用環境では、このリストを必ず明示的に設定してください。
strictMode(任意、デフォルトは true)。手早く試すためのデフォルト設定で、exec を通じて、組み込みの危険なコマンドの拒否リストをブロックします。対象はファイル操作コマンド(cat、ls、rm、tar、chmod、…)、シェル、インタープリター(python、node、perl、…)、ネットワークツール(curl、wget、ssh、…)、コマンド実行ツール(xargs、nohup、env、…)などです。照合では、コマンドのベース名を大文字と小文字を区別せずに比較します。気軽に試すには便利ですが、実運用には大まかすぎるため、commandPermissions の使用を推奨します。
networkPermissions(任意)。ネットワークの許可リストです。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.* | 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 が各呼び出しで取得する stdout と stderr は、それぞれ最大 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")
セキュリティ機能
TACIT の型システムは、エージェントが非整合であるか、幻覚を起こしているか、プロンプトインジェクション攻撃を受けているかにかかわらず、3 つの安全性を保証します。
| 特性 | 意味 |
|---|---|
| ケイパビリティの安全性 | ケイパビリティを偽造したり、忘れたりすることはできません。エージェントは、明示的に付与されたケイパビリティを通じてのみリソースにアクセスできます。 |
| ケイパビリティの完全性 | ケイパビリティは、安全性に関わるすべての作用を制御します。エージェントは、付与されたケイパビリティを通じてのみ外部とやり取りします。 |
| 局所的な純粋性 | 特定の計算を副作用のないものとして強制できます。これにより、エージェントが分類対象データを処理する際の情報漏えいを防ぎます。 |
ケイパビリティ API
このライブラリには、アクセス範囲をブロック内に限定する 3 つのケイパビリティ要求メソッドがあります。ケイパビリティがスコープ対象のブロックから外に出ることはありません。これはキャプチャチェッカーによってコンパイル時に強制されます。
// 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 によって機能が補完されます。このメソッドは Classified[String] の本文を POST し、Classified[String] のレスポンスを返すため、情報フロー制御(下記参照)のもとで機密データを外部サービス経由で往復させられます。
Classified による情報フロー制御
プロジェクトディレクトリで作業する一般的なコーディングエージェントを考えてみましょう。一部のファイルは通常のもので(ソースコード、ビルド設定、README など)、ほかには機密情報が含まれるものがあります。たとえば、.env の API キー、secrets/ の認証情報、社内文書などです。エージェントはクラウドでホストされる LLM(サードパーティサービス)を利用します。エージェントには機密データを使用または処理(社内文書の要約、キーのローテーション、レポートの処理など)させたい一方で、その情報をクラウドプロバイダーに漏らすことは決して避けたいと考えています。
TACIT は Classified[T] 型によってこれを実現します。指定した機密パス(.ssh、.env.*、secrets、**/keys などの gitignore 形式のパターンを --classified-paths で設定)配下のファイルの内容は、通常の String ではなく Classified[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)でコンパイルされます。これにより、ケイパビリティの安全性が保たれた言語サブセットが強制されます。
- チェックされない型キャストやパターンマッチングは禁止
caps.unsafeモジュールの機能は禁止@uncheckedアノテーションは禁止- 実行時リフレクションは禁止
- キャプチャチェックと明示的 null を有効にしてコンパイルし、すべての変更エフェクトを追跡
- グローバルオブジェクトや関数は、安全に実装されている場合にのみアクセス可能
これらの制約により、安全でないキャスト、リフレクション、型システムの穴を使って、エージェントがケイパビリティを「忘れる」ことを防ぎます。コンパイルに成功しないコードは実行されません。
セーフモードは、現在も活発に開発されている実験的な機能です。デフォルトでは、TACIT は禁止パターンをチェックする静的コードバリデーターを使用して、セーフモードのサブセットを適用します。--safe-mode フラグ(または JSON 設定の "safeMode": true)を指定すると、すべての REPL 実行に language.experimental.safe がインポートされ、Scala 3 のコンパイラーによる強制が有効になります。
実行タイムアウト
--exec-timeout-ms <ms>(または JSON 設定の "executionTimeoutMs")は、単一の REPL 評価の実時間を制限します。値は正の数である必要があり、ゼロまたは負の値は起動時に拒否されます。タイムアウトが発生すると、クライアントには停止し続ける代わりにプロンプトエラーが返されます。状態を保持するセッションでは、それまでの状態が維持されるため、中断されたステートメントが観測可能な影響を及ぼすことはありません。
ウォッチドッグは各評価をワーカースレッド上で実行するため、制限の適用はベストエフォートです。割り込みに応答する処理(ブロッキング I/O、スリープ、大半のライブラリ呼び出し)は確実に制限されますが、割り込みを一度も確認しない純粋な 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)で設定します。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 のケーパビリティセーフなハーネスを使用するエージェントが、テストしたすべてのモデル(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.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 を試す
エージェントを起動せずに手早く試行錯誤するには、開発用 REPL を起動します。これは、ケーパビリティ API と、MCP サーバーが使用するものと同じ CodeValidator があらかじめ読み込まれた、対話型の Scala プロンプトです。
sbt devRepl # default config
sbt "devRepl --strict --config my.json" # with flags
留意点
-
Capability は
caps.SharedCapabilityを継承する必要があります。 これによってキャプチャチェックが機能します。継承しない場合、コンパイラーは Capability のスコープを追跡できず、ユーザーがrequest*ブロックの外に漏らしてしまう可能性があります。 -
Capability クラスとその実装はシールドされています。 すべての Capability 型(
FileSystem、Network、ProcessPermission、IOCapability、Classified、FileEntry)とすべての impl クラス(RealFileSystem、NetworkImpl、LlmOpsなど)のコンストラクターはprivate[library]であるため、エージェントのコードからインスタンス化も拡張もできません。Capability はrequest*スコープからのみ取得できます。追加する Capability でもこの不変条件を維持してください。クラスのコンストラクターをprivate[library]にし、具象 impl も同様にprivate[library]にしてください。 -
インターフェース自体もシールドされています。
InterfaceImplのコンストラクターはprivate[library]であるため、ライブラリーの外部からポリシー JSON を選択することはできません。サーバーはサンドボックスごとにライブラリー設定を一度登録します(コードの実行前に REPL のクラスローダー経由でInterfaceImpl.configureを呼び出します)。そしてプリアンブルがパラメーターなしのSandboxInterfaceをインスタンス化し、そのポリシーには登録済みの設定が使われます。SandboxInterface自体を拡張するエージェントコードも、同一のポリシーに束縛されたインターフェースを取得するだけで、より広い権限を持つインターフェースは取得できません。 -
キャプチャチェックは実験的な機能です。 このプロジェクトでは
-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 をラップする場合は、ユーザーが Capability ラッパーを迂回できないよう、対応する禁止パターンを追加してください。 -
ライブラリーの 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 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で別途実行します。
サーバーを直接実行する(エージェントなし)
# 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 サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。