
За последнюю пару лет появилось множество приложений на основе ИИ и ИИ‑функций. Чем больше появляется таких приложений, тем любопытнее мне становится изучить их внутреннюю работу. Надеюсь, мне удастся узнать что‑то новое; по крайней мере, почерпну какие‑нибудь знания о разработке десктопных приложений.
В то же время я заметил, что каждый месяц всё быстрее исчерпываю свои кредиты Copilot. Это заставило меня выбрать его в качестве главного кандидата для моих экспериментов. Я решил подробно исследовать VS Code и Copilot.
Общий делитель: Electron
Общее для подобных приложений заключается в том, что они разработаны на основе Electron. Это JavaScript‑фреймворк, помогающий разработчикам собирать и распространять десктопные приложения. Если вкратце, он объединяет среду исполнения Node.js с артефактами HTML, CSS и JavaScript, которые затем рендерятся при помощи Chromium.
Это позволяет избавиться от необходимости поддержки нескольких кодовых баз на нативных языках для разных платформ (например, C# для Windows и Swift для macOS), упрощая разработчикам создание десктопных приложений, работающих из единой кодовой базы на множестве разных платформ. (Для нативных модулей и некоторых этапов упаковки всё же требуется платформозависимая обработка, но основная часть логики приложений остаётся неизменной.)
Так как все они используют Electron, у них приблизительно одинаковая архитектура, а значит, навыки исследования одного приложения должны быть применимы и к другим.
Сначала сетевые пакеты, потом исходники
Первым делом я хотел просто изучить исходники VS Code, чтобы понять, смогут ли они ответить на мои вопросы. Проблема в том, что у меня пока не было полного списка вопросов, а их поиск среди миллионов строк кода стоил бы мне или слишком много времени, или слишком много токенов.
Это заставило меня двигаться по пути реверс‑инжиниринга: сначала пассивно отслеживать трафик, позволив запросам и ответам сформулировать интересующие меня вопросы, и только затем переходить к исходникам, чтобы подтвердить (или опровергнуть) наблюдаемое мной.
Для этого мне придётся погрузиться в изучение архитектуры и сетевого стека Electron, а эти навыки могут приходиться и в будущем.
Сетевая архитектура Electron
Мы уже знаем, что приложения Electron содержат Chromium. Браузер предоставляет движок рендеринга для веб‑UI приложения, а также обеспечивает сетевой стек, который могут использовать процессы рендерера для соединений по HTTP и WebSocket.
Это популярный (и рекомендуемый) способ общения приложений с удалённым бэкендом, но он не единственный. Приложения также могут выполнять HTTP‑запросы при помощи http/https/fetch Node. Если мы пытаемся перехватить запрос, то важен путь, по которому он идёт.
В некоторых случаях, например, в случае VS Code, приложение имеет изолированную архитектуру: отдельная группа процессов работает с хостом расширений. Это помогает поддерживать чёткие границы между задачами; в случае VS Code это граница между UI, базовой функциональностью IDE и плагинами/расширениями.
Изучаем сетевой трафик от приложений Electron
Один из классических способов перехвата сетевого трафика приложения заключается в создании прокси‑сервера, которым будет пользоваться приложение.
Прокси действует в качестве «человека посередине» (man‑in‑the‑middle, MITM): он перехватывает HTTP‑запросы со стороны клиента, переадресует их на сервер и ретранслирует ответы сервера клиенту.
Любопытный факт: похожее решение довольно часто применяется для изучения трафика в корпоративных сетевых окружениях, особенно в отраслях с сильной нормативной регуляцией. Один из основных опенсорсных инструментов для этого называется mitmproxy; его мы и будем использовать в дальнейшем.
Важная особенность заключается в том, что сегодня сетевой трафик почти всегда передаётся через защищённый HTTP (HTTPS). То есть трафик зашифрован при помощи TLS.
Доверившись локально сгенерированному поставщику сертификатов (certificate authority, CA) mitmproxy, клиент может принимать генерируемые на лету сертификаты mitmproxy для каждой конечной точки. Вместо одного сквозного зашифрованного подключения мы получаем два: одно между приложением и mitmproxy, второе между mitmproxy и целевым сервером.
Следовательно, mitmproxy может расшифровать запрос, изучить его, создать отдельное TLS‑соединение на более высоком уровне и переадресовать ответ обратно приложению.
Приступаем к работе
Если вы не хотите изучать код и вам просто нужно узнать результаты, то можете пропустить этот раздел.
Установка mitmproxy
В macOS проще всего воспользоваться brew:
brew install mitmproxy
Настройка VS Code
Для маршрутизации трафик через mitmproxy нужно изменить параметры VS Code. Для этого можно нажать сочетание клавиш Cmd+Shift+P и выполнить поиск User Settings. Затем следует задать для приведённых ниже настроек следующие значения:
-
Http Proxy: http://localhost:8080 (mitmproxy будет слушать соединения по этому порту)
-
Http Proxy Strict SSL: снять флажок (мы хотим отказаться от верификации сертификата mitmproxy по списку CA)
-
Http: Proxy Support: override (принудительная поддержка прокси для расширений)

После внесения этих изменений следует перезапустить VS Code.
Веб‑UI mitm
Последним этапом перед началом работы будет запуск веб‑UI mitmproxy:
mitmweb
Подождите несколько секунд, и вскоре вы начнёте видеть, как него проходит сетевой трафик от VS Code.
В верхней части окна можно увидеть несколько текстовых полей. Пока самое полезное из них для нас первое — Search. Это поле позволяет выполнять поиск по ключевым словам, регулярным выражениям и так далее. Например, если нас особенно интересуют запросы, выполняемые VS Code к его Extensions Marketplace API, то можно просто в качестве строки фильтра указать marketplace.
Будут найдены все совпадения запросов к https://marketplace.visualstudio.com.

Устаревшие процессы Extension Host
Может случиться так, что даже после всех этих действий прокси всё ещё не сможет перехватывать трафик расширений. Такое может произойти, если группа процессов VS Code Extension Host устарела. Чтобы проверить это, выполните в терминале следующую команду:
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, то процесс хоста расширений, скорее всего, устарел. Эта проблема решается легко:
-
В VSCode откройте Command Palette (
Cmd+Shift+P) -
Выполните «Developer: Restart Extension Host»
-
Затем снова выполните команду
psgrep; вы должны теперь увидеть новые PID с актуальной временной меткойCode Helper
Что делает Copilot до того, как мы начинаем ввод
Окинув взглядом сетевые запросы от VS Code в mitmweb, вы заметите, что основная их часть связана или с GitHub, или с Github Copilot. HTTP‑запросы выполняются ещё до того, как мы нажмём хотя бы одну клавишу в VS Code или в расширении Copilot.
Высокоуровневый анализ
Запросы, выполняемые VS Code и Copilot на этапе подготовки, можно разделить на следующие категории: аутентификация и сессия (Auth & Session), конфигурация и политики (Config & Policy), реестр MCP (MCP Registry), контекст репозитория и сессии (Repo & Session), определение доступности моделей (Model Discovery) и недавние репозитории (Recent repos).
Ниже я расскажу о том, что обнаружил в каждом типе запросов: информацию, включённую в заголовки и полезную нагрузку запросов и ответов.

Подготовка аутентификации и сессии
Это первое, что делает Copilot при запуске. Он получает токен OAuth, обменивает его на токен с коротким сроком действия и валидирует права пользователя. Процесс практически аналогичен обычному потоку OAuth; он описан на диаграмме.
Определение доступности моделей и возможностей
Перед выполнением запросов к LLM Copilot проверяет, какие модели и функции агентов доступны для его аккаунта/тарифа.
Есть два вида запросов. Первый — это запрос, выполняемый к /models. Этот начальный запрос возвращает общий список моделей, доступных внутри Copilot.
Следующий запрос выполняется к /agents/swe/models. Это запрос нужен для определения того, какие модели доступны для агентских функций, связанных с разработкой ПО (SWE).
Немного информации о промптах, контексте и обвязке
Самое интересное начинается после этой подготовки.
Маршрутизатор модели Copilot
В этом эксперименте я выбрал режим Auto для всех тестов Copilot. После отправки каждого сообщения я мог перехватывать запрос к конечной точке /models/session/intent ещё до ответа модели.
Здесь происходит следующее: промпт пользователя оценивается на его возможный смысл, например, code-gen, debugging, reasoning и tool-use. Результат классификации смысла помогает Copilot решить, какая из доступных моделей будет исполнять задачу.

На самом деле, это не секрет: такое поведение описано в документации Copilot, но мне всё равно было любопытно изучать реальные запросы и ответы на них.
(Секретные) переменные окружения
Я начал экспериментировать со встроенными подсказками и призрачным текстом, наблюдая за передаваемыми по HTTP данными. Я уже знал, что функция встроенных подсказок инъецирует в промпты в качестве контекста текущий файл; именно так она и должна работать.
Но мне всё равно было любопытно, что ещё передаётся, поэтому я провёл небольшой тест. Я вставил фальшивый секрет в файл .env — печально известный файл, который рекомендуют не коммитить, но некоторые из нас всё равно это делают.
TEST_ENV_VAR_SECRET=”реалистично выглядящий фальшивый токен”
Редактирование этого файла на вызвало никаких HTTP‑запросов, и я посчитал, что это хороший знак. Затем я открыл совершенно не связанный с ним файл pyproject.toml и начал вводить в него текст.
Узрите же, какой запрос подсказки я увидел:
{
"prompt":"TEST_ENV_VAR_SECRET="mysecretenvvar"nnT",
"suffix":"",
"max_tokens":500,
"temperature":0,
"top_p":1,
"n":1,
"stop":["nnn","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.tomln--- a/file:///Users/rafaelpierre/copilot-mitm/pyproject.tomln+++ b/file:///Users/rafaelpierre/copilot-mitm/pyproject.tomln@@ -18,4 +18,4 @@n "polars>=1.41.0",n ]n n+# testingn- --- IGNORE ---nFile: config.inin--- a/file:///Users/rafaelpierre/copilot-mitm/config.inin+++ b/file:///Users/rafaelpierre/copilot-mitm/config.inin@@ -1,2 +1,3 @@n TEST_CONFIG="test-config"n n+# test .envnEnd 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 — это часть инструмента Copilot Chronicle, позволяющий выполнять SQL‑запросы к session-store.db. В этой базе данных хранятся краткие сводки сессий, репозитории и ветви, с которыми работал пользователь.
Также в ней хранятся все промпты пользователя и соответствующие ответы LLM. Copilot хранит историю всего, что я у него спрашивал, и при необходимости обращается к этой истории.
Привлекло моё внимание то, что модель не знала заранее схему. Изначально она пробовала выполнить показанный ниже запрос, который завершился неудачно.
# Отправка определения инструмента
{
"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"
}
# Инструмент исполняется локально, результаты передаются агенту/LLM:
{
"type":"function_call_output",
"call_id":"call_Ay35CDeV0EFXtvFI8l3VgbWI",
"output":"Error: no such column: s.start_time"
}
Затем она выполняет интроспекцию метаданных схемы, чтобы найти определения таблиц. После этого ей наконец‑то удаётся получить записи из моей локальной базы данных SQLite.
# Вызов инструмента интроспеции базы данных SQLite Session Store
{
"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"
}
# Результаты вызова инструмента интроспекции отправляются агенту/LLM:
{
"type":"function_call_output",
"call_id":"call_wY9dGEI4DSYbOXzpzPg3JTGN",
"output":"Results: 13 rows (source: local)
| name | sql |
| --- | --- |
| schema_version | CREATE TABLE schema_version (nttttversion INTEGER NOT NULL (...)
",
}
Мне стало любопытно узнать, что ещё хранится в данных сессий, поэтому я начал изучать метаданные.
$ sqlite3 ~/Library/Application Support/Code/User/globalStorage/github.copilot-chat/session-store.db
# Вывод
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
...
Честно говоря, у меня было искушение дать статье заголовок «Всё лежит здесь и в незашифрованном виде».
Но хотя это правда, мой вывод был гораздо менее тревожным и, как мне кажется, более интересным: инструменты ИИ‑кодинга становятся хранящими состояние системами.
Инструменты ИИ‑кодинга становятся системами, хранящими состояние. Они всё теснее сочетают в себе пользовательское рабочее пространство + недавние изменения + беседы + инструменты + историю + маршрутизацию моделей.
Каждый новый источник контекста повышает полезность и увеличивает объём состояния разработчика, доступного системе. Но это привносит и дополнительные трудности: всё большее раздувание контекста, проблемы конфиденциальности и приватности данных.
Хоть мне и понравилось заниматься этим реверс‑инжинирингом, я ещё и заинтересовался, подтверждаются ли мои предположения в реальном коде. Чтобы проверить это, мне нужно было изучить код.
Проверка моих открытий при помощи исходников
Незашифрованное хранение сессий
Код хранения сессий — часть расширения Chronicle; он находится в sessionStore.ts. Определение таблиц там точно такое же, какое я видел на диске: user_message и assistant_response хранятся в незашифрованном тексте, никакого маскирования на уровне столбцов или чего‑то подобного.
Но сама схема не говорит о том, очищаются ли данные при поступлении. Об этом нам говорит путь записи. Вот insert, который записывается при каждой реплике:
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‑запроса к Copilot API.
Правила запрета на
.envпо умолчанию нигде нет. На индивидуальном тарифе система не обращается с файлами.env, как с чем‑то особенным; отсутствует и какая‑либо интеграция с.gitignoreтекущего пространства.
Есть возможность исключения расширения, но она привязана к «политике репозитория» — функции GitHub Business/Enterprise, контролируемой администратором.
В заключение
С большой силой приходит большая ответственность
Моё исследование оказалось замечательным способом разобраться, как инструменты ИИ‑кодинга реализуют свои обвязки. Я считаю, что эту информацию следует изучать командами, создающими собственные ИИ‑системы.
Часть вопросов, которыми я часто задаюсь при разработке таких систем, остаётся актуальной. Какой контекст следует инъецировать? Что нужно передавать модели? Что должно оставаться локальным? Какие инструменты должны быть доступны модели? Что хранить в кратковременной памяти? Что надо переносить в долговременную?
Контекст становится продуктом
Я считаю, что контекст всё больше становится продуктом.
Разумеется, модели и бенчмарк SOTA важны, но главное различие между инструментами ИИ‑кодинга (а также между ИИ‑инструментами в целом), похоже, постепенно сдвигается в сторону качества сборки нужного контекста: кода, последних изменений, действий, бесед, инструментов, истории и всего остального, что помогает решать текущие задачи. При этом возникают две сложности.
Во‑первых, инженерная: увеличение контекста не всегда приводит к улучшению контекста. Сложность в том, чтобы он оставался сжатым, релевантным и удобным для кэширования, не позволяя модели утонуть в раздувшихся промптах.
Во‑вторых, вопрос приватности и конфиденциальности. Чем больше контекстных данных собирает и хранит обвязка, тем тщательнее она должна определять, что может пересекать границы (между файлами, сессиями, машинами и, в конечном итоге, API моделями).
Очевидно, Copilot движется в этом направлении, и часть обнаруженных мной решений можно считать продуманными. Другая же часть вызвала у меня чувство неловкости. И пока ничто из увиденного не убедило меня снова становиться платным пользователем.
Но это убедило меня кое в чём ещё: если вы разрабатываете ИИ‑приложения, реверс‑инжиниринг и изучение обвязок моделей может научить вас большему, чем изучение самой модели.
Автор: PatientZero


