@rubytech/create-realagent 1.0.684 → 1.0.686
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +49 -203
- package/dist/pinned-binaries.js +10 -41
- package/dist/uninstall.js +47 -19
- package/package.json +1 -1
- package/payload/platform/config/brand.json +1 -0
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +35 -9
- package/payload/platform/plugins/docs/PLUGIN.md +2 -0
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +19 -6
- package/payload/platform/plugins/docs/references/graph.md +38 -0
- package/payload/platform/plugins/docs/references/platform.md +10 -7
- package/payload/platform/plugins/docs/references/troubleshooting.md +23 -13
- package/payload/platform/scripts/vnc.sh +7 -7
- package/payload/platform/templates/systemd/{maxy-edge.service → edge.service.template} +9 -6
- package/payload/server/maxy-edge.js +7 -369
- package/payload/server/public/assets/admin-BqLtaMVu.js +352 -0
- package/payload/server/public/assets/{data-DUSyrydY.js → data-BZ7v-zug.js} +1 -1
- package/payload/server/public/assets/{file-CDJ6dUV3.js → file-CScYkZq5.js} +1 -1
- package/payload/server/public/assets/graph-tjXdtwk-.js +50 -0
- package/payload/server/public/assets/{house-CNP_bwvT.js → house-CdFRNujU.js} +1 -1
- package/payload/server/public/assets/{jsx-runtime-BFFQvkdQ.css → jsx-runtime-Og0q7dXg.css} +1 -1
- package/payload/server/public/assets/{public-sHoAccvb.js → public-CrkQJek6.js} +2 -2
- package/payload/server/public/assets/{share-2-DBcb9j6E.js → share-2-Ev-D4Lm9.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-CtSgpc95.js → useVoiceRecorder-DyDXH7EA.js} +2 -2
- package/payload/server/public/assets/{x-CTVJaC_u.js → x-D5W7ddgP.js} +1 -1
- package/payload/server/public/data.html +6 -6
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +7 -8
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +830 -258
- package/payload/platform/templates/dotfiles/.tmux.conf +0 -1
- package/payload/platform/templates/systemd/maxy-ttyd.service +0 -25
- package/payload/server/public/assets/admin-WQxJgaus.js +0 -362
- package/payload/server/public/assets/admin-kHJ-D0s7.css +0 -1
- package/payload/server/public/assets/graph-CWcYp5bE.js +0 -50
- /package/payload/server/public/assets/{jsx-runtime-BVKWELH6.js → jsx-runtime-CHqDsKlc.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, symlinkSync, unlinkSync, lstatSync, readlinkSync, accessSync, constants as fsConstants } from "node:fs";
|
|
4
4
|
import { resolve, join, dirname } from "node:path";
|
|
5
|
-
import { randomBytes
|
|
6
|
-
import { TTYD_VERSION, TTYD_SHA256_BY_ARCH, mapUnameToTtydArch, ttydDownloadUrl, } from "./pinned-binaries.js";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
7
6
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
8
7
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
9
8
|
// The bundler stamps brand.json into the payload at build time.
|
|
@@ -347,7 +346,7 @@ function installAptGroup(label, pkgs) {
|
|
|
347
346
|
// ---------------------------------------------------------------------------
|
|
348
347
|
// Installation steps
|
|
349
348
|
// ---------------------------------------------------------------------------
|
|
350
|
-
const TOTAL = "
|
|
349
|
+
const TOTAL = "11";
|
|
351
350
|
function installSystemDeps() {
|
|
352
351
|
log("1", TOTAL, "System dependencies and network...");
|
|
353
352
|
if (!isLinux()) {
|
|
@@ -366,12 +365,12 @@ function installSystemDeps() {
|
|
|
366
365
|
// assertion in vnc.sh check_window_on_display, closing the silent-fail
|
|
367
366
|
// class where PID is alive but no window is mapped on the target display.
|
|
368
367
|
const VNC_DEPS = ["tigervnc-standalone-server", "python3-websockify", "novnc", "xdg-utils", "chromium", "xterm", "xdotool"];
|
|
369
|
-
// Task
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
368
|
+
// Task 664 retired the ttyd/tmux admin terminal stack — upgrades run via
|
|
369
|
+
// the action runner (systemd-run --user transient units) and no longer
|
|
370
|
+
// need a shared tmux session. `tmux` was only required by the retired
|
|
371
|
+
// byte-stream terminal; removing it shrinks the apt footprint.
|
|
373
372
|
const WIFI_DEPS = ["hostapd", "dnsmasq"];
|
|
374
|
-
const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...
|
|
373
|
+
const ALL_APT_DEPS = [...BASE_DEPS, ...VNC_DEPS, ...WIFI_DEPS];
|
|
375
374
|
// Task 634 — verify the "deps are present" assumption with `dpkg -s` instead
|
|
376
375
|
// of asserting it (feedback_loud_failures.md). The previous silent-skip
|
|
377
376
|
// branch was benign until Task 632 added xdotool (the first new apt dep
|
|
@@ -1590,183 +1589,16 @@ function installCrons() {
|
|
|
1590
1589
|
logFile(` crontab write failed: ${write.stderr}`);
|
|
1591
1590
|
}
|
|
1592
1591
|
}
|
|
1593
|
-
// Task
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
|
|
1601
|
-
function sha256File(path) {
|
|
1602
|
-
const hash = createHash("sha256");
|
|
1603
|
-
hash.update(readFileSync(path));
|
|
1604
|
-
return hash.digest("hex");
|
|
1605
|
-
}
|
|
1606
|
-
// Provision the upstream ttyd binary into /usr/local/bin/ttyd. Degrades with
|
|
1607
|
-
// a loud warning and a copy-pasteable remediation command on any failure —
|
|
1608
|
-
// never throws. Contract: the caller (installTerminalService) uses the
|
|
1609
|
-
// presence of TTYD_INSTALL_PATH after return to decide whether to enable the
|
|
1610
|
-
// maxy-ttyd.service systemd unit. ttyd is NOT in Debian Bookworm apt, so we
|
|
1611
|
-
// own the full download / verify / install flow here.
|
|
1612
|
-
function provisionTtydBinary() {
|
|
1613
|
-
const unameRaw = spawnSync("uname", ["-m"], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
|
|
1614
|
-
const uname = (unameRaw.stdout || "").trim();
|
|
1615
|
-
const arch = mapUnameToTtydArch(uname);
|
|
1616
|
-
if (arch === null) {
|
|
1617
|
-
console.error(` WARNING: ttyd — unsupported architecture 'uname -m'='${uname}'. Admin terminal will be unavailable.`);
|
|
1618
|
-
console.error(` Remediate: install ttyd ${TTYD_VERSION} manually for your platform and place it at ${TTYD_INSTALL_PATH}, then 'sudo chmod +x ${TTYD_INSTALL_PATH}'.`);
|
|
1619
|
-
return false;
|
|
1620
|
-
}
|
|
1621
|
-
const pinnedDigest = TTYD_SHA256_BY_ARCH[arch];
|
|
1622
|
-
const url = ttydDownloadUrl(arch);
|
|
1623
|
-
const remediation = `curl -L -o /tmp/ttyd.${arch} '${url}' && sudo mv /tmp/ttyd.${arch} ${TTYD_INSTALL_PATH} && sudo chmod +x ${TTYD_INSTALL_PATH}`;
|
|
1624
|
-
// Idempotency: existing binary with matching pinned digest → skip download.
|
|
1625
|
-
if (existsSync(TTYD_INSTALL_PATH)) {
|
|
1626
|
-
try {
|
|
1627
|
-
const existingDigest = sha256File(TTYD_INSTALL_PATH);
|
|
1628
|
-
if (existingDigest === pinnedDigest) {
|
|
1629
|
-
console.log(` ttyd ${TTYD_VERSION} already installed at ${TTYD_INSTALL_PATH} (SHA256 match — skipping download)`);
|
|
1630
|
-
return true;
|
|
1631
|
-
}
|
|
1632
|
-
console.log(` ttyd at ${TTYD_INSTALL_PATH} has different digest — replacing with pinned ${TTYD_VERSION}`);
|
|
1633
|
-
}
|
|
1634
|
-
catch (err) {
|
|
1635
|
-
console.error(` WARNING: could not read existing ${TTYD_INSTALL_PATH}: ${err instanceof Error ? err.message : String(err)} — will overwrite`);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
if (!canSudo()) {
|
|
1639
|
-
console.error(` WARNING: ttyd — sudo unavailable non-interactively, cannot write ${TTYD_INSTALL_PATH}. Admin terminal will be unavailable.`);
|
|
1640
|
-
console.error(` Remediate: ${remediation}`);
|
|
1641
|
-
return false;
|
|
1642
|
-
}
|
|
1643
|
-
const tmpPath = `/tmp/ttyd.${arch}`;
|
|
1644
|
-
try {
|
|
1645
|
-
console.log(` Downloading ttyd ${TTYD_VERSION} for ${arch} from ${url}`);
|
|
1646
|
-
shellRetry("curl", ["-fL", "--retry", "3", "--retry-delay", "5", "-o", tmpPath, url], { timeout: 60_000 });
|
|
1647
|
-
}
|
|
1648
|
-
catch (err) {
|
|
1649
|
-
console.error(` WARNING: ttyd download failed: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1650
|
-
console.error(` Remediate: ${remediation}`);
|
|
1651
|
-
try {
|
|
1652
|
-
unlinkSync(tmpPath);
|
|
1653
|
-
}
|
|
1654
|
-
catch { /* nothing to clean */ }
|
|
1655
|
-
return false;
|
|
1656
|
-
}
|
|
1657
|
-
let actualDigest;
|
|
1658
|
-
try {
|
|
1659
|
-
actualDigest = sha256File(tmpPath);
|
|
1660
|
-
}
|
|
1661
|
-
catch (err) {
|
|
1662
|
-
console.error(` WARNING: ttyd — could not read downloaded file ${tmpPath}: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1663
|
-
try {
|
|
1664
|
-
unlinkSync(tmpPath);
|
|
1665
|
-
}
|
|
1666
|
-
catch { /* nothing to clean */ }
|
|
1667
|
-
return false;
|
|
1668
|
-
}
|
|
1669
|
-
if (actualDigest !== pinnedDigest) {
|
|
1670
|
-
console.error(` WARNING: ttyd SHA256 mismatch — refusing to install unverified binary.`);
|
|
1671
|
-
console.error(` expected: ${pinnedDigest}`);
|
|
1672
|
-
console.error(` actual: ${actualDigest}`);
|
|
1673
|
-
console.error(` Admin terminal will be unavailable. A later installer version may pin a newer digest.`);
|
|
1674
|
-
try {
|
|
1675
|
-
unlinkSync(tmpPath);
|
|
1676
|
-
}
|
|
1677
|
-
catch { /* nothing to clean */ }
|
|
1678
|
-
return false;
|
|
1679
|
-
}
|
|
1680
|
-
console.log(` ttyd ${TTYD_VERSION} SHA256 verified (${actualDigest.slice(0, 12)}…)`);
|
|
1681
|
-
try {
|
|
1682
|
-
console.log(` [privileged] install ttyd binary to ${TTYD_INSTALL_PATH}`);
|
|
1683
|
-
shell("mv", [tmpPath, TTYD_INSTALL_PATH], { sudo: true });
|
|
1684
|
-
console.log(` [privileged] chmod +x ${TTYD_INSTALL_PATH}`);
|
|
1685
|
-
shell("chmod", ["+x", TTYD_INSTALL_PATH], { sudo: true });
|
|
1686
|
-
}
|
|
1687
|
-
catch (err) {
|
|
1688
|
-
console.error(` WARNING: ttyd — could not install to ${TTYD_INSTALL_PATH}: ${err instanceof Error ? err.message : String(err)}. Admin terminal will be unavailable.`);
|
|
1689
|
-
console.error(` Remediate: ${remediation}`);
|
|
1690
|
-
try {
|
|
1691
|
-
unlinkSync(tmpPath);
|
|
1692
|
-
}
|
|
1693
|
-
catch { /* already moved or cleaned */ }
|
|
1694
|
-
return false;
|
|
1695
|
-
}
|
|
1696
|
-
console.log(` ttyd ${TTYD_VERSION} installed at ${TTYD_INSTALL_PATH}`);
|
|
1697
|
-
return true;
|
|
1698
|
-
}
|
|
1699
|
-
function installTerminalService() {
|
|
1700
|
-
log("11", TOTAL, "Installing admin terminal service (ttyd + tmux)...");
|
|
1701
|
-
if (!isLinux()) {
|
|
1702
|
-
console.log(" Skipping admin terminal service (not Linux). On macOS start manually:");
|
|
1703
|
-
console.log(" brew install ttyd tmux && ttyd -p 7681 -i 127.0.0.1 -W tmux new-session -A -s maxy-pty");
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
// ttyd is provisioned from upstream GitHub releases (pinned + SHA256-verified)
|
|
1707
|
-
// because Debian Bookworm's apt does NOT carry a ttyd package (Task 602).
|
|
1708
|
-
// A failure here is loud but non-fatal — the rest of the install completes
|
|
1709
|
-
// and the admin UI degrades to "terminal unavailable" per Task 603.
|
|
1710
|
-
const ttydReady = provisionTtydBinary();
|
|
1711
|
-
// Default ~/.tmux.conf — only written if the operator doesn't already have
|
|
1712
|
-
// one. `history-limit 50000` is load-bearing: a closed-tab + reopen during
|
|
1713
|
-
// an upgrade must show every line the operator missed in scrollback.
|
|
1714
|
-
const homeDir = process.env.HOME ?? "/root";
|
|
1715
|
-
const tmuxConfDest = resolve(homeDir, ".tmux.conf");
|
|
1716
|
-
if (!existsSync(tmuxConfDest)) {
|
|
1717
|
-
const tmuxConfTemplate = resolve(INSTALL_DIR, "platform/templates/dotfiles/.tmux.conf");
|
|
1718
|
-
try {
|
|
1719
|
-
if (existsSync(tmuxConfTemplate)) {
|
|
1720
|
-
writeFileSync(tmuxConfDest, readFileSync(tmuxConfTemplate, "utf-8"));
|
|
1721
|
-
console.log(` Wrote default ~/.tmux.conf (history-limit 50000)`);
|
|
1722
|
-
}
|
|
1723
|
-
else {
|
|
1724
|
-
// Fallback if the template was not in the payload for any reason —
|
|
1725
|
-
// preserves the load-bearing scrollback-size guarantee.
|
|
1726
|
-
writeFileSync(tmuxConfDest, "set -g history-limit 50000\n");
|
|
1727
|
-
console.log(` Wrote default ~/.tmux.conf (fallback — template missing)`);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
catch (err) {
|
|
1731
|
-
console.error(` WARNING: failed to write ~/.tmux.conf: ${err instanceof Error ? err.message : String(err)}`);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
// Install and enable the maxy-ttyd.service --user unit. Independent of
|
|
1735
|
-
// BRAND.serviceName — a single device runs one admin terminal regardless of
|
|
1736
|
-
// brand, because the unit binds to 127.0.0.1:7681 which only one process can
|
|
1737
|
-
// hold anyway. On a multi-brand device, the first brand's install writes the
|
|
1738
|
-
// unit and every subsequent install is a no-op (idempotent overwrite).
|
|
1739
|
-
const systemdUserDir = resolve(homeDir, ".config/systemd/user");
|
|
1740
|
-
mkdirSync(systemdUserDir, { recursive: true });
|
|
1741
|
-
// Skip systemd-unit install if the ttyd binary is not in place — enabling
|
|
1742
|
-
// a unit whose ExecStart points at a missing file just churns systemd with
|
|
1743
|
-
// restart failures.
|
|
1744
|
-
if (!ttydReady) {
|
|
1745
|
-
console.error(" Skipping maxy-ttyd.service install — ttyd binary not present. Admin terminal will be unavailable until remediated.");
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/maxy-ttyd.service");
|
|
1749
|
-
const ttydUnitDest = join(systemdUserDir, "maxy-ttyd.service");
|
|
1750
|
-
try {
|
|
1751
|
-
if (existsSync(ttydUnitTemplate)) {
|
|
1752
|
-
writeFileSync(ttydUnitDest, readFileSync(ttydUnitTemplate, "utf-8"));
|
|
1753
|
-
}
|
|
1754
|
-
else {
|
|
1755
|
-
console.error(` WARNING: maxy-ttyd.service template missing at ${ttydUnitTemplate} — admin terminal will not work`);
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
catch (err) {
|
|
1760
|
-
console.error(` WARNING: failed to write ${ttydUnitDest}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
1764
|
-
spawnSync("systemctl", ["--user", "enable", "maxy-ttyd"], { stdio: "inherit" });
|
|
1765
|
-
spawnSync("systemctl", ["--user", "restart", "maxy-ttyd"], { stdio: "inherit" });
|
|
1766
|
-
console.log(" maxy-ttyd.service enabled — admin terminal available on 127.0.0.1:7681");
|
|
1767
|
-
}
|
|
1592
|
+
// Task 664 retired the ttyd/tmux/xterm admin terminal stack. Upgrades run
|
|
1593
|
+
// via the action runner — `systemd-run --user` transient units spawned by
|
|
1594
|
+
// POST /api/admin/actions/upgrade — whose lifetime is independent of
|
|
1595
|
+
// maxy-ui, achieving the Task 647 invariant structurally rather than via
|
|
1596
|
+
// a peer edge service proxying a ttyd process. The installer no longer
|
|
1597
|
+
// provisions the ttyd binary, writes a tmux conf, or installs a ttyd
|
|
1598
|
+
// systemd unit. The corresponding admin UI (RemoteTerminal, TerminalOverlay,
|
|
1599
|
+
// xterm.js) was deleted in the same task.
|
|
1768
1600
|
function installService() {
|
|
1769
|
-
log("
|
|
1601
|
+
log("11", TOTAL, `Starting ${BRAND.productName}...`);
|
|
1770
1602
|
if (!isLinux()) {
|
|
1771
1603
|
console.log(" Skipping systemd service (not Linux). Start manually with:");
|
|
1772
1604
|
console.log(` cd ${INSTALL_DIR}/server && MAXY_PLATFORM_ROOT=${INSTALL_DIR}/platform PORT=${PORT} HOSTNAME=0.0.0.0 KEEP_ALIVE_TIMEOUT=61000 node --require ./server-init.cjs server.js`);
|
|
@@ -1847,10 +1679,12 @@ function installService() {
|
|
|
1847
1679
|
// non-default --port.
|
|
1848
1680
|
const MAXY_UI_INTERNAL_PORT = PORT + 1;
|
|
1849
1681
|
const neo4jServiceDep = NEO4J_DEDICATED ? `neo4j-${BRAND.hostname}.service` : "neo4j.service";
|
|
1682
|
+
const edgeUnitShort = `${BRAND.hostname}-edge`;
|
|
1683
|
+
const edgeUnitName = `${edgeUnitShort}.service`;
|
|
1850
1684
|
const serviceFile = `[Unit]
|
|
1851
1685
|
Description=${BRAND.productName} AI Assistant
|
|
1852
|
-
After=${neo4jServiceDep}
|
|
1853
|
-
Wants
|
|
1686
|
+
After=${neo4jServiceDep} ${edgeUnitName}
|
|
1687
|
+
Wants=${edgeUnitName}
|
|
1854
1688
|
|
|
1855
1689
|
[Service]
|
|
1856
1690
|
Type=notify
|
|
@@ -1879,23 +1713,30 @@ StandardError=append:${persistDir}/logs/server.log
|
|
|
1879
1713
|
WantedBy=default.target
|
|
1880
1714
|
`;
|
|
1881
1715
|
writeFileSync(join(serviceDir, BRAND.serviceName), serviceFile);
|
|
1882
|
-
// Task 647 —
|
|
1883
|
-
//
|
|
1884
|
-
//
|
|
1885
|
-
// from the admin terminal can restart
|
|
1886
|
-
// browser's remote terminal WebSocket.
|
|
1887
|
-
|
|
1716
|
+
// Task 647 — the edge service: always-on front door that owns the public
|
|
1717
|
+
// port (PORT) and the VNC stack (Xtigervnc + websockify). Its lifecycle is
|
|
1718
|
+
// independent of the main brand service, so an in-place upgrade triggered
|
|
1719
|
+
// from the admin terminal can restart the main brand service without
|
|
1720
|
+
// disconnecting the browser's remote terminal WebSocket.
|
|
1721
|
+
//
|
|
1722
|
+
// Task 662: the unit is per-brand so two brands on the same device each
|
|
1723
|
+
// own their own edge listener on their own EDGE_PORT — installing brand B
|
|
1724
|
+
// never rewrites brand A's unit or steals brand A's public port. Upgrades
|
|
1725
|
+
// from pre-662 installs require the manual recovery paragraph in
|
|
1726
|
+
// .docs/deployment.md before re-running this installer; auto-migration is
|
|
1727
|
+
// intentionally scoped out.
|
|
1728
|
+
const edgeTemplatePath = resolve(INSTALL_DIR, "platform/templates/systemd/edge.service.template");
|
|
1888
1729
|
if (existsSync(edgeTemplatePath)) {
|
|
1889
1730
|
const edgeServiceContent = readFileSync(edgeTemplatePath, "utf-8")
|
|
1890
1731
|
.replace(/__INSTALL_DIR__/g, INSTALL_DIR)
|
|
1891
1732
|
.replace(/__EDGE_PORT__/g, String(PORT))
|
|
1892
1733
|
.replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
|
|
1893
1734
|
.replace(/__PERSIST_DIR__/g, persistDir);
|
|
1894
|
-
writeFileSync(join(serviceDir,
|
|
1895
|
-
logFile(`
|
|
1735
|
+
writeFileSync(join(serviceDir, edgeUnitName), edgeServiceContent);
|
|
1736
|
+
logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}`);
|
|
1896
1737
|
}
|
|
1897
1738
|
else {
|
|
1898
|
-
console.error(` WARNING:
|
|
1739
|
+
console.error(` WARNING: edge.service.template missing at ${edgeTemplatePath} — VNC transport unavailable`);
|
|
1899
1740
|
}
|
|
1900
1741
|
// Task 560: the unit declares Environment=PATH=%h/.local/bin:... so the graph
|
|
1901
1742
|
// MCP shim's spawn("uvx", ...) resolves against uv's install location. Without
|
|
@@ -1952,17 +1793,17 @@ WantedBy=multi-user.target
|
|
|
1952
1793
|
catch { /* not critical */ }
|
|
1953
1794
|
// Reload and (re)start.
|
|
1954
1795
|
//
|
|
1955
|
-
// Task 647 ordering: on upgrades, the old
|
|
1956
|
-
// port (PORT). Stop it FIRST so
|
|
1957
|
-
//
|
|
1958
|
-
// Fresh installs: the stop is a no-op.
|
|
1796
|
+
// Task 647 ordering: on upgrades, the old main brand service still holds
|
|
1797
|
+
// the public port (PORT). Stop it FIRST so the edge can bind that socket;
|
|
1798
|
+
// starting the edge first would race against the old main brand service
|
|
1799
|
+
// and fail with EADDRINUSE. Fresh installs: the stop is a no-op.
|
|
1959
1800
|
const unitName = BRAND.serviceName.replace(".service", "");
|
|
1960
1801
|
spawnSync("systemctl", ["--user", "stop", unitName], { stdio: "inherit" });
|
|
1961
1802
|
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
1962
|
-
spawnSync("systemctl", ["--user", "enable",
|
|
1803
|
+
spawnSync("systemctl", ["--user", "enable", edgeUnitShort], { stdio: "inherit" });
|
|
1963
1804
|
spawnSync("systemctl", ["--user", "enable", unitName], { stdio: "inherit" });
|
|
1964
|
-
//
|
|
1965
|
-
spawnSync("systemctl", ["--user", "restart",
|
|
1805
|
+
// edge first: binds public port + starts VNC stack. Then main brand service.
|
|
1806
|
+
spawnSync("systemctl", ["--user", "restart", edgeUnitShort], { stdio: "inherit" });
|
|
1966
1807
|
spawnSync("systemctl", ["--user", "restart", unitName], { stdio: "inherit" });
|
|
1967
1808
|
// Wait for the server to come up
|
|
1968
1809
|
console.log(" Waiting for web server...");
|
|
@@ -2312,6 +2153,12 @@ else {
|
|
|
2312
2153
|
}
|
|
2313
2154
|
// Dedicated = port differs from the default shared instance
|
|
2314
2155
|
const NEO4J_DEDICATED = NEO4J_PORT !== DEFAULT_NEO4J_PORT;
|
|
2156
|
+
// ---------------------------------------------------------------------------
|
|
2157
|
+
// Task 664 removed the per-brand ttyd port — the admin terminal stack
|
|
2158
|
+
// (ttyd, tmux, xterm.js) was retired in favour of the action runner that
|
|
2159
|
+
// spawns transient `systemd-run --user` units per upgrade or setup-tunnel
|
|
2160
|
+
// invocation. No TCP listener on the device needs to be reserved for an
|
|
2161
|
+
// interactive-shell surface any more.
|
|
2315
2162
|
const PKG_VERSION = JSON.parse(readFileSync(resolve(import.meta.dirname, "../package.json"), "utf-8")).version;
|
|
2316
2163
|
initLogging();
|
|
2317
2164
|
console.log("================================================================");
|
|
@@ -2344,7 +2191,6 @@ try {
|
|
|
2344
2191
|
setupVncViewer();
|
|
2345
2192
|
setupAccount();
|
|
2346
2193
|
installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
|
|
2347
|
-
installTerminalService(); // Task 657: installs maxy-ttyd.service (ttyd + tmux) for byte-stream admin terminal
|
|
2348
2194
|
installService();
|
|
2349
2195
|
console.log("");
|
|
2350
2196
|
console.log("================================================================");
|
package/dist/pinned-binaries.js
CHANGED
|
@@ -1,43 +1,12 @@
|
|
|
1
1
|
// Pinned upstream binaries installed by the Linux provisioner.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
export const TTYD_VERSION = '1.7.7';
|
|
14
|
-
// Asset file name as published on github.com/tsl0922/ttyd/releases.
|
|
15
|
-
export const TTYD_ASSET_BY_ARCH = {
|
|
16
|
-
aarch64: 'ttyd.aarch64',
|
|
17
|
-
arm: 'ttyd.arm',
|
|
18
|
-
x86_64: 'ttyd.x86_64',
|
|
19
|
-
};
|
|
20
|
-
// SHA256 digest of each asset for TTYD_VERSION. Computed by downloading the
|
|
21
|
-
// release asset directly from GitHub and running `sha256sum`. Bump together
|
|
22
|
-
// with TTYD_VERSION — never update one without the other.
|
|
23
|
-
export const TTYD_SHA256_BY_ARCH = {
|
|
24
|
-
aarch64: 'b38acadd89d1d396a0f5649aa52c539edbad07f4bc7348b27b4f4b7219dd4165',
|
|
25
|
-
arm: '05eac1223914f18c65898d72c8d14e76bbb5435f7762c6dc7f16f041994a8109',
|
|
26
|
-
x86_64: '8a217c968aba172e0dbf3f34447218dc015bc4d5e59bf51db2f2cd12b7be4f55',
|
|
27
|
-
};
|
|
28
|
-
// Map Linux kernel `uname -m` output to a TtydArch key. Returns null for
|
|
29
|
-
// architectures we have not pinned a binary for — the caller must treat this
|
|
30
|
-
// as a hard error (no silent fallback, no "try latest").
|
|
31
|
-
export function mapUnameToTtydArch(uname) {
|
|
32
|
-
const trimmed = uname.trim();
|
|
33
|
-
if (trimmed === 'aarch64' || trimmed === 'arm64')
|
|
34
|
-
return 'aarch64';
|
|
35
|
-
if (trimmed === 'armv7l' || trimmed === 'armv6l' || trimmed === 'arm')
|
|
36
|
-
return 'arm';
|
|
37
|
-
if (trimmed === 'x86_64' || trimmed === 'amd64')
|
|
38
|
-
return 'x86_64';
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
export function ttydDownloadUrl(arch) {
|
|
42
|
-
return `https://github.com/tsl0922/ttyd/releases/download/${TTYD_VERSION}/${TTYD_ASSET_BY_ARCH[arch]}`;
|
|
43
|
-
}
|
|
3
|
+
// Task 664 removed the ttyd pin along with the embedded-terminal admin
|
|
4
|
+
// stack. If a future installer step needs to download a pinned binary
|
|
5
|
+
// (as ttyd did, because Debian Bookworm's apt does not carry it), the
|
|
6
|
+
// conventions live here: version constant, SHA256-by-arch map, and a
|
|
7
|
+
// `mapUnameToArch` helper plus a `downloadUrl` builder. SHA256 mismatch
|
|
8
|
+
// must abort with a loud error and never write bytes — no silent
|
|
9
|
+
// install of unverified content. This file is currently empty of
|
|
10
|
+
// entries; it is kept as the canonical location for when the next
|
|
11
|
+
// upstream-pinned binary is added.
|
|
12
|
+
export {};
|
package/dist/uninstall.js
CHANGED
|
@@ -85,14 +85,23 @@ export function isMaxyInstalled() {
|
|
|
85
85
|
* present — its runtime still depends on those singletons.
|
|
86
86
|
*
|
|
87
87
|
* Detection: any `.service` file in `~/.config/systemd/user/` whose name is
|
|
88
|
-
* not this brand's `BRAND.serviceName
|
|
89
|
-
*
|
|
88
|
+
* not this brand's `BRAND.serviceName` and not this brand's own edge unit
|
|
89
|
+
* (Task 662 — each brand owns its own per-brand edge unit, so that file
|
|
90
|
+
* must not register as peer evidence). Task 664 retired the per-brand
|
|
91
|
+
* ttyd unit, so it no longer needs an exclusion.
|
|
92
|
+
*
|
|
93
|
+
* Legacy pre-662 shared `maxy-edge.service` (and pre-664 `maxy-ttyd.service`)
|
|
94
|
+
* are intentionally counted as peer evidence on uninstall: on a dual-brand
|
|
95
|
+
* device the legacy shared unit belonged to whichever brand last ran the
|
|
96
|
+
* installer — treating it as peer evidence keeps the uninstaller from
|
|
97
|
+
* purging device-wide singletons the other brand still depends on. */
|
|
90
98
|
function peerBrandPresent() {
|
|
91
99
|
const systemdUserDir = resolve(HOME, ".config/systemd/user");
|
|
92
100
|
if (!existsSync(systemdUserDir))
|
|
93
101
|
return false;
|
|
102
|
+
const thisBrandEdge = `${BRAND.hostname}-edge.service`;
|
|
94
103
|
try {
|
|
95
|
-
return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName &&
|
|
104
|
+
return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && f !== thisBrandEdge);
|
|
96
105
|
}
|
|
97
106
|
catch {
|
|
98
107
|
return false;
|
|
@@ -111,15 +120,26 @@ function stopServices() {
|
|
|
111
120
|
catch {
|
|
112
121
|
console.log(` ${BRAND.serviceName} not running`);
|
|
113
122
|
}
|
|
114
|
-
// Task 647:
|
|
115
|
-
//
|
|
123
|
+
// Task 647: the edge service owns the public port and VNC stack. Stop it
|
|
124
|
+
// after the main brand service so the edge's ExecStopPost (vnc.sh stop)
|
|
125
|
+
// runs cleanly. Task 662: the edge unit is per-brand (`<hostname>-edge`).
|
|
126
|
+
// Task 664 retired the ttyd unit; pre-664 devices may still have
|
|
127
|
+
// `<hostname>-ttyd` — best-effort stop so the uninstall is clean on
|
|
128
|
+
// upgraded devices too.
|
|
129
|
+
const edgeUnitShort = `${BRAND.hostname}-edge`;
|
|
130
|
+
const legacyTtydUnitShort = `${BRAND.hostname}-ttyd`;
|
|
116
131
|
try {
|
|
117
|
-
spawnSync("systemctl", ["--user", "stop",
|
|
118
|
-
console.log(
|
|
132
|
+
spawnSync("systemctl", ["--user", "stop", edgeUnitShort], { stdio: "pipe", timeout: 15_000 });
|
|
133
|
+
console.log(` Stopped ${edgeUnitShort}`);
|
|
119
134
|
}
|
|
120
135
|
catch {
|
|
121
|
-
console.log(
|
|
136
|
+
console.log(` ${edgeUnitShort} not running`);
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
spawnSync("systemctl", ["--user", "stop", legacyTtydUnitShort], { stdio: "pipe", timeout: 15_000 });
|
|
140
|
+
console.log(` Stopped ${legacyTtydUnitShort} (legacy pre-664 unit)`);
|
|
122
141
|
}
|
|
142
|
+
catch { /* not present on post-664 installs */ }
|
|
123
143
|
// Stop Neo4j — dedicated branded instance if this brand uses one, else shared.
|
|
124
144
|
// Brand isolation (Task 659): never stop `neo4j.service` when this brand runs
|
|
125
145
|
// a dedicated `neo4j-<hostname>.service` — stopping the shared instance would
|
|
@@ -551,19 +571,27 @@ function removeSystemdService() {
|
|
|
551
571
|
console.log(` Failed to remove service file: ${err instanceof Error ? err.message : String(err)}`);
|
|
552
572
|
}
|
|
553
573
|
}
|
|
554
|
-
// Task 647: remove
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
574
|
+
// Task 647 + 662: remove this brand's per-brand edge unit alongside the
|
|
575
|
+
// main brand unit. Task 664 retired the per-brand ttyd unit, but an
|
|
576
|
+
// upgraded device may still have `<hostname>-ttyd.service` on disk from
|
|
577
|
+
// a pre-664 install — cleanup both so the uninstall leaves nothing
|
|
578
|
+
// behind on either generation.
|
|
579
|
+
const thisBrandEdge = `${BRAND.hostname}-edge`;
|
|
580
|
+
const legacyBrandTtyd = `${BRAND.hostname}-ttyd`;
|
|
581
|
+
for (const unit of [thisBrandEdge, legacyBrandTtyd]) {
|
|
561
582
|
try {
|
|
562
|
-
|
|
563
|
-
console.log(" Removed maxy-edge.service");
|
|
583
|
+
spawnSync("systemctl", ["--user", "disable", unit], { stdio: "pipe" });
|
|
564
584
|
}
|
|
565
|
-
catch
|
|
566
|
-
|
|
585
|
+
catch { /* ignore */ }
|
|
586
|
+
const unitFile = resolve(HOME, `.config/systemd/user/${unit}.service`);
|
|
587
|
+
if (existsSync(unitFile)) {
|
|
588
|
+
try {
|
|
589
|
+
rmSync(unitFile);
|
|
590
|
+
console.log(` Removed ${unit}.service`);
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
console.log(` Failed to remove ${unit}.service: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
}
|
|
567
595
|
}
|
|
568
596
|
}
|
|
569
597
|
// Reload daemon
|
package/package.json
CHANGED
|
@@ -79,16 +79,19 @@ mkdir -p "${CFG_DIR}"
|
|
|
79
79
|
if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
80
80
|
phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-:99}"
|
|
81
81
|
|
|
82
|
-
# CDP precheck —
|
|
82
|
+
# CDP precheck — if Chromium DevTools is answering, we'll drive the
|
|
83
|
+
# Authorize click for the operator. If it isn't (action runner on a
|
|
84
|
+
# device without VNC — Task 664), we fall back to the ActionLogPanel
|
|
85
|
+
# URL-to-button flow: extract the URL, emit it as `OAUTH_URL: <url>`,
|
|
86
|
+
# and wait for cert.pem to land from the operator's own browser click.
|
|
87
|
+
CDP_AVAILABLE=1
|
|
83
88
|
if ! curl -sf --max-time 2 "http://127.0.0.1:9222/json/version" > /dev/null 2>&1; then
|
|
84
|
-
phase_line setup-tunnel step=oauth-login
|
|
85
|
-
endpoint=http://127.0.0.1:9222
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
exit 1
|
|
89
|
+
phase_line setup-tunnel step=oauth-login cdp=unavailable \
|
|
90
|
+
endpoint=http://127.0.0.1:9222 mode=operator-browser-fallback
|
|
91
|
+
CDP_AVAILABLE=0
|
|
92
|
+
else
|
|
93
|
+
phase_line setup-tunnel step=oauth-login cdp=ok
|
|
90
94
|
fi
|
|
91
|
-
phase_line setup-tunnel step=oauth-login cdp=ok
|
|
92
95
|
|
|
93
96
|
URL_FILE="$(mktemp -t maxy-setup-tunnel-url.XXXXXX)"
|
|
94
97
|
LAST_LINE_FILE="$(mktemp -t maxy-setup-tunnel-last.XXXXXX)"
|
|
@@ -154,6 +157,22 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
154
157
|
AUTH_URL="$(cat "${URL_FILE}")"
|
|
155
158
|
phase_line setup-tunnel step=browser-drive url_extracted=1
|
|
156
159
|
|
|
160
|
+
# Emit the URL on stdout in a shape ActionLogPanel's regex captures.
|
|
161
|
+
# Structured-looking enough to route to the "Authorise in Cloudflare"
|
|
162
|
+
# button on Task 664's admin surface without interfering with the
|
|
163
|
+
# existing cloudflared-line extraction (which scans the same URL on
|
|
164
|
+
# stderr from the line above). Emitted before CDP so the button is
|
|
165
|
+
# available even when CDP succeeds — same-URL clicks are idempotent.
|
|
166
|
+
printf 'OAUTH_URL: %s\n' "${AUTH_URL}"
|
|
167
|
+
|
|
168
|
+
# If CDP isn't available on this device, skip the auto-click branch
|
|
169
|
+
# and fall through to the cert.pem poll. The operator's own browser
|
|
170
|
+
# click (via ActionLogPanel's button) hits the same webhook; the
|
|
171
|
+
# callback writes ~/.cloudflared/cert.pem the moment consent lands,
|
|
172
|
+
# and the existing LOGIN_TIMEOUT loop below picks it up.
|
|
173
|
+
if [ "${CDP_AVAILABLE}" -eq 0 ]; then
|
|
174
|
+
phase_line setup-tunnel step=browser-drive mode=operator-click url="${AUTH_URL}"
|
|
175
|
+
else
|
|
157
176
|
# Drive CDP. Same PUT /json/new?<url> contract as
|
|
158
177
|
# platform/ui/app/lib/cdp-client.ts (which uses encodeURIComponent on
|
|
159
178
|
# the URL). Without percent-encoding, CDP's URL parser splits on the
|
|
@@ -227,6 +246,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
227
246
|
echo " close the sign-in tab, and re-run setup — or run ~/reset-tunnel.sh first." >&2
|
|
228
247
|
exit 1
|
|
229
248
|
fi
|
|
249
|
+
fi # end CDP-available branch (Task 664)
|
|
230
250
|
|
|
231
251
|
# Wait for cert.pem to land — cloudflared writes to ~/.cloudflared/cert.pem
|
|
232
252
|
# regardless of --origincert, so watch the canonical location. Task 588
|
|
@@ -235,7 +255,13 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
235
255
|
# 2-second heartbeat inside the loop is the observability contract for
|
|
236
256
|
# this bounded wait — no form-spawned script is allowed a silent poll of
|
|
237
257
|
# more than ~2 s per criterion 3 of Task 588.
|
|
238
|
-
|
|
258
|
+
# Task 664: operator-click path (no CDP) needs a human-paced window;
|
|
259
|
+
# CDP auto-click stays on the 20s round-trip budget.
|
|
260
|
+
if [ "${CDP_AVAILABLE}" -eq 0 ]; then
|
|
261
|
+
LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-180}"
|
|
262
|
+
else
|
|
263
|
+
LOGIN_TIMEOUT="${SETUP_TUNNEL_LOGIN_TIMEOUT:-20}"
|
|
264
|
+
fi
|
|
239
265
|
LOGIN_WAIT=0
|
|
240
266
|
while [ ! -f "${HOME}/.cloudflared/cert.pem" ]; do
|
|
241
267
|
if ! kill -0 "${CF_PIPELINE_PID}" 2>/dev/null; then
|
|
@@ -17,6 +17,7 @@ Load these when users ask about Maxy features or need guidance:
|
|
|
17
17
|
- **Getting started** → `references/getting-started.md` — first run, what Maxy is, how to use it
|
|
18
18
|
- **Plugins** → `references/plugins-guide.md` — what plugins are, how to install/remove them, the marketplace
|
|
19
19
|
- **Memory** → `references/memory-guide.md` — how Maxy remembers things, what is stored, privacy
|
|
20
|
+
- **Graph view** → `references/graph.md` — the admin `/graph` page: node/edge display, zoom-adaptive Conversation labels, filter chips, trashed nodes
|
|
20
21
|
- **Contacts** → `references/contacts-guide.md` — adding, looking up, and managing contacts
|
|
21
22
|
- **Telegram** → `references/telegram-guide.md` — Telegram setup, the bot, daily use
|
|
22
23
|
- **Settings** → `references/settings.md` — output style, effort level, context mode, account preferences
|
|
@@ -38,6 +39,7 @@ Load these when performing admin tasks or diagnosing platform behaviour:
|
|
|
38
39
|
- references/getting-started.md
|
|
39
40
|
- references/plugins-guide.md
|
|
40
41
|
- references/memory-guide.md
|
|
42
|
+
- references/graph.md
|
|
41
43
|
- references/contacts-guide.md
|
|
42
44
|
- references/telegram-guide.md
|
|
43
45
|
- references/settings.md
|
|
@@ -22,7 +22,7 @@ Ask the agent to set up Cloudflare. The agent first confirms the domain is alrea
|
|
|
22
22
|
- **Proxy apex** — optional bare-domain hostname (e.g. `yourdomain.com`) that should also serve the public agent.
|
|
23
23
|
- **Admin password** — the password used to gate remote access to the admin surface.
|
|
24
24
|
|
|
25
|
-
When you submit, the `/api/admin/cloudflare/setup` endpoint runs — in strict order — `setRemotePassword`, `setup-tunnel.sh
|
|
25
|
+
When you submit, the `/api/admin/cloudflare/setup` endpoint runs — in strict order — `setRemotePassword`, launches a `cloudflare-setup` action (Task 664: `systemd-run --user` transient unit wrapping `setup-tunnel.sh <brand> <port> <hostname...>`), and registers a post-exit handler to write alias-domains for every non-`public.*` public or apex hostname (so e.g. `chat.yourdomain.com` is classified as public by `isPublicHost()`). The script runs end-to-end:
|
|
26
26
|
|
|
27
27
|
- `cloudflared tunnel login` — OAuth browser sign-in. The VNC browser opens the Cloudflare authorize page; pick the account that owns your domain, click Authorize. `cert.pem` lands.
|
|
28
28
|
- Tunnel creation under the naming convention `{brand}-{hostname}` (e.g. `maxy-neo`). Stream log emits `step=tunnel-resolve action=reused|created` once the UUID is known so the admin agent can see which tunnel the later steps will write against.
|