@rubytech/create-realagent 1.0.852 → 1.0.854
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__/preflight-port-classifier.test.js +240 -73
- package/dist/index.js +59 -11
- package/dist/preflight-port-classifier.js +176 -41
- package/package.json +1 -1
- package/payload/platform/config/brand-registry.json +44 -0
- package/payload/platform/lib/persistent-components/dist/index.d.ts +21 -0
- package/payload/platform/lib/persistent-components/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/persistent-components/dist/index.js +32 -0
- package/payload/platform/lib/persistent-components/dist/index.js.map +1 -0
- package/payload/platform/lib/persistent-components/src/index.ts +28 -0
- package/payload/platform/lib/persistent-components/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/playwright-file-guard.test.sh +278 -0
- package/payload/platform/plugins/admin/hooks/playwright-file-guard.sh +204 -20
- package/payload/platform/plugins/admin/mcp/dist/index.js +40 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +2 -0
- package/payload/platform/plugins/docs/references/getting-started.md +2 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +10 -0
- package/payload/platform/scripts/admin-persist-audit.ts +191 -0
- package/payload/platform/scripts/component-knowledgedoc-backfill.ts +214 -0
- package/payload/platform/scripts/installer-device-verify.sh +249 -0
- package/payload/platform/templates/specialists/agents/content-producer.md +2 -2
- package/payload/server/chunk-CFNSKDGA.js +667 -0
- package/payload/server/chunk-DC6DWYZJ.js +1603 -0
- package/payload/server/chunk-LTB5SSQW.js +10889 -0
- package/payload/server/chunk-MN2LGNUB.js +2143 -0
- package/payload/server/client-pool-AMT2W3II.js +34 -0
- package/payload/server/cloudflare-task-tracker-LJ4SMK2D.js +20 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/public/assets/admin-DZ8Ke7t3.js +352 -0
- package/payload/server/public/assets/public-DApUXgoq.js +5 -0
- package/payload/server/public/assets/useVoiceRecorder-CI8GpxfU.js +36 -0
- package/payload/server/public/index.html +2 -2
- package/payload/server/public/public.html +2 -2
- package/payload/server/server.js +535 -351
- package/payload/server/public/assets/admin-Dyl8uNxX.js +0 -352
- package/payload/server/public/assets/public-B_PNZUph.js +0 -5
- package/payload/server/public/assets/useVoiceRecorder-fD0IWzJj.js +0 -36
|
@@ -1,163 +1,330 @@
|
|
|
1
|
-
// Task 938 — acceptance grid for classifyPortHolder.
|
|
1
|
+
// Task 938 — acceptance grid for classifyPortHolder (chromium argv-anchor).
|
|
2
|
+
// Task 939 — extended grid for Xtigervnc display anchor + websockify port
|
|
3
|
+
// anchor + holder-type detection on argv[0] basename / full-argv scan.
|
|
2
4
|
//
|
|
3
5
|
// The wrapper in index.ts owns ss(8), /proc reads, kill(2), and the operator-
|
|
4
6
|
// override exit. This suite exercises only the pure decision rule: ssOutput +
|
|
5
|
-
//
|
|
7
|
+
// brand identities → kind. Inputs in, decision out — no fs, no exec, no spawn.
|
|
6
8
|
//
|
|
7
9
|
// Cmdlines below mimic /proc/<pid>/cmdline format: argv joined by NUL bytes,
|
|
8
|
-
// possibly with a trailing NUL. The classifier requires this format so it
|
|
9
|
-
// distinguish
|
|
10
|
-
// flag whose value happens to contain the
|
|
10
|
+
// possibly with a trailing NUL. The classifier requires this format so it
|
|
11
|
+
// can distinguish a flag value (a real ownership claim) from a different
|
|
12
|
+
// flag whose value happens to contain the marker substring.
|
|
11
13
|
//
|
|
12
14
|
// Runs via Node's built-in test runner — same convention as
|
|
13
15
|
// peer-brand-detect.test.ts.
|
|
14
16
|
import test from "node:test";
|
|
15
17
|
import assert from "node:assert/strict";
|
|
16
18
|
import { classifyPortHolder } from "../preflight-port-classifier.js";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
// Per-brand identities mirror the live brands/<brand>/brand.json values that
|
|
20
|
+
// bundle.js stamps into payload/platform/config/brand-registry.json. Values
|
|
21
|
+
// are derived from the brand manifests, not hardcoded as ground-truth here:
|
|
22
|
+
// the test suite asserts the rule, not the numbers. Any rebalance in
|
|
23
|
+
// brands/*/brand.json updates the registry on the next bundle and the
|
|
24
|
+
// classifier consumes the new values without source change.
|
|
25
|
+
const MAXY = { configDir: ".maxy", vncDisplay: 99, websockifyPort: 6080 };
|
|
26
|
+
const REAL = { configDir: ".realagent", vncDisplay: 100, websockifyPort: 6081 };
|
|
27
|
+
const MAXY_2 = { configDir: ".maxy-2", vncDisplay: 101, websockifyPort: 6082 };
|
|
28
|
+
const MAXY_3 = { configDir: ".maxy-3", vncDisplay: 102, websockifyPort: 6083 };
|
|
29
|
+
const MAXY_4 = { configDir: ".maxy-4", vncDisplay: 103, websockifyPort: 6084 };
|
|
30
|
+
const ALL = [MAXY, REAL, MAXY_2, MAXY_3, MAXY_4];
|
|
31
|
+
const peers = (own) => ALL.filter(b => b.configDir !== own.configDir);
|
|
19
32
|
const SS_PID_42 = "LISTEN 0 4096 *:9222 *:* users:((\"chrome\",pid=42,fd=12))";
|
|
20
33
|
const SS_PID_99 = "LISTEN 0 4096 *:9223 *:* users:((\"chrome\",pid=99,fd=12))";
|
|
34
|
+
const SS_PID_777 = "LISTEN 0 5 127.0.0.1:5900 0.0.0.0:* users:((\"Xtigervnc\",pid=777,fd=9))";
|
|
35
|
+
const SS_PID_888 = "LISTEN 0 5 127.0.0.1:5901 0.0.0.0:* users:((\"Xtigervnc\",pid=888,fd=9))";
|
|
36
|
+
const SS_PID_555 = "LISTEN 0 5 *:6080 *:* users:((\"websockify\",pid=555,fd=8))";
|
|
21
37
|
const cmd = (...argv) => argv.join("\0") + "\0";
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// EMPTY / no pid / cmdline race
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
22
41
|
test("empty ssOutput → EMPTY", () => {
|
|
23
42
|
const r = classifyPortHolder({
|
|
24
43
|
ssOutput: "",
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
ownBrand: MAXY,
|
|
45
|
+
peerBrands: peers(MAXY),
|
|
27
46
|
getCmdline: () => { throw new Error("should not be called"); },
|
|
28
47
|
});
|
|
29
48
|
assert.equal(r.kind, "EMPTY");
|
|
30
49
|
});
|
|
31
|
-
test("
|
|
50
|
+
test("UNRELATED: ssOutput non-empty but no pid= token", () => {
|
|
51
|
+
const r = classifyPortHolder({
|
|
52
|
+
ssOutput: "Recv-Q Send-Q Local Address:Port Peer Address:Port",
|
|
53
|
+
ownBrand: MAXY,
|
|
54
|
+
peerBrands: peers(MAXY),
|
|
55
|
+
getCmdline: () => { throw new Error("should not be called"); },
|
|
56
|
+
});
|
|
57
|
+
assert.equal(r.kind, "UNRELATED");
|
|
58
|
+
assert.equal(r.pid, undefined);
|
|
59
|
+
});
|
|
60
|
+
test("UNRELATED: getCmdline throws (race — process exited between ss and read)", () => {
|
|
61
|
+
const r = classifyPortHolder({
|
|
62
|
+
ssOutput: SS_PID_42,
|
|
63
|
+
ownBrand: MAXY,
|
|
64
|
+
peerBrands: peers(MAXY),
|
|
65
|
+
getCmdline: () => { const e = new Error("ENOENT"); e.code = "ENOENT"; throw e; },
|
|
66
|
+
});
|
|
67
|
+
assert.equal(r.kind, "UNRELATED");
|
|
68
|
+
assert.equal(r.pid, 42);
|
|
69
|
+
assert.equal(r.cmdlineReadFailed, true);
|
|
70
|
+
});
|
|
71
|
+
test("ss header line containing 'pid=': last pid= wins", () => {
|
|
72
|
+
// If a future ss locale prepends header text containing the literal `pid=`,
|
|
73
|
+
// the first-match heuristic would lift the wrong value. The LISTEN row is
|
|
74
|
+
// always last, so taking the last pid= match is the structural fix.
|
|
75
|
+
const ssOutput = `State pid= notes\nLISTEN 0 4096 *:9222 *:* users:(("chrome",pid=42,fd=12))`;
|
|
76
|
+
const cmdline = cmd("chromium", "--user-data-dir=/home/neo/.maxy/chromium-profile");
|
|
77
|
+
const r = classifyPortHolder({
|
|
78
|
+
ssOutput,
|
|
79
|
+
ownBrand: MAXY,
|
|
80
|
+
peerBrands: peers(MAXY),
|
|
81
|
+
getCmdline: pid => { assert.equal(pid, 42, "must pick the LISTEN row's pid"); return cmdline; },
|
|
82
|
+
});
|
|
83
|
+
assert.equal(r.kind, "OWN_BRAND");
|
|
84
|
+
assert.equal(r.pid, 42);
|
|
85
|
+
});
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Chromium (Task 938 grid, migrated to BrandIdentity API)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
test("chromium OWN_BRAND: --user-data-dir=PATH single argv", () => {
|
|
32
90
|
const cmdline = cmd("/opt/google/chrome/chrome", "--user-data-dir=/home/neo/.maxy/chromium-profile", "--remote-debugging-port=9222");
|
|
33
91
|
const r = classifyPortHolder({
|
|
34
92
|
ssOutput: SS_PID_42,
|
|
35
|
-
|
|
36
|
-
|
|
93
|
+
ownBrand: MAXY,
|
|
94
|
+
peerBrands: peers(MAXY),
|
|
37
95
|
getCmdline: pid => { assert.equal(pid, 42); return cmdline; },
|
|
38
96
|
});
|
|
39
97
|
assert.equal(r.kind, "OWN_BRAND");
|
|
98
|
+
assert.equal(r.holderType, "chromium");
|
|
40
99
|
assert.equal(r.pid, 42);
|
|
41
100
|
assert.equal(r.profilePath, "/home/neo/.maxy/chromium-profile");
|
|
42
101
|
});
|
|
43
|
-
test("OWN_BRAND: --user-data-dir PATH split across two argvs", () => {
|
|
102
|
+
test("chromium OWN_BRAND: --user-data-dir PATH split across two argvs", () => {
|
|
44
103
|
const cmdline = cmd("/usr/bin/chromium", "--user-data-dir", "/home/admin/.maxy/chromium-profile", "--remote-debugging-port=9222");
|
|
45
104
|
const r = classifyPortHolder({
|
|
46
105
|
ssOutput: SS_PID_42,
|
|
47
|
-
|
|
48
|
-
|
|
106
|
+
ownBrand: MAXY,
|
|
107
|
+
peerBrands: peers(MAXY),
|
|
49
108
|
getCmdline: () => cmdline,
|
|
50
109
|
});
|
|
51
110
|
assert.equal(r.kind, "OWN_BRAND");
|
|
111
|
+
assert.equal(r.holderType, "chromium");
|
|
52
112
|
assert.equal(r.profilePath, "/home/admin/.maxy/chromium-profile");
|
|
53
113
|
});
|
|
54
|
-
test("PEER_BRAND: cmdline holds another known brand's profile path", () => {
|
|
114
|
+
test("chromium PEER_BRAND: cmdline holds another known brand's profile path", () => {
|
|
55
115
|
const cmdline = cmd("/usr/bin/chromium", "--user-data-dir=/home/admin/.realagent/chromium-profile", "--remote-debugging-port=9223");
|
|
56
116
|
const r = classifyPortHolder({
|
|
57
117
|
ssOutput: SS_PID_99,
|
|
58
|
-
|
|
59
|
-
|
|
118
|
+
ownBrand: MAXY,
|
|
119
|
+
peerBrands: peers(MAXY),
|
|
60
120
|
getCmdline: () => cmdline,
|
|
61
121
|
});
|
|
62
122
|
assert.equal(r.kind, "PEER_BRAND");
|
|
123
|
+
assert.equal(r.holderType, "chromium");
|
|
63
124
|
assert.equal(r.pid, 99);
|
|
64
125
|
assert.equal(r.profilePath, "/home/admin/.realagent/chromium-profile");
|
|
65
126
|
});
|
|
66
|
-
test("UNRELATED: cmdline matches no known brand profile", () => {
|
|
67
|
-
|
|
127
|
+
test("chromium UNRELATED: cmdline matches no known brand profile", () => {
|
|
128
|
+
// Anchors only on the basename — "/usr/bin/python3" is not chromium.
|
|
129
|
+
// Use an actual chromium binary with a non-brand profile path instead.
|
|
130
|
+
const cmdline = cmd("chromium", "--user-data-dir=/tmp/scratch", "--remote-debugging-port=9999");
|
|
68
131
|
const r = classifyPortHolder({
|
|
69
132
|
ssOutput: SS_PID_42,
|
|
70
|
-
|
|
71
|
-
|
|
133
|
+
ownBrand: MAXY,
|
|
134
|
+
peerBrands: peers(MAXY),
|
|
72
135
|
getCmdline: () => cmdline,
|
|
73
136
|
});
|
|
74
137
|
assert.equal(r.kind, "UNRELATED");
|
|
138
|
+
assert.equal(r.holderType, "chromium");
|
|
75
139
|
assert.equal(r.pid, 42);
|
|
76
|
-
assert.equal(r.cmdline, "/usr/bin/python3 -m http.server 9222");
|
|
77
|
-
});
|
|
78
|
-
test("UNRELATED: ssOutput non-empty but no pid= token", () => {
|
|
79
|
-
const r = classifyPortHolder({
|
|
80
|
-
ssOutput: "Recv-Q Send-Q Local Address:Port Peer Address:Port",
|
|
81
|
-
ownConfigDir: ".maxy",
|
|
82
|
-
peerConfigDirs: peers(".maxy"),
|
|
83
|
-
getCmdline: () => { throw new Error("should not be called"); },
|
|
84
|
-
});
|
|
85
|
-
assert.equal(r.kind, "UNRELATED");
|
|
86
|
-
assert.equal(r.pid, undefined);
|
|
87
|
-
});
|
|
88
|
-
test("UNRELATED: getCmdline throws (race — process exited between ss and read)", () => {
|
|
89
|
-
const r = classifyPortHolder({
|
|
90
|
-
ssOutput: SS_PID_42,
|
|
91
|
-
ownConfigDir: ".maxy",
|
|
92
|
-
peerConfigDirs: peers(".maxy"),
|
|
93
|
-
getCmdline: () => { const e = new Error("ENOENT"); e.code = "ENOENT"; throw e; },
|
|
94
|
-
});
|
|
95
|
-
assert.equal(r.kind, "UNRELATED");
|
|
96
|
-
assert.equal(r.pid, 42);
|
|
97
|
-
assert.equal(r.cmdlineReadFailed, true);
|
|
98
140
|
});
|
|
99
|
-
test("UNRELATED: --user-data-dir absent (no profile claim)", () => {
|
|
141
|
+
test("chromium UNRELATED: --user-data-dir absent (no profile claim)", () => {
|
|
100
142
|
// Kernel threads, GPU/zygote/utility chrome processes inherit profile via
|
|
101
|
-
// fork without re-stating --user-data-dir
|
|
102
|
-
//
|
|
103
|
-
// UNRELATED, never as OWN_BRAND on a substring fluke.
|
|
143
|
+
// fork without re-stating --user-data-dir. They wouldn't be listening
|
|
144
|
+
// anyway, but if one ever showed up here we must classify as UNRELATED.
|
|
104
145
|
const cmdline = cmd("/opt/google/chrome/chrome", "--type=gpu-process", "--no-sandbox");
|
|
105
146
|
const r = classifyPortHolder({
|
|
106
147
|
ssOutput: SS_PID_42,
|
|
107
|
-
|
|
108
|
-
|
|
148
|
+
ownBrand: MAXY,
|
|
149
|
+
peerBrands: peers(MAXY),
|
|
109
150
|
getCmdline: () => cmdline,
|
|
110
151
|
});
|
|
111
152
|
assert.equal(r.kind, "UNRELATED");
|
|
153
|
+
assert.equal(r.holderType, "chromium");
|
|
112
154
|
});
|
|
113
|
-
test("substring boundary: own=.maxy must NOT match .maxy-2 cmdline", () => {
|
|
155
|
+
test("chromium substring boundary: own=.maxy must NOT match .maxy-2 cmdline", () => {
|
|
114
156
|
const cmdline = cmd("chromium", "--user-data-dir=/home/neo/.maxy-2/chromium-profile");
|
|
115
157
|
const r = classifyPortHolder({
|
|
116
158
|
ssOutput: SS_PID_42,
|
|
117
|
-
|
|
118
|
-
|
|
159
|
+
ownBrand: MAXY,
|
|
160
|
+
peerBrands: peers(MAXY),
|
|
119
161
|
getCmdline: () => cmdline,
|
|
120
162
|
});
|
|
121
163
|
assert.equal(r.kind, "PEER_BRAND");
|
|
122
164
|
assert.equal(r.profilePath, "/home/neo/.maxy-2/chromium-profile");
|
|
123
165
|
});
|
|
124
|
-
test("substring boundary: own=.maxy-2 must NOT misclassify .maxy cmdline as own", () => {
|
|
166
|
+
test("chromium substring boundary: own=.maxy-2 must NOT misclassify .maxy cmdline as own", () => {
|
|
125
167
|
const cmdline = cmd("chromium", "--user-data-dir=/home/neo/.maxy/chromium-profile");
|
|
126
168
|
const r = classifyPortHolder({
|
|
127
169
|
ssOutput: SS_PID_42,
|
|
128
|
-
|
|
129
|
-
|
|
170
|
+
ownBrand: MAXY_2,
|
|
171
|
+
peerBrands: peers(MAXY_2),
|
|
130
172
|
getCmdline: () => cmdline,
|
|
131
173
|
});
|
|
132
174
|
assert.equal(r.kind, "PEER_BRAND");
|
|
133
175
|
assert.equal(r.profilePath, "/home/neo/.maxy/chromium-profile");
|
|
134
176
|
});
|
|
135
|
-
test("argv-anchor safety: marker in --load-extension value must NOT
|
|
136
|
-
// Adversarial case
|
|
137
|
-
//
|
|
138
|
-
//
|
|
177
|
+
test("chromium argv-anchor safety: marker in --load-extension value must NOT match", () => {
|
|
178
|
+
// Adversarial case: a Chrome flag whose value contains the profile-path
|
|
179
|
+
// substring, but no actual --user-data-dir claim. Naive substring matching
|
|
180
|
+
// would false-positive OWN_BRAND.
|
|
139
181
|
const cmdline = cmd("chromium", "--load-extension=/tmp/decoy/.maxy/chromium-profile", "--remote-debugging-port=9222");
|
|
140
182
|
const r = classifyPortHolder({
|
|
141
183
|
ssOutput: SS_PID_42,
|
|
142
|
-
|
|
143
|
-
|
|
184
|
+
ownBrand: MAXY,
|
|
185
|
+
peerBrands: peers(MAXY),
|
|
144
186
|
getCmdline: () => cmdline,
|
|
145
187
|
});
|
|
146
188
|
assert.equal(r.kind, "UNRELATED");
|
|
147
189
|
});
|
|
148
|
-
test("
|
|
149
|
-
//
|
|
150
|
-
// (
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
const ssOutput = `State pid= notes\nLISTEN 0 4096 *:9222 *:* users:(("chrome",pid=42,fd=12))`;
|
|
154
|
-
const cmdline = cmd("chromium", "--user-data-dir=/home/neo/.maxy/chromium-profile");
|
|
190
|
+
test("chromium variant basenames: google-chrome-stable detected as chromium", () => {
|
|
191
|
+
// Ubuntu Noble laptop replaces snap-confined chromium with
|
|
192
|
+
// google-chrome-stable (Task 929). The classifier must still recognise
|
|
193
|
+
// it as a chromium-family holder.
|
|
194
|
+
const cmdline = cmd("/usr/bin/google-chrome-stable", "--user-data-dir=/home/neo/.maxy/chromium-profile", "--remote-debugging-port=9222");
|
|
155
195
|
const r = classifyPortHolder({
|
|
156
|
-
ssOutput,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
getCmdline:
|
|
196
|
+
ssOutput: SS_PID_42,
|
|
197
|
+
ownBrand: MAXY,
|
|
198
|
+
peerBrands: peers(MAXY),
|
|
199
|
+
getCmdline: () => cmdline,
|
|
200
|
+
});
|
|
201
|
+
assert.equal(r.kind, "OWN_BRAND");
|
|
202
|
+
assert.equal(r.holderType, "chromium");
|
|
203
|
+
});
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Xtigervnc (Task 939 — display-number anchor)
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
test("xtigervnc OWN_BRAND: argv[1] :99 matches own.vncDisplay", () => {
|
|
208
|
+
// Reproduces the laptop-maxy and Pi-maxy install-time log:
|
|
209
|
+
// cmdline: Xtigervnc :99 -geometry 1280x800 -depth 24 -rfbport 5900 …
|
|
210
|
+
const cmdline = cmd("Xtigervnc", ":99", "-geometry", "1280x800", "-depth", "24", "-rfbport", "5900", "-localhost", "-SecurityTypes", "None", "-AlwaysShared", "-BlacklistThreshold", "1000000");
|
|
211
|
+
const r = classifyPortHolder({
|
|
212
|
+
ssOutput: SS_PID_777,
|
|
213
|
+
ownBrand: MAXY,
|
|
214
|
+
peerBrands: peers(MAXY),
|
|
215
|
+
getCmdline: pid => { assert.equal(pid, 777); return cmdline; },
|
|
216
|
+
});
|
|
217
|
+
assert.equal(r.kind, "OWN_BRAND");
|
|
218
|
+
assert.equal(r.holderType, "xtigervnc");
|
|
219
|
+
assert.equal(r.pid, 777);
|
|
220
|
+
assert.equal(r.vncDisplay, 99);
|
|
221
|
+
});
|
|
222
|
+
test("xtigervnc PEER_BRAND: argv[1] :100 matches Real Agent vncDisplay when own is Maxy", () => {
|
|
223
|
+
const cmdline = cmd("Xtigervnc", ":100", "-rfbport", "5901");
|
|
224
|
+
const r = classifyPortHolder({
|
|
225
|
+
ssOutput: SS_PID_888,
|
|
226
|
+
ownBrand: MAXY,
|
|
227
|
+
peerBrands: peers(MAXY),
|
|
228
|
+
getCmdline: () => cmdline,
|
|
229
|
+
});
|
|
230
|
+
assert.equal(r.kind, "PEER_BRAND");
|
|
231
|
+
assert.equal(r.holderType, "xtigervnc");
|
|
232
|
+
assert.equal(r.vncDisplay, 100);
|
|
233
|
+
});
|
|
234
|
+
test("xtigervnc UNRELATED: argv display number matches no known brand", () => {
|
|
235
|
+
const cmdline = cmd("Xtigervnc", ":42", "-rfbport", "5942");
|
|
236
|
+
const r = classifyPortHolder({
|
|
237
|
+
ssOutput: SS_PID_777,
|
|
238
|
+
ownBrand: MAXY,
|
|
239
|
+
peerBrands: peers(MAXY),
|
|
240
|
+
getCmdline: () => cmdline,
|
|
241
|
+
});
|
|
242
|
+
assert.equal(r.kind, "UNRELATED");
|
|
243
|
+
assert.equal(r.holderType, "xtigervnc");
|
|
244
|
+
assert.equal(r.vncDisplay, 42);
|
|
245
|
+
});
|
|
246
|
+
test("xtigervnc UNRELATED: no `:N` token in argv", () => {
|
|
247
|
+
// Defence-in-depth: if argv parsing ever sees an Xtigervnc invocation
|
|
248
|
+
// with no display literal, classify UNRELATED rather than guessing.
|
|
249
|
+
const cmdline = cmd("Xtigervnc", "-help");
|
|
250
|
+
const r = classifyPortHolder({
|
|
251
|
+
ssOutput: SS_PID_777,
|
|
252
|
+
ownBrand: MAXY,
|
|
253
|
+
peerBrands: peers(MAXY),
|
|
254
|
+
getCmdline: () => cmdline,
|
|
255
|
+
});
|
|
256
|
+
assert.equal(r.kind, "UNRELATED");
|
|
257
|
+
assert.equal(r.holderType, "xtigervnc");
|
|
258
|
+
assert.equal(r.vncDisplay, undefined);
|
|
259
|
+
});
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// websockify (Task 939 — bind-port anchor with full-argv scan for the binary)
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
test("websockify OWN_BRAND: bind port [::]:6080 matches own.websockifyPort (direct invocation)", () => {
|
|
264
|
+
const cmdline = cmd("websockify", "--web", "/usr/share/novnc", "[::]:6080", "localhost:5900");
|
|
265
|
+
const r = classifyPortHolder({
|
|
266
|
+
ssOutput: SS_PID_555,
|
|
267
|
+
ownBrand: MAXY,
|
|
268
|
+
peerBrands: peers(MAXY),
|
|
269
|
+
getCmdline: pid => { assert.equal(pid, 555); return cmdline; },
|
|
270
|
+
});
|
|
271
|
+
assert.equal(r.kind, "OWN_BRAND");
|
|
272
|
+
assert.equal(r.holderType, "websockify");
|
|
273
|
+
assert.equal(r.pid, 555);
|
|
274
|
+
assert.equal(r.websockifyPort, 6080);
|
|
275
|
+
});
|
|
276
|
+
test("websockify OWN_BRAND: detected via argv scan when invoked through python", () => {
|
|
277
|
+
// Pi Bookworm packages websockify as a python script with no shebang
|
|
278
|
+
// honoured by systemd in some configs — argv[0] may be `python3` and
|
|
279
|
+
// argv[1] = `/usr/bin/websockify`. The detector scans the full argv.
|
|
280
|
+
const cmdline = cmd("/usr/bin/python3", "/usr/bin/websockify", "--web", "/usr/share/novnc", "[::]:6080", "localhost:5900");
|
|
281
|
+
const r = classifyPortHolder({
|
|
282
|
+
ssOutput: SS_PID_555,
|
|
283
|
+
ownBrand: MAXY,
|
|
284
|
+
peerBrands: peers(MAXY),
|
|
285
|
+
getCmdline: () => cmdline,
|
|
160
286
|
});
|
|
161
287
|
assert.equal(r.kind, "OWN_BRAND");
|
|
288
|
+
assert.equal(r.holderType, "websockify");
|
|
289
|
+
assert.equal(r.websockifyPort, 6080);
|
|
290
|
+
});
|
|
291
|
+
test("websockify PEER_BRAND: bind port matches Real Agent websockifyPort when own is Maxy", () => {
|
|
292
|
+
const cmdline = cmd("websockify", "--web", "/usr/share/novnc", "[::]:6081", "localhost:5901");
|
|
293
|
+
const r = classifyPortHolder({
|
|
294
|
+
ssOutput: SS_PID_555,
|
|
295
|
+
ownBrand: MAXY,
|
|
296
|
+
peerBrands: peers(MAXY),
|
|
297
|
+
getCmdline: () => cmdline,
|
|
298
|
+
});
|
|
299
|
+
assert.equal(r.kind, "PEER_BRAND");
|
|
300
|
+
assert.equal(r.holderType, "websockify");
|
|
301
|
+
assert.equal(r.websockifyPort, 6081);
|
|
302
|
+
});
|
|
303
|
+
test("websockify UNRELATED: bind port matches no known brand", () => {
|
|
304
|
+
const cmdline = cmd("websockify", "--web", "/usr/share/novnc", "[::]:9999", "localhost:5900");
|
|
305
|
+
const r = classifyPortHolder({
|
|
306
|
+
ssOutput: SS_PID_555,
|
|
307
|
+
ownBrand: MAXY,
|
|
308
|
+
peerBrands: peers(MAXY),
|
|
309
|
+
getCmdline: () => cmdline,
|
|
310
|
+
});
|
|
311
|
+
assert.equal(r.kind, "UNRELATED");
|
|
312
|
+
assert.equal(r.holderType, "websockify");
|
|
313
|
+
assert.equal(r.websockifyPort, undefined);
|
|
314
|
+
});
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Unknown holder (defence-in-depth)
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
test("UNRELATED holderType=unknown: argv[0] is neither chromium, Xtigervnc, nor websockify", () => {
|
|
319
|
+
const cmdline = cmd("/usr/bin/python3", "-m", "http.server", "9222");
|
|
320
|
+
const r = classifyPortHolder({
|
|
321
|
+
ssOutput: SS_PID_42,
|
|
322
|
+
ownBrand: MAXY,
|
|
323
|
+
peerBrands: peers(MAXY),
|
|
324
|
+
getCmdline: () => cmdline,
|
|
325
|
+
});
|
|
326
|
+
assert.equal(r.kind, "UNRELATED");
|
|
327
|
+
assert.equal(r.holderType, "unknown");
|
|
162
328
|
assert.equal(r.pid, 42);
|
|
329
|
+
assert.equal(r.cmdline, "/usr/bin/python3 -m http.server 9222");
|
|
163
330
|
});
|
package/dist/index.js
CHANGED
|
@@ -2457,12 +2457,13 @@ function installService() {
|
|
|
2457
2457
|
// three brand-scoped ports is already held by a process that is NOT this
|
|
2458
2458
|
// brand's own on-demand browser nor a peer brand's edge stack.
|
|
2459
2459
|
//
|
|
2460
|
-
// Classification (Task 938
|
|
2461
|
-
//
|
|
2462
|
-
//
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
//
|
|
2460
|
+
// Classification (Task 938 chromium, Task 939 Xtigervnc + websockify) reads
|
|
2461
|
+
// `/proc/<pid>/cmdline` and applies a holder-specific argv anchor. Task 938
|
|
2462
|
+
// covered chromium-only via `--user-data-dir=`; Task 939 closed the gap on
|
|
2463
|
+
// Xtigervnc (no such flag — anchor on the `:N` display literal) and
|
|
2464
|
+
// websockify (anchor on bind port). Brand identities (configDir,
|
|
2465
|
+
// vncDisplay, websockifyPort) come from brand-registry.json which the
|
|
2466
|
+
// bundler stamps at build time from every brands/<brand>/brand.json.
|
|
2466
2467
|
//
|
|
2467
2468
|
// Decisions per holder:
|
|
2468
2469
|
// OWN_BRAND — SIGTERM, recheck, SIGKILL on stragglers, exit-1 only if
|
|
@@ -2471,9 +2472,42 @@ function installService() {
|
|
|
2471
2472
|
// UNRELATED — refuse to write service files; emit operator override.
|
|
2472
2473
|
// macOS dev hosts (no ss) fall through the catch and skip pre-flight
|
|
2473
2474
|
// entirely — the runtime check in vnc.sh covers Linux production.
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
|
|
2475
|
+
const ownBrand = {
|
|
2476
|
+
configDir: BRAND.configDir,
|
|
2477
|
+
vncDisplay: VNC_DISPLAY,
|
|
2478
|
+
websockifyPort: WEBSOCKIFY_PORT_BRAND,
|
|
2479
|
+
};
|
|
2480
|
+
// Peer registry — load from payload/platform/config/brand-registry.json
|
|
2481
|
+
// when present (Task 939+ bundles). Older bundles ship without the
|
|
2482
|
+
// registry; in that case peerBrands stays empty. PEER_BRAND classification
|
|
2483
|
+
// for Xtigervnc/websockify is a defence-in-depth case anyway (port sets
|
|
2484
|
+
// are disjoint by Task 924), so the empty-list fallback is safe — peer
|
|
2485
|
+
// chromium will fall through to UNRELATED, matching pre-Task 938 behaviour
|
|
2486
|
+
// for the only realistic scenario (a stale peer browser on the wrong CDP
|
|
2487
|
+
// port).
|
|
2488
|
+
const peerBrands = (() => {
|
|
2489
|
+
const registryPath = join(PAYLOAD_DIR, "platform", "config", "brand-registry.json");
|
|
2490
|
+
if (!existsSync(registryPath)) {
|
|
2491
|
+
logFile(` [preflight] brand-registry.json not in payload — peer matching disabled`);
|
|
2492
|
+
return [];
|
|
2493
|
+
}
|
|
2494
|
+
try {
|
|
2495
|
+
const raw = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
2496
|
+
const entries = [];
|
|
2497
|
+
for (const b of raw.brands ?? []) {
|
|
2498
|
+
if (b.hostname === BRAND.hostname)
|
|
2499
|
+
continue;
|
|
2500
|
+
if (typeof b.configDir !== "string" || typeof b.vncDisplay !== "number" || typeof b.websockifyPort !== "number")
|
|
2501
|
+
continue;
|
|
2502
|
+
entries.push({ configDir: b.configDir, vncDisplay: b.vncDisplay, websockifyPort: b.websockifyPort });
|
|
2503
|
+
}
|
|
2504
|
+
return entries;
|
|
2505
|
+
}
|
|
2506
|
+
catch (err) {
|
|
2507
|
+
logFile(` [preflight] brand-registry.json parse failed: ${err instanceof Error ? err.message : String(err)} — peer matching disabled`);
|
|
2508
|
+
return [];
|
|
2509
|
+
}
|
|
2510
|
+
})();
|
|
2477
2511
|
const ssReadHolder = (port) => {
|
|
2478
2512
|
return execFileSync("ss", ["-tlnpH", `sport = :${port}`], {
|
|
2479
2513
|
encoding: "utf-8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -2513,8 +2547,22 @@ function installService() {
|
|
|
2513
2547
|
}
|
|
2514
2548
|
};
|
|
2515
2549
|
const classify = (ssOutput) => classifyPortHolder({
|
|
2516
|
-
ssOutput,
|
|
2550
|
+
ssOutput, ownBrand, peerBrands, getCmdline: readCmdline,
|
|
2517
2551
|
});
|
|
2552
|
+
// Task 939 — log line varies by detected holder so the operator can see
|
|
2553
|
+
// which OWN_BRAND stack is being killed. The kill loop is identical for
|
|
2554
|
+
// all three holders (SIGTERM → 300ms → recheck → SIGKILL → recheck), so
|
|
2555
|
+
// only the announce line differs.
|
|
2556
|
+
const ownBrandAnnounceLine = (label, port, c) => {
|
|
2557
|
+
if (c.holderType === "xtigervnc") {
|
|
2558
|
+
return ` [preflight] ${label}=${port} held by OWN brand Xtigervnc display=:${c.vncDisplay} pid=${c.pid} — sending SIGTERM`;
|
|
2559
|
+
}
|
|
2560
|
+
if (c.holderType === "websockify") {
|
|
2561
|
+
return ` [preflight] ${label}=${port} held by OWN brand websockify pid=${c.pid} — sending SIGTERM`;
|
|
2562
|
+
}
|
|
2563
|
+
// Default — chromium / unknown OWN_BRAND
|
|
2564
|
+
return ` [preflight] ${label}=${port} held by OWN brand process pid=${c.pid} profile=${c.profilePath} — sending SIGTERM`;
|
|
2565
|
+
};
|
|
2518
2566
|
const checkInstallPortFree = (label, port) => {
|
|
2519
2567
|
let firstSsOutput;
|
|
2520
2568
|
try {
|
|
@@ -2541,7 +2589,7 @@ function installService() {
|
|
|
2541
2589
|
return;
|
|
2542
2590
|
}
|
|
2543
2591
|
if (r.kind === "OWN_BRAND" && r.pid !== undefined) {
|
|
2544
|
-
logFile(
|
|
2592
|
+
logFile(ownBrandAnnounceLine(label, port, r));
|
|
2545
2593
|
killNoThrow(r.pid, "SIGTERM");
|
|
2546
2594
|
sleepMs(300);
|
|
2547
2595
|
const after = classify(ssReadOrAbort(label, port));
|