@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.
Files changed (121) hide show
  1. package/dist/__tests__/port-canonicalisation.test.js +1 -0
  2. package/dist/__tests__/snap-chromium.test.js +115 -0
  3. package/dist/index.js +201 -1
  4. package/dist/port-resolution.js +1 -1
  5. package/dist/snap-chromium.js +89 -0
  6. package/package.json +1 -1
  7. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +1 -1
  8. package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -2
  9. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
  10. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +25 -7
  11. package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +1 -1
  12. package/payload/platform/plugins/docs/references/adherence.md +1 -1
  13. package/payload/platform/plugins/docs/references/deployment.md +8 -0
  14. package/payload/platform/plugins/docs/references/plugins-guide.md +4 -2
  15. package/payload/platform/plugins/docs/references/troubleshooting.md +33 -0
  16. package/payload/platform/plugins/linkedin-import/PLUGIN.md +2 -2
  17. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +2 -2
  18. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  19. package/payload/platform/plugins/memory/references/schema-base.md +1 -1
  20. package/payload/platform/scripts/test-laptop-vnc-boot.sh +81 -0
  21. package/payload/platform/scripts/vnc.sh +42 -2
  22. package/payload/platform/templates/agents/admin/AGENTS.md +6 -4
  23. package/payload/platform/templates/agents/admin/IDENTITY.md +6 -2
  24. package/payload/platform/templates/specialists/agents/content-producer.md +2 -2
  25. package/payload/premium-plugins/real-agency/BUNDLE.md +3 -3
  26. package/payload/premium-plugins/real-agency/agents/valuer.md +10 -0
  27. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/SKILL.md +42 -0
  28. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-qualification-questions.md +16 -0
  29. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-qualification.md +59 -0
  30. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-scripts.md +63 -0
  31. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/buyer-working-scripts.md +54 -0
  32. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/feedback-collection.md +42 -0
  33. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/offer-capture.md +38 -0
  34. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/viewing-booking.md +32 -0
  35. package/payload/premium-plugins/real-agency/plugins/buyers/skills/buyer-management/references/viewing-management.md +52 -0
  36. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/SKILL.md +35 -0
  37. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/deal-saving.md +47 -0
  38. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/negotiation-deep-guide.md +64 -0
  39. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/negotiation-prep-principles.md +29 -0
  40. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/negotiation-techniques.md +42 -0
  41. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/negotiation/references/offer-presentation.md +43 -0
  42. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/SKILL.md +29 -0
  43. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/chris-voss-negotiation.md +70 -0
  44. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/phil-jones-price-words.md +40 -0
  45. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/serhant-negotiation-plus.md +55 -0
  46. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/tom-panos-commission-pricing.md +57 -0
  47. package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/sales-negotiation/references/tony-morris-questioning.md +54 -0
  48. package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.d.ts +6 -4
  49. package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
  50. package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.js +82 -10
  51. package/payload/premium-plugins/real-agency/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
  52. package/payload/premium-plugins/real-agency/plugins/loop/mcp/src/lib/loop-api.ts +111 -15
  53. package/payload/server/chunk-DIRNBH7F.js +1603 -0
  54. package/payload/server/chunk-GOO2J3X7.js +10561 -0
  55. package/payload/server/chunk-LCAFHNZR.js +10420 -0
  56. package/payload/server/chunk-X3LVMXI5.js +10578 -0
  57. package/payload/server/client-pool-7Z6YRUQT.js +34 -0
  58. package/payload/server/maxy-edge.js +2 -2
  59. package/payload/server/public/assets/{admin-DFUet1XM.js → admin-Dyl8uNxX.js} +1 -1
  60. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Bs5MjIKf.js → architectureDiagram-Q4EWVU46-BePoi8XC.js} +1 -1
  61. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BVSXiX4T.js → blockDiagram-DXYQGD6D-BkiwLTtq.js} +1 -1
  62. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DBqsWCjl.js → c4Diagram-AHTNJAMY-bpjPj2Ln.js} +1 -1
  63. package/payload/server/public/assets/channel-D3U0_a1j.js +1 -0
  64. package/payload/server/public/assets/{chunk-336JU56O-COUTB2TN.js → chunk-336JU56O-BpATJiGl.js} +2 -2
  65. package/payload/server/public/assets/{chunk-426QAEUC-zKsTsXw6.js → chunk-426QAEUC-Wz6Bpsil.js} +1 -1
  66. package/payload/server/public/assets/{chunk-4TB4RGXK-CI9i2J5g.js → chunk-4TB4RGXK-CLXL19Wd.js} +1 -1
  67. package/payload/server/public/assets/{chunk-5FUZZQ4R-DfchzZ2Y.js → chunk-5FUZZQ4R-BoTfWHuW.js} +1 -1
  68. package/payload/server/public/assets/{chunk-5PVQY5BW-_iMyxz0C.js → chunk-5PVQY5BW-RhIfPCRB.js} +1 -1
  69. package/payload/server/public/assets/{chunk-EDXVE4YY-Ddtw_ZHk.js → chunk-EDXVE4YY-utELKGQK.js} +1 -1
  70. package/payload/server/public/assets/{chunk-ENJZ2VHE-BrGMkslM.js → chunk-ENJZ2VHE-CNHjq5xK.js} +1 -1
  71. package/payload/server/public/assets/{chunk-ICPOFSXX-DHInTpuR.js → chunk-ICPOFSXX-Di63NBur.js} +1 -1
  72. package/payload/server/public/assets/{chunk-OYMX7WX6-mOQ4KZ9j.js → chunk-OYMX7WX6-BSPzqyxs.js} +1 -1
  73. package/payload/server/public/assets/{chunk-U2HBQHQK-D5vGkUe9.js → chunk-U2HBQHQK-BZnA7c4T.js} +1 -1
  74. package/payload/server/public/assets/{chunk-X2U36JSP-CnNxZbqc.js → chunk-X2U36JSP-DpQ2OA_c.js} +1 -1
  75. package/payload/server/public/assets/{chunk-YZCP3GAM-Cb7OSc_r.js → chunk-YZCP3GAM-BAkNXu0G.js} +1 -1
  76. package/payload/server/public/assets/{chunk-ZZ45TVLE-BipxpzL8.js → chunk-ZZ45TVLE-DBSm41oP.js} +1 -1
  77. package/payload/server/public/assets/classDiagram-6PBFFD2Q-6EGGLDD_.js +1 -0
  78. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DfAV4tgE.js +1 -0
  79. package/payload/server/public/assets/clone-BoV8noAi.js +1 -0
  80. package/payload/server/public/assets/{dagre-KV5264BT-B91sXKT2.js → dagre-KV5264BT-BkvWofSp.js} +1 -1
  81. package/payload/server/public/assets/{dagre-lObrgXUJ.js → dagre-nvPNAunb.js} +1 -1
  82. package/payload/server/public/assets/{diagram-5BDNPKRD-DB2Kcx9r.js → diagram-5BDNPKRD-CMEgyt4E.js} +1 -1
  83. package/payload/server/public/assets/{diagram-G4DWMVQ6-Cq1J05SW.js → diagram-G4DWMVQ6-ChorrAF0.js} +1 -1
  84. package/payload/server/public/assets/{diagram-MMDJMWI5-CwqXxUXd.js → diagram-MMDJMWI5-D_iD27po.js} +1 -1
  85. package/payload/server/public/assets/{diagram-TYMM5635-Nmk38u6a.js → diagram-TYMM5635-8qXI1ioG.js} +1 -1
  86. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D2EfAdSD.js → erDiagram-SMLLAGMA-BFjtKDSB.js} +1 -1
  87. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CS98o6Jz.js → flowDiagram-DWJPFMVM-Bpd7IL9l.js} +1 -1
  88. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-rWm6xQ8t.js → ganttDiagram-T4ZO3ILL-CwOozU85.js} +1 -1
  89. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BuwECvwx.js → gitGraphDiagram-UUTBAWPF-CcPILiC9.js} +1 -1
  90. package/payload/server/public/assets/{graphlib-BXEED8qM.js → graphlib-B_mcXEVr.js} +1 -1
  91. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CEmJMuVh.js → infoDiagram-42DDH7IO-T2sn--WJ.js} +1 -1
  92. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CAQMMx-Y.js → ishikawaDiagram-UXIWVN3A-DOP9-Q8H.js} +1 -1
  93. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-toHrBGCq.js → journeyDiagram-VCZTEJTY-DGATg0WC.js} +1 -1
  94. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DwXLkenV.js → kanban-definition-6JOO6SKY-C5PigmKg.js} +1 -1
  95. package/payload/server/public/assets/{line-BkM2KuUb.js → line-DlKKhwkO.js} +1 -1
  96. package/payload/server/public/assets/{mermaid-parser.core-CsaDWYZC.js → mermaid-parser.core-C8xGCa9p.js} +1 -1
  97. package/payload/server/public/assets/{mermaid.core-CvICILSR.js → mermaid.core-CCUSwZB_.js} +3 -3
  98. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-DQyCWpKS.js → mindmap-definition-QFDTVHPH-75k-IVhC.js} +1 -1
  99. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CVRIcK6b.js → pieDiagram-DEJITSTG-DN5RsDwZ.js} +1 -1
  100. package/payload/server/public/assets/preload-helper-qlgyTAkD.js +1 -0
  101. package/payload/server/public/assets/{public-CR_CX3K5.js → public-B_PNZUph.js} +1 -1
  102. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CnfXm2xw.js → quadrantDiagram-34T5L4WZ-Sd9x6pNe.js} +1 -1
  103. package/payload/server/public/assets/{requirementDiagram-MS252O5E-BntW6fnu.js → requirementDiagram-MS252O5E-BDgifYzj.js} +1 -1
  104. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-Bt6AfgXn.js → sankeyDiagram-XADWPNL6-BX9VULNJ.js} +1 -1
  105. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D6s0kP6H.js → sequenceDiagram-FGHM5R23-z3vMxhgE.js} +1 -1
  106. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-B9y9cOff.js → stateDiagram-FHFEXIEX-DlP0hBxF.js} +1 -1
  107. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DSddQStC.js +1 -0
  108. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DIFjQGfl.js → timeline-definition-GMOUNBTQ-DwQbhKCo.js} +1 -1
  109. package/payload/server/public/assets/{useVoiceRecorder-DWRtIHOw.js → useVoiceRecorder-fD0IWzJj.js} +3 -3
  110. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-YRVHIBU9.js → vennDiagram-DHZGUBPP-WTqmZWWa.js} +1 -1
  111. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-B20PPeAr.js → wardleyDiagram-NUSXRM2D-BUY50x5T.js} +1 -1
  112. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-BztKw58Q.js → xychartDiagram-5P7HB3ND-Btdq-fDj.js} +1 -1
  113. package/payload/server/public/index.html +3 -3
  114. package/payload/server/public/public.html +3 -3
  115. package/payload/server/server.js +6 -3
  116. package/payload/server/public/assets/channel-lEc18pSi.js +0 -1
  117. package/payload/server/public/assets/classDiagram-6PBFFD2Q-rkW6IED-.js +0 -1
  118. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CRQJXpMG.js +0 -1
  119. package/payload/server/public/assets/clone-icRAjexu.js +0 -1
  120. package/payload/server/public/assets/preload-helper-bPV_ZjF3.js +0 -1
  121. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DH4gbGCS.js +0 -1
@@ -32,6 +32,7 @@ function makeUnitFile(port, internal) {
32
32
  rfbPort: 5900,
33
33
  websockifyPort: 6080,
34
34
  cdpPort: 9222,
35
+ chromiumBin: "/usr/bin/chromium",
35
36
  });
36
37
  }
37
38
  function makeEdgeFile(edgePort) {
@@ -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.
@@ -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=/usr/bin/chromium
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.847",
3
+ "version": "1.0.850",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -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 Pi VNC chromium (`DISPLAY=${DISPLAY:-:99} /usr/bin/chromium <url> &`), 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. |
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 `/usr/bin/chromium <url>` against
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: /usr/bin/chromium is the canonical runtime binary on every
243
- # supported distro (Bookworm + Noble see vnc.sh and packages/create-maxy/
244
- # src/apt-resolve.ts). The Ubuntu transitional `chromium-browser` does not
245
- # exist on Bookworm Pis, so using the absolute path is the only spelling
246
- # that survives both distros.
247
- DISPLAY="${DISPLAY:-${BRAND_VNC_DISPLAY}}" /usr/bin/chromium "${AUTH_URL}" >/dev/null 2>&1 &
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 Pi's VNC chromium (`DISPLAY=${DISPLAY:-:99} /usr/bin/chromium <url> &`) — 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.
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.