@rubytech/create-realagent 1.0.685 → 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 +23 -215
- package/dist/pinned-binaries.js +10 -41
- package/dist/uninstall.js +23 -23
- package/package.json +1 -1
- 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 +13 -10
- 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/edge.service.template +5 -4
- package/payload/server/maxy-edge.js +5 -367
- 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/ttyd.service.template +0 -30
- package/payload/server/public/assets/admin-BFmYXz1V.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,188 +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 per-brand ttyd --user unit (Task 662). The unit
|
|
1735
|
-
// file name and the loopback port are both brand-scoped so two brands
|
|
1736
|
-
// installed on the same device each run their own ttyd process without
|
|
1737
|
-
// contending for either the filesystem path or the TCP port:
|
|
1738
|
-
// `${BRAND.hostname}-ttyd.service` + 127.0.0.1:${BRAND.ttydPort}
|
|
1739
|
-
const systemdUserDir = resolve(homeDir, ".config/systemd/user");
|
|
1740
|
-
mkdirSync(systemdUserDir, { recursive: true });
|
|
1741
|
-
const ttydUnitShort = `${BRAND.hostname}-ttyd`;
|
|
1742
|
-
const ttydUnitName = `${ttydUnitShort}.service`;
|
|
1743
|
-
// Skip systemd-unit install if the ttyd binary is not in place — enabling
|
|
1744
|
-
// a unit whose ExecStart points at a missing file just churns systemd with
|
|
1745
|
-
// restart failures.
|
|
1746
|
-
if (!ttydReady) {
|
|
1747
|
-
console.error(` Skipping ${ttydUnitName} install — ttyd binary not present. Admin terminal will be unavailable until remediated.`);
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
const ttydUnitTemplate = resolve(INSTALL_DIR, "platform/templates/systemd/ttyd.service.template");
|
|
1751
|
-
const ttydUnitDest = join(systemdUserDir, ttydUnitName);
|
|
1752
|
-
try {
|
|
1753
|
-
if (existsSync(ttydUnitTemplate)) {
|
|
1754
|
-
const ttydServiceContent = readFileSync(ttydUnitTemplate, "utf-8")
|
|
1755
|
-
.replace(/__TTYD_PORT__/g, String(TTYD_PORT));
|
|
1756
|
-
writeFileSync(ttydUnitDest, ttydServiceContent);
|
|
1757
|
-
logFile(` ${ttydUnitName}: TTYD_PORT=${TTYD_PORT}`);
|
|
1758
|
-
}
|
|
1759
|
-
else {
|
|
1760
|
-
console.error(` WARNING: ttyd.service.template missing at ${ttydUnitTemplate} — admin terminal will not work`);
|
|
1761
|
-
return;
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
catch (err) {
|
|
1765
|
-
console.error(` WARNING: failed to write ${ttydUnitDest}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
1769
|
-
spawnSync("systemctl", ["--user", "enable", ttydUnitShort], { stdio: "inherit" });
|
|
1770
|
-
spawnSync("systemctl", ["--user", "restart", ttydUnitShort], { stdio: "inherit" });
|
|
1771
|
-
console.log(` ${ttydUnitName} enabled — admin terminal available on 127.0.0.1:${TTYD_PORT}`);
|
|
1772
|
-
}
|
|
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.
|
|
1773
1600
|
function installService() {
|
|
1774
|
-
log("
|
|
1601
|
+
log("11", TOTAL, `Starting ${BRAND.productName}...`);
|
|
1775
1602
|
if (!isLinux()) {
|
|
1776
1603
|
console.log(" Skipping systemd service (not Linux). Start manually with:");
|
|
1777
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`);
|
|
@@ -1904,13 +1731,12 @@ WantedBy=default.target
|
|
|
1904
1731
|
.replace(/__INSTALL_DIR__/g, INSTALL_DIR)
|
|
1905
1732
|
.replace(/__EDGE_PORT__/g, String(PORT))
|
|
1906
1733
|
.replace(/__MAXY_UI_PORT__/g, String(MAXY_UI_INTERNAL_PORT))
|
|
1907
|
-
.replace(/__TTYD_PORT__/g, String(TTYD_PORT))
|
|
1908
1734
|
.replace(/__PERSIST_DIR__/g, persistDir);
|
|
1909
1735
|
writeFileSync(join(serviceDir, edgeUnitName), edgeServiceContent);
|
|
1910
|
-
logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}
|
|
1736
|
+
logFile(` ${edgeUnitName}: EDGE_PORT=${PORT} MAXY_UI_PORT=${MAXY_UI_INTERNAL_PORT}`);
|
|
1911
1737
|
}
|
|
1912
1738
|
else {
|
|
1913
|
-
console.error(` WARNING: edge.service.template missing at ${edgeTemplatePath} —
|
|
1739
|
+
console.error(` WARNING: edge.service.template missing at ${edgeTemplatePath} — VNC transport unavailable`);
|
|
1914
1740
|
}
|
|
1915
1741
|
// Task 560: the unit declares Environment=PATH=%h/.local/bin:... so the graph
|
|
1916
1742
|
// MCP shim's spawn("uvx", ...) resolves against uv's install location. Without
|
|
@@ -2328,27 +2154,11 @@ else {
|
|
|
2328
2154
|
// Dedicated = port differs from the default shared instance
|
|
2329
2155
|
const NEO4J_DEDICATED = NEO4J_PORT !== DEFAULT_NEO4J_PORT;
|
|
2330
2156
|
// ---------------------------------------------------------------------------
|
|
2331
|
-
//
|
|
2332
|
-
//
|
|
2333
|
-
//
|
|
2334
|
-
//
|
|
2335
|
-
//
|
|
2336
|
-
// NEO4J_PORT (Task 659).
|
|
2337
|
-
// ---------------------------------------------------------------------------
|
|
2338
|
-
const DEFAULT_TTYD_PORT = 7681;
|
|
2339
|
-
let TTYD_PORT = BRAND.ttydPort ?? DEFAULT_TTYD_PORT;
|
|
2340
|
-
let TTYD_PORT_SOURCE = BRAND.ttydPort ? "brand.json" : "default";
|
|
2341
|
-
const ttydPortIdx = _args.indexOf("--ttyd-port");
|
|
2342
|
-
if (ttydPortIdx !== -1) {
|
|
2343
|
-
const raw = _args[ttydPortIdx + 1];
|
|
2344
|
-
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
2345
|
-
if (isNaN(parsed) || parsed < 1024 || parsed > 65535) {
|
|
2346
|
-
console.error(`Setup failed: --ttyd-port requires a numeric value between 1024 and 65535 (got: ${raw ?? "nothing"})`);
|
|
2347
|
-
process.exit(1);
|
|
2348
|
-
}
|
|
2349
|
-
TTYD_PORT = parsed;
|
|
2350
|
-
TTYD_PORT_SOURCE = "--ttyd-port flag";
|
|
2351
|
-
}
|
|
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.
|
|
2352
2162
|
const PKG_VERSION = JSON.parse(readFileSync(resolve(import.meta.dirname, "../package.json"), "utf-8")).version;
|
|
2353
2163
|
initLogging();
|
|
2354
2164
|
console.log("================================================================");
|
|
@@ -2361,7 +2171,6 @@ if (HOSTNAME_FLAG)
|
|
|
2361
2171
|
console.log(` Display: ${DISPLAY_MODE} (${DISPLAY_MODE_SOURCE})`);
|
|
2362
2172
|
console.log(` Embed model: ${EMBED_MODEL} (${EMBED_DIMS} dims, ${EMBED_SOURCE})`);
|
|
2363
2173
|
console.log(` Neo4j: ${NEO4J_DEDICATED ? "dedicated" : "shared"} on bolt://localhost:${NEO4J_PORT} (${NEO4J_PORT_SOURCE})`);
|
|
2364
|
-
console.log(` ttyd: 127.0.0.1:${TTYD_PORT} (${TTYD_PORT_SOURCE})`);
|
|
2365
2174
|
console.log("");
|
|
2366
2175
|
logDiagnostics("pre-flight");
|
|
2367
2176
|
logFile(` Neo4j instance: ${NEO4J_DEDICATED ? "dedicated" : "shared"} on bolt://localhost:${NEO4J_PORT}`);
|
|
@@ -2382,7 +2191,6 @@ try {
|
|
|
2382
2191
|
setupVncViewer();
|
|
2383
2192
|
setupAccount();
|
|
2384
2193
|
installTunnelScripts(); // ~/setup-tunnel.sh, ~/reset-tunnel.sh — the SKILL contract
|
|
2385
|
-
installTerminalService(); // Task 657: installs maxy-ttyd.service (ttyd + tmux) for byte-stream admin terminal
|
|
2386
2194
|
installService();
|
|
2387
2195
|
console.log("");
|
|
2388
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,13 +85,14 @@ 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` and not this brand's own edge
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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.
|
|
91
92
|
*
|
|
92
|
-
* Legacy pre-662 shared `maxy-edge.service`
|
|
93
|
-
* intentionally counted as peer evidence on uninstall: on a dual-brand
|
|
94
|
-
*
|
|
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
|
|
95
96
|
* installer — treating it as peer evidence keeps the uninstaller from
|
|
96
97
|
* purging device-wide singletons the other brand still depends on. */
|
|
97
98
|
function peerBrandPresent() {
|
|
@@ -99,9 +100,8 @@ function peerBrandPresent() {
|
|
|
99
100
|
if (!existsSync(systemdUserDir))
|
|
100
101
|
return false;
|
|
101
102
|
const thisBrandEdge = `${BRAND.hostname}-edge.service`;
|
|
102
|
-
const thisBrandTtyd = `${BRAND.hostname}-ttyd.service`;
|
|
103
103
|
try {
|
|
104
|
-
return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && f !== thisBrandEdge
|
|
104
|
+
return readdirSync(systemdUserDir).some((f) => f.endsWith(".service") && f !== BRAND.serviceName && f !== thisBrandEdge);
|
|
105
105
|
}
|
|
106
106
|
catch {
|
|
107
107
|
return false;
|
|
@@ -122,10 +122,12 @@ function stopServices() {
|
|
|
122
122
|
}
|
|
123
123
|
// Task 647: the edge service owns the public port and VNC stack. Stop it
|
|
124
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
|
-
//
|
|
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.
|
|
127
129
|
const edgeUnitShort = `${BRAND.hostname}-edge`;
|
|
128
|
-
const
|
|
130
|
+
const legacyTtydUnitShort = `${BRAND.hostname}-ttyd`;
|
|
129
131
|
try {
|
|
130
132
|
spawnSync("systemctl", ["--user", "stop", edgeUnitShort], { stdio: "pipe", timeout: 15_000 });
|
|
131
133
|
console.log(` Stopped ${edgeUnitShort}`);
|
|
@@ -134,12 +136,10 @@ function stopServices() {
|
|
|
134
136
|
console.log(` ${edgeUnitShort} not running`);
|
|
135
137
|
}
|
|
136
138
|
try {
|
|
137
|
-
spawnSync("systemctl", ["--user", "stop",
|
|
138
|
-
console.log(` Stopped ${
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
console.log(` ${ttydUnitShort} not running`);
|
|
139
|
+
spawnSync("systemctl", ["--user", "stop", legacyTtydUnitShort], { stdio: "pipe", timeout: 15_000 });
|
|
140
|
+
console.log(` Stopped ${legacyTtydUnitShort} (legacy pre-664 unit)`);
|
|
142
141
|
}
|
|
142
|
+
catch { /* not present on post-664 installs */ }
|
|
143
143
|
// Stop Neo4j — dedicated branded instance if this brand uses one, else shared.
|
|
144
144
|
// Brand isolation (Task 659): never stop `neo4j.service` when this brand runs
|
|
145
145
|
// a dedicated `neo4j-<hostname>.service` — stopping the shared instance would
|
|
@@ -571,14 +571,14 @@ function removeSystemdService() {
|
|
|
571
571
|
console.log(` Failed to remove service file: ${err instanceof Error ? err.message : String(err)}`);
|
|
572
572
|
}
|
|
573
573
|
}
|
|
574
|
-
// Task 647 + 662: remove this brand's per-brand edge
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
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
579
|
const thisBrandEdge = `${BRAND.hostname}-edge`;
|
|
580
|
-
const
|
|
581
|
-
for (const unit of [thisBrandEdge,
|
|
580
|
+
const legacyBrandTtyd = `${BRAND.hostname}-ttyd`;
|
|
581
|
+
for (const unit of [thisBrandEdge, legacyBrandTtyd]) {
|
|
582
582
|
try {
|
|
583
583
|
spawnSync("systemctl", ["--user", "disable", unit], { stdio: "pipe" });
|
|
584
584
|
}
|
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.
|
|
@@ -68,21 +68,24 @@ The logs will show which service failed to start and why. Common causes:
|
|
|
68
68
|
|
|
69
69
|
## Systemd units on each device
|
|
70
70
|
|
|
71
|
-
Each installed brand runs
|
|
71
|
+
Each installed brand runs two per-brand `--user` systemd units (Task 662 + Task 664 — unit filenames are prefixed with the brand's `hostname` so two brands on the same device never share a unit file):
|
|
72
72
|
|
|
73
|
-
- `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19199`. Restarted by the upgrade flow; short downtime is expected during steps 8→
|
|
74
|
-
- `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service
|
|
75
|
-
- `{hostname}-ttyd.service` — `ttyd` bound to `127.0.0.1:{BRAND.ttydPort}` (7681 Maxy, 7682 Real Agent), running `tmux new-session -A -s maxy-pty`. Owns the byte-stream admin terminal rendered by xterm.js in the header overlay and the Software Update modal (Task 657). Independent of the main brand service and the edge; outlives service restarts so scrollback is preserved.
|
|
73
|
+
- `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19199`. Restarted by the upgrade flow; short downtime is expected during steps 8→11 of an upgrade.
|
|
74
|
+
- `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
Upgrade and Cloudflare setup (Task 664) run as detached actions: `systemd-run --user` transient units per invocation with stdout+stderr persisted to `~/.maxy/logs/actions/<actionId>.log` and streamed to the UI via SSE. No boot-time service file exists for these.
|
|
78
77
|
|
|
79
|
-
|
|
78
|
+
If an action looks stuck, read `~/.maxy/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record.
|
|
79
|
+
|
|
80
|
+
**Pre-Task-662 / pre-Task-664 upgrade** — devices that ran an installer before Task 662 have legacy shared `maxy-edge.service` / `maxy-ttyd.service` units; devices that ran before Task 664 have per-brand `{hostname}-ttyd.service` units plus a pinned `/usr/local/bin/ttyd` binary. Neither is removed automatically — do this cleanup once per device before re-running any installer:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
|
-
systemctl --user stop maxy-edge maxy-ttyd 2>/dev/null || true
|
|
83
|
-
systemctl --user disable maxy-edge maxy-ttyd 2>/dev/null || true
|
|
83
|
+
systemctl --user stop maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
84
|
+
systemctl --user disable maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
84
85
|
rm -f ~/.config/systemd/user/maxy-edge.service \
|
|
85
|
-
~/.config/systemd/user/maxy-ttyd.service
|
|
86
|
+
~/.config/systemd/user/maxy-ttyd.service \
|
|
87
|
+
~/.config/systemd/user/realagent-ttyd.service
|
|
88
|
+
sudo rm -f /usr/local/bin/ttyd
|
|
86
89
|
systemctl --user daemon-reload
|
|
87
90
|
```
|
|
88
91
|
|
|
@@ -90,7 +93,7 @@ systemctl --user daemon-reload
|
|
|
90
93
|
|
|
91
94
|
A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.
|
|
92
95
|
|
|
93
|
-
- **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge
|
|
96
|
+
- **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge systemd unit (`maxy-edge.service` vs `realagent-edge.service`), and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688). Action runner units are transient and per-invocation, not per-brand, so no naming conflict is possible.
|
|
94
97
|
- **Shared:** both brands share the system Chromium/VNC stack, the Ollama model server, and the `cloudflared` command itself. Browser automation is serialised — one admin session at a time across both brands.
|
|
95
98
|
|
|
96
99
|
To install a second brand on a device that already runs the first, just run the other installer. No flags needed for isolation:
|