@wipcomputer/wip-ldm-os 0.4.85-alpha.3 → 0.4.85-alpha.4
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/package.json +4 -1
- package/scripts/test-crc-agentid-tenant-boundary.mjs +72 -0
- package/scripts/test-crc-e2ee-session-route.mjs +122 -0
- package/scripts/test-crc-pair-login-flow.mjs +40 -0
- package/src/hosted-mcp/app/footer.js +74 -0
- package/src/hosted-mcp/app/kaleidoscope-login.html +843 -0
- package/src/hosted-mcp/app/pair.html +147 -57
- package/src/hosted-mcp/app/sprites.png +0 -0
- package/src/hosted-mcp/demo/index.html +3 -7
- package/src/hosted-mcp/demo/login.html +318 -20
- package/src/hosted-mcp/deploy.sh +306 -56
- package/src/hosted-mcp/nginx/codex-relay.conf +25 -0
- package/src/hosted-mcp/nginx/conf.d/redact-logs.conf +60 -0
- package/src/hosted-mcp/nginx/mcp-oauth.conf +58 -0
- package/src/hosted-mcp/nginx/wip.computer.conf +25 -1
- package/src/hosted-mcp/scripts/audit-logs.sh +205 -0
- package/src/hosted-mcp/scripts/verify-deploy.sh +102 -0
- package/src/hosted-mcp/server.mjs +775 -112
|
@@ -13,13 +13,21 @@
|
|
|
13
13
|
--accent: #0033FF;
|
|
14
14
|
--input-bg: #F5F3ED;
|
|
15
15
|
--input-border: #E0DDD6;
|
|
16
|
+
--card-bg: #FFFFFF;
|
|
16
17
|
--font: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
|
|
17
18
|
}
|
|
18
19
|
html, body { height: 100%; font-family: var(--font); background: var(--bg); color: var(--text); }
|
|
19
20
|
.page { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
|
|
20
21
|
.card { max-width: 420px; width: 100%; text-align: center; }
|
|
21
22
|
h1 { font-size: 26px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; }
|
|
22
|
-
.sub { font-size: 16px; color: var(--text-muted); margin-bottom:
|
|
23
|
+
.sub { font-size: 16px; color: var(--text-muted); margin-bottom: 24px; }
|
|
24
|
+
.code-display {
|
|
25
|
+
font-family: ui-monospace, "SF Mono", monospace;
|
|
26
|
+
font-size: 28px; font-weight: 600; letter-spacing: 0.3em;
|
|
27
|
+
background: var(--card-bg); border: 1px solid var(--input-border);
|
|
28
|
+
border-radius: 12px; padding: 18px;
|
|
29
|
+
margin: 0 auto 20px; display: inline-block; min-width: 240px;
|
|
30
|
+
}
|
|
23
31
|
input[type="text"] {
|
|
24
32
|
width: 100%; padding: 18px; font-size: 22px; font-weight: 600;
|
|
25
33
|
text-align: center; letter-spacing: 0.4em;
|
|
@@ -28,9 +36,10 @@
|
|
|
28
36
|
text-transform: uppercase; font-family: ui-monospace, "SF Mono", monospace;
|
|
29
37
|
}
|
|
30
38
|
input[type="text"]:focus { border-color: var(--accent); outline: none; }
|
|
31
|
-
.btn { display: block; width: 100%; padding: 18px; border: none; border-radius: 12px; font-size: 18px; font-weight: 600; font-family: var(--font); cursor: pointer; margin-top:
|
|
39
|
+
.btn { display: block; width: 100%; padding: 18px; border: none; border-radius: 12px; font-size: 18px; font-weight: 600; font-family: var(--font); cursor: pointer; margin-top: 12px; }
|
|
32
40
|
.btn:active { transform: scale(0.98); }
|
|
33
41
|
.btn-primary { background: var(--accent); color: white; }
|
|
42
|
+
.btn-secondary { background: var(--card-bg); color: var(--text-muted); border: 1px solid var(--input-border); }
|
|
34
43
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
35
44
|
.status { margin-top: 18px; font-size: 14px; min-height: 1.4em; color: var(--text-muted); }
|
|
36
45
|
.status.error { color: #b00020; }
|
|
@@ -41,78 +50,159 @@
|
|
|
41
50
|
<body>
|
|
42
51
|
<div class="page">
|
|
43
52
|
<div class="card">
|
|
44
|
-
|
|
45
|
-
<div
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
<!-- View 1: confirm pair (when URL has a valid code + user is signed in) -->
|
|
54
|
+
<div id="confirm-view" style="display: none;">
|
|
55
|
+
<h1>Pair this laptop with Codex Remote Control?</h1>
|
|
56
|
+
<div class="sub">A device wants to drive its local Codex session from your phone.</div>
|
|
57
|
+
<div class="code-display" id="confirm-code">______</div>
|
|
58
|
+
<button id="confirmBtn" class="btn btn-primary" type="button">Confirm</button>
|
|
59
|
+
<button id="cancelBtn" class="btn btn-secondary" type="button">Cancel</button>
|
|
60
|
+
<div id="status" class="status"></div>
|
|
61
|
+
<div class="footer">Signed in as <span id="handle">...</span></div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- View 2: manual code entry (fallback for bare /pair) -->
|
|
65
|
+
<div id="manual-view" style="display: none;">
|
|
66
|
+
<h1>Pair your laptop</h1>
|
|
67
|
+
<div class="sub">Type the 6-character code printed by <code>codex-daemon link</code></div>
|
|
68
|
+
<input id="codeInput" type="text" maxlength="6" autocomplete="off" autocapitalize="characters" inputmode="text" placeholder="ABCDEF">
|
|
69
|
+
<button id="manualBtn" class="btn btn-primary" type="button">Pair</button>
|
|
70
|
+
<div id="manualStatus" class="status"></div>
|
|
71
|
+
<div class="footer">Signed in as <span id="manualHandle">...</span></div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- View 3: success -->
|
|
75
|
+
<div id="success-view" style="display: none;">
|
|
76
|
+
<h1>Paired.</h1>
|
|
77
|
+
<div class="sub">Your laptop will pick this up in a few seconds.</div>
|
|
78
|
+
<div class="footer">You can close this tab.</div>
|
|
79
|
+
</div>
|
|
50
80
|
</div>
|
|
51
81
|
</div>
|
|
52
82
|
<script>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
// Real daemon alphabet: A-Z minus I and O, digits 2-9. L IS included.
|
|
84
|
+
// Length 6. Per CODEX_PAIR_ALPHABET in server.mjs and plan C1+C3 round 5.
|
|
85
|
+
var PAIR_CODE_REGEX = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
|
|
86
|
+
|
|
87
|
+
function getApiKey() { return sessionStorage.getItem('wip_api_key'); }
|
|
88
|
+
function getHandle() { return sessionStorage.getItem('wip_handle') || '(unknown)'; }
|
|
89
|
+
|
|
90
|
+
function setStatus(elId, msg, kind) {
|
|
91
|
+
var el = document.getElementById(elId);
|
|
61
92
|
el.textContent = msg;
|
|
62
|
-
el.className =
|
|
63
|
-
}
|
|
64
|
-
function ensureSignedIn() {
|
|
65
|
-
if (!getApiKey()) {
|
|
66
|
-
location.href = "/app/login.html?next=" + encodeURIComponent("/pair");
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
document.getElementById("handle").textContent = getHandle();
|
|
70
|
-
return true;
|
|
93
|
+
el.className = 'status' + (kind ? ' ' + kind : '');
|
|
71
94
|
}
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
96
|
+
// Read the code from /pair/<CODE>. Returns null if path is bare /pair or
|
|
97
|
+
// the code doesn't match the daemon alphabet. Defense-in-depth: server
|
|
98
|
+
// validates authoritatively via its route regex.
|
|
99
|
+
function readCodeFromPath() {
|
|
100
|
+
var m = location.pathname.match(/^\/pair\/([A-Za-z0-9]+)\/?$/);
|
|
101
|
+
if (!m) return null;
|
|
102
|
+
var raw = m[1].trim().toUpperCase();
|
|
103
|
+
return PAIR_CODE_REGEX.test(raw) ? raw : null;
|
|
104
|
+
}
|
|
79
105
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (code
|
|
84
|
-
setStatus(
|
|
85
|
-
return;
|
|
106
|
+
// POST the code to /api/codex-relay/pair-complete using the api_key
|
|
107
|
+
// from sessionStorage as Bearer token. On success, show the success view.
|
|
108
|
+
async function submitPair(code, statusElId) {
|
|
109
|
+
if (!PAIR_CODE_REGEX.test(code)) {
|
|
110
|
+
setStatus(statusElId, 'That does not look like a valid code.', 'error');
|
|
111
|
+
return false;
|
|
86
112
|
}
|
|
87
|
-
|
|
88
|
-
btn.disabled = true;
|
|
89
|
-
setStatus("Pairing...");
|
|
113
|
+
setStatus(statusElId, 'Pairing...', '');
|
|
90
114
|
try {
|
|
91
|
-
|
|
92
|
-
method:
|
|
115
|
+
var res = await fetch('/api/codex-relay/pair-complete', {
|
|
116
|
+
method: 'POST',
|
|
93
117
|
headers: {
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
'Authorization': 'Bearer ' + getApiKey(),
|
|
96
120
|
},
|
|
97
|
-
body: JSON.stringify({ code }),
|
|
121
|
+
body: JSON.stringify({ code: code }),
|
|
98
122
|
});
|
|
99
|
-
|
|
100
|
-
if (!res.ok)
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
var data = await res.json();
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
var msg = (data && data.error) || 'Pairing failed.';
|
|
126
|
+
// If the code expired or was already used, suggest rerunning the daemon.
|
|
127
|
+
if (res.status === 410 || res.status === 404) {
|
|
128
|
+
msg += ' Run codex-daemon link again on your laptop to get a fresh code.';
|
|
129
|
+
}
|
|
130
|
+
setStatus(statusElId, msg, 'error');
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
document.getElementById('confirm-view').style.display = 'none';
|
|
134
|
+
document.getElementById('manual-view').style.display = 'none';
|
|
135
|
+
document.getElementById('success-view').style.display = 'block';
|
|
136
|
+
return true;
|
|
103
137
|
} catch (err) {
|
|
104
|
-
setStatus(err.message || String(err),
|
|
105
|
-
|
|
138
|
+
setStatus(statusElId, (err && err.message) || String(err), 'error');
|
|
139
|
+
return false;
|
|
106
140
|
}
|
|
107
141
|
}
|
|
108
142
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
// ── Routing ──
|
|
144
|
+
//
|
|
145
|
+
// 1. If not signed in: redirect to /login?next=<this-path-with-code>.
|
|
146
|
+
// The existing Kaleidoscope login flow will QR + passkey + redirect
|
|
147
|
+
// back here. The server's `next` whitelist also enforces this shape.
|
|
148
|
+
// 2. If signed in AND URL has a valid code: render the confirm view.
|
|
149
|
+
// User must tap Confirm before we POST pair-complete (per plan C7).
|
|
150
|
+
// 3. If signed in but no code in URL (bare /pair): render the manual
|
|
151
|
+
// entry view. Fallback only.
|
|
152
|
+
|
|
153
|
+
(function init() {
|
|
154
|
+
var code = readCodeFromPath();
|
|
155
|
+
var apiKey = getApiKey();
|
|
156
|
+
|
|
157
|
+
if (!apiKey) {
|
|
158
|
+
// Not signed in. Send through /login keeping the current path as next.
|
|
159
|
+
var nextParam = encodeURIComponent(location.pathname);
|
|
160
|
+
location.replace('/login?next=' + nextParam);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (code) {
|
|
165
|
+
// Signed in + URL has a valid code. Show confirm step.
|
|
166
|
+
var confirmView = document.getElementById('confirm-view');
|
|
167
|
+
document.getElementById('confirm-code').textContent = code;
|
|
168
|
+
document.getElementById('handle').textContent = getHandle();
|
|
169
|
+
confirmView.style.display = 'block';
|
|
170
|
+
document.getElementById('confirmBtn').addEventListener('click', async function() {
|
|
171
|
+
var confirmBtn = document.getElementById('confirmBtn');
|
|
172
|
+
var cancelBtn = document.getElementById('cancelBtn');
|
|
173
|
+
confirmBtn.disabled = true;
|
|
174
|
+
cancelBtn.disabled = true;
|
|
175
|
+
var ok = await submitPair(code, 'status');
|
|
176
|
+
// On failure, re-enable Confirm/Cancel so the user can retry or cancel.
|
|
177
|
+
// Server may have rejected the code as expired or already-used; the
|
|
178
|
+
// user should see the error and have the buttons back.
|
|
179
|
+
if (!ok) {
|
|
180
|
+
confirmBtn.disabled = false;
|
|
181
|
+
cancelBtn.disabled = false;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
document.getElementById('cancelBtn').addEventListener('click', function() {
|
|
185
|
+
// Best-effort: just close the tab if we can; otherwise redirect home.
|
|
186
|
+
try { window.close(); } catch (e) {}
|
|
187
|
+
setTimeout(function() { location.replace('/'); }, 100);
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Bare /pair, signed in. Manual code entry fallback.
|
|
193
|
+
var manualView = document.getElementById('manual-view');
|
|
194
|
+
document.getElementById('manualHandle').textContent = getHandle();
|
|
195
|
+
manualView.style.display = 'block';
|
|
196
|
+
function manualSubmit() {
|
|
197
|
+
var raw = document.getElementById('codeInput').value.trim().toUpperCase();
|
|
198
|
+
submitPair(raw, 'manualStatus');
|
|
199
|
+
}
|
|
200
|
+
document.getElementById('manualBtn').addEventListener('click', manualSubmit);
|
|
201
|
+
document.getElementById('codeInput').addEventListener('keydown', function(ev) {
|
|
202
|
+
if (ev.key === 'Enter') manualSubmit();
|
|
113
203
|
});
|
|
114
|
-
document.getElementById(
|
|
115
|
-
}
|
|
204
|
+
document.getElementById('codeInput').focus();
|
|
205
|
+
})();
|
|
116
206
|
</script>
|
|
117
207
|
</body>
|
|
118
208
|
</html>
|
|
Binary file
|
|
@@ -1233,13 +1233,9 @@ if (sessionStorage.getItem('lesa-token')) {
|
|
|
1233
1233
|
showChat();
|
|
1234
1234
|
}
|
|
1235
1235
|
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
document.getElementById('createBtn').onclick = function() { doSignIn(); };
|
|
1240
|
-
document.getElementById('handleInputWrap').style.display = 'none';
|
|
1241
|
-
document.getElementById('signInBtn').parentElement.style.display = 'none';
|
|
1242
|
-
}
|
|
1236
|
+
// /demo/ always shows the original create-account UI. Do not key off
|
|
1237
|
+
// localStorage["kscope-has-account"] to swap into sign-in mode here;
|
|
1238
|
+
// that coupled /login state to /demo/ rendering.
|
|
1243
1239
|
|
|
1244
1240
|
// Handle send (not used in demo flow, but wired up)
|
|
1245
1241
|
function handleSend() {
|