Skip to content

File I/O

A plugin runs sandboxed and cannot touch the filesystem directly. Instead it requests file access through FengYuClient.files.*, which the host translates into calls under /api/plugin-runtime/{id}/files/**. The host mints an opaque FileRef (id ref_<uuid>) for each granted path, snapshots uploads into a temp tree, and rewrites FileRefs to absolute paths only at the moment it dispatches a worker RPC. Permissions gate every endpoint.

The grant model

A FileRef is an opaque handle:

ts
interface FileRef { id: string; name: string; kind: 'file'|'directory'; access: 'read'|'write'|'read-write'; size: number }
  • The id (e.g. ref_3f2a...) is the only value the UI ever sees.
  • Grants live in an in-memory ConcurrentHashMap keyed by id, scoped to the plugin id.
  • At RPC dispatch time, the host walks the params, finds any value shaped {id:"ref_..."}, resolves it via the grant map, and rewrites it to an absolute filesystem path before sending to the worker. The worker receives real paths, never raw upload bytes and never another plugin's refs.

Endpoints

All five endpoints live under base /api/plugin-runtime/{id}/files. Each is gated by a permission declared in the plugin manifest.

Method + pathBodyPermissionReturns
POST /uploadmultipart filefiles.readFileRef (a single uploaded file, snapshotted into temp)
POST /upload-directorymultipart files + paths[]; optional access=read-writefiles.read; both files.read + files.write for read-writeFileRef (a directory rebuilt in temp from the uploaded tree)
POST /nativeJSON {path, kind, access}files.read and/or files.writeFileRef (desktop only — the Electron native dialog returns a native path the host wraps as a ref)
POST /output(none)files.writeFileRef (a freshly allocated writable output directory)
GET /export/{ref}files.writeA zip of the granted directory, streamed for download

/native is meaningful only under the Electron desktop shell, where ctx.desktop.pickFile / pickDirectory yield real OS paths; in the browser, use /upload and /upload-directory instead. A workspace request uses native read-write access on desktop and an uploaded read-write working copy on the web. The requested access must match a permission the plugin actually holds.

A request that needs a permission the plugin did not declare returns 403. See Pitfalls.

AI chat fans a selected file/directory out into separate plugin-scoped grants for every compatible backend plugin. Files are read-only inputs. A selected directory receives read-write access only for plugins declaring both files.read and files.write; read-only plugins receive an isolated snapshot. An existing absolute path typed in the latest user message follows the same flow but is always read-only. FileRefs live for the chat session (they are not persisted; restart clears them).

Temp storage and cleanup

Uploads and output directories live under a per-plugin temp root:

${java.io.tmpdir}/fengyu/runtime-files/<pluginId>/<uuid>/{in|out}/...
  • The host snapshots every upload into a fresh <uuid>/in/ tree. Symlinks inside an uploaded tree are rejected, and traversal outside the snapshot root is blocked.
  • POST /output allocates a fresh <uuid>/out/ directory.
  • Grants are held in memory only — they do not survive a host restart.

Cleanup

  • The whole runtime-files tree is deleted in a @PreDestroy hook when the host shuts down.
  • There is no scheduled sweep. A long-running host accumulates granted files until process exit. Do not rely on individual files being reclaimed mid-session.

How the UI uses it

The FengYuClient.files.* helpers wrap these endpoints so the UI never builds multipart itself:

js
const file   = await fengyu.files.open({ extensions: ['xlsx'] })      // → POST /upload under the hood
const inDir  = await fengyu.files.inputDirectory()                    // → POST /upload-directory
const project = await fengyu.files.workspaceDirectory()               // → writable selected project / working copy
const outDir = await fengyu.files.outputDirectory()                   // → POST /output  (needs files.write)
await fengyu.files.export(outDir)                                     // → GET  /export/{ref}

workspaceDirectory() requires files.write; it returns the selected native directory on desktop and a writable uploaded working copy in a browser. Pass every FileRef straight into an RPC — do not extract its id — so the host can recognize and rewrite the complete {id, kind, access} object before the worker sees it:

js
import { createPluginRpc } from './generated/fengyu-rpc'
const rpc = createPluginRpc(fengyu)
const analysis = await rpc.analyze({ filePath: file as unknown as string })
await rpc.excelExecute({ outputDir: outDir as unknown as string, filePrefix: 'q3-' })

See Official Plugin — Excel for this flow end to end.

Next steps

Released under the GPL-3.0 License.