Skip to main content
Glama

TACIT: 타입으로 추적되는 에이전트 기능

논문: Securing Agents With Tracked Capabilities (ACM) · arXiv:2603.00991 · 🏆 Best Paper Award at CAIS 26

TACIT(타입으로 추적되는 에이전트 기능)은 AI 에이전트를 위한 안전 하네스입니다. 에이전트는 도구를 직접 호출하는 대신 캡처 검사를 사용하는 Scala 3로 코드를 작성합니다. 캡처 검사는 기능을 정적으로 추적하고 에이전트 코드가 접근 권한을 위조할 수 없고, 예산을 초과하는 효과를 수행할 수 없으며, 순수 하위 계산에서 정보를 유출할 수 없도록 강제하는 타입 시스템입니다. MCP 인터페이스를 제공하므로 모든 MCP 호환 에이전트에서 쉽게 사용할 수 있습니다.

TACIT Framework Overview

프레임워크는 세 가지 주요 구성 요소로 이루어져 있습니다:

  • 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/binPATH에 있는지 확인하며, 최신 릴리스를 ~/.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의 기능 기반 안전을 완전히 활용하려면 에이전트의 기본 제공 파일, 셸, 네트워크 도구를 비활성화하여 모든 작업이 샌드박스 처리된 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: edict-lang

구성

서버는 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를 통해 안전하지 않은 명령(파일 작업, 셸, 인터프리터, 네트워크 도구, 명령 실행기 등)의 기본 제공 차단 목록을 차단합니다. 일치는 대소문자를 구분하지 않습니다. 빠른 실험에 편리하며 실제 배포에서는 --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 허용 목록: exec에 전달되는 모든 명령이 일치해야 하는 glob 패턴 목록(*만 와일드카드). 이는 requestExecPermission(...)으로 선언된 범위별 집합 위에 적용됩니다. 명령이 실제로 실행되려면 둘 다에 있어야 합니다. 설정하면 strictMode는 무시됩니다. 실제 배포에서는 항상 이 목록을 명시적으로 구성해야 합니다.

strictMode (선택 사항, 기본값 true). exec를 통해 안전하지 않은 명령의 기본 제공 차단 목록을 차단하는 빠른 실험용 기본값: 파일 작업 명령(cat, ls, rm, tar, chmod, ...), 셸, 인터프리터(python, node, perl, ...), 네트워크 도구(curl, wget, ssh, ...), 명령 실행기(xargs, nohup, env, ...) 등. 일치는 명령의 기본 이름에 대해 대소문자를 구분하지 않습니다. 그냥 시험해 보고 싶을 때 편리하지만 실제 사용에는 너무 거칠므로 commandPermissions를 선호합니다.

networkPermissions (선택 사항). 네트워크 허용 목록: httpGet/httpPost/httpRequest를 통해 접근하는 모든 호스트가 일치해야 하는 glob 패턴 목록(*만 와일드카드). 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는 호출당 최대 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가 그림을 완성합니다: Classified[String] 본문을 POST하고 Classified[String] 응답을 반환하므로, 민감한 데이터가 정보 흐름 제어(아래 참조)를 유지하면서 외부 서비스를 통해 왕복할 수 있습니다.

Classified를 통한 정보 흐름 제어

프로젝트 디렉터리에서 작업하는 일반적인 코드 에이전트를 생각해 보세요. 일부 파일은 일반적입니다(소스 코드, 빌드 구성, README). 다른 파일은 민감합니다: .env의 API 키, secrets/의 자격 증명, 내부 문서. 에이전트는 클라우드 호스팅 LLM(타사 서비스)으로 구동됩니다. 우리는 에이전트가 민감한 데이터를 사용하거나 처리하도록(내부 문서 요약, 키 교체, 보고서 처리) 하되 클라우드 제공자에게 절대 유출하지 않기를 원합니다.

TACIT는 Classified[T] 타입을 통해 이 문제를 해결합니다. 지정된 분류 경로(gitignore 스타일 패턴으로 --classified-paths를 통해 구성, 예: .ssh, .env.*, secrets, **/keys) 아래의 파일은 내용이 일반 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)로 컴파일되며, 이는 능력 안전 언어 하위 집합을 강제합니다:

  1. 검사되지 않은 타입 캐스트 또는 패턴 매치 없음

  2. caps.unsafe 모듈의 기능 없음

  3. @unchecked 주석 없음

  4. 런타임 리플렉션 없음

  5. 캡처 검사와 명시적 널을 활성화한 상태로 컴파일, 모든 변형 효과 추적

  6. 전역 객체와 함수는 안전하게 구현된 경우에만 접근 가능

이러한 제한은 에이전트가 안전하지 않은 캐스트, 리플렉션 또는 타입 시스템 구멍을 통해 능력을 "잊어버리는" 것을 방지합니다. 컴파일을 통과하지 못하는 코드는 절대 실행되지 않습니다.

안전 모드는 아직 활발히 개발 중인 실험적 기능입니다. 기본적으로 TACIT는 안전 모드 하위 집합을 강제하기 위해 금지된 패턴을 검사하는 정적 코드 검증기를 사용합니다. --safe-mode 플래그(또는 JSON 구성의 "safeMode": true)는 추가로 모든 REPL 실행에 language.experimental.safe를 가져와 Scala 3의 컴파일러 내 강제를 선택합니다.

실행 시간 제한

--exec-timeout-ms <ms>(또는 JSON 구성의 "executionTimeoutMs")는 단일 REPL 평가의 벽시계 시간을 제한합니다. 값은 양수여야 하며, 0 또는 음수 값은 시작 시 거부됩니다. 시간 초과 시 클라이언트는 멈추는 대신 즉각적인 오류를 받고, 상태 저장 세션의 경우 세션은 이전 상태를 유지하므로 중단된 문은 관찰 가능한 효과가 없습니다.

감시자는 각 평가를 작업자 스레드에서 실행하며 최선 노력입니다: 인터럽트에 반응하는 작업(차단 I/O, sleep, 대부분의 라이브러리 호출)은 안정적으로 제한되지만, 인터럽트를 확인하지 않는 순수 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[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.sbtlib 프로젝트에 추가합니다:

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"

CodeValidator(4단계) 또는 기타 서버 측 코드를 변경하지 않은 경우 서버 JAR를 다시 빌드할 필요가 없습니다. 서버가 새 라이브러리 JAR를 가리키기만 하면 됩니다:

java -jar server.jar --library-jar new-library.jar

7. 개발 REPL에서 새 API 시도하기

에이전트를 실행하지 않고 빠르게 반복 작업을 하려면 기능 API와 MCP 서버가 사용하는 것과 동일한 CodeValidator가 미리 로드된 대화형 Scala 프롬프트인 개발 REPL을 실행합니다:

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 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로 별도로 실행됩니다.

# 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

A
license - permissive license
Not graded
quality - not tested
A
maintenance

Maintenance

UpdatingMaintainers
UpdatingResponse time
2wRelease cycle
10Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A unified MCP server providing observability, safety control, and behavior evolution for high-agency AI agents through tracing, replaying, and auditing. It features real-time firewall guardrails and ML-driven anomaly detection to monitor, block, or fork agent actions based on risk.
    7
  • 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
    123
    11
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lampepfl/TACIT'

If you have feedback or need assistance with the MCP directory API, please join our Discord server