chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes

Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool):
- server/hub/ holds the hub-only code (devices, remote sessions, upload ingest,
  LAN gateway), required only in server mode; hub-boundary.test.js keeps local
  code from importing it.
- A local install enrolls with `agenthub remote login` or Settings → AI tools →
  Company hub; the "Company account" AI Proxy (reserved name agenthub.company)
  sends a session through the hub's gateway, which adds a pool account's token —
  no credential reaches the machine. Only those sessions are uploaded, hourly
  and at their end; secret-shaped strings are redacted on the client.
- Gateway refuses: unknown device (401), a message without a session id, an
  unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota
  (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30.
- New alert rule type remote_unmonitored: a company-account session its device
  never registered; a late registration clears the flag instead.
- The hub imports a session once per upload round (chunks carry `more`),
  not once per chunk.

Artifacts:
- Share an artifact with chosen people (artifact_shares, PATCH sharedWith,
  GET /api/artifacts/people) besides everyone; a "Shared with you" tab.
  Only the owner or an admin sees or edits who it is shared with; audited.
- Folders: each account's own arrangement (agenthub.artifacts.folders pref).
- A claude.ai page keeps a copy viewable without the publishing account: its
  HTML file (and supporting files) snapshotted at publish, or the copy Claude
  saves for a Doc / Slides / Design in artifacts-export/ (a synchronous
  PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch),
  attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or
  pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF).

Fixes:
- tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's
  tmux (stop the last pane, start again within 10 s failed with ENOENT).
- shelves: a shelf restores beside changes in other files.
- chat: a finished reply ends the turn even with no hook at all.
- Settings → AI tools: profile list display tweaks.
This commit is contained in:
2026-11-04 15:16:49 +07:00
parent 3daf7ed4cb
commit c946b73d11
95 changed files with 7918 additions and 340 deletions
+58 -8
View File
@@ -50,6 +50,16 @@ const schemas = {
size: { type: "integer", nullable: true },
shared: { type: "boolean" },
sharedAt: { type: "string", nullable: true },
sharedWith: {
type: "array",
items: {
type: "object",
properties: { id: { type: "integer" }, name: { type: "string" } },
},
description:
"The people it is shared with by name — listed only to its owner or an admin, else empty.",
},
sharedWithMe: { type: "boolean", description: "Shared with the caller by name." },
installs: { type: "integer" },
createdAt: { type: "string" },
updatedAt: { type: "string" },
@@ -88,12 +98,12 @@ const paths = {
summary: "List artifacts",
operationId: "listArtifacts",
description:
"A developer sees their own rows plus shared ones; staff see every row in `hub` scope.",
"A developer sees their own rows plus shared ones (to everyone, or to them by name); staff see every row in `hub` scope. `withme`: the rows shared with the caller by name (server mode).",
parameters: [
{
name: "scope",
in: "query",
schema: { type: "string", enum: ["all", "mine", "hub", "unowned"] },
schema: { type: "string", enum: ["all", "mine", "hub", "withme", "unowned"] },
},
{
name: "kind",
@@ -116,7 +126,7 @@ const paths = {
operationId: "getArtifactSettings",
responses: {
200: {
description: "`{serverMode, canShare, installsEnabled, canManage}`.",
description: "`{serverMode, canShare, installsEnabled, exportsEnabled, canManage}`.",
content: {
"application/json": {
schema: {
@@ -125,6 +135,11 @@ const paths = {
serverMode: { type: "boolean" },
canShare: { type: "boolean" },
installsEnabled: { type: "boolean" },
exportsEnabled: {
type: "boolean",
description:
"Claude panes are asked to save a viewable copy of a Doc / Slides / Design they publish (scripts/artifact-export-hint.js).",
},
canManage: { type: "boolean" },
},
},
@@ -135,17 +150,46 @@ const paths = {
},
put: {
tags: ["Artifacts"],
summary: "Turn installs on or off (admin; any caller in local mode)",
summary:
"Turn installs, or the copy hint for Docs/Slides, 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"]),
description:
"Audited in server mode (Settings → AI tools → Artifacts Hub). Send either or both; the export switch reaches panes started afterwards.",
requestBody: json({
installsEnabled: { type: "boolean" },
exportsEnabled: { type: "boolean" },
}),
responses: {
200: { description: "`{installsEnabled}`." },
200: { description: "`{installsEnabled, exportsEnabled}`." },
400: { description: "EBADFIELD" },
403: { description: "EADMIN / EBADORIGIN" },
},
},
},
"/api/artifacts/{id}/copy": {
post: {
tags: ["Artifacts"],
summary: "Take a page's viewable copy from the file it was published from",
operationId: "copyArtifactPage",
description:
"A `page` captured before pages kept a copy: if the HTML file it was published from is still in its workspace, it is snapshotted (read under the pinned-file guards) and the row's `files` name it. Once per row per server run; any viewer may ask and gets only the copy. A row that already has one, or is not a page, is answered as it is.",
parameters: [idParam],
responses: {
200: { description: "`{item}`.", content: { "application/json": { schema: item } } },
404: errs,
},
},
},
"/api/artifacts/people": {
get: {
tags: ["Artifacts"],
summary: "The people the caller may share an artifact with",
operationId: "listArtifactPeople",
description:
"Server mode: every enabled account but the caller, by name only (`{people: [{id, username, name}]}`); empty in local mode.",
responses: { 200: { description: "`{people: [{id, username, name}]}`." } },
},
},
"/api/artifacts/candidates": {
get: {
tags: ["Artifacts"],
@@ -326,7 +370,13 @@ const paths = {
requestBody: json({
title: { type: "string" },
description: { type: "string" },
shared: { type: "boolean" },
shared: { type: "boolean", description: "Share with everyone (the Hub)." },
sharedWith: {
type: "array",
items: { type: "integer" },
description:
"Server mode: the user ids it is shared with by name (replaces the list; up to 100 enabled users; the owner is dropped). Out of its owner's library and shared with nobody, the row is deleted (`removed`).",
},
source: { type: "string", description: "Plugin only." },
market: { type: "string", description: "Plugin only." },
}),