@rubytech/create-realagent 1.0.856 → 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.
- package/dist/__tests__/cdp-port-no-silent-fallback.test.js +53 -0
- package/dist/index.js +58 -22
- package/dist/port-resolution.js +1 -1
- package/package.json +2 -2
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +2 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js +20 -10
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/admin/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +1 -1
- package/payload/platform/plugins/cloudflare/scripts/_stream-log.sh +1 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +36 -9
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/neo4j.js +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/scripts/check-no-task-id-leaks.mjs +110 -0
- package/payload/platform/scripts/test-laptop-vnc-boot.sh +8 -1
- package/payload/platform/scripts/vnc.sh +40 -34
- package/payload/server/chunk-22LK7D5R.js +1612 -0
- package/payload/server/chunk-2Q2S52GB.js +10906 -0
- package/payload/server/chunk-7ADUQXTU.js +2143 -0
- package/payload/server/chunk-BY4LZDL4.js +667 -0
- package/payload/server/chunk-CNNPNADU.js +10891 -0
- package/payload/server/chunk-FL3H3AQD.js +1603 -0
- package/payload/server/client-pool-3BCJTPPA.js +34 -0
- package/payload/server/client-pool-WA5WGN7W.js +34 -0
- package/payload/server/cloudflare-task-tracker-OOQCL5ZB.js +20 -0
- package/payload/server/maxy-edge.js +34 -6
- package/payload/server/public/assets/{admin-CZpefPcA.js → admin-BumnnEDn.js} +60 -60
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +202 -134
|
@@ -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
|
@@ -473,7 +473,7 @@ function ensureNonSnapChromium() {
|
|
|
473
473
|
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.`);
|
|
474
474
|
}
|
|
475
475
|
if (decision.action === "install-google-chrome") {
|
|
476
|
-
console.log(" Detected snap-confined Chromium
|
|
476
|
+
console.log(" Detected snap-confined Chromium — installing Google Chrome stable...");
|
|
477
477
|
logFile(` [snap-chromium] installing google-chrome-stable from Google's signed apt repo`);
|
|
478
478
|
// Fetch + dearmor the signing key, write to /etc/apt/trusted.gpg.d/. Pipe
|
|
479
479
|
// composition runs through bash -c so the curl|gpg pipeline is one
|
|
@@ -495,7 +495,7 @@ function ensureNonSnapChromium() {
|
|
|
495
495
|
], { sudo: true });
|
|
496
496
|
console.log(" [privileged] apt-get update");
|
|
497
497
|
shell("apt-get", ["update"], { sudo: true });
|
|
498
|
-
installAptGroup("Google Chrome stable
|
|
498
|
+
installAptGroup("Google Chrome stable", ["google-chrome-stable"]);
|
|
499
499
|
// Re-resolve after install to capture the now-installed absolute path.
|
|
500
500
|
const postInstallWhich = which("google-chrome-stable");
|
|
501
501
|
if (!postInstallWhich) {
|
|
@@ -518,7 +518,7 @@ function ensureNonSnapChromium() {
|
|
|
518
518
|
// surfaces the contract breach with the install context still in scope.
|
|
519
519
|
const finalRealpath = realpath(RESOLVED_CHROMIUM_BIN);
|
|
520
520
|
if (isSnapConfinedPath(finalRealpath)) {
|
|
521
|
-
throw new Error(`ensureNonSnapChromium: resolved Chromium binary ${RESOLVED_CHROMIUM_BIN} realpaths to ${finalRealpath} which is under /snap/ — refusing to persist
|
|
521
|
+
throw new Error(`ensureNonSnapChromium: resolved Chromium binary ${RESOLVED_CHROMIUM_BIN} realpaths to ${finalRealpath} which is under /snap/ — refusing to persist.`);
|
|
522
522
|
}
|
|
523
523
|
console.log(` Chromium binary: ${RESOLVED_CHROMIUM_BIN} (realpath=${finalRealpath ?? "?"})`);
|
|
524
524
|
logFile(` [snap-chromium] resolved bin=${RESOLVED_CHROMIUM_BIN} realpath=${finalRealpath ?? "null"}`);
|
|
@@ -1171,7 +1171,7 @@ function peerBrandUsingSystemUnit() {
|
|
|
1171
1171
|
peerEnvContents.push([hostname, readFileSync(envPath, "utf-8")]);
|
|
1172
1172
|
}
|
|
1173
1173
|
catch (err) {
|
|
1174
|
-
console.error(` WARNING: unable to read peer brand .env at ${envPath} — treating as potential dependency to avoid data loss
|
|
1174
|
+
console.error(` WARNING: unable to read peer brand .env at ${envPath} — treating as potential dependency to avoid data loss: ${err instanceof Error ? err.message : String(err)}`);
|
|
1175
1175
|
return hostname;
|
|
1176
1176
|
}
|
|
1177
1177
|
}
|
|
@@ -1302,7 +1302,7 @@ WantedBy=multi-user.target
|
|
|
1302
1302
|
// mutually exclusive with the disable path: exactly one log line per install.
|
|
1303
1303
|
const peerOnSystemUnit = peerBrandUsingSystemUnit();
|
|
1304
1304
|
if (peerOnSystemUnit !== null) {
|
|
1305
|
-
const keptActiveMsg = ` [neo4j] system unit kept active — peer brand ${peerOnSystemUnit} depends on port ${DEFAULT_NEO4J_PORT}
|
|
1305
|
+
const keptActiveMsg = ` [neo4j] system unit kept active — peer brand ${peerOnSystemUnit} depends on port ${DEFAULT_NEO4J_PORT}`;
|
|
1306
1306
|
console.log(keptActiveMsg);
|
|
1307
1307
|
logFile(keptActiveMsg);
|
|
1308
1308
|
}
|
|
@@ -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
|
-
|
|
2462
|
-
//
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
//
|
|
2466
|
-
//
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
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.
|
|
@@ -2671,7 +2707,7 @@ function installService() {
|
|
|
2671
2707
|
if (!installAccountId) {
|
|
2672
2708
|
throw new Error(`installService: no account discovered at ${INSTALL_DIR}/data/accounts/<uuid>/account.json — ` +
|
|
2673
2709
|
`setupAccount() (seed-neo4j.sh) should have created one. Refusing to write a systemd unit ` +
|
|
2674
|
-
`without ACCOUNT_ID; the boot validator would FATAL on every restart
|
|
2710
|
+
`without ACCOUNT_ID; the boot validator would FATAL on every restart.`);
|
|
2675
2711
|
}
|
|
2676
2712
|
const serviceFile = buildMaxyUnitFile({
|
|
2677
2713
|
productName: BRAND.productName,
|
package/dist/port-resolution.js
CHANGED
|
@@ -76,7 +76,7 @@ export function buildMaxyUnitFile(o) {
|
|
|
76
76
|
// with no Environment=ACCOUNT_ID line — bootValidator would FATAL on
|
|
77
77
|
// every restart with reason=missing. Throw at build time so the install
|
|
78
78
|
// aborts loudly instead of bricking the boot loop.
|
|
79
|
-
throw new Error("buildMaxyUnitFile: accountId is required — caller must resolve the on-disk account UUID before stamping the systemd unit
|
|
79
|
+
throw new Error("buildMaxyUnitFile: accountId is required — caller must resolve the on-disk account UUID before stamping the systemd unit.");
|
|
80
80
|
}
|
|
81
81
|
const neo4jServiceDep = o.neo4jDedicated
|
|
82
82
|
? `neo4j-${o.brandHostname}.service`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/create-realagent",
|
|
3
|
-
"version": "1.0.
|
|
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"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"bundle": "node scripts/bundle.js",
|
|
12
12
|
"test": "npm run build && node --test 'dist/__tests__/*.test.js'",
|
|
13
|
-
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
13
|
+
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
@@ -43,7 +43,7 @@ if [ "$AGENT_TYPE" = "admin" ]; then
|
|
|
43
43
|
# Patterns intentionally cover both absolute (*/...) and relative
|
|
44
44
|
# (no leading slash) paths so an agent can't bypass via cwd-relative writes.
|
|
45
45
|
*/platform/lib/entitlement/*|platform/lib/entitlement/*|*/entitlement.json|entitlement.json)
|
|
46
|
-
echo "Blocked: Admin agent cannot modify entitlement files at $FILE_PATH
|
|
46
|
+
echo "Blocked: Admin agent cannot modify entitlement files at $FILE_PATH. Effective tier and purchasedPlugins derive from a Rubytech-signed payload." >&2
|
|
47
47
|
echo "[entitlement] tool-deny: tool=${TOOL_NAME} path=${FILE_PATH} field=entitlement-file" >&2
|
|
48
48
|
exit 2
|
|
49
49
|
;;
|
|
@@ -76,7 +76,7 @@ if [ "$AGENT_TYPE" = "admin" ]; then
|
|
|
76
76
|
;;
|
|
77
77
|
# Entitlement files via shell (Task 831) — same surface, blocked symmetrically
|
|
78
78
|
*"platform/lib/entitlement/"*|*"entitlement.json"*)
|
|
79
|
-
echo "Blocked: Admin agent cannot reference entitlement files via shell
|
|
79
|
+
echo "Blocked: Admin agent cannot reference entitlement files via shell." >&2
|
|
80
80
|
echo "[entitlement] tool-deny: tool=Bash path=entitlement-file field=entitlement-file" >&2
|
|
81
81
|
exit 2
|
|
82
82
|
;;
|
|
@@ -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 —
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
const
|
|
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:
|
|
65
|
-
websockifyPort:
|
|
66
|
-
cdpPort:
|
|
73
|
+
vncDisplay: brand.vncDisplay,
|
|
74
|
+
rfbPort: brand.rfbPort,
|
|
75
|
+
websockifyPort: brand.websockifyPort,
|
|
76
|
+
cdpPort: brand.cdpPort,
|
|
67
77
|
};
|
|
68
78
|
}
|
|
69
79
|
catch (err) {
|