@rubytech/create-realagent 1.0.847 → 1.0.850
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/__tests__/port-canonicalisation.test.js +1 -0
- package/dist/__tests__/snap-chromium.test.js +115 -0
- package/dist/index.js +201 -1
- package/dist/port-resolution.js +1 -1
- package/dist/snap-chromium.js +89 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +1 -1
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -2
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +25 -7
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/adherence.md +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +8 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +4 -2
- package/payload/platform/plugins/docs/references/troubleshooting.md +33 -0
- package/payload/platform/plugins/linkedin-import/PLUGIN.md +2 -2
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +2 -2
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +1 -1
- package/payload/platform/scripts/test-laptop-vnc-boot.sh +81 -0
- package/payload/platform/scripts/vnc.sh +42 -2
- package/payload/platform/templates/agents/admin/AGENTS.md +6 -4
- package/payload/platform/templates/agents/admin/IDENTITY.md +6 -2
- package/payload/platform/templates/specialists/agents/content-producer.md +2 -2
- package/payload/premium-plugins/real-agency/BUNDLE.md +3 -3
- package/payload/premium-plugins/real-agency/agents/valuer.md +10 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/SKILL.md +42 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-qualification-questions.md +16 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-qualification.md +59 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-scripts.md +63 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-working-scripts.md +54 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/feedback-collection.md +42 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/offer-capture.md +38 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/viewing-booking.md +32 -0
- package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/viewing-management.md +52 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/SKILL.md +35 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/deal-saving.md +47 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/negotiation-deep-guide.md +64 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/negotiation-prep-principles.md +29 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/negotiation-techniques.md +42 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/offer-presentation.md +43 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/SKILL.md +29 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/chris-voss-negotiation.md +70 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/phil-jones-price-words.md +40 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/serhant-negotiation-plus.md +55 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/tom-panos-commission-pricing.md +57 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/tony-morris-questioning.md +54 -0
- package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.d.ts +6 -4
- package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.js +82 -10
- package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/loop/mcp/src/lib/loop-api.ts +111 -15
- package/payload/server/chunk-DIRNBH7F.js +1603 -0
- package/payload/server/chunk-GOO2J3X7.js +10561 -0
- package/payload/server/chunk-LCAFHNZR.js +10420 -0
- package/payload/server/chunk-X3LVMXI5.js +10578 -0
- package/payload/server/client-pool-7Z6YRUQT.js +34 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/{admin-DFUet1XM.js → admin-Dyl8uNxX.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Bs5MjIKf.js → architectureDiagram-Q4EWVU46-BePoi8XC.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BVSXiX4T.js → blockDiagram-DXYQGD6D-BkiwLTtq.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DBqsWCjl.js → c4Diagram-AHTNJAMY-bpjPj2Ln.js} +1 -1
- package/payload/server/public/assets/channel-D3U0_a1j.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-COUTB2TN.js → chunk-336JU56O-BpATJiGl.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-zKsTsXw6.js → chunk-426QAEUC-Wz6Bpsil.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-CI9i2J5g.js → chunk-4TB4RGXK-CLXL19Wd.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-DfchzZ2Y.js → chunk-5FUZZQ4R-BoTfWHuW.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-_iMyxz0C.js → chunk-5PVQY5BW-RhIfPCRB.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-Ddtw_ZHk.js → chunk-EDXVE4YY-utELKGQK.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-BrGMkslM.js → chunk-ENJZ2VHE-CNHjq5xK.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-DHInTpuR.js → chunk-ICPOFSXX-Di63NBur.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-mOQ4KZ9j.js → chunk-OYMX7WX6-BSPzqyxs.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-D5vGkUe9.js → chunk-U2HBQHQK-BZnA7c4T.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CnNxZbqc.js → chunk-X2U36JSP-DpQ2OA_c.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-Cb7OSc_r.js → chunk-YZCP3GAM-BAkNXu0G.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-BipxpzL8.js → chunk-ZZ45TVLE-DBSm41oP.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-6EGGLDD_.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfAV4tgE.js +1 -0
- package/payload/server/public/assets/clone-BoV8noAi.js +1 -0
- package/payload/server/public/assets/{dagre-KV5264BT-B91sXKT2.js → dagre-KV5264BT-BkvWofSp.js} +1 -1
- package/payload/server/public/assets/{dagre-lObrgXUJ.js → dagre-nvPNAunb.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-DB2Kcx9r.js → diagram-5BDNPKRD-CMEgyt4E.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-Cq1J05SW.js → diagram-G4DWMVQ6-ChorrAF0.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CwqXxUXd.js → diagram-MMDJMWI5-D_iD27po.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-Nmk38u6a.js → diagram-TYMM5635-8qXI1ioG.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-D2EfAdSD.js → erDiagram-SMLLAGMA-BFjtKDSB.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CS98o6Jz.js → flowDiagram-DWJPFMVM-Bpd7IL9l.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-rWm6xQ8t.js → ganttDiagram-T4ZO3ILL-CwOozU85.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BuwECvwx.js → gitGraphDiagram-UUTBAWPF-CcPILiC9.js} +1 -1
- package/payload/server/public/assets/{graphlib-BXEED8qM.js → graphlib-B_mcXEVr.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CEmJMuVh.js → infoDiagram-42DDH7IO-T2sn--WJ.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CAQMMx-Y.js → ishikawaDiagram-UXIWVN3A-DOP9-Q8H.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-toHrBGCq.js → journeyDiagram-VCZTEJTY-DGATg0WC.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DwXLkenV.js → kanban-definition-6JOO6SKY-C5PigmKg.js} +1 -1
- package/payload/server/public/assets/{line-BkM2KuUb.js → line-DlKKhwkO.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-CsaDWYZC.js → mermaid-parser.core-C8xGCa9p.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-CvICILSR.js → mermaid.core-CCUSwZB_.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DQyCWpKS.js → mindmap-definition-QFDTVHPH-75k-IVhC.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CVRIcK6b.js → pieDiagram-DEJITSTG-DN5RsDwZ.js} +1 -1
- package/payload/server/public/assets/preload-helper-qlgyTAkD.js +1 -0
- package/payload/server/public/assets/{public-CR_CX3K5.js → public-B_PNZUph.js} +1 -1
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CnfXm2xw.js → quadrantDiagram-34T5L4WZ-Sd9x6pNe.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-BntW6fnu.js → requirementDiagram-MS252O5E-BDgifYzj.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-Bt6AfgXn.js → sankeyDiagram-XADWPNL6-BX9VULNJ.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D6s0kP6H.js → sequenceDiagram-FGHM5R23-z3vMxhgE.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-B9y9cOff.js → stateDiagram-FHFEXIEX-DlP0hBxF.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DSddQStC.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DIFjQGfl.js → timeline-definition-GMOUNBTQ-DwQbhKCo.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-DWRtIHOw.js → useVoiceRecorder-fD0IWzJj.js} +3 -3
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-YRVHIBU9.js → vennDiagram-DHZGUBPP-WTqmZWWa.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-B20PPeAr.js → wardleyDiagram-NUSXRM2D-BUY50x5T.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BztKw58Q.js → xychartDiagram-5P7HB3ND-Btdq-fDj.js} +1 -1
- package/payload/server/public/index.html +3 -3
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +6 -3
- package/payload/server/public/assets/channel-lEc18pSi.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-rkW6IED-.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CRQJXpMG.js +0 -1
- package/payload/server/public/assets/clone-icRAjexu.js +0 -1
- package/payload/server/public/assets/preload-helper-bPV_ZjF3.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DH4gbGCS.js +0 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Task 929 — pure-decision grid for snap-chromium.ts.
|
|
2
|
+
//
|
|
3
|
+
// Locks the resolver branches that decide whether the laptop installer must
|
|
4
|
+
// replace snap-confined Chromium with Google Chrome stable. Inputs are passed
|
|
5
|
+
// directly so no spawn/fs mocking is needed — every test exercises the pure
|
|
6
|
+
// decision with realpath fixtures captured from real devices (Ubuntu Noble
|
|
7
|
+
// laptop and Debian Bookworm Pi).
|
|
8
|
+
//
|
|
9
|
+
// Runs via Node's built-in test runner; no vitest dependency. Compiles to
|
|
10
|
+
// dist/__tests__/snap-chromium.test.js alongside the rest of the package so
|
|
11
|
+
// `node --test dist/__tests__/*.test.js` picks it up after build.
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { decideChromiumAction, isSnapConfinedPath } from "../snap-chromium.js";
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// isSnapConfinedPath — `readlink -f` outputs from real devices.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
test("isSnapConfinedPath: laptop noble realpath /usr/bin/snap is snap-confined", () => {
|
|
19
|
+
assert.equal(isSnapConfinedPath("/usr/bin/snap"), true);
|
|
20
|
+
});
|
|
21
|
+
test("isSnapConfinedPath: nested snap revision path is snap-confined", () => {
|
|
22
|
+
assert.equal(isSnapConfinedPath("/snap/chromium/2924/usr/lib/chromium/chromium"), true);
|
|
23
|
+
});
|
|
24
|
+
test("isSnapConfinedPath: snap wrapper bin path is snap-confined", () => {
|
|
25
|
+
assert.equal(isSnapConfinedPath("/snap/bin/chromium"), true);
|
|
26
|
+
});
|
|
27
|
+
test("isSnapConfinedPath: bookworm /usr/bin/chromium realpath is non-snap", () => {
|
|
28
|
+
assert.equal(isSnapConfinedPath("/usr/lib/chromium/chromium"), false);
|
|
29
|
+
});
|
|
30
|
+
test("isSnapConfinedPath: google-chrome-stable realpath is non-snap", () => {
|
|
31
|
+
assert.equal(isSnapConfinedPath("/opt/google/chrome/google-chrome"), false);
|
|
32
|
+
});
|
|
33
|
+
test("isSnapConfinedPath: empty / null guards return false", () => {
|
|
34
|
+
assert.equal(isSnapConfinedPath(""), false);
|
|
35
|
+
});
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// decideChromiumAction — the six branches.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
test("darwin: action=skip-non-linux, no resolvedPath", () => {
|
|
40
|
+
const out = decideChromiumAction({
|
|
41
|
+
platform: "darwin",
|
|
42
|
+
whichChromium: "/usr/bin/chromium",
|
|
43
|
+
realpathChromium: "/usr/bin/chromium",
|
|
44
|
+
whichGoogleChrome: null,
|
|
45
|
+
realpathGoogleChrome: null,
|
|
46
|
+
});
|
|
47
|
+
assert.equal(out.action, "skip-non-linux");
|
|
48
|
+
assert.equal(out.resolvedPath, null);
|
|
49
|
+
});
|
|
50
|
+
test("linux + non-snap chromium (Pi Bookworm): action=use chromium realpath", () => {
|
|
51
|
+
const out = decideChromiumAction({
|
|
52
|
+
platform: "linux",
|
|
53
|
+
whichChromium: "/usr/bin/chromium",
|
|
54
|
+
realpathChromium: "/usr/lib/chromium/chromium",
|
|
55
|
+
whichGoogleChrome: null,
|
|
56
|
+
realpathGoogleChrome: null,
|
|
57
|
+
});
|
|
58
|
+
assert.equal(out.action, "use");
|
|
59
|
+
assert.equal(out.resolvedPath, "/usr/bin/chromium");
|
|
60
|
+
});
|
|
61
|
+
test("linux + snap-confined chromium (laptop Noble), no google-chrome: action=install-google-chrome", () => {
|
|
62
|
+
const out = decideChromiumAction({
|
|
63
|
+
platform: "linux",
|
|
64
|
+
whichChromium: "/usr/bin/chromium",
|
|
65
|
+
realpathChromium: "/usr/bin/snap",
|
|
66
|
+
whichGoogleChrome: null,
|
|
67
|
+
realpathGoogleChrome: null,
|
|
68
|
+
});
|
|
69
|
+
assert.equal(out.action, "install-google-chrome");
|
|
70
|
+
assert.equal(out.resolvedPath, null);
|
|
71
|
+
assert.match(out.reason, /snap/);
|
|
72
|
+
});
|
|
73
|
+
test("linux + snap chromium + google-chrome already installed: action=use google-chrome", () => {
|
|
74
|
+
const out = decideChromiumAction({
|
|
75
|
+
platform: "linux",
|
|
76
|
+
whichChromium: "/usr/bin/chromium",
|
|
77
|
+
realpathChromium: "/usr/bin/snap",
|
|
78
|
+
whichGoogleChrome: "/usr/bin/google-chrome-stable",
|
|
79
|
+
realpathGoogleChrome: "/opt/google/chrome/google-chrome",
|
|
80
|
+
});
|
|
81
|
+
assert.equal(out.action, "use");
|
|
82
|
+
assert.equal(out.resolvedPath, "/usr/bin/google-chrome-stable");
|
|
83
|
+
});
|
|
84
|
+
test("linux + chromium absent + no google-chrome: action=fail", () => {
|
|
85
|
+
const out = decideChromiumAction({
|
|
86
|
+
platform: "linux",
|
|
87
|
+
whichChromium: null,
|
|
88
|
+
realpathChromium: null,
|
|
89
|
+
whichGoogleChrome: null,
|
|
90
|
+
realpathGoogleChrome: null,
|
|
91
|
+
});
|
|
92
|
+
assert.equal(out.action, "fail");
|
|
93
|
+
assert.equal(out.resolvedPath, null);
|
|
94
|
+
});
|
|
95
|
+
test("linux + chromium absent + google-chrome present: action=use google-chrome", () => {
|
|
96
|
+
const out = decideChromiumAction({
|
|
97
|
+
platform: "linux",
|
|
98
|
+
whichChromium: null,
|
|
99
|
+
realpathChromium: null,
|
|
100
|
+
whichGoogleChrome: "/usr/bin/google-chrome-stable",
|
|
101
|
+
realpathGoogleChrome: "/opt/google/chrome/google-chrome",
|
|
102
|
+
});
|
|
103
|
+
assert.equal(out.action, "use");
|
|
104
|
+
assert.equal(out.resolvedPath, "/usr/bin/google-chrome-stable");
|
|
105
|
+
});
|
|
106
|
+
test("linux + snap chromium + google-chrome installed but ALSO under /snap (defensive): action=install-google-chrome", () => {
|
|
107
|
+
const out = decideChromiumAction({
|
|
108
|
+
platform: "linux",
|
|
109
|
+
whichChromium: "/usr/bin/chromium",
|
|
110
|
+
realpathChromium: "/usr/bin/snap",
|
|
111
|
+
whichGoogleChrome: "/snap/bin/google-chrome",
|
|
112
|
+
realpathGoogleChrome: "/snap/google-chrome/current/usr/bin/google-chrome",
|
|
113
|
+
});
|
|
114
|
+
assert.equal(out.action, "install-google-chrome");
|
|
115
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, symlinkSync, unlinkSync, lstatSync, statSync, readlinkSync, accessSync, constants as fsConstants } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, symlinkSync, unlinkSync, lstatSync, statSync, readlinkSync, realpathSync, accessSync, constants as fsConstants } from "node:fs";
|
|
4
4
|
import { resolve, join, dirname } from "node:path";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { resolveInstallPortFromFs, buildMaxyUnitFile } from "./port-resolution.js";
|
|
@@ -10,6 +10,7 @@ import { requireSupportedPlatform } from "./platform-detect.js";
|
|
|
10
10
|
import { renderPlist } from "./launchd-plist.js";
|
|
11
11
|
import { installAllBrewPackages } from "./brew-install.js";
|
|
12
12
|
import { parseSwVers, isSupportedMacosVersion } from "./macos-version.js";
|
|
13
|
+
import { decideChromiumAction, isSnapConfinedPath } from "./snap-chromium.js";
|
|
13
14
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
14
15
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
15
16
|
// The bundler stamps brand.json into the payload at build time.
|
|
@@ -42,6 +43,17 @@ const KNOWN_BRAND_HOSTNAMES = ["maxy", "realagent", "maxy-2", "maxy-3", "maxy-4"
|
|
|
42
43
|
// The device's actual hostname — may differ from BRAND.hostname if the user customized it.
|
|
43
44
|
// Updated by installSystemDeps() after hostname setup; used for user-facing URLs.
|
|
44
45
|
let DEVICE_HOSTNAME = BRAND.hostname;
|
|
46
|
+
// Task 929 — absolute path to the non-snap Chromium binary chosen during
|
|
47
|
+
// installSystemDeps(). Defaults to /usr/bin/chromium so non-Linux installs
|
|
48
|
+
// (which never call ensureNonSnapChromium) and the systemd unit's
|
|
49
|
+
// PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH still see a sensible value. On Linux,
|
|
50
|
+
// installSystemDeps() always overwrites this — either /usr/bin/chromium (Pi
|
|
51
|
+
// Bookworm: real .deb) or /usr/bin/google-chrome-stable (Ubuntu Noble laptop:
|
|
52
|
+
// snap-confined chromium replaced). Read by writeChromiumBinaryPathFile()
|
|
53
|
+
// and threaded into buildMaxyUnitFile() so the chromium-binary.path config
|
|
54
|
+
// file, the systemd unit's PLAYWRIGHT env var, and vnc.sh's runtime resolver
|
|
55
|
+
// all agree on one absolute path.
|
|
56
|
+
let RESOLVED_CHROMIUM_BIN = "/usr/bin/chromium";
|
|
45
57
|
// npm flags tuned for Raspberry Pi — reduce parallelism, increase patience
|
|
46
58
|
const NPM_NET_FLAGS = [
|
|
47
59
|
"--fetch-retries=5",
|
|
@@ -397,6 +409,179 @@ function setMacosHostnameViaScutil(hostname) {
|
|
|
397
409
|
}
|
|
398
410
|
console.log(` [scutil] HostName=${hostname} LocalHostName=${hostname} ComputerName=${hostname} set ok`);
|
|
399
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Task 929 — detect snap-confined Chromium on Linux and install Google Chrome
|
|
414
|
+
* stable as the non-snap replacement. Runs after `installAptGroup("VNC stack")`
|
|
415
|
+
* inside `installSystemDeps`. Resolution rules live in `snap-chromium.ts`
|
|
416
|
+
* (pure decision); this wrapper does the spawnSync + apt-repo writes + post-
|
|
417
|
+
* install gate. Skipped on darwin (Maxy uses Playwright-managed Chromium per
|
|
418
|
+
* brew-install.ts) — RESOLVED_CHROMIUM_BIN keeps its `/usr/bin/chromium`
|
|
419
|
+
* default which is unused on darwin (no systemd unit).
|
|
420
|
+
*
|
|
421
|
+
* Detection: `command -v chromium` + `realpath`; an extra probe for
|
|
422
|
+
* `google-chrome-stable` covers re-run installs where a previous run already
|
|
423
|
+
* landed Chrome. Snap detection is the literal `snap` segment in the realpath
|
|
424
|
+
* (see isSnapConfinedPath in snap-chromium.ts) — covers the three real-world
|
|
425
|
+
* shapes (`/snap/bin/chromium`, `/snap/<rev>/usr/...`, `/usr/bin/snap` which
|
|
426
|
+
* is the snap launcher binary that `readlink -f` terminates at on Noble).
|
|
427
|
+
*
|
|
428
|
+
* Replacement: Google's signed apt repo (cryptographic verification via
|
|
429
|
+
* `signed-by=` GPG key) — the canonical pinned-deterministic source for
|
|
430
|
+
* Chrome stable. Pinning a specific Chrome version would require an out-of-
|
|
431
|
+
* band SHA-bump cadence and contradicts the apt-repo trust model.
|
|
432
|
+
*
|
|
433
|
+
* Post-install gate: spawn the resolved binary headless against a throwaway
|
|
434
|
+
* profile dir under persistDir, assert exit 0. The AppArmor denial that
|
|
435
|
+
* triggered Task 929 was on SingletonLock writes which `--headless=new` still
|
|
436
|
+
* attempts, so the headless probe fires the same EACCES path that production
|
|
437
|
+
* VNC headed launches do — closing the post-fix-sibling-audit-skipped gap.
|
|
438
|
+
*/
|
|
439
|
+
function ensureNonSnapChromium() {
|
|
440
|
+
if (process.platform !== "linux") {
|
|
441
|
+
logFile(` ensureNonSnapChromium skipped: platform=${process.platform}`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const which = (cmd) => {
|
|
445
|
+
const r = spawnSync("command", ["-v", cmd], { stdio: "pipe", encoding: "utf-8", shell: "/bin/bash", timeout: 5_000 });
|
|
446
|
+
if (r.status !== 0)
|
|
447
|
+
return null;
|
|
448
|
+
const out = (r.stdout ?? "").trim();
|
|
449
|
+
return out || null;
|
|
450
|
+
};
|
|
451
|
+
const realpath = (path) => {
|
|
452
|
+
if (!path)
|
|
453
|
+
return null;
|
|
454
|
+
try {
|
|
455
|
+
return realpathSync(path);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
const whichChromium = which("chromium");
|
|
462
|
+
const whichGoogleChrome = which("google-chrome-stable");
|
|
463
|
+
const decision = decideChromiumAction({
|
|
464
|
+
platform: "linux",
|
|
465
|
+
whichChromium,
|
|
466
|
+
realpathChromium: realpath(whichChromium),
|
|
467
|
+
whichGoogleChrome,
|
|
468
|
+
realpathGoogleChrome: realpath(whichGoogleChrome),
|
|
469
|
+
});
|
|
470
|
+
logFile(` [snap-chromium] decision: ${decision.action} reason="${decision.reason}"`);
|
|
471
|
+
if (decision.action === "fail") {
|
|
472
|
+
throw new Error(`ensureNonSnapChromium: ${decision.reason}. apt install of \`chromium\` ran in installAptGroup(VNC stack) above; if its post-check passed but no chromium binary is on PATH, the system PATH is misconfigured.`);
|
|
473
|
+
}
|
|
474
|
+
if (decision.action === "install-google-chrome") {
|
|
475
|
+
console.log(" Detected snap-confined Chromium (Task 929) — installing Google Chrome stable...");
|
|
476
|
+
logFile(` [snap-chromium] installing google-chrome-stable from Google's signed apt repo`);
|
|
477
|
+
// Fetch + dearmor the signing key, write to /etc/apt/trusted.gpg.d/. Pipe
|
|
478
|
+
// composition runs through bash -c so the curl|gpg pipeline is one
|
|
479
|
+
// privileged command rather than two separate sudo escalations.
|
|
480
|
+
console.log(" [privileged] curl + gpg --dearmor (Google Chrome signing key)");
|
|
481
|
+
shell("bash", ["-c",
|
|
482
|
+
"set -euo pipefail; " +
|
|
483
|
+
"curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | " +
|
|
484
|
+
"gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/google-chrome.gpg",
|
|
485
|
+
], { sudo: true });
|
|
486
|
+
// Add the apt source list with `signed-by=` scoping so the key only
|
|
487
|
+
// verifies google-chrome-* packages, not arbitrary repo overrides. arch
|
|
488
|
+
// pinned to amd64 — Google does not ship arm64 Chrome for Linux.
|
|
489
|
+
console.log(" [privileged] tee /etc/apt/sources.list.d/google-chrome.list");
|
|
490
|
+
shell("bash", ["-c",
|
|
491
|
+
"echo 'deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/google-chrome.gpg] " +
|
|
492
|
+
"http://dl.google.com/linux/chrome/deb/ stable main' " +
|
|
493
|
+
"> /etc/apt/sources.list.d/google-chrome.list",
|
|
494
|
+
], { sudo: true });
|
|
495
|
+
console.log(" [privileged] apt-get update");
|
|
496
|
+
shell("apt-get", ["update"], { sudo: true });
|
|
497
|
+
installAptGroup("Google Chrome stable (Task 929)", ["google-chrome-stable"]);
|
|
498
|
+
// Re-resolve after install to capture the now-installed absolute path.
|
|
499
|
+
const postInstallWhich = which("google-chrome-stable");
|
|
500
|
+
if (!postInstallWhich) {
|
|
501
|
+
throw new Error("ensureNonSnapChromium: apt install of google-chrome-stable returned 0 and dpkg -s passed, but `command -v google-chrome-stable` is empty — PATH is broken.");
|
|
502
|
+
}
|
|
503
|
+
RESOLVED_CHROMIUM_BIN = postInstallWhich;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
// action === "use" — decision.resolvedPath is the existing non-snap binary
|
|
507
|
+
// (chromium or google-chrome-stable already installed).
|
|
508
|
+
if (!decision.resolvedPath) {
|
|
509
|
+
throw new Error(`ensureNonSnapChromium: action=use returned without resolvedPath — bug in snap-chromium.ts (input: chromium=${whichChromium} google-chrome=${whichGoogleChrome})`);
|
|
510
|
+
}
|
|
511
|
+
RESOLVED_CHROMIUM_BIN = decision.resolvedPath;
|
|
512
|
+
}
|
|
513
|
+
// Defensive: never persist a snap-confined path. If realpath of the resolved
|
|
514
|
+
// binary still lands under /snap/ (e.g. apt landed a snap package by mistake
|
|
515
|
+
// on a misconfigured device), throw before writeChromiumBinaryPathFile sees
|
|
516
|
+
// it — the runtime gate in vnc.sh would refuse anyway, but failing here
|
|
517
|
+
// surfaces the contract breach with the install context still in scope.
|
|
518
|
+
const finalRealpath = realpath(RESOLVED_CHROMIUM_BIN);
|
|
519
|
+
if (isSnapConfinedPath(finalRealpath)) {
|
|
520
|
+
throw new Error(`ensureNonSnapChromium: resolved Chromium binary ${RESOLVED_CHROMIUM_BIN} realpaths to ${finalRealpath} which is under /snap/ — refusing to persist (Task 929).`);
|
|
521
|
+
}
|
|
522
|
+
console.log(` Chromium binary: ${RESOLVED_CHROMIUM_BIN} (realpath=${finalRealpath ?? "?"})`);
|
|
523
|
+
logFile(` [snap-chromium] resolved bin=${RESOLVED_CHROMIUM_BIN} realpath=${finalRealpath ?? "null"}`);
|
|
524
|
+
runChromiumPostInstallGate(RESOLVED_CHROMIUM_BIN);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Task 929 post-install gate. Spawns the resolved Chromium binary headless
|
|
528
|
+
* against a throwaway profile dir under persistDir (`~/.{brand}/chromium-
|
|
529
|
+
* gate-profile/`). The AppArmor denial that triggered the task was on
|
|
530
|
+
* SingletonLock writes which `--headless=new` still attempts, so this probe
|
|
531
|
+
* fires the same EACCES path the headed VNC stack would. Cleans up the gate
|
|
532
|
+
* profile afterward — the live profile (`chromium-profile/`) is owned by
|
|
533
|
+
* vnc.sh's start_chrome and not touched here.
|
|
534
|
+
*/
|
|
535
|
+
function runChromiumPostInstallGate(chromiumBin) {
|
|
536
|
+
const persistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
|
|
537
|
+
const gateProfileDir = join(persistDir, "chromium-gate-profile");
|
|
538
|
+
mkdirSync(gateProfileDir, { recursive: true });
|
|
539
|
+
console.log(` Verifying ${chromiumBin} can write to ${gateProfileDir} (Task 929 post-install gate)...`);
|
|
540
|
+
const r = spawnSync(chromiumBin, [
|
|
541
|
+
`--user-data-dir=${gateProfileDir}`,
|
|
542
|
+
"--headless=new",
|
|
543
|
+
"--disable-gpu",
|
|
544
|
+
"--no-sandbox",
|
|
545
|
+
"--disable-dev-shm-usage",
|
|
546
|
+
"--dump-dom",
|
|
547
|
+
"about:blank",
|
|
548
|
+
], { stdio: "pipe", encoding: "utf-8", timeout: 30_000 });
|
|
549
|
+
// Cleanup before throwing on failure so successive runs start clean.
|
|
550
|
+
try {
|
|
551
|
+
rmSync(gateProfileDir, { recursive: true, force: true });
|
|
552
|
+
}
|
|
553
|
+
catch { /* best-effort */ }
|
|
554
|
+
if (r.status !== 0) {
|
|
555
|
+
const stderr = (r.stderr ?? "").slice(-2000);
|
|
556
|
+
const eaccesHit = /Permission denied/i.test(stderr) || /EACCES/i.test(stderr);
|
|
557
|
+
const taskRef = eaccesHit
|
|
558
|
+
? "Task 929: chromium-profile not writable (likely AppArmor denial on snap-confined binary). "
|
|
559
|
+
: "";
|
|
560
|
+
throw new Error(`${taskRef}Chromium post-install gate failed: ${chromiumBin} exited ${r.status} signal=${r.signal ?? "none"}. stderr:\n${stderr}`);
|
|
561
|
+
}
|
|
562
|
+
console.log(" Chromium post-install gate passed.");
|
|
563
|
+
logFile(` [snap-chromium] post-install gate ok: ${chromiumBin} exit=0`);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Task 929 — write the resolved Chromium absolute path to
|
|
567
|
+
* `<INSTALL_DIR>/platform/config/chromium-binary.path` so vnc.sh,
|
|
568
|
+
* writeChromiumWrapper, and setup-tunnel.sh all read the same value. Called
|
|
569
|
+
* after deployPayload so the platform/config/ directory exists. Idempotent:
|
|
570
|
+
* re-running the installer with the same RESOLVED_CHROMIUM_BIN is a no-op
|
|
571
|
+
* write (writeFileSync overwrites in place).
|
|
572
|
+
*/
|
|
573
|
+
function writeChromiumBinaryPathFile() {
|
|
574
|
+
if (process.platform !== "linux") {
|
|
575
|
+
logFile(` writeChromiumBinaryPathFile skipped: platform=${process.platform}`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const configDir = resolve(INSTALL_DIR, "platform/config");
|
|
579
|
+
mkdirSync(configDir, { recursive: true });
|
|
580
|
+
const target = join(configDir, "chromium-binary.path");
|
|
581
|
+
writeFileSync(target, RESOLVED_CHROMIUM_BIN + "\n", { mode: 0o644 });
|
|
582
|
+
console.log(` Wrote ${target} → ${RESOLVED_CHROMIUM_BIN}`);
|
|
583
|
+
logFile(` [snap-chromium] wrote ${target} contents=${RESOLVED_CHROMIUM_BIN}`);
|
|
584
|
+
}
|
|
400
585
|
function installSystemDeps() {
|
|
401
586
|
log("1", TOTAL, "System dependencies and network...");
|
|
402
587
|
const platform = requireSupportedPlatform(process.platform);
|
|
@@ -475,6 +660,15 @@ function installSystemDeps() {
|
|
|
475
660
|
installAptGroup("VNC stack", VNC_DEPS);
|
|
476
661
|
installAptGroup("WiFi AP", WIFI_DEPS);
|
|
477
662
|
}
|
|
663
|
+
// Task 929 — replace snap-confined Chromium with Google Chrome stable on
|
|
664
|
+
// Linux laptops (Ubuntu Noble) where `/usr/bin/chromium` realpaths to the
|
|
665
|
+
// snap launcher. The snap AppArmor profile denies writes to hidden top-level
|
|
666
|
+
// paths under $HOME, so any write to `~/.{brand}/chromium-profile/SingletonLock`
|
|
667
|
+
// hits EACCES and Chromium never starts the CDP listener. Always sets
|
|
668
|
+
// RESOLVED_CHROMIUM_BIN even on Pi Bookworm (where the path is unchanged),
|
|
669
|
+
// so deployPayload's writeChromiumBinaryPathFile and installService's
|
|
670
|
+
// buildMaxyUnitFile both have a real absolute path to thread through.
|
|
671
|
+
ensureNonSnapChromium();
|
|
478
672
|
// Hostname resolution — four sources, in priority order:
|
|
479
673
|
// 1. --hostname flag (unconditional — the caller is the authority)
|
|
480
674
|
// 2. OS detection on same-brand upgrade (service exists → keep whatever is currently set)
|
|
@@ -2307,6 +2501,7 @@ function installService() {
|
|
|
2307
2501
|
rfbPort: RFB_PORT,
|
|
2308
2502
|
websockifyPort: WEBSOCKIFY_PORT_BRAND,
|
|
2309
2503
|
cdpPort: CDP_PORT_BRAND,
|
|
2504
|
+
chromiumBin: RESOLVED_CHROMIUM_BIN, // Task 929
|
|
2310
2505
|
});
|
|
2311
2506
|
writeFileSync(join(serviceDir, BRAND.serviceName), serviceFile);
|
|
2312
2507
|
// Task 647 — the edge service: always-on front door that owns the public
|
|
@@ -2801,6 +2996,11 @@ try {
|
|
|
2801
2996
|
installCloudflared();
|
|
2802
2997
|
installWhisperCpp();
|
|
2803
2998
|
deployPayload(); // Must happen before ensureNeo4jPassword — restores config backup
|
|
2999
|
+
// Task 929: write the resolved Chromium absolute path into the deployed
|
|
3000
|
+
// platform/config/ so vnc.sh, writeChromiumWrapper, and setup-tunnel.sh
|
|
3001
|
+
// all read the same value. Must run after deployPayload (config dir is
|
|
3002
|
+
// a payload subdirectory). Linux-only; no-op on darwin/non-linux.
|
|
3003
|
+
writeChromiumBinaryPathFile();
|
|
2804
3004
|
// Task 744: scrub plaintext neo4j passwords from any pre-fix install-*.log.
|
|
2805
3005
|
// Idempotent — re-running on already-redacted logs is a no-op. Runs after
|
|
2806
3006
|
// payload deploy so the bundled redact-install-logs.sh is on disk.
|
package/dist/port-resolution.js
CHANGED
|
@@ -99,7 +99,7 @@ Environment=DISPLAY=:${o.vncDisplay}
|
|
|
99
99
|
Environment=RFB_PORT=${o.rfbPort}
|
|
100
100
|
Environment=WEBSOCKIFY_PORT=${o.websockifyPort}
|
|
101
101
|
Environment=CDP_PORT=${o.cdpPort}
|
|
102
|
-
Environment=PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
|
102
|
+
Environment=PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=${o.chromiumBin}
|
|
103
103
|
Environment=MAXY_PLATFORM_ROOT=${o.installDir}/platform
|
|
104
104
|
Environment=CLAUDE_CONFIG_DIR=${o.persistDir}/.claude
|
|
105
105
|
Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Task 929 — pure decision: should the laptop installer replace snap-confined
|
|
2
|
+
// Chromium with Google Chrome stable, and which absolute binary path should
|
|
3
|
+
// vnc.sh / writeChromiumWrapper / setup-tunnel.sh / the Playwright env var
|
|
4
|
+
// resolve to?
|
|
5
|
+
//
|
|
6
|
+
// Mirrors apt-resolve.ts (Task 638) and port-resolution.ts (Task 666): inputs
|
|
7
|
+
// in, decision out, no I/O. The installer wraps this with the actual
|
|
8
|
+
// `command -v` and `realpath` spawnSync calls and the side-effecting
|
|
9
|
+
// apt-repo-add → apt-get install → writeFileSync sequence.
|
|
10
|
+
//
|
|
11
|
+
// Snap detection: on Ubuntu Noble (the only currently-supported snap-confined
|
|
12
|
+
// distro for Maxy/Real Agent) `readlink -f /usr/bin/chromium` resolves to
|
|
13
|
+
// `/usr/bin/snap` — the snap launcher binary, which then re-execs into
|
|
14
|
+
// `/snap/chromium/<rev>/usr/lib/chromium/chromium` under AppArmor. The
|
|
15
|
+
// AppArmor `home` interface profile excludes hidden top-level paths under
|
|
16
|
+
// `$HOME`, so any write to `~/.maxy/chromium-profile/SingletonLock` (and the
|
|
17
|
+
// equivalent realagent path) hits EACCES regardless of UID. The fix is binary
|
|
18
|
+
// replacement at install time; runtime patches are forbidden by
|
|
19
|
+
// `feedback_no_admin_upgrade_path.md`.
|
|
20
|
+
/**
|
|
21
|
+
* True iff the realpath resolves to the snap launcher (`/usr/bin/snap`) or
|
|
22
|
+
* lives anywhere under `/snap/`. Splitting on `/` and checking for a literal
|
|
23
|
+
* `snap` segment catches all three real-world shapes:
|
|
24
|
+
* - `/snap/bin/chromium` (intermediate snap wrapper symlink)
|
|
25
|
+
* - `/snap/chromium/2924/usr/...` (squashfs-mounted snap revision)
|
|
26
|
+
* - `/usr/bin/snap` (snap launcher — `readlink -f` terminus on Noble)
|
|
27
|
+
*
|
|
28
|
+
* Empty / null inputs return false so callers can pass through `realpathSync`
|
|
29
|
+
* results without an explicit guard.
|
|
30
|
+
*/
|
|
31
|
+
export function isSnapConfinedPath(realpath) {
|
|
32
|
+
if (!realpath)
|
|
33
|
+
return false;
|
|
34
|
+
return realpath.split("/").includes("snap");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Pure decision. Resolution order:
|
|
38
|
+
* 1. Non-linux platform → skip-non-linux. Darwin Maxy uses Playwright's
|
|
39
|
+
* managed Chromium (brew-install.ts), Pi-only chromium-binary.path
|
|
40
|
+
* logic does not apply. No file is written.
|
|
41
|
+
* 2. A non-snap `google-chrome-stable` is on PATH → use it. Covers re-run
|
|
42
|
+
* installs on a previously-fixed laptop and operator-supplied Chrome.
|
|
43
|
+
* 3. A non-snap `chromium` is on PATH → use it. Covers Pi Bookworm where
|
|
44
|
+
* `apt-get install chromium` lands a real .deb, not a snap symlink.
|
|
45
|
+
* 4. `chromium` is on PATH but realpath is snap-confined, and no
|
|
46
|
+
* google-chrome-stable is present → install-google-chrome. The caller
|
|
47
|
+
* adds Google's signed apt repo + installs google-chrome-stable, then
|
|
48
|
+
* writes the resolved path.
|
|
49
|
+
* 5. Neither chromium nor google-chrome-stable is on PATH → fail. The VNC
|
|
50
|
+
* apt step ran before this decision, so chromium absence here means
|
|
51
|
+
* `apt-get install chromium` did not land its binary — a contract
|
|
52
|
+
* breach the installer must surface loudly.
|
|
53
|
+
*/
|
|
54
|
+
export function decideChromiumAction(input) {
|
|
55
|
+
const { platform, whichChromium, realpathChromium, whichGoogleChrome, realpathGoogleChrome } = input;
|
|
56
|
+
if (platform !== "linux") {
|
|
57
|
+
return {
|
|
58
|
+
action: "skip-non-linux",
|
|
59
|
+
resolvedPath: null,
|
|
60
|
+
reason: `platform=${platform} — chromium-binary.path is linux-only`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (whichGoogleChrome && !isSnapConfinedPath(realpathGoogleChrome)) {
|
|
64
|
+
return {
|
|
65
|
+
action: "use",
|
|
66
|
+
resolvedPath: whichGoogleChrome,
|
|
67
|
+
reason: `google-chrome-stable already installed (realpath=${realpathGoogleChrome})`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (whichChromium && !isSnapConfinedPath(realpathChromium)) {
|
|
71
|
+
return {
|
|
72
|
+
action: "use",
|
|
73
|
+
resolvedPath: whichChromium,
|
|
74
|
+
reason: `chromium is non-snap (realpath=${realpathChromium})`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (whichChromium && isSnapConfinedPath(realpathChromium)) {
|
|
78
|
+
return {
|
|
79
|
+
action: "install-google-chrome",
|
|
80
|
+
resolvedPath: null,
|
|
81
|
+
reason: `chromium is snap-confined (realpath=${realpathChromium}) — installing google-chrome-stable per Task 929`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
action: "fail",
|
|
86
|
+
resolvedPath: null,
|
|
87
|
+
reason: "neither chromium nor google-chrome-stable is on PATH after VNC apt install — contract breach",
|
|
88
|
+
};
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -112,7 +112,7 @@ Present the admin SOUL via `render-component` with `name: "document-editor"` and
|
|
|
112
112
|
|
|
113
113
|
After the admin SOUL is written and approved, call `onboarding-complete-step` with step 6.
|
|
114
114
|
|
|
115
|
-
**Document ingestion.** If the user uploaded any documents during Step 6 (or earlier in the session), dispatch the `database-operator` subagent (via the universal `document-ingest` skill) to ingest them AFTER calling `onboarding-complete-step` — not before. Use the Agent tool with `run_in_background: true`. The critical path (SOUL file, step completion) must not depend on document ingestion succeeding. Include the document path, the document subject (typically the account owner's UserProfile or the LocalBusiness depending on the doc), and the scope in the brief. If no documents were uploaded, skip this step.
|
|
115
|
+
**Document ingestion.** If the user uploaded any documents during Step 6 (or earlier in the session), dispatch the `specialists:database-operator` subagent (via the universal `document-ingest` skill) to ingest them AFTER calling `onboarding-complete-step` — not before. Use the Agent tool with `run_in_background: true`. The critical path (SOUL file, step completion) must not depend on document ingestion succeeding. Include the document path, the document subject (typically the account owner's UserProfile or the LocalBusiness depending on the doc), and the scope in the brief. If no documents were uploaded, skip this step.
|
|
116
116
|
|
|
117
117
|
**Next steps.** After completing onboarding, let the user know that everything configured during onboarding — plugins, WiFi, output style, thinking view, timezone, and personality — can be changed at any time through conversation. Then suggest three things the user can do next — all optional and available whenever they are ready:
|
|
118
118
|
|
|
@@ -195,7 +195,7 @@ After creation, no template metadata persists in the agent's files. The resultin
|
|
|
195
195
|
9. **KNOWLEDGE.md generation** — populate from the now-tagged set plus keyword matches using the `update-knowledge` skill workflow
|
|
196
196
|
10. Write `config.json` with selected model, plugins, status "active", `liveMemory`, and `knowledgeKeywords`. This is the last gated write — placed after IDENTITY.md, SOUL.md, and KNOWLEDGE.md to prevent cascade failure if one gate stalls.
|
|
197
197
|
11. Check context budget — auto-summarise if over threshold
|
|
198
|
-
12. **Project the agent into the graph** — delegate to the `database-operator` specialist with the instruction: "Project public agent `{slug}` into the graph by POSTing to `/api/admin/agents/{slug}/project`." The route reads the on-disk files and idempotently MERGEs the `:Agent` node, the four owned `:KnowledgeDocument` projections (IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY when present, namespaced `attachmentId="agent:<slug>:<role>"`), the `HAS_*` edges, and the `USES_KNOWLEDGE` edges to every operator-tagged doc. Loud-fail: if the route returns non-2xx, surface the error to the user verbatim — the agent's files exist on disk but its graph projection is incomplete, which means the operator's /graph view will not show this agent. Re-running the projection is safe; it is the same idempotent MERGE.
|
|
198
|
+
12. **Project the agent into the graph** — delegate to the `specialists:database-operator` specialist with the instruction: "Project public agent `{slug}` into the graph by POSTing to `/api/admin/agents/{slug}/project`." The route reads the on-disk files and idempotently MERGEs the `:Agent` node, the four owned `:KnowledgeDocument` projections (IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY when present, namespaced `attachmentId="agent:<slug>:<role>"`), the `HAS_*` edges, and the `USES_KNOWLEDGE` edges to every operator-tagged doc. Loud-fail: if the route returns non-2xx, surface the error to the user verbatim — the agent's files exist on disk but its graph projection is incomplete, which means the operator's /graph view will not show this agent. Re-running the projection is safe; it is the same idempotent MERGE.
|
|
199
199
|
13. Confirm creation: "Agent created. Visitors can reach it at `/{slug}`"
|
|
200
200
|
|
|
201
201
|
### List
|
|
@@ -232,7 +232,7 @@ For knowledge scope changes:
|
|
|
232
232
|
- Allow toggling `liveMemory` on/off (update `config.json`).
|
|
233
233
|
- After changes, offer to refresh KNOWLEDGE.md using the `update-knowledge` skill.
|
|
234
234
|
|
|
235
|
-
**After every Edit operation that touches IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY/`config.json` (including `liveMemory`, `knowledgeKeywords`, or direct-tag mutations), delegate to `database-operator` to re-project: POST `/api/admin/agents/{slug}/project`.** Without re-projection the on-disk files diverge from the graph state — the operator sees stale `:Agent` properties and stale `USES_KNOWLEDGE` edges in /graph. Loud-fail: surface non-2xx errors verbatim.
|
|
235
|
+
**After every Edit operation that touches IDENTITY/SOUL/KNOWLEDGE/KNOWLEDGE-SUMMARY/`config.json` (including `liveMemory`, `knowledgeKeywords`, or direct-tag mutations), delegate to `specialists:database-operator` to re-project: POST `/api/admin/agents/{slug}/project`.** Without re-projection the on-disk files diverge from the graph state — the operator sees stale `:Agent` properties and stale `USES_KNOWLEDGE` edges in /graph. Loud-fail: surface non-2xx errors verbatim.
|
|
236
236
|
|
|
237
237
|
### Delete
|
|
238
238
|
|
|
@@ -30,7 +30,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is dr
|
|
|
30
30
|
|
|
31
31
|
| Script | Purpose |
|
|
32
32
|
|---|---|
|
|
33
|
-
| [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel resolve (operator-supplied identity), DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Required env: `STREAM_LOG_PATH`, `ACCOUNT_DIR`, AND exactly one of `TUNNEL_ID` (operator selected an existing tunnel from `/api/admin/cloudflare/tunnels`) or `TUNNEL_NAME` (operator typed a name to create) per the operator-selected-tunnel fix. The pre-fix derivation `${BRAND}-$(hostname -s)` is removed — the operator's logged-in Cloudflare account is the source of truth for which tunnel exists. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Step 1 (wrappers faithfully relay third-party CLI) spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, mechanically opens it on the
|
|
33
|
+
| [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel resolve (operator-supplied identity), DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Required env: `STREAM_LOG_PATH`, `ACCOUNT_DIR`, AND exactly one of `TUNNEL_ID` (operator selected an existing tunnel from `/api/admin/cloudflare/tunnels`) or `TUNNEL_NAME` (operator typed a name to create) per the operator-selected-tunnel fix. The pre-fix derivation `${BRAND}-$(hostname -s)` is removed — the operator's logged-in Cloudflare account is the source of truth for which tunnel exists. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Step 1 (wrappers faithfully relay third-party CLI) spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, mechanically opens it on the brand's VNC chromium using the install-time-resolved binary (`DISPLAY=${DISPLAY:-${BRAND_VNC_DISPLAY}} "${SETUP_TUNNEL_CHROMIUM_BIN}" <url> &` — `SETUP_TUNNEL_CHROMIUM_BIN` is read from `${MAXY_PLATFORM_ROOT}/config/chromium-binary.path` at script start so Ubuntu Noble laptop's snap-replaced Google Chrome is honoured by the install-time chromium resolver), then polls for `~/.cloudflared/cert.pem` while the operator clicks the zone row + Authorize on the VNC. 180 s budget with a 2-second `step=oauth-login result=awaiting-cert` heartbeat. No CDP auto-click, no DOM matcher. |
|
|
34
34
|
| [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. No polling blocks — every long-wait is bounded by `cloudflared`'s network round-trip, so no heartbeat contract applies. |
|
|
35
35
|
|
|
36
36
|
### Skills
|
|
@@ -80,6 +80,23 @@ if [ -n "${SETUP_TUNNEL_BRAND_JSON}" ] && command -v jq >/dev/null 2>&1; then
|
|
|
80
80
|
fi
|
|
81
81
|
fi
|
|
82
82
|
|
|
83
|
+
# Task 929 — read the install-time-resolved non-snap Chromium binary path so
|
|
84
|
+
# the OAuth-URL spawn below uses the same binary vnc.sh's start_chrome runs.
|
|
85
|
+
# Hardcoded `/usr/bin/chromium` would re-introduce the snap-AppArmor failure
|
|
86
|
+
# on Ubuntu Noble laptop. The installer writes this file under platform/config/
|
|
87
|
+
# during installSystemDeps; the spawn at step=browser-spawn fails loud if the
|
|
88
|
+
# file is absent rather than silently falling back to /usr/bin/chromium.
|
|
89
|
+
SETUP_TUNNEL_CHROMIUM_BIN=""
|
|
90
|
+
if [ -n "${MAXY_PLATFORM_ROOT:-}" ] && [ -r "${MAXY_PLATFORM_ROOT}/config/chromium-binary.path" ]; then
|
|
91
|
+
SETUP_TUNNEL_CHROMIUM_BIN="$(head -n1 "${MAXY_PLATFORM_ROOT}/config/chromium-binary.path" | tr -d '[:space:]')"
|
|
92
|
+
fi
|
|
93
|
+
if [ -z "${SETUP_TUNNEL_CHROMIUM_BIN}" ] || [ ! -x "${SETUP_TUNNEL_CHROMIUM_BIN}" ]; then
|
|
94
|
+
echo "ERROR: setup-tunnel.sh cannot resolve a non-snap Chromium binary." >&2
|
|
95
|
+
echo " Expected: \${MAXY_PLATFORM_ROOT}/config/chromium-binary.path → executable absolute path." >&2
|
|
96
|
+
echo " Re-run the installer to provision Chromium (Task 929)." >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
83
100
|
# --------------------------------------------------------------------------
|
|
84
101
|
# Step 1: OAuth login. Corresponds to runbook Step 1.
|
|
85
102
|
#
|
|
@@ -231,7 +248,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
231
248
|
|
|
232
249
|
# Mechanically open the URL on the Pi VNC chromium (Task 858). Chromium
|
|
233
250
|
# is already running on this brand's ${BRAND_VNC_DISPLAY} with CDP enabled
|
|
234
|
-
# (vnc.sh start_chrome at boot); invoking
|
|
251
|
+
# (vnc.sh start_chrome at boot); invoking the resolved binary <url> against
|
|
235
252
|
# a running instance IPCs the URL into it as a new tab. Fire-and-forget —
|
|
236
253
|
# the spawn is intentionally NOT tracked in cleanup_oauth's EXIT trap
|
|
237
254
|
# because it is a sibling open, not a child of cloudflared, and an
|
|
@@ -239,12 +256,13 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
239
256
|
# optimistic xdg-open, which does not reliably target the brand's VNC
|
|
240
257
|
# display in this environment.
|
|
241
258
|
#
|
|
242
|
-
# Binary path:
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
#
|
|
247
|
-
|
|
259
|
+
# Binary path: SETUP_TUNNEL_CHROMIUM_BIN is read at startup from
|
|
260
|
+
# ${MAXY_PLATFORM_ROOT}/config/chromium-binary.path — `/usr/bin/chromium`
|
|
261
|
+
# on Pi Bookworm and `/usr/bin/google-chrome-stable` on Ubuntu Noble laptop
|
|
262
|
+
# where the system chromium is snap-confined (Task 929). Hardcoding
|
|
263
|
+
# `/usr/bin/chromium` here would re-introduce the AppArmor SingletonLock
|
|
264
|
+
# failure on the laptop.
|
|
265
|
+
DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}" "${SETUP_TUNNEL_CHROMIUM_BIN}" "${AUTH_URL}" >/dev/null 2>&1 &
|
|
248
266
|
phase_line setup-tunnel step=browser-spawn result=ok \
|
|
249
267
|
display="${DISPLAY:-${BRAND_VNC_DISPLAY}}" url_extracted=1
|
|
250
268
|
phase_line setup-tunnel step=browser-drive mode=operator-click url="${AUTH_URL}"
|
|
@@ -22,7 +22,7 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
|
|
|
22
22
|
|
|
23
23
|
Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit — all in one invocation. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
|
|
24
24
|
|
|
25
|
-
Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and the instant the URL surfaces, mechanically opens it on the
|
|
25
|
+
Step 1's OAuth flow is a state machine over two observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`) and the OAuth-default cert path (`~/.cloudflared/cert.pem`). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the wrapper spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, and the instant the URL surfaces, mechanically opens it on the brand's VNC chromium using the install-time-resolved binary (`DISPLAY=${DISPLAY:-${BRAND_VNC_DISPLAY}} "${SETUP_TUNNEL_CHROMIUM_BIN}" <url> &` — `SETUP_TUNNEL_CHROMIUM_BIN` is read from `${MAXY_PLATFORM_ROOT}/config/chromium-binary.path` so Ubuntu Noble laptop's snap-replaced Google Chrome is honoured per Task 929) — emitting `step=browser-spawn result=ok` and `step=browser-drive mode=operator-click`. The operator clicks the zone row + Authorize on the VNC; cloudflared's callback writes `~/.cloudflared/cert.pem`; the wrapper's cert-poll (180 s budget) picks it up and `mv`s it to the brand-scoped path. There is no CDP auto-click, no DOM matcher, no consent-page driver — the wrapper's job is to faithfully relay `cloudflared tunnel login`, never to layer automation on top.
|
|
26
26
|
|
|
27
27
|
### How inputs reach the script
|
|
28
28
|
|
|
@@ -93,6 +93,6 @@ The constraint is computed once per turn at the top of `invokeAgent` and frozen
|
|
|
93
93
|
|
|
94
94
|
## Limits and deferrals
|
|
95
95
|
|
|
96
|
-
v1 covers the admin agent only. Specialist subagents (`personal-assistant`, `project-manager`, `research-assistant`, `content-producer`, `database-operator`) do not receive their own ledger injection yet — their `.md` templates load via `--plugin-dir` and have no TS-side assembly site. Follow-up task filed.
|
|
96
|
+
v1 covers the admin agent only. Specialist subagents (`specialists:personal-assistant`, `specialists:project-manager`, `specialists:research-assistant`, `specialists:content-producer`, `specialists:database-operator`) do not receive their own ledger injection yet — their `.md` templates load via `--plugin-dir` and have no TS-side assembly site. Follow-up task filed.
|
|
97
97
|
|
|
98
98
|
No cross-agent rule inheritance, no user-visible correction-ack signal, no blocking-critic retry loop in v1 — each is a separate follow-up task. See [`.docs/agents.md`](../../../../.docs/agents.md) § Adherence Fidelity for the full deferral list with task numbers.
|
|
@@ -107,6 +107,14 @@ sudo rm -f /usr/local/bin/ttyd
|
|
|
107
107
|
systemctl --user daemon-reload
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
## Linux laptops: snap-confined Chromium replacement
|
|
111
|
+
|
|
112
|
+
On Ubuntu 24.04 (Noble) the system Chromium binary at `/usr/bin/chromium` is a symlink into the snap. Snap's AppArmor profile denies writes to hidden directories under your home folder, so the per-brand Chromium profile at `~/.{brand}/chromium-profile/` is unwritable and the VNC browser never starts. Pi installs (Debian Bookworm) are unaffected because Bookworm ships a real `.deb` chromium.
|
|
113
|
+
|
|
114
|
+
The installer detects this case during system-dependency setup and replaces the snap binary with Google Chrome stable, installed from Google's signed apt repo. The chosen binary's absolute path is recorded in `<INSTALL_DIR>/platform/config/chromium-binary.path` and used by every Chromium call site (the VNC service, the in-page Chromium wrapper, the Cloudflare tunnel OAuth spawn, and the Playwright server). If you ever see `chromium-binary.path missing` or `Chromium ... resolves to ... which is snap-confined` in `~/.{brand}/logs/vnc-boot.log`, re-run the installer to re-provision.
|
|
115
|
+
|
|
116
|
+
The post-install acceptance gate at `platform/scripts/test-laptop-vnc-boot.sh` runs four checks: the configured Chromium realpath is non-snap, the path is absolute and executable, the per-brand CDP port returns Chromium version JSON, and the VNC boot log ends with `VNC + browser stack running` with no preceding `Chromium failed to start`. The gate runs automatically at the end of every install on Linux; manual invocation is `MAXY_PLATFORM_ROOT=<install-dir>/platform <install-dir>/platform/scripts/test-laptop-vnc-boot.sh`.
|
|
117
|
+
|
|
110
118
|
## Running multiple brands on one device
|
|
111
119
|
|
|
112
120
|
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.
|