
Легковесный инструмент для песочницы, обеспечивающий соблюдение ограничений файловой системы и сети для произвольных процессов на уровне ОС, без необходимости в контейнере.
# Anthropic Sandbox Runtime (srt)
Легковесный инструмент песочницы для принудительного применения ограничений файловой системы и сети к произвольным процессам на уровне ОС без необходимости в контейнере.
`srt` использует нативные примитивы песочницы ОС (`sandbox-exec` в macOS, `bubblewrap` в Linux) и прокси-фильтрацию сети. Его можно использовать для изоляции поведения агентов, локальных MCP-серверов, bash-команд и произвольных процессов.
> **Бета-версия исследовательского превью**
>
> Sandbox Runtime — это исследовательское превью, разработанное для [Claude Code](https://www.claude.com/product/claude-code) с целью обеспечения более безопасной работы ИИ-агентов. Оно публикуется как раннее превью с открытым исходным кодом, чтобы помочь более широкой экосистеме создавать более безопасные агентные системы. Поскольку это раннее исследовательское превью, API и форматы конфигурации могут меняться. Мы приветствуем отзывы и вклад, чтобы сделать ИИ-агентов безопасными по умолчанию!
## Установка
```bash
npm install -g @anthropic-ai/sandbox-runtime
```
## Базовое использование
```bash
# Network restrictions
$ srt "curl anthropic.com"
Running: curl anthropic.com
<html>...</html> # Request succeeds
$ srt "curl example.com"
Running: curl example.com
Connection blocked by network allowlist # Request blocked
# Filesystem restrictions
$ srt "cat README.md"
Running: cat README.md
# Anthropic Sandb... # Current directory access allowed
$ srt "cat ~/.ssh/id_rsa"
Running: cat ~/.ssh/id_rsa
cat: /Users/ollie/.ssh/id_rsa: Operation not permitted # Specific file blocked
```
## Обзор
Этот пакет предоставляет автономную реализацию песочницы, которую можно использовать как в качестве инструмента CLI, так и в качестве библиотеки. Он разработан с философией **безопасности по умолчанию**, адаптированной под типичные сценарии использования разработчиками: процессы запускаются с минимальным доступом, и вы явно открываете только те «дыры», которые вам нужны.
**Ключевые возможности:**
- **Сетевые ограничения**: контроль того, к каким хостам/доменам можно обращаться по HTTP/HTTPS и другим протоколам
- **Ограничения файловой системы**: контроль того, какие файлы/каталоги можно читать/записывать
- **Ограничения Unix-сокетов**: контроль доступа к локальным IPC-сокетам
- **Мониторинг нарушений**: в macOS — доступ к системному хранилищу журнала нарушений песочницы для оповещений в реальном времени
### Пример использования: изоляция MCP-серверов
Ключевой сценарий — изоляция серверов Model Context Protocol (MCP) для ограничения их возможностей. Например, чтобы изолировать MCP-сервер файловой системы:
**Без песочницы** (`.mcp.json`):
```json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
```
**С песочницей** (`.mcp.json`):
```json
{
"mcpServers": {
"filesystem": {
"command": "srt",
"args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
}
}
}
```
Затем настройте ограничения в `~/.srt-settings.json`:
```json
{
"filesystem": {
"denyRead": [],
"allowWrite": ["."],
"denyWrite": ["~/sensitive-folder"]
},
"network": {
"allowedDomains": [],
"deniedDomains": []
}
}
```
Теперь MCP-серверу будет заблокирована запись в запрещённый путь:
```
> Write a file to ~/sensitive-folder
✗ Error: EPERM: operation not permitted, open '/Users/ollie/sensitive-folder/test.txt'
```
## Как это работает
Песочница использует примитивы уровня ОС для применения ограничений, действующих на всё дерево процессов:
- **macOS**: Использует `sandbox-exec` с динамически генерируемыми [профилями Seatbelt](https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf)
- **Linux**: Использует [bubblewrap](https://github.com/containers/bubblewrap) для контейнеризации с изоляцией сетевого пространства имён
- **Windows**: Запускает изолированный процесс под выделенной локальной учётной записью `srt-sandbox`, с [Windows Filtering Platform](https://learn.microsoft.com/en-us/windows/win32/fwp/windows-filtering-platform-start-page) egress fence, привязанным к SID этой учётной записи, токеном с ограничением записи и явными ACE на рабочем дереве
0d1c612947c798aef48e6ab4beb7e8544da9d41a-4096x2305
### Модель двойной изоляции
Для эффективной изоляции необходимы как файловая, так и сетевая изоляция. Без файловой изоляции скомпрометированный процесс мог бы похитить SSH-ключи или другие конфиденциальные файлы. Без сетевой изоляции процесс мог бы выйти из песочницы и получить неограниченный сетевой доступ.
**Файловая изоляция** применяет ограничения на чтение и запись:
- **Чтение** (шаблон «сначала запретить, затем разрешить»): По умолчанию доступ на чтение разрешён везде. Вы можете запретить широкие области (например, `/Users`), а затем повторно разрешить конкретные пути внутри них (например, `.`). `allowRead` имеет приоритет над `denyRead` — в отличие от записи, где `denyWrite` имеет приоритет над `allowWrite`. Запись `denyRead`, которая более конкретна, чем область `allowRead`, в которую она попадает (например, `denyRead: ["**/.env"]` или `["./secrets"]` при `allowRead: ["."]`), всё равно остаётся запрещённой.
- **Запись** (шаблон «только разрешить»): По умолчанию доступ на запись запрещён везде. Вы должны явно разрешить пути (например, `.`, `/tmp`). Пустой список разрешений означает отсутствие доступа на запись.
**Сетевая изоляция** (шаблон «только разрешить»): По умолчанию весь сетевой доступ запрещён. Вы должны явно разрешить домены. Пустой список allowedDomains означает отсутствие сетевого доступа. Сетевой трафик маршрутизируется через прокси-серверы, работающие на хосте:
- **Linux**: Запросы маршрутизируются через файловую систему по Unix domain socket. Сетевое пространство имён изолированного процесса полностью удаляется, поэтому весь сетевой трафик должен проходить через прокси, работающие на хосте (прослушивающие Unix-сокеты, которые примонтированы в песочницу)
- **macOS**: Профиль Seatbelt разрешает связь только с определённым портом localhost. Прокси прослушивают этот порт, создавая контролируемый канал для всего сетевого доступа
- **Windows**: Набор машинных фильтров WFP блокирует исходящие соединения, устанавливаемые процессами, работающими под учётной записью `srt-sandbox`, за исключением loopback к диапазону портов прокси. Прокси прослушивают внутри этого диапазона, создавая контролируемый канал для всего сетевого доступа. Фильтры не отслеживают изолированный процесс, который устанавливает другой токен на поток перед созданием сокета, а также трафик, который системная служба передаёт от его имени; см. раздел Windows [Известные ограничения](#known-limitations)
Как HTTP/HTTPS (через HTTP-прокси), так и другой TCP-трафик (через SOCKS5-прокси) опосредуются этими прокси, которые применяют ваши списки разрешённых и запрещённых доменов.
Для получения дополнительной информации об изоляции в Claude Code см.:
- [Документация по изоляции Claude Code](https://docs.claude.com/en/docs/claude-code/sandboxing)
- [Beyond Permission Prompts: Making Claude Code More Secure and Autonomous](https://www.anthropic.com/engineering/claude-code-sandboxing)
## Архитектура
```
src/
├── index.ts # Library exports
├── cli.ts # CLI entrypoint (srt command)
├── utils/ # Shared utilities
│ ├── debug.ts # Debug logging
│ ├── settings.ts # Settings reader (permissions + sandbox config)
│ ├── platform.ts # Platform detection
│ └── exec.ts # Command execution utilities
└── sandbox/ # Sandbox implementation
├── sandbox-manager.ts # Main sandbox manager
├── sandbox-schemas.ts # Zod schemas for validation
├── sandbox-violation-store.ts # Violation tracking
├── sandbox-utils.ts # Shared sandbox utilities
├── http-proxy.ts # HTTP/HTTPS proxy for network filtering
├── socks-proxy.ts # SOCKS5 proxy for network filtering
├── linux-sandbox-utils.ts # Linux bubblewrap sandboxing
├── macos-sandbox-utils.ts # macOS sandbox-exec sandboxing
└── windows-sandbox-utils.ts # Windows srt-win sandboxing
```
## Использование
### Как инструмент CLI
Команда `srt` (Anthropic Sandbox Runtime) оборачивает любую команду с помощью границ безопасности:
```bash
# Run a command in the sandbox
srt echo "hello world"
# With debug logging
srt --debug curl https://example.com
# Specify custom settings file
srt --settings /path/to/srt-settings.json npm install
```
Файл настроек необязателен — если файла `~/.srt-settings.json` нет, `srt`
работает со встроенными значениями по умолчанию: без доступа к сети, без записи за пределы путей записи по умолчанию
и с неограниченным чтением. Файл настроек, который _есть_, но
пуст, не может быть прочитан или не проходит валидацию, является ошибкой: `srt` сообщает об этом и
завершает работу, а не откатывается к этим значениям по умолчанию, которые представляют собой другую
конфигурацию, а не более слабую — откат отбросил бы правила `denyRead`, `allowRead` и правила учётных данных из файла
вместе со всем остальным, что в нём было указано. То же самое относится к файлу, указанному через `--settings`, который также должен
существовать.
#### Обновление конфигурации во время выполнения команды: `--control-fd`
`--control-fd <fd>` читает обновления конфигурации из дескриптора, который вызывающая сторона уже
открыла, по одному JSON-объекту на строку в том же формате, что и файл настроек.
Каждая строка заменяет всю конфигурацию и валидируется по той же
схеме, поэтому строка — это полная конфигурация, а не патч: `network` и
`filesystem` обязательны, а ключ, который строка не содержит, возвращается к своему
значению по умолчанию.
```bash
# fd 3 is the read end of a pipe the caller writes lines to
srt --control-fd 3 -- npm test
```
Что меняет принятая строка после запуска команды:
- Списки сети — `network.allowedDomains`, `deniedDomains` (вместе с
`deniedDomainReasons`) и `deniedResolvedAddresses` — вступают в силу при
следующем соединении на всех платформах: прокси обращается к ним при каждом
запросе, а проверка разрешённых адресов перестраивается из той же строки.
То же касается `network.mitmProxy` и `network.tlsTerminate.excludeDomains`,
которые он также читает при каждом запросе.
- `credentials.sigv4` вступает в силу при следующем запросе при условии, что
srt запущен с блоком `credentials`: хук подписи устанавливается только в
этом случае и читает политики при каждом запросе, а не захватывает их.
- Больше ничего не меняется. Правила файловой системы уже находятся в профиле
seatbelt, в argv bubblewrap или в Windows ACE, маски учётных данных были
построены при обёртывании команды, а работающие прокси-серверы зафиксировали
`network.parentProxy` и то, завершается ли TLS вообще, в момент своего
запуска.
- В Windows srt замечает строку, набор доступа к файлам которой (`filesystem.*`
вместе с `credentials.files`) отличается от применённого при запуске, и
сообщает об этом только при `SRT_DEBUG`: выдача ACL действует в пределах
сессии, поэтому набор, применённый при запуске, остаётся в силе. Остальная
часть строки применяется, как описано выше.
Как srt читает канал и что он отклоняет:
- Дескриптор должен быть целым числом **3 или больше** и доступным для
чтения — `0`–`2` являются стандартными потоками. srt завершается с ошибкой
вместо запуска команды, когда не может прочитать переданный ему дескриптор,
поэтому мёртвый канал никогда не сойдёт за живой. Канал, который умирает,
не доставив ни одного обновления, утягивает за собой команду; канал,
который умирает после, сообщает об этом и оставляет команду работать с
последней применённой конфигурацией.
- Строка, не являющаяся допустимой конфигурацией, выводится в stderr и
отбрасывается; предыдущая конфигурация остаётся в силе. Сама строка
попадает в отладочный журнал, а не в терминал, а пустая строка игнорируется
молча.
- srt начинает чтение только после того, как песочница поднята, поэтому
обновление, записанное до этого, ожидает в канале, а не теряется — и не
может быть перезаписано конфигурацией, с которой запускается песочница.
Такая строка может быть прочитана, пока команда ещё обёртывается, и тогда
она становится конфигурацией, с которой она обёрнута, включая правила
файловой системы. То, будет ли она прочитана так рано, зависит от того,
сколько длится обёртывание (в Linux deny-глоб по большому дереву бывает
достаточно долгим), поэтому правило, под которым команда должна
запускаться, следует помещать в файл настроек.
- srt **завершается вместе с обёрнутой командой** и не ждёт, пока писатель
закроет дескриптор. Конец ввода тоже не является ошибкой: команда
продолжает работать с последней применённой конфигурацией.
- Давайте srt **выделенный конец только для чтения**. srt переводит канал
или сокет в неблокирующий режим, а этот флаг живёт на открытом описании
файла, поэтому всё остальное, что держит то же описание — `exec 3<fifo`
оболочки, `pass_fds` дескриптора, который родитель продолжает
использовать, — с этого момента получает `EAGAIN` при собственных
блокирующих чтениях.
- В macOS и Linux команда в песочнице не получает дескриптор: srt указывает
этот слот для команды на `/dev/null`, поэтому ничто внутри песочницы не
может прочитать обновления или записать собственную конфигурацию. В Windows
команда порождается только с тремя стандартными потоками, поэтому
управляющего дескриптора среди переданных ей дескрипторов нет.
### Как отдельный прокси с внешним решателем: `srt proxy`
`srt proxy` (или однофайловый исполняемый файл `srt-proxy`, собираемый
командой `bun run build:srt-proxy`) запускает только HTTP-прокси SRT, без
дочернего процесса в песочнице, для хоста, который выполняет рабочую нагрузку
в другом месте. Он принимает соединения на слушающем сокете, который передаёт
хост, завершает каждый CONNECT в своём процессе и пересылает запрос только
тогда, когда это разрешает отдельный процесс-решатель, доступный через другой
унаследованный дескриптор. Командную строку, протокол решателя и модель
безопасности см. в [docs/srt-proxy.md](https://github.com/anthropic-experimental/sandbox-runtime/blob/main/docs/srt-proxy.md).
### Как библиотека
```typescript
import {
SandboxManager,
type SandboxRuntimeConfig,
} from '@anthropic-ai/sandbox-runtime'
import { spawn } from 'child_process'
// Define your sandbox configuration
const config: SandboxRuntimeConfig = {
network: {
allowedDomains: ['example.com', 'api.github.com'],
deniedDomains: [],
},
filesystem: {
denyRead: ['~/.ssh'],
allowWrite: ['.', '/tmp'],
denyWrite: ['.env'],
},
}
// Initialize the sandbox (starts proxy servers, etc.)
await SandboxManager.initialize(config)
// Wrap a command with sandbox restrictions
const sandboxedCommand = await SandboxManager.wrapWithSandbox(
'curl https://example.com',
)
// Execute the sandboxed command
const child = spawn(sandboxedCommand, { shell: true, stdio: 'inherit' })
// Handle exit and cleanup after child process completes
child.on('exit', async code => {
console.log(`Command exited with code ${code}`)
// Cleanup when done (optional, happens automatically on process exit)
await SandboxManager.reset()
})
```
**Атрибуция нарушений (`commandId` / `commandText`).** Нарушения, зафиксированные во время выполнения обёрнутой команды (строки журнала seatbelt, события seccomp, отказы прокси), сохраняются под ключом атрибуции, и `annotateStderrWithSandboxFailures(key, stderr)` / `getViolationsForCommand(key)` ищут их по тому же самому ключу. По умолчанию ключом является сама обёрнутая строка. Передайте непрозрачный `commandId` для каждого вызова (например, id использования инструмента), чтобы использовать его в качестве ключа — рекомендуется: ключи сравниваются по первым 100 символам, поэтому длинные команды с общим префиксом иначе перепутали бы атрибуцию, а повторный запуск того же текста унаследовал бы события предыдущего запуска. Если строка, которую вы _выполняете_, не является командой, которую _представляет_ вызов (например, вы оборачиваете собранную `source <snapshot> && eval '<cmd>'`), также передайте `commandText: '<cmd>'`: именно с ним сопоставляются шаблоны команд `ignoreViolations` и именно его каждое нарушение сообщает как свой `command`. `commandId`, который вы передаёте в `wrapWithSandbox`, должен быть той же непустой строкой, которую вы затем передаёте в `annotateStderrWithSandboxFailures` / `getViolationsForCommand`; пустой обрабатывается как отсутствие `commandId` вообще, поэтому ключом становится команда.
Обрезается до 100 символов только ключ. Начиная с v0.0.76 сообщаемый `command` — и текст, с которым сопоставляются шаблоны команд `ignoreViolations`, — это вся команда для вызова, обёрнутого без `commandId`, а не её первые 100 символов; поэтому шаблон может подавить только больше, чем раньше, но никогда не меньше. Ключ атрибуции, который не зарегистрировал ни один вызов этого процесса (носители доступны для записи изнутри песочницы), сообщается очищенным и обрезанным до той же длины ключа.
```typescript
const wrapped = await SandboxManager.wrapWithSandbox(
assembledCommand, // what actually runs
undefined,
undefined,
undefined,
{ commandId: invocationId, commandText: rawCommand },
)
// ... run it ...
const annotated = SandboxManager.annotateStderrWithSandboxFailures(
invocationId,
stderr,
)
```
**Запрос о хосте, который не решает ни одно правило (колбэк ask).** Второй аргумент `SandboxManager.initialize` — необязательный `SandboxAskCallback`. Он запрашивается о назначении, которое не решили ни `network.deniedDomains`, ни `network.allowedDomains`, ни список разрешений для отдельной команды (следующий раздел), и он никогда не запрашивается при `network.strictAllowlist`. Без колбэка такое назначение запрещается. Колбэк возвращает одно из:
- `true` — разрешить соединение. Разрешает только значение `true`.
- `false` — запретить его. Строка нарушения сообщает общую причину `user denied`.
- `{ allow: false, reason: '...' }` — запретить его и сообщить `reason` в строке нарушения вместо общего текста, чтобы тот, кто читает блок `<sandbox_violations>` (включая модель), узнал, почему и что делать вместо этого. Клиент в песочнице тоже читает его как фразу статуса `403`, которым отвечают на его CONNECT.
Любой другой ответ запрещает, независимо от его истинности, и сообщает общую причину. Это включает объект, который несёт `reason` без указания `allow: false`, например `{ allow: true, reason: '...' }`: его причина не сообщается.
Причина очищается перед сохранением так же, как остальная часть строки нарушения. Каждая последовательность управляющих символов (включая переводы строк и табуляции) или невидимых символов (символы нулевой ширины, включая соединитель, и двунаправленные управляющие символы) превращается в один пробел; `<` и `>` удаляются, поэтому `re-run with <host> listed` сохраняется как `re-run with host listed`; концы обрезаются. Результат обрезается до 500 символов, считаемых как кодовые единицы UTF-16 так, как их считает `String.prototype.length`. Причина, от которой после этого ничего не осталось, заменяется на `user denied`. Пишите её как одну строку простого текста.
**Изменение поведения в первом выпуске после 0.0.79:** фильтр раньше разрешал при любом истинном ответе, поэтому колбэк, возвращавший истинное значение, отличное от `true` (`1`, строка, любой объект), разрешал соединение. Теперь он запрещает. По той же причине `{ allow: false, reason }`, возвращённый в более старый выпуск, был бы прочитан там как разрешение, поскольку объект истинен. `SandboxManager.askCallbackDenyReason` равен `true` в выпуске, который понимает объект: проверяйте его перед возвратом объекта и запрещайте простым `false` там, где он отсутствует.
```typescript
const deny = (reason: string) =>
SandboxManager.askCallbackDenyReason
? { allow: false as const, reason }
: false
await SandboxManager.initialize(config, async ({ host, port }) => {
if (await userApproves(host, port)) return true
return deny(`${host} was not approved for this session`)
})
```
**Списки разрешённых сетей для отдельных команд (`registerCommandNetworkLists`).** Один вызов может нести собственный список разрешённых сетей в дополнение к настроенному, на всё время своего выполнения:
```typescript
import { randomBytes } from 'node:crypto'
// 128 bits of randomness, 22 characters. See below for why nothing less will do.
const commandId = randomBytes(16).toString('base64url')
const wrapped = await SandboxManager.wrapWithSandbox(
command,
undefined,
undefined,
undefined,
{ commandId },
)
SandboxManager.registerCommandNetworkLists(commandId, {
allowedDomains: ['registry.example.org', '*.cdn.example.org:443'],
})
const child = spawn(wrapped, { shell: true })
const unregister = () => SandboxManager.unregisterCommandNetworkLists(commandId)
child.once('exit', unregister)
child.once('error', unregister) // the child never started
```
---
[Read more](https://github.com/anthropic-experimental/sandbox-runtime)