@rubytech/create-realagent 1.0.854 → 1.0.856

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 (40) hide show
  1. package/dist/__tests__/account-id-env.test.js +47 -0
  2. package/dist/__tests__/port-canonicalisation.test.js +1 -0
  3. package/dist/index.js +51 -17
  4. package/dist/port-resolution.js +9 -0
  5. package/package.json +1 -1
  6. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.d.ts +2 -0
  7. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.d.ts.map +1 -0
  8. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.js +55 -0
  9. package/payload/platform/lib/account-enumeration/dist/__tests__/validate-env.test.js.map +1 -0
  10. package/payload/platform/lib/account-enumeration/dist/index.d.ts +26 -0
  11. package/payload/platform/lib/account-enumeration/dist/index.d.ts.map +1 -1
  12. package/payload/platform/lib/account-enumeration/dist/index.js +13 -0
  13. package/payload/platform/lib/account-enumeration/dist/index.js.map +1 -1
  14. package/payload/platform/lib/account-enumeration/src/__tests__/validate-env.test.ts +57 -0
  15. package/payload/platform/lib/account-enumeration/src/index.ts +44 -0
  16. package/payload/platform/plugins/cloudflare/references/manual-setup.md +2 -0
  17. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +39 -10
  18. package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +112 -20
  19. package/payload/platform/plugins/docs/references/cloudflare.md +1 -0
  20. package/payload/platform/plugins/docs/references/internals.md +2 -0
  21. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  22. package/payload/server/public/assets/{Checkbox-U-H3_oQu.js → Checkbox-BySsatDO.js} +1 -1
  23. package/payload/server/public/assets/{admin-DZ8Ke7t3.js → admin-CZpefPcA.js} +2 -2
  24. package/payload/server/public/assets/data-BuuqlV4L.js +1 -0
  25. package/payload/server/public/assets/graph-CtVITeok.js +1 -0
  26. package/payload/server/public/assets/jsx-runtime-O5ef8xK8.css +1 -0
  27. package/payload/server/public/assets/{page-D_6h4ZZy.js → page-Ddc_nKh8.js} +1 -1
  28. package/payload/server/public/assets/{page-CNKytKTe.js → page-IQBQoOdT.js} +1 -1
  29. package/payload/server/public/assets/{public-DApUXgoq.js → public-BhyNH7eq.js} +1 -1
  30. package/payload/server/public/assets/{useAdminFetch-Cex4bYm7.js → useAdminFetch-B3MO55eB.js} +1 -1
  31. package/payload/server/public/assets/{useVoiceRecorder-CI8GpxfU.js → useVoiceRecorder-B_zVS4Oe.js} +1 -1
  32. package/payload/server/public/data.html +5 -5
  33. package/payload/server/public/graph.html +6 -6
  34. package/payload/server/public/index.html +8 -8
  35. package/payload/server/public/public.html +5 -5
  36. package/payload/server/server.js +32 -3
  37. package/payload/server/public/assets/data-BbczthXl.js +0 -1
  38. package/payload/server/public/assets/graph-DzK_bDyH.js +0 -1
  39. package/payload/server/public/assets/jsx-runtime-OD2WKrlG.css +0 -1
  40. /package/payload/server/public/assets/{jsx-runtime-BjkIZEse.js → jsx-runtime-DnY0498s.js} +0 -0
@@ -42,19 +42,29 @@
42
42
  // "selector drift" from "empty account" — both shapes arrive via
43
43
  // stdout).
44
44
 
45
- import { appendFileSync } from "node:fs";
45
+ import { appendFileSync, readFileSync } from "node:fs";
46
46
  import { writeFile } from "node:fs/promises";
47
47
  import { resolve } from "node:path";
48
48
  import { homedir } from "node:os";
49
49
  import { fileURLToPath } from "node:url";
50
50
 
51
- // CDP host/port default to the VNC Chromium's localhost bind; env overrides
52
- // exist solely so the vitest regression under platform/ui/__tests__ can force
53
- // a deterministic ECONNREFUSED (point at 127.0.0.1:1 RFC 6335 reserved,
54
- // guaranteed-refused on every platform) without depending on whether a dev
55
- // machine happens to have VNC Chromium running on 9222.
56
- const CDP_HOST = process.env.LIST_CF_DOMAINS_CDP_HOST ?? "127.0.0.1";
57
- const CDP_PORT = Number(process.env.LIST_CF_DOMAINS_CDP_PORT ?? 9222);
51
+ // CDP endpoint is resolved lazily inside `main()` so importing this module
52
+ // for in-process tests (`scrapeCurrentPage` / `scrapeDomains` under JSDOM)
53
+ // does not require an installed brand on disk.
54
+ //
55
+ // Task 954 source-of-truth contract:
56
+ // Runtime: brand.json `cdpPort` at `${MAXY_PLATFORM_ROOT}/config/brand.json`
57
+ // is authoritative. Wrapper exports MAXY_PLATFORM_ROOT and BRAND. Missing
58
+ // env / file / field → loud-fail with one of three named reasons. The
59
+ // pre-Task-954 silent CDP-port default made every non-Maxy brand fail
60
+ // `cdp-unreachable` because the helper hit Maxy's port instead of the
61
+ // brand's; Task 787 / NEO4J_URI sets the precedent that runtime config
62
+ // never falls back silently.
63
+ //
64
+ // Test overrides: when BOTH `LIST_CF_DOMAINS_CDP_HOST` and
65
+ // `LIST_CF_DOMAINS_CDP_PORT` are set, they win over brand.json. The
66
+ // existing ECONNREFUSED vitest forces `127.0.0.1:1` (RFC 6335-reserved)
67
+ // this way without an installed brand. Production never sets these.
58
68
 
59
69
  // Phase budgets total 30s (enforced by the shell wrapper's `timeout`).
60
70
  // Individual steps get sub-budgets so an early stall does not eat the
@@ -72,7 +82,14 @@ type Reason =
72
82
  | "not-signed-in"
73
83
  | "navigate-failed"
74
84
  | "table-wait-timeout"
75
- | "runtime-error";
85
+ | "runtime-error"
86
+ // Task 954 — config-class failures. The first three short-circuit before
87
+ // any CDP work begins; the route maps them to `field='config'` so the
88
+ // form renders a config-card naming the brand + path instead of a Retry
89
+ // button (retrying re-fires the same wrongly-resolved spawn — Task 787
90
+ // pattern).
91
+ | "brand-config-missing"
92
+ | "cdp-port-unresolved";
76
93
 
77
94
  // Sticky flag: once a stream-log write fails (EACCES, ENOENT, …), skip
78
95
  // subsequent file writes for the rest of this process. Repeated appends to an
@@ -120,11 +137,85 @@ function die(reason: Reason, detail = ""): never {
120
137
  process.exit(1);
121
138
  }
122
139
 
123
- async function cdpVersion(): Promise<void> {
140
+ interface CdpEndpoint {
141
+ host: string;
142
+ port: number;
143
+ source: "env-override" | "brand.json";
144
+ }
145
+
146
+ // Task 954 — single source of truth for the CDP endpoint. Called once from
147
+ // `main()` so the JSDOM scrape test (which imports `scrapeCurrentPage`
148
+ // directly without invoking `main`) does not need an installed brand.
149
+ function resolveCdpEndpoint(): CdpEndpoint {
150
+ const envHost = process.env.LIST_CF_DOMAINS_CDP_HOST;
151
+ const envPortRaw = process.env.LIST_CF_DOMAINS_CDP_PORT;
152
+ if (envHost !== undefined && envPortRaw !== undefined) {
153
+ const envPort = Number(envPortRaw);
154
+ if (!Number.isInteger(envPort) || envPort <= 0 || envPort > 65535) {
155
+ die(
156
+ "cdp-port-unresolved",
157
+ `LIST_CF_DOMAINS_CDP_PORT="${envPortRaw}" is not a valid integer port`,
158
+ );
159
+ }
160
+ return { host: envHost, port: envPort, source: "env-override" };
161
+ }
162
+
163
+ const brand = process.env.BRAND;
164
+ if (!brand) {
165
+ die(
166
+ "brand-config-missing",
167
+ "BRAND env var not set by wrapper — refusing to guess brand identity",
168
+ );
169
+ }
170
+ const platformRoot = process.env.MAXY_PLATFORM_ROOT;
171
+ if (!platformRoot) {
172
+ die(
173
+ "brand-config-missing",
174
+ "MAXY_PLATFORM_ROOT env var not set — wrapper must export it before spawn",
175
+ );
176
+ }
177
+ const brandPath = resolve(platformRoot, "config", "brand.json");
178
+ let raw: string;
179
+ try {
180
+ raw = readFileSync(brandPath, "utf-8");
181
+ } catch (err) {
182
+ // Distinct emission (not via `die`) so the path is named on the same line
183
+ // — config-card rendering on the route side keys off it.
184
+ logPhase(
185
+ `phase=error reason=brand-config-missing path=${brandPath} detail="${(err instanceof Error ? err.message : String(err)).replace(/"/g, "'").slice(0, 200)}"`,
186
+ );
187
+ process.exit(1);
188
+ }
189
+ let parsed: Record<string, unknown>;
190
+ try {
191
+ parsed = JSON.parse(raw) as Record<string, unknown>;
192
+ } catch (err) {
193
+ logPhase(
194
+ `phase=error reason=brand-config-missing path=${brandPath} detail="parse failed: ${(err instanceof Error ? err.message : String(err)).replace(/"/g, "'").slice(0, 160)}"`,
195
+ );
196
+ process.exit(1);
197
+ }
198
+ const cdpPort = parsed.cdpPort;
199
+ if (typeof cdpPort !== "number" || !Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65535) {
200
+ // Names the keys actually present so a schema-drift incident surfaces
201
+ // immediately ("oh, brand.json renamed `cdpPort` to `cdp_port` in r123").
202
+ const keys = Object.keys(parsed).join(",");
203
+ logPhase(
204
+ `phase=error reason=cdp-port-unresolved brand=${brand} json_keys=${keys}`,
205
+ );
206
+ process.exit(1);
207
+ }
208
+ // Host stays 127.0.0.1 on the runtime path — vnc.sh binds Chromium's CDP
209
+ // server to localhost only, by construction. brand.json carries the port,
210
+ // not the host, because the host has never varied across brands.
211
+ return { host: "127.0.0.1", port: cdpPort, source: "brand.json" };
212
+ }
213
+
214
+ async function cdpVersion(host: string, port: number): Promise<void> {
124
215
  const controller = new AbortController();
125
216
  const timer = setTimeout(() => controller.abort(), CONNECT_TIMEOUT_MS);
126
217
  try {
127
- const res = await fetch(`http://${CDP_HOST}:${CDP_PORT}/json/version`, { signal: controller.signal });
218
+ const res = await fetch(`http://${host}:${port}/json/version`, { signal: controller.signal });
128
219
  if (!res.ok) die("cdp-unreachable", `HTTP ${res.status}`);
129
220
  } catch (err) {
130
221
  die("cdp-unreachable", err instanceof Error ? err.message : String(err));
@@ -138,12 +229,12 @@ interface CdpTarget {
138
229
  webSocketDebuggerUrl: string;
139
230
  }
140
231
 
141
- async function createTarget(): Promise<CdpTarget> {
232
+ async function createTarget(host: string, port: number): Promise<CdpTarget> {
142
233
  // `PUT /json/new?<url>` returns JSON describing the target. The URL goes
143
234
  // as a query-string argument (not a ?url= key), matching Chromium's CDP
144
235
  // server. about:blank avoids a wasted navigation — the real navigate
145
236
  // happens over WS after we attach.
146
- const endpoint = `http://${CDP_HOST}:${CDP_PORT}/json/new?about:blank`;
237
+ const endpoint = `http://${host}:${port}/json/new?about:blank`;
147
238
  let res: Response;
148
239
  try {
149
240
  res = await fetch(endpoint, { method: "PUT", signal: AbortSignal.timeout(CONNECT_TIMEOUT_MS) });
@@ -158,9 +249,9 @@ async function createTarget(): Promise<CdpTarget> {
158
249
  return target as CdpTarget;
159
250
  }
160
251
 
161
- async function closeTarget(id: string): Promise<void> {
252
+ async function closeTarget(host: string, port: number, id: string): Promise<void> {
162
253
  try {
163
- await fetch(`http://${CDP_HOST}:${CDP_PORT}/json/close/${id}`, {
254
+ await fetch(`http://${host}:${port}/json/close/${id}`, {
164
255
  signal: AbortSignal.timeout(CONNECT_TIMEOUT_MS),
165
256
  });
166
257
  } catch {
@@ -602,19 +693,20 @@ async function waitForDocumentReady(cdp: CdpClient): Promise<void> {
602
693
  }
603
694
 
604
695
  async function main(): Promise<void> {
605
- logPhase(`phase=script-start cdp=${CDP_HOST}:${CDP_PORT}`);
696
+ const endpoint = resolveCdpEndpoint();
697
+ logPhase(`phase=script-start cdp=${endpoint.host}:${endpoint.port} source=${endpoint.source}`);
606
698
 
607
- await cdpVersion();
699
+ await cdpVersion(endpoint.host, endpoint.port);
608
700
  logPhase(`phase=cdp-connect result=ok`);
609
701
 
610
- const target = await createTarget();
702
+ const target = await createTarget(endpoint.host, endpoint.port);
611
703
  logPhase(`phase=target-created targetId=${target.id.slice(0, 8)}…`);
612
704
 
613
705
  let cdp: CdpClient;
614
706
  try {
615
707
  cdp = await CdpClient.connect(target.webSocketDebuggerUrl);
616
708
  } catch (err) {
617
- await closeTarget(target.id);
709
+ await closeTarget(endpoint.host, endpoint.port, target.id);
618
710
  die("ws-connect-failed", err instanceof Error ? err.message : String(err));
619
711
  }
620
712
 
@@ -643,7 +735,7 @@ async function main(): Promise<void> {
643
735
  die("runtime-error", err instanceof Error ? err.message : String(err));
644
736
  } finally {
645
737
  cdp.close();
646
- await closeTarget(target.id);
738
+ await closeTarget(endpoint.host, endpoint.port, target.id);
647
739
  }
648
740
  }
649
741
 
@@ -9,6 +9,7 @@ Each installation has its own Cloudflare account. Sign-in is OAuth in the device
9
9
  | **Product identity** (Maxy vs Real Agent) | `brand.json` (`productName`, `configDir`) — known at install. |
10
10
  | **Cloudflare account identity** | `cert.pem` from OAuth. One account per brand per device. |
11
11
  | **Domain scope** (which zones the operator can route) | Live Cloudflare dashboard at form-render time via `list-cf-domains.sh`, not `brand.json`. Brand identity has no authority over which domains the operator's CF account holds. When the scrape returns an unexpected count (e.g. 1 on a two-zone account), the stream log's per-poll `phase=dom-scrape-poll n=<k> count=<n> domains=[…]` trajectory + the on-disk HTML dump at `~/{configDir}/logs/list-cf-domains-<ts>-count<n>-<mode>-pid<pid>.html` (earlier platform fixes — written on every scrape outcome, not just empty ones) give the operator everything they need to triage the cause without re-running. |
12
+ | **CDP port the scrape attaches to** | `brand.json` (`cdpPort`, stamped at install time) — the same brand-scoped port `vnc.sh` binds Chromium to. `list-cf-domains.sh <brand>` requires the brand arg; the helper reads `${MAXY_PLATFORM_ROOT}/config/brand.json` (no silent default). Missing brand arg, missing brand.json, or missing `cdpPort` field each exit 1 with one of three named reasons (`brand-arg-missing`, `brand-config-missing`, `cdp-port-unresolved`); the route maps all three to `field=config` so the form renders a config card naming the brand instead of a Retry button — retrying would re-fire the same wrongly-resolved spawn. |
12
13
  | **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `tunnel.state`, `alias-domains.json`. |
13
14
 
14
15
  There is no token-based auth for the operator-owned path (Mode A). To switch Cloudflare accounts, run `reset-tunnel.sh` (which deletes the cert and every tunnel on the current account), then run `setup-tunnel.sh` again — `cloudflared tunnel login` inside the setup script will pick a fresh account when you sign in.
@@ -142,6 +142,8 @@ Multi-tenancy boundary. Every query is scoped to the requesting account. The `AC
142
142
 
143
143
  The read filter alone is not sufficient — it correctly *hides* alien-account nodes from every UI but does not prevent them existing. A writer that misresolves `accountId` (literal, undefined, or inferred-from-the-wrong-context) leaks nodes into the graph with no downstream symptom; the read filter then keeps them invisible indefinitely. The write-side doctrine is documented in `.docs/neo4j.md` "Account isolation invariant" — every writer that stamps `n.accountId` must verify the value against `${DATA_ROOT}/accounts/<id>/account.json` before write. The live floor is `writeNodeWithEdges` — every doctrine-primitive write is gated by an `accountId == process.env.ACCOUNT_ID` check (the spawning process validates `ACCOUNT_ID` at boot against the on-disk account set via the `account-enumeration` lib), with `[graph-write] reject reason=invalid-account-id …` as the rejection signal.
144
144
 
145
+ **Two boot-time surfaces stamp + validate the env** (added 2026-05-07). The brand systemd unit emits `Environment=ACCOUNT_ID=<uuid>` (resolved by the installer from `INSTALL_DIR/data/accounts/<uuid>/account.json`); the Hono boot path then calls `validateAccountIdEnv` against the on-disk set and emits `[graph-health] account-id-env present=true id=<8> matches-on-disk=true` on success or `[graph-health] account-id-env FATAL reason=<missing|no-on-disk-account|mismatch>` + `process.exit(1)` on failure. No fallback — a misconfigured Pi cannot silently boot.
146
+
145
147
  ---
146
148
 
147
149
  ## Query Classification
@@ -53,7 +53,7 @@ When per-group activation is `mention`, the agent fires only if the inbound mess
53
53
 
54
54
  Every `messages.upsert` event (both `notify` and `append`, both `fromMe` directions) writes a `:Message:WhatsAppMessage` row to Neo4j attached to the sessionKey-keyed `:Conversation`. A single capture site at `platform/ui/app/lib/whatsapp/manager.ts` covers inbound, outbound (Baileys echoes agent-sent messages back through `messages.upsert` with `fromMe=true`), and owner-mirror — without touching `outbound/send.ts`. `messageId` namespace is `whatsapp-live:<waName>:<remoteJid>:<msg.key.id>` where `<waName>` is the Baileys credential dirname (e.g. `default`); distinct from the `:Section:Conversation` chunks written by the source-agnostic `conversation-archive` skill — live and archive live in disjoint label spaces. Persist failures are loud (`[whatsapp-persist] FAIL …`) and never block dispatch — silent loss is the worse failure mode.
55
55
 
56
- **`accountId` contract.** `n.accountId` on every `:Conversation`, `:Person`, and `:Message:WhatsAppMessage` row stamped by this plugin is the **platform-side UUID** resolved by [`resolvePlatformAccountId()`](../../ui/app/lib/whatsapp/platform-account-id.ts) from `data/accounts/<uuid>/account.json` — NOT the Baileys credential dirname (which is only used as the `messageId`/`sessionKey` namespace token). The boot-time line `[whatsapp-persist] resolved-account-id waname=<dir> uuid=<uuid>` records the resolution. Doctrine: see `.docs/neo4j.md` "Account isolation invariant" — every writer that stamps `n.accountId` must verify the value against `${DATA_ROOT}/accounts/<id>/account.json` before write. The helper loud-throws on zero or multi accounts (Phase 0 single-account invariant), aborting the WhatsApp connection start before any write can occur. The same boot-validated identity (`process.env.ACCOUNT_ID`) backs the central live floor at [`writeNodeWithEdges`](../../lib/graph-write/src/index.ts) — any write whose `accountId` differs from the spawning process's `ACCOUNT_ID` is rejected by the gate; the WhatsApp helper is the writer-side discipline, the gate is the universal floor.
56
+ **`accountId` contract.** `n.accountId` on every `:Conversation`, `:Person`, and `:Message:WhatsAppMessage` row stamped by this plugin is the **platform-side UUID** resolved by [`resolvePlatformAccountId()`](../../ui/app/lib/whatsapp/platform-account-id.ts) from `data/accounts/<uuid>/account.json` — NOT the Baileys credential dirname (which is only used as the `messageId`/`sessionKey` namespace token). The boot-time line `[whatsapp-persist] resolved-account-id waname=<dir> uuid=<uuid>` records the resolution. Doctrine: see `.docs/neo4j.md` "Account isolation invariant" — every writer that stamps `n.accountId` must verify the value against `${DATA_ROOT}/accounts/<id>/account.json` before write. The helper loud-throws on zero or multi accounts (Phase 0 single-account invariant), aborting the WhatsApp connection start before any write can occur. The same boot-validated identity (`process.env.ACCOUNT_ID`) backs the central live floor at [`writeNodeWithEdges`](../../lib/graph-write/src/index.ts) — any write whose `accountId` differs from the spawning process's `ACCOUNT_ID` is rejected by the gate; the WhatsApp helper is the writer-side discipline, the gate is the universal floor. The env itself is stamped onto the brand systemd unit by `buildMaxyUnitFile` and re-validated at every Hono boot (`[graph-health] account-id-env present=true matches-on-disk=true`) — see `.docs/neo4j.md` "Two boot-time surfaces" (added 2026-05-07).
57
57
 
58
58
  ## Skills
59
59
 
@@ -1 +1 @@
1
- import{t as e}from"./jsx-runtime-BjkIZEse.js";var t=e();function n({checked:e,onChange:n,label:r,disabled:i}){return(0,t.jsxs)(`label`,{className:`maxy-checkbox${i?` maxy-checkbox--disabled`:``}`,children:[(0,t.jsx)(`input`,{type:`checkbox`,checked:e,onChange:e=>n(e.target.checked),disabled:i}),(0,t.jsx)(`span`,{className:`maxy-checkbox__box`,children:`✱`}),r&&(0,t.jsx)(`span`,{className:`maxy-checkbox__label`,children:r})]})}export{n as t};
1
+ import{t as e}from"./jsx-runtime-DnY0498s.js";var t=e();function n({checked:e,onChange:n,label:r,disabled:i}){return(0,t.jsxs)(`label`,{className:`maxy-checkbox${i?` maxy-checkbox--disabled`:``}`,children:[(0,t.jsx)(`input`,{type:`checkbox`,checked:e,onChange:e=>n(e.target.checked),disabled:i}),(0,t.jsx)(`span`,{className:`maxy-checkbox__box`,children:`✱`}),r&&(0,t.jsx)(`span`,{className:`maxy-checkbox__label`,children:r})]})}export{n as t};