12 KiB
Milestone 6a Files Core Service Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-developmentorsuperpowers:executing-plansto 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.gointernal/core/files/path_policy.gointernal/core/files/service.gointernal/core/files/*_test.go
Actual plugin-scoped Wails methods:
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
readTextlimited 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.Verstakcase variants.
Delete behavior:
TrashVaultPathmoves files/folders into.verstak/trash.- Trash metadata includes
originalPath,deletedAt,originalType,trashId, andbasename. - 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:
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:
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) boolreturning true for.verstakand.verstak/.... -
Add tests:
TestNormalizeRejectsAbsolutePath,TestNormalizeRejectsTraversal,TestNormalizeRejectsNullByte,TestNormalizePreservesCase,TestReservedPathPolicy. -
Run:
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
Servicewith 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
.verstakpaths 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:
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
TestWriteTextIsAtomicOnFailureusing a controlled failing path or permission-denied directory. -
Add test
TestWriteTextDoesNotLeaveTempFile. -
Run:
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/v1when vault services are initialized. -
Add API guard tests proving each Files bridge method rejects plugins that are missing the required permission.
-
Run:
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.Servicetoapi.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:
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:
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
filesAPI 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:
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
.verstakreserved 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:
cd verstak-desktop
go test ./...
- Run desktop frontend build:
cd verstak-desktop/frontend
npm run build
- Run desktop e2e:
cd verstak-desktop/frontend
npm run test:e2e -- --reporter=list
- Run official plugins checks:
cd verstak-official-plugins
./scripts/check.sh
./scripts/build.sh
- Run SDK checks:
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.