Материал начинается с наблюдения: за последние пару лет практически все крупные приложения — от Slack до Cursor, Notion, ChatGPT Desktop и Claude Desktop — обзавелись AI-функциями. Разработчик, потративший больше кредитов Copilot, чем обычно, решил разобраться, что именно происходит "под капотом" у VS Code и GitHub Copilot.

Общий знаменатель: Electron

Все перечисленные приложения построены на Electron — JavaScript-фреймворке для десктопных приложений. Он упаковывает Node.js runtime вместе с HTML, CSS и JavaScript, которые затем рендерятся через Chromium.

Список приложений на Electron
Список приложений на Electron. Источник

Это устраняет необходимость держать отдельные кодовые базы для каждой платформы (например, C# для Windows и Swift для macOS) — большая часть логики приложения общая для всех платформ, хотя нативные модули и упаковка часто всё равно требуют платформо-специфичной обработки.

Поскольку все эти приложения используют Electron, у них похожая архитектура — значит, находки на одном должны переноситься на остальные.

Сетевые пакеты, затем исходный код

Первым порывом было пролистать исходный код VS Code в поисках ответов. Проблема в том, что полного набора вопросов пока не было — искать их в миллионах строк кода означало бы потратить либо слишком много времени, либо слишком много токенов.

Исходный код показывает, что приложение может делать; выяснить, что оно действительно делает во время выполнения — задача сложнее. Особенно когда пока не знаешь, что искать.

Была и вторая проблема: VS Code — исключение среди рассматриваемых приложений, поскольку его исходный код (по большей части) открыт. Это не так для Claude, ChatGPT, Codex, Notion и Slack.

Это подтолкнуло к пути реверс-инжиниринга: сначала пассивно наблюдать за трафиком, позволить запросам и ответам подсказать, какие вопросы стоит задавать, и только потом обращаться к коду, чтобы подтвердить или опровергнуть увиденное.

Пришлось разобраться с архитектурой Electron и его сетевым стеком — навыки, которые в любом случае не бывают лишними.

Сетевая архитектура Electron

Приложения на Electron поставляются вместе с Chromium. Браузер обеспечивает движок рендеринга веб-интерфейса приложения, но также предоставляет сетевой стек, который renderer-процессы могут использовать для HTTP и WebSocket-соединений.

Это распространённый (и рекомендуемый) способ общения приложения с удалённым бэкендом, но не единственный: приложения также могут делать HTTP-запросы через Node.js http/https/fetch. То, каким путём идёт запрос, становится важным, когда пытаешься его перехватить.

В некоторых случаях, как у VS Code, приложение имеет разделённую архитектуру, где есть отдельная группа процессов, выступающая в роли extension host. Это помогает поддерживать чёткие границы между разными зонами ответственности — в случае VS Code, между UI, функциональностью IDE и плагинами/расширениями.

Модель процессов VS Code после введения sandboxing в конце 2022
Архитектура VS Code. Источник: документация VS Code

Перехват сетевого трафика Electron-приложений

Один из классических способов перехватить сетевой трафик приложения — поднять прокси-сервер и настроить приложение на его использование.

Прокси действует как man-in-the-middle (MITM): перехватывает HTTP-запросы от клиента, перенаправляет их на сервер и передаёт ответы сервера обратно клиенту.

Забавный факт: похожий подход довольно распространён в корпоративных сетях для инспекции трафика, особенно в жёстко регулируемых отраслях. Кстати, один из основных open source инструментов для этого называется mitmproxy — его и будем использовать дальше.

Важная деталь: большая часть сетевого трафика сегодня идёт по защищённому HTTP (HTTPS), то есть зашифрована с помощью TLS.

Доверяя локально сгенерированному сертификату центра сертификации (CA) mitmproxy, клиент принимает сертификаты, которые mitmproxy генерирует на ходу для каждого адресата. Вместо одного сквозного зашифрованного соединения получаются два: одно между приложением и mitmproxy, второе — между mitmproxy и целевым сервером.

Таким образом mitmproxy может расшифровать запрос, проинспектировать его, установить отдельное TLS-соединение с сервером и передать ответ обратно приложению.

С чего начать

Если не интересен код, а важны только результаты — этот раздел можно пропустить.

Установка mitmproxy

На macOS проще всего установить через brew:

brew install mitmproxy

Настройка VS Code

Нужно изменить несколько настроек VS Code, чтобы направить его трафик через mitmproxy. Настройки меняются через комбинацию клавиш Cmd+Shift+P и поиск User Settings. Убедитесь, что настройки ниже выставлены следующим образом:

  • Http Proxy: http://localhost:8080 (mitmproxy будет слушать подключения на этом порту)

  • Http Proxy Strict SSL: отключено (нужно пропустить проверку сертификата mitmproxy по списку CA)

  • Http: Proxy Support: override (принудительная поддержка прокси для расширений)

Настройки VS Code для маршрутизации трафика через mitmproxy
Настройки VS Code для маршрутизации трафика через mitmproxy

После изменения настроек нужно перезапустить VS Code.

Веб-интерфейс mitm

Последний шаг перед началом — запуск веб-интерфейса mitmproxy:

mitmweb

Через несколько секунд начнёт появляться сетевой трафик от VS Code.

В верхней части интерфейса есть несколько текстовых полей. Большинство пока можно игнорировать; самое полезное — первое, Search. Оно предоставляет мощные возможности поиска: по ключевым словам, регулярным выражениям и т. д. Например, если интересуют запросы VS Code к API Extensions Marketplace, достаточно использовать marketplace как фильтр.

Это найдёт все запросы к https://marketplace.visualstudio.com и его подпутям.

mitmweb - список захваченных потоков от VS Code
mitmweb — пример списка захваченных потоков от VS Code

Устаревшие процессы Extension Host

Может оказаться, что даже после всех этих манипуляций прокси не захватывает трафик расширений. Это происходит, если группа процессов Extension Host в VS Code устарела. Чтобы проверить это, выполните в терминале:

ps -eo pid,ppid,lstart,command | grep -i -E "copilot|extensionHost|Code Helper"

# Должно отображаться что-то вроде этого:

27896 27243 Fri Jul 24 15:40:11 2026.    \
    /Applications/Visual Studio Code.app/Contents/Frameworks/ # (...)

Проверьте отображаемую дату. Если она не совпадает с датой и временем перезапуска VS Code, процесс extension host скорее всего устарел. Решение простое:

  • Откройте Command Palette в VS Code (Cmd+Shift+P)

  • Выполните «Developer: Restart Extension Host»

  • Повторите grep по ps — теперь должны появиться новые PID с сегодняшней временной меткой для Code Helper

Что Copilot делает до того, как вы нажали хоть одну клавишу

Быстрый просмотр сетевых запросов от VS Code в mitmweb показывает, что большинство из них связаны либо с GitHub, либо напрямую с GitHub Copilot. Ещё до первого нажатия клавиши в VS Code или в расширении Copilot выполняется целая серия HTTP-запросов.

Общий анализ

Запросы, которые VS Code и Copilot делают на этапе загрузки, распределяются по следующим категориям: аутентификация и сессия, конфигурация и политики, реестр MCP, контекст репозитория и сессии, обнаружение моделей и недавние репозитории.

Далее — детали по каждому типу запросов: что содержится в заголовках и телах запросов и ответов.

Распределение сетевых запросов VS Code на этапе загрузки
Общий вид распределения сетевых запросов VS Code на этапе загрузки

Аутентификация и запуск сессии

Это первое, что делает Copilot при старте. Он получает OAuth-токен, обменивает его на краткоживущий токен и проверяет права пользователя. Схема довольно стандартная для OAuth — она описана на диаграмме ниже.

Диаграмма последовательности OAuth-аутентификации Copilot
Диаграмма последовательности, описывающая OAuth-аутентификацию Copilot

Обнаружение моделей и возможностей

Перед выполнением любых запросов к LLM Copilot проверяет, какие модели и возможности агентов доступны для аккаунта или плана.

Здесь два отдельных типа запросов. Сначала запрос к /models — он возвращает общий список доступных в Copilot моделей.

Затем следует второй запрос к /agents/swe/models — конкретный запрос для выяснения, какие модели доступны для агентных возможностей, связанных с разработкой ПО (SWE).

Последовательность запросов Copilot на этапе обнаружения моделей
Последовательность запросов Copilot на этапе обнаружения моделей: один для общего списка моделей, второй — специально для моделей с агентными возможностями в разработке ПО

Детали о промптах, контексте и harness

После этапа загрузки начинается самое интересное.

Роутер моделей в Copilot

Для всех тестов Copilot в этом эксперименте использовался режим Auto. После отправки каждого сообщения удавалось перехватить запрос к эндпоинту /models/session/intent ещё до того, как модель отвечала.

Здесь происходит следующее: промпт оценивается по возможным намерениям — таким как code-gen, debugging, reasoning и tool-use. Результат классификации намерения помогает Copilot определить, какая из доступных моделей выполнит задачу.

Диаграмма отдельных запросов для обнаружения моделей и возможностей
Диаграмма, показывающая отдельные запросы для обнаружения моделей и возможностей

Это не секрет — такое поведение описано в документации Copilot. Но всё равно было интересно увидеть реальные запросы и ответы, стоящие за этим.

(Секретные) переменные окружения

Дальше пошли эксперименты с инлайн-автодополнением и ghost text, с наблюдением за тем, что отправляется по HTTP. Было уже известно, что инлайн-автодополнение вставляет текущий файл в промпт как контекст — так это и должно работать. Пока никаких сюрпризов.

Но оставался вопрос, что ещё отправляется, поэтому был проведён небольшой тест: в файл .env добавили фейковый секрет — тот самый пресловутый файл, который все обещают не коммитить, но некоторые всё равно коммитят.

TEST_ENV_VAR_SECRET=”a realistic looking fake token”

Редактирование этого файла не вызвало никаких HTTP-запросов, что показалось хорошим знаком. Затем был открыт совершенно не связанный файл pyproject.toml, и там началось печатание.

И вот что удивительно — во время печати ушёл следующий запрос на автодополнение:

{
    "prompt":"TEST_ENV_VAR_SECRET=\"mysecretenvvar\"\n\nT",
    "suffix":"",
    "max_tokens":500,
    "temperature":0,
    "top_p":1,
    "n":1,
    "stop":["\n\n\n","\n```"],
    "stream":true,
    "extra":{
        "language":"dotenv",
        "next_indent":0,
        "trim_by_indentation":true,
        "prompt_tokens":175,
        "suffix_tokens":0,
        "context":[
           "Path: .env",
           "These are recently edited files. Do not suggest code that has been deleted.\nFile: pyproject.toml\n--- a/file:///Users/rafaelpierre/copilot-mitm/pyproject.toml\n+++ b/file:///Users/rafaelpierre/copilot-mitm/pyproject.toml\n@@ -18,4 +18,4 @@\n     \"polars>=1.41.0\",\n ]\n \n+# testing\n- --- IGNORE ---\nFile: config.ini\n--- a/file:///Users/rafaelpierre/copilot-mitm/config.ini\n+++ b/file:///Users/rafaelpierre/copilot-mitm/config.ini\n@@ -1,2 +1,3 @@\n TEST_CONFIG=\"test-config\"\n \n+# test .env\nEnd of recent edits"
        ]
    },
    "code_annotations":false
}

Первой мыслью было: ладно, просто отключу Copilot для файлов .env. Оказалось, он уже был отключён — про это просто забыли.

Впрочем, это не имело бы значения: запрос был вызван нажатиями клавиш в файле pyproject.toml, где инлайн-автодополнение было включено и работало.

Заметка на память: отключение инлайн-автодополнения для .env или любого другого «секретного» расширения файла ничего не меняет, потому что запрос запускается не им. Но другие запросы всё же могут быть вызваны.

Просьба к Copilot освежить память

В системных промптах многих перехваченных запросов на автодополнение встречалось определение инструмента session_store_sql. Вот его описание, взятое из одного из таких запросов:

Query the local session store containing history from past coding sessions.

Uses SQLite syntax (NOT DuckDB or Postgres).

SQL queries are read-only — only SELECT and WITH are allowed.

Use `datetime('now', '-1 day')` for date math (NOT `now() - INTERVAL '1 day'`), FTS5 `MATCH` for text search.

Tables: `sessions`, `turns`, `session_files`, `session_refs`, `checkpoints`, `search_index`.

For column details and query patterns, use the **chronicle** skill.

Actions: 'query' (execute SQL — supports JOINs, FTS5 MATCH, aggregations), 'reindex' (rebuild index from debug logs).

Однако результатов вызова этого инструмента в перехваченном трафике не было видно — вероятно, он просто не вызывался. Чтобы это проверить, был предпринят вызов инструмента напрямую — простой вопрос в чате: «Над чем я работал на этой неделе?».

После этого начался обмен запросами между моделью и локальной базой данных SQLite под названием session-store.db, о существовании которой ранее не было известно:

Последовательность HTTP-запросов со спецификацией и выполнением инструмента
Последовательность HTTP-запросов: спецификация инструмента, выбор, выполнение и финальный запрос с результатами выполнения инструмента для локальной SQLite-базы, где хранятся предыдущие промпты, действия и т. д.

Как выяснилось из этих переписок, session_store_sql — часть инструмента Chronicle в Copilot, который позволяет выполнять SQL-запросы к session-store.db. В этой базе хранятся сводки сессий, репозитории и ветки, с которыми вы работали.

Там же хранятся все ваши промпты вместе с соответствующими ответами LLM. Copilot ведёт запрашиваемую историю всего, что вы у него спрашивали, и обращается к ней при необходимости.

Одна деталь бросилась в глаза: модель не знала схему базы заранее. Сначала она попыталась выполнить запрос ниже, который завершился ошибкой.

# Tool definition gets sent

{
    "type":"function_call",
    "name":"session_store_sql",
    "arguments":"{
        \"action\":\"query\",
        \"description\":\"Fetch recent session activity for the past week\",
        \"query\":\"SELECT s.id, s.start_time, s.title, t.turn_index, t.role, t.content FROM sessions s JOIN turns t ON t.session_id = s.id WHERE s.start_time >= datetime('now', '-7 days') ORDER BY s.start_time, t.turn_index;\"
    }",
    "call_id":"call_Ay35CDeV0EFXtvFI8l3VgbWI"
}

# Tool gets executed locally, results are sent back to the agent/LLM:

{
    "type":"function_call_output",
    "call_id":"call_Ay35CDeV0EFXtvFI8l3VgbWI",
    "output":"Error: no such column: s.start_time"
}

После этого модель проинспектировала метаданные схемы, чтобы найти определения таблиц. Только после этого она смогла получить какие-то записи из локальной базы SQLite.

# Session Store SQLite DB introspection tool call

{
    "type":"function_call",
    "name":"session_store_sql",
    "arguments":"{
        \"action\":\"query\",
        \"description\":\"Inspect session store schema\",
        \"query\":\"
            SELECT name, sql
            FROM sqlite_schema
            WHERE type IN ('table','view');
        \"
    }",
    "call_id":"call_wY9dGEI4DSYbOXzpzPg3JTGN"
}

# Introspection tool call results get sent back to agent/LLM:

{
    "type":"function_call_output",
    "call_id":"call_wY9dGEI4DSYbOXzpzPg3JTGN",
    "output":"Results: 13 rows (source: local)
        | name | sql |
        | --- | --- |
        | schema_version | CREATE TABLE schema_version (\n\t\t\t\tversion INTEGER NOT NULL (...)\
    ",
}

Дальше возник интерес — что ещё хранится в этой базе сессий. Начали с изучения метаданных.

$ sqlite3 ~/Library/Application Support/Code/User/globalStorage/github.copilot-chat/session-store.db

# Output

CREATE TABLE turns (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id TEXT NOT NULL REFERENCES sessions(id),
  turn_index INTEGER NOT NULL,
  user_message TEXT,
  assistant_response TEXT,
  timestamp TEXT DEFAULT (...),
  UNIQUE(session_id, turn_index)
);

Как видно, user_message и assistant_response хранятся в виде обычного текста. Стоит сделать несколько запросов вручную.

$ sqlite3 session-store.db "SELECT substr(user_message,1,60) FROM turns LIMIT 5;"
What is ML?
hello
testing

Это были сообщения, отправленные Copilot ранее для тестирования захвата mitmproxy — снова без сюрпризов. Но что насчёт сообщений, которые потенциально могли бы содержать что-то более… проблемное?

Чтобы это проверить, в чат Copilot было отправлено сообщение с фейковыми секретными данными: фейковым GitHub-токеном, фейковым AWS-ключом, строкой подключения с паролем.

После этого была снова проверена база данных на предмет записанного.

$ sqlite3 session-store.db "SELECT user_message FROM turns \
    WHERE user_message LIKE '%ghp_%' OR user_message LIKE '%postgres://%';"

...
GITHUB_TOKEN=ghp_«fake token, stored exactly as typed»
DATABASE_URL=postgres://admin:«password»@db.example.com:5432/prod
...

Соблазн назвать статью «Всё в открытом виде» присутствовал.

Но хотя это и правда, реальный вывод оказался менее тревожным — и при этом более интересным: AI-инструменты для разработки становятся stateful-системами.

AI-инструменты для разработки становятся stateful-системами. Они всё чаще объединяют рабочее пространство пользователя + недавние правки + переписки + инструменты + историю + маршрутизацию моделей.

Каждый новый источник контекста повышает полезность и увеличивает объём состояния разработчика, к которому у системы есть доступ. Но это и приносит дополнительные сложности: рост объёма контекста, вопросы конфиденциальности данных и приватности.

Реверс-инжиниринг сам по себе был увлекателен, но также захотелось проверить, насколько предположения соответствуют реальному коду.

Сопоставление находок с исходным кодом

Незашифрованное хранилище сессий

Код хранилища сессий — часть расширения Chronicle, находится в sessionStore.ts. Определение таблицы точно совпадает с увиденным на диске: user_message и assistant_response хранятся как обычный текст, без какой-либо маскировки на уровне столбцов.

Но сама схема не говорит о том, очищаются ли данные при записи. Об этом говорит путь записи. Вот вставка, которая фиксирует каждый turn:

INSERT INTO turns (session_id, turn_index, user_message, assistant_response, timestamp)
VALUES (?, ?, ?, ?, ?)

…и значения, которые в неё передаются:

turn.session_id,
turn.turn_index,
turn.user_message ?? null,
turn.assistant_response ?? null,
turn.timestamp ?? new Date().toISOString(),

turn.user_message записывается как есть. Поиск по коду на предмет редактирования, санитизации, фильтрации секретов или маскировки в пути записи ничего не дал. Хранение в открытом виде — не баг и не упущенный краевой случай, это просто то, как код устроен.

Это отвечает на первый вопрос: это осознанное решение — в том смысле, что ничего никогда не строилось для предотвращения такого хранения.

Утекать или не утекать

Строка «recently edited files», увиденная в захвате mitm, идёт из recentEdits.tsx. Поведение скользящего окна по умолчанию жёстко задано в коде: до 20 файлов, 8 сводок правок и 3 строки контекста вокруг каждого изменения — именно так строка, которую не трогали (с фейковым секретом), оказалась частью HTTP-запроса к API Copilot.

Правила по умолчанию для .env нигде нет. На индивидуальном плане ничего не относится к .env как к особому случаю, и никакой интеграции с текущим .gitignore пространства тоже нет.

Есть механизм исключения, но он привязан к «политике репозитория» — функции уровня Business/Enterprise GitHub, управляемой администратором.

Заключение

С большой силой приходит большая ответственность

Это оказалось отличным упражнением в понимании того, как AI-инструмент для написания кода реализует свой harness. Многие из этих деталей и практик могут быть полезны разным командам, строящим собственные AI-системы.

Вопросы, которые часто возникают при построении таких систем, остаются актуальными и после этого разбора. Какой контекст должен внедряться? Что должно отправляться модели? Что должно оставаться локально? Какие инструменты модель должна иметь возможность вызывать? Что хранится в краткосрочной памяти? Что переходит в долгосрочную?

Контекст становится продуктом

Всё более очевидно, что контекст становится продуктом.

Модели и результаты на бенчмарках SOTA, конечно, важны. Но реальная дифференциация между AI-инструментами для разработки — и AI-инструментами в целом — всё больше смещается в сторону того, насколько хорошо они собирают правильный контекст: код, недавние изменения, действия, переписки, инструменты, историю и всё остальное, что может помочь решить задачу. Это создаёт две проблемы.

Первая — инженерная: больше контекста не обязательно означает лучший контекст. Задача — держать его компактным, релевантным и удобным для кэширования, не перегружая модель раздутым промптом.

Вторая связана с приватностью и конфиденциальностью. Чем больше harness собирает и сохраняет контекстных данных, тем тщательнее нужно определять, что может пересекать границы — между файлами, сессиями, машинами и, наконец, API модели.

Copilot явно движется в этом направлении, и некоторые находки выглядят действительно продуманными. Другие вызвали неловкое чувство. Пока ничего из этого не убедило вернуться в число платящих пользователей.

Но кое в чём другом это убедило: если вы строите AI-приложения, реверс-инжиниринг и изучение harness вокруг моделей может научить больше, чем изучение самой модели.