@wipcomputer/wip-ldm-os 0.4.85-alpha.2 → 0.4.85-alpha.21

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 (37) hide show
  1. package/README.md +22 -2
  2. package/SKILL.md +8 -5
  3. package/bin/ldm.js +169 -65
  4. package/docs/universal-installer/SPEC.md +16 -3
  5. package/docs/universal-installer/TECHNICAL.md +4 -4
  6. package/lib/deploy.mjs +104 -20
  7. package/lib/detect.mjs +35 -4
  8. package/package.json +13 -2
  9. package/scripts/test-crc-agentid-tenant-boundary.mjs +80 -0
  10. package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
  11. package/scripts/test-crc-e2ee-session-route.mjs +129 -0
  12. package/scripts/test-crc-pair-login-flow.mjs +40 -0
  13. package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
  14. package/scripts/test-crc-pair-status-poll-token.mjs +73 -0
  15. package/scripts/test-install-prompt-policy.mjs +60 -0
  16. package/scripts/test-installer-skill-directory.mjs +55 -0
  17. package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
  18. package/scripts/test-installer-target-self-update.mjs +131 -0
  19. package/scripts/test-ldm-status-timeout.mjs +80 -0
  20. package/shared/templates/install-prompt.md +20 -2
  21. package/src/hosted-mcp/README.md +15 -0
  22. package/src/hosted-mcp/app/footer.js +74 -0
  23. package/src/hosted-mcp/app/kaleidoscope-login.html +846 -0
  24. package/src/hosted-mcp/app/pair.html +165 -57
  25. package/src/hosted-mcp/app/sprites.png +0 -0
  26. package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
  27. package/src/hosted-mcp/demo/index.html +3 -7
  28. package/src/hosted-mcp/demo/login.html +318 -20
  29. package/src/hosted-mcp/deploy.sh +307 -56
  30. package/src/hosted-mcp/docs/self-host.md +268 -0
  31. package/src/hosted-mcp/nginx/codex-relay.conf +25 -0
  32. package/src/hosted-mcp/nginx/conf.d/redact-logs.conf +60 -0
  33. package/src/hosted-mcp/nginx/mcp-oauth.conf +58 -0
  34. package/src/hosted-mcp/nginx/wip.computer.conf +25 -1
  35. package/src/hosted-mcp/scripts/audit-logs.sh +205 -0
  36. package/src/hosted-mcp/scripts/verify-deploy.sh +102 -0
  37. package/src/hosted-mcp/server.mjs +963 -146
package/lib/detect.mjs CHANGED
@@ -15,6 +15,23 @@ function readJSON(path) {
15
15
  }
16
16
  }
17
17
 
18
+ function detectSkillDirectories(repoPath) {
19
+ const skillsDir = join(repoPath, 'skills');
20
+ if (!existsSync(skillsDir)) return [];
21
+
22
+ try {
23
+ return readdirSync(skillsDir, { withFileTypes: true })
24
+ .filter(e => e.isDirectory() && existsSync(join(skillsDir, e.name, 'SKILL.md')))
25
+ .map(e => ({
26
+ name: e.name,
27
+ path: join(skillsDir, e.name),
28
+ skillPath: join(skillsDir, e.name, 'SKILL.md'),
29
+ }));
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
18
35
  /**
19
36
  * Detect all interfaces in a repo.
20
37
  * Returns { interfaces, pkg } where interfaces is an object keyed by interface type.
@@ -48,9 +65,18 @@ export function detectInterfaces(repoPath) {
48
65
  interfaces.openclaw = { config: readJSON(ocPlugin), path: ocPlugin };
49
66
  }
50
67
 
51
- // 5. Skill: SKILL.md exists
52
- if (existsSync(join(repoPath, 'SKILL.md'))) {
53
- interfaces.skill = { path: join(repoPath, 'SKILL.md') };
68
+ // 5. Skill: root SKILL.md, or a skills/<name>/SKILL.md collection.
69
+ const rootSkillPath = join(repoPath, 'SKILL.md');
70
+ if (existsSync(rootSkillPath)) {
71
+ interfaces.skill = { path: rootSkillPath };
72
+ }
73
+
74
+ const skillDirs = detectSkillDirectories(repoPath);
75
+ if (skillDirs.length > 0) {
76
+ interfaces.skill = {
77
+ path: join(repoPath, 'skills'),
78
+ skills: skillDirs,
79
+ };
54
80
  }
55
81
 
56
82
  // 6. Claude Code Hook: guard.mjs or claudeCode.hook(s) in package.json
@@ -103,7 +129,12 @@ export function describeInterfaces(interfaces) {
103
129
  if (interfaces.module) lines.push(`Module: ${JSON.stringify(interfaces.module.main)}`);
104
130
  if (interfaces.mcp) lines.push(`MCP Server: ${interfaces.mcp.file}`);
105
131
  if (interfaces.openclaw) lines.push(`OpenClaw Plugin: ${interfaces.openclaw.config?.name || 'detected'}`);
106
- if (interfaces.skill) lines.push(`Skill: SKILL.md`);
132
+ if (interfaces.skill?.skills?.length) {
133
+ const skills = interfaces.skill.skills.map(s => `${s.name} (${s.skillPath})`);
134
+ lines.push(`Skill: ${skills.join(', ')}`);
135
+ } else if (interfaces.skill) {
136
+ lines.push(`Skill: SKILL.md`);
137
+ }
107
138
  if (interfaces.claudeCodeHook) {
108
139
  const events = interfaces.claudeCodeHook.map(h => h.event || 'PreToolUse');
109
140
  lines.push(`Claude Code Hook: ${events.join(', ')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.85-alpha.2",
3
+ "version": "0.4.85-alpha.21",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -18,14 +18,25 @@
18
18
  "scripts": {
19
19
  "build:bridge": "cd src/bridge && npm install && npx tsup core.ts mcp-server.ts cli.ts openclaw.ts --format esm --dts --clean --outDir ../../dist/bridge",
20
20
  "build": "npm run build:bridge",
21
- "prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest",
21
+ "prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy && npm run test:ldm-status-timeout",
22
22
  "validate:bin-manifest": "node scripts/validate-bin-manifest.mjs",
23
23
  "test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
24
+ "test:install-prompt-policy": "node scripts/test-install-prompt-policy.mjs",
25
+ "test:ldm-status-timeout": "node scripts/test-ldm-status-timeout.mjs",
24
26
  "test:installer-update-tracks": "node scripts/test-installer-update-tracks.mjs",
25
27
  "test:installer-hook-toolname": "node scripts/test-installer-hook-toolname.mjs",
28
+ "test:installer-target-self-update": "node scripts/test-installer-target-self-update.mjs",
29
+ "test:installer-skill-directory": "node scripts/test-installer-skill-directory.mjs",
30
+ "test:installer-skill-dry-run-destinations": "node scripts/test-installer-skill-dry-run-destinations.mjs",
26
31
  "test:ldm-install-bin-shim": "node scripts/test-ldm-install-preserves-foreign-bin.mjs",
27
32
  "test:doctor-cron-target": "node scripts/test-doctor-cron-target.mjs",
28
33
  "test:bin-manifest": "node scripts/test-bin-manifest.mjs",
34
+ "test:crc-agentid-tenant-boundary": "node scripts/test-crc-agentid-tenant-boundary.mjs",
35
+ "test:crc-pair-login-flow": "node scripts/test-crc-pair-login-flow.mjs",
36
+ "test:crc-pair-status-poll-token": "node scripts/test-crc-pair-status-poll-token.mjs",
37
+ "test:crc-pair-relink-audit-and-rotation": "node scripts/test-crc-pair-relink-audit-and-rotation.mjs",
38
+ "test:crc-e2ee-key-persistence": "node scripts/test-crc-e2ee-key-persistence.mjs",
39
+ "test:crc-e2ee-session-route": "node scripts/test-crc-e2ee-session-route.mjs",
29
40
  "fmt": "npx prettier --write 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'",
30
41
  "fmt:check": "npx prettier --check 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'"
31
42
  },
@@ -0,0 +1,80 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ const server = readFileSync("src/hosted-mcp/server.mjs", "utf8");
5
+
6
+ function assertContains(needle, label) {
7
+ if (!server.includes(needle)) {
8
+ throw new Error(`${label} missing expected text: ${needle}`);
9
+ }
10
+ }
11
+
12
+ function assertNotContains(needle, label) {
13
+ if (server.includes(needle)) {
14
+ throw new Error(`${label} still contains forbidden text: ${needle}`);
15
+ }
16
+ }
17
+
18
+ assertContains('const ACCOUNT_TENANT_PREFIX = "acct:";', "account tenant prefix");
19
+ assertContains('const LEGACY_API_KEY_TENANT_PREFIX = "key:";', "legacy key tenant prefix");
20
+ assertContains('function accountTenantIdForUserId(userId)', "account tenant helper");
21
+ assertContains('function identityForApiKey(key)', "api key identity helper");
22
+ assertContains('return identityForApiKey(key);', "http auth uses identity helper");
23
+ assertContains("const agentId = accountTenantIdForUserId(stored.userId);", "registration uses immutable account tenant");
24
+ assertContains("function sanitizeDisplayLabel(raw)", "display label sanitizer");
25
+ assertContains('replace(/[\\u0000-\\u001f\\u007f]/g, "").replace(/\\s+/g, " ").trim().slice(0, 64)', "display label sanitizer preserves label semantics");
26
+ assertContains("const displayLabel = sanitizeDisplayLabel(body?.displayName || body?.username);", "registration treats entered name as display label");
27
+ assertContains("displayLabel,", "registration challenge stores display label");
28
+ assertContains("await saveApiKey(apiKey, agentId, { handle: credentialLabel });", "registration stores handle separately");
29
+ assertContains("p.handle = identity.handle;", "pair stores display handle separately");
30
+ assertContains("handle: identity.handle,", "relay metadata returns display handle");
31
+ assertContains("codexDaemons.has(identity.agentId)", "daemon presence uses tenant id");
32
+ assertContains("codexDaemonPubkeyRegistry.get(identity.agentId)", "daemon pubkey uses tenant id");
33
+ assertContains("agentId: identity.agentId,", "relay tickets bind tenant id");
34
+ assertContains("handle: identity.handle,", "relay tickets preserve display handle");
35
+ assertContains("codexDaemons.set(identity.agentId, ws);", "daemon ws keyed by tenant id");
36
+ assertContains("const key = codexRelayKey(identity.agentId, threadId);", "web ws keyed by tenant id");
37
+ assertContains("const daemonWs = codexDaemons.get(identity.agentId);", "web sends to tenant daemon");
38
+ assertNotContains("const agentId = stored.username || (\"passkey-\"", "registration must not use chosen handle as tenant");
39
+ assertNotContains("const existingKey = Object.entries(API_KEYS).find(([k, v]) => v === agentId);", "oauth must not reuse chosen handle as tenant");
40
+ assertNotContains("function isUsernameTaken", "display labels must not be globally unique usernames");
41
+ assertNotContains("function sanitizeUsername", "display labels must not be modeled as usernames");
42
+ assertNotContains('json(res, 409, { error: "reserved_handle"', "display labels must not be blocked as reserved security handles");
43
+ assertNotContains('json(res, 409, { error: "handle_taken"', "duplicate display labels must be allowed");
44
+
45
+ function legacyTenantIdForApiKey(key) {
46
+ return "key:" + createHash("sha256").update(key).digest("base64url").slice(0, 32);
47
+ }
48
+
49
+ function accountTenantIdForUserId(userId) {
50
+ return "acct:" + userId;
51
+ }
52
+
53
+ const chosenHandle = "parker-smoke-test";
54
+ const sharedDisplayLabel = "Parker";
55
+ const accountA = accountTenantIdForUserId("user-a");
56
+ const accountB = accountTenantIdForUserId("user-b");
57
+ const threadId = "thread-019dfa";
58
+ if (accountA === accountB) {
59
+ throw new Error("different user ids collapsed to one account tenant");
60
+ }
61
+ if (`${sharedDisplayLabel}:${threadId}` === `${accountA}:${threadId}` || `${sharedDisplayLabel}:${threadId}` === `${accountB}:${threadId}`) {
62
+ throw new Error("display label was used as a relay route key");
63
+ }
64
+
65
+ const legacyA = legacyTenantIdForApiKey("ck-a");
66
+ const legacyB = legacyTenantIdForApiKey("ck-b");
67
+ if (legacyA === legacyB) {
68
+ throw new Error("legacy API-key tenants collided");
69
+ }
70
+
71
+ const webKeyA = `${accountA}:${threadId}`;
72
+ const webKeyB = `${accountB}:${threadId}`;
73
+ if (webKeyA === webKeyB) {
74
+ throw new Error("same display handle can still collide across account tenants");
75
+ }
76
+ if (`${chosenHandle}:${threadId}` === webKeyA || `${chosenHandle}:${threadId}` === webKeyB) {
77
+ throw new Error("model still keys relay routes by display handle");
78
+ }
79
+
80
+ console.log("crc agentId tenant boundary checks passed");
@@ -0,0 +1,150 @@
1
+ import { readFileSync } from "node:fs";
2
+ import {
3
+ buildCodexBootstrapPayload,
4
+ createCodexDaemonPubkeyRegistry,
5
+ } from "../src/hosted-mcp/codex-relay-e2ee-registry.mjs";
6
+
7
+ const server = readFileSync("src/hosted-mcp/server.mjs", "utf8");
8
+ const registrySource = readFileSync("src/hosted-mcp/codex-relay-e2ee-registry.mjs", "utf8");
9
+ const deployScript = readFileSync("src/hosted-mcp/deploy.sh", "utf8");
10
+
11
+ function assertContains(haystack, needle, label) {
12
+ if (!haystack.includes(needle)) {
13
+ throw new Error(`${label} missing expected text: ${needle}`);
14
+ }
15
+ }
16
+
17
+ function assertBefore(haystack, first, second, label) {
18
+ const firstIndex = haystack.indexOf(first);
19
+ const secondIndex = haystack.indexOf(second);
20
+ if (firstIndex === -1 || secondIndex === -1 || firstIndex >= secondIndex) {
21
+ throw new Error(`${label} expected "${first}" before "${second}"`);
22
+ }
23
+ }
24
+
25
+ function assert(condition, label, detail = "") {
26
+ if (!condition) throw new Error(`${label}${detail ? ": " + detail : ""}`);
27
+ }
28
+
29
+ function bootstrapPayloadFor(registry, identity, threadId, daemonOnline = true) {
30
+ return buildCodexBootstrapPayload({
31
+ identity,
32
+ threadId,
33
+ daemonOnline,
34
+ daemonKey: registry.get(identity.agentId),
35
+ });
36
+ }
37
+
38
+ function createFakePrisma() {
39
+ const rows = new Map();
40
+ return {
41
+ rows,
42
+ async $executeRawUnsafe(sql, tenantId, pubkey, cryptoVersionsJson) {
43
+ if (/CREATE TABLE IF NOT EXISTS codex_daemon_e2ee_keys/.test(sql)) return;
44
+ if (/CREATE TABLE IF NOT EXISTS codex_daemon_e2ee_key_audit/.test(sql)) return;
45
+ if (/INSERT INTO codex_daemon_e2ee_keys/.test(sql)) {
46
+ rows.set(tenantId, {
47
+ tenant_id: tenantId,
48
+ pubkey,
49
+ crypto_versions_json: cryptoVersionsJson,
50
+ registered_at: new Date("2026-05-11T17:37:18.000Z"),
51
+ });
52
+ return;
53
+ }
54
+ if (/INSERT INTO codex_daemon_e2ee_key_audit/.test(sql)) return;
55
+ throw new Error("unexpected fake prisma execute: " + sql);
56
+ },
57
+ async $queryRawUnsafe(sql) {
58
+ if (/FROM codex_daemon_e2ee_keys/.test(sql)) return [...rows.values()];
59
+ throw new Error("unexpected fake prisma query: " + sql);
60
+ },
61
+ };
62
+ }
63
+
64
+ function createSilentLogger() {
65
+ return { log() {}, error() {} };
66
+ }
67
+
68
+ assertContains(registrySource, "CREATE TABLE IF NOT EXISTS codex_daemon_e2ee_keys", "persistent key table");
69
+ assertContains(registrySource, "async function loadFromDb()", "boot load helper");
70
+ assertContains(registrySource, "async function persist(agentId, pubkey, cryptoVersions)", "persist helper");
71
+ assertContains(registrySource, "function register(agentId, pubkey, cryptoVersions, source)", "registration helper");
72
+ assertContains(registrySource, "pubkeys.set(agentId, {", "registration updates in-memory bootstrap cache");
73
+ assertContains(registrySource, "return persist(agentId, pubkey, normalizedVersions)", "registration persists after cache update");
74
+ assertContains(server, "await codexDaemonPubkeyRegistry.loadFromDb();", "server boot load call");
75
+ assertContains(server, "await codexDaemonPubkeyRegistry.register(identity.agentId, p.daemon_public_key, p.crypto_versions, \"pair-complete\");", "pair-complete persists key");
76
+ assertContains(server, "if (envelope?.type === \"daemon.identity\") {", "daemon reconnect identity frame");
77
+ assertContains(server, "codexDaemonPubkeyRegistry.register(", "daemon reconnect register call");
78
+ assertContains(server, "buildCodexBootstrapPayload({ identity, threadId, daemonOnline, daemonKey })", "bootstrap uses shared payload builder");
79
+ assertContains(deployScript, "codex-relay-e2ee-registry.mjs", "hosted deploy copies registry module");
80
+ assertBefore(
81
+ server,
82
+ "await codexDaemonPubkeyRegistry.loadFromDb();",
83
+ "function handleCodexBootstrap(req, res, threadId)",
84
+ "persisted keys load before bootstrap handler",
85
+ );
86
+
87
+ const identity = {
88
+ agentId: "acct:test-user-a",
89
+ tenantId: "acct:test-user-a",
90
+ handle: "Parker smoke test",
91
+ apiKey: "ck-test",
92
+ };
93
+ const threadId = "019dfa1e-0c3d-7f01-86b9-9a22cd452bde";
94
+
95
+ const fakePrisma = createFakePrisma();
96
+ const registryBeforeRestart = createCodexDaemonPubkeyRegistry({
97
+ usePrisma: true,
98
+ prisma: fakePrisma,
99
+ devMode: false,
100
+ logger: createSilentLogger(),
101
+ });
102
+ await registryBeforeRestart.register(identity.agentId, "spki-key-before-restart", ["e2ee-v1"], "pair-complete");
103
+
104
+ const beforeRestartBootstrap = bootstrapPayloadFor(registryBeforeRestart, identity, threadId);
105
+ assert(beforeRestartBootstrap.e2ee_available === true, "bootstrap reports e2ee before restart");
106
+ assert(beforeRestartBootstrap.daemon_public_key === "spki-key-before-restart", "bootstrap returns registered daemon key before restart");
107
+
108
+ const registryAfterRestart = createCodexDaemonPubkeyRegistry({
109
+ usePrisma: true,
110
+ prisma: fakePrisma,
111
+ devMode: false,
112
+ logger: createSilentLogger(),
113
+ });
114
+ await registryAfterRestart.loadFromDb();
115
+
116
+ const afterRestartBootstrap = bootstrapPayloadFor(registryAfterRestart, identity, threadId);
117
+ assert(afterRestartBootstrap.e2ee_available === true, "bootstrap reports e2ee after restart from persisted key");
118
+ assert(afterRestartBootstrap.daemon_public_key === "spki-key-before-restart", "bootstrap restores persisted daemon key after restart");
119
+ assert(afterRestartBootstrap.daemon_crypto_versions?.[0] === "e2ee-v1", "bootstrap restores crypto versions after restart");
120
+
121
+ const emptyFakePrisma = createFakePrisma();
122
+ const registryBeforeReconnect = createCodexDaemonPubkeyRegistry({
123
+ usePrisma: true,
124
+ prisma: emptyFakePrisma,
125
+ devMode: false,
126
+ logger: createSilentLogger(),
127
+ });
128
+ await registryBeforeReconnect.loadFromDb();
129
+ const beforeReconnectBootstrap = bootstrapPayloadFor(registryBeforeReconnect, identity, threadId);
130
+ assert(beforeReconnectBootstrap.e2ee_available === false, "bootstrap is not e2ee available before daemon reconnect when no key exists");
131
+ assert(beforeReconnectBootstrap.daemon_public_key === null, "bootstrap has no daemon key before daemon reconnect");
132
+
133
+ await registryBeforeReconnect.register(identity.agentId, "spki-key-from-daemon-reconnect", [], "daemon-reconnect");
134
+ const afterReconnectBootstrap = bootstrapPayloadFor(registryBeforeReconnect, identity, threadId);
135
+ assert(afterReconnectBootstrap.e2ee_available === true, "bootstrap reports e2ee after daemon reconnect self-heal");
136
+ assert(afterReconnectBootstrap.daemon_public_key === "spki-key-from-daemon-reconnect", "bootstrap returns daemon reconnect key");
137
+ assert(afterReconnectBootstrap.daemon_crypto_versions?.[0] === "e2ee-v1", "daemon reconnect defaults crypto version");
138
+
139
+ const registryAfterReconnectRestart = createCodexDaemonPubkeyRegistry({
140
+ usePrisma: true,
141
+ prisma: emptyFakePrisma,
142
+ devMode: false,
143
+ logger: createSilentLogger(),
144
+ });
145
+ await registryAfterReconnectRestart.loadFromDb();
146
+ const afterReconnectRestartBootstrap = bootstrapPayloadFor(registryAfterReconnectRestart, identity, threadId);
147
+ assert(afterReconnectRestartBootstrap.e2ee_available === true, "daemon reconnect key is persisted for the next restart");
148
+ assert(afterReconnectRestartBootstrap.daemon_public_key === "spki-key-from-daemon-reconnect", "daemon reconnect key survives restart");
149
+
150
+ console.log("crc e2ee key persistence restart regression checks passed");
@@ -0,0 +1,129 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const server = readFileSync("src/hosted-mcp/server.mjs", "utf8");
4
+
5
+ function assertContains(needle, label) {
6
+ if (!server.includes(needle)) {
7
+ throw new Error(`${label} missing expected text: ${needle}`);
8
+ }
9
+ }
10
+
11
+ function assertBefore(first, second, label) {
12
+ const firstIndex = server.indexOf(first);
13
+ const secondIndex = firstIndex === -1 ? -1 : server.indexOf(second, firstIndex + first.length);
14
+ if (firstIndex === -1 || secondIndex === -1 || firstIndex >= secondIndex) {
15
+ throw new Error(`${label} expected "${first}" before "${second}"`);
16
+ }
17
+ }
18
+
19
+ assertContains("const codexWebClients = new Map(); // `${agentId}:${threadId}` -> Set<ws>", "thread-keyed web client sets");
20
+ assertContains("const codexE2eeSessionRoutes = new Map(); // `${agentId}:${e2eeSession}` -> { threadId, webKey, ws }", "e2ee session route map");
21
+ assertContains("function registerCodexE2eeSessionRoute(agentId, e2eeSession, threadId, ws)", "route registration helper");
22
+ assertContains("codexE2eeSessionRoutes.set(codexRelayKey(agentId, e2eeSession), { threadId, webKey, ws });", "route map stores e2ee session to thread");
23
+ assertContains("function addCodexWebClient(webKey, ws)", "web client set add helper");
24
+ assertContains("function removeCodexWebClient(webKey, ws)", "web client set remove helper");
25
+ assertContains("function openCodexWebClientsForKey(webKey)", "web client set read helper");
26
+ assertContains("function resolveCodexWebClientsForDaemonFrame(agentId, routeId)", "daemon route resolver");
27
+ assertContains("const routed = codexE2eeSessionRoutes.get(codexRelayKey(agentId, routeId));", "daemon route lookup uses e2ee session map");
28
+ assertContains("if (routed && routed.ws && routed.ws.readyState === routed.ws.OPEN) return [routed.ws];", "daemon route resolves to active owner socket");
29
+ assertContains("return openCodexWebClientsForKey(codexRelayKey(agentId, routeId));", "daemon route keeps direct thread fallback");
30
+ assertContains("const targets = resolveCodexWebClientsForDaemonFrame(identity.agentId, sessionId);", "daemon frames use route resolver");
31
+ assertContains("for (const target of targets) {", "daemon frames send to every resolved target");
32
+ assertContains("if (isCodexE2eeEnvelope(envelope) && envelope.session) {", "web e2ee messages are detected");
33
+ assertContains("envelope.route_thread_id = threadId;", "relay injects ticket-bound thread into e2ee hello");
34
+ assertContains("text = JSON.stringify(envelope);", "relay forwards the route-bound e2ee hello");
35
+ assertContains("registerCodexE2eeSessionRoute(identity.agentId, envelope.session, threadId, ws);", "web e2ee session is registered");
36
+ assertContains("const clientCount = addCodexWebClient(key, ws);", "new web connections are added without replacing existing clients");
37
+ assertContains("removeCodexWebClient(key, ws);", "close cleanup removes only the closing socket");
38
+ assertContains("removeCodexE2eeRoutesForWeb(identity.agentId, threadId, ws);", "close cleanup");
39
+ assertContains("if (route.webKey === webKey && (!ws || route.ws === ws)) {", "cleanup only removes routes owned by the closing socket");
40
+ assertBefore(
41
+ "envelope.route_thread_id = threadId;",
42
+ "daemonWs.send(text);",
43
+ "web route thread is injected before forwarding to daemon",
44
+ );
45
+ assertBefore(
46
+ "registerCodexE2eeSessionRoute(identity.agentId, envelope.session, threadId, ws);",
47
+ "daemonWs.send(text);",
48
+ "web session route registered before forwarding to daemon",
49
+ );
50
+ if (server.includes("const previous = codexWebClients.get(key);")) {
51
+ throw new Error("web client replacement lookup is still present");
52
+ }
53
+ if (server.includes("codexWebClients.set(key, ws);")) {
54
+ throw new Error("web client singleton set is still present");
55
+ }
56
+
57
+ const OPEN = 1;
58
+ const agentId = "parker-smoke-test";
59
+ const threadId = "thread-123";
60
+ const e2eeSession = "e2ee-random-session-456";
61
+ if (e2eeSession === threadId) {
62
+ throw new Error("test setup must use a random E2EE session distinct from threadId");
63
+ }
64
+
65
+ const modelWebClients = new Map();
66
+ const modelRoutes = new Map();
67
+ const webSocketA = { readyState: OPEN, OPEN };
68
+ const webSocketB = { readyState: OPEN, OPEN };
69
+ const webSocketC = { readyState: OPEN, OPEN };
70
+ const webKey = `${agentId}:${threadId}`;
71
+
72
+ function modelAddWebClient(ws) {
73
+ let clients = modelWebClients.get(webKey);
74
+ if (!clients) {
75
+ clients = new Set();
76
+ modelWebClients.set(webKey, clients);
77
+ }
78
+ clients.add(ws);
79
+ }
80
+
81
+ function modelRegister(session, ws) {
82
+ modelRoutes.set(`${agentId}:${session}`, { webKey, ws });
83
+ }
84
+
85
+ function modelResolve(routeId) {
86
+ const route = modelRoutes.get(`${agentId}:${routeId}`);
87
+ if (route && route.ws.readyState === route.ws.OPEN) return [route.ws];
88
+ const clients = modelWebClients.get(`${agentId}:${routeId}`) || new Set();
89
+ return [...clients].filter((webWs) => webWs.readyState === webWs.OPEN);
90
+ }
91
+
92
+ function modelRemoveOwnedRoutes(ws) {
93
+ for (const [routeKey, route] of modelRoutes) {
94
+ if (route.webKey === webKey && route.ws === ws) modelRoutes.delete(routeKey);
95
+ }
96
+ }
97
+
98
+ function modelRemoveWebClient(ws) {
99
+ const clients = modelWebClients.get(webKey);
100
+ if (!clients) return;
101
+ clients.delete(ws);
102
+ if (clients.size === 0) modelWebClients.delete(webKey);
103
+ }
104
+
105
+ modelAddWebClient(webSocketA);
106
+ modelAddWebClient(webSocketB);
107
+ modelRegister(e2eeSession, webSocketA);
108
+ if (modelResolve(e2eeSession)[0] !== webSocketA) {
109
+ throw new Error("random E2EE session did not route to the owning thread web socket");
110
+ }
111
+ const threadTargets = modelResolve(threadId);
112
+ if (threadTargets.length !== 2 || !threadTargets.includes(webSocketA) || !threadTargets.includes(webSocketB)) {
113
+ throw new Error("direct thread fallback did not broadcast to every thread web socket");
114
+ }
115
+
116
+ modelRemoveOwnedRoutes(webSocketA);
117
+ modelRemoveWebClient(webSocketA);
118
+ modelAddWebClient(webSocketC);
119
+ modelRegister(e2eeSession, webSocketC);
120
+ modelRemoveOwnedRoutes(webSocketA);
121
+ if (modelResolve(e2eeSession)[0] !== webSocketC) {
122
+ throw new Error("old socket cleanup removed or stole the replacement E2EE route");
123
+ }
124
+ const remainingThreadTargets = modelResolve(threadId);
125
+ if (remainingThreadTargets.length !== 2 || !remainingThreadTargets.includes(webSocketB) || !remainingThreadTargets.includes(webSocketC)) {
126
+ throw new Error("closing one web socket removed another browser client");
127
+ }
128
+
129
+ console.log("crc e2ee session route checks passed");
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const server = readFileSync("src/hosted-mcp/server.mjs", "utf8");
4
+ const loginFiles = [
5
+ "src/hosted-mcp/app/kaleidoscope-login.html",
6
+ "src/hosted-mcp/demo/login.html",
7
+ ];
8
+
9
+ function assertContains(source, needle, label) {
10
+ if (!source.includes(needle)) {
11
+ throw new Error(`${label} missing expected text: ${needle}`);
12
+ }
13
+ }
14
+
15
+ function assertBefore(source, first, second, label) {
16
+ const firstIndex = source.indexOf(first);
17
+ const secondIndex = source.indexOf(second);
18
+ if (firstIndex === -1 || secondIndex === -1 || firstIndex >= secondIndex) {
19
+ throw new Error(`${label} expected "${first}" before "${second}"`);
20
+ }
21
+ }
22
+
23
+ assertContains(server, "const PAIR_NEXT_REGEX = /^\\/pair\\/[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;", "server pair regex");
24
+ assertContains(server, "const REMOTE_CONTROL_NEXT_REGEX = /^\\/codex-remote-control\\/", "server remote-control regex");
25
+ assertContains(server, "purpose, // \"pair\" | null", "server stores pair purpose");
26
+ assertContains(server, "next: next || null, // sanitized `/pair/<CODE>` or null", "server stores sanitized next");
27
+ assertContains(server, "json(res, 200, { status: \"approved\", agentId: entry.agentId });", "server strips desktop pair status");
28
+ assertContains(server, "json(res, 200, { ok: true, next: entry.next });", "server returns next to phone approve");
29
+
30
+ for (const file of loginFiles) {
31
+ const html = readFileSync(file, "utf8");
32
+ assertContains(html, "function isPairNextOnDesktop()", `${file} desktop pair helper`);
33
+ assertContains(html, "} else if (isPairNextOnDesktop()) {", `${file} auto-start desktop pair QR`);
34
+ assertContains(html, "startQrLogin('', 'signin');", `${file} pair QR uses sign-in mode`);
35
+ assertContains(html, "if (approveResponse && typeof approveResponse.next === 'string' && isWhitelistedNext(approveResponse.next))", `${file} consumes approve next`);
36
+ assertContains(html, "if (urlNext && PAIR_NEXT_REGEX.test(urlNext))", `${file} desktop pair approved branch`);
37
+ assertBefore(html, "if (isPairNextOnDesktop() && !qrSessionMode)", "if (needsCustomQR() && !qrSessionMode)", `${file} create button forces pair QR before normal QR fallback`);
38
+ }
39
+
40
+ console.log("crc pair login flow checks passed");