/** * @file OpenAPI fragment for the Artifacts hub (`/api/artifacts/*`, server/routes/artifacts.js) * and the Administration page's Server tab (`GET /api/host`, server/routes/host.js). * Hand-written from the routes; docs/API.md § Artifacts hub is the long form. * @author Nguyễn Ngọc Trí Vĩ */ const tags = [ { name: "Artifacts", description: "What agents produced, shared as a SNAPSHOT: a claude.ai page link, a pinned or uploaded file, a skill or a plugin. A viewer reads only the snapshot copy (sandbox CSP, nosniff); an install writes into the caller's own account only. Every write is same-origin.", }, { name: "Host", description: "Administration → Server: the machine's hardware, load, disks and each user's share of CPU and memory. Read-only, staff only in server mode.", }, ]; const errorCodes = "400 EBADURL/EBADPATH/EBADNAME/ENOSKILLMD/ENODESCRIPTION/EBADFIELD/EBADUPLOAD, 403 EFORBIDDEN/ESECRET/EFOREIGN/EINSTALLSOFF/EADMIN, 404 ENOTFOUND/ENOLANE/ENOFILE, 409 EEXISTS/ENOTSHARED/ENOACCOUNT/EUPGRADE, 413 ETOOBIG, 502 EPLUGIN/EMARKETFETCH/ENEEDLOGIN."; const schemas = { Artifact: { type: "object", properties: { id: { type: "string" }, kind: { type: "string", enum: ["page", "file", "skill", "plugin"] }, title: { type: "string" }, description: { type: "string", nullable: true }, ownerId: { type: "integer", nullable: true }, owner: { type: "string", nullable: true, description: "Display name or username." }, mine: { type: "boolean" }, canChange: { type: "boolean" }, laneId: { type: "integer", nullable: true }, sessionId: { type: "string", nullable: true }, source: { type: "string", nullable: true, description: "Plugin: `name@marketplace`." }, market: { type: "string", nullable: true, description: "Plugin: owner/repo, git URL or marketplace.json URL." }, url: { type: "string", nullable: true, description: "Page: a https://claude.ai/… link." }, files: { type: "array", items: { type: "object" }, description: "The files the snapshot recorded." }, size: { type: "integer", nullable: true }, shared: { type: "boolean" }, sharedAt: { type: "string", nullable: true }, installs: { type: "integer" }, createdAt: { type: "string" }, updatedAt: { type: "string" }, }, }, }; const json = (properties, required = []) => ({ required: true, content: { "application/json": { schema: { type: "object", required, properties } } }, }); const multipart = (properties, required = []) => ({ required: true, content: { "multipart/form-data": { schema: { type: "object", required, properties } } }, }); const item = { type: "object", properties: { item: { $ref: "#/components/schemas/Artifact" } }, }; const items = { type: "object", properties: { items: { type: "array", items: { $ref: "#/components/schemas/Artifact" } } }, }; const idParam = { name: "id", in: "path", required: true, schema: { type: "string" } }; const laneQuery = { name: "laneId", in: "query", required: false, schema: { type: "integer" } }; const errs = { description: `Error \`{error:{code,message}}\`: ${errorCodes}` }; const created = (schema, description = "Created.") => ({ 201: { description, content: { "application/json": { schema } } }, 400: errs, }); const paths = { "/api/artifacts": { get: { tags: ["Artifacts"], summary: "List artifacts", operationId: "listArtifacts", description: "A developer sees their own rows plus shared ones; staff see every row in `hub` scope.", parameters: [ { name: "scope", in: "query", schema: { type: "string", enum: ["mine", "hub"] } }, { name: "kind", in: "query", schema: { type: "string", enum: ["page", "file", "skill", "plugin"] } }, { name: "q", in: "query", schema: { type: "string" } }, laneQuery, { name: "sessionId", in: "query", schema: { type: "string" } }, ], responses: { 200: { description: "`{items}`.", content: { "application/json": { schema: items } } } }, }, }, "/api/artifacts/settings": { get: { tags: ["Artifacts"], summary: "Hub switches and what the caller may do", operationId: "getArtifactSettings", responses: { 200: { description: "`{serverMode, canShare, installsEnabled, canManage}`.", content: { "application/json": { schema: { type: "object", properties: { serverMode: { type: "boolean" }, canShare: { type: "boolean" }, installsEnabled: { type: "boolean" }, canManage: { type: "boolean" }, }, }, }, }, }, }, }, put: { tags: ["Artifacts"], summary: "Turn installs on or off (admin; any caller in local mode)", operationId: "setArtifactSettings", description: "Audited in server mode (Settings → AI tools → Artifacts Hub).", requestBody: json({ installsEnabled: { type: "boolean" } }, ["installsEnabled"]), responses: { 200: { description: "`{installsEnabled}`." }, 400: { description: "EBADFIELD" }, 403: { description: "EADMIN / EBADORIGIN" }, }, }, }, "/api/artifacts/candidates": { get: { tags: ["Artifacts"], summary: "Documents a workspace's sessions wrote (to pin)", operationId: "listArtifactCandidates", parameters: [laneQuery], responses: { 200: { description: "`{items: [{path, sessionId, at, exists, pinned}]}`." }, 404: errs, }, }, }, "/api/artifacts/skills": { get: { tags: ["Artifacts"], summary: "Skills the caller can share (user + workspace)", operationId: "listArtifactSkills", parameters: [laneQuery], responses: { 200: { description: '`{items: [{name, from: "user"|"workspace"}]}`.' } }, }, }, "/api/artifacts/page": { post: { tags: ["Artifacts"], summary: "Add a claude.ai page link", operationId: "addArtifactPage", description: "Only `https://claude.ai/…` is accepted; one row per owner and link.", requestBody: json( { url: { type: "string" }, title: { type: "string" }, description: { type: "string" } }, ["url"] ), responses: created(item), }, }, "/api/artifacts/file": { post: { tags: ["Artifacts"], summary: "Pin a workspace file (snapshotted)", operationId: "pinArtifactFile", description: "Read under the download route's guards (`lanePath`, `resolveInLane`, `ESECRET`, ≤ 25 MB) and copied once.", requestBody: json( { laneId: { type: "integer" }, path: { type: "string" }, title: { type: "string" }, sessionId: { type: "string" }, }, ["laneId", "path"] ), responses: created(item), }, }, "/api/artifacts/file/upload": { post: { tags: ["Artifacts"], summary: "Keep one uploaded file (≤ 25 MB)", operationId: "uploadArtifactFile", requestBody: multipart({ files: { type: "string", format: "binary" }, title: { type: "string" }, description: { type: "string" }, }, ["files"]), responses: created(item), }, }, "/api/artifacts/skill": { post: { tags: ["Artifacts"], summary: "Share an existing skill", operationId: "addArtifactSkill", requestBody: json( { name: { type: "string" }, laneId: { type: "integer" }, title: { type: "string" }, description: { type: "string" }, }, ["name"] ), responses: created(item), }, }, "/api/artifacts/skill/upload": { post: { tags: ["Artifacts"], summary: "Skills from files: a file, a folder, a .zip/.skill or several skills", operationId: "uploadArtifactSkills", description: "`paths` is a JSON array of each file's relative path. Zip sizes are read before inflating; nothing is stored if any skill fails.", requestBody: multipart({ files: { type: "array", items: { type: "string", format: "binary" } }, paths: { type: "string" }, name: { type: "string" }, title: { type: "string" }, description: { type: "string" }, }, ["files"]), responses: created(items), }, }, "/api/artifacts/skill/new": { post: { tags: ["Artifacts"], summary: "Write a skill in the dialog", operationId: "writeArtifactSkill", requestBody: json( { name: { type: "string" }, description: { type: "string" }, content: { type: "string" }, title: { type: "string" }, }, ["name", "description", "content"] ), responses: created({ type: "object", properties: { item: item.properties.item, items: items.properties.items } }), }, }, "/api/artifacts/plugin/inspect": { post: { tags: ["Artifacts"], summary: "Read a marketplace's plugin list", operationId: "inspectArtifactMarket", description: "Clones a user-named https link only to read `marketplace.json`; no credential is ever sent.", requestBody: json({ market: { type: "string" } }, ["market"]), responses: { 200: { description: "`{market, name, description, plugins: [{name, key, description, version, category}]}`." }, 502: errs, }, }, }, "/api/artifacts/plugin": { post: { tags: ["Artifacts"], summary: "Share a plugin from a marketplace", operationId: "addArtifactPlugin", requestBody: json( { key: { type: "string", description: "`name@marketplace`." }, market: { type: "string" }, title: { type: "string" }, description: { type: "string" }, }, ["key", "market"] ), responses: created(item), }, }, "/api/artifacts/{id}": { get: { tags: ["Artifacts"], summary: "One artifact", operationId: "getArtifact", parameters: [idParam], responses: { 200: { description: "`{item}`.", content: { "application/json": { schema: item } } }, 404: errs }, }, patch: { tags: ["Artifacts"], summary: "Rename, describe, share or unshare (owner or admin)", operationId: "updateArtifact", parameters: [idParam], requestBody: json({ title: { type: "string" }, description: { type: "string" }, shared: { type: "boolean" }, source: { type: "string", description: "Plugin only." }, market: { type: "string", description: "Plugin only." }, }), responses: { 200: { description: "`{item}`.", content: { "application/json": { schema: item } } }, 403: errs, 404: errs }, }, delete: { tags: ["Artifacts"], summary: "Delete an artifact and its snapshot (owner or admin)", operationId: "deleteArtifact", parameters: [idParam], responses: { 200: { description: "`{ok: true}`." }, 403: errs, 404: errs }, }, }, "/api/artifacts/{id}/raw": { get: { tags: ["Artifacts"], summary: "One recorded file of a snapshot", operationId: "getArtifactRaw", description: "Always served under a `sandbox` CSP with `nosniff`; an unlisted type, or `download=1`, is an attachment. Only a file the snapshot recorded can be named.", parameters: [ idParam, { name: "path", in: "query", required: true, schema: { type: "string" } }, { name: "download", in: "query", schema: { type: "string", enum: ["1"] } }, ], responses: { 200: { description: "The file bytes." }, 400: errs, 404: errs }, }, }, "/api/artifacts/{id}/install": { post: { tags: ["Artifacts"], summary: "Install a shared skill or plugin into the caller's own account", operationId: "installArtifact", description: "A skill goes to the caller's `~/.claude/skills/` (never over an existing one without `replace`; the old one is moved aside). A plugin runs `claude plugin install --scope user` as the caller.", parameters: [idParam], requestBody: json({ replace: { type: "boolean" } }), responses: { 200: { description: "`{installed, replaced?}`." }, 403: errs, 409: errs, 502: errs, }, }, }, "/api/host": { get: { tags: ["Host"], summary: "The machine and each user's live share of it", operationId: "getHost", description: "Cached 3 s. `usage` is null off Linux. Server mode: admin/analyst only (403 EADMIN for a developer).", responses: { 200: { description: "`{at, hostname, os, uptime, cpu, memory, disks, homesMount, gpus, usage}` — see docs/API.md.", content: { "application/json": { schema: { type: "object" } } }, }, 500: { description: "EHOST" }, }, }, }, }; module.exports = { tags, schemas, paths };