Implement milestone 6b workbench routing skeleton

This commit is contained in:
2026-06-19 07:51:57 +08:00
parent a100f5a441
commit 6ed6df311a
53 changed files with 7592 additions and 335 deletions
+41 -2
View File
@@ -59,12 +59,51 @@ This validates:
## Desktop Runtime Scanning Paths
The desktop scans two directories for plugins:
The desktop resolves plugin directories in one shared backend resolver. Priority:
| Path | Purpose |
|------|---------|
| `VERSTAK_PLUGIN_DIR` | Dev/test override. Multiple paths can be separated with the OS path separator |
| `./plugins/` | Bundled/dev plugins relative to the current working directory |
| `<binary-dir>/plugins/` | Packaged plugins shipped next to the desktop executable |
| `~/.config/verstak/plugins/` | User-installed plugins |
| `./plugins/` | Bundled/dev plugins (project-local) |
The resolver normalizes paths and removes duplicates before scanning. Missing
directories are ignored by discovery.
Discovery scans all resolved directories in order. If two plugin packages declare
the same `id`, the first package wins and later duplicates are skipped. The
warning includes both package paths, so during development check the log if an
updated plugin appears to be ignored.
## Bundled Plugin API During Development
Frontend bundles are mounted with a plugin-scoped API created by
`createPluginAPI(pluginId)`. The current API supports:
- `settings.read/write/writeAll`
- `capabilities.list/get/has`
- `commands.register/execute` for handlers declared in `contributes.commands`
- `events.publish/subscribe` using the bundled frontend event bus
- `files.list/metadata/readText/writeText/createFolder/move/trash` for
canonical vault-relative slash paths guarded by `files.read`, `files.write`,
and `files.delete`. Backslashes, Windows absolute paths, UNC paths,
traversal, `.verstak` variants, and symlink read/write/move/trash operations
are rejected. Text read/write is UTF-8 only and limited to 2 MB for reads.
- `workbench.openResource/editResource` for routing vault resources to
contributed `openProviders`. Plugins must declare `workbench.open`; this is a
policy/contract check. Files and Notes plugins call this API and do not import
a concrete editor plugin.
Editor/viewer plugins contribute providers with `contributes.openProviders`.
Workbench selects by resource kind, extension/mime, context (`generic-text`,
`generic-markdown`, `notes-markdown`), user preference, priority, then
deterministic `pluginId/providerId` tie-break. If nothing matches, Workbench
shows `no-provider` fallback instead of a core editor.
This is a cooperative contract, not a sandbox. Bundled plugins run in the same JS
context as the desktop frontend; real isolation is deferred to the sidecar/sandbox
milestone.
## Important Rules
+143
View File
@@ -0,0 +1,143 @@
# GUI Testing
## Overview
Verstak Desktop uses **Playwright** for frontend E2E tests that run in a real
Chromium browser with mocked Wails bindings. This tests the Svelte component
logic, user interactions, and UI state transitions — without needing the actual
Wails desktop shell.
## What is tested
### Frontend E2E (Playwright)
Located in `frontend/e2e/`, run via `npm run test:e2e`.
These tests:
- Launch a Vite dev server with mock Wails bindings
- Open the app in a real Chromium browser via Playwright
- Simulate user clicks, wait for UI transitions, assert DOM state
- Collect console errors and page errors on failure
- Capture screenshots on failure
### Test suites
| File | Suite | Tests | Status |
|------|-------|-------|--------|
| `plugin-manager-disable-enable.spec.js` | A: Disable/Enable refresh | 4 | 3 pass, 1 fail* |
| `sidebar-opens-view.spec.js` | B: Sidebar → view routing | 3 | 3 pass |
| `reload-updates-state.spec.js` | C: Reload updates UI | 3 | 2 pass, 1 fail* |
\* Failing tests document **known bugs** (see below).
## Known bugs detected by tests
### Bug M5-1: Sidebar does not update when plugin state changes
**Symptom:** After disabling a plugin in Plugin Manager, the sidebar item for
that plugin remains visible. After re-enabling, it stays visible (doesn't
disappear then reappear — it was never gone).
**Root cause:** `Sidebar.svelte` loads plugin/contribution data once in
`onMount` and stores it in local `sidebarItems`. When `PluginManager`
disables/enables a plugin and calls `ReloadPlugins`, the `PluginManager`
component re-fetches data, but `Sidebar` does not react to the change — it
still holds the stale list.
**Affected tests:**
- `A: Disable plugin: button changes to Enable, sidebar item disappears`
- `A: Disable → Enable full flow in sequence`
- `C: Reload after mock state change reflects new plugin status`
**Fix needed:** Sidebar must either:
1. Re-fetch contributions when it receives a custom event (e.g.
`verstak:plugins-reloaded`), or
2. Read plugin state reactively from a shared store that both
PluginManager and Sidebar subscribe to.
## What is NOT tested
### Real desktop GUI (WebKitGTK + Wails native shell)
The Playwright tests run the frontend in a **standard Chromium browser** with
mocked Wails bindings. They do **not** test:
- Actual WebKitGTK rendering (Wails uses WebKitGTK, not Chromium)
- Native window management (minimize, maximize, resize)
- Native file dialogs (SelectDirectory, SelectVaultForOpen)
- Clipboard integration
- System tray / menu bar
- Plugin frontend bundle loading from real filesystem
- Wails event system (window.runtime.EventsOn/Emit)
For real Wails smoke tests, a separate layer is needed using:
- **AT-SPI2** (Linux accessibility tree inspection)
- **xdotool** / **ydotool** (input simulation)
- **scrot** / **import** (screenshot capture)
## Running tests
```bash
cd frontend
# Run all E2E tests (headless)
npm run test:e2e
# Run with Playwright UI (interactive)
npm run test:e2e:ui
# Run in headed browser (visible)
npm run test:e2e:headed
```
## Test infrastructure
### Mock bridge (`src/lib/test/wails-mock.js`)
Replaces `window['go']['api']['App']` with in-memory mock implementations of
all Wails backend methods. Provides:
- Mutable plugin state (enable/disable/status)
- Mutable vault state
- Mutable contributions (views, commands, sidebar items, settings panels)
- Test helpers via `window.__wailsMock`:
- `reset()` — reset all state to defaults
- `setPluginStatus(id, status, enabled)` — change plugin state
- `getPluginState(id)` — read current state
- `setVaultStatus(status)` — change vault state
### Test harness (`index.html`)
The same `index.html` is used for both production and test. It detects whether
the Wails runtime (`window['go']`) is present. If not (i.e. running in a plain
browser), it loads the mock bridge before the Svelte app.
### Playwright config (`playwright.config.js`)
- Dev server: `vite --mode test --port 5174`
- Browser: Chromium headless
- Timeouts: 30s test, 10s expect
- Workers: 1 (sequential)
- Screenshots: on failure
- Traces: on first retry
- Results: `e2e-results/test-results.json`
## Adding new tests
1. Create `e2e/your-test.spec.js`
2. Import helpers from `./helpers.js`
3. Use `test.beforeEach` to reset mock state and navigate to `/`
4. Use `test.afterEach` to assert no console errors
5. Write scenarios as user actions + assertions
6. Run with `npm run test:e2e`
### Selector conventions
- Plugin cards: `.plugin-card` filtered by text
- Buttons: `.btn-disable`, `.btn-enable`, `.btn-settings`, `.reload-btn`
- Sidebar items: `.sidebar .plugin-item`
- View container: `.view-container`
- View header: `.view-header h2`
- Status badges: `.status-badge`
- Toast: `.toast`
+403
View File
@@ -0,0 +1,403 @@
# Milestone 6a Files Core Service Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> `superpowers:subagent-driven-development` or `superpowers:executing-plans` to
> implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for
> tracking.
**Goal:** Add a safe backend Files core service and plugin bridge for
vault-relative text file operations.
**Architecture:** Files Core is a backend service under `internal/core/files`
that accepts vault-relative paths, enforces reserved path policy, and performs
atomic text writes. The Wails API exposes plugin-scoped methods guarded by plugin
state and `files.*` permissions; SDK types describe the bridge shape.
**Tech Stack:** Go backend services/tests, Wails-bound API methods, TypeScript
SDK type definitions, existing Playwright/Vitest checks.
---
## Implementation Status
Milestone 6a is implemented.
Actual backend package:
- `internal/core/files/types.go`
- `internal/core/files/path_policy.go`
- `internal/core/files/service.go`
- `internal/core/files/*_test.go`
Actual plugin-scoped Wails methods:
```go
func (a *App) ListVaultFiles(pluginID string, relativeDir string) ([]files.FileEntry, string)
func (a *App) GetVaultFileMetadata(pluginID string, relativePath string) (files.FileMetadata, string)
func (a *App) ReadVaultTextFile(pluginID string, relativePath string) (string, string)
func (a *App) WriteVaultTextFile(pluginID string, relativePath string, content string, options files.WriteOptions) string
func (a *App) CreateVaultFolder(pluginID string, relativePath string) string
func (a *App) MoveVaultPath(pluginID string, fromRelativePath string, toRelativePath string, options files.MoveOptions) string
func (a *App) TrashVaultPath(pluginID string, relativePath string) (files.TrashResult, string)
```
Actual bundled frontend API:
- `api.files.list(relativeDir)`
- `api.files.metadata(relativePath)`
- `api.files.readText(relativePath)`
- `api.files.writeText(relativePath, content, options)`
- `api.files.createFolder(relativePath)`
- `api.files.move(fromRelativePath, toRelativePath, options)`
- `api.files.trash(relativePath)`
Implemented limits:
- canonical vault-relative slash paths only;
- backslashes, POSIX absolute paths, Windows drive paths, UNC paths, traversal,
null bytes, and empty file paths are rejected;
- `.verstak/` is reserved case-insensitively and hidden from public Files API;
- metadata may report symlinks, but list-through-symlink and
read/write/move/trash through symlink are forbidden;
- text read/write only, with `readText` limited to UTF-8 files up to 2 MB;
- trash uses `.verstak/trash/files/<trashId>/...` with restore metadata, but
restore itself is deferred;
- binary streaming, watcher, external editor, Files UI, Notes service, sidecar,
sandbox/security isolation deferred.
---
## Scope
Implement:
- Backend Files service.
- Safe vault-relative path handling.
- Reserved `.verstak/` policy.
- List files.
- Read/write text files.
- Create folder.
- Move path.
- Trash path.
- Atomic writes.
- Backend tests.
- SDK bridge shape draft.
Do not implement:
- Full Notes plugin.
- Notes UI.
- Sync.
- Watcher.
- Binary streaming.
- External editor integration.
- Sidecar/security isolation.
## Canonical Policy
All public Files API methods use canonical vault-relative slash paths.
Rejected inputs:
- absolute paths;
- backslashes;
- Windows drive paths and UNC/network paths;
- paths containing `..` after normalization;
- null bytes;
- empty paths where a file path is required;
- access to `.verstak/` through the public plugin Files API, including
`.Verstak` case variants.
Delete behavior:
- `TrashVaultPath` moves files/folders into `.verstak/trash`.
- Trash metadata includes `originalPath`, `deletedAt`, `originalType`,
`trashId`, and `basename`.
- Permanent delete is out of scope.
- Restore is out of scope.
Write behavior:
- Text writes use a temporary file in the target directory and rename into place.
- Existing files are overwritten only when the method explicitly allows overwrite.
- Parent directory must exist unless the method explicitly creates it.
Binary behavior:
- Binary files can appear in list/metadata results.
- Binary read/write streaming is out of scope.
## Public Backend Shape
Implemented plugin-scoped Wails methods:
```go
func (a *App) ListVaultFiles(pluginID string, relativeDir string) ([]files.FileEntry, string)
func (a *App) GetVaultFileMetadata(pluginID string, relativePath string) (files.FileMetadata, string)
func (a *App) ReadVaultTextFile(pluginID string, relativePath string) (string, string)
func (a *App) WriteVaultTextFile(pluginID string, relativePath string, content string, options files.WriteOptions) string
func (a *App) CreateVaultFolder(pluginID string, relativePath string) string
func (a *App) MoveVaultPath(pluginID string, fromRelativePath string, toRelativePath string, options files.MoveOptions) string
func (a *App) TrashVaultPath(pluginID string, relativePath string) (files.TrashResult, string)
```
Permission mapping:
- `ListVaultFiles`, `GetVaultFileMetadata`, `ReadVaultTextFile`: `files.read`.
- `WriteVaultTextFile`, `CreateVaultFolder`, `MoveVaultPath`: `files.write`.
- `TrashVaultPath`: `files.delete`.
## Data Types
Create `internal/core/files/types.go`:
```go
package files
type EntryKind string
const (
KindFile EntryKind = "file"
KindDirectory EntryKind = "directory"
)
type Entry struct {
Name string `json:"name"`
Path string `json:"path"`
Kind EntryKind `json:"kind"`
Size int64 `json:"size"`
ModifiedAt string `json:"modifiedAt"`
IsText bool `json:"isText"`
IsBinary bool `json:"isBinary"`
IsHidden bool `json:"isHidden"`
IsReserved bool `json:"isReserved"`
}
```
## Task 1: Path Policy
**Files:**
- Create: `internal/core/files/path.go`
- Create: `internal/core/files/path_test.go`
- [ ] Add `NormalizeVaultRelativePath(relative string) (string, error)`.
- [ ] Reject absolute paths, null bytes, `..`, and empty file paths.
- [ ] Preserve path case, including canonical `Notes`.
- [ ] Add `IsReservedPath(relative string) bool` returning true for `.verstak`
and `.verstak/...`.
- [ ] Add tests:
`TestNormalizeRejectsAbsolutePath`,
`TestNormalizeRejectsTraversal`,
`TestNormalizeRejectsNullByte`,
`TestNormalizePreservesCase`,
`TestReservedPathPolicy`.
- [ ] Run:
```bash
go test ./internal/core/files
```
Expected: all `internal/core/files` tests pass.
## Task 2: Files Service
**Files:**
- Create: `internal/core/files/service.go`
- Create/modify: `internal/core/files/service_test.go`
- [ ] Define `Service` with a vault dependency that can return the current vault
root and status.
- [ ] Implement `List(relativeDir string) ([]Entry, error)`.
- [ ] Implement `Metadata(relativePath string) (Entry, error)`.
- [ ] Implement `ReadText(relativePath string) (string, error)`.
- [ ] Implement `WriteText(relativePath, content string, overwrite bool) (Entry, error)`.
- [ ] Implement `Mkdir(relativePath string) (Entry, error)`.
- [ ] Implement `Move(fromRelativePath, toRelativePath string, overwrite bool) (Entry, error)`.
- [ ] Implement `Trash(relativePath string) (Entry, error)`.
- [ ] Use the shared path policy for every public method.
- [ ] Block `.verstak` paths in every public method.
- [ ] Add tests for closed vault, list, metadata, text read/write, mkdir, move,
trash, overwrite false conflict, overwrite true replace, and reserved path
rejection.
- [ ] Run:
```bash
go test ./internal/core/files
```
Expected: all `internal/core/files` tests pass.
## Task 3: Atomic Writes
**Files:**
- Modify: `internal/core/files/service.go`
- Modify: `internal/core/files/service_test.go`
- [ ] Write text content to a temp file in the target directory.
- [ ] Rename the temp file into the final path only after successful write.
- [ ] Remove temp file on write failure.
- [ ] Add test `TestWriteTextIsAtomicOnFailure` using a controlled failing path
or permission-denied directory.
- [ ] Add test `TestWriteTextDoesNotLeaveTempFile`.
- [ ] Run:
```bash
go test ./internal/core/files
```
Expected: all `internal/core/files` tests pass.
## Task 4: Permissions And Capabilities
**Files:**
- Modify: `internal/core/permissions/registry.go`
- Modify: `main.go`
- Modify: `internal/api/app_test.go`
- [ ] Register permissions: `files.read`, `files.write`, `files.delete`.
- [ ] Register core capability `verstak/core/files/v1` when vault services are
initialized.
- [ ] Add API guard tests proving each Files bridge method rejects plugins that
are missing the required permission.
- [ ] Run:
```bash
go test ./internal/core/permissions ./internal/api
```
Expected: permission registry and API tests pass.
## Task 5: Wails API Bridge
**Files:**
- Modify: `internal/api/app.go`
- Modify: `internal/api/app_test.go`
- Modify after Wails generation or by hand if generation is unavailable:
`frontend/wailsjs/go/api/App.d.ts`
- Modify after Wails generation or by hand if generation is unavailable:
`frontend/wailsjs/go/api/App.js`
- [ ] Add `files.Service` to `api.App`.
- [ ] Add plugin-scoped methods listed in "Public Backend Shape".
- [ ] Use `requirePluginAccess(pluginID, permission)` for every method.
- [ ] Return readable errors for closed vault, missing file, reserved path,
conflict, and missing permission.
- [ ] Add tests for successful read/write/list/mkdir/move/trash through `App`.
- [ ] Run:
```bash
go test ./internal/api
```
Expected: API tests pass.
## Task 6: Frontend Plugin API Draft
**Files:**
- Modify: `frontend/src/lib/plugin-host/VerstakPluginAPI.js`
- Modify: `frontend/src/lib/test/wails-mock.js`
- Add/modify focused frontend tests under `frontend/e2e/` only if existing test
coverage cannot validate the shape outside Playwright.
- [ ] Add `api.files.list(relativeDir)`.
- [ ] Add `api.files.metadata(relativePath)`.
- [ ] Add `api.files.readText(relativePath)`.
- [ ] Add `api.files.writeText(relativePath, content, options)`.
- [ ] Add `api.files.mkdir(relativePath)`.
- [ ] Add `api.files.move(fromRelativePath, toRelativePath, options)`.
- [ ] Add `api.files.trash(relativePath)`.
- [ ] Keep all calls plugin-scoped; plugin code must not pass `pluginId`.
- [ ] Mock readable errors for reserved path and missing permission.
- [ ] Run:
```bash
cd frontend
npm run build
```
Expected: frontend build passes.
## Task 7: SDK Bridge Shape Draft
**Files:**
- Modify: `../verstak-sdk/src/plugin-api.ts`
- Modify: `../verstak-sdk/src/test-utils.ts`
- Modify: `../verstak-sdk/src/plugin-api.test.ts`
- [ ] Add `files` API TypeScript interfaces matching the frontend API names.
- [ ] Add mock Files API methods in `createMockPluginAPI`.
- [ ] Add contract tests for API shape, text write/read, reserved path error, and
trash result shape.
- [ ] Run:
```bash
cd ../verstak-sdk
./scripts/check.sh
./scripts/build.sh
./scripts/test.sh
```
Expected: SDK check, build, and tests pass.
## Task 8: Documentation
**Files:**
- Modify: `docs/PLUGIN_RUNTIME.md`
- Modify: `docs/NOTES_FILES_PLUGIN_PLAN.md`
- [ ] Document Files Core API as functional for Milestone 6a.
- [ ] Keep Notes API documented as planned until Milestone 6b or later.
- [ ] Document `.verstak` reserved path policy.
- [ ] Document slash-only path policy, Windows/UNC rejection, and symlink policy.
- [ ] Document text-only write support and deferred binary streaming.
## Task 9: Final Verification
- [ ] Run desktop backend tests:
```bash
cd verstak-desktop
go test ./...
```
- [ ] Run desktop frontend build:
```bash
cd verstak-desktop/frontend
npm run build
```
- [ ] Run desktop e2e:
```bash
cd verstak-desktop/frontend
npm run test:e2e -- --reporter=list
```
- [ ] Run official plugins checks:
```bash
cd verstak-official-plugins
./scripts/check.sh
./scripts/build.sh
```
- [ ] Run SDK checks:
```bash
cd verstak-sdk
./scripts/check.sh
./scripts/build.sh
./scripts/test.sh
```
Expected: all commands exit 0. Existing Svelte unused CSS warnings are acceptable
only if they remain warnings and do not fail the build.
+135
View File
@@ -0,0 +1,135 @@
# Milestone 6b - Open/Edit Provider Registry + Workbench Routing Skeleton
This milestone adds the minimal infrastructure layer for open/edit routing before
Files UI, Notes UI, or editor implementation starts. It does not implement those
plugins and does not add a concrete core-owned Markdown editor.
## Existing Architecture Found
| Path | Section/title | Summary |
|---|---|---|
| `../verstak-docs/00_README.md` | Main architecture invariant | Core does not know concrete notes, file manager, or markdown editor features. It owns vault, plugin runtime, capability registry, contribution points, permissions, settings, events, storage, and UI shell. |
| `../verstak-docs/01_Product_Vision.md` | What is not in core / platform goal | Markdown editor, file manager, preview, and notes workflow are plugins. Users should be able to replace the markdown editor or install multiple editors. |
| `../verstak-docs/02_Platform_Architecture.md` | UI Shell / Capability Registry | UI Shell knows contribution points, not concrete note editor or file preview implementations. Files plugin checks editor/viewer capabilities instead of depending on `official.markdown-editor`. |
| `../verstak-docs/03_Repositories.md` | Repository split | `verstak-desktop` is Core Platform + UI Shell and does not contain mandatory notes, file manager, or markdown editor modules. Official plugins live separately. |
| `../verstak-docs/04_Plugin_System.md` | Goal / capabilities instead of plugin names / contribution points | Notes, file manager, editor, and viewer are plugin functions. The docs explicitly reject `requires: ["official.markdown-editor"]` and prefer `optionalRequires: ["editor.text.markdown"]`. |
| `../verstak-docs/05_Official_Plugins.md` | official.files / official.notes / official.markdown-editor | Files optionally depends on editor/viewer capabilities. Markdown editor provides `editor.text`, `editor.text.markdown`, and `editor.note.markdown`, and must not own note storage or depend directly on `official.notes`. |
| `../verstak-docs/06_Migration_Strategy.md` | Do not / Definition of Done | Do not make notes/files/editor mandatory core parts. The platform transition is done only when notes/files/editor/preview/activity work as plugins. |
| `docs/NOTES_FILES_PLUGIN_PLAN.md` | Canonical Notes Model | Notes are ordinary Markdown files in canonical `Notes/` folders. No lowercase `notes`, no `.verstak/notes`, no plugin-data note content, no UUID-only filenames. |
| `docs/NOTES_FILES_PLUGIN_PLAN.md` | Files Service Model | Files Core is raw vault file access and does not understand note semantics. Milestone 6a exposes safe text file methods and defers UI, watcher, binary streaming, external editor, and restore. |
| `docs/PLUGIN_RUNTIME.md` | Contribution Points / Bundled Frontend Plugin API | Current runtime hosts views/sidebar/settings/commands and exposes plugin-scoped Files API. `fileActions`, `noteActions`, `contextMenuEntries`, search/activity/status bar entries are registered but not hosted. |
| `internal/core/contribution/registry.go` | Contribution registry | Registry has the established contribution points and now extends that model with `openProviders`. |
| `internal/core/plugin/plugin.go` | Manifest contributions | Plugin manifest types now include `openProviders` alongside existing contribution points. |
| `frontend/src/lib/shell/WorkbenchHost.svelte` and `frontend/src/lib/plugin-host/PluginBundleHost.svelte` | Frontend plugin host | Workbench mounts the selected provider component by plugin id and component id. It remains generic and does not know a concrete editor. |
| `frontend/src/lib/plugin-host/VerstakPluginAPI.js` | Plugin API | Bundled plugins can call `api.workbench.openResource()` and `api.workbench.editResource()` in addition to settings, capabilities, events, commands, and Files API. |
| `../verstak-sdk/src/types.ts` and `../verstak-sdk/schemas/manifest.json` | SDK contribution contracts | SDK types/schema define `openProviders`, `OpenResourceRequest`, provider supports, and `files.*` permissions. |
## What Matches The Desired Model
- Files/Notes/Editor are already documented as plugins, not core modules.
- Official plugins are expected to use the same runtime as community plugins.
- Files plugin is already documented as capability-driven, not hard-wired to a markdown editor.
- Markdown editor is already documented as replaceable via capabilities.
- Notes are already documented as Markdown files under canonical `Notes/` folders, without `.verstak/notes`, UUID note entities, or a second storage truth.
- Desktop code has no hardcoded Markdown editor component.
- Existing plugin host can mount arbitrary plugin components, which is enough foundation for an editor provider host.
## Contradictions Found
- `docs/NOTES_FILES_PLUGIN_PLAN.md` still said Files Core API/capability/permissions were unavailable, while later sections and code show Milestone 6a implemented them.
- `../verstak-docs/05_Official_Plugins.md` calls notes "first-class Verstak entities". That is acceptable only as UI semantics; implementation must not create a separate note storage entity.
- `../verstak-docs/05_Official_Plugins.md` lists "Open externally" as a Files fallback. External open remains deferred and must not enter Milestone 6b.
- `../verstak-sdk/schemas/manifest.json` previously did not include `files.read`, `files.write`, or `files.delete` in the permissions enum, while SDK TS types, desktop permissions, and official `platform-test` manifest already used them. Milestone 6b resolves this 6a contract cleanup item.
## Missing Before 6b
- `openProviders` contribution point.
- `OpenResourceRequest` contract.
- Workbench open/edit routing API for Files/Notes plugins.
- Provider selection model using resource kind, extension/mime, notes context, user preference, provider priority, deterministic fallback, and disabled provider fallback.
- User preferences for default text editor provider, default markdown editor provider, and default notes-context markdown editor provider.
- Host slot/tab that mounts the selected provider component with an open resource request.
- Tests for provider registration, selection, preferences, disabled provider fallback, and notes-context routing.
## Added In 6b
- `contributes.openProviders` in SDK schema/types and desktop manifest structs.
- Desktop contribution registry support for registering, replacing, listing, and
unregistering open providers.
- `OpenResourceRequest`, `OpenResourceContext`, `OpenResourceResult`, and
opened-resource state in the Workbench routing skeleton.
- Provider selection by resource kind, extension/mime, `generic-text`,
`generic-markdown`, or `notes-markdown` context, user preference, priority,
deterministic `pluginId/providerId` fallback, and active plugin filtering.
- `workbench.open` policy permission for plugins that request Workbench routing.
- Draft app settings preferences for default text, markdown, and notes-context
markdown providers.
- `api.workbench.openResource()` and `api.workbench.editResource()` exposed to
frontend plugin bundles.
- Minimal Workbench host that mounts the selected provider component from the
selected provider plugin.
- `no-provider` fallback state when no matching provider exists.
- `platform-test` diagnostic open provider used only to prove routing.
## Decision
Open/edit provider should be an extension of the existing contribution registry,
not a parallel system. It should add `openProviders` beside `views`, `commands`,
`fileActions`, and `noteActions`.
Capabilities remain useful for broad availability and degraded mode, but they are
too coarse for choosing between multiple providers. Provider selection needs a
declarative provider contribution with `supports`, priority, and component id.
## Correct Notes Model
- Notes are a contextual view over ordinary Markdown files under canonical
`Notes/` folders.
- `.md` inside `Notes/` opens through the selected markdown editor with
notes-context.
- `.md` outside `Notes/` opens through the selected markdown editor in generic
markdown mode.
- Plain text opens through the selected text editor provider in `generic-text`
context.
- Files and Notes call open/edit resource; neither embeds a concrete editor.
- Editor provider selection belongs to Workbench/provider registry.
- No `.verstak/notes`, no UUID note entities, no second truth separate from the
`.md` file.
## Minimal Infrastructure Changes
1. Add `openProviders` to SDK manifest/schema/types.
2. Add `openProviders` to desktop plugin manifest structs and contribution
registry.
3. Add `OpenResourceRequest` and provider support match types.
4. Add Workbench/provider selection service with deterministic rules.
5. Add user preferences for `defaultTextEditorProvider`,
`defaultMarkdownEditorProvider`, and `defaultNotesMarkdownEditorProvider`.
6. Add frontend host plumbing so Workbench can mount the selected plugin
component.
7. Fix `verstak-sdk/schemas/manifest.json` permissions enum for `files.*`.
## Proposed Milestone 6b Scope
In scope:
- Contribution/types/schema support for `openProviders`.
- Workbench `openResource`/`editResource` routing API.
- Provider selection with notes context and user preferences.
- Minimal host tab/slot for provider component mounting.
- Diagnostic provider plugin contribution sufficient to prove routing.
- Tests for routing, provider selection, disabled fallback, and notes-context
markdown.
Out of scope:
- Full Files UI feature set.
- Full Notes UI feature set.
- Full editor implementation.
- Real default editor plugin (Milestone 6c).
- Files plugin open/edit integration.
- Notes plugin open/edit integration.
- Hardcoded core Markdown editor.
- Watcher/sync/binary streaming/external editor.
- Sidecar/security boundary.
- Large rewrite.
+425
View File
@@ -0,0 +1,425 @@
# Notes/Files Plugin Architecture Plan
This document locks the Notes/Files/Open architecture for the next milestones.
Files Core Service was implemented in Milestone 6a; this document still does not
start Notes UI, Notes plugin, Files UI plugin, editor plugin, watcher, sync, or
binary streaming.
## Current Readiness
The platform is ready for bundled plugin UI experiments. Files Core is available
as a safe vault-scoped text file API. Notes, Files UI, and editor/viewer UI still
need plugin-level implementations and host surfaces before real product use.
Already available:
- Plugin discovery, lifecycle, settings, capabilities, bundled commands, and
bundled frontend events.
- Workspace tree APIs for `space`, `case`, and `folder`.
- Plugin-owned internal storage directories:
`.verstak/plugin-data/<pluginId>`, `.verstak/plugin-settings/<pluginId>`, and
`.verstak/plugin-cache/<pluginId>`.
- Contribution registry entries for `fileActions`, `noteActions`,
`contextMenuEntries`, `searchProviders`, `activityProviders`, and
`statusBarItems`.
Not available yet:
- Notes plugin/API as a semantic view over Markdown files.
- Files UI plugin.
- Editor/viewer plugin.
- Open/edit resource routing and provider selection.
- UI hosts for file actions, note actions, context menus, search providers,
activity providers, or status bar items.
- Watcher/indexer for external filesystem changes.
- Real plugin isolation. Current permission checks are contract/policy checks,
not a security boundary for bundled frontend JavaScript.
## Canonical Notes Model
Notes are ordinary human-readable Markdown files inside the vault. They must be
visible and editable outside Verstak.
Canonical rules:
- Notes are `.md` files, not opaque records.
- Canonical folder name is exactly `Notes`.
- Do not create lowercase `notes`.
- Do not store user notes in `.verstak/notes/`.
- Do not store user notes in `.verstak/plugin-data/verstak.notes/`.
- Do not use UUID-only filenames for notes.
- The note title is the source of truth.
- The filename is a normalized projection from the title.
- `RenameNote` must update both the title and the `.md` filename.
Canonical scoped paths:
- Workspace/root overview notes live under `Notes/`.
- Case/project/folder scoped notes live under `<case-or-parent>/Notes/`.
- The default overview note is `<case-or-parent>/Notes/Overview.md`.
Visibility requirements:
- Notes UI must show notes as semantic notes.
- Files UI must show the same `.md` files as ordinary files.
- External file managers must show the same `.md` files.
- Outside Verstak, the files must remain useful as normal Markdown.
The workspace tree can remain `space`/`case`/`folder`. Adding `note` as a
workspace node type is not part of the next milestone because it would require a
schema migration. The Notes service can index and manage Markdown files inside
canonical `Notes/` folders without changing workspace node types.
## Title To Filename Contract
The title is the source of truth. The filename is derived from the title when a
note is created or renamed.
Normalization rules:
- Replace spaces with `_`.
- Replace typographic dashes with `-`.
- Allow only letters, digits, `.`, `_`, and `-`.
- Append `.md` if the normalized name does not already end with `.md`.
- Reject empty normalized names.
- Preserve canonical `Notes` folder casing.
Examples:
| Title | Filename |
|---|---|
| `Overview` | `Overview.md` |
| `Meeting Notes` | `Meeting_Notes.md` |
| `Plan — Phase 1` | `Plan_-_Phase_1.md` |
| `A/B Test: Result` | `AB_Test_Result.md` |
## Collision Policy
Same-folder collisions must not be solved silently with `_2`, `_3`, or timestamp
suffixes.
`CreateNote` and `RenameNote` must return a conflict error if the normalized
target filename already exists in the target `Notes/` folder. The UI should show a
clear dialog or notification and ask the user to change the title.
Required conflict metadata:
- requested title;
- normalized filename;
- target vault-relative path;
- existing vault-relative path.
## Files Service Model
Files service is the raw vault file layer. It works with vault-relative paths and
does not understand note semantics.
Rules:
- All public Files API paths are canonical vault-relative slash paths.
- Backslashes are rejected instead of normalized.
- Absolute POSIX paths, Windows drive paths, and UNC/network paths are rejected.
- `..` traversal is rejected.
- Null bytes are rejected.
- `.verstak/` is reserved case-insensitively and hidden/forbidden by default.
- Access to `.verstak/` is allowed only through internal APIs, not through the
normal plugin Files API.
- Symlink read/write/move/trash operations are forbidden in Milestone 6a.
Metadata may report a final symlink as `type: "symlink"`.
- Writes must be atomic: write a temp file in the same directory, close it, then
rename.
- Delete must follow the trash policy until permanent delete is explicitly
designed.
- Trash metadata records `originalPath`, `deletedAt`, `originalType`, `trashId`,
and `basename` for future restore work. Restore is deferred.
- Binary files are deferred for write/streaming APIs. Milestone 6a lists binary
metadata but read/write is UTF-8 text only with a 2 MB read limit.
Minimum Files methods:
- `ListVaultFiles(relativeDir)`.
- `GetVaultFileMetadata(relativePath)`.
- `ReadVaultTextFile(relativePath)`.
- `WriteVaultTextFile(relativePath, content, options)`.
- `CreateVaultFolder(relativePath)`.
- `MoveVaultPath(fromRelativePath, toRelativePath)`.
- `TrashVaultPath(relativePath)`.
Milestone 6a status: implemented in `internal/core/files` and exposed to bundled
plugins as `api.files.list`, `api.files.metadata`, `api.files.readText`,
`api.files.writeText`, `api.files.createFolder`, `api.files.move`, and
`api.files.trash`. It is still text-only for reads/writes and has no watcher,
binary streaming, external editor integration, or Files UI plugin.
Later Files methods:
- `WatchVaultFiles(scope)` once watcher/event delivery is ready.
- `ReadVaultFileBytes` / `WriteVaultFileBytes` for binary files.
- `OpenExternal(relativePath)` with explicit permission and UX confirmation.
- `RevealInFileManager(relativePath)`.
## Notes Service Model
Notes API is a semantic layer over Markdown files managed by the Files/path
policy.
Rules:
- A note physically is a `.md` file.
- Notes API and Files API must not create two sources of truth.
- Notes API reads/writes the same files that Files API lists.
- The note title is the semantic source of truth and is projected to the filename.
If frontmatter or a first-heading convention is introduced later, `RenameNote`
must keep that visible title metadata and the filename synchronized.
- Other note metadata should be derived from the file path and filesystem
metadata, or from Markdown frontmatter if a future milestone introduces it.
- If a note is changed through Files API or an external editor, the future
watcher/indexer must observe it.
- Until watcher/indexer exists, external changes require reload/rescan.
Minimum Notes methods:
- `ListNotes(scope)` where `scope` resolves to a canonical `Notes/` folder.
- `GetNote(notePath)`.
- `CreateNote(scope, title, initialBody)`.
- `RenameNote(notePath, newTitle)`.
- `UpdateNoteBody(notePath, body)`.
- `TrashNote(notePath)`.
Later Notes methods:
- `SearchNotes(query, filters)`.
- `ListBacklinks(notePath)`.
- `ResolveNoteLinks(notePath)`.
- `ExportNote(notePath, format)`.
## Notes Vs Files Relationship
Files owns safe raw vault file access. Notes owns note semantics.
The same physical note must be visible through both APIs:
- Files sees `SomeCase/Notes/Overview.md` as a file.
- Notes sees `SomeCase/Notes/Overview.md` as a note with title `Overview`.
There must be no duplicate note content stored in plugin settings, plugin data,
or a separate `.verstak` note database. Indexes and caches may exist later, but
they must be rebuildable from the canonical Markdown files.
## Capabilities And Permissions
Existing permissions that remain useful:
- `vault.read` for existing vault-level read policy.
- `vault.write` for existing vault-level write policy.
- `vault.watch` for future watcher support.
- `ui.register` for sidebar/views/settings contributions.
- `commands.register` for bundled command handlers.
- `events.publish` and `events.subscribe` for frontend/backend event flows.
New permissions required before real Notes/Files plugins ship:
- `files.read`
- `files.write`
- `files.delete`
- `workbench.open`
Future Notes semantic permissions are deferred until a real Notes plugin/API
ships:
- `notes.read`
- `notes.write`
- `notes.delete`
Those permissions are still policy checks until sidecar/sandbox work provides a
real isolation boundary.
Recommended capabilities:
- `verstak/core/files/v1`
- `verstak/core/notes/v1`
- `verstak/files/v1` provided by the official Files plugin.
- `verstak/notes/v1` provided by the official Notes plugin.
## Frontend Components And Extension Points
Contribution points already registered but not fully hosted:
- `fileActions`
- `noteActions`
- `contextMenuEntries`
- `searchProviders`
- `activityProviders`
- `statusBarItems`
UI work needed:
- Files view host with tree/list modes and selection state.
- Notes view host with note list, open/edit entry points, and preview/details
region.
- Context menu host that merges core actions with plugin contributions.
- Command palette host for contributed commands.
- Search provider host with cancellation/debounce and result ownership.
- Status bar host for lightweight plugin state.
- Selection/event model for active file, active note, and active workspace node.
The first implementation should host only the contribution points needed by the
official Notes and Files plugins.
## Open/Edit Resource Model
Files and Notes must not embed a concrete editor or viewer. They request that the
Workbench open or edit a resource. The Workbench/provider registry selects the
plugin component.
Required model:
- Files plugin lists files and calls open/edit for a vault file.
- Notes plugin presents Markdown files under canonical `Notes/` folders and calls
open/edit for the same vault file with notes context.
- `.md` or `.markdown` inside a canonical `Notes/` folder opens in markdown mode
with notes context.
- `.md` or `.markdown` outside `Notes/` opens in generic markdown mode.
- Plain text opens in `generic-text` mode.
- The same editor provider may support text, generic markdown, and notes-context
markdown.
- User preferences can select another provider for text, markdown, and
notes-context markdown.
- Community editor plugins can replace the default editor through the same
provider registry.
- Core desktop owns registry, routing, Workbench host slot/tab, and preferences.
- Core desktop does not own concrete Files UI, Notes UI, Markdown editor, or file
preview UI.
Minimal contribution extension:
```json
{
"contributes": {
"openProviders": [
{
"id": "verstak.platform-test.markdown-diagnostic",
"title": "Platform Test Markdown Diagnostic",
"priority": 100,
"component": "MarkdownDiagnosticProvider",
"supports": [
{
"kind": "vault-file",
"extensions": [".md", ".markdown"],
"contexts": ["generic-markdown", "notes-markdown"]
},
{
"kind": "vault-file",
"mime": ["text/plain"],
"extensions": [".txt", ".log", ".json", ".yaml", ".yml", ".toml", ".ini", ".conf"],
"contexts": ["generic-text"]
}
]
}
]
}
}
```
Open request shape:
```ts
type OpenResourceRequest = {
kind: "vault-file";
path: string;
mode?: "view" | "edit";
mime?: string;
extension?: string;
context?: {
sourcePluginId?: string;
sourceView?: "files" | "notes" | string;
isInsideNotesFolder?: boolean;
notesScopePath?: string;
notesMode?: boolean;
};
};
```
Provider selection rules:
1. Match resource kind.
2. Match extension and/or mime.
3. Prefer providers that explicitly support the request context.
4. Apply user preference for text, markdown, or notes-context markdown when the
preferred provider is enabled and still supports the resource.
5. Otherwise choose highest priority.
6. Break ties deterministically by plugin id, then provider id.
7. If no provider matches, return a Workbench `no-provider` state rather than
hardcoding a core editor.
8. If a preferred provider plugin is disabled or unavailable, fall back to the
deterministic default and surface a non-blocking preference warning later.
Initial preferences:
- `defaultTextEditorProvider`;
- `defaultMarkdownEditorProvider`;
- `defaultNotesMarkdownEditorProvider`.
Per-extension overrides are deferred.
## Migration Risks
- Adding `note` as a workspace node type is a workspace schema migration and is
explicitly out of scope for the next milestone.
- The canonical path rules must be locked before writing real files into user
vaults.
- Rename behavior can break external links if link rewriting is not designed.
- Case/folder path ownership must be clear before scoped `Notes/` folders are
created.
- Raw Files API can expose `.verstak` internals unless reserved paths are blocked.
- File writes need atomic behavior and conflict handling before sync.
- Large/binary files require streaming or byte APIs; text APIs are not enough.
- External editor changes require reload/rescan until watcher/indexer exists.
- Bundled frontend plugins are trusted/cooperative and not isolated from the
shared JS context.
## Test Plan
Backend Go tests:
- Vault-relative path normalization and traversal rejection.
- Reserved `.verstak` path behavior.
- Files list/read/write/mkdir/move/trash with vault closed/open states.
- Atomic text writes and temp-file cleanup on failure.
- Notes `Notes/` folder casing and no lowercase `notes`.
- Title to filename normalization.
- `CreateNote` and `RenameNote` conflict errors without silent suffixes.
- Notes and Files read the same physical `.md` file.
- Permission checks for `files.*`, `notes.*`, `vault.read`, and `vault.write`.
Frontend/unit tests:
- SDK and plugin API shape for Files and Notes draft methods.
- Readable errors for missing permissions, closed vault, missing file, missing
note, reserved path, and collision.
- Contribution host rendering for note/file actions and context menus.
Playwright e2e tests:
- Create a text file, reload, and verify it is visible in Files.
- Create a note, reload, and verify it is visible in both Notes and Files.
- Rename a note and verify title plus filename change together.
- Attempt same-folder collision and verify user-facing conflict handling.
- External file change requires reload/rescan until watcher exists.
## Implementation Order
1. Define canonical vault-relative path rules and reserved path policy.
2. Implement Files core service with safe list/read/write/mkdir/move/trash.
3. Define open/edit resource request, provider contribution shape, and provider
selection rules.
4. Extend contribution registry/types with `openProviders`.
5. Add Workbench open/edit routing and a provider-hosted tab/slot.
6. Add preferences for text, markdown, and notes-context markdown provider ids.
7. Use `platform-test` diagnostic provider to verify routing; real default
editor plugin is deferred to Milestone 6c.
8. Build official Files plugin that calls open/edit resource.
9. Build official Notes plugin as a contextual view over Markdown files in
canonical `Notes/` folders.
10. Implement future Notes semantic helpers only as a facade over Markdown files,
never as a second source of truth.
+227 -12
View File
@@ -6,19 +6,34 @@
### Discovery Directories
Plugins ищутся в двух директориях (порядок приоритета):
Plugins ищутся через единый resolver `internal/core/plugin.ResolveDiscoveryDirs`.
Порядок приоритета:
| Путь | Назначение | Коммитится |
|---|---|---|
| `VERSTAK_PLUGIN_DIR` | Override для тестов/dev; можно передать несколько путей через OS path separator | Нет |
| `./plugins/` | Dev plugins относительно текущей рабочей директории/repo | Нет (`.gitignore`) |
| `<binary-dir>/plugins/` | Packaged plugins рядом с desktop binary | Зависит от дистрибутива |
| `~/.config/verstak/plugins/` | User-installed plugins | Нет (user home) |
| `./plugins/` | Bundled / dev plugins | Нет (`.gitignore`) |
Resolver нормализует пути, удаляет дубликаты и передает discovery только канонический
список директорий. Отсутствующие директории просто пропускаются на этапе scanning.
Discovery сканирует **все** resolved директории в указанном порядке. Если один и тот
же `plugin.id` найден несколько раз, применяется правило **first plugin wins**:
первый найденный plugin загружается, последующие plugins с тем же id пропускаются.
Конфликт логируется и возвращается как discovery warning с двумя путями: путь
пропущенного duplicate и путь уже загруженного winner.
### ./plugins/ как Dev/Install Target
Директория `./plugins/` в корне `verstak-desktop` используется как:
Директория `./plugins/` от текущей рабочей директории используется как:
- **Dev target**`install-dev-plugins.sh` коприрует сюда собранные пакеты из `verstak-official-plugins/dist/`.
- **Bundled plugins** — при дистрибутиве core может поставлять плагины здесь.
- **Local override** — при запуске desktop из repo позволяет быстро проверять packaged bundles.
В packaged-сборке bundled plugins должны лежать в `plugins/` рядом с executable.
Для тестов и локальных сценариев можно задать `VERSTAK_PLUGIN_DIR=/path/to/plugins`.
Директория **не коммитится**. Каждый разработчик устанавливает плагины через `install-dev-plugins.sh`.
@@ -83,6 +98,8 @@ coreCaps := []string{
"verstak/core/contribution-registry/v1",
"verstak/core/permissions/v1",
"verstak/core/events/v1",
"verstak/core/files/v1",
"verstak/core/workbench/v1",
}
capRegistry.Register("verstak-desktop", coreCaps)
@@ -150,7 +167,7 @@ foreach plugin:
"provides": ["verstak/platform-test/v1"],
"requires": ["verstak/core/plugin-manager/v1"],
"optionalRequires": ["verstak/core/vault/v1", "verstak/core/sync/v1"],
"permissions": ["vault.read", "events.publish", "ui.register"],
"permissions": ["vault.read", "events.publish", "ui.register", "workbench.open"],
"frontend": { "entry": "frontend/dist/index.js" },
"contributes": {
"views": [{ "id": "my.view", "title": "My View", "component": "MyPanel" }],
@@ -171,6 +188,7 @@ foreach plugin:
| Основные панели | `views` | Полноценные страницы/панели | ✅ ViewContainer.svelte (PluginBundleHost — real frontend bundle) |
| Панели настроек | `settingsPanels` | Панели в Plugin Manager | ✅ PluginManager.svelte (кнопка Settings, открывает modal) |
| Команды | `commands` | Команды для command palette | ✅ ContributionRegistry (UI command palette не реализован) |
| Open/edit providers | `openProviders` | Провайдеры viewer/editor для Workbench routing | ✅ ContributionRegistry + минимальный Workbench host |
### Планируемые contribution points
@@ -233,6 +251,176 @@ foreach plugin:
5. Enable plugin → `Register` при следующем Reload
6. Registry idempotent: Register удаляет старые записи перед добавлением новых
## Bundled Frontend Plugin API
Bundled frontend plugins получают API от host через `createPluginAPI(pluginId)`.
Обычный plugin code не передает `pluginId` в методы API: scope закрепляется в
host при mount компонента. Это защищает нормальный cooperative path от случайного
доступа к чужому namespace.
Текущая модель безопасности честно ограничена:
- bundled frontend plugins исполняются в общем JS-контексте приложения;
- проверки permissions/capabilities сейчас являются contract/policy checks, а не
полноценной security boundary;
- malicious JS в общем контексте теоретически может обойти frontend wrapper;
- настоящая изоляция будет только после отдельного sidecar/sandbox milestone.
## Workbench Open/Edit Routing
Files and Notes plugins do not import or embed a concrete editor plugin. They
call `api.workbench.openResource(request)` or `api.workbench.editResource(request)`.
The backend requires the source plugin to be enabled, loaded/degraded, and to
declare `workbench.open`. This is a policy/contract check, not a security
boundary.
`OpenResourceRequest`:
```ts
type OpenResourceRequest = {
kind: "vault-file";
path: string;
mode?: "view" | "edit";
mime?: string;
extension?: string;
context?: {
sourcePluginId?: string;
sourceView?: "files" | "notes" | string;
isInsideNotesFolder?: boolean;
notesScopePath?: string;
notesMode?: boolean;
};
};
```
Routing contexts are fixed as `generic-text`, `generic-markdown`, and
`notes-markdown`. `.md`/`.markdown` inside canonical `Notes/` folders uses
`notes-markdown`; markdown outside Notes uses `generic-markdown`; ordinary text
uses `generic-text`. Milestone 6b derives context from request fields; future
Files/Notes integrations can centralize canonical Notes folder auto-detection in
the Workbench helper.
`contributes.openProviders` extends the existing contribution registry:
```json
{
"contributes": {
"openProviders": [
{
"id": "verstak.platform-test.markdown-diagnostic",
"title": "Platform Test Markdown Diagnostic",
"priority": 100,
"component": "MarkdownDiagnosticProvider",
"supports": [
{
"kind": "vault-file",
"extensions": [".md", ".markdown"],
"contexts": ["generic-markdown", "notes-markdown"]
},
{
"kind": "vault-file",
"mime": ["text/plain"],
"extensions": [".txt", ".log"],
"contexts": ["generic-text"]
}
]
}
]
}
}
```
Selection uses enabled loaded/degraded provider plugins, resource kind,
extension/mime, context, user preference, priority, then deterministic
`pluginId/providerId` fallback. If nothing matches, Workbench returns
`status: "no-provider"` and shows the fallback view instead of a core editor.
Draft app-global preferences are `defaultTextEditorProvider`,
`defaultMarkdownEditorProvider`, and `defaultNotesMarkdownEditorProvider`.
Vault-scoped and per-extension overrides are deferred.
### API methods
`settings`
- `settings.read()` — читает весь settings namespace текущего plugin.
- `settings.read(key)` — читает один ключ.
- `settings.write(key, value)` — обновляет один ключ и пишет namespace обратно.
- `settings.writeAll(settings)` — заменяет settings namespace.
- Backend требует plugin exists, enabled, status `loaded`/`degraded` и permission
`storage.namespace`.
`capabilities`
- `capabilities.list()` — возвращает текущий capability registry.
- `capabilities.get(name)` — возвращает `{ available, name, pluginId, status }`.
- `capabilities.has(name)` — boolean wrapper над `get`.
- Backend требует, чтобы plugin был enabled/loaded и декларировал dependency на
`verstak/core/capability-registry/v1` в `requires` или `optionalRequires`.
`commands`
- `commands.register(commandId, handler)` — регистрирует bundled frontend handler.
Возвращает `Promise<unsubscribe>`.
- `commands.execute(commandId, args)` — backend сначала проверяет plugin status,
permission `commands.register` и что command объявлен в `contributes.commands`
именно этим plugin. Затем frontend registry вызывает зарегистрированный handler.
- Если command объявлен в manifest, но handler не зарегистрирован, API возвращает
понятную ошибку `declared-but-unhandled`.
- Handler registry очищается при component unmount, reload/disable flow и
`api.dispose()`.
`events`
- `events.subscribe(eventName, handler)` — frontend-local subscription с backend
validation permission `events.subscribe`. Возвращает `Promise<unsubscribe>`.
- `events.publish(eventName, payload)` — backend проверяет `events.publish`, затем
событие dispatch'ится в bundled frontend event bus.
- Handler получает envelope `{ name, pluginId, payload, timestamp }`.
- Subscriptions очищаются при component unmount, reload/disable flow и
`api.dispose()`.
`files`
- `files.list(relativeDir)` — list directory using a vault-relative path.
- `files.metadata(relativePath)` — returns file/folder/symlink metadata.
- `files.readText(relativePath)` — reads a UTF-8 regular file, with a size limit.
- `files.writeText(relativePath, content, options)` — atomically writes text via
temp-file-and-rename. `options.createIfMissing` and `options.overwrite`
control conflicts.
- `files.createFolder(relativePath)` — creates one folder when the parent exists.
- `files.move(from, to, options)` — moves a file or folder; rejects moving a
folder into itself and conflicts unless `options.overwrite` is true.
- `files.trash(relativePath)` — moves a file/folder into internal
`.verstak/trash/files/<trashId>/...` and returns trash metadata.
- Backend requires plugin exists, enabled, status `loaded`/`degraded`, open
vault, and `files.read`, `files.write`, or `files.delete`.
- All paths are canonical vault-relative slash paths. Backslashes, POSIX
absolute paths, Windows drive paths, UNC/network paths, `..`, null bytes,
symlink traversal, and public access to `.verstak/` are rejected.
- `.verstak` is reserved case-insensitively: `.verstak`, `.Verstak`, and any
first path segment with that spelling are internal-only.
- `files.metadata` may report a final symlink as `type: "symlink"`, but
`files.list` through a symlink directory and all read/write/move/trash
operations through symlinks are forbidden in Milestone 6a.
- Files API is text-only for read/write in Milestone 6a. `readText` is limited
to UTF-8 regular files up to 2 MB. Binary streaming, watcher, restore,
external editor integration, and Files UI plugin are deferred.
`dispose`
- `dispose()` вызывается host'ом при cleanup. Plugin code обычно не вызывает его
напрямую. Он удаляет зарегистрированные command handlers и event subscriptions.
### Runtime boundaries
| Layer | Current status |
|---|---|
| Bundled frontend runtime | Functional for settings, capabilities, commands, events and text Files API |
| Backend validation | Checks plugin exists, enabled/loaded state, permissions and declarations |
| Security boundary | Not implemented; bundled plugins share the desktop frontend JS context |
| Sidecar/RPC/sandbox | Not implemented |
### Error boundary
- Ошибка в plugin view/settings placeholder не роняет shell
@@ -265,17 +453,44 @@ window.VerstakPluginRegister('plugin.id', {
### VerstakPluginAPI
API объект передаётся в `mount()` и содержит только ограниченный набор методов:
API объект передаётся в `mount()` и содержит plugin-scoped методы текущего
bundled runtime. Это реальный runtime contract для cooperative bundled plugins,
но не sandbox/security boundary.
| Свойство | Статус | Описание |
|---|---|---|
| `api.pluginId` | ✅ Работает | ID плагина |
| `api.capabilities.has(id)` | 🔧 Stub | Запрос capability registry (planned) |
| `api.events.publish(type, payload)` | 🔧 Stub | Публикация события (planned) |
| `api.events.subscribe(type, handler)` | 🔧 Stub | Подписка на события (planned) |
| `api.settings.read(key)` | 🔧 Stub | Чтение настроек плагина (planned) |
| `api.settings.write(key, value)` | 🔧 Stub | Запись настроек плагина (planned) |
| `api.commands.execute(id, args)` | 🔧 Stub | Выполнение команды (planned) |
| `api.settings.read(key?)` | ✅ Работает | Читает plugin-scoped settings через backend bridge |
| `api.settings.write(key, value)` | ✅ Работает | Пишет один settings key через backend bridge |
| `api.settings.writeAll(settings)` | ✅ Работает | Заменяет settings namespace плагина |
| `api.capabilities.list()` | ✅ Работает | Возвращает capability registry |
| `api.capabilities.get(id)` | ✅ Работает | Возвращает capability entry/status |
| `api.capabilities.has(id)` | ✅ Работает | Boolean wrapper над `get` |
| `api.commands.register(id, handler)` | ✅ Работает | Регистрирует bundled frontend handler для объявленной command |
| `api.commands.execute(id, args)` | ✅ Работает | Валидирует declaration/permission/backend state и вызывает bundled handler |
| `api.events.publish(type, payload)` | ✅ Работает | Валидирует permission и публикует во frontend event bus |
| `api.events.subscribe(type, handler)` | ✅ Работает | Валидирует permission и подписывает handler на frontend event bus |
| `api.files.list(relativeDir)` | ✅ Работает | Список vault-relative директории, `.verstak` скрыта |
| `api.files.metadata(relativePath)` | ✅ Работает | Metadata для файла/папки/symlink без чтения содержимого |
| `api.files.readText(relativePath)` | ✅ Работает | Читает UTF-8 regular file до 2 MB |
| `api.files.writeText(relativePath, content, options)` | ✅ Работает | Atomic text write с явным create/overwrite policy |
| `api.files.createFolder(relativePath)` | ✅ Работает | Создаёт vault-relative folder |
| `api.files.move(from, to, options)` | ✅ Работает | Move file/folder с conflict и path-policy checks |
| `api.files.trash(relativePath)` | ✅ Работает | Перемещает в internal trash, permanent delete нет |
| `api.workbench.openResource(request)` | ✅ Работает | Routes vault resources to `openProviders` |
| `api.workbench.editResource(request)` | ✅ Работает | Same routing, forcing `mode: "edit"` |
| `api.dispose()` | ✅ Работает | Очищает command handlers и event subscriptions текущего API instance |
Ограничения:
- permissions/capabilities checks являются contract/policy checks;
- bundled frontend plugins исполняются в общем JS-контексте;
- malicious JS не изолирован;
- sidecar process lifecycle, RPC transport и sandbox enforcement ещё не
реализованы.
- Files paths are slash-only vault-relative contract paths; backslashes,
Windows absolute paths, UNC paths, `.verstak` variants, traversal and symlink
operations are rejected by backend policy checks.
### Загрузка бандла