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.
70 lines
2.6 KiB
JavaScript
70 lines
2.6 KiB
JavaScript
/**
|
|
* @file hub-boundary.test.js
|
|
* @description `server/hub/` (the remote-clients hub, server mode only) is
|
|
* loaded only behind `isServerMode()`: no file outside it may require it,
|
|
* except `server/index.js` on lines that test the mode themselves. And a
|
|
* local-mode app really leaves it out — no module under `server/hub/` is in
|
|
* the require cache after `createApp()` in local mode.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const path = require("node:path");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const HUB = path.join(ROOT, "server", "hub");
|
|
const SCAN = ["server", "bin", "scripts", "mcp", "statusline"];
|
|
const REQUIRE_HUB = /require\(\s*["'`][^"'`]*\bhub(?:\/[^"'`]*)?["'`]\s*\)/;
|
|
|
|
function jsFiles(dir) {
|
|
const out = [];
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) {
|
|
if (["node_modules", "__tests__", "dist"].includes(e.name) || full === HUB) continue;
|
|
out.push(...jsFiles(full));
|
|
} else if (/\.(c|m)?js$/.test(e.name)) {
|
|
out.push(full);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe("server/hub boundary", () => {
|
|
it("nothing outside server/hub requires it, except index.js behind isServerMode()", () => {
|
|
const offenders = [];
|
|
for (const top of SCAN) {
|
|
const dir = path.join(ROOT, top);
|
|
if (!fs.existsSync(dir)) continue;
|
|
for (const file of jsFiles(dir)) {
|
|
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
lines.forEach((line, i) => {
|
|
if (!REQUIRE_HUB.test(line)) return;
|
|
const rel = path.relative(ROOT, file);
|
|
if (rel === path.join("server", "index.js") && /isServerMode\(\)/.test(line)) return;
|
|
offenders.push(`${rel}:${i + 1}: ${line.trim()}`);
|
|
});
|
|
}
|
|
}
|
|
assert.deepEqual(offenders, []);
|
|
});
|
|
|
|
it("a local-mode app loads no module of server/hub", () => {
|
|
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "hub-boundary-"));
|
|
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
|
|
process.env.CLAUDE_HOME = path.join(TMP, ".claude");
|
|
process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data");
|
|
delete process.env.AGENTHUB_MODE;
|
|
try {
|
|
require("../index").createApp();
|
|
const loaded = Object.keys(require.cache).filter((k) => k.startsWith(HUB + path.sep));
|
|
assert.deepEqual(loaded, []);
|
|
} finally {
|
|
fs.rmSync(TMP, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|