diff --git a/docs/MILESTONE_PLATFORM_RUNTIME_5b.md b/docs/MILESTONE_PLATFORM_RUNTIME_5b.md new file mode 100644 index 0000000..a23a7c7 --- /dev/null +++ b/docs/MILESTONE_PLATFORM_RUNTIME_5b.md @@ -0,0 +1,187 @@ +# Milestone 5b — Frontend Bundle Host / Plugin API Stub + +## Цель + +Первый настоящий frontend plugin host слой: shell умеет загружать frontend bundle плагина, давать ему ограниченный VerstakPluginAPI stub и рендерить view/settings panel через зарегистрированный component id. + +## Что реализовано + +### 1. Bundle Contract + +Плагин регистрирует компоненты через глобальную функцию: + +```javascript +window.VerstakPluginRegister('verstak.platform-test', { + components: { + 'DiagnosticsPanel': { + mount: function(containerEl, props, api) { + // Рендерит UI в containerEl + // containerEl — div, созданный PluginBundleHost + // api — ограниченный VerstakPluginAPI + }, + unmount: function(containerEl) { + // Очистка при смене view/unmount + containerEl.innerHTML = ''; + } + }, + 'PlatformTestSettings': { + mount: function(containerEl, props, api) { /*...*/ }, + unmount: function(containerEl) { /*...*/ } + } + } +}); +``` + +**VerstakPluginAPI** — ограниченный API, передаваемый в mount(): + +| Метод | Статус | Описание | +|---|---|---| +| `api.pluginId` | ✅ Работает | ID плагина | +| `api.capabilities.has(id)` | ✅ Stub | Возвращает false (planned: реальный запрос к registry) | +| `api.events.publish(type, payload)` | ✅ Stub | Логирует в console (planned: event bus bridge) | +| `api.events.subscribe(type, handler)` | ✅ Stub | Логирует в console (planned: event bus bridge) | +| `api.settings.read(key)` | ✅ Stub | Возвращает null (planned: backend storage namespace) | +| `api.settings.write(key, value)` | ✅ Stub | Логирует в console (planned: backend storage) | +| `api.commands.execute(id, args)` | ✅ Stub | Логирует в console (planned: command execution) | + +### 2. Безопасная резолюция asset path + +**Backend методы:** + +| Метод | Описание | +|---|---| +| `GetPluginFrontendInfo(pluginID)` | Возвращает frontend metadata (entry, style, rootPath, name, icon, version) | +| `GetPluginAssetContent(pluginID, assetPath)` | Читает файл из директории плагина с валидацией безопасности | + +**Проверки безопасности:** +- Абсолютные пути (начинающиеся с `/` или `\`) — отклоняются +- Path traversal (`..`) — отклоняется +- Выход за пределы plugin root — отклоняется через `filepath.Abs` + `strings.HasPrefix` + +### 3. FrontendPluginHost + +**PluginBundleHost.svelte** — загружает и рендерит плагин бандлы: + +1. Получает plugin frontend info через `GetPluginFrontendInfo()` +2. Если у плагина есть frontend entry — загружает JS контент через `GetPluginAssetContent()` +3. Выполняет bundle через `new Function(content)` (безопасно: нет доступа к внешней области видимости) +4. Ждёт вызов `VerstakPluginRegister` и находит компонент по componentId +5. Создаёт `VerstakPluginAPI` и вызывает `component.mount(container, props, api)` +6. При смене view — вызывает `component.unmount(container)` и очищает + +**Error boundary:** +- Если bundle не загружается — fallback с pluginID, componentId, error text +- Если компонент не найден — показывает доступные components +- Если mount выбрасывает исключение — fallback без падения shell +- Все состояния: idle, loading, error, loaded + +### 4. ViewContainer.svelte + +- Проверяет наличие frontend bundle у плагина +- Если есть — рендерит PluginBundleHost +- Если нет — показывает "frontend bundle not available" placeholder +- Badge в заголовке: "frontend bundle" (зелёный) или "no frontend bundle" (красный) + +### 5. PluginManager — Settings Panel + +- Убран hardcoded platform-test settings form +- Settings panel рендерится через PluginBundleHost, если у плагина есть frontend entry +- Если нет — показывает "Settings panel frontend bundle not available" + +### 6. platform-test plugin — Real Frontend Bundle + +**`frontend/dist/index.js`** (14.6 KB): +- Регистрирует компоненты через `VerstakPluginRegister` +- Диагностическая панель: + - Plugin name, version, ID + - "✅ Frontend Bundle Loaded" badge + - Test results summary + - Capabilities status section + - API methods info +- Settings panel: + - Plugin name + ID + - Interactive counter (increment/decrement/reset) + - Demo settings list +- Темная тема (совпадает с shell) + +**`frontend/style.css`** (4.9 KB): +- Shared dark-theme styles +- Используется обоими компонентами + +### 7. Тесты + +**Backend (11 новых тестов в `internal/api/app_test.go`):** +| Тест | Проверяет | +|---|---| +| GetPluginFrontendInfo (known) | Полные данные для плагина с frontend | +| GetPluginFrontendInfo (no frontend) | Статус "no-frontend" | +| GetPluginFrontendInfo (unknown) | Статус "not-found" | +| GetPluginAssetContent (existing) | Чтение существующего файла | +| GetPluginAssetContent (style) | Чтение style.css | +| GetPluginAssetContent (absolute path) | Отклонение `/` и `\` | +| GetPluginAssetContent (path traversal) | Отклонение `..` | +| GetPluginAssetContent (path escape) | Отклонение выхода за root | +| GetPluginAssetContent (not found) | Ошибка для неизвестного pluginID | +| GetPluginAssetContent (no frontend) | Ошибка если нет frontend | +| GetPluginAssetContent (missing file) | Ошибка если файл не существует | + +**Smoke test (frontend bundle checks):** +- Manifest объявляет `frontend.entry = "frontend/dist/index.js"` +- Файл бандла существует на диске +- Бандл содержит `"VerstakPluginRegister"` +- Компоненты `DiagnosticsPanel` и `PlatformTestSettings` зарегистрированы + +### 8. Security Constraints + +| Сценарий | Результат | +|---|---| +| frontend entry `../etc/passwd` | Отклоняется (path traversal) | +| frontend entry `/etc/passwd` | Отклоняется (absolute path) | +| Плагин без frontend | Не ломает UI, показывает placeholder | +| Плагин с missing entry | Error fallback с понятным сообщением | +| Bundle execution error | Error fallback, shell не падает | +| Компонент не найден в bundle | Error fallback со списком доступных components | + +## Изменённые файлы + +### verstak-desktop + +| Файл | Изменение | +|---|---| +| `internal/api/app.go` | + `GetPluginFrontendInfo()`, `GetPluginAssetContent()` с path validation | +| `internal/api/app_test.go` | **NEW**: 11 тестов | +| `frontend/src/lib/plugin-host/VerstakPluginAPI.js` | **NEW**: Bundle contract + API stub | +| `frontend/src/lib/plugin-host/PluginBundleHost.svelte` | **NEW**: Загрузка/рендер бандлов, error boundary | +| `frontend/src/lib/shell/ViewContainer.svelte` | Обновлён: PluginBundleHost вместо placeholder | +| `frontend/src/lib/plugin-manager/PluginManager.svelte` | Обновлён: Settings через PluginBundleHost | +| `cmd/smoke-platform/main.go` | Обновлён: frontend bundle checks | +| `docs/PLUGIN_RUNTIME.md` | Обновлён: bundle contract, security | + +### verstak-official-plugins + +| Файл | Изменение | +|---|---| +| `plugins/platform-test/frontend/src/index.js` | **NEW**: DiagnosticsPanel + SettingsPanel | +| `plugins/platform-test/frontend/style.css` | **NEW**: Dark theme styles | +| `plugins/platform-test/frontend/dist/index.js` | **REPLACED**: VerstakPluginRegister contract | + +### verstak-docs +| `docs/MILESTONE_PLATFORM_RUNTIME_5b.md` | **NEW**: этот документ | + +## Проверки + +``` +go test ./internal/... -count=1 → ✅ 56 PASS (all packages) +go vet ./... → ✅ clean +cd frontend && npm run build → ✅ built (72.98 KB gzip:21.64 KB) +bash scripts/smoke-platform.sh → ✅ 4 теста (plugin + enable/disable + workspace + contributions + frontend bundle) +bash scripts/build.sh → ✅ wails build +``` + +## Non-goals (не реализовано) +- Official notes/files/editor extraction +- Backend sidecar runtime +- Secrets +- Полноценный command palette +- Remote plugin registry +- Прямой доступ плагина к Wails backend methods (кроме VerstakPluginAPI)