@wipcomputer/wip-ldm-os 0.4.85-alpha.8 → 0.4.86

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 (57) hide show
  1. package/README.md +22 -2
  2. package/SKILL.md +137 -15
  3. package/bin/ldm.js +608 -75
  4. package/lib/deploy.mjs +90 -12
  5. package/lib/registry-migrations.mjs +296 -0
  6. package/package.json +21 -3
  7. package/scripts/test-boot-dir-truth.mjs +158 -0
  8. package/scripts/test-boot-hook-registration.mjs +136 -0
  9. package/scripts/test-boot-payload-trim.mjs +200 -0
  10. package/scripts/test-crc-agentid-tenant-boundary.mjs +2 -2
  11. package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
  12. package/scripts/test-crc-e2ee-session-route.mjs +9 -2
  13. package/scripts/test-crc-pair-login-flow.mjs +76 -1
  14. package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
  15. package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
  16. package/scripts/test-deploy-hook-ownership.mjs +160 -0
  17. package/scripts/test-doctor-hook-dedupe.mjs +188 -0
  18. package/scripts/test-install-prompt-policy.mjs +84 -0
  19. package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
  20. package/scripts/test-installer-target-self-update.mjs +131 -0
  21. package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
  22. package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
  23. package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
  24. package/scripts/test-ldm-status-concurrency.mjs +118 -0
  25. package/scripts/test-ldm-status-timeout.mjs +96 -0
  26. package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
  27. package/scripts/test-readme-install-prompt.mjs +66 -0
  28. package/shared/boot/boot-config.json +18 -8
  29. package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
  30. package/shared/rules/security.md +4 -0
  31. package/shared/templates/install-prompt.md +20 -2
  32. package/src/boot/README.md +24 -1
  33. package/src/boot/boot-config.json +18 -8
  34. package/src/boot/boot-hook.mjs +118 -28
  35. package/src/boot/installer.mjs +33 -10
  36. package/src/hosted-mcp/.env.example +4 -0
  37. package/src/hosted-mcp/README.md +37 -0
  38. package/src/hosted-mcp/app/footer.js +2 -2
  39. package/src/hosted-mcp/app/kaleidoscope-login.html +489 -42
  40. package/src/hosted-mcp/app/pair.html +21 -3
  41. package/src/hosted-mcp/app/wip-logo.png +0 -0
  42. package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
  43. package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
  44. package/src/hosted-mcp/demo/footer.js +2 -2
  45. package/src/hosted-mcp/demo/index.html +166 -44
  46. package/src/hosted-mcp/demo/login.html +87 -23
  47. package/src/hosted-mcp/demo/privacy.html +4 -214
  48. package/src/hosted-mcp/demo/tos.html +4 -189
  49. package/src/hosted-mcp/deploy.sh +2 -0
  50. package/src/hosted-mcp/docs/self-host.md +268 -0
  51. package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
  52. package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
  53. package/src/hosted-mcp/legal/legal-footer.js +75 -0
  54. package/src/hosted-mcp/legal/legal.css +166 -0
  55. package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
  56. package/src/hosted-mcp/legal/privacy/index.html +253 -0
  57. package/src/hosted-mcp/server.mjs +920 -74
@@ -0,0 +1,89 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ const loginFiles = [
4
+ "src/hosted-mcp/app/kaleidoscope-login.html",
5
+ "src/hosted-mcp/demo/login.html",
6
+ ];
7
+
8
+ function assertContains(source, needle, label) {
9
+ if (!source.includes(needle)) {
10
+ throw new Error(`${label} missing expected text: ${needle}`);
11
+ }
12
+ }
13
+
14
+ function assertBefore(source, first, second, label) {
15
+ const firstIndex = source.indexOf(first);
16
+ const secondIndex = source.indexOf(second);
17
+ if (firstIndex === -1 || secondIndex === -1 || firstIndex >= secondIndex) {
18
+ throw new Error(`${label} expected "${first}" before "${second}"`);
19
+ }
20
+ }
21
+
22
+ function extractFunction(source, name) {
23
+ const start = source.indexOf(`function ${name}(`);
24
+ if (start === -1) throw new Error(`could not find function ${name}`);
25
+ const bodyStart = source.indexOf("{", start);
26
+ let depth = 0;
27
+ for (let i = bodyStart; i < source.length; i += 1) {
28
+ if (source[i] === "{") depth += 1;
29
+ if (source[i] === "}") depth -= 1;
30
+ if (depth === 0) return source.slice(start, i + 1);
31
+ }
32
+ throw new Error(`could not extract function ${name}`);
33
+ }
34
+
35
+ function assertNotContains(source, needle, label) {
36
+ if (source.includes(needle)) {
37
+ throw new Error(`${label} must not contain: ${needle}`);
38
+ }
39
+ }
40
+
41
+ for (const file of loginFiles) {
42
+ const html = readFileSync(file, "utf8");
43
+ const followFunction = extractFunction(html, "followPairNextIfPresent");
44
+ const backFunction = extractFunction(html, "backToLoginAfterQrScan");
45
+
46
+ assertContains(html, 'id="qr-auth-confirm-view"', `${file} confirmation view`);
47
+ assertContains(html, "Your authenticated Kaleidoscope session is available on your other device.", `${file} confirmation headline`);
48
+ assertContains(html, "Back to login", `${file} back button copy`);
49
+ assertNotContains(html, "Open session here", `${file} authenticator completion must not offer local demo open`);
50
+ assertNotContains(html, ">Close</button>", `${file} authenticator completion must not show close action`);
51
+ assertNotContains(html, "Kaleidoscope is ready there.", `${file} removed ready helper copy`);
52
+ assertNotContains(html, "Open Kaleidoscope here", `${file} removed duplicated open copy`);
53
+ assertNotContains(html, "Keep using Kaleidoscope on the other device.", `${file} removed cancel helper copy`);
54
+ assertNotContains(html, "Scan QR Code", `${file} login-page scanner action must be absent`);
55
+ assertNotContains(html, "BarcodeDetector", `${file} login-page scanner detector must be absent`);
56
+ assertNotContains(html, "getUserMedia", `${file} login-page scanner camera code must be absent`);
57
+ assertContains(html, "You can close this page.", `${file} close fallback copy`);
58
+
59
+ assertContains(html, "function showQrAuthenticatorConfirmation(identity, isNewAccount)", `${file} confirmation function`);
60
+ assertContains(html, "function backToLoginAfterQrScan()", `${file} back-to-login function`);
61
+
62
+ assertContains(
63
+ html,
64
+ "if (DEMO_NEXT_REGEX.test(approveResponse.next)) {\n return showQrAuthenticatorConfirmation(identity, isNewAccount === true);\n }",
65
+ `${file} QR authenticator demo next shows confirmation`,
66
+ );
67
+ assertBefore(
68
+ html,
69
+ "return showQrAuthenticatorConfirmation(identity, isNewAccount === true);",
70
+ "next = approveResponse.next;",
71
+ `${file} confirmation intercepts approve next before redirect`,
72
+ );
73
+ assertNotContains(followFunction, "location.replace('/demo');", `${file} QR approve path must not directly enter demo`);
74
+ assertNotContains(followFunction, 'location.href = "/demo";', `${file} QR approve path must not assign demo href`);
75
+ assertNotContains(followFunction, "storeDemoHandoffIfNeeded('/demo'", `${file} QR approve path must not store demo handoff`);
76
+
77
+ assertContains(backFunction, "location.replace('/login?next=/demo');", `${file} back returns to login`);
78
+ assertNotContains(backFunction, "storeDemoHandoffIfNeeded", `${file} back must not store demo handoff`);
79
+ assertNotContains(backFunction, "location.replace('/demo')", `${file} back must not open demo`);
80
+
81
+ assertContains(html, "if (await followPairNextIfPresent(approveResponse, result, true)) return;", `${file} create caller stops after confirmation`);
82
+ assertContains(html, "if (await followPairNextIfPresent(approveResponse, result, false)) return;", `${file} sign-in caller stops after confirmation`);
83
+ assertContains(html, "storeDemoHandoffIfNeeded(data.next, data, qrLoginMode === 'register');", `${file} requester QR status still stores handoff`);
84
+ assertContains(html, "location.replace(data.next);", `${file} requester QR status still redirects`);
85
+ assertContains(html, "storeDemoHandoffIfNeeded('/demo', result, true);", `${file} same-device create still stores demo handoff`);
86
+ assertContains(html, "storeDemoHandoffIfNeeded('/demo', result, false);", `${file} same-device sign-in still stores demo handoff`);
87
+ }
88
+
89
+ console.log("kaleidoscope QR authenticator confirmation checks passed");
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { createServer } from 'node:http';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
10
+ const tempRoot = mkdtempSync(join(tmpdir(), 'ldm-status-concurrency-'));
11
+ const sourceVersion = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
12
+
13
+ function assert(condition, message) {
14
+ if (!condition) throw new Error(message);
15
+ }
16
+
17
+ function listen(server) {
18
+ return new Promise((resolve) => {
19
+ server.listen(0, '127.0.0.1', () => resolve(server.address()));
20
+ });
21
+ }
22
+
23
+ function createRegistryServer(delayMs) {
24
+ let active = 0;
25
+ let maxActive = 0;
26
+ const server = createServer((_req, res) => {
27
+ active += 1;
28
+ maxActive = Math.max(maxActive, active);
29
+ setTimeout(() => {
30
+ res.setHeader('content-type', 'application/json');
31
+ res.end(JSON.stringify({ 'dist-tags': { latest: '1.0.0' } }));
32
+ active -= 1;
33
+ }, delayMs);
34
+ });
35
+ return { server, getMaxActive: () => maxActive };
36
+ }
37
+
38
+ function writeFixture(home) {
39
+ const extensions = join(home, '.ldm', 'extensions');
40
+ mkdirSync(extensions, { recursive: true });
41
+ writeFileSync(join(home, '.ldm', 'version.json'), JSON.stringify({
42
+ version: '0.0.0-test',
43
+ installed: '2026-05-12T00:00:00.000Z',
44
+ updated: '2026-05-12T00:00:00.000Z',
45
+ }, null, 2) + '\n');
46
+
47
+ const registry = { extensions: {} };
48
+ for (let i = 1; i <= 8; i += 1) {
49
+ registry.extensions[`ext-${i}`] = {
50
+ source: { npm: `ext-${i}` },
51
+ installed: { version: '1.0.0' },
52
+ };
53
+ }
54
+ writeFileSync(join(extensions, 'registry.json'), JSON.stringify(registry, null, 2) + '\n');
55
+ }
56
+
57
+ function runStatus({ concurrency, registryUrl, home }) {
58
+ return new Promise((resolve) => {
59
+ const child = spawn(process.execPath, [join(root, 'bin', 'ldm.js'), 'status'], {
60
+ cwd: root,
61
+ env: {
62
+ ...process.env,
63
+ HOME: home,
64
+ LDM_STATUS_NPM_REGISTRY_URL: registryUrl,
65
+ LDM_STATUS_NPM_CONCURRENCY: String(concurrency),
66
+ LDM_STATUS_NPM_TIMEOUT_MS: '2000',
67
+ LDM_STATUS_TOTAL_BUDGET_MS: '10000',
68
+ },
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ });
71
+
72
+ let stdout = '';
73
+ let stderr = '';
74
+ child.stdout.setEncoding('utf8');
75
+ child.stderr.setEncoding('utf8');
76
+ child.stdout.on('data', chunk => { stdout += chunk; });
77
+ child.stderr.on('data', chunk => { stderr += chunk; });
78
+ child.on('close', status => resolve({ status, stdout, stderr }));
79
+ });
80
+ }
81
+
82
+ async function runFixture({ concurrency, delayMs }) {
83
+ const home = join(tempRoot, `home-${concurrency}-${delayMs}`);
84
+ writeFixture(home);
85
+ const registry = createRegistryServer(delayMs);
86
+ const address = await listen(registry.server);
87
+ const startedAt = Date.now();
88
+ const result = await runStatus({
89
+ concurrency,
90
+ home,
91
+ registryUrl: `http://${address.address}:${address.port}`,
92
+ });
93
+ const elapsedMs = Date.now() - startedAt;
94
+ registry.server.closeAllConnections();
95
+ registry.server.close();
96
+ return { result, elapsedMs, maxActive: registry.getMaxActive() };
97
+ }
98
+
99
+ try {
100
+ const concurrent = await runFixture({ concurrency: 4, delayMs: 500 });
101
+ assert(concurrent.result.status === 0, `concurrent ldm status exited ${concurrent.result.status}\nstdout:\n${concurrent.result.stdout}\nstderr:\n${concurrent.result.stderr}`);
102
+ assert(concurrent.elapsedMs < 3000, `concurrent ldm status should finish well before serial runtime; elapsed ${concurrent.elapsedMs}ms`);
103
+ assert(concurrent.maxActive >= 4, `registry server should see concurrent probes; max active ${concurrent.maxActive}`);
104
+ assert(concurrent.result.stdout.includes(`LDM OS v${sourceVersion}`), `status should print installed LDM OS version\n${concurrent.result.stdout}`);
105
+ assert(concurrent.result.stdout.includes('Extensions: 8'), `status should print extension count\n${concurrent.result.stdout}`);
106
+ assert(concurrent.result.stdout.includes('ext-8: checking npm'), `status should check every staged extension\n${concurrent.result.stdout}`);
107
+ assert(!concurrent.result.stdout.includes('Update checks skipped:'), `concurrent status should not skip checks in this fixture\n${concurrent.result.stdout}`);
108
+
109
+ const serialFallback = await runFixture({ concurrency: 1, delayMs: 10 });
110
+ assert(serialFallback.result.status === 0, `serial fallback ldm status exited ${serialFallback.result.status}\nstdout:\n${serialFallback.result.stdout}\nstderr:\n${serialFallback.result.stderr}`);
111
+ assert(serialFallback.maxActive === 1, `serial fallback should only run one probe at a time; max active ${serialFallback.maxActive}`);
112
+ assert(serialFallback.result.stdout.includes('ext-8: checking npm'), `serial fallback should still check every staged extension\n${serialFallback.result.stdout}`);
113
+ assert(!serialFallback.result.stdout.includes('Update checks skipped:'), `serial fallback should not skip checks in this fixture\n${serialFallback.result.stdout}`);
114
+ } finally {
115
+ rmSync(tempRoot, { recursive: true, force: true });
116
+ }
117
+
118
+ console.log('ldm status concurrency regression passed');
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { createServer } from 'node:http';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const root = dirname(dirname(fileURLToPath(import.meta.url)));
10
+ const tempRoot = mkdtempSync(join(tmpdir(), 'ldm-status-timeout-'));
11
+ const sourceVersion = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
12
+
13
+ function assert(condition, message) {
14
+ if (!condition) throw new Error(message);
15
+ }
16
+
17
+ function runStatus({ home, registryUrl }) {
18
+ return new Promise((resolve) => {
19
+ const child = spawn(process.execPath, [join(root, 'bin', 'ldm.js'), 'status'], {
20
+ cwd: root,
21
+ env: {
22
+ ...process.env,
23
+ HOME: home,
24
+ LDM_STATUS_NPM_REGISTRY_URL: registryUrl,
25
+ LDM_STATUS_NPM_TIMEOUT_MS: '75',
26
+ LDM_STATUS_TOTAL_BUDGET_MS: '250',
27
+ },
28
+ stdio: ['ignore', 'pipe', 'pipe'],
29
+ });
30
+
31
+ let stdout = '';
32
+ let stderr = '';
33
+ child.stdout.setEncoding('utf8');
34
+ child.stderr.setEncoding('utf8');
35
+ child.stdout.on('data', chunk => { stdout += chunk; });
36
+ child.stderr.on('data', chunk => { stderr += chunk; });
37
+ child.on('close', status => resolve({ status, stdout, stderr }));
38
+ });
39
+ }
40
+
41
+ function listen(server) {
42
+ return new Promise((resolve) => {
43
+ server.listen(0, '127.0.0.1', () => resolve(server.address()));
44
+ });
45
+ }
46
+
47
+ try {
48
+ const home = join(tempRoot, 'home');
49
+ const extensions = join(home, '.ldm', 'extensions');
50
+
51
+ mkdirSync(extensions, { recursive: true });
52
+ writeFileSync(join(home, '.ldm', 'version.json'), JSON.stringify({
53
+ version: '0.0.0-test',
54
+ installed: '2026-05-12T00:00:00.000Z',
55
+ updated: '2026-05-12T00:00:00.000Z',
56
+ }, null, 2) + '\n');
57
+ writeFileSync(join(extensions, 'registry.json'), JSON.stringify({
58
+ extensions: {
59
+ 'hung-extension': {
60
+ source: { npm: 'hung-extension' },
61
+ installed: { version: '1.0.0' },
62
+ },
63
+ 'second-extension': {
64
+ source: { npm: 'second-extension' },
65
+ installed: { version: '1.0.0' },
66
+ },
67
+ },
68
+ }, null, 2) + '\n');
69
+
70
+ const server = createServer((_req, res) => {
71
+ setTimeout(() => {
72
+ res.setHeader('content-type', 'application/json');
73
+ res.end(JSON.stringify({ 'dist-tags': { latest: '9.9.9' } }));
74
+ }, 2000);
75
+ });
76
+ const address = await listen(server);
77
+
78
+ const startedAt = Date.now();
79
+ const result = await runStatus({ home, registryUrl: `http://${address.address}:${address.port}` });
80
+ const elapsedMs = Date.now() - startedAt;
81
+ server.closeAllConnections();
82
+ server.close();
83
+
84
+ assert(result.status === 0, `ldm status exited ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
85
+ assert(elapsedMs < 2500, `ldm status should return before the process timeout; elapsed ${elapsedMs}ms`);
86
+ assert(result.stdout.includes(`LDM OS v${sourceVersion}`), `status should print installed LDM OS version\n${result.stdout}`);
87
+ assert(result.stdout.includes('Extensions: 2'), `status should print extension count\n${result.stdout}`);
88
+ assert(result.stdout.includes('Checking updates:'), `status should show progress before update checks\n${result.stdout}`);
89
+ assert(result.stdout.includes('hung-extension: checking npm'), `status should print the extension name before probing it\n${result.stdout}`);
90
+ assert(result.stdout.includes('Update checks skipped:'), `status should report skipped checks instead of hanging\n${result.stdout}`);
91
+ assert(/hung-extension: \[timeout \d+(ms|\.\d+s)\] hung-extension/.test(result.stdout), `hung extension should be reported as a timeout with elapsed time\n${result.stdout}`);
92
+ } finally {
93
+ rmSync(tempRoot, { recursive: true, force: true });
94
+ }
95
+
96
+ console.log('ldm status timeout regression passed');