From 589516a0216fdd5d92ca252bd681af5573c103a4 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Thu, 11 Jun 2026 19:24:31 -0400 Subject: [PATCH] feat(api): complete per-instance file op-set (delete/rename/mkdir/mkfile/move/copy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rounds out the per-instance file bridge to the agent's full jailed file manager so a real file browser can be built on it: POST :id/files/ {delete,rename,mkdir,mkfile,move,copy}, all via requestScoped (license- scoped reply) on the new agent {op,path} protocol. files.manage. The broken legacy VueFinder /api/files (retired Go fm_* protocol, wrong subject, default _INBOX) is superseded by this — frontend rewrite next. Co-Authored-By: Claude Fable 5 --- .../modules/instances/instances.controller.ts | 66 +++++++++++++++++++ .../modules/instances/instances.service.ts | 45 +++++++++++++ 2 files changed, 111 insertions(+) diff --git a/backend-nest/src/modules/instances/instances.controller.ts b/backend-nest/src/modules/instances/instances.controller.ts index 3d81f3a..d372472 100644 --- a/backend-nest/src/modules/instances/instances.controller.ts +++ b/backend-nest/src/modules/instances/instances.controller.ts @@ -64,4 +64,70 @@ export class InstancesController { ) { return this.instances.writeFile(licenseId, id, body.path, body.content ?? ''); } + + @Post(':id/files/delete') + @RequirePermission('files.manage') + @ApiOperation({ summary: 'Delete a file or directory (jailed)' }) + async deleteFile( + @CurrentTenant() licenseId: string, + @Param('id') id: string, + @Body() body: { path: string }, + ) { + return this.instances.deleteFile(licenseId, id, body.path); + } + + @Post(':id/files/rename') + @RequirePermission('files.manage') + @ApiOperation({ summary: 'Rename a file/directory within its parent (jailed)' }) + async renameFile( + @CurrentTenant() licenseId: string, + @Param('id') id: string, + @Body() body: { path: string; name: string }, + ) { + return this.instances.renameFile(licenseId, id, body.path, body.name); + } + + @Post(':id/files/mkdir') + @RequirePermission('files.manage') + @ApiOperation({ summary: 'Create a directory (jailed)' }) + async mkdir( + @CurrentTenant() licenseId: string, + @Param('id') id: string, + @Body() body: { path: string }, + ) { + return this.instances.mkdir(licenseId, id, body.path); + } + + @Post(':id/files/mkfile') + @RequirePermission('files.manage') + @ApiOperation({ summary: 'Create an empty file (jailed)' }) + async mkfile( + @CurrentTenant() licenseId: string, + @Param('id') id: string, + @Body() body: { path: string }, + ) { + return this.instances.mkfile(licenseId, id, body.path); + } + + @Post(':id/files/move') + @RequirePermission('files.manage') + @ApiOperation({ summary: 'Move a file/directory (jailed)' }) + async moveFile( + @CurrentTenant() licenseId: string, + @Param('id') id: string, + @Body() body: { path: string; dest: string }, + ) { + return this.instances.moveFile(licenseId, id, body.path, body.dest); + } + + @Post(':id/files/copy') + @RequirePermission('files.manage') + @ApiOperation({ summary: 'Copy a file/directory (jailed)' }) + async copyFile( + @CurrentTenant() licenseId: string, + @Param('id') id: string, + @Body() body: { path: string; dest: string }, + ) { + return this.instances.copyFile(licenseId, id, body.path, body.dest); + } } diff --git a/backend-nest/src/modules/instances/instances.service.ts b/backend-nest/src/modules/instances/instances.service.ts index 508eda4..3780c12 100644 --- a/backend-nest/src/modules/instances/instances.service.ts +++ b/backend-nest/src/modules/instances/instances.service.ts @@ -97,4 +97,49 @@ export class InstancesService { const res = await this.fileOp(licenseId, instanceId, { op: 'write', path, content }); return res.data ?? { status: 'success' }; } + + async deleteFile(licenseId: string, instanceId: string, path: string): Promise { + if (!path) throw new BadRequestException('path is required'); + return (await this.fileOp(licenseId, instanceId, { op: 'delete', path })).data ?? { ok: true }; + } + + async renameFile( + licenseId: string, + instanceId: string, + path: string, + name: string, + ): Promise { + if (!path || !name) throw new BadRequestException('path and name are required'); + return (await this.fileOp(licenseId, instanceId, { op: 'rename', path, name })).data ?? { ok: true }; + } + + async mkdir(licenseId: string, instanceId: string, path: string): Promise { + if (!path) throw new BadRequestException('path is required'); + return (await this.fileOp(licenseId, instanceId, { op: 'mkdir', path })).data ?? { ok: true }; + } + + async mkfile(licenseId: string, instanceId: string, path: string): Promise { + if (!path) throw new BadRequestException('path is required'); + return (await this.fileOp(licenseId, instanceId, { op: 'mkfile', path })).data ?? { ok: true }; + } + + async moveFile( + licenseId: string, + instanceId: string, + path: string, + dest: string, + ): Promise { + if (!path || !dest) throw new BadRequestException('path and dest are required'); + return (await this.fileOp(licenseId, instanceId, { op: 'move', path, dest })).data ?? { ok: true }; + } + + async copyFile( + licenseId: string, + instanceId: string, + path: string, + dest: string, + ): Promise { + if (!path || !dest) throw new BadRequestException('path and dest are required'); + return (await this.fileOp(licenseId, instanceId, { op: 'copy', path, dest })).data ?? { ok: true }; + } }