/** * @file OpenAPI fragments for the remote-clients architecture * (docs/plans/remote-clients/README.md): the HUB's routes (server mode only, * server/hub/ — a device's enrollment, session registration and hourly upload * under `/api/remote/device`, and the signed-in views under `/api/remote`) and * a LOCAL AgentHub's `/api/remote-client` (server/routes/remote-client.js). * The gateway `/api/remote/gw/v1/*` is a raw relay of Anthropic's Messages API * and is described in the tag, not as operations. Exports `{ tags, schemas, * paths }` for server/openapi-extra.js. * @author Nguyễn Ngọc Trí Vĩ */ const tags = [ { name: "Remote clients", description: "Company hub ↔ local AgentHub. On the hub (server mode): a machine enrolls with its user's password and gets ONE device token (shown once, 90 days, revocable); it registers each Company-account session at start (id + folder, no content) and uploads the new lines of its transcripts hourly. Its Claude Code reaches Anthropic only through `/api/remote/gw/v1/*`, which checks the device token and the registered session id (`x-claude-code-session-id`), swaps in a pool account's OAuth token and relays the answer — the pool credential never leaves the hub. On a local install, `/api/remote-client` shows and manages this machine's enrollment; the device token is never returned.", }, ]; const err = { $ref: "#/components/schemas/ErrorResponse" }; const json = (schema) => ({ "application/json": { schema } }); const ok = (description, schema = { type: "object" }) => ({ description, content: json(schema) }); const fail = (description) => ({ description, content: json(err) }); const schemas = { RemoteDevice: { type: "object", description: "An enrolled machine. Never carries its token or token hash.", properties: { id: { type: "integer" }, userId: { type: "integer" }, name: { type: "string" }, createdAt: { type: "string", format: "date-time" }, expiresAt: { type: "string", format: "date-time" }, lastSeenAt: { type: "string", format: "date-time", nullable: true }, lastIp: { type: "string", nullable: true }, revokedAt: { type: "string", format: "date-time", nullable: true }, active: { type: "boolean" }, }, }, RemoteUploadFile: { type: "object", required: ["path", "from", "to", "data"], properties: { path: { type: "string", description: "`.jsonl` or `/subagents/agent-.jsonl` — nothing else.", }, from: { type: "integer", description: "Local byte offset this chunk starts at; must equal the hub's `consumed`.", }, to: { type: "integer", description: "Local byte offset after the chunk." }, data: { type: "string", description: "Whole JSONL lines, secret-shaped strings already replaced.", }, }, }, RemoteClientStatus: { type: "object", properties: { enrolled: { type: "boolean" }, expired: { type: "boolean" }, hub: { type: "string" }, user: { type: "object", nullable: true }, device: { $ref: "#/components/schemas/RemoteDevice" }, enrolledAt: { type: "string", nullable: true }, retainDays: { type: "integer", nullable: true }, uploads: { type: "array", items: { type: "object" } }, }, }, }; const T = ["Remote clients"]; const sid = { name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }; const did = { name: "id", in: "path", required: true, schema: { type: "integer" } }; const paths = { "/api/remote/device/login": { post: { tags: T, operationId: "remoteDeviceLogin", summary: "Enroll a machine (hub, server mode)", description: "The user's hub password, throttled like a login. Answers the device token ONCE.", requestBody: { required: true, content: json({ type: "object", required: ["username", "password"], properties: { username: { type: "string" }, password: { type: "string" }, deviceName: { type: "string" }, }, }), }, responses: { 200: ok("Enrolled", { type: "object", properties: { token: { type: "string", example: "ahdev_…" }, device: { $ref: "#/components/schemas/RemoteDevice" }, user: { type: "object" }, retainDays: { type: "integer" }, }, }), 401: fail("Wrong username or password"), 429: fail("Too many failed logins"), }, }, }, "/api/remote/device/me": { get: { tags: T, operationId: "remoteDeviceMe", summary: "This device and its user (device token)", responses: { 200: ok("The device"), 401: fail("Token missing, expired or revoked") }, }, }, "/api/remote/device/logout": { post: { tags: T, operationId: "remoteDeviceLogout", summary: "Revoke this device's own token", responses: { 200: ok("Revoked"), 401: fail("Token missing, expired or revoked") }, }, }, "/api/remote/device/sessions": { post: { tags: T, operationId: "remoteDeviceRegisterSession", summary: "Register a Company-account session at its start (id + folder, no content)", requestBody: { required: true, content: json({ type: "object", required: ["sessionId", "cwd"], properties: { sessionId: { type: "string", format: "uuid" }, cwd: { type: "string" }, tool: { type: "string", enum: ["claude"] }, }, }), }, responses: { 200: ok("Registered (again, on a resume)"), 400: fail("Not a UUID, not an absolute folder, not Claude"), 409: fail("EFOREIGN — the session belongs to another device or is already known"), }, }, }, "/api/remote/device/sessions/{id}/end": { post: { tags: T, operationId: "remoteDeviceEndSession", summary: "Mark a registered session ended", parameters: [sid], responses: { 200: ok("Done") }, }, }, "/api/remote/device/sessions/{id}/files": { get: { tags: T, operationId: "remoteDeviceSessionFiles", summary: "How far the hub holds each file of a registered session", parameters: [sid], responses: { 200: ok("Files and offsets"), 404: fail("Not this device's session") }, }, }, "/api/remote/device/upload": { post: { tags: T, operationId: "remoteDeviceUpload", summary: "Upload new transcript lines of a registered session (hourly)", description: "Each chunk must start where the hub stopped; the hub then imports its copy with the local-history importer and stamps the session as this device's user's.", requestBody: { required: true, content: json({ type: "object", required: ["sessionId"], properties: { sessionId: { type: "string", format: "uuid" }, files: { type: "array", items: { $ref: "#/components/schemas/RemoteUploadFile" } }, ended: { type: "boolean" }, }, }), }, responses: { 200: ok("Taken"), 400: fail("A path outside the two file shapes, a bad range, half a line"), 404: fail("Not this device's session"), 409: fail("EOFFSET — `error.expected.consumed` says where to resume"), 410: fail("EPURGED — the conversation passed its retention"), }, }, }, "/api/remote/devices": { get: { tags: T, operationId: "remoteListDevices", summary: "Enrolled machines — your own; staff may pass ?user=", parameters: [{ name: "user", in: "query", schema: { type: "integer" } }], responses: { 200: ok("Devices", { type: "object", properties: { devices: { type: "array", items: { $ref: "#/components/schemas/RemoteDevice" } }, }, }), }, }, }, "/api/remote/devices/{id}/revoke": { post: { tags: T, operationId: "remoteRevokeDevice", summary: "Revoke a device — its owner or an admin, with their own password again (audited)", parameters: [did], requestBody: { required: true, content: json({ type: "object", required: ["password"], properties: { password: { type: "string" } }, }), }, responses: { 200: ok("Revoked"), 403: fail("Wrong password"), 404: fail("No such device of yours"), }, }, }, "/api/remote/sessions": { get: { tags: T, operationId: "remoteListSessions", summary: "Registered Company-account sessions (device › folder) — your own; staff may pass ?user=", parameters: [{ name: "user", in: "query", schema: { type: "integer" } }], responses: { 200: ok("Sessions") }, }, }, "/api/remote/audit": { get: { tags: T, operationId: "remoteMyAudit", summary: "Who (admin/analyst) opened your sessions", responses: { 200: ok("Audit entries") }, }, }, "/api/remote-client": { get: { tags: T, operationId: "remoteClientStatus", summary: "This local machine's enrollment and what it sent (local AgentHub)", responses: { 200: ok("Status", { $ref: "#/components/schemas/RemoteClientStatus" }) }, }, }, "/api/remote-client/login": { post: { tags: T, operationId: "remoteClientLogin", summary: "Enroll this machine with a hub — the password goes to the hub once and is not kept", requestBody: { required: true, content: json({ type: "object", required: ["hub", "username", "password"], properties: { hub: { type: "string", example: "http://10.1.13.7:4820" }, username: { type: "string" }, password: { type: "string" }, deviceName: { type: "string" }, }, }), }, responses: { 200: ok("Enrolled", { $ref: "#/components/schemas/RemoteClientStatus" }), 400: fail("Bad hub address"), 401: fail("Wrong username or password"), 503: fail("EHUBDOWN — the hub cannot be reached"), }, }, }, "/api/remote-client/logout": { post: { tags: T, operationId: "remoteClientLogout", summary: "Revoke this machine on the hub (best effort) and forget its token", responses: { 200: ok("Logged out") }, }, }, "/api/remote-client/upload": { post: { tags: T, operationId: "remoteClientUpload", summary: "Send the Company-account sessions' new lines now", responses: { 200: ok("Round result"), 400: fail("ENOHUB — not enrolled") }, }, }, }; module.exports = { tags, schemas, paths };