@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
@@ -1,70 +1,321 @@
1
1
  #!/usr/bin/env bash
2
- # deploy.sh: Deploy hosted MCP server to wip.computer
2
+ # deploy.sh: Deploy hosted MCP server + nginx config to wip.computer.
3
+ #
4
+ # Writes a JSON provenance manifest to
5
+ # /var/www/wip.computer/deploy-manifests/hosted-mcp/<timestamp>.json
6
+ # on the VPS per F-009 of the VPS hosted-mcp audit
7
+ # (ai/product/bugs/security/2026-04-28--cc-mini--vps-hosted-mcp-audit.md).
8
+ #
9
+ # Manifest fields:
10
+ # deployedAt, deployedBy, git (remote + branch + commit + dirty),
11
+ # files[] (source, destination, sha256),
12
+ # nginx (test_pass, test_output) when nginx is deployed,
13
+ # pm2 (pid, status, restart_time, exec_path) when app is deployed.
14
+ #
15
+ # Usage:
16
+ # bash deploy.sh # deploy app + nginx + manifest
17
+ # bash deploy.sh --dry-run # preview, no scp/reload/restart
18
+ # bash deploy.sh --allow-dirty # allow uncommitted changes
19
+ # bash deploy.sh --skip-nginx # only deploy Node app
20
+ # bash deploy.sh --skip-app # only deploy nginx config
21
+ # bash deploy.sh --remote host # override SSH host (default: wip.computer)
3
22
  #
4
23
  # Prerequisites:
5
- # - SSH config has Host wip.computer
24
+ # - SSH config has Host wip.computer with key auth
25
+ # - VPS user has passwordless sudo for nginx + systemctl reload
6
26
  # - pm2 installed on the server
7
- # - nginx configured on the server
8
- #
9
- # Usage: bash deploy.sh
27
+ # - python3 and shasum/sha256sum available locally
10
28
 
11
29
  set -euo pipefail
12
30
 
31
+ # ── Args ────────────────────────────────────────────────────────────
32
+
33
+ DRY_RUN=0
34
+ ALLOW_DIRTY=0
35
+ SKIP_NGINX=0
36
+ SKIP_APP=0
13
37
  REMOTE="wip.computer"
14
- REMOTE_DIR="/var/www/wip.computer/app/mcp-server"
38
+
39
+ while [ $# -gt 0 ]; do
40
+ case "$1" in
41
+ --dry-run) DRY_RUN=1; shift ;;
42
+ --allow-dirty) ALLOW_DIRTY=1; shift ;;
43
+ --skip-nginx) SKIP_NGINX=1; shift ;;
44
+ --skip-app) SKIP_APP=1; shift ;;
45
+ --remote) REMOTE=$2; shift 2 ;;
46
+ -h|--help) sed -n '2,30p' "$0"; exit 0 ;;
47
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
48
+ esac
49
+ done
50
+
51
+ APP_REMOTE_DIR="/var/www/wip.computer/app/mcp-server"
52
+ NGINX_SNIPPETS_DIR="/etc/nginx/snippets"
53
+ NGINX_CONFD_DIR="/etc/nginx/conf.d"
54
+ MANIFEST_DIR="/var/www/wip.computer/deploy-manifests/hosted-mcp"
15
55
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
56
 
17
- echo "Deploying hosted MCP server to ${REMOTE}..."
18
-
19
- # 1. Create remote directory structure
20
- echo "Creating remote directories..."
21
- ssh "${REMOTE}" "mkdir -p ${REMOTE_DIR}/inbox"
22
-
23
- # 2. Copy server files
24
- echo "Copying files..."
25
- scp "${SCRIPT_DIR}/server.mjs" "${REMOTE}:${REMOTE_DIR}/"
26
- scp "${SCRIPT_DIR}/inbox.mjs" "${REMOTE}:${REMOTE_DIR}/"
27
- scp "${SCRIPT_DIR}/tools.mjs" "${REMOTE}:${REMOTE_DIR}/"
28
- scp "${SCRIPT_DIR}/package.json" "${REMOTE}:${REMOTE_DIR}/"
29
-
30
- # 3. Install dependencies
31
- echo "Installing dependencies..."
32
- ssh "${REMOTE}" "cd ${REMOTE_DIR} && npm install --omit=dev"
33
-
34
- # 4. Register with pm2 (restart if already running)
35
- echo "Starting with pm2..."
36
- ssh "${REMOTE}" "cd ${REMOTE_DIR} && pm2 delete mcp-server 2>/dev/null || true && pm2 start server.mjs --name mcp-server && pm2 save"
37
-
38
- # 5. Configure nginx reverse proxy
39
- echo "Configuring nginx..."
40
- ssh "${REMOTE}" "cat > /tmp/mcp-server.conf << 'NGINX'
41
- # MCP server reverse proxy
42
- # Location block to add inside the wip.computer server block
43
- location /mcp {
44
- proxy_pass http://127.0.0.1:18800/mcp;
45
- proxy_http_version 1.1;
46
- proxy_set_header Host \$host;
47
- proxy_set_header X-Real-IP \$remote_addr;
48
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
49
- proxy_set_header X-Forwarded-Proto \$scheme;
50
-
51
- # SSE support (for MCP Streamable HTTP GET streams)
52
- proxy_set_header Connection '';
53
- proxy_buffering off;
54
- proxy_cache off;
55
- proxy_read_timeout 86400;
56
- chunked_transfer_encoding on;
57
+ # Pick whichever sha256 tool is available locally (macOS vs Linux).
58
+ if command -v sha256sum >/dev/null 2>&1; then
59
+ SHA256_CMD="sha256sum"
60
+ elif command -v shasum >/dev/null 2>&1; then
61
+ SHA256_CMD="shasum -a 256"
62
+ else
63
+ echo "FATAL: neither sha256sum nor shasum found locally" >&2
64
+ exit 1
65
+ fi
66
+
67
+ # Pick python3 for JSON-escape helpers; fall back to node if absent.
68
+ if command -v python3 >/dev/null 2>&1; then
69
+ json_escape() { python3 -c 'import sys,json;print(json.dumps(sys.stdin.read()),end="")'; }
70
+ elif command -v node >/dev/null 2>&1; then
71
+ json_escape() { node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>process.stdout.write(JSON.stringify(d)));'; }
72
+ else
73
+ echo "FATAL: neither python3 nor node found locally for JSON escaping" >&2
74
+ exit 1
75
+ fi
76
+
77
+ # ── Pre-deploy: git state ───────────────────────────────────────────
78
+
79
+ REPO_ROOT="$(cd "${SCRIPT_DIR}" && git rev-parse --show-toplevel)"
80
+ cd "${REPO_ROOT}"
81
+ GIT_REMOTE_URL="$(git config --get remote.origin.url || echo 'unknown')"
82
+ GIT_COMMIT="$(git rev-parse HEAD)"
83
+ GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
84
+ GIT_DIRTY_LIST="$(git status --porcelain)"
85
+ if [ -n "$GIT_DIRTY_LIST" ] && [ "$ALLOW_DIRTY" -ne 1 ]; then
86
+ echo "FATAL: working tree has uncommitted changes:" >&2
87
+ echo "$GIT_DIRTY_LIST" >&2
88
+ echo "Pass --allow-dirty to deploy anyway (dev only)." >&2
89
+ exit 1
90
+ fi
91
+ GIT_DIRTY=false
92
+ [ -n "$GIT_DIRTY_LIST" ] && GIT_DIRTY=true
93
+
94
+ # ── File inventory ──────────────────────────────────────────────────
95
+ #
96
+ # Two parallel arrays: SRC_FILES (relative to SCRIPT_DIR) and DST_FILES
97
+ # (absolute path on remote). Plain bash arrays for macOS bash 3.2
98
+ # compatibility.
99
+
100
+ SRC_FILES=()
101
+ DST_FILES=()
102
+ SHA_FILES=()
103
+
104
+ add_file() {
105
+ local src=$1 dst=$2
106
+ if [ ! -f "${SCRIPT_DIR}/${src}" ]; then
107
+ echo "WARN: ${src} not found locally; skipping" >&2
108
+ return
109
+ fi
110
+ SRC_FILES+=("$src")
111
+ DST_FILES+=("$dst")
112
+ SHA_FILES+=("$(${SHA256_CMD} "${SCRIPT_DIR}/${src}" | awk '{print $1}')")
113
+ }
114
+
115
+ if [ "$SKIP_APP" -ne 1 ]; then
116
+ add_file "server.mjs" "${APP_REMOTE_DIR}/server.mjs"
117
+ add_file "inbox.mjs" "${APP_REMOTE_DIR}/inbox.mjs"
118
+ add_file "tools.mjs" "${APP_REMOTE_DIR}/tools.mjs"
119
+ add_file "codex-relay-e2ee-registry.mjs" "${APP_REMOTE_DIR}/codex-relay-e2ee-registry.mjs"
120
+ add_file "package.json" "${APP_REMOTE_DIR}/package.json"
121
+ # Phone app static files (codex-remote-control, login).
122
+ if [ -d "${SCRIPT_DIR}/app" ]; then
123
+ while IFS= read -r f; do
124
+ rel=${f#${SCRIPT_DIR}/}
125
+ add_file "$rel" "${APP_REMOTE_DIR}/${rel}"
126
+ done < <(find "${SCRIPT_DIR}/app" -type f | sort)
127
+ fi
128
+ # Kaleidoscope demo files (login.html, index.html, agent.html, etc.).
129
+ if [ -d "${SCRIPT_DIR}/demo" ]; then
130
+ while IFS= read -r f; do
131
+ rel=${f#${SCRIPT_DIR}/}
132
+ add_file "$rel" "${APP_REMOTE_DIR}/${rel}"
133
+ done < <(find "${SCRIPT_DIR}/demo" -type f | sort)
134
+ fi
135
+ fi
136
+
137
+ if [ "$SKIP_NGINX" -ne 1 ]; then
138
+ # Snippets included from the site config.
139
+ if [ -d "${SCRIPT_DIR}/nginx" ]; then
140
+ while IFS= read -r f; do
141
+ rel=${f#${SCRIPT_DIR}/nginx/}
142
+ base=$(basename "$f")
143
+ case "$rel" in
144
+ conf.d/*) add_file "nginx/${rel}" "${NGINX_CONFD_DIR}/${base}" ;;
145
+ wip.computer.conf) ;; # site config: deploy manually, has carry-over
146
+ *) add_file "nginx/${rel}" "${NGINX_SNIPPETS_DIR}/${base}" ;;
147
+ esac
148
+ done < <(find "${SCRIPT_DIR}/nginx" -maxdepth 2 -type f -name '*.conf' | sort)
149
+ fi
150
+ fi
151
+
152
+ # ── Dry-run preview ─────────────────────────────────────────────────
153
+
154
+ if [ "$DRY_RUN" -eq 1 ]; then
155
+ echo "DRY RUN. Git: ${GIT_BRANCH} @ ${GIT_COMMIT} (dirty=${GIT_DIRTY})"
156
+ echo "Remote: ${REMOTE}"
157
+ echo "Files (${#SRC_FILES[@]}):"
158
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
159
+ printf " %-40s -> %s [%s]\n" "${SRC_FILES[$i]}" "${DST_FILES[$i]}" "${SHA_FILES[$i]:0:12}"
160
+ done
161
+ echo ""
162
+ echo "Would write manifest to: ${MANIFEST_DIR}/$(date -u +%Y-%m-%dT%H-%M-%SZ).json"
163
+ exit 0
164
+ fi
165
+
166
+ if [ ${#SRC_FILES[@]} -eq 0 ]; then
167
+ echo "Nothing to deploy (both --skip-app and --skip-nginx, or no files matched)." >&2
168
+ exit 1
169
+ fi
170
+
171
+ # ── Deploy app files ────────────────────────────────────────────────
172
+
173
+ # /var/www/wip.computer/ is root-owned; the manifest dir must be
174
+ # created with sudo on first deploy. After creation, parker:parker
175
+ # ownership lets subsequent deploys write the manifest without sudo.
176
+ ssh "${REMOTE}" "sudo install -d -o parker -g parker -m 0755 ${MANIFEST_DIR}"
177
+
178
+ if [ "$SKIP_APP" -ne 1 ]; then
179
+ ssh "${REMOTE}" "mkdir -p ${APP_REMOTE_DIR}/inbox"
180
+ fi
181
+
182
+ # scp each file. Pre-create destination dir on the remote so app/foo/bar.html
183
+ # does not fail on missing parent.
184
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
185
+ src=${SRC_FILES[$i]}
186
+ dst=${DST_FILES[$i]}
187
+ case "$dst" in
188
+ /etc/nginx/*) need_sudo=1 ;;
189
+ *) need_sudo=0 ;;
190
+ esac
191
+ remote_parent=$(dirname "$dst")
192
+ if [ "$need_sudo" -eq 1 ]; then
193
+ # Stage to a temp dir on remote, then sudo mv into place.
194
+ tmp="/tmp/deploy-staging-$$/$(basename "$src")"
195
+ ssh "${REMOTE}" "mkdir -p $(dirname "$tmp")"
196
+ scp "${SCRIPT_DIR}/${src}" "${REMOTE}:${tmp}"
197
+ ssh "${REMOTE}" "sudo install -m 0644 ${tmp} ${dst}"
198
+ else
199
+ ssh "${REMOTE}" "mkdir -p ${remote_parent}"
200
+ scp "${SCRIPT_DIR}/${src}" "${REMOTE}:${dst}"
201
+ fi
202
+ done
203
+
204
+ # Cleanup staging dir if it exists.
205
+ ssh "${REMOTE}" "rm -rf /tmp/deploy-staging-$$" || true
206
+
207
+ # ── Verify post-deploy hashes (transfer integrity) ──────────────────
208
+
209
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
210
+ remote_sha=$(ssh "${REMOTE}" "sudo sha256sum ${DST_FILES[$i]}" 2>/dev/null | awk '{print $1}' || echo "")
211
+ if [ "${SHA_FILES[$i]}" != "$remote_sha" ]; then
212
+ echo "FATAL: post-deploy hash mismatch for ${SRC_FILES[$i]}" >&2
213
+ echo " local : ${SHA_FILES[$i]}" >&2
214
+ echo " remote: ${remote_sha}" >&2
215
+ exit 1
216
+ fi
217
+ done
218
+
219
+ # ── npm install (app only) ──────────────────────────────────────────
220
+
221
+ if [ "$SKIP_APP" -ne 1 ]; then
222
+ ssh "${REMOTE}" "cd ${APP_REMOTE_DIR} && npm install --omit=dev"
223
+ fi
224
+
225
+ # ── nginx test + reload (nginx only) ────────────────────────────────
226
+
227
+ NGINX_TEST_PASS=null
228
+ NGINX_TEST_OUTPUT_JSON='""'
229
+ if [ "$SKIP_NGINX" -ne 1 ]; then
230
+ set +e
231
+ NGINX_TEST_OUTPUT=$(ssh "${REMOTE}" "sudo nginx -t" 2>&1)
232
+ NGINX_TEST_RC=$?
233
+ set -e
234
+ if [ "$NGINX_TEST_RC" -eq 0 ]; then
235
+ NGINX_TEST_PASS=true
236
+ ssh "${REMOTE}" "sudo systemctl reload nginx"
237
+ else
238
+ NGINX_TEST_PASS=false
239
+ echo "FATAL: nginx -t failed; not reloading." >&2
240
+ echo "$NGINX_TEST_OUTPUT" >&2
241
+ fi
242
+ NGINX_TEST_OUTPUT_JSON=$(printf '%s' "$NGINX_TEST_OUTPUT" | json_escape)
243
+ fi
244
+
245
+ # ── PM2 reload ──────────────────────────────────────────────────────
246
+
247
+ PM2_INFO_JSON=null
248
+ if [ "$SKIP_APP" -ne 1 ]; then
249
+ ssh "${REMOTE}" "cd ${APP_REMOTE_DIR} && (pm2 reload mcp-server || (pm2 start server.mjs --name mcp-server && pm2 save))"
250
+
251
+ # Capture pm2 status for the manifest.
252
+ PM2_RAW=$(ssh "${REMOTE}" "pm2 jlist" 2>/dev/null || echo "[]")
253
+ PM2_INFO_JSON=$(printf '%s' "$PM2_RAW" | python3 -c '
254
+ import json,sys
255
+ data=json.load(sys.stdin)
256
+ target="mcp-server"
257
+ for p in data:
258
+ if p.get("name")==target:
259
+ env=p.get("pm2_env",{})
260
+ print(json.dumps({
261
+ "name": p.get("name"),
262
+ "pid": p.get("pid"),
263
+ "status": env.get("status"),
264
+ "restart_time": env.get("restart_time"),
265
+ "uptime": env.get("pm_uptime"),
266
+ "exec_path": env.get("pm_exec_path"),
267
+ "node_version": env.get("node_version"),
268
+ }))
269
+ break
270
+ else:
271
+ print("null")
272
+ ')
273
+ fi
274
+
275
+ # ── Build manifest ──────────────────────────────────────────────────
276
+
277
+ DEPLOYED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
278
+ TIMESTAMP_SLUG=$(date -u +%Y-%m-%dT%H-%M-%SZ)
279
+ DEPLOYED_BY="$(whoami)@$(hostname)"
280
+
281
+ # files[] JSON
282
+ FILES_JSON=""
283
+ for ((i=0; i<${#SRC_FILES[@]}; i++)); do
284
+ [ "$i" -eq 0 ] || FILES_JSON+=","
285
+ FILES_JSON+=$(printf '{"source":"%s","destination":"%s","sha256":"%s"}' \
286
+ "${SRC_FILES[$i]}" "${DST_FILES[$i]}" "${SHA_FILES[$i]}")
287
+ done
288
+
289
+ MANIFEST=$(cat <<JSON
290
+ {
291
+ "deployedAt": "${DEPLOYED_AT}",
292
+ "deployedBy": "${DEPLOYED_BY}",
293
+ "git": {
294
+ "remote": "${GIT_REMOTE_URL}",
295
+ "branch": "${GIT_BRANCH}",
296
+ "commit": "${GIT_COMMIT}",
297
+ "dirty": ${GIT_DIRTY}
298
+ },
299
+ "files": [${FILES_JSON}],
300
+ "nginx": {
301
+ "skipped": $([ "$SKIP_NGINX" -eq 1 ] && echo true || echo false),
302
+ "test_pass": ${NGINX_TEST_PASS},
303
+ "test_output": ${NGINX_TEST_OUTPUT_JSON}
304
+ },
305
+ "pm2": ${PM2_INFO_JSON}
57
306
  }
58
- NGINX
59
- "
307
+ JSON
308
+ )
309
+
310
+ # ── Write manifest to VPS ───────────────────────────────────────────
311
+
312
+ MANIFEST_PATH="${MANIFEST_DIR}/${TIMESTAMP_SLUG}.json"
313
+ printf '%s\n' "$MANIFEST" | ssh "${REMOTE}" "sudo install -d -m 0755 ${MANIFEST_DIR} && sudo tee ${MANIFEST_PATH} > /dev/null && sudo chmod 0644 ${MANIFEST_PATH}"
60
314
 
61
315
  echo ""
62
- echo "nginx config written to /tmp/mcp-server.conf on the server."
63
- echo "To activate, add it to your server block and reload:"
64
- echo " ssh ${REMOTE} 'sudo cp /tmp/mcp-server.conf /etc/nginx/snippets/mcp-server.conf'"
65
- echo " # Then include it in your server block: include snippets/mcp-server.conf;"
66
- echo " ssh ${REMOTE} 'sudo nginx -t && sudo systemctl reload nginx'"
316
+ echo "Deploy complete."
317
+ echo "Manifest: ${REMOTE}:${MANIFEST_PATH}"
67
318
  echo ""
68
- echo "Deployment complete."
69
- echo "Health check: curl https://wip.computer/health"
70
- echo "MCP endpoint: https://wip.computer/mcp"
319
+ echo "Verify:"
320
+ echo " curl -fsS https://wip.computer/health"
321
+ echo " bash ${SCRIPT_DIR}/scripts/verify-deploy.sh ${MANIFEST_PATH}"
@@ -0,0 +1,268 @@
1
+ # Hosted Relay Self-Host Guide
2
+
3
+ This guide explains how to inspect and run the hosted relay source that backs WIP-hosted services such as Codex Remote Control.
4
+
5
+ The public source lives here:
6
+
7
+ ```text
8
+ src/hosted-mcp
9
+ ```
10
+
11
+ WIP's production relay runs at `wip.computer`. Self-hosting means running the same relay source on your own domain, with your own database, TLS certificate, process manager, and secrets.
12
+
13
+ ## What The Hosted Relay Does
14
+
15
+ The hosted relay is a Node server with several surfaces:
16
+
17
+ - hosted MCP over HTTP at `/mcp`;
18
+ - OAuth and passkey login routes;
19
+ - demo routes under `/demo`;
20
+ - Codex Remote Control pairing and relay routes under `/api/codex-relay/*`;
21
+ - health reporting at `/health`.
22
+
23
+ For Codex Remote Control, the relay lets a local `codex-daemon` dial out to a public server. Browser and phone clients connect to that same public server. After E2EE setup, prompt text, assistant output, command output, and errors are carried as encrypted frames. The relay routes and authorizes frames, but it is not the place where Codex runs.
24
+
25
+ ## Current Self-Host Status
26
+
27
+ The relay source is inspectable and runnable, but the non-WIP self-host story is not yet a one-command installer.
28
+
29
+ Important current constraints:
30
+
31
+ - `server.mjs` currently defaults `ISSUER_URL`, `MCP_RESOURCE_URL`, `RP_ID`, and `RP_ORIGIN` to `wip.computer`;
32
+ - the nginx examples are written for the WIP production domain and filesystem layout;
33
+ - the Codex Remote Control browser surface at `/codex-remote-control/<threadId>` is served by the WIP web app, not by the hosted-mcp Node process itself;
34
+ - `codex-daemon` can point at a custom relay with environment variables, but a complete non-WIP phone/web UI deployment must also point at that same relay.
35
+
36
+ That means a production self-host should treat this guide as the infrastructure map. Before broad use, parameterize or patch the WIP domain constants for your domain.
37
+
38
+ ## Prerequisites
39
+
40
+ You need:
41
+
42
+ - Node.js 20 or newer;
43
+ - npm;
44
+ - Postgres;
45
+ - nginx or another reverse proxy that supports WebSocket upgrades;
46
+ - TLS for your domain;
47
+ - PM2 or another process manager;
48
+ - a public domain such as `relay.example.com`.
49
+
50
+ Optional demo surfaces may also need:
51
+
52
+ - `OPENAI_API_KEY`;
53
+ - `XAI_API_KEY`.
54
+
55
+ Codex Remote Control relay operation does not require those demo keys.
56
+
57
+ ## Environment
58
+
59
+ Start from:
60
+
61
+ ```bash
62
+ cd src/hosted-mcp
63
+ cp .env.example .env
64
+ ```
65
+
66
+ Required:
67
+
68
+ ```bash
69
+ DATABASE_URL=postgresql://kaleidoscope:YOUR_PASSWORD@localhost:5432/kaleidoscope
70
+ ```
71
+
72
+ Common optional variables:
73
+
74
+ ```bash
75
+ MCP_PORT=18800
76
+ LDM_HOSTED_MCP_WS_ORIGIN_ALLOWLIST=https://relay.example.com
77
+ LDM_HOSTED_MCP_RL_MINT=30
78
+ LDM_HOSTED_MCP_RL_VALIDATE=60
79
+ LDM_HOSTED_MCP_RL_STATUS=120
80
+ ```
81
+
82
+ Development-only variables:
83
+
84
+ ```bash
85
+ LDM_HOSTED_MCP_DEV_MODE=1
86
+ LDM_HOSTED_MCP_ALLOW_WS_URL_TOKEN=1
87
+ ```
88
+
89
+ Do not enable those development flags in production. Production should use Postgres and bearer or ticket authentication, not JSON fallback files or URL token fallback.
90
+
91
+ ## Database Setup
92
+
93
+ Create a Postgres database and user for the relay. Then run Prisma from `src/hosted-mcp`:
94
+
95
+ ```bash
96
+ npm install
97
+ npx prisma generate
98
+ npx prisma migrate deploy
99
+ ```
100
+
101
+ The Prisma schema stores:
102
+
103
+ - users;
104
+ - WebAuthn credentials;
105
+ - device tokens;
106
+ - wallets;
107
+ - API keys.
108
+
109
+ Production should use Postgres. If Prisma cannot connect and `LDM_HOSTED_MCP_DEV_MODE` is not set, the server fails closed.
110
+
111
+ ## Local Smoke Test
112
+
113
+ From `src/hosted-mcp`:
114
+
115
+ ```bash
116
+ npm install
117
+ node server.mjs
118
+ ```
119
+
120
+ In another shell:
121
+
122
+ ```bash
123
+ curl -fsS http://127.0.0.1:18800/health
124
+ ```
125
+
126
+ Expected result: JSON health output from the Node process.
127
+
128
+ ## Process Management
129
+
130
+ WIP production uses PM2 with:
131
+
132
+ ```text
133
+ src/hosted-mcp/ecosystem.config.cjs
134
+ ```
135
+
136
+ For a self-host:
137
+
138
+ ```bash
139
+ cd src/hosted-mcp
140
+ pm2 start ecosystem.config.cjs --update-env
141
+ pm2 save
142
+ pm2 status mcp-server
143
+ ```
144
+
145
+ If you use another process manager, preserve the same contract:
146
+
147
+ - run `server.mjs` from `src/hosted-mcp`;
148
+ - provide `DATABASE_URL`;
149
+ - keep the process alive across restarts;
150
+ - preserve environment variables on reload;
151
+ - verify `/health` after restart.
152
+
153
+ ## Deploy Helper
154
+
155
+ WIP's production deploy helper is:
156
+
157
+ ```text
158
+ src/hosted-mcp/deploy.sh
159
+ ```
160
+
161
+ It copies `server.mjs`, supporting modules, static app/demo files, nginx snippets, and package metadata to WIP's VPS, then reloads nginx, reloads PM2, and writes a deploy manifest.
162
+
163
+ For self-hosting, read it as an example of the file inventory and verification sequence. Do not run it unmodified unless your SSH host, remote directories, nginx layout, PM2 process name, and deploy-manifest path intentionally match the WIP production layout.
164
+
165
+ ## nginx, TLS, And Domain
166
+
167
+ The production nginx examples live in:
168
+
169
+ ```text
170
+ src/hosted-mcp/nginx
171
+ ```
172
+
173
+ Key files:
174
+
175
+ - `codex-relay.conf` contains the `/api/codex-relay/*`, `/pair`, and WebSocket proxy routes;
176
+ - `mcp-oauth.conf` and `mcp-server.conf` contain hosted MCP and OAuth routes;
177
+ - `wip.computer.conf` shows how WIP includes those snippets inside the public site config;
178
+ - `conf.d/redact-logs.conf` defines the redacted access-log format.
179
+
180
+ For self-hosting:
181
+
182
+ 1. Put TLS in front of your relay domain.
183
+ 2. Proxy HTTP routes to `http://127.0.0.1:18800`.
184
+ 3. Preserve WebSocket upgrade headers for `/api/codex-relay/web/` and `/api/codex-relay/daemon`.
185
+ 4. Use redacted logs so bearer tokens, relay tickets, and API keys do not land in access logs.
186
+ 5. Replace WIP paths and domains with your own.
187
+
188
+ Minimum verification:
189
+
190
+ ```bash
191
+ sudo nginx -t
192
+ sudo systemctl reload nginx
193
+ curl -fsS https://relay.example.com/health
194
+ ```
195
+
196
+ ## Pointing Codex Remote Control At A Custom Relay
197
+
198
+ `codex-daemon` defaults to WIP's hosted relay. For a custom relay, set the relay endpoints before pairing and starting the daemon:
199
+
200
+ ```bash
201
+ export CODEX_DAEMON_RELAY_HTTP=https://relay.example.com
202
+ export CODEX_DAEMON_RELAY_WS=wss://relay.example.com/api/codex-relay/daemon
203
+ codex-daemon link
204
+ codex-daemon start
205
+ ```
206
+
207
+ The MCP tool that creates browser links also defaults to WIP's hosted origin. Set this for sessions that should generate links for your relay domain:
208
+
209
+ ```bash
210
+ export CODEX_REMOTE_CONTROL_ORIGIN=https://relay.example.com
211
+ ```
212
+
213
+ A full non-WIP deployment also needs a browser or phone UI that serves `/codex-remote-control/<threadId>` and talks to the same relay routes. In WIP production, that UI is part of the Kaleidoscope web app.
214
+
215
+ ## Verify The Relay
216
+
217
+ Use these checks after any deploy:
218
+
219
+ ```bash
220
+ curl -fsS https://relay.example.com/health
221
+ curl -fsS https://relay.example.com/api/codex-relay/state
222
+ ```
223
+
224
+ For WIP production deploys, `scripts/verify-deploy.sh` verifies a deploy manifest against live remote file hashes:
225
+
226
+ ```bash
227
+ bash src/hosted-mcp/scripts/verify-deploy.sh latest
228
+ ```
229
+
230
+ That script assumes the WIP deploy-manifest layout unless you pass a different manifest and remote.
231
+
232
+ ## What Not To Copy From WIP Production
233
+
234
+ Do not copy:
235
+
236
+ - WIP `.env` files;
237
+ - WIP Postgres credentials;
238
+ - WIP API keys or `ck-` tokens;
239
+ - WIP passkey, device, wallet, or user rows;
240
+ - WIP PM2 process state;
241
+ - WIP nginx certificate paths;
242
+ - WIP domain constants without changing them for your domain;
243
+ - WIP deploy manifests as proof of your deploy.
244
+
245
+ Use the source shape, not WIP's production secrets or account data.
246
+
247
+ ## Production Checklist
248
+
249
+ - Domain and TLS are live.
250
+ - `DATABASE_URL` points at your Postgres database.
251
+ - Prisma migrations have run.
252
+ - `server.mjs` starts without `LDM_HOSTED_MCP_DEV_MODE`.
253
+ - nginx proxies `/health`, `/mcp`, `/oauth/*`, `/api/codex-relay/*`, `/pair`, and WebSocket upgrades.
254
+ - WebSocket origins are restricted with `LDM_HOSTED_MCP_WS_ORIGIN_ALLOWLIST`.
255
+ - URL token fallback is disabled.
256
+ - Access logs redact bearer tokens, relay tickets, and `ck-` values.
257
+ - `codex-daemon link` completes against your domain.
258
+ - `codex-daemon start` reports relay paired.
259
+ - Browser links are generated for your domain, not `wip.computer`.
260
+
261
+ ## Open Work
262
+
263
+ The remaining product work is to turn this infrastructure map into a first-class self-host installer:
264
+
265
+ - parameterize issuer and WebAuthn relying party settings;
266
+ - package the phone/web Remote Control UI for non-WIP domains;
267
+ - add a guided `ldm` self-host profile;
268
+ - add an end-to-end self-host smoke test that pairs a daemon and browser through a non-WIP domain.
@@ -107,3 +107,28 @@ location /api/codex-relay/daemon {
107
107
  proxy_send_timeout 86400;
108
108
  proxy_buffering off;
109
109
  }
110
+
111
+ # /pair (bare) ... fallback manual code entry. Without this, nginx falls
112
+ # through to `location /` and returns the static homepage. Pairs with
113
+ # server.mjs pair-page route that serves app/pair.html.
114
+ location = /pair {
115
+ proxy_pass http://127.0.0.1:18800;
116
+ proxy_http_version 1.1;
117
+ proxy_set_header Host $host;
118
+ proxy_set_header X-Real-IP $remote_addr;
119
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
120
+ proxy_set_header X-Forwarded-Proto $scheme;
121
+ }
122
+
123
+ # /pair/<CODE> ... URL-first pair flow. Real daemon alphabet
124
+ # (CODEX_PAIR_ALPHABET): A-Z minus I and O, digits 2-9. Length 6. L IS
125
+ # included. Per plan 2026-04-30--cc-mini--pair-via-login-qr-flow.md,
126
+ # constraints C1+C3 round 5.
127
+ location ~ "^/pair/[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$" {
128
+ proxy_pass http://127.0.0.1:18800;
129
+ proxy_http_version 1.1;
130
+ proxy_set_header Host $host;
131
+ proxy_set_header X-Real-IP $remote_addr;
132
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
133
+ proxy_set_header X-Forwarded-Proto $scheme;
134
+ }