c946b73d11
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.
72 lines
2.1 KiB
JavaScript
72 lines
2.1 KiB
JavaScript
/**
|
|
* @file remote-client.js (routes)
|
|
* @description `/api/remote-client` — a LOCAL AgentHub's link to the company
|
|
* hub (server/lib/remote-client.js): whether this machine is enrolled, which
|
|
* Company-account sessions it sent and when, and enroll / log out / send now.
|
|
* Every write is same-origin; the password in `login` goes on to the hub once
|
|
* and is never stored; no answer ever carries the device token. A hub (server
|
|
* mode) has none of this: it answers 404 ENOTLOCAL there.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const express = require("express");
|
|
const { sameOriginGuard } = require("../lib/same-origin");
|
|
const remote = require("../lib/remote-client");
|
|
|
|
const router = express.Router();
|
|
|
|
router.use((req, res, next) =>
|
|
require("../lib/auth").isServerMode()
|
|
? res
|
|
.status(404)
|
|
.json({ error: { code: "ENOTLOCAL", message: "a hub does not enroll with another hub" } })
|
|
: next()
|
|
);
|
|
|
|
const statusOf = (e) =>
|
|
e.code === "EHUBDOWN"
|
|
? 503
|
|
: e.code === "ETOOMANY"
|
|
? 429
|
|
: e.status && e.status >= 400 && e.status < 600
|
|
? e.status
|
|
: 400;
|
|
const send = (res, e) =>
|
|
res.status(statusOf(e)).json({ error: { code: e.code || "EINTERNAL", message: e.message } });
|
|
|
|
router.get("/", (_req, res) => {
|
|
res.json({ ...remote.status(), uploads: remote.uploads() });
|
|
});
|
|
|
|
router.post("/login", sameOriginGuard, async (req, res) => {
|
|
try {
|
|
const { hub, username, password, deviceName } = req.body || {};
|
|
res.json(await remote.login({ hub, username, password, deviceName }));
|
|
} catch (e) {
|
|
send(res, e);
|
|
}
|
|
});
|
|
|
|
router.post("/logout", sameOriginGuard, async (_req, res) => {
|
|
try {
|
|
res.json(await remote.logout());
|
|
} catch (e) {
|
|
send(res, e);
|
|
}
|
|
});
|
|
|
|
router.post("/upload", sameOriginGuard, async (_req, res) => {
|
|
try {
|
|
if (!remote.status().enrolled) {
|
|
return res
|
|
.status(400)
|
|
.json({ error: { code: "ENOHUB", message: "this machine is not enrolled with a hub" } });
|
|
}
|
|
res.json(await remote.uploadAll());
|
|
} catch (e) {
|
|
send(res, e);
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|