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

Это устраняет необходимость держать отдельные кодовые базы для каждой платформы (например, 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 и плагинами/расширениями.

Перехват сетевого трафика 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.
Веб-интерфейс mitm
Последний шаг перед началом — запуск веб-интерфейса mitmproxy:
mitmweb
Через несколько секунд начнёт появляться сетевой трафик от VS Code.
В верхней части интерфейса есть несколько текстовых полей. Большинство пока можно игнорировать; самое полезное — первое, Search. Оно предоставляет мощные возможности поиска: по ключевым словам, регулярным выражениям и т. д. Например, если интересуют запросы VS Code к API Extensions Marketplace, достаточно использовать marketplace как фильтр.
Это найдёт все запросы к https://marketplace.visualstudio.com и его подпутям.

Устаревшие процессы 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, контекст репозитория и сессии, обнаружение моделей и недавние репозитории.
Далее — детали по каждому типу запросов: что содержится в заголовках и телах запросов и ответов.

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

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

Детали о промптах, контексте и 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, о существовании которой ранее не было известно:

Как выяснилось из этих переписок, 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 вокруг моделей может научить больше, чем изучение самой модели.