refactor(plugin-api): trim plugin API scope to grounded capabilities

Builds on marcus's plugin-api-completeness work. Narrows the new plugin
surface to capabilities that expose real, otherwise-unreachable pi-web
functionality, and drops invented/duplicative surfaces:

Kept:
- files.writeFile / deleteFile / moveFile (genuine workspace mutation,
  federated, path-safe)
- prompt.insertText / getText / getSelection (editor state access)

Dropped:
- attachments.* (insertFileReference/getAttachedFiles/removeFileReference):
  getAttachedFiles invented a structured-attachment notion pi-web does not
  have and duplicated prompt.getText() + a regex with a false email-safety
  claim; insert/removeFileReference were thin sugar over readFile +
  insertText that plugins can compose themselves.
- prompt.onPaste / onKeyDown: an incomplete two-event hook system shaped
  around a single use case, overlapping the editor's native image-paste
  handling. Deferred until a real editor event/hook surface is designed.
- prompt.focus: redundant and buggier duplicate of the existing
  focusPrompt() (silently no-ops when not on the chat view). Focus stays
  as focusPrompt().

Security fix:
- deleteWorkspaceFile now resolves the parent via realpath + ensureInside
  before lstat/unlink, closing a symlinked-parent-directory escape that
  allowed deleting files outside the workspace (write/move already did
  this). Final path component is still not resolved, so deleting a symlink
  removes the link, not its target. Adds a regression test.

Docs and the registry test mock updated to match the trimmed surface.
This commit is contained in:
Federico Jaramillo Martinez
2026-06-14 23:16:37 +02:00
parent 7c915d7861
commit 3742bcc962
9 changed files with 34 additions and 223 deletions
-26
View File
@@ -81,37 +81,11 @@ export interface PluginPromptEditor {
getText(): string;
/** Get the current selection range, or null if no selection or editor not mounted. */
getSelection(): { start: number; end: number; text: string } | null;
/** Register a paste event handler scoped to the prompt editor.
* Handlers run in registration order; first handler returning true consumes the event.
* Returns an unsubscribe function. No-op if the editor is not mounted. */
onPaste(handler: (event: ClipboardEvent) => boolean): () => void;
/** Register a keydown handler scoped to the prompt editor.
* Handlers run in registration order; first handler returning true consumes the event.
* Returns unsubscribe. No-op if the editor is not mounted. */
onKeyDown(handler: (event: KeyboardEvent) => boolean): () => void;
/** Focus the prompt editor. No-op if not mounted. */
focus(): void;
}
export interface PluginAttachments {
/** Insert a file reference at the current cursor position in the chat prompt.
* Validates that the file exists in the workspace before insertion.
* Does not auto-focus the editor (unlike prompt.insertText). Use prompt.focus() first if needed.
* @throws Error if no workspace is selected or the file doesn't exist
* Returns the canonical @file reference string (e.g., "@path/to/file.png"). */
insertFileReference(path: string): Promise<string>;
/** List currently attached file paths in the prompt. Returns paths without the @ prefix.
* Best-effort heuristic: matches @path/to/file.ext patterns. May match email-like patterns;
* use insertFileReference() for guaranteed-accurate insertion. */
getAttachedFiles(): string[];
/** Remove a file reference from the prompt by path. Removes the first occurrence of @path. */
removeFileReference(path: string): void;
}
export interface PluginRuntimeContext {
state: PluginRuntimeState;
prompt: PluginPromptEditor;
attachments: PluginAttachments;
openActionPalette: () => void;
focusPrompt: () => void;
addProject: () => void | Promise<void>;