@rubytech/create-realagent 1.0.858 → 1.0.859

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.
@@ -0,0 +1,53 @@
1
+ // Task 959 — structural gate against the recurrence-class
2
+ // silent-fallback-masks-root-cause violation. The four runtime sites that
3
+ // previously substituted `9222 + offset` for a missing brand.json.cdpPort
4
+ // (paths.ts, admin/mcp/index.ts, vnc.sh, test-laptop-vnc-boot.sh) plus the
5
+ // installer-side brand stamp at packages/create-maxy/src/index.ts have all
6
+ // been swept to loud-fail. This test asserts the three greps from criterion
7
+ // 2 of the task brief return zero matches; reintroducing any silent fallback
8
+ // fails CI immediately.
9
+ //
10
+ // Compiles to dist/__tests__/cdp-port-no-silent-fallback.test.js and runs
11
+ // via `node --test 'dist/__tests__/*.test.js'` per package.json's test
12
+ // script. Greps run via `git grep` rooted at the repo top-level (resolved
13
+ // relative to dist/__tests__/'s known offset) so the test is invariant to
14
+ // the caller's cwd.
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { spawnSync } from "node:child_process";
18
+ import { resolve } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+ const SELF_DIR = fileURLToPath(new URL(".", import.meta.url));
21
+ // dist/__tests__/ → repo root is four levels up
22
+ // (packages/create-maxy/dist/__tests__ → packages/create-maxy → packages → repo).
23
+ const REPO_ROOT = resolve(SELF_DIR, "..", "..", "..", "..");
24
+ function grepReturns(pattern, includes) {
25
+ const args = ["-RnE", pattern];
26
+ for (const inc of includes)
27
+ args.push(`--include=${inc}`);
28
+ args.push("platform/", "packages/");
29
+ // Exclude this test file (it embeds the patterns as string literals,
30
+ // which would otherwise self-match) and exclude the gitignored payload
31
+ // mirror generated by `npm run bundle`.
32
+ args.push("--exclude-dir=payload");
33
+ args.push("--exclude=cdp-port-no-silent-fallback.test.*");
34
+ const out = spawnSync("grep", args, { cwd: REPO_ROOT, encoding: "utf8" });
35
+ // grep exit 1 = no matches (success for this test); exit 0 = matches found
36
+ // (failure); exit 2 = grep error (test infrastructure fault).
37
+ if (out.status !== 0 && out.status !== 1) {
38
+ throw new Error(`grep error (status=${out.status}): ${out.stderr}`);
39
+ }
40
+ return (out.stdout ?? "").trim();
41
+ }
42
+ test("Task 959 grep 1: no `9222 + offset` arithmetic in source files", () => {
43
+ const matches = grepReturns("9222\\s*\\+\\s*\\w*offset", ["*.ts", "*.tsx", "*.sh"]);
44
+ assert.equal(matches, "", `silent-fallback regression — '9222 + offset' arithmetic reintroduced:\n${matches}`);
45
+ });
46
+ test("Task 959 grep 2: no jq `cdpPort // 9222` fallback in shell or TS", () => {
47
+ const matches = grepReturns("cdpPort\\s*//\\s*9222", ["*.sh", "*.ts"]);
48
+ assert.equal(matches, "", `silent-fallback regression — jq cdpPort // 9222 fallback reintroduced:\n${matches}`);
49
+ });
50
+ test("Task 959 grep 3: no `?? 9222` or `|| 9222` nullish-coalesce in TS", () => {
51
+ const matches = grepReturns("\\?\\?\\s*9222|\\|\\|\\s*9222", ["*.ts"]);
52
+ assert.equal(matches, "", `silent-fallback regression — '?? 9222' or '|| 9222' coalesce reintroduced:\n${matches}`);
53
+ });
package/dist/index.js CHANGED
@@ -1931,12 +1931,36 @@ function setupVncViewer() {
1931
1931
  } catch (e) { /* swallow */ }
1932
1932
  }
1933
1933
 
1934
+ // Layer-6 beacon (Task 958): emit event=rfb-connected and
1935
+ // event=rfb-error to the operator-grep lifecycle endpoint so
1936
+ // server.log shows the noVNC outcome alongside [setup-tunnel] /
1937
+ // [cloudflare-setup] / [device-url:click] / [http] / [websockify].
1938
+ // Same-origin POST works whether the iframe is parented by the
1939
+ // React BrowserViewer or by vnc-popout.html.
1940
+ function reportBrowserViewerEvent(event, fields) {
1941
+ try {
1942
+ var body = JSON.stringify(Object.assign(
1943
+ { event: event, surface: 'iframe' },
1944
+ fields || {},
1945
+ ));
1946
+ fetch('/api/admin/browser-iframe/event', {
1947
+ method: 'POST',
1948
+ credentials: 'same-origin',
1949
+ headers: { 'Content-Type': 'application/json' },
1950
+ body: body,
1951
+ keepalive: true,
1952
+ }).catch(function() { /* swallow */ });
1953
+ } catch (e) { /* swallow */ }
1954
+ }
1955
+ var connectStartedAt = 0;
1956
+
1934
1957
  function connect() {
1935
1958
  status.classList.remove('hidden');
1936
1959
  status.querySelector('.status-spinner').style.display = '';
1937
1960
  status.querySelector('div:nth-child(2)').textContent =
1938
1961
  retryCount > 0 ? 'Reconnecting… (' + retryCount + ')' : 'Connecting to browser…';
1939
1962
  status.querySelector('.status-reason').textContent = '';
1963
+ connectStartedAt = Date.now();
1940
1964
 
1941
1965
  const rfb = new RFB(screen, wsUrl);
1942
1966
  rfb.scaleViewport = true;
@@ -1947,6 +1971,9 @@ function setupVncViewer() {
1947
1971
  rfb.addEventListener('connect', () => {
1948
1972
  status.classList.add('hidden');
1949
1973
  retryCount = 0;
1974
+ reportBrowserViewerEvent('rfb-connected', {
1975
+ durationMs: Date.now() - connectStartedAt,
1976
+ });
1950
1977
  });
1951
1978
 
1952
1979
  rfb.addEventListener('disconnect', (e) => {
@@ -1954,6 +1981,11 @@ function setupVncViewer() {
1954
1981
  const detail = e.detail || {};
1955
1982
  const reason = detail.reason || (detail.clean === false ? 'Connection refused' : '');
1956
1983
  reportClientEvent('disconnect', reason || (detail.clean === false ? 'unclean-close' : 'normal'));
1984
+ reportBrowserViewerEvent('rfb-error', {
1985
+ durationMs: Date.now() - connectStartedAt,
1986
+ errorCode: detail.clean === false ? 'unclean-close' : 'normal',
1987
+ message: reason || '',
1988
+ });
1957
1989
  const reasonEl = status.querySelector('.status-reason');
1958
1990
  if (retryCount < MAX_RETRIES) {
1959
1991
  retryCount++;
@@ -2458,22 +2490,26 @@ function installService() {
2458
2490
  // Per-brand X display (Task 553). Same value used for the edge unit's
2459
2491
  // DISPLAY env (stamped via __VNC_DISPLAY__ a few lines down) so the main
2460
2492
  // brand service and the edge service agree on which display Chromium runs.
2461
- const VNC_DISPLAY = BRAND.vncDisplay ?? 99;
2462
- // Task 924 derive the three additional brand-scoped ports from
2463
- // `vncDisplay` when brand.json omits them. Operator-set explicit values
2464
- // in brand.json win; the derivation rule is the deterministic fallback.
2465
- // Mirrors paths.ts and vnc.sh so all four sites compute the same numbers
2466
- // regardless of which one reads brand.json first.
2467
- const VNC_OFFSET = VNC_DISPLAY - 99;
2468
- // Parenthesise the deterministic derivation. Operator-precedence already
2469
- // groups (offset + base) ahead of nullish-coalesce, so this is purely a
2470
- // textual change for readability and to satisfy Task 954's grep audit
2471
- // (the runtime path was the real silent default; these are install-time
2472
- // deterministic fallbacks but the audit is a single regex over the
2473
- // codebase).
2474
- const RFB_PORT = BRAND.rfbPort ?? (5900 + VNC_OFFSET);
2475
- const WEBSOCKIFY_PORT_BRAND = BRAND.websockifyPort ?? (6080 + VNC_OFFSET);
2476
- const CDP_PORT_BRAND = BRAND.cdpPort ?? (9222 + VNC_OFFSET);
2493
+ // Task 924 + 959 — brand.json (BRAND) is the single source of truth for
2494
+ // these fields at install time. The vncDisplay-derived offset rule lives
2495
+ // in the brand-creation tooling; at this point in the installer BRAND
2496
+ // already represents a parsed, validated brand manifest, and any missing
2497
+ // field is a brand-publish defect. Loud-fail rather than silently
2498
+ // substituting an offset (silent-fallback-masks-root-cause recurrence).
2499
+ if (typeof BRAND.vncDisplay !== "number") {
2500
+ console.error(`[create-maxy] error reason=cdp-port-unresolved brand=${BRAND.configDir} field=vncDisplay`);
2501
+ throw new Error(`brand.json missing required field: vncDisplay`);
2502
+ }
2503
+ const VNC_DISPLAY = BRAND.vncDisplay;
2504
+ for (const field of ["rfbPort", "websockifyPort", "cdpPort"]) {
2505
+ if (typeof BRAND[field] !== "number") {
2506
+ console.error(`[create-maxy] error reason=cdp-port-unresolved brand=${BRAND.configDir} field=${field}`);
2507
+ throw new Error(`brand.json missing required field: ${field}`);
2508
+ }
2509
+ }
2510
+ const RFB_PORT = BRAND.rfbPort;
2511
+ const WEBSOCKIFY_PORT_BRAND = BRAND.websockifyPort;
2512
+ const CDP_PORT_BRAND = BRAND.cdpPort;
2477
2513
  // Task 924/938 pre-flight — refuse to write service files if any of the
2478
2514
  // three brand-scoped ports is already held by a process that is NOT this
2479
2515
  // brand's own on-demand browser nor a peer brand's edge stack.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.858",
3
+ "version": "1.0.859",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -50,20 +50,30 @@ function resolveBrandConfig() {
50
50
  if (!brand.productName) {
51
51
  throw new Error(`brand.json at ${brandPath} is missing the productName field`);
52
52
  }
53
- // Task 924 — derive port set from `vncDisplay` ordinal offset when any
54
- // of rfbPort/websockifyPort/cdpPort is missing. Mirrors paths.ts's
55
- // deterministic fallback so an admin MCP probe sees the same per-brand
56
- // numbers the rest of the platform binds.
57
- const vncDisplay = typeof brand.vncDisplay === "number" ? brand.vncDisplay : 99;
58
- const offset = vncDisplay - 99;
53
+ // Task 924 + 959 brand.json is the single source of truth for the
54
+ // four brand-scoped port fields. An admin MCP probe must see the exact
55
+ // same numbers the rest of the platform binds; silent vncDisplay-derived
56
+ // fallback was a recurrence-class silent-fallback-masks-root-cause
57
+ // violation (Task 959) and is now loud-fail.
58
+ const brandLabel = String(brand.configDir).replace(/^\./, "");
59
+ if (typeof brand.vncDisplay !== "number") {
60
+ console.error(`[mcp:admin] error reason=cdp-port-unresolved brand=${brandLabel} path=${brandPath} field=vncDisplay json_keys=${Object.keys(brand).join(",")}`);
61
+ throw new Error(`brand.json at ${brandPath} missing required field: vncDisplay`);
62
+ }
63
+ for (const field of ["rfbPort", "websockifyPort", "cdpPort"]) {
64
+ if (typeof brand[field] !== "number") {
65
+ console.error(`[mcp:admin] error reason=cdp-port-unresolved brand=${brandLabel} path=${brandPath} field=${field} json_keys=${Object.keys(brand).join(",")}`);
66
+ throw new Error(`brand.json at ${brandPath} missing required field: ${field}`);
67
+ }
68
+ }
59
69
  return {
60
70
  configDir: brand.configDir,
61
71
  productName: brand.productName,
62
72
  commercialMode: brand.commercialMode === true,
63
- vncDisplay,
64
- rfbPort: typeof brand.rfbPort === "number" ? brand.rfbPort : 5900 + offset,
65
- websockifyPort: typeof brand.websockifyPort === "number" ? brand.websockifyPort : 6080 + offset,
66
- cdpPort: typeof brand.cdpPort === "number" ? brand.cdpPort : 9222 + offset,
73
+ vncDisplay: brand.vncDisplay,
74
+ rfbPort: brand.rfbPort,
75
+ websockifyPort: brand.websockifyPort,
76
+ cdpPort: brand.cdpPort,
67
77
  };
68
78
  }
69
79
  catch (err) {