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
+53
View File
@@ -49,6 +49,52 @@ const { getCodexHome } = require("./codex-home");
const { PROXY_CAPABLE } = require("./ai-tools");
const { gatewayBase } = require("./proxy-gateway");
/** The remote client's Company account (server/lib/remote-client.js `HUB_PROXY`). */
const COMPANY = proxies.COMPANY_NAME;
/**
* The Company account as a profile `proxyForSpawn` understands. Local mode on
* an enrolled machine only (a hub never enrolls itself), Claude only.
*/
function companyProfile(tool) {
if (require("./auth").isServerMode()) {
throw Object.assign(new Error("the Company account is for a local AgentHub"), {
code: "EBADPROXY",
});
}
if (tool !== "claude") {
throw Object.assign(new Error("only Claude sessions can use the Company account"), {
code: "EUNSUPPORTED",
});
}
const st = require("./remote-client").status();
if (!st.enrolled) {
throw Object.assign(
new Error(
st.expired
? "this machine's hub login expired — run agenthub remote login again"
: "this machine is not enrolled with a company hub — run agenthub remote login"
),
{ code: "ENOHUB" }
);
}
return { name: COMPANY, label: "Company account", company: true, builtin: false };
}
/**
* The Company account's `--settings` file: the hub's gateway and this
* machine's device token (its own, in a 0600 file of ours — the pool
* credential stays on the hub). Rewritten at every spawn. A pane keeps the
* token it started with (Claude Code reads it once — checked 2026-11-04), so
* after a new `agenthub remote login` an open pane is resumed, not refreshed.
*/
function writeCompanySettings() {
const env = require("./remote-client").claudeEnv();
const file = claudeSettingsPath(COMPANY);
writeSecret(file, `${JSON.stringify({ env }, null, 2)}\n`);
return file;
}
/** Server mode: panes reach a proxy only through the key gateway. */
const viaGateway = () => require("./auth").isServerMode();
@@ -191,6 +237,9 @@ function codexOverrides(profile) {
*/
function resolveProxy(tool, name, ownerId = null) {
if (name == null || name === "" || name === proxies.BUILTIN_NAME) return null;
// A remote client's Company account: the hub's gateway, Claude only, and
// only on a machine enrolled with a hub (server/lib/remote-client.js).
if (name === COMPANY) return companyProfile(tool);
if (typeof name !== "string") {
throw Object.assign(new Error("proxy must be a profile name"), { code: "EBADPROXY" });
}
@@ -225,6 +274,9 @@ function resumeProxy(sessionId, ownerId = null) {
if (!sessionId) return null;
const name = require("./dashboard-runs").proxyForSession(sessionId);
if (!name) return null;
// A Company-account session resumes on it (refused at spawn if this machine
// is no longer enrolled — never silently moved to the user's own login).
if (name === COMPANY) return name;
const owner = proxies.ownerOfName(name);
if (owner != null && owner !== ownerId) return null;
try {
@@ -244,6 +296,7 @@ function resumeProxy(sessionId, ownerId = null) {
*/
function proxyForSpawn(tool, profile, paneToken = null, member = null, runId = null) {
if (!profile) return { argv: [], env: {} };
if (profile.company) return { argv: [`--settings=${writeCompanySettings()}`], env: {} };
const gw = viaGateway();
if (gw && !paneToken) {
// Server mode always mints one (auth.newPaneToken); without it the pane