feat(server-mode): native-like folder browser, a developer kept to their own folder, no password minimum

- Folder browser (the Browse fallback for a remote browser): back/forward/up,
  a clickable breadcrumb that becomes a path field (Ctrl+L), Places (Home,
  the user's workspaces, recent picks), a filter, Name/Modified columns,
  Show hidden and New folder. A click selects; a double-click or Enter
  opens a folder or picks a file; Backspace goes up; typing jumps to a name.
- GET /api/lanes/browse: `hidden=1` (never the dot entries at the top of a
  non-admin's own folder), each entry's `mtime`, the answer's `home`.
- POST /api/lanes/browse/mkdir: one plain folder name inside an existing
  folder; a non-admin only in their own folder, none in server mode without
  one; under S3 a member's folder is made as the member.
- scope.ownFolder falls back to server.conf's USERS_ROOT under S3, so a
  settings file without AGENTHUB_USERS_ROOT no longer opens the host's home
  to every developer.
- Passwords: any non-empty password is accepted (no 8-character minimum).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 14:43:03 +07:00
parent b0dd5af22f
commit 763759189f
22 changed files with 1174 additions and 110 deletions
+2 -2
View File
@@ -241,7 +241,7 @@ const paths = {
required: ["username", "password", "currentPassword"],
properties: {
username: { type: "string" },
password: { type: "string", minLength: 8 },
password: { type: "string", minLength: 1 },
role: {
type: "string",
enum: ["admin", "analyst", "developer"],
@@ -282,7 +282,7 @@ const paths = {
operationId: "resetServerUserPassword",
summary: "Admin: a new password for that user; every login of theirs ends",
parameters: [idParam],
requestBody: changeBody({ password: { type: "string", minLength: 8 } }, ["password"]),
requestBody: changeBody({ password: { type: "string", minLength: 1 } }, ["password"]),
responses: changeResponses,
},
},
+36
View File
@@ -151,6 +151,42 @@ const paths = {
},
},
},
"/api/lanes/browse/mkdir": {
post: {
tags: ["Lanes"],
summary: "New folder, from the in-app folder browser",
description:
"Makes ONE folder `name` (no `/`, `\\`, NUL, `.` or `..`; ≤ 255 bytes) inside the existing folder `parent` — never `-p`, never over an existing entry. A non-admin makes one only inside their own folder, by real path (`EFOLDER`), and in server mode a non-admin with no folder of their own makes none. Under S3 a folder in a member's home is made as that member.",
operationId: "makeBrowseFolder",
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
required: ["parent", "name"],
properties: {
parent: { type: "string", description: "Absolute path of an existing folder." },
name: { type: "string" },
},
},
},
},
},
responses: {
201: {
description: "`{path}` — the folder made.",
content: {
"application/json": {
schema: { type: "object", properties: { path: { type: "string" } } },
},
},
},
400: { description: "EBADNAME, EBADPATH, or ENOTFOUND (no such parent folder)." },
403: { description: "EBADORIGIN, or EFOLDER (outside the caller's own folder)." },
409: { description: "EEXISTS (the name is taken) or EMEMBERGONE." },
},
},
},
"/api/lanes/ensure": {
post: {
tags: ["Lanes"],