feat(api): complete per-instance file op-set (delete/rename/mkdir/mkfile/move/copy)
All checks were successful
CI / backend-types (push) Successful in 8s
CI / frontend-build (push) Successful in 15s
CI / agent-tests (push) Successful in 54s
CI / integration (push) Successful in 25s

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 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-06-11 19:24:31 -04:00
parent f60e6abd33
commit 589516a021
2 changed files with 111 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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<unknown> {
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<unknown> {
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<unknown> {
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<unknown> {
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<unknown> {
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<unknown> {
if (!path || !dest) throw new BadRequestException('path and dest are required');
return (await this.fileOp(licenseId, instanceId, { op: 'copy', path, dest })).data ?? { ok: true };
}
}