verstak-desktop/docs/MILESTONE_6A_FILES_CORE_PLA...

12 KiB

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:

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:

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) bool returning true for .verstak and .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 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:

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:

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:

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:

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 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:

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:
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.